diff options
Diffstat (limited to '')
-rw-r--r-- | editor/libeditor/HTMLStyleEditor.cpp | 2689 |
1 files changed, 2689 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLStyleEditor.cpp b/editor/libeditor/HTMLStyleEditor.cpp new file mode 100644 index 0000000000..5a62978a63 --- /dev/null +++ b/editor/libeditor/HTMLStyleEditor.cpp @@ -0,0 +1,2689 @@ +/* -*- 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 "TypeInState.h" +#include "mozilla/Assertions.h" +#include "mozilla/ContentIterator.h" +#include "mozilla/EditAction.h" +#include "mozilla/EditorUtils.h" +#include "mozilla/SelectionState.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 ChildBlockBoundary = HTMLEditUtils::ChildBlockBoundary; + +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 (SelectionRefPtr()->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 (IsPlaintextEditor()) { + 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(SelectionRefPtr()); + 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 (IsTextPropertySetByContent(&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 = GetPriorHTMLSibling(textNodeForTheRange); + 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 = GetNextHTMLSibling(textNodeForTheRange); + 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) && + !IsEmptyTextNode(*child)) { + 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 = GetPriorHTMLSibling(&aContent); + nsCOMPtr<nsIContent> nextSibling = GetNextHTMLSibling(&aContent); + 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 (IsTextPropertySetByContent(&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); + 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; + } + MOZ_ASSERT(splitNodeResult.Handled()); + // 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) { + 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() && + IsEmptyNode(*splitResult.GetPreviousNode(), false, true)) { + // 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()) { + MOZ_ASSERT(IsCSSEnabled()); + 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::GetFirstLeafChild( + *splitResult.GetNextNode(), ChildBlockBoundary::Ignore); + 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() && + IsEmptyNode(*splitResultAtStartOfNextNode.GetNextNode(), false, true)) { + // 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::GetFirstLeafChild( + *splitResultAtStartOfNextNode.GetPreviousNode(), + ChildBlockBoundary::Ignore); + 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); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RemoveStyleInside() failed"); + return EditResult(rv); + } + } + return EditResult(pointToPutCaret); +} + +nsresult HTMLEditor::RemoveStyleInside(Element& aElement, nsAtom* aProperty, + nsAtom* aAttribute) { + // 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); + 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 && + (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 = + GetFirstEditableChild(*aPoint.GetContainer()); + 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 = GetLastEditableChild(*aPoint.GetContainer()); + 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 = SelectionRefPtr()->IsCollapsed(); + RefPtr<nsRange> range = SelectionRefPtr()->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; + } + + isSet = IsTextPropertySetByContent(collapsedNode, &aHTMLProperty, + aAttribute, aValue, outValue); + *aFirst = *aAny = *aAll = isSet; + return NS_OK; + } + + // Non-collapsed selection + + nsAutoString firstValue, theValue; + + nsCOMPtr<nsINode> endNode = range->GetEndContainer(); + int32_t endOffset = range->EndOffset(); + + PostContentIterator postOrderIter; + DebugOnly<nsresult> rvIgnored = postOrderIter.Init(range); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to initialize post-order content iterator"); + for (; !postOrderIter.IsDone(); postOrderIter.Next()) { + if (!postOrderIter.GetCurrentNode()->IsContent()) { + continue; + } + nsCOMPtr<nsIContent> content = + postOrderIter.GetCurrentNode()->AsContent(); + + if (content->IsHTMLElement(nsGkAtoms::body)) { + break; + } + + // just ignore any non-editable nodes + if (content->IsText() && + (!EditorUtils::IsEditableContent(*content, EditorType::HTML) || + IsEmptyTextNode(*content))) { + continue; + } + if (content->GetAsText()) { + if (!isCollapsed && first && firstNodeInRange) { + firstNodeInRange = false; + if (range->StartOffset() == content->Length()) { + continue; + } + } else if (content == endNode && !endOffset) { + continue; + } + } else if (content->IsElement()) { + // handle non-text leaf nodes here + continue; + } + + bool isSet = false; + bool useTextDecoration = + &aHTMLProperty == nsGkAtoms::u || &aHTMLProperty == nsGkAtoms::strike; + if (first) { + if (CSSEditUtils::IsCSSEditableProperty(content, &aHTMLProperty, + aAttribute)) { + // The HTML styles defined by aHTMLProperty/aAttribute have a CSS + // equivalence in this implementation for node; let's check if it + // carries those CSS styles + if (aValue) { + firstValue.Assign(*aValue); + } + isSet = CSSEditUtils::IsComputedCSSEquivalentToHTMLInlineStyleSet( + *content, &aHTMLProperty, aAttribute, firstValue); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + } else { + isSet = IsTextPropertySetByContent(content, &aHTMLProperty, + aAttribute, aValue, &firstValue); + } + *aFirst = isSet; + first = false; + if (outValue) { + *outValue = firstValue; + } + } else { + if (CSSEditUtils::IsCSSEditableProperty(content, &aHTMLProperty, + aAttribute)) { + // The HTML styles defined by aHTMLProperty/aAttribute have a CSS + // equivalence in this implementation for node; let's check if it + // carries those CSS styles + if (aValue) { + theValue.Assign(*aValue); + } + isSet = CSSEditUtils::IsComputedCSSEquivalentToHTMLInlineStyleSet( + *content, &aHTMLProperty, aAttribute, theValue); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + } else { + isSet = IsTextPropertySetByContent(content, &aHTMLProperty, + aAttribute, aValue, &theValue); + } + + if (firstValue != theValue && + // For text-decoration related HTML properties, i.e. <u> and + // <strike>, we have to also check |isSet| because text-decoration + // is a shorthand property, and it may contains other unrelated + // longhand components, e.g. text-decoration-color, so we have to do + // an extra check before setting |*aAll| to false. + // e.g. + // firstValue: "underline rgb(0, 0, 0)" + // theValue: "underline rgb(0, 0, 238)" // <a> uses blue color + // These two values should be the same if we are checking `<u>`. + // That's why we need to check |*aFirst| and |isSet|. + // + // This is a work-around for text-decoration. + // The spec issue: https://github.com/w3c/editing/issues/241. + // Once this spec issue is resolved, we could drop this work-around + // check. + (!useTextDecoration || *aFirst != isSet)) { + *aAll = false; + } + } + + if (isSet) { + *aAny = true; + } else { + *aAll = false; + } + } + } + if (!*aAny) { + // make sure that if none of the selection is set, we don't report all is + // set + *aAll = false; + } + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetInlineProperty(const nsAString& aHTMLProperty, + const nsAString& aAttribute, + const nsAString& aValue, + bool* aFirst, bool* aAny, + bool* aAll) { + RefPtr<nsAtom> property = NS_Atomize(aHTMLProperty); + nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute); + nsresult rv = GetInlineProperty(property, MOZ_KnownLive(attribute), aValue, + aFirst, aAny, aAll); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::GetInlineProperty() failed"); + return rv; +} + +nsresult HTMLEditor::GetInlineProperty(nsAtom* aHTMLProperty, + nsAtom* aAttribute, + const nsAString& aValue, bool* aFirst, + bool* aAny, bool* aAll) const { + if (NS_WARN_IF(!aHTMLProperty) || NS_WARN_IF(!aFirst) || NS_WARN_IF(!aAny) || + NS_WARN_IF(!aAll)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + const nsAString* val = !aValue.IsEmpty() ? &aValue : nullptr; + nsresult rv = GetInlinePropertyBase(*aHTMLProperty, aAttribute, val, aFirst, + aAny, aAll, nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::GetInlinePropertyBase() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::GetInlinePropertyWithAttrValue( + const nsAString& aHTMLProperty, const nsAString& aAttribute, + const nsAString& aValue, bool* aFirst, bool* aAny, bool* aAll, + nsAString& outValue) { + RefPtr<nsAtom> property = NS_Atomize(aHTMLProperty); + nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute); + nsresult rv = GetInlinePropertyWithAttrValue( + property, MOZ_KnownLive(attribute), aValue, aFirst, aAny, aAll, outValue); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::GetInlinePropertyWithAttrValue() failed"); + return rv; +} + +nsresult HTMLEditor::GetInlinePropertyWithAttrValue( + nsAtom* aHTMLProperty, nsAtom* aAttribute, const nsAString& aValue, + bool* aFirst, bool* aAny, bool* aAll, nsAString& outValue) { + if (NS_WARN_IF(!aHTMLProperty) || NS_WARN_IF(!aFirst) || NS_WARN_IF(!aAny) || + NS_WARN_IF(!aAll)) { + return NS_ERROR_INVALID_ARG; + } + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + const nsAString* val = !aValue.IsEmpty() ? &aValue : nullptr; + nsresult rv = GetInlinePropertyBase(*aHTMLProperty, aAttribute, val, aFirst, + aAny, aAll, &outValue); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::GetInlinePropertyBase() failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::RemoveAllInlinePropertiesAsAction( + nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData( + *this, EditAction::eRemoveAllInlineStyleProperties, aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + AutoPlaceholderBatch treatAsOneTransaction(*this, + ScrollSelectionIntoView::Yes); + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eRemoveAllTextProperties, nsIEditor::eNext, + ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return EditorBase::ToGenericNSResult(ignoredError.StealNSResult()); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + + rv = + RemoveInlinePropertyInternal(nullptr, nullptr, RemoveRelatedElements::No); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::RemoveInlinePropertyInternal(nullptr, " + "nullptr, RemoveRelatedElements::No) failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::RemoveInlinePropertyAsAction(nsStaticAtom& aHTMLProperty, + nsStaticAtom* aAttribute, + nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData( + *this, + HTMLEditUtils::GetEditActionForFormatText(aHTMLProperty, aAttribute, + false), + aPrincipal); + switch (editActionData.GetEditAction()) { + case EditAction::eRemoveFontFamilyProperty: + MOZ_ASSERT(!u""_ns.IsVoid()); + editActionData.SetData(u""_ns); + break; + case EditAction::eRemoveColorProperty: + case EditAction::eRemoveBackgroundColorPropertyInline: + editActionData.SetColorData(u""_ns); + 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 = RemoveInlinePropertyInternal(&aHTMLProperty, aAttribute, + RemoveRelatedElements::Yes); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::RemoveInlinePropertyInternal(" + "RemoveRelatedElements::Yes) failed"); + return EditorBase::ToGenericNSResult(rv); +} + +NS_IMETHODIMP HTMLEditor::RemoveInlineProperty(const nsAString& aProperty, + const nsAString& aAttribute) { + nsStaticAtom* property = NS_GetStaticAtom(aProperty); + nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute); + + AutoEditActionDataSetter editActionData( + *this, + HTMLEditUtils::GetEditActionForFormatText(*property, attribute, false)); + switch (editActionData.GetEditAction()) { + case EditAction::eRemoveFontFamilyProperty: + MOZ_ASSERT(!u""_ns.IsVoid()); + editActionData.SetData(u""_ns); + break; + case EditAction::eRemoveColorProperty: + case EditAction::eRemoveBackgroundColorPropertyInline: + editActionData.SetColorData(u""_ns); + 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 = RemoveInlinePropertyInternal(MOZ_KnownLive(property), + MOZ_KnownLive(attribute), + RemoveRelatedElements::No); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::RemoveInlinePropertyInternal(" + "RemoveRelatedElements::No) failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::RemoveInlinePropertyInternal( + nsStaticAtom* aProperty, nsStaticAtom* aAttribute, + RemoveRelatedElements aRemoveRelatedElements) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aAttribute != nsGkAtoms::_empty); + + 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"); + + // Also remove equivalent properties (bug 317093) + struct HTMLStyle final { + // HTML tag name or nsGkAtoms::href or nsGkAtoms::name. + nsStaticAtom* mProperty = nullptr; + // HTML attribute like nsGkAtom::color for nsGkAtoms::font. + nsStaticAtom* mAttribute = nullptr; + + explicit HTMLStyle(nsStaticAtom* aProperty, + nsStaticAtom* aAttribute = nullptr) + : mProperty(aProperty), mAttribute(aAttribute) {} + }; + AutoTArray<HTMLStyle, 3> removeStyles; + if (aRemoveRelatedElements == RemoveRelatedElements::Yes) { + if (aProperty == nsGkAtoms::b) { + removeStyles.AppendElement(HTMLStyle(nsGkAtoms::strong)); + } else if (aProperty == nsGkAtoms::i) { + removeStyles.AppendElement(HTMLStyle(nsGkAtoms::em)); + } else if (aProperty == nsGkAtoms::strike) { + removeStyles.AppendElement(HTMLStyle(nsGkAtoms::s)); + } else if (aProperty == nsGkAtoms::font) { + if (aAttribute == nsGkAtoms::size) { + removeStyles.AppendElement(HTMLStyle(nsGkAtoms::big)); + removeStyles.AppendElement(HTMLStyle(nsGkAtoms::small)); + } + // Handling `<tt>` element code was implemented for composer (bug 115922). + // This shouldn't work with `Document.execCommand()` for compatibility + // with the other browsers. Currently, edit action principal is set only + // when the root caller is Document::ExecCommand() so that we should + // handle `<tt>` element only when the principal is nullptr that must be + // only when XUL command is executed on composer. + else if (aAttribute == nsGkAtoms::face && !GetEditActionPrincipal()) { + removeStyles.AppendElement(HTMLStyle(nsGkAtoms::tt)); + } + } + } + removeStyles.AppendElement(HTMLStyle(aProperty, aAttribute)); + + if (SelectionRefPtr()->IsCollapsed()) { + // Manipulating text attributes on a collapsed selection only sets state + // for the next text insertion + if (removeStyles[0].mProperty) { + for (HTMLStyle& style : removeStyles) { + MOZ_ASSERT(style.mProperty); + if (style.mProperty == nsGkAtoms::href || + style.mProperty == nsGkAtoms::name) { + mTypeInState->ClearProp(nsGkAtoms::a, nullptr); + } else { + mTypeInState->ClearProp(style.mProperty, style.mAttribute); + } + } + } else { + mTypeInState->ClearAllProps(); + } + return NS_OK; + } + + // XXX Shouldn't we quit before calling `CommitComposition()`? + if (IsPlaintextEditor()) { + 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::eRemoveTextProperty, 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); + + for (HTMLStyle& style : removeStyles) { + // Loop through the ranges in the selection. + // XXX Although `Selection` will be restored by AutoSelectionRestorer, + // AutoSelectionRangeArray just grabs the ranges in `Selection`. + // Therefore, modifying each range may notify selection listener. So + // perhaps, we should clone each range here instead. + AutoSelectionRangeArray arrayOfRanges(SelectionRefPtr()); + for (auto& range : arrayOfRanges.mRanges) { + if (style.mProperty == nsGkAtoms::name) { + // Promote range if it starts or end in a named anchor and we want to + // remove named anchors + nsresult rv = PromoteRangeIfStartsOrEndsInNamedAnchor(*range); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::PromoteRangeIfStartsOrEndsInNamedAnchor() failed"); + return rv; + } + } else { + // 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; + } + } + + // Remove this style from ancestors of our range endpoints, splitting + // them as appropriate + SplitRangeOffResult splitRangeOffResult = + SplitAncestorStyledInlineElementsAtRangeEdges( + EditorDOMPoint(range->StartRef()), + EditorDOMPoint(range->EndRef()), MOZ_KnownLive(style.mProperty), + MOZ_KnownLive(style.mAttribute)); + if (splitRangeOffResult.Failed()) { + NS_WARNING( + "HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges() " + "failed"); + return splitRangeOffResult.Rv(); + } + + // XXX Modifying `range` means that we may modify ranges in `Selection`. + // Is this intentional? Note that the range may be not in + // `Selection` too. It seems that at least one of them is not + // an unexpected case. + const EditorDOMPoint& startOfRange( + splitRangeOffResult.SplitPointAtStart()); + const EditorDOMPoint& endOfRange(splitRangeOffResult.SplitPointAtEnd()); + if (NS_WARN_IF(!startOfRange.IsSet()) || + NS_WARN_IF(!endOfRange.IsSet())) { + continue; + } + + nsresult rv = range->SetStartAndEnd(startOfRange.ToRawRangeBoundary(), + endOfRange.ToRawRangeBoundary()); + // Note that modifying a range in `Selection` may run script so that + // we might have been destroyed here. + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (NS_FAILED(rv)) { + NS_WARNING("nsRange::SetStartAndEnd() failed"); + return rv; + } + + // Collect editable nodes which are entirely contained in the range. + AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents; + if (startOfRange.GetContainer() == endOfRange.GetContainer() && + startOfRange.IsInTextNode()) { + if (!EditorUtils::IsEditableContent(*startOfRange.ContainerAsText(), + EditorType::HTML)) { + continue; + } + arrayOfContents.AppendElement(*startOfRange.ContainerAsText()); + } else if (startOfRange.IsInTextNode() && endOfRange.IsInTextNode() && + startOfRange.GetContainer()->GetNextSibling() == + endOfRange.GetContainer()) { + if (EditorUtils::IsEditableContent(*startOfRange.ContainerAsText(), + EditorType::HTML)) { + arrayOfContents.AppendElement(*startOfRange.ContainerAsText()); + } + if (EditorUtils::IsEditableContent(*endOfRange.ContainerAsText(), + EditorType::HTML)) { + arrayOfContents.AppendElement(*endOfRange.ContainerAsText()); + } + if (arrayOfContents.IsEmpty()) { + continue; + } + } else { + // Append first node if it's a text node but selected not entirely. + if (startOfRange.IsInTextNode() && + !startOfRange.IsStartOfContainer() && + EditorUtils::IsEditableContent(*startOfRange.ContainerAsText(), + EditorType::HTML)) { + arrayOfContents.AppendElement(*startOfRange.ContainerAsText()); + } + // Append all entirely selected nodes. + ContentSubtreeIterator subtreeIter; + if (NS_SUCCEEDED(subtreeIter.Init(range))) { + for (; !subtreeIter.IsDone(); subtreeIter.Next()) { + nsCOMPtr<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()); + } + } + } + // Append last node if it's a text node but selected not entirely. + if (startOfRange.GetContainer() != endOfRange.GetContainer() && + endOfRange.IsInTextNode() && !endOfRange.IsEndOfContainer() && + EditorUtils::IsEditableContent(*endOfRange.ContainerAsText(), + EditorType::HTML)) { + arrayOfContents.AppendElement(*endOfRange.ContainerAsText()); + } + } + + for (OwningNonNull<nsIContent>& content : arrayOfContents) { + if (content->IsElement()) { + nsresult rv = + RemoveStyleInside(MOZ_KnownLive(*content->AsElement()), + MOZ_KnownLive(style.mProperty), + MOZ_KnownLive(style.mAttribute)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RemoveStyleInside() failed"); + return rv; + } + } + + bool isRemovable = IsRemovableParentStyleWithNewSpanElement( + MOZ_KnownLive(content), MOZ_KnownLive(style.mProperty), + MOZ_KnownLive(style.mAttribute)); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (!isRemovable) { + continue; + } + + if (!content->IsText()) { + // XXX Do we need to call this even when data node or something? If + // so, for what? + // MOZ_KnownLive because 'arrayOfContents' is guaranteed to + // keep it alive. + DebugOnly<nsresult> rvIgnored = SetInlinePropertyOnNode( + MOZ_KnownLive(content), MOZ_KnownLive(*style.mProperty), + MOZ_KnownLive(style.mAttribute), + u"-moz-editor-invert-value"_ns); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "HTMLEditor::SetInlinePropertyOnNode(-moz-editor-invert-value) " + "failed, but ignored"); + continue; + } + + // If current node is a text node, we need to create `<span>` element + // for it to overwrite parent style. Unfortunately, all browsers + // don't join text nodes when removing a style. Therefore, there + // may be multiple text nodes as adjacent siblings. That's the + // reason why we need to handle text nodes in this loop. + uint32_t startOffset = content == startOfRange.GetContainer() + ? startOfRange.Offset() + : 0; + uint32_t endOffset = content == endOfRange.GetContainer() + ? endOfRange.Offset() + : content->Length(); + nsresult rv = SetInlinePropertyOnTextNode( + MOZ_KnownLive(*content->AsText()), startOffset, endOffset, + MOZ_KnownLive(*style.mProperty), MOZ_KnownLive(style.mAttribute), + u"-moz-editor-invert-value"_ns); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::SetInlinePropertyOnTextNode(-moz-editor-invert-" + "value) failed"); + return rv; + } + } + + // For avoiding unnecessary loop cost, check whether the style is + // invertible first. + if (style.mProperty && + CSSEditUtils::IsCSSInvertible(*style.mProperty, style.mAttribute)) { + // Finally, we should remove the style from all leaf text nodes if + // they still have the style. + AutoTArray<OwningNonNull<Text>, 32> leafTextNodes; + for (OwningNonNull<nsIContent>& content : arrayOfContents) { + if (content->IsElement()) { + CollectEditableLeafTextNodes(*content->AsElement(), + leafTextNodes); + } + } + for (OwningNonNull<Text>& textNode : leafTextNodes) { + bool isRemovable = IsRemovableParentStyleWithNewSpanElement( + MOZ_KnownLive(textNode), MOZ_KnownLive(style.mProperty), + MOZ_KnownLive(style.mAttribute)); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (!isRemovable) { + continue; + } + // MOZ_KnownLive because 'leafTextNodes' is guaranteed to + // keep it alive. + nsresult rv = SetInlinePropertyOnTextNode( + MOZ_KnownLive(textNode), 0, textNode->TextLength(), + MOZ_KnownLive(*style.mProperty), + MOZ_KnownLive(style.mAttribute), + u"-moz-editor-invert-value"_ns); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::SetInlinePropertyOnTextNode(-moz-editor-invert-" + "value) failed"); + return rv; + } + } + } + } // for-loop of selection ranges + } // for-loop of styles + } // AutoSelectionRestorer and AutoTransactionsConserveSelection + + // Restoring `Selection` may cause destroying us. + return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; +} + +bool HTMLEditor::IsRemovableParentStyleWithNewSpanElement( + nsIContent& aContent, nsAtom* aHTMLProperty, nsAtom* aAttribute) const { + // We don't support to remove all inline styles with this path. + if (!aHTMLProperty) { + return false; + } + + // First check whether the style is invertible since this is the fastest + // check. + if (!CSSEditUtils::IsCSSInvertible(*aHTMLProperty, aAttribute)) { + return false; + } + + // If parent block has the removing style, we should create `<span>` + // element to remove the style even in HTML mode since Chrome does it. + if (!CSSEditUtils::IsCSSEditableProperty(&aContent, aHTMLProperty, + aAttribute)) { + return false; + } + + // aContent's computed style indicates the CSS equivalence to + // the HTML style to remove is applied; but we found no element + // in the ancestors of aContent carrying specified styles; + // assume it comes from a rule and let's try to insert a span + // "inverting" the style + nsAutoString emptyString; + bool isSet = CSSEditUtils::IsComputedCSSEquivalentToHTMLInlineStyleSet( + aContent, aHTMLProperty, aAttribute, emptyString); + return NS_WARN_IF(Destroyed()) ? false : isSet; +} + +void HTMLEditor::CollectEditableLeafTextNodes( + Element& aElement, nsTArray<OwningNonNull<Text>>& aLeafTextNodes) const { + for (nsIContent* child = aElement.GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsElement()) { + CollectEditableLeafTextNodes(*child->AsElement(), aLeafTextNodes); + continue; + } + if (child->IsText()) { + aLeafTextNodes.AppendElement(*child->AsText()); + } + } +} + +nsresult HTMLEditor::IncreaseFontSizeAsAction(nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData(*this, EditAction::eIncrementFontSize, + aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + rv = RelativeFontChange(FontSize::incr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::RelativeFontChange(FontSize::incr) failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::DecreaseFontSizeAsAction(nsIPrincipal* aPrincipal) { + AutoEditActionDataSetter editActionData(*this, EditAction::eDecrementFontSize, + aPrincipal); + nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent(); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED, + "CanHandleAndMaybeDispatchBeforeInputEvent(), failed"); + return EditorBase::ToGenericNSResult(rv); + } + + rv = RelativeFontChange(FontSize::decr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::RelativeFontChange(FontSize::decr) failed"); + return EditorBase::ToGenericNSResult(rv); +} + +nsresult HTMLEditor::RelativeFontChange(FontSize aDir) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + DebugOnly<nsresult> rvIgnored = CommitComposition(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "EditorBase::CommitComposition() failed, but ignored"); + + // If selection is collapsed, set typing state + if (SelectionRefPtr()->IsCollapsed()) { + nsAtom& atom = aDir == FontSize::incr ? *nsGkAtoms::big : *nsGkAtoms::small; + + // Let's see in what kind of element the selection is + if (NS_WARN_IF(!SelectionRefPtr()->RangeCount())) { + return NS_OK; + } + RefPtr<const nsRange> firstRange = SelectionRefPtr()->GetRangeAt(0); + if (NS_WARN_IF(!firstRange) || + NS_WARN_IF(!firstRange->GetStartContainer())) { + return NS_OK; + } + OwningNonNull<nsINode> selectedNode = *firstRange->GetStartContainer(); + if (selectedNode->IsText()) { + if (NS_WARN_IF(!selectedNode->GetParentNode())) { + return NS_OK; + } + selectedNode = *selectedNode->GetParentNode(); + } + if (!HTMLEditUtils::CanNodeContain(selectedNode, atom)) { + return NS_OK; + } + + // Manipulating text attributes on a collapsed selection only sets state + // for the next text insertion + mTypeInState->SetProp(&atom, nullptr, u""_ns); + return NS_OK; + } + + // Wrap with txn batching, rules sniffing, and selection preservation code + AutoPlaceholderBatch treatAsOneTransaction(*this, + ScrollSelectionIntoView::Yes); + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eSetTextProperty, 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 + AutoSelectionRangeArray arrayOfRanges(SelectionRefPtr()); + for (auto& range : arrayOfRanges.mRanges) { + // Adjust range to include any ancestors with entirely selected children + nsresult rv = PromoteInlineRange(*range); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::PromoteInlineRange() failed"); + return rv; + } + + // Check for easy case: both range endpoints in same text node + nsCOMPtr<nsINode> startNode = range->GetStartContainer(); + nsCOMPtr<nsINode> endNode = range->GetEndContainer(); + MOZ_ASSERT(startNode); + MOZ_ASSERT(endNode); + if (startNode == endNode && startNode->IsText()) { + nsresult rv = RelativeFontChangeOnTextNode( + aDir, MOZ_KnownLive(*startNode->GetAsText()), range->StartOffset(), + range->EndOffset()); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RelativeFontChangeOnTextNode() failed"); + return rv; + } + } else { + // Not the easy case. Range not contained in single text node. There + // are up to three phases here. There are all the nodes reported by the + // subtree iterator to be processed. And there are potentially a + // starting textnode and an ending textnode which are only partially + // contained by the range. + + // Let's handle the nodes reported by the iterator. These nodes are + // entirely contained in the selection range. We build up a list of them + // (since doing operations on the document during iteration would perturb + // the iterator). + + // Iterate range and build up array + ContentSubtreeIterator subtreeIter; + if (NS_SUCCEEDED(subtreeIter.Init(range))) { + nsTArray<OwningNonNull<nsIContent>> arrayOfContents; + for (; !subtreeIter.IsDone(); subtreeIter.Next()) { + if (NS_WARN_IF(!subtreeIter.GetCurrentNode()->IsContent())) { + return NS_ERROR_FAILURE; + } + OwningNonNull<nsIContent> content = + *subtreeIter.GetCurrentNode()->AsContent(); + + if (EditorUtils::IsEditableContent(content, EditorType::HTML)) { + arrayOfContents.AppendElement(content); + } + } + + // Now that we have the list, do the font size change on each node + for (OwningNonNull<nsIContent>& content : arrayOfContents) { + // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it + // alive. + nsresult rv = RelativeFontChangeOnNode( + aDir == FontSize::incr ? +1 : -1, MOZ_KnownLive(content)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RelativeFontChangeOnNode() failed"); + return rv; + } + } + } + // Now check the start and end parents of the range to see if they need + // to be separately handled (they do if they are text nodes, due to how + // the subtree iterator works - it will not have reported them). + if (startNode->IsText() && EditorUtils::IsEditableContent( + *startNode->AsText(), EditorType::HTML)) { + nsresult rv = RelativeFontChangeOnTextNode( + aDir, MOZ_KnownLive(*startNode->AsText()), range->StartOffset(), + startNode->Length()); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RelativeFontChangeOnTextNode() failed"); + return rv; + } + } + if (endNode->IsText() && EditorUtils::IsEditableContent( + *endNode->AsText(), EditorType::HTML)) { + nsresult rv = RelativeFontChangeOnTextNode( + aDir, MOZ_KnownLive(*endNode->AsText()), 0, range->EndOffset()); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RelativeFontChangeOnTextNode() failed"); + return rv; + } + } + } + } + + return NS_OK; +} + +nsresult HTMLEditor::RelativeFontChangeOnTextNode(FontSize aDir, + Text& aTextNode, + int32_t aStartOffset, + int32_t aEndOffset) { + // Don't need to do anything if no characters actually selected + if (aStartOffset == aEndOffset) { + return NS_OK; + } + + if (!aTextNode.GetParentNode() || + !HTMLEditUtils::CanNodeContain(*aTextNode.GetParentNode(), + *nsGkAtoms::big)) { + return NS_OK; + } + + // -1 is a magic value meaning to the end of node + if (aEndOffset == -1) { + aEndOffset = aTextNode.Length(); + } + + // Make the range an independent node. + nsCOMPtr<nsIContent> textNodeForTheRange = &aTextNode; + + // 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 (error.Failed()) { + NS_WARNING("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 (error.Failed()) { + NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); + return error.StealNSResult(); + } + Unused << newLeftNode; + } + + // Look for siblings that are correct type of node + nsAtom* nodeType = aDir == FontSize::incr ? nsGkAtoms::big : nsGkAtoms::small; + nsCOMPtr<nsIContent> sibling = GetPriorHTMLSibling(textNodeForTheRange); + if (sibling && sibling->IsHTMLElement(nodeType)) { + // Previous sib is already right kind of inline node; slide this over + nsresult rv = MoveNodeToEndWithTransaction(*textNodeForTheRange, *sibling); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::MoveNodeToEndWithTransaction() failed"); + return rv; + } + sibling = GetNextHTMLSibling(textNodeForTheRange); + if (sibling && sibling->IsHTMLElement(nodeType)) { + // Following sib is already right kind of inline node; slide this over + nsresult rv = MoveNodeWithTransaction(*textNodeForTheRange, + EditorDOMPoint(sibling, 0)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::MoveNodeWithTransaction() failed"); + return rv; + } + + // Else reparent the node inside font node with appropriate relative size + RefPtr<Element> newElement = InsertContainerWithTransaction( + *textNodeForTheRange, MOZ_KnownLive(*nodeType)); + NS_WARNING_ASSERTION(newElement, + "HTMLEditor::InsertContainerWithTransaction() failed"); + return newElement ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult HTMLEditor::RelativeFontChangeHelper(int32_t aSizeChange, + nsINode* aNode) { + MOZ_ASSERT(aNode); + + /* This routine looks for all the font nodes in the tree rooted by aNode, + including aNode itself, looking for font nodes that have the size attr + set. Any such nodes need to have big or small put inside them, since + they override any big/small that are above them. + */ + + // Can only change font size by + or - 1 + if (aSizeChange != 1 && aSizeChange != -1) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // If this is a font node with size, put big/small inside it. + if (aNode->IsHTMLElement(nsGkAtoms::font) && + aNode->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::size)) { + // Cycle through children and adjust relative font size. + AutoTArray<nsCOMPtr<nsIContent>, 10> childList; + for (nsIContent* child = aNode->GetFirstChild(); child; + child = child->GetNextSibling()) { + childList.AppendElement(child); + } + + for (const auto& child : childList) { + // MOZ_KnownLive because 'childList' is guaranteed to + // keep it alive. + nsresult rv = RelativeFontChangeOnNode(aSizeChange, MOZ_KnownLive(child)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RelativeFontChangeOnNode() failed"); + return rv; + } + } + + // RelativeFontChangeOnNode already calls us recursively, + // so we don't need to check our children again. + return NS_OK; + } + + // Otherwise cycle through the children. + AutoTArray<nsCOMPtr<nsIContent>, 10> childList; + for (nsIContent* child = aNode->GetFirstChild(); child; + child = child->GetNextSibling()) { + childList.AppendElement(child); + } + + for (const auto& child : childList) { + // MOZ_KnownLive because 'childList' is guaranteed to + // keep it alive. + nsresult rv = RelativeFontChangeHelper(aSizeChange, MOZ_KnownLive(child)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RelativeFontChangeHelper() failed"); + return rv; + } + } + + return NS_OK; +} + +nsresult HTMLEditor::RelativeFontChangeOnNode(int32_t aSizeChange, + nsIContent* aNode) { + MOZ_ASSERT(aNode); + // Can only change font size by + or - 1 + if (aSizeChange != 1 && aSizeChange != -1) { + return NS_ERROR_ILLEGAL_VALUE; + } + + nsAtom* atom; + if (aSizeChange == 1) { + atom = nsGkAtoms::big; + } else { + atom = nsGkAtoms::small; + } + + // Is it the opposite of what we want? + if ((aSizeChange == 1 && aNode->IsHTMLElement(nsGkAtoms::small)) || + (aSizeChange == -1 && aNode->IsHTMLElement(nsGkAtoms::big))) { + // first populate any nested font tags that have the size attr set + nsresult rv = RelativeFontChangeHelper(aSizeChange, aNode); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RelativeFontChangeHelper() failed"); + return rv; + } + // in that case, just remove this node and pull up the children + rv = RemoveContainerWithTransaction(MOZ_KnownLive(*aNode->AsElement())); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::RemoveContainerWithTransaction() failed"); + return rv; + } + + // can it be put inside a "big" or "small"? + if (HTMLEditUtils::CanNodeContain(*atom, *aNode)) { + // first populate any nested font tags that have the size attr set + nsresult rv = RelativeFontChangeHelper(aSizeChange, aNode); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RelativeFontChangeHelper() failed"); + return rv; + } + + // ok, chuck it in. + // first look at siblings of aNode for matching bigs or smalls. + // if we find one, move aNode into it. + nsCOMPtr<nsIContent> sibling = GetPriorHTMLSibling(aNode); + if (sibling && sibling->IsHTMLElement(atom)) { + // previous sib is already right kind of inline node; slide this over into + // it + nsresult rv = MoveNodeToEndWithTransaction(*aNode, *sibling); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "HTMLEditor::MoveNodeToEndWithTransaction() failed"); + return rv; + } + + sibling = GetNextHTMLSibling(aNode); + if (sibling && sibling->IsHTMLElement(atom)) { + // following sib is already right kind of inline node; slide this over + // into it + return MoveNodeWithTransaction(*aNode, EditorDOMPoint(sibling, 0)); + } + + // else insert it above aNode + RefPtr<Element> newElement = + InsertContainerWithTransaction(*aNode, MOZ_KnownLive(*atom)); + NS_WARNING_ASSERTION(newElement, + "HTMLEditor::InsertContainerWithTransaction() failed"); + return newElement ? NS_OK : NS_ERROR_FAILURE; + } + + // none of the above? then cycle through the children. + // MOOSE: we should group the children together if possible + // into a single "big" or "small". For the moment they are + // each getting their own. + AutoTArray<nsCOMPtr<nsIContent>, 10> childList; + for (nsIContent* child = aNode->GetFirstChild(); child; + child = child->GetNextSibling()) { + childList.AppendElement(child); + } + + for (const auto& child : childList) { + // MOZ_KnownLive because 'childList' is guaranteed to + // keep it alive. + nsresult rv = RelativeFontChangeOnNode(aSizeChange, MOZ_KnownLive(child)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::RelativeFontChangeOnNode() failed"); + return rv; + } + } + + return NS_OK; +} + +NS_IMETHODIMP HTMLEditor::GetFontFaceState(bool* aMixed, nsAString& outFace) { + if (NS_WARN_IF(!aMixed)) { + return NS_ERROR_INVALID_ARG; + } + + *aMixed = true; + outFace.Truncate(); + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + bool first, any, all; + + nsresult rv = GetInlinePropertyBase(*nsGkAtoms::font, nsGkAtoms::face, + nullptr, &first, &any, &all, &outFace); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::GetInlinePropertyBase(nsGkAtoms::font, nsGkAtoms::face) " + "failed"); + return EditorBase::ToGenericNSResult(rv); + } + if (any && !all) { + return NS_OK; // mixed + } + if (all) { + *aMixed = false; + return NS_OK; + } + + // if there is no font face, check for tt + rv = GetInlinePropertyBase(*nsGkAtoms::tt, nullptr, nullptr, &first, &any, + &all, nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetInlinePropertyBase(nsGkAtoms::tt) failed"); + return EditorBase::ToGenericNSResult(rv); + } + if (any && !all) { + return NS_OK; // mixed + } + if (all) { + *aMixed = false; + outFace.AssignLiteral("tt"); + } + + if (!any) { + // there was no font face attrs of any kind. We are in normal font. + outFace.Truncate(); + *aMixed = false; + } + return NS_OK; +} + +nsresult HTMLEditor::GetFontColorState(bool* aMixed, nsAString& aOutColor) { + if (NS_WARN_IF(!aMixed)) { + return NS_ERROR_INVALID_ARG; + } + + *aMixed = true; + aOutColor.Truncate(); + + AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing); + if (NS_WARN_IF(!editActionData.CanHandle())) { + return NS_ERROR_NOT_INITIALIZED; + } + + bool first, any, all; + nsresult rv = GetInlinePropertyBase(*nsGkAtoms::font, nsGkAtoms::color, + nullptr, &first, &any, &all, &aOutColor); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::GetInlinePropertyBase(nsGkAtoms::font, nsGkAtoms::color) " + "failed"); + return EditorBase::ToGenericNSResult(rv); + } + + if (any && !all) { + return NS_OK; // mixed + } + if (all) { + *aMixed = false; + return NS_OK; + } + + if (!any) { + // there was no font color attrs of any kind.. + aOutColor.Truncate(); + *aMixed = false; + } + return NS_OK; +} + +// the return value is true only if the instance of the HTML editor we created +// can handle CSS styles (for instance, Composer can, Messenger can't) and if +// the CSS preference is checked +NS_IMETHODIMP HTMLEditor::GetIsCSSEnabled(bool* aIsCSSEnabled) { + *aIsCSSEnabled = IsCSSEnabled(); + return NS_OK; +} + +bool HTMLEditor::HasStyleOrIdOrClassAttribute(Element& aElement) { + return aElement.HasNonEmptyAttr(nsGkAtoms::style) || + aElement.HasNonEmptyAttr(nsGkAtoms::_class) || + aElement.HasNonEmptyAttr(nsGkAtoms::id); +} + +} // namespace mozilla |