Source code

Revision control

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "HTMLEditor.h"
#include "HTMLEditUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/EditAction.h"
#include "mozilla/EditorUtils.h"
#include "mozilla/SelectionState.h"
#include "mozilla/TypeInState.h"
#include "mozilla/mozalloc.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/Selection.h"
#include "nsAString.h"
#include "nsAttrName.h"
#include "nsCOMPtr.h"
#include "nsCaseTreatment.h"
#include "nsComponentManagerUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsGkAtoms.h"
#include "nsAtom.h"
#include "nsIContent.h"
#include "nsNameSpaceManager.h"
#include "nsINode.h"
#include "nsIPrincipal.h"
#include "nsISupportsImpl.h"
#include "nsLiteralString.h"
#include "nsRange.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsStyledElement.h"
#include "nsTArray.h"
#include "nsUnicharUtils.h"
#include "nscore.h"
class nsISupports;
namespace mozilla {
using namespace dom;
using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
using LeafNodeType = HTMLEditUtils::LeafNodeType;
using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
nsresult HTMLEditor::SetInlinePropertyAsAction(nsAtom& aProperty,
nsAtom* aAttribute,
const nsAString& aValue,
nsIPrincipal* aPrincipal) {
AutoEditActionDataSetter editActionData(
*this,
HTMLEditUtils::GetEditActionForFormatText(aProperty, aAttribute, true),
aPrincipal);
switch (editActionData.GetEditAction()) {
case EditAction::eSetFontFamilyProperty:
MOZ_ASSERT(!aValue.IsVoid());
// XXX Should we trim unnecessary white-spaces?
editActionData.SetData(aValue);
break;
case EditAction::eSetColorProperty:
case EditAction::eSetBackgroundColorPropertyInline:
editActionData.SetColorData(aValue);
break;
default:
break;
}
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
// XXX Due to bug 1659276 and bug 1659924, we should not scroll selection
// into view after setting the new style.
AutoPlaceholderBatch treatAsOneTransaction(*this,
ScrollSelectionIntoView::No);
nsAtom* property = &aProperty;
nsAtom* attribute = aAttribute;
nsAutoString value(aValue);
if (&aProperty == nsGkAtoms::sup) {
// Superscript and Subscript styles are mutually exclusive.
nsresult rv = RemoveInlinePropertyInternal(nsGkAtoms::sub, nullptr,
RemoveRelatedElements::No);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::RemoveInlinePropertyInternal(nsGkAtoms::sub, "
"RemoveRelatedElements::No) failed");
return EditorBase::ToGenericNSResult(rv);
}
} else if (&aProperty == nsGkAtoms::sub) {
// Superscript and Subscript styles are mutually exclusive.
nsresult rv = RemoveInlinePropertyInternal(nsGkAtoms::sup, nullptr,
RemoveRelatedElements::No);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::RemoveInlinePropertyInternal(nsGkAtoms::sup, "
"RemoveRelatedElements::No) failed");
return EditorBase::ToGenericNSResult(rv);
}
}
// Handling `<tt>` element code was implemented for composer (bug 115922).
// This shouldn't work with `Document.execCommand()`. Currently, aPrincipal
// is set only when the root caller is Document::ExecCommand() so that
// we should handle `<tt>` element only when aPrincipal is nullptr that
// must be only when XUL command is executed on composer.
else if (!aPrincipal) {
if (&aProperty == nsGkAtoms::tt) {
nsresult rv = RemoveInlinePropertyInternal(
nsGkAtoms::font, nsGkAtoms::face, RemoveRelatedElements::No);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::RemoveInlinePropertyInternal(nsGkAtoms::font, "
"nsGkAtoms::face, RemoveRelatedElements::No) failed");
return EditorBase::ToGenericNSResult(rv);
}
} else if (&aProperty == nsGkAtoms::font && aAttribute == nsGkAtoms::face) {
if (!value.LowerCaseEqualsASCII("tt")) {
nsresult rv = RemoveInlinePropertyInternal(nsGkAtoms::tt, nullptr,
RemoveRelatedElements::No);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::RemoveInlinePropertyInternal(nsGkAtoms::tt, "
"RemoveRelatedElements::No) failed");
return EditorBase::ToGenericNSResult(rv);
}
} else {
nsresult rv = RemoveInlinePropertyInternal(
nsGkAtoms::font, nsGkAtoms::face, RemoveRelatedElements::No);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::RemoveInlinePropertyInternal(nsGkAtoms::font, "
"nsGkAtoms::face, RemoveRelatedElements::No) failed");
return EditorBase::ToGenericNSResult(rv);
}
// Override property, attribute and value if the new font face value is
// "tt".
property = nsGkAtoms::tt;
attribute = nullptr;
value.Truncate();
}
}
}
rv = SetInlinePropertyInternal(MOZ_KnownLive(*property),
MOZ_KnownLive(attribute), value);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::SetInlinePropertyInternal() failed");
return EditorBase::ToGenericNSResult(rv);
}
NS_IMETHODIMP HTMLEditor::SetInlineProperty(const nsAString& aProperty,
const nsAString& aAttribute,
const nsAString& aValue) {
RefPtr<nsAtom> property = NS_Atomize(aProperty);
if (NS_WARN_IF(!property)) {
return NS_ERROR_INVALID_ARG;
}
nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute);
AutoEditActionDataSetter editActionData(
*this,
HTMLEditUtils::GetEditActionForFormatText(*property, attribute, true));
switch (editActionData.GetEditAction()) {
case EditAction::eSetFontFamilyProperty:
MOZ_ASSERT(!aValue.IsVoid());
// XXX Should we trim unnecessary white-spaces?
editActionData.SetData(aValue);
break;
case EditAction::eSetColorProperty:
case EditAction::eSetBackgroundColorPropertyInline:
editActionData.SetColorData(aValue);
break;
default:
break;
}
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
rv = SetInlinePropertyInternal(*property, MOZ_KnownLive(attribute), aValue);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::SetInlinePropertyInternal() failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult HTMLEditor::SetInlinePropertyInternal(
nsAtom& aProperty, nsAtom* aAttribute, const nsAString& aAttributeValue) {
MOZ_ASSERT(IsEditActionDataAvailable());
if (NS_WARN_IF(!mInitSucceeded)) {
return NS_ERROR_NOT_INITIALIZED;
}
DebugOnly<nsresult> rvIgnored = CommitComposition();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"EditorBase::CommitComposition() failed, but ignored");
if (SelectionRef().IsCollapsed()) {
// Manipulating text attributes on a collapsed selection only sets state
// for the next text insertion
mTypeInState->SetProp(&aProperty, aAttribute, aAttributeValue);
return NS_OK;
}
// XXX Shouldn't we return before calling `CommitComposition()`?
if (IsInPlaintextMode()) {
return NS_OK;
}
EditActionResult result = CanHandleHTMLEditSubAction();
if (result.Failed() || result.Canceled()) {
NS_WARNING_ASSERTION(result.Succeeded(),
"HTMLEditor::CanHandleHTMLEditSubAction() failed");
return result.Rv();
}
AutoPlaceholderBatch treatAsOneTransaction(*this,
ScrollSelectionIntoView::Yes);
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eInsertElement, nsIEditor::eNext, ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return ignoredError.StealNSResult();
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
{
AutoSelectionRestorer restoreSelectionLater(*this);
AutoTransactionsConserveSelection dontChangeMySelection(*this);
// Loop through the ranges in the selection
// XXX This is different from `SetCSSBackgroundColorWithTransaction()`.
// It refers `Selection::GetRangeAt()` in each time. The result may
// be different if mutation event listener changes the `Selection`.
AutoSelectionRangeArray arrayOfRanges(SelectionRef());
for (auto& range : arrayOfRanges.mRanges) {
// Adjust range to include any ancestors whose children are entirely
// selected
nsresult rv = PromoteInlineRange(*range);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::PromoteInlineRange() failed");
return rv;
}
// XXX Shouldn't we skip the range if it's been collapsed by mutation
// event listener?
EditorDOMPoint startOfRange(range->StartRef());
EditorDOMPoint endOfRange(range->EndRef());
if (NS_WARN_IF(!startOfRange.IsSet()) ||
NS_WARN_IF(!endOfRange.IsSet())) {
continue;
}
// If range is in a text node, apply new style simply.
if (startOfRange.GetContainer() == endOfRange.GetContainer() &&
startOfRange.IsInTextNode()) {
nsresult rv = SetInlinePropertyOnTextNode(
MOZ_KnownLive(*startOfRange.GetContainerAsText()),
startOfRange.Offset(), endOfRange.Offset(), aProperty, aAttribute,
aAttributeValue);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed");
return rv;
}
continue;
}
// Collect editable nodes which are entirely contained in the range.
AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
ContentSubtreeIterator subtreeIter;
// If there is no node which is entirely in the range,
// `ContentSubtreeIterator::Init()` fails, but this is possible case,
// don't warn it.
if (NS_SUCCEEDED(subtreeIter.Init(range))) {
for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
nsINode* node = subtreeIter.GetCurrentNode();
if (NS_WARN_IF(!node)) {
return NS_ERROR_FAILURE;
}
if (node->IsContent() && EditorUtils::IsEditableContent(
*node->AsContent(), EditorType::HTML)) {
arrayOfContents.AppendElement(*node->AsContent());
}
}
}
// If start node is a text node, apply new style to a part of it.
if (startOfRange.IsInTextNode() &&
EditorUtils::IsEditableContent(*startOfRange.ContainerAsText(),
EditorType::HTML)) {
nsresult rv = SetInlinePropertyOnTextNode(
MOZ_KnownLive(*startOfRange.GetContainerAsText()),
startOfRange.Offset(), startOfRange.GetContainer()->Length(),
aProperty, aAttribute, aAttributeValue);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed");
return rv;
}
}
// Then, apply new style to all nodes in the range entirely.
for (auto& content : arrayOfContents) {
// MOZ_KnownLive because 'arrayOfContents' is guaranteed to
// keep it alive.
nsresult rv = SetInlinePropertyOnNode(
MOZ_KnownLive(*content), aProperty, aAttribute, aAttributeValue);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::SetInlinePropertyOnNode() failed");
return rv;
}
}
// Finally, if end node is a text node, apply new style to a part of it.
if (endOfRange.IsInTextNode() &&
EditorUtils::IsEditableContent(*endOfRange.GetContainerAsText(),
EditorType::HTML)) {
nsresult rv = SetInlinePropertyOnTextNode(
MOZ_KnownLive(*endOfRange.GetContainerAsText()), 0,
endOfRange.Offset(), aProperty, aAttribute, aAttributeValue);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::SetInlinePropertyOnNode() failed");
return rv;
}
}
}
}
// Restoring `Selection` may have destroyed us.
return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
}
Result<bool, nsresult> HTMLEditor::ElementIsGoodContainerForTheStyle(
Element& aElement, nsAtom* aProperty, nsAtom* aAttribute,
const nsAString* aValue) {
// aContent can be null, in which case we'll return false in a few lines
MOZ_ASSERT(aProperty);
MOZ_ASSERT_IF(aAttribute, aValue);
// First check for <b>, <i>, etc.
if (aElement.IsHTMLElement(aProperty) && !aElement.GetAttrCount() &&
!aAttribute) {
return true;
}
// Special cases for various equivalencies: <strong>, <em>, <s>
if (!aElement.GetAttrCount() &&
((aProperty == nsGkAtoms::b &&
aElement.IsHTMLElement(nsGkAtoms::strong)) ||
(aProperty == nsGkAtoms::i && aElement.IsHTMLElement(nsGkAtoms::em)) ||
(aProperty == nsGkAtoms::strike &&
aElement.IsHTMLElement(nsGkAtoms::s)))) {
return true;
}
// Now look for things like <font>
if (aAttribute) {
nsString attrValue;
if (aElement.IsHTMLElement(aProperty) &&
IsOnlyAttribute(&aElement, aAttribute) &&
aElement.GetAttr(kNameSpaceID_None, aAttribute, attrValue) &&
attrValue.Equals(*aValue, nsCaseInsensitiveStringComparator)) {
// This is not quite correct, because it excludes cases like
// <font face=000> being the same as <font face=#000000>.
// Property-specific handling is needed (bug 760211).
return true;
}
}
// No luck so far. Now we check for a <span> with a single style=""
// attribute that sets only the style we're looking for, if this type of
// style supports it
if (!CSSEditUtils::IsCSSEditableProperty(&aElement, aProperty, aAttribute) ||
!aElement.IsHTMLElement(nsGkAtoms::span) ||
aElement.GetAttrCount() != 1 ||
!aElement.HasAttr(kNameSpaceID_None, nsGkAtoms::style)) {
return false;
}
// Some CSS styles are not so simple. For instance, underline is
// "text-decoration: underline", which decomposes into four different text-*
// properties. So for now, we just create a span, add the desired style, and
// see if it matches.
RefPtr<Element> newSpanElement = CreateHTMLContent(nsGkAtoms::span);
if (!newSpanElement) {
NS_WARNING("EditorBase::CreateHTMLContent(nsGkAtoms::span) failed");
return false;
}
nsStyledElement* styledNewSpanElement =
nsStyledElement::FromNode(newSpanElement);
if (!styledNewSpanElement) {
return false;
}
// MOZ_KnownLive(*styledNewSpanElement): It's newSpanElement whose type is
// RefPtr.
Result<int32_t, nsresult> result =
mCSSEditUtils->SetCSSEquivalentToHTMLStyleWithoutTransaction(
MOZ_KnownLive(*styledNewSpanElement), aProperty, aAttribute, aValue);
if (result.isErr()) {
// The call shouldn't return destroyed error because it must be
// impossible to run script with modifying the new orphan node.
MOZ_ASSERT_UNREACHABLE("How did you destroy this editor?");
if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
return false;
}
nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement);
if (!styledElement) {
return false;
}
return CSSEditUtils::DoStyledElementsHaveSameStyle(*styledNewSpanElement,
*styledElement);
}
nsresult HTMLEditor::SetInlinePropertyOnTextNode(
Text& aText, uint32_t aStartOffset, uint32_t aEndOffset, nsAtom& aProperty,
nsAtom* aAttribute, const nsAString& aValue) {
if (!aText.GetParentNode() ||
!HTMLEditUtils::CanNodeContain(*aText.GetParentNode(), aProperty)) {
return NS_OK;
}
// Don't need to do anything if no characters actually selected
if (aStartOffset == aEndOffset) {
return NS_OK;
}
// Don't need to do anything if property already set on node
if (CSSEditUtils::IsCSSEditableProperty(&aText, &aProperty, aAttribute)) {
// The HTML styles defined by aProperty/aAttribute have a CSS equivalence
// for node; let's check if it carries those CSS styles
nsAutoString value(aValue);
if (CSSEditUtils::IsComputedCSSEquivalentToHTMLInlineStyleSet(
aText, &aProperty, aAttribute, value)) {
return NS_OK;
}
} else if (HTMLEditUtils::IsInlineStyleSetByElement(aText, aProperty,
aAttribute, &aValue)) {
return NS_OK;
}
// Make the range an independent node.
nsCOMPtr<nsIContent> textNodeForTheRange = &aText;
// Split at the end of the range.
EditorDOMPoint atEnd(textNodeForTheRange, aEndOffset);
if (!atEnd.IsEndOfContainer()) {
// We need to split off back of text node
ErrorResult error;
textNodeForTheRange = SplitNodeWithTransaction(atEnd, error);
if (NS_WARN_IF(Destroyed())) {
error = NS_ERROR_EDITOR_DESTROYED;
}
if (error.Failed()) {
NS_WARNING_ASSERTION(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED),
"HTMLEditor::SplitNodeWithTransaction() failed");
return error.StealNSResult();
}
}
// Split at the start of the range.
EditorDOMPoint atStart(textNodeForTheRange, aStartOffset);
if (!atStart.IsStartOfContainer()) {
// We need to split off front of text node
ErrorResult error;
nsCOMPtr<nsIContent> newLeftNode = SplitNodeWithTransaction(atStart, error);
if (NS_WARN_IF(Destroyed())) {
error = NS_ERROR_EDITOR_DESTROYED;
}
if (error.Failed()) {
NS_WARNING_ASSERTION(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED),
"HTMLEditor::SplitNodeWithTransaction() failed");
return error.StealNSResult();
}
Unused << newLeftNode;
}
if (aAttribute) {
// Look for siblings that are correct type of node
nsIContent* sibling = HTMLEditUtils::GetPreviousSibling(
*textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode});
if (sibling && sibling->IsElement()) {
OwningNonNull<Element> element(*sibling->AsElement());
Result<bool, nsresult> result = ElementIsGoodContainerForTheStyle(
element, &aProperty, aAttribute, &aValue);
if (result.isErr()) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
return result.unwrapErr();
}
if (result.inspect()) {
// Previous sib is already right kind of inline node; slide this over
nsresult rv =
MoveNodeToEndWithTransaction(*textNodeForTheRange, element);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::MoveNodeToEndWithTransaction() failed");
return rv;
}
}
sibling = HTMLEditUtils::GetNextSibling(
*textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode});
if (sibling && sibling->IsElement()) {
OwningNonNull<Element> element(*sibling->AsElement());
Result<bool, nsresult> result = ElementIsGoodContainerForTheStyle(
element, &aProperty, aAttribute, &aValue);
if (result.isErr()) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
return result.unwrapErr();
}
if (result.inspect()) {
// Following sib is already right kind of inline node; slide this over
nsresult rv = MoveNodeWithTransaction(*textNodeForTheRange,
EditorDOMPoint(sibling, 0));
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::MoveNodeWithTransaction() failed");
return rv;
}
}
}
// Reparent the node inside inline node with appropriate {attribute,value}
nsresult rv = SetInlinePropertyOnNode(*textNodeForTheRange, aProperty,
aAttribute, aValue);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::SetInlinePropertyOnNode() failed");
return rv;
}
nsresult HTMLEditor::SetInlinePropertyOnNodeImpl(nsIContent& aContent,
nsAtom& aProperty,
nsAtom* aAttribute,
const nsAString& aValue) {
// If this is an element that can't be contained in a span, we have to
// recurse to its children.
if (!HTMLEditUtils::CanNodeContain(*nsGkAtoms::span, aContent)) {
if (aContent.HasChildren()) {
nsTArray<OwningNonNull<nsIContent>> arrayOfNodes;
// Populate the list.
for (nsCOMPtr<nsIContent> child = aContent.GetFirstChild(); child;
child = child->GetNextSibling()) {
if (EditorUtils::IsEditableContent(*child, EditorType::HTML) &&
(!child->IsText() ||
HTMLEditUtils::IsVisibleTextNode(*child->AsText()))) {
arrayOfNodes.AppendElement(*child);
}
}
// Then loop through the list, set the property on each node.
for (auto& node : arrayOfNodes) {
// MOZ_KnownLive because 'arrayOfNodes' is guaranteed to
// keep it alive.
nsresult rv = SetInlinePropertyOnNode(MOZ_KnownLive(node), aProperty,
aAttribute, aValue);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::SetInlinePropertyOnNode() failed");
return rv;
}
}
}
return NS_OK;
}
// First check if there's an adjacent sibling we can put our node into.
nsCOMPtr<nsIContent> previousSibling = HTMLEditUtils::GetPreviousSibling(
aContent, {WalkTreeOption::IgnoreNonEditableNode});
nsCOMPtr<nsIContent> nextSibling = HTMLEditUtils::GetNextSibling(
aContent, {WalkTreeOption::IgnoreNonEditableNode});
if (previousSibling && previousSibling->IsElement()) {
OwningNonNull<Element> previousElement(*previousSibling->AsElement());
Result<bool, nsresult> canMoveIntoPreviousSibling =
ElementIsGoodContainerForTheStyle(previousElement, &aProperty,
aAttribute, &aValue);
if (canMoveIntoPreviousSibling.isErr()) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
return canMoveIntoPreviousSibling.unwrapErr();
}
if (canMoveIntoPreviousSibling.inspect()) {
nsresult rv = MoveNodeToEndWithTransaction(aContent, *previousSibling);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
return rv;
}
if (!nextSibling || !nextSibling->IsElement()) {
return NS_OK;
}
OwningNonNull<Element> nextElement(*nextSibling->AsElement());
Result<bool, nsresult> canMoveIntoNextSibling =
ElementIsGoodContainerForTheStyle(nextElement, &aProperty, aAttribute,
&aValue);
if (canMoveIntoNextSibling.isErr()) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
return canMoveIntoNextSibling.unwrapErr();
}
if (!canMoveIntoNextSibling.inspect()) {
return NS_OK;
}
rv = JoinNodesWithTransaction(*previousSibling, *nextSibling);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::JoinNodesWithTransaction() failed");
return rv;
}
}
if (nextSibling && nextSibling->IsElement()) {
OwningNonNull<Element> nextElement(*nextSibling->AsElement());
Result<bool, nsresult> canMoveIntoNextSibling =
ElementIsGoodContainerForTheStyle(nextElement, &aProperty, aAttribute,
&aValue);
if (canMoveIntoNextSibling.isErr()) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
return canMoveIntoNextSibling.unwrapErr();
}
if (canMoveIntoNextSibling.inspect()) {
nsresult rv =
MoveNodeWithTransaction(aContent, EditorDOMPoint(nextElement, 0));
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::MoveNodeWithTransaction() failed");
return rv;
}
}
// Don't need to do anything if property already set on node
if (CSSEditUtils::IsCSSEditableProperty(&aContent, &aProperty, aAttribute)) {
nsAutoString value(aValue);
if (CSSEditUtils::IsComputedCSSEquivalentToHTMLInlineStyleSet(
aContent, &aProperty, aAttribute, value)) {
return NS_OK;
}
} else if (HTMLEditUtils::IsInlineStyleSetByElement(aContent, aProperty,
aAttribute, &aValue)) {
return NS_OK;
}
bool useCSS = (IsCSSEnabled() && CSSEditUtils::IsCSSEditableProperty(
&aContent, &aProperty, aAttribute)) ||
// bgcolor is always done using CSS
aAttribute == nsGkAtoms::bgcolor ||
// called for removing parent style, we should use CSS with
// `<span>` element.
aValue.EqualsLiteral("-moz-editor-invert-value");
if (useCSS) {
RefPtr<Element> spanElement;
// We only add style="" to <span>s with no attributes (bug 746515). If we
// don't have one, we need to make one.
if (aContent.IsHTMLElement(nsGkAtoms::span) &&
!aContent.AsElement()->GetAttrCount()) {
spanElement = aContent.AsElement();
} else {
spanElement = InsertContainerWithTransaction(aContent, *nsGkAtoms::span);
if (!spanElement) {
NS_WARNING(
"HTMLEditor::InsertContainerWithTransaction(nsGkAtoms::span) "
"failed");
return NS_ERROR_FAILURE;
}
}
// Add the CSS styles corresponding to the HTML style request
if (nsStyledElement* spanStyledElement =
nsStyledElement::FromNode(spanElement)) {
// MOZ_KnownLive(*spanStyledElement): It's spanElement whose type is
// RefPtr.
Result<int32_t, nsresult> result =
mCSSEditUtils->SetCSSEquivalentToHTMLStyleWithTransaction(
MOZ_KnownLive(*spanStyledElement), &aProperty, aAttribute,
&aValue);
if (result.isErr()) {
if (result.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
NS_WARNING(
"CSSEditUtils::SetCSSEquivalentToHTMLStyleWithTransaction() "
"destroyed the editor");
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING(
"CSSEditUtils::SetCSSEquivalentToHTMLStyleWithTransaction() "
"failed, "
"but ignored");
}
}
return NS_OK;
}
// is it already the right kind of node, but with wrong attribute?
if (aContent.IsHTMLElement(&aProperty)) {
if (NS_WARN_IF(!aAttribute)) {
return NS_ERROR_INVALID_ARG;
}
// Just set the attribute on it.
nsresult rv = SetAttributeWithTransaction(
MOZ_KnownLive(*aContent.AsElement()), *aAttribute, aValue);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::SetAttributeWithTransaction() failed");
return rv;
}
// ok, chuck it in its very own container
RefPtr<Element> newContainerElement = InsertContainerWithTransaction(
aContent, aProperty, aAttribute ? *aAttribute : *nsGkAtoms::_empty,
aValue);
NS_WARNING_ASSERTION(newContainerElement,
"HTMLEditor::InsertContainerWithTransaction() failed");
return newContainerElement ? NS_OK : NS_ERROR_FAILURE;
}
nsresult HTMLEditor::SetInlinePropertyOnNode(nsIContent& aNode,
nsAtom& aProperty,
nsAtom* aAttribute,
const nsAString& aValue) {
nsCOMPtr<nsIContent> previousSibling = aNode.GetPreviousSibling(),
nextSibling = aNode.GetNextSibling();
if (NS_WARN_IF(!aNode.GetParentNode())) {
return NS_ERROR_INVALID_ARG;
}
OwningNonNull<nsINode> parent = *aNode.GetParentNode();
if (aNode.IsElement()) {
nsresult rv =
RemoveStyleInside(MOZ_KnownLive(*aNode.AsElement()), &aProperty,
aAttribute, SpecifiedStyle::Preserve);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
return rv;
}
}
if (aNode.GetParentNode()) {
// The node is still where it was
nsresult rv =
SetInlinePropertyOnNodeImpl(aNode, aProperty, aAttribute, aValue);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::SetInlinePropertyOnNodeImpl() failed");
return rv;
}
// It's vanished. Use the old siblings for reference to construct a
// list. But first, verify that the previous/next siblings are still
// where we expect them; otherwise we have to give up.
if (NS_WARN_IF(previousSibling &&
previousSibling->GetParentNode() != parent) ||
NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parent)) {
return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
}
AutoTArray<OwningNonNull<nsIContent>, 24> nodesToSet;
for (nsIContent* content = previousSibling ? previousSibling->GetNextSibling()
: parent->GetFirstChild();
content && content != nextSibling; content = content->GetNextSibling()) {
if (EditorUtils::IsEditableContent(*content, EditorType::HTML)) {
nodesToSet.AppendElement(*content);
}
}
for (OwningNonNull<nsIContent>& content : nodesToSet) {
// MOZ_KnownLive because 'nodesToSet' is guaranteed to
// keep it alive.
nsresult rv = SetInlinePropertyOnNodeImpl(MOZ_KnownLive(content), aProperty,
aAttribute, aValue);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::SetInlinePropertyOnNodeImpl() failed");
return rv;
}
}
return NS_OK;
}
SplitRangeOffResult HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges(
const EditorDOMPoint& aStartOfRange, const EditorDOMPoint& aEndOfRange,
nsAtom* aProperty, nsAtom* aAttribute) {
MOZ_ASSERT(IsEditActionDataAvailable());
if (NS_WARN_IF(!aStartOfRange.IsSet()) || NS_WARN_IF(!aEndOfRange.IsSet())) {
return SplitRangeOffResult(NS_ERROR_INVALID_ARG);
}
EditorDOMPoint startOfRange(aStartOfRange);
EditorDOMPoint endOfRange(aEndOfRange);
// split any matching style nodes above the start of range
SplitNodeResult resultAtStart(NS_ERROR_NOT_INITIALIZED);
{
AutoTrackDOMPoint tracker(RangeUpdaterRef(), &endOfRange);
resultAtStart = SplitAncestorStyledInlineElementsAt(startOfRange, aProperty,
aAttribute);
if (resultAtStart.Failed()) {
NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
return SplitRangeOffResult(resultAtStart.Rv());
}
if (resultAtStart.Handled()) {
startOfRange = resultAtStart.SplitPoint();
if (!startOfRange.IsSet()) {
NS_WARNING(
"HTMLEditor::SplitAncestorStyledInlineElementsAt() didn't return "
"split point");
return SplitRangeOffResult(NS_ERROR_FAILURE);
}
}
}
// second verse, same as the first...
SplitNodeResult resultAtEnd(NS_ERROR_NOT_INITIALIZED);
{
AutoTrackDOMPoint tracker(RangeUpdaterRef(), &startOfRange);
resultAtEnd =
SplitAncestorStyledInlineElementsAt(endOfRange, aProperty, aAttribute);
if (resultAtEnd.Failed()) {
NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
return SplitRangeOffResult(resultAtEnd.Rv());
}
if (resultAtEnd.Handled()) {
endOfRange = resultAtEnd.SplitPoint();
if (!endOfRange.IsSet()) {
NS_WARNING(
"HTMLEditor::SplitAncestorStyledInlineElementsAt() didn't return "
"split point");
return SplitRangeOffResult(NS_ERROR_FAILURE);
}
}
}
return SplitRangeOffResult(startOfRange, resultAtStart, endOfRange,
resultAtEnd);
}
SplitNodeResult HTMLEditor::SplitAncestorStyledInlineElementsAt(
const EditorDOMPoint& aPointToSplit, nsAtom* aProperty,
nsAtom* aAttribute) {
if (NS_WARN_IF(!aPointToSplit.IsSet()) ||
NS_WARN_IF(!aPointToSplit.GetContainerAsContent())) {
return SplitNodeResult(NS_ERROR_INVALID_ARG);
}
// We assume that this method is called only when we're removing style(s).
// Even if we're in HTML mode and there is no presentation element in the
// block, we may need to overwrite the block's style with `<span>` element
// and CSS. For example, `<h1>` element has `font-weight: bold;` as its
// default style. If `Document.execCommand("bold")` is called for its
// text, we should make it unbold. Therefore, we shouldn't check
// IsCSSEnabled() in most cases. However, there is an exception.
// FontFaceStateCommand::SetState() calls RemoveInlinePropertyAsAction()
// with nsGkAtoms::tt before calling SetInlinePropertyAsAction() if we
// are handling a XUL command. Only in that case, we need to check
// IsCSSEnabled().
bool useCSS = aProperty != nsGkAtoms::tt || IsCSSEnabled();
AutoTArray<OwningNonNull<nsIContent>, 24> arrayOfParents;
for (nsIContent* content :
aPointToSplit.GetContainer()->InclusiveAncestorsOfType<nsIContent>()) {
if (HTMLEditUtils::IsBlockElement(*content) || !content->GetParent() ||
!EditorUtils::IsEditableContent(*content->GetParent(),
EditorType::HTML)) {
break;
}
arrayOfParents.AppendElement(*content);
}
// Split any matching style nodes above the point.
SplitNodeResult result(aPointToSplit);
MOZ_ASSERT(!result.Handled());
for (OwningNonNull<nsIContent>& content : arrayOfParents) {
bool isSetByCSS = false;
if (useCSS &&
CSSEditUtils::IsCSSEditableProperty(content, aProperty, aAttribute)) {
// The HTML style defined by aProperty/aAttribute has a CSS equivalence
// in this implementation for the node; let's check if it carries those
// CSS styles
nsAutoString firstValue;
isSetByCSS = CSSEditUtils::IsSpecifiedCSSEquivalentToHTMLInlineStyleSet(
*content, aProperty, aAttribute, firstValue);
}
if (!isSetByCSS) {
if (!content->IsElement()) {
continue;
}
// If aProperty is set, we need to split only elements which applies the
// given style.
if (aProperty) {
// If the content is an inline element represents aProperty or
// the content is a link element and aProperty is `href`, we should
// split the content.
if (!content->IsHTMLElement(aProperty) &&
!(aProperty == nsGkAtoms::href && HTMLEditUtils::IsLink(content))) {
continue;
}
}
// If aProperty is nullptr, we need to split any style.
else if (!EditorUtils::IsEditableContent(content, EditorType::HTML) ||
!HTMLEditUtils::IsRemovableInlineStyleElement(
*content->AsElement())) {
continue;
}
}
// Found a style node we need to split.
// XXX If first content is a text node and CSS is enabled, we call this
// with text node but in such case, this does nothing, but returns
// as handled with setting only previous or next node. If its parent
// is a block, we do nothing but return as handled.
SplitNodeResult splitNodeResult = SplitNodeDeepWithTransaction(
MOZ_KnownLive(content), result.SplitPoint(),
SplitAtEdges::eAllowToCreateEmptyContainer);
if (splitNodeResult.Failed()) {
NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
return splitNodeResult;
}
// If it's not handled, it means that `content` is not a splitable node
// like a void element even if it has some children, and the split point
// is middle of it.
if (!splitNodeResult.Handled()) {
continue;
}
// Mark the final result as handled forcibly.
result = SplitNodeResult(splitNodeResult.GetPreviousNode(),
splitNodeResult.GetNextNode());
MOZ_ASSERT(result.Handled());
}
return result;
}
EditResult HTMLEditor::ClearStyleAt(const EditorDOMPoint& aPoint,
nsAtom* aProperty, nsAtom* aAttribute,
SpecifiedStyle aSpecifiedStyle) {
MOZ_ASSERT(IsEditActionDataAvailable());
if (NS_WARN_IF(!aPoint.IsSet())) {
return EditResult(NS_ERROR_INVALID_ARG);
}
// First, split inline elements at the point.
// E.g., if aProperty is nsGkAtoms::b and `<p><b><i>a[]bc</i></b></p>`,
// we want to make it as `<p><b><i>a</i></b><b><i>bc</i></b></p>`.
SplitNodeResult splitResult =
SplitAncestorStyledInlineElementsAt(aPoint, aProperty, aAttribute);
if (splitResult.Failed()) {
NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
return EditResult(splitResult.Rv());
}
// If there is no styled inline elements of aProperty/aAttribute, we just
// return the given point.
// E.g., `<p><i>a[]bc</i></p>` for nsGkAtoms::b.
if (!splitResult.Handled()) {
return EditResult(aPoint);
}
// If it did split nodes, but topmost ancestor inline element is split
// at start of it, we don't need the empty inline element. Let's remove
// it now.
if (splitResult.GetPreviousNode() &&
HTMLEditUtils::IsEmptyNode(
*splitResult.GetPreviousNode(),
{EmptyCheckOption::TreatSingleBRElementAsVisible,
EmptyCheckOption::TreatListItemAsVisible,
EmptyCheckOption::TreatTableCellAsVisible})) {
// Delete previous node if it's empty.
nsresult rv = DeleteNodeWithTransaction(
MOZ_KnownLive(*splitResult.GetPreviousNode()));
if (NS_WARN_IF(Destroyed())) {
return EditResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
return EditResult(rv);
}
}
// If we reached block from end of a text node, we can do nothing here.
// E.g., `<p style="font-weight: bold;">a[]bc</p>` for nsGkAtoms::b and
// we're in CSS mode.
// XXX Chrome resets block style and creates `<span>` elements for each
// line in this case.
if (!splitResult.GetNextNode()) {
return EditResult(aPoint);
}
// Otherwise, the next node is topmost ancestor inline element which has
// the style. We want to put caret between the split nodes, but we need
// to keep other styles. Therefore, next, we need to split at start of
// the next node. The first example should become
// `<p><b><i>a</i></b><b><i></i></b><b><i>bc</i></b></p>`.
// ^^^^^^^^^^^^^^
nsIContent* firstLeafChildOfNextNode = HTMLEditUtils::GetFirstLeafContent(
*splitResult.GetNextNode(), {LeafNodeType::OnlyLeafNode});
EditorDOMPoint atStartOfNextNode(firstLeafChildOfNextNode
? firstLeafChildOfNextNode
: splitResult.GetNextNode(),
0);
RefPtr<HTMLBRElement> brElement;
// But don't try to split non-containers like `<br>`, `<hr>` and `<img>`
// element.
if (!atStartOfNextNode.IsInContentNode() ||
!HTMLEditUtils::IsContainerNode(
*atStartOfNextNode.ContainerAsContent())) {
// If it's a `<br>` element, let's move it into new node later.
brElement = HTMLBRElement::FromNode(atStartOfNextNode.GetContainer());
if (!atStartOfNextNode.GetContainerParentAsContent()) {
NS_WARNING("atStartOfNextNode was in an orphan node");
return EditResult(NS_ERROR_FAILURE);
}
atStartOfNextNode.Set(atStartOfNextNode.GetContainerParent(), 0);
}
SplitNodeResult splitResultAtStartOfNextNode =
SplitAncestorStyledInlineElementsAt(atStartOfNextNode, aProperty,
aAttribute);
if (splitResultAtStartOfNextNode.Failed()) {
NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
return EditResult(splitResultAtStartOfNextNode.Rv());
}
// Let's remove the next node if it becomes empty by splitting it.
// XXX Is this possible case without mutation event listener?
if (splitResultAtStartOfNextNode.Handled() &&
splitResultAtStartOfNextNode.GetNextNode() &&
HTMLEditUtils::IsEmptyNode(
*splitResultAtStartOfNextNode.GetNextNode(),
{EmptyCheckOption::TreatSingleBRElementAsVisible,
EmptyCheckOption::TreatListItemAsVisible,
EmptyCheckOption::TreatTableCellAsVisible})) {
// Delete next node if it's empty.
nsresult rv = DeleteNodeWithTransaction(
MOZ_KnownLive(*splitResultAtStartOfNextNode.GetNextNode()));
if (NS_WARN_IF(Destroyed())) {
return EditResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
return EditResult(rv);
}
}
// If there is no content, we should return here.
// XXX Is this possible case without mutation event listener?
if (NS_WARN_IF(!splitResultAtStartOfNextNode.Handled()) ||
!splitResultAtStartOfNextNode.GetPreviousNode()) {
// XXX This is really odd, but we retrun this value...
return EditResult(
EditorDOMPoint(splitResult.SplitPoint().GetContainer(),
splitResultAtStartOfNextNode.SplitPoint().Offset()));
}
// Now, we want to put `<br>` element into the empty split node if
// it was in next node of the first split.
// E.g., `<p><b><i>a</i></b><b><i><br></i></b><b><i>bc</i></b></p>`
nsIContent* firstLeafChildOfPreviousNode = HTMLEditUtils::GetFirstLeafContent(
*splitResultAtStartOfNextNode.GetPreviousNode(),
{LeafNodeType::OnlyLeafNode});
EditorDOMPoint pointToPutCaret(
firstLeafChildOfPreviousNode
? firstLeafChildOfPreviousNode
: splitResultAtStartOfNextNode.GetPreviousNode(),
0);
// If the right node starts with a `<br>`, suck it out of right node and into
// the left node left node. This is so we you don't revert back to the
// previous style if you happen to click at the end of a line.
if (brElement) {
nsresult rv = MoveNodeWithTransaction(*brElement, pointToPutCaret);
if (NS_WARN_IF(Destroyed())) {
return EditResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
return EditResult(rv);
}
// Update the child.
pointToPutCaret.Set(pointToPutCaret.GetContainer(), 0);
}
// Finally, remove the specified style in the previous node at the
// second split and tells good insertion point to the caller. I.e., we
// want to make the first example as:
// `<p><b><i>a</i></b><i>[]</i><b><i>bc</i></b></p>`
// ^^^^^^^^^
if (splitResultAtStartOfNextNode.GetPreviousNode()->IsElement()) {
// Track the point at the new hierarchy. This is so we can know where
// to put the selection after we call RemoveStyleInside().
// RemoveStyleInside() could remove any and all of those nodes, so I
// have to use the range tracking system to find the right spot to put
// selection.
AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToPutCaret);
nsresult rv = RemoveStyleInside(
MOZ_KnownLive(
*splitResultAtStartOfNextNode.GetPreviousNode()->AsElement()),
aProperty, aAttribute, aSpecifiedStyle);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
return EditResult(rv);
}
}
return EditResult(pointToPutCaret);
}
nsresult HTMLEditor::RemoveStyleInside(Element& aElement, nsAtom* aProperty,
nsAtom* aAttribute,
SpecifiedStyle aSpecifiedStyle) {
// First, handle all descendants.
RefPtr<nsIContent> child = aElement.GetFirstChild();
while (child) {
// cache next sibling since we might remove child
// XXX Well, the next sibling is moved from `aElement`, shouldn't we skip
// it here?
nsCOMPtr<nsIContent> nextSibling = child->GetNextSibling();
if (child->IsElement()) {
nsresult rv = RemoveStyleInside(MOZ_KnownLive(*child->AsElement()),
aProperty, aAttribute, aSpecifiedStyle);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
return rv;
}
}
child = ToRefPtr(std::move(nextSibling));
}
// Next, remove the element or its attribute.
bool removeHTMLStyle = false;
if (aProperty) {
removeHTMLStyle =
// If the element is a presentation element of aProperty
aElement.NodeInfo()->NameAtom() == aProperty ||
// or an `<a>` element with `href` attribute
(aProperty == nsGkAtoms::href && HTMLEditUtils::IsLink(&aElement)) ||
// or an `<a>` element with `name` attribute
(aProperty == nsGkAtoms::name &&
HTMLEditUtils::IsNamedAnchor(&aElement));
}
// XXX Why do we check if aElement is editable only when aProperty is
// nullptr?
else if (EditorUtils::IsEditableContent(aElement, EditorType::HTML)) {
// or removing all styles and the element is a presentation element.
removeHTMLStyle = HTMLEditUtils::IsRemovableInlineStyleElement(aElement);
}
if (removeHTMLStyle) {
// If aAttribute is nullptr, we want to remove any matching inline styles
// entirely.
if (!aAttribute) {
// If some style rules are specified to aElement, we need to keep them
// as far as possible.
// XXX Why don't we clone `id` attribute?
if (aProperty && aSpecifiedStyle != SpecifiedStyle::Discard &&
(aElement.HasAttr(kNameSpaceID_None, nsGkAtoms::style) ||
aElement.HasAttr(kNameSpaceID_None, nsGkAtoms::_class))) {
// Move `style` attribute and `class` element to span element before
// removing aElement from the tree.
RefPtr<Element> spanElement =
InsertContainerWithTransaction(aElement, *nsGkAtoms::span);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (!spanElement) {
NS_WARNING(
"HTMLEditor::InsertContainerWithTransaction(nsGkAtoms::span) "
"failed");
return NS_ERROR_FAILURE;
}
nsresult rv = CloneAttributeWithTransaction(*nsGkAtoms::style,
*spanElement, aElement);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_FAILED(rv)) {
NS_WARNING(
"EditorBase::CloneAttributeWithTransaction(nsGkAtoms::style) "
"failed");
return rv;
}
rv = CloneAttributeWithTransaction(*nsGkAtoms::_class, *spanElement,
aElement);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_FAILED(rv)) {
NS_WARNING(
"EditorBase::CloneAttributeWithTransaction(nsGkAtoms::_class) "
"failed");
return rv;
}
}
nsresult rv = RemoveContainerWithTransaction(aElement);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
return rv;
}
}
// If aAttribute is specified, we want to remove only the attribute
// unless it's the last attribute of aElement.
else if (aElement.HasAttr(kNameSpaceID_None, aAttribute)) {
if (IsOnlyAttribute(&aElement, aAttribute)) {
nsresult rv = RemoveContainerWithTransaction(aElement);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
return rv;
}
} else {
nsresult rv = RemoveAttributeWithTransaction(aElement, *aAttribute);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::RemoveAttributeWithTransaction() failed");
return rv;
}
}
}
}
// Then, remove CSS style if specified.
// XXX aElement may have already been removed from the DOM tree. Why
// do we keep handling aElement here??
if (CSSEditUtils::IsCSSEditableProperty(&aElement, aProperty, aAttribute) &&
CSSEditUtils::HaveSpecifiedCSSEquivalentStyles(aElement, aProperty,
aAttribute)) {
if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) {
// If aElement has CSS declaration of the given style, remove it.
// MOZ_KnownLive(*styledElement): It's aElement and its lifetime must be
// guaranteed by the caller because of MOZ_CAN_RUN_SCRIPT method.
nsresult rv =
mCSSEditUtils->RemoveCSSEquivalentToHTMLStyleWithTransaction(
MOZ_KnownLive(*styledElement), aProperty, aAttribute, nullptr);
if (rv == NS_ERROR_EDITOR_DESTROYED) {
NS_WARNING(
"CSSEditUtils::RemoveCSSEquivalentToHTMLStyleWithTransaction() "
"destroyed the editor");
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"CSSEditUtils::RemoveCSSEquivalentToHTMLStyleWithTransaction() "
"failed, but ignored");
}
// Additionally, remove aElement itself if it's a `<span>` or `<font>`
// and it does not have non-empty `style`, `id` nor `class` attribute.
if (aElement.IsAnyOfHTMLElements(nsGkAtoms::span, nsGkAtoms::font) &&
!HTMLEditor::HasStyleOrIdOrClassAttribute(aElement)) {
DebugOnly<nsresult> rvIgnored = RemoveContainerWithTransaction(aElement);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"HTMLEditor::RemoveContainerWithTransaction() failed, but ignored");
}
}
// Finally, remove aElement if it's a `<big>` or `<small>` element and
// we're removing `<font size>`.
if (aProperty == nsGkAtoms::font && aAttribute == nsGkAtoms::size &&
aElement.IsAnyOfHTMLElements(nsGkAtoms::big, nsGkAtoms::small)) {
nsresult rv = RemoveContainerWithTransaction(aElement);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::RemoveContainerWithTransaction() failed");
return rv;
}
return NS_OK;
}
bool HTMLEditor::IsOnlyAttribute(const Element* aElement, nsAtom* aAttribute) {
MOZ_ASSERT(aElement);
uint32_t attrCount = aElement->GetAttrCount();
for (uint32_t i = 0; i < attrCount; ++i) {
const nsAttrName* name = aElement->GetAttrNameAt(i);
if (!name->NamespaceEquals(kNameSpaceID_None)) {
return false;
}
// if it's the attribute we know about, or a special _moz attribute,
// keep looking
if (name->LocalName() != aAttribute) {
nsAutoString attrString;
name->LocalName()->ToString(attrString);
if (!StringBeginsWith(attrString, u"_moz"_ns)) {
return false;
}
}
}
// if we made it through all of them without finding a real attribute
// other than aAttribute, then return true
return true;
}
nsresult HTMLEditor::PromoteRangeIfStartsOrEndsInNamedAnchor(nsRange& aRange) {
// We assume that <a> is not nested.
// XXX Shouldn't ignore the editing host.
if (NS_WARN_IF(!aRange.GetStartContainer()) ||
NS_WARN_IF(!aRange.GetEndContainer())) {
return NS_ERROR_INVALID_ARG;
}
EditorRawDOMPoint newRangeStart(aRange.StartRef());
for (Element* element :
aRange.GetStartContainer()->InclusiveAncestorsOfType<Element>()) {
if (element->IsHTMLElement(nsGkAtoms::body)) {
break;
}
if (!HTMLEditUtils::IsNamedAnchor(element)) {
continue;
}
newRangeStart.Set(element);
break;
}
if (!newRangeStart.GetContainerAsContent()) {
NS_WARNING(
"HTMLEditor::PromoteRangeIfStartsOrEndsInNamedAnchor() reached root "
"element from start container");
return NS_ERROR_FAILURE;
}
EditorRawDOMPoint newRangeEnd(aRange.EndRef());
for (Element* element :
aRange.GetEndContainer()->InclusiveAncestorsOfType<Element>()) {
if (element->IsHTMLElement(nsGkAtoms::body)) {
break;
}
if (!HTMLEditUtils::IsNamedAnchor(element)) {
continue;
}
newRangeEnd.SetAfter(element);
break;
}
if (!newRangeEnd.GetContainerAsContent()) {
NS_WARNING(
"HTMLEditor::PromoteRangeIfStartsOrEndsInNamedAnchor() reached root "
"element from end container");
return NS_ERROR_FAILURE;
}
if (newRangeStart == aRange.StartRef() && newRangeEnd == aRange.EndRef()) {
return NS_OK;
}
nsresult rv = aRange.SetStartAndEnd(newRangeStart.ToRawRangeBoundary(),
newRangeEnd.ToRawRangeBoundary());
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed");
return rv;
}
nsresult HTMLEditor::PromoteInlineRange(nsRange& aRange) {
if (NS_WARN_IF(!aRange.GetStartContainer()) ||
NS_WARN_IF(!aRange.GetEndContainer())) {
return NS_ERROR_INVALID_ARG;
}
EditorRawDOMPoint newRangeStart(aRange.StartRef());
for (nsIContent* content :
aRange.GetStartContainer()->InclusiveAncestorsOfType<nsIContent>()) {
MOZ_ASSERT(newRangeStart.GetContainer() == content);
if (content->IsHTMLElement(nsGkAtoms::body) ||
!EditorUtils::IsEditableContent(*content, EditorType::HTML) ||
!IsStartOfContainerOrBeforeFirstEditableChild(newRangeStart)) {
break;
}
newRangeStart.Set(content);
}
if (!newRangeStart.GetContainerAsContent()) {
NS_WARNING(
"HTMLEditor::PromoteInlineRange() reached root element from start "
"container");
return NS_ERROR_FAILURE;
}
EditorRawDOMPoint newRangeEnd(aRange.EndRef());
for (nsIContent* content :
aRange.GetEndContainer()->InclusiveAncestorsOfType<nsIContent>()) {
MOZ_ASSERT(newRangeEnd.GetContainer() == content);
if (content->IsHTMLElement(nsGkAtoms::body) ||
!EditorUtils::IsEditableContent(*content, EditorType::HTML) ||
!IsEndOfContainerOrEqualsOrAfterLastEditableChild(newRangeEnd)) {
break;
}
newRangeEnd.SetAfter(content);
}
if (!newRangeEnd.GetContainerAsContent()) {
NS_WARNING(
"HTMLEditor::PromoteInlineRange() reached root element from end "
"container");
return NS_ERROR_FAILURE;
}
if (newRangeStart == aRange.StartRef() && newRangeEnd == aRange.EndRef()) {
return NS_OK;
}
nsresult rv = aRange.SetStartAndEnd(newRangeStart.ToRawRangeBoundary(),
newRangeEnd.ToRawRangeBoundary());
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed");
return rv;
}
bool HTMLEditor::IsStartOfContainerOrBeforeFirstEditableChild(
const EditorRawDOMPoint& aPoint) const {
MOZ_ASSERT(aPoint.IsSet());
if (aPoint.IsStartOfContainer()) {
return true;
}
if (aPoint.IsInTextNode()) {
return false;
}
nsIContent* firstEditableChild = HTMLEditUtils::GetFirstChild(
*aPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode});
if (!firstEditableChild) {
return true;
}
return EditorRawDOMPoint(firstEditableChild).Offset() >= aPoint.Offset();
}
bool HTMLEditor::IsEndOfContainerOrEqualsOrAfterLastEditableChild(
const EditorRawDOMPoint& aPoint) const {
MOZ_ASSERT(aPoint.IsSet());
if (aPoint.IsEndOfContainer()) {
return true;
}
if (aPoint.IsInTextNode()) {
return false;
}
nsIContent* lastEditableChild = HTMLEditUtils::GetLastChild(
*aPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode});
if (!lastEditableChild) {
return true;
}
return EditorRawDOMPoint(lastEditableChild).Offset() < aPoint.Offset();
}
nsresult HTMLEditor::GetInlinePropertyBase(nsAtom& aHTMLProperty,
nsAtom* aAttribute,
const nsAString* aValue,
bool* aFirst, bool* aAny, bool* aAll,
nsAString* outValue) const {
MOZ_ASSERT(IsEditActionDataAvailable());
*aAny = false;
*aAll = true;
*aFirst = false;
bool first = true;
bool isCollapsed = SelectionRef().IsCollapsed();
RefPtr<nsRange> range = SelectionRef().GetRangeAt(0);
// XXX: Should be a while loop, to get each separate range
// XXX: ERROR_HANDLING can currentItem be null?
if (range) {
// For each range, set a flag
bool firstNodeInRange = true;
if (isCollapsed) {
nsCOMPtr<nsINode> collapsedNode = range->GetStartContainer();
if (NS_WARN_IF(!collapsedNode)) {
return NS_ERROR_FAILURE;
}
bool isSet, theSetting;
nsString tOutString;
if (aAttribute) {
mTypeInState->GetTypingState(isSet, theSetting, &aHTMLProperty,
aAttribute, &tOutString);
if (outValue) {
outValue->Assign(tOutString);
}
} else {
mTypeInState->GetTypingState(isSet, theSetting, &aHTMLProperty);
}
if (isSet) {
*aFirst = *aAny = *aAll = theSetting;
return NS_OK;
}
if (collapsedNode->IsContent() &&
CSSEditUtils::IsCSSEditableProperty(collapsedNode, &aHTMLProperty,
aAttribute)) {
if (aValue) {
tOutString.Assign(*aValue);
}
*aFirst = *aAny = *aAll =
CSSEditUtils::IsComputedCSSEquivalentToHTMLInlineStyleSet(
MOZ_KnownLive(*collapsedNode->AsContent()), &aHTMLProperty,
aAttribute, tOutString);
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (outValue) {
outValue->Assign(tOutString);
}
return NS_OK;
}
*aFirst = *aAny = *aAll = collapsedNode->IsContent() &&
HTMLEditUtils::IsInlineStyleSetByElement(
*collapsedNode->AsContent(), aHTMLProperty</