/* -*- 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 "ErrorList.h" #include "HTMLEditor.h" #include "HTMLEditorInlines.h" #include "HTMLEditorNestedClasses.h" #include "AutoRangeArray.h" #include "CSSEditUtils.h" #include "EditAction.h" #include "EditorUtils.h" #include "HTMLEditHelpers.h" #include "HTMLEditUtils.h" #include "PendingStyles.h" #include "SelectionState.h" #include "WSRunObject.h" #include "mozilla/Assertions.h" #include "mozilla/ContentIterator.h" #include "mozilla/EditorForwards.h" #include "mozilla/mozalloc.h" #include "mozilla/SelectionState.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/NameSpaceConstants.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/Text.h" #include "nsAString.h" #include "nsAtom.h" #include "nsAttrName.h" #include "nsAttrValue.h" #include "nsCaseTreatment.h" #include "nsColor.h" #include "nsComponentManagerUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsGkAtoms.h" #include "nsIContent.h" #include "nsINode.h" #include "nsIPrincipal.h" #include "nsISupportsImpl.h" #include "nsLiteralString.h" #include "nsNameSpaceManager.h" #include "nsRange.h" #include "nsReadableUtils.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsStyledElement.h" #include "nsTArray.h" #include "nsTextNode.h" #include "nsUnicharUtils.h" namespace mozilla { using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using LeafNodeType = HTMLEditUtils::LeafNodeType; using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; template nsresult HTMLEditor::SetInlinePropertiesAsSubAction( const AutoTArray<EditorInlineStyleAndValue, 1>& aStylesToSet); template nsresult HTMLEditor::SetInlinePropertiesAsSubAction( const AutoTArray<EditorInlineStyleAndValue, 32>& aStylesToSet); template nsresult HTMLEditor::SetInlinePropertiesAroundRanges( AutoRangeArray& aRanges, const AutoTArray<EditorInlineStyleAndValue, 1>& aStylesToSet, const Element& aEditingHost); template nsresult HTMLEditor::SetInlinePropertiesAroundRanges( AutoRangeArray& aRanges, const AutoTArray<EditorInlineStyleAndValue, 32>& aStylesToSet, const Element& aEditingHost); nsresult HTMLEditor::SetInlinePropertyAsAction(nsStaticAtom& aProperty, nsStaticAtom* 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, __FUNCTION__); nsStaticAtom* property = &aProperty; nsStaticAtom* attribute = aAttribute; nsString value(aValue); if (attribute == nsGkAtoms::color || attribute == nsGkAtoms::bgcolor) { if (!IsCSSEnabled()) { // We allow CSS style color value even in the HTML mode. In the cases, // we will apply the style with CSS. For considering it in the value // as-is if it's a known CSS keyboard, `rgb()` or `rgba()` style. // NOTE: It may be later that we set the color into the DOM tree and at // that time, IsCSSEnabled() may return true. E.g., setting color value // to collapsed selection, then, change the CSS enabled, finally, user // types text there. if (!HTMLEditUtils::MaybeCSSSpecificColorValue(value)) { HTMLEditUtils::GetNormalizedHTMLColorValue(value, value); } } else { HTMLEditUtils::GetNormalizedCSSColorValue( value, HTMLEditUtils::ZeroAlphaColor::RGBAValue, value); } } AutoTArray<EditorInlineStyle, 1> stylesToRemove; if (&aProperty == nsGkAtoms::sup) { // Superscript and Subscript styles are mutually exclusive. stylesToRemove.AppendElement(EditorInlineStyle(*nsGkAtoms::sub)); } else if (&aProperty == nsGkAtoms::sub) { // Superscript and Subscript styles are mutually exclusive. stylesToRemove.AppendElement(EditorInlineStyle(*nsGkAtoms::sup)); } // 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) { stylesToRemove.AppendElement( EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::face)); } else if (&aProperty == nsGkAtoms::font && aAttribute == nsGkAtoms::face) { if (!value.LowerCaseEqualsASCII("tt")) { stylesToRemove.AppendElement(EditorInlineStyle(*nsGkAtoms::tt)); } else { stylesToRemove.AppendElement( EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::face)); // Override property, attribute and value if the new font face value is // "tt". property = nsGkAtoms::tt; attribute = nullptr; value.Truncate(); } } } if (!stylesToRemove.IsEmpty()) { nsresult rv = RemoveInlinePropertiesAsSubAction(stylesToRemove); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::RemoveInlinePropertiesAsSubAction() failed"); return rv; } } AutoTArray<EditorInlineStyleAndValue, 1> styleToSet; styleToSet.AppendElement( attribute ? EditorInlineStyleAndValue(*property, *attribute, std::move(value)) : EditorInlineStyleAndValue(*property)); rv = SetInlinePropertiesAsSubAction(styleToSet); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::SetInlinePropertiesAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } NS_IMETHODIMP HTMLEditor::SetInlineProperty(const nsAString& aProperty, const nsAString& aAttribute, const nsAString& aValue) { nsStaticAtom* property = NS_GetStaticAtom(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); } AutoTArray<EditorInlineStyleAndValue, 1> styleToSet; styleToSet.AppendElement( attribute ? EditorInlineStyleAndValue(*property, *attribute, aValue) : EditorInlineStyleAndValue(*property)); rv = SetInlinePropertiesAsSubAction(styleToSet); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::SetInlinePropertiesAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } template <size_t N> nsresult HTMLEditor::SetInlinePropertiesAsSubAction( const AutoTArray<EditorInlineStyleAndValue, N>& aStylesToSet) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!aStylesToSet.IsEmpty()); DebugOnly<nsresult> rvIgnored = CommitComposition(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "EditorBase::CommitComposition() failed, but ignored"); if (SelectionRef().IsCollapsed()) { // Manipulating text attributes on a collapsed selection only sets state // for the next text insertion mPendingStylesToApplyToNewContent->PreserveStyles(aStylesToSet); return NS_OK; } // XXX Shouldn't we return before calling `CommitComposition()`? if (IsPlaintextMailComposer()) { return NS_OK; } { Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } RefPtr<Element> const editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { return NS_ERROR_FAILURE; } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); 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"); // TODO: We don't need AutoTransactionsConserveSelection here in the normal // cases, but removing this may cause the behavior with the legacy // mutation event listeners. We should try to delete this in a bug. AutoTransactionsConserveSelection dontChangeMySelection(*this); AutoRangeArray selectionRanges(SelectionRef()); nsresult rv = SetInlinePropertiesAroundRanges(selectionRanges, aStylesToSet, *editingHost); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::SetInlinePropertiesAroundRanges() failed"); return rv; } MOZ_ASSERT(!selectionRanges.HasSavedRanges()); rv = selectionRanges.ApplyTo(SelectionRef()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed"); return rv; } template <size_t N> nsresult HTMLEditor::SetInlinePropertiesAroundRanges( AutoRangeArray& aRanges, const AutoTArray<EditorInlineStyleAndValue, N>& aStylesToSet, const Element& aEditingHost) { MOZ_ASSERT(!aRanges.HasSavedRanges()); for (const EditorInlineStyleAndValue& styleToSet : aStylesToSet) { AutoInlineStyleSetter inlineStyleSetter(styleToSet); for (OwningNonNull<nsRange>& domRange : aRanges.Ranges()) { inlineStyleSetter.Reset(); auto rangeOrError = [&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMRange, nsresult> { EditorDOMRange range(domRange); // If we're setting <font>, we want to remove ancestors which set // `font-size` or <font size="..."> recursively. Therefore, for // extending the ranges to contain all ancestors in the range, we need // to split ancestors first. // XXX: Blink and WebKit inserts <font> elements to inner most // elements, however, we cannot do it under current design because // once we contain ancestors which have `font-size` or are // <font size="...">, we lost the original ranges which we wanted to // apply the style. For fixing this, we need to manage both ranges, but // it's too expensive especially we allow to run script when we touch // the DOM tree. Additionally, font-size value affects the height // of the element, but does not affect the height of ancestor inline // elements. Therefore, following the behavior may cause similar issue // as bug 1808906. So at least for now, we should not do this big work. if (styleToSet.IsStyleOfFontElement()) { Result<SplitRangeOffResult, nsresult> splitAncestorsResult = SplitAncestorStyledInlineElementsAtRangeEdges( range, styleToSet, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(splitAncestorsResult.isErr())) { NS_WARNING( "HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges() " "failed"); return splitAncestorsResult.propagateErr(); } SplitRangeOffResult unwrappedResult = splitAncestorsResult.unwrap(); unwrappedResult.IgnoreCaretPointSuggestion(); range = unwrappedResult.RangeRef(); if (NS_WARN_IF(!range.IsPositionedAndValid())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } Result<EditorRawDOMRange, nsresult> rangeOrError = inlineStyleSetter.ExtendOrShrinkRangeToApplyTheStyle(*this, range, aEditingHost); if (MOZ_UNLIKELY(rangeOrError.isErr())) { NS_WARNING( "HTMLEditor::ExtendOrShrinkRangeToApplyTheStyle() failed, but " "ignored"); return EditorDOMRange(); } return EditorDOMRange(rangeOrError.unwrap()); }(); if (MOZ_UNLIKELY(rangeOrError.isErr())) { return rangeOrError.unwrapErr(); } const EditorDOMRange range = rangeOrError.unwrap(); if (!range.IsPositioned()) { continue; } // If the range is collapsed, we should insert new element there. if (range.Collapsed()) { Result<RefPtr<Text>, nsresult> emptyTextNodeOrError = AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle( *this, range.StartRef(), aEditingHost); if (MOZ_UNLIKELY(emptyTextNodeOrError.isErr())) { NS_WARNING( "AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle() " "failed"); return emptyTextNodeOrError.unwrapErr(); } if (MOZ_UNLIKELY(!emptyTextNodeOrError.inspect())) { continue; // Couldn't insert text node there } RefPtr<Text> emptyTextNode = emptyTextNodeOrError.unwrap(); Result<CaretPoint, nsresult> caretPointOrError = inlineStyleSetter .ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle( *this, *emptyTextNode); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "AutoInlineStyleSetter::" "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed"); return caretPointOrError.unwrapErr(); } DebugOnly<nsresult> rvIgnored = domRange->CollapseTo(emptyTextNode, 0); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "nsRange::CollapseTo() failed, but ignored"); continue; } // Use const_cast hack here for preventing the others to update the range. AutoTrackDOMRange trackRange(RangeUpdaterRef(), const_cast<EditorDOMRange*>(&range)); auto UpdateSelectionRange = [&]() MOZ_CAN_RUN_SCRIPT { // If inlineStyleSetter creates elements or setting styles, we should // select between start of first element and end of last element. if (inlineStyleSetter.FirstHandledPointRef().IsInContentNode()) { MOZ_ASSERT(inlineStyleSetter.LastHandledPointRef().IsInContentNode()); const auto startPoint = !inlineStyleSetter.FirstHandledPointRef().IsStartOfContainer() ? inlineStyleSetter.FirstHandledPointRef() .To<EditorRawDOMPoint>() : HTMLEditUtils::GetDeepestEditableStartPointOf< EditorRawDOMPoint>( *inlineStyleSetter.FirstHandledPointRef() .ContainerAs<nsIContent>()); const auto endPoint = !inlineStyleSetter.LastHandledPointRef().IsEndOfContainer() ? inlineStyleSetter.LastHandledPointRef() .To<EditorRawDOMPoint>() : HTMLEditUtils::GetDeepestEditableEndPointOf< EditorRawDOMPoint>( *inlineStyleSetter.LastHandledPointRef() .ContainerAs<nsIContent>()); nsresult rv = domRange->SetStartAndEnd( startPoint.ToRawRangeBoundary(), endPoint.ToRawRangeBoundary()); if (NS_SUCCEEDED(rv)) { trackRange.StopTracking(); return; } } // Otherwise, use the range computed with the tracking original range. trackRange.FlushAndStopTracking(); domRange->SetStartAndEnd(range.StartRef().ToRawRangeBoundary(), range.EndRef().ToRawRangeBoundary()); }; // If range is in a text node, apply new style simply. if (range.InSameContainer() && range.StartRef().IsInTextNode()) { // MOZ_KnownLive(...ContainerAs<Text>()) because of grabbed by `range`. // MOZ_KnownLive(styleToSet.*) due to bug 1622253. Result<SplitRangeOffFromNodeResult, nsresult> wrapTextInStyledElementResult = inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode( *this, MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()), range.StartRef().Offset(), range.EndRef().Offset()); if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) { NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed"); return wrapTextInStyledElementResult.unwrapErr(); } // The caller should handle the ranges as Selection if necessary, and we // don't want to update aRanges with this result. wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion(); UpdateSelectionRange(); continue; } // Collect editable nodes which are entirely contained in the range. AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContentsAroundRange; { 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.StartRef().ToRawRangeBoundary(), range.EndRef().ToRawRangeBoundary()))) { for (; !subtreeIter.IsDone(); subtreeIter.Next()) { nsINode* node = subtreeIter.GetCurrentNode(); if (NS_WARN_IF(!node)) { return NS_ERROR_FAILURE; } if (MOZ_UNLIKELY(!node->IsContent())) { continue; } // We don't need to wrap non-editable node in new inline element // nor shouldn't modify `style` attribute of non-editable element. if (!EditorUtils::IsEditableContent(*node->AsContent(), EditorType::HTML)) { continue; } // We shouldn't wrap invisible text node in new inline element. if (node->IsText() && !HTMLEditUtils::IsVisibleTextNode(*node->AsText())) { continue; } arrayOfContentsAroundRange.AppendElement(*node->AsContent()); } } } // If start node is a text node, apply new style to a part of it. if (range.StartRef().IsInTextNode() && EditorUtils::IsEditableContent(*range.StartRef().ContainerAs<Text>(), EditorType::HTML)) { // MOZ_KnownLive(...ContainerAs<Text>()) because of grabbed by `range`. // MOZ_KnownLive(styleToSet.*) due to bug 1622253. Result<SplitRangeOffFromNodeResult, nsresult> wrapTextInStyledElementResult = inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode( *this, MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()), range.StartRef().Offset(), range.StartRef().ContainerAs<Text>()->TextDataLength()); if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) { NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed"); return wrapTextInStyledElementResult.unwrapErr(); } // The caller should handle the ranges as Selection if necessary, and we // don't want to update aRanges with this result. wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion(); } // Then, apply new style to all nodes in the range entirely. for (auto& content : arrayOfContentsAroundRange) { // MOZ_KnownLive due to bug 1622253. Result<CaretPoint, nsresult> pointToPutCaretOrError = inlineStyleSetter .ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle( *this, MOZ_KnownLive(*content)); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING( "AutoInlineStyleSetter::" "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed"); return pointToPutCaretOrError.unwrapErr(); } // The caller should handle the ranges as Selection if necessary, and we // don't want to update aRanges with this result. pointToPutCaretOrError.inspect().IgnoreCaretPointSuggestion(); } // Finally, if end node is a text node, apply new style to a part of it. if (range.EndRef().IsInTextNode() && EditorUtils::IsEditableContent(*range.EndRef().ContainerAs<Text>(), EditorType::HTML)) { // MOZ_KnownLive(...ContainerAs<Text>()) because of grabbed by `range`. // MOZ_KnownLive(styleToSet.mAttribute) due to bug 1622253. Result<SplitRangeOffFromNodeResult, nsresult> wrapTextInStyledElementResult = inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode( *this, MOZ_KnownLive(*range.EndRef().ContainerAs<Text>()), 0, range.EndRef().Offset()); if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) { NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed"); return wrapTextInStyledElementResult.unwrapErr(); } // The caller should handle the ranges as Selection if necessary, and we // don't want to update aRanges with this result. wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion(); } UpdateSelectionRange(); } } return NS_OK; } // static Result<RefPtr<Text>, nsresult> HTMLEditor::AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCandidatePointToInsert, const Element& aEditingHost) { auto pointToInsertNewText = HTMLEditUtils::GetBetterCaretPositionToInsertText<EditorDOMPoint>( aCandidatePointToInsert, aEditingHost); if (MOZ_UNLIKELY(!pointToInsertNewText.IsSet())) { return RefPtr<Text>(); // cannot insert text there } auto pointToInsertNewStyleOrError = [&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMPoint, nsresult> { if (!pointToInsertNewText.IsInTextNode()) { return pointToInsertNewText; } if (!pointToInsertNewText.ContainerAs<Text>()->TextDataLength()) { return pointToInsertNewText; // Use it } if (pointToInsertNewText.IsStartOfContainer()) { return pointToInsertNewText.ParentPoint(); } if (pointToInsertNewText.IsEndOfContainer()) { return EditorDOMPoint::After(*pointToInsertNewText.ContainerAs<Text>()); } Result<SplitNodeResult, nsresult> splitTextNodeResult = aHTMLEditor.SplitNodeWithTransaction(pointToInsertNewText); if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return splitTextNodeResult.propagateErr(); } SplitNodeResult unwrappedSplitTextNodeResult = splitTextNodeResult.unwrap(); unwrappedSplitTextNodeResult.IgnoreCaretPointSuggestion(); return unwrappedSplitTextNodeResult.AtSplitPoint<EditorDOMPoint>(); }(); if (MOZ_UNLIKELY(pointToInsertNewStyleOrError.isErr())) { return pointToInsertNewStyleOrError.propagateErr(); } // If we already have empty text node which is available for placeholder in // new styled element, let's use it. if (pointToInsertNewStyleOrError.inspect().IsInTextNode()) { return RefPtr<Text>( pointToInsertNewStyleOrError.inspect().ContainerAs<Text>()); } // Otherwise, we need an empty text node to create new inline style. RefPtr<Text> newEmptyTextNode = aHTMLEditor.CreateTextNode(u""_ns); if (MOZ_UNLIKELY(!newEmptyTextNode)) { NS_WARNING("EditorBase::CreateTextNode() failed"); return Err(NS_ERROR_FAILURE); } Result<CreateTextResult, nsresult> insertNewTextNodeResult = aHTMLEditor.InsertNodeWithTransaction<Text>( *newEmptyTextNode, pointToInsertNewStyleOrError.inspect()); if (MOZ_UNLIKELY(insertNewTextNodeResult.isErr())) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertNewTextNodeResult.propagateErr(); } insertNewTextNodeResult.inspect().IgnoreCaretPointSuggestion(); return newEmptyTextNode; } Result<bool, nsresult> HTMLEditor::AutoInlineStyleSetter::ElementIsGoodContainerForTheStyle( HTMLEditor& aHTMLEditor, Element& aElement) const { // If the editor is in the CSS mode and the style can be specified with CSS, // we should not use existing HTML element as a new container. const bool isCSSEditable = IsCSSSettable(aElement); if (!aHTMLEditor.IsCSSEnabled() || !isCSSEditable) { // First check for <b>, <i>, etc. if (aElement.IsHTMLElement(&HTMLPropertyRef()) && !HTMLEditUtils::ElementHasAttribute(aElement) && !mAttribute) { return true; } // Now look for things like <font> if (mAttribute) { nsString attrValue; if (aElement.IsHTMLElement(&HTMLPropertyRef()) && !HTMLEditUtils::ElementHasAttributeExcept(aElement, *mAttribute) && aElement.GetAttr(mAttribute, attrValue)) { if (attrValue.Equals(mAttributeValue, nsCaseInsensitiveStringComparator)) { return true; } if (mAttribute == nsGkAtoms::color || mAttribute == nsGkAtoms::bgcolor) { if (aHTMLEditor.IsCSSEnabled()) { if (HTMLEditUtils::IsSameCSSColorValue(mAttributeValue, attrValue)) { return true; } } else if (HTMLEditUtils::IsSameHTMLColorValue( mAttributeValue, attrValue, HTMLEditUtils::TransparentKeyword::Allowed)) { return true; } } } } if (!isCSSEditable) { return false; } } // 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 (!aElement.IsHTMLElement(nsGkAtoms::span) || !aElement.HasAttr(nsGkAtoms::style) || HTMLEditUtils::ElementHasAttributeExcept(aElement, *nsGkAtoms::style)) { return false; } nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement); if (MOZ_UNLIKELY(!styledElement)) { 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 = aHTMLEditor.CreateHTMLContent(nsGkAtoms::span); if (MOZ_UNLIKELY(!newSpanElement)) { NS_WARNING("EditorBase::CreateHTMLContent(nsGkAtoms::span) failed"); return false; } nsStyledElement* styledNewSpanElement = nsStyledElement::FromNode(newSpanElement); if (MOZ_UNLIKELY(!styledNewSpanElement)) { return false; } // MOZ_KnownLive(*styledNewSpanElement): It's newSpanElement whose type is // RefPtr. Result<size_t, nsresult> result = CSSEditUtils::SetCSSEquivalentToStyle( WithTransaction::No, aHTMLEditor, MOZ_KnownLive(*styledNewSpanElement), *this, &mAttributeValue); if (MOZ_UNLIKELY(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; } return CSSEditUtils::DoStyledElementsHaveSameStyle(*styledNewSpanElement, *styledElement); } bool HTMLEditor::AutoInlineStyleSetter::ElementIsGoodContainerToSetStyle( nsStyledElement& aStyledElement) const { if (!HTMLEditUtils::IsContainerNode(aStyledElement) || !EditorUtils::IsEditableContent(aStyledElement, EditorType::HTML)) { return false; } // If it has `style` attribute, let's use it. if (aStyledElement.HasAttr(nsGkAtoms::style)) { return true; } // If it has `class` or `id` attribute, the element may have specific rule. // For applying the new style, we may need to set `style` attribute to it // to override the specified rule. if (aStyledElement.HasAttr(nsGkAtoms::id) || aStyledElement.HasAttr(nsGkAtoms::_class)) { return true; } // If we're setting text-decoration and the element represents a value of // text-decoration, <ins> or <del>, let's use it. if (IsStyleOfTextDecoration(IgnoreSElement::No) && aStyledElement.IsAnyOfHTMLElements(nsGkAtoms::u, nsGkAtoms::s, nsGkAtoms::strike, nsGkAtoms::ins, nsGkAtoms::del)) { return true; } // If we're setting font-size, color or background-color, we should use <font> // for compatibility with the other browsers. if (&HTMLPropertyRef() == nsGkAtoms::font && aStyledElement.IsHTMLElement(nsGkAtoms::font)) { return true; } // If the element has one or more <br> (even if it's invisible), we don't // want to use the <span> for compatibility with the other browsers. if (aStyledElement.QuerySelector("br"_ns, IgnoreErrors())) { return false; } // NOTE: The following code does not match with the other browsers not // completely. Blink considers this with relation with the range. // However, we cannot do it now. We should fix here after or at // fixing bug 1792386. // If it's only visible element child of parent block, let's use it. // E.g., we don't want to create new <span> when // `<p>{ <span>abc</span> }</p>`. if (aStyledElement.GetParentElement() && HTMLEditUtils::IsBlockElement( *aStyledElement.GetParentElement(), BlockInlineCheck::UseComputedDisplayStyle)) { for (nsIContent* previousSibling = aStyledElement.GetPreviousSibling(); previousSibling; previousSibling = previousSibling->GetPreviousSibling()) { if (previousSibling->IsElement()) { return false; // Assume any elements visible. } if (Text* text = Text::FromNode(previousSibling)) { if (HTMLEditUtils::IsVisibleTextNode(*text)) { return false; } continue; } } for (nsIContent* nextSibling = aStyledElement.GetNextSibling(); nextSibling; nextSibling = nextSibling->GetNextSibling()) { if (nextSibling->IsElement()) { if (!HTMLEditUtils::IsInvisibleBRElement(*nextSibling)) { return false; } continue; // The invisible <br> element may be followed by a child // block, let's continue to check it. } if (Text* text = Text::FromNode(nextSibling)) { if (HTMLEditUtils::IsVisibleTextNode(*text)) { return false; } continue; } } return true; } // Otherwise, wrap it into new <span> for making // `<span>[abc</span> <span>def]</span>` become // `<span style="..."><span>abc</span> <span>def</span></span>` rather // than `<span style="...">abc <span>def</span></span>`. return false; } Result<SplitRangeOffFromNodeResult, nsresult> HTMLEditor::AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode( HTMLEditor& aHTMLEditor, Text& aText, uint32_t aStartOffset, uint32_t aEndOffset) { const RefPtr<Element> element = aText.GetParentElement(); if (!element || !HTMLEditUtils::CanNodeContain(*element, HTMLPropertyRef())) { OnHandled(EditorDOMPoint(&aText, aStartOffset), EditorDOMPoint(&aText, aEndOffset)); return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr); } // Don't need to do anything if no characters actually selected if (aStartOffset == aEndOffset) { OnHandled(EditorDOMPoint(&aText, aStartOffset), EditorDOMPoint(&aText, aEndOffset)); return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr); } // Don't need to do anything if property already set on node if (IsCSSSettable(*element)) { // The HTML styles defined by this have a CSS equivalence for node; // let's check if it carries those CSS styles nsAutoString value(mAttributeValue); Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError = CSSEditUtils::IsComputedCSSEquivalentTo(aHTMLEditor, *element, *this, value); if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) { NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed"); return isComputedCSSEquivalentToStyleOrError.propagateErr(); } if (isComputedCSSEquivalentToStyleOrError.unwrap()) { OnHandled(EditorDOMPoint(&aText, aStartOffset), EditorDOMPoint(&aText, aEndOffset)); return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr); } } else if (HTMLEditUtils::IsInlineStyleSetByElement(aText, *this, &mAttributeValue)) { OnHandled(EditorDOMPoint(&aText, aStartOffset), EditorDOMPoint(&aText, aEndOffset)); return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr); } // Make the range an independent node. auto splitAtEndResult = [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> { EditorDOMPoint atEnd(&aText, aEndOffset); if (atEnd.IsEndOfContainer()) { return SplitNodeResult::NotHandled(atEnd); } // We need to split off back of text node Result<SplitNodeResult, nsresult> splitNodeResult = aHTMLEditor.SplitNodeWithTransaction(atEnd); if (splitNodeResult.isErr()) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return splitNodeResult; } if (MOZ_UNLIKELY(!splitNodeResult.inspect().HasCaretPointSuggestion())) { NS_WARNING( "HTMLEditor::SplitNodeWithTransaction() didn't suggest caret " "point"); return Err(NS_ERROR_FAILURE); } return splitNodeResult; }(); if (MOZ_UNLIKELY(splitAtEndResult.isErr())) { return splitAtEndResult.propagateErr(); } SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap(); EditorDOMPoint pointToPutCaret = unwrappedSplitAtEndResult.UnwrapCaretPoint(); auto splitAtStartResult = [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> { EditorDOMPoint atStart(unwrappedSplitAtEndResult.DidSplit() ? unwrappedSplitAtEndResult.GetPreviousContent() : &aText, aStartOffset); if (atStart.IsStartOfContainer()) { return SplitNodeResult::NotHandled(atStart); } // We need to split off front of text node Result<SplitNodeResult, nsresult> splitNodeResult = aHTMLEditor.SplitNodeWithTransaction(atStart); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return splitNodeResult; } if (MOZ_UNLIKELY(!splitNodeResult.inspect().HasCaretPointSuggestion())) { NS_WARNING( "HTMLEditor::SplitNodeWithTransaction() didn't suggest caret " "point"); return Err(NS_ERROR_FAILURE); } return splitNodeResult; }(); if (MOZ_UNLIKELY(splitAtStartResult.isErr())) { return splitAtStartResult.propagateErr(); } SplitNodeResult unwrappedSplitAtStartResult = splitAtStartResult.unwrap(); if (unwrappedSplitAtStartResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedSplitAtStartResult.UnwrapCaretPoint(); } MOZ_ASSERT_IF(unwrappedSplitAtStartResult.DidSplit(), unwrappedSplitAtStartResult.GetPreviousContent()->IsText()); MOZ_ASSERT_IF(unwrappedSplitAtStartResult.DidSplit(), unwrappedSplitAtStartResult.GetNextContent()->IsText()); MOZ_ASSERT_IF(unwrappedSplitAtEndResult.DidSplit(), unwrappedSplitAtEndResult.GetPreviousContent()->IsText()); MOZ_ASSERT_IF(unwrappedSplitAtEndResult.DidSplit(), unwrappedSplitAtEndResult.GetNextContent()->IsText()); // Note that those text nodes are grabbed by unwrappedSplitAtStartResult, // unwrappedSplitAtEndResult or the callers. Therefore, we don't need to make // them strong pointer. Text* const leftTextNode = unwrappedSplitAtStartResult.DidSplit() ? unwrappedSplitAtStartResult.GetPreviousContentAs<Text>() : nullptr; Text* const middleTextNode = unwrappedSplitAtStartResult.DidSplit() ? unwrappedSplitAtStartResult.GetNextContentAs<Text>() : (unwrappedSplitAtEndResult.DidSplit() ? unwrappedSplitAtEndResult.GetPreviousContentAs<Text>() : &aText); Text* const rightTextNode = unwrappedSplitAtEndResult.DidSplit() ? unwrappedSplitAtEndResult.GetNextContentAs<Text>() : nullptr; if (mAttribute) { // Look for siblings that are correct type of node nsIContent* sibling = HTMLEditUtils::GetPreviousSibling( *middleTextNode, {WalkTreeOption::IgnoreNonEditableNode}); if (sibling && sibling->IsElement()) { OwningNonNull<Element> element(*sibling->AsElement()); Result<bool, nsresult> result = ElementIsGoodContainerForTheStyle(aHTMLEditor, element); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed"); return result.propagateErr(); } if (result.inspect()) { // Previous sib is already right kind of inline node; slide this over Result<MoveNodeResult, nsresult> moveTextNodeResult = aHTMLEditor.MoveNodeToEndWithTransaction( MOZ_KnownLive(*middleTextNode), element); if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveTextNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveTextNodeResult = moveTextNodeResult.unwrap(); unwrappedMoveTextNodeResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); OnHandled(*middleTextNode); return SplitRangeOffFromNodeResult(leftTextNode, middleTextNode, rightTextNode, std::move(pointToPutCaret)); } } sibling = HTMLEditUtils::GetNextSibling( *middleTextNode, {WalkTreeOption::IgnoreNonEditableNode}); if (sibling && sibling->IsElement()) { OwningNonNull<Element> element(*sibling->AsElement()); Result<bool, nsresult> result = ElementIsGoodContainerForTheStyle(aHTMLEditor, element); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed"); return result.propagateErr(); } if (result.inspect()) { // Following sib is already right kind of inline node; slide this over Result<MoveNodeResult, nsresult> moveTextNodeResult = aHTMLEditor.MoveNodeWithTransaction(MOZ_KnownLive(*middleTextNode), EditorDOMPoint(sibling, 0u)); if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveTextNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveTextNodeResult = moveTextNodeResult.unwrap(); unwrappedMoveTextNodeResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); OnHandled(*middleTextNode); return SplitRangeOffFromNodeResult(leftTextNode, middleTextNode, rightTextNode, std::move(pointToPutCaret)); } } } // Wrap the node inside inline node. Result<CaretPoint, nsresult> setStyleResult = ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle( aHTMLEditor, MOZ_KnownLive(*middleTextNode)); if (MOZ_UNLIKELY(setStyleResult.isErr())) { NS_WARNING( "AutoInlineStyleSetter::" "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed"); return setStyleResult.propagateErr(); } return SplitRangeOffFromNodeResult( leftTextNode, middleTextNode, rightTextNode, setStyleResult.unwrap().UnwrapCaretPoint()); } Result<CaretPoint, nsresult> HTMLEditor::AutoInlineStyleSetter::ApplyStyle( HTMLEditor& aHTMLEditor, nsIContent& aContent) { // 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()) { return CaretPoint(EditorDOMPoint()); } AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents; HTMLEditUtils::CollectChildren( aContent, arrayOfContents, {CollectChildrenOption::IgnoreNonEditableChildren, CollectChildrenOption::IgnoreInvisibleTextNodes}); // Then loop through the list, set the property on each node. EditorDOMPoint pointToPutCaret; for (const OwningNonNull<nsIContent>& content : arrayOfContents) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result<CaretPoint, nsresult> setInlinePropertyResult = ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle( aHTMLEditor, MOZ_KnownLive(content)); if (MOZ_UNLIKELY(setInlinePropertyResult.isErr())) { NS_WARNING( "AutoInlineStyleSetter::" "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed"); return setInlinePropertyResult; } setInlinePropertyResult.unwrap().MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); } return CaretPoint(std::move(pointToPutCaret)); } // First check if there's an adjacent sibling we can put our node into. nsCOMPtr<nsIContent> previousSibling = HTMLEditUtils::GetPreviousSibling( aContent, {WalkTreeOption::IgnoreNonEditableNode}); nsCOMPtr<nsIContent> nextSibling = HTMLEditUtils::GetNextSibling( aContent, {WalkTreeOption::IgnoreNonEditableNode}); if (RefPtr<Element> previousElement = Element::FromNodeOrNull(previousSibling)) { Result<bool, nsresult> canMoveIntoPreviousSibling = ElementIsGoodContainerForTheStyle(aHTMLEditor, *previousElement); if (MOZ_UNLIKELY(canMoveIntoPreviousSibling.isErr())) { NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed"); return canMoveIntoPreviousSibling.propagateErr(); } if (canMoveIntoPreviousSibling.inspect()) { Result<MoveNodeResult, nsresult> moveNodeResult = aHTMLEditor.MoveNodeToEndWithTransaction(aContent, *previousSibling); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); RefPtr<Element> nextElement = Element::FromNodeOrNull(nextSibling); if (!nextElement) { OnHandled(aContent); return CaretPoint(unwrappedMoveNodeResult.UnwrapCaretPoint()); } Result<bool, nsresult> canMoveIntoNextSibling = ElementIsGoodContainerForTheStyle(aHTMLEditor, *nextElement); if (MOZ_UNLIKELY(canMoveIntoNextSibling.isErr())) { NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed"); unwrappedMoveNodeResult.IgnoreCaretPointSuggestion(); return canMoveIntoNextSibling.propagateErr(); } if (!canMoveIntoNextSibling.inspect()) { OnHandled(aContent); return CaretPoint(unwrappedMoveNodeResult.UnwrapCaretPoint()); } unwrappedMoveNodeResult.IgnoreCaretPointSuggestion(); // JoinNodesWithTransaction (DoJoinNodes) tries to collapse selection to // the joined point and we want to skip updating `Selection` here. AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); Result<JoinNodesResult, nsresult> joinNodesResult = aHTMLEditor.JoinNodesWithTransaction(*previousElement, *nextElement); if (MOZ_UNLIKELY(joinNodesResult.isErr())) { NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed"); return joinNodesResult.propagateErr(); } // So, let's take it. OnHandled(aContent); return CaretPoint( joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>()); } } if (RefPtr<Element> nextElement = Element::FromNodeOrNull(nextSibling)) { Result<bool, nsresult> canMoveIntoNextSibling = ElementIsGoodContainerForTheStyle(aHTMLEditor, *nextElement); if (MOZ_UNLIKELY(canMoveIntoNextSibling.isErr())) { NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed"); return canMoveIntoNextSibling.propagateErr(); } if (canMoveIntoNextSibling.inspect()) { Result<MoveNodeResult, nsresult> moveNodeResult = aHTMLEditor.MoveNodeWithTransaction(aContent, EditorDOMPoint(nextElement, 0u)); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveNodeResult.propagateErr(); } OnHandled(aContent); return CaretPoint(moveNodeResult.unwrap().UnwrapCaretPoint()); } } // Don't need to do anything if property already set on node if (const RefPtr<Element> element = aContent.GetAsElementOrParentElement()) { if (IsCSSSettable(*element)) { nsAutoString value(mAttributeValue); // MOZ_KnownLive(element) because it's aContent. Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError = CSSEditUtils::IsComputedCSSEquivalentTo(aHTMLEditor, *element, *this, value); if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) { NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed"); return isComputedCSSEquivalentToStyleOrError.propagateErr(); } if (isComputedCSSEquivalentToStyleOrError.unwrap()) { OnHandled(aContent); return CaretPoint(EditorDOMPoint()); } } else if (HTMLEditUtils::IsInlineStyleSetByElement(*element, *this, &mAttributeValue)) { OnHandled(aContent); return CaretPoint(EditorDOMPoint()); } } auto ShouldUseCSS = [&]() { if (aHTMLEditor.IsCSSEnabled() && aContent.GetAsElementOrParentElement() && IsCSSSettable(*aContent.GetAsElementOrParentElement())) { return true; } // bgcolor is always done using CSS if (mAttribute == nsGkAtoms::bgcolor) { return true; } // called for removing parent style, we should use CSS with <span> element. if (IsStyleToInvert()) { return true; } // If we set color value, the value may be able to specified only with CSS. // In that case, we need to use CSS even in the HTML mode. if (mAttribute == nsGkAtoms::color) { return mAttributeValue.First() != '#' && !HTMLEditUtils::CanConvertToHTMLColorValue(mAttributeValue); } return false; }; if (ShouldUseCSS()) { // We need special handlings for text-decoration. if (IsStyleOfTextDecoration(IgnoreSElement::No)) { Result<CaretPoint, nsresult> result = ApplyCSSTextDecoration(aHTMLEditor, aContent); NS_WARNING_ASSERTION( result.isOk(), "AutoInlineStyleSetter::ApplyCSSTextDecoration() failed"); return result; } EditorDOMPoint pointToPutCaret; RefPtr<nsStyledElement> styledElement = [&]() -> nsStyledElement* { auto* const styledElement = nsStyledElement::FromNode(&aContent); return styledElement && ElementIsGoodContainerToSetStyle(*styledElement) ? styledElement : nullptr; }(); // If we don't have good element to set the style, let's create new <span>. if (!styledElement) { Result<CreateElementResult, nsresult> wrapInSpanElementResult = aHTMLEditor.InsertContainerWithTransaction(aContent, *nsGkAtoms::span); if (MOZ_UNLIKELY(wrapInSpanElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertContainerWithTransaction(nsGkAtoms::span) " "failed"); return wrapInSpanElementResult.propagateErr(); } CreateElementResult unwrappedWrapInSpanElementResult = wrapInSpanElementResult.unwrap(); MOZ_ASSERT(unwrappedWrapInSpanElementResult.GetNewNode()); unwrappedWrapInSpanElementResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); styledElement = nsStyledElement::FromNode( unwrappedWrapInSpanElementResult.GetNewNode()); MOZ_ASSERT(styledElement); if (MOZ_UNLIKELY(!styledElement)) { // Don't return error to avoid creating new path to throwing error. OnHandled(aContent); return CaretPoint(pointToPutCaret); } } // Add the CSS styles corresponding to the HTML style request if (IsCSSSettable(*styledElement)) { Result<size_t, nsresult> result = CSSEditUtils::SetCSSEquivalentToStyle( WithTransaction::Yes, aHTMLEditor, *styledElement, *this, &mAttributeValue); if (MOZ_UNLIKELY(result.isErr())) { if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING( "CSSEditUtils::SetCSSEquivalentToStyle() failed, but ignored"); } } OnHandled(aContent); return CaretPoint(pointToPutCaret); } nsAutoString attributeValue(mAttributeValue); if (mAttribute == nsGkAtoms::color && mAttributeValue.First() != '#') { // At here, all color values should be able to be parsed as a CSS color // value. Therefore, we need to convert it to normalized HTML color value. HTMLEditUtils::ConvertToNormalizedHTMLColorValue(attributeValue, attributeValue); } // is it already the right kind of node, but with wrong attribute? if (aContent.IsHTMLElement(&HTMLPropertyRef())) { if (NS_WARN_IF(!mAttribute)) { return Err(NS_ERROR_INVALID_ARG); } // Just set the attribute on it. nsresult rv = aHTMLEditor.SetAttributeWithTransaction( MOZ_KnownLive(*aContent.AsElement()), *mAttribute, attributeValue); if (NS_WARN_IF(aHTMLEditor.Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("EditorBase::SetAttributeWithTransaction() failed"); return Err(rv); } OnHandled(aContent); return CaretPoint(EditorDOMPoint()); } // ok, chuck it in its very own container Result<CreateElementResult, nsresult> wrapWithNewElementToFormatResult = aHTMLEditor.InsertContainerWithTransaction( aContent, MOZ_KnownLive(HTMLPropertyRef()), !mAttribute ? HTMLEditor::DoNothingForNewElement // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868 : [&](HTMLEditor& aHTMLEditor, Element& aNewElement, const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY { nsresult rv = aNewElement.SetAttr(kNameSpaceID_None, mAttribute, attributeValue, false); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Element::SetAttr() failed"); return rv; }); if (MOZ_UNLIKELY(wrapWithNewElementToFormatResult.isErr())) { NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed"); return wrapWithNewElementToFormatResult.propagateErr(); } OnHandled(aContent); MOZ_ASSERT(wrapWithNewElementToFormatResult.inspect().GetNewNode()); return CaretPoint( wrapWithNewElementToFormatResult.unwrap().UnwrapCaretPoint()); } Result<CaretPoint, nsresult> HTMLEditor::AutoInlineStyleSetter::ApplyCSSTextDecoration( HTMLEditor& aHTMLEditor, nsIContent& aContent) { MOZ_ASSERT(IsStyleOfTextDecoration(IgnoreSElement::No)); EditorDOMPoint pointToPutCaret; RefPtr<nsStyledElement> styledElement = nsStyledElement::FromNode(aContent); nsAutoString newTextDecorationValue; if (&HTMLPropertyRef() == nsGkAtoms::u) { newTextDecorationValue.AssignLiteral(u"underline"); } else if (&HTMLPropertyRef() == nsGkAtoms::s || &HTMLPropertyRef() == nsGkAtoms::strike) { newTextDecorationValue.AssignLiteral(u"line-through"); } else { MOZ_ASSERT_UNREACHABLE( "Was new value added in " "IsStyleOfTextDecoration(IgnoreSElement::No))?"); } if (styledElement && IsCSSSettable(*styledElement) && ElementIsGoodContainerToSetStyle(*styledElement)) { nsAutoString textDecorationValue; nsresult rv = CSSEditUtils::GetSpecifiedProperty( *styledElement, *nsGkAtoms::text_decoration, textDecorationValue); if (NS_FAILED(rv)) { NS_WARNING( "CSSEditUtils::GetSpecifiedProperty(nsGkAtoms::text_decoration) " "failed"); return Err(rv); } // However, if the element is an element to style the text-decoration, // replace it with new <span>. if (styledElement && styledElement->IsAnyOfHTMLElements( nsGkAtoms::u, nsGkAtoms::s, nsGkAtoms::strike)) { Result<CreateElementResult, nsresult> replaceResult = aHTMLEditor.ReplaceContainerAndCloneAttributesWithTransaction( *styledElement, *nsGkAtoms::span); if (MOZ_UNLIKELY(replaceResult.isErr())) { NS_WARNING( "HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction() " "failed"); return replaceResult.propagateErr(); } CreateElementResult unwrappedReplaceResult = replaceResult.unwrap(); MOZ_ASSERT(unwrappedReplaceResult.GetNewNode()); unwrappedReplaceResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); // The new <span> needs to specify the original element's text-decoration // style unless it's specified explicitly. if (textDecorationValue.IsEmpty()) { if (!newTextDecorationValue.IsEmpty()) { newTextDecorationValue.Append(HTMLEditUtils::kSpace); } if (styledElement->IsHTMLElement(nsGkAtoms::u)) { newTextDecorationValue.AppendLiteral(u"underline"); } else { newTextDecorationValue.AppendLiteral(u"line-through"); } } styledElement = nsStyledElement::FromNode(unwrappedReplaceResult.GetNewNode()); if (NS_WARN_IF(!styledElement)) { OnHandled(aContent); return CaretPoint(pointToPutCaret); } } // If the element has default style, we need to keep it after specifying // text-decoration. else if (textDecorationValue.IsEmpty() && styledElement->IsAnyOfHTMLElements(nsGkAtoms::u, nsGkAtoms::ins)) { if (!newTextDecorationValue.IsEmpty()) { newTextDecorationValue.Append(HTMLEditUtils::kSpace); } newTextDecorationValue.AppendLiteral(u"underline"); } else if (textDecorationValue.IsEmpty() && styledElement->IsAnyOfHTMLElements( nsGkAtoms::s, nsGkAtoms::strike, nsGkAtoms::del)) { if (!newTextDecorationValue.IsEmpty()) { newTextDecorationValue.Append(HTMLEditUtils::kSpace); } newTextDecorationValue.AppendLiteral(u"line-through"); } } // Otherwise, use new <span> element. else { Result<CreateElementResult, nsresult> wrapInSpanElementResult = aHTMLEditor.InsertContainerWithTransaction(aContent, *nsGkAtoms::span); if (MOZ_UNLIKELY(wrapInSpanElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertContainerWithTransaction(nsGkAtoms::span) failed"); return wrapInSpanElementResult.propagateErr(); } CreateElementResult unwrappedWrapInSpanElementResult = wrapInSpanElementResult.unwrap(); MOZ_ASSERT(unwrappedWrapInSpanElementResult.GetNewNode()); unwrappedWrapInSpanElementResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); styledElement = nsStyledElement::FromNode( unwrappedWrapInSpanElementResult.GetNewNode()); if (NS_WARN_IF(!styledElement)) { OnHandled(aContent); return CaretPoint(pointToPutCaret); } } nsresult rv = CSSEditUtils::SetCSSPropertyWithTransaction( aHTMLEditor, *styledElement, *nsGkAtoms::text_decoration, newTextDecorationValue); if (NS_FAILED(rv)) { NS_WARNING("CSSEditUtils::SetCSSPropertyWithTransaction() failed"); return Err(rv); } OnHandled(aContent); return CaretPoint(pointToPutCaret); } Result<CaretPoint, nsresult> HTMLEditor::AutoInlineStyleSetter:: ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(HTMLEditor& aHTMLEditor, nsIContent& aContent) { if (NS_WARN_IF(!aContent.GetParentNode())) { return Err(NS_ERROR_FAILURE); } OwningNonNull<nsINode> parent = *aContent.GetParentNode(); nsCOMPtr<nsIContent> previousSibling = aContent.GetPreviousSibling(), nextSibling = aContent.GetNextSibling(); EditorDOMPoint pointToPutCaret; if (aContent.IsElement()) { Result<EditorDOMPoint, nsresult> removeStyleResult = aHTMLEditor.RemoveStyleInside(MOZ_KnownLive(*aContent.AsElement()), *this, SpecifiedStyle::Preserve); if (MOZ_UNLIKELY(removeStyleResult.isErr())) { NS_WARNING("HTMLEditor::RemoveStyleInside() failed"); return removeStyleResult.propagateErr(); } if (removeStyleResult.inspect().IsSet()) { pointToPutCaret = removeStyleResult.unwrap(); } if (nsStaticAtom* similarElementNameAtom = GetSimilarElementNameAtom()) { Result<EditorDOMPoint, nsresult> removeStyleResult = aHTMLEditor.RemoveStyleInside( MOZ_KnownLive(*aContent.AsElement()), EditorInlineStyle(*similarElementNameAtom), SpecifiedStyle::Preserve); if (MOZ_UNLIKELY(removeStyleResult.isErr())) { NS_WARNING("HTMLEditor::RemoveStyleInside() failed"); return removeStyleResult.propagateErr(); } if (removeStyleResult.inspect().IsSet()) { pointToPutCaret = removeStyleResult.unwrap(); } } } if (aContent.GetParentNode()) { // The node is still where it was Result<CaretPoint, nsresult> pointToPutCaretOrError = ApplyStyle(aHTMLEditor, aContent); NS_WARNING_ASSERTION(pointToPutCaretOrError.isOk(), "AutoInlineStyleSetter::ApplyStyle() failed"); return pointToPutCaretOrError; } // 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) || NS_WARN_IF(!parent->IsInComposedDoc())) { return Err(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. Result<CaretPoint, nsresult> pointToPutCaretOrError = ApplyStyle(aHTMLEditor, MOZ_KnownLive(content)); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING("AutoInlineStyleSetter::ApplyStyle() failed"); return pointToPutCaretOrError; } pointToPutCaretOrError.unwrap().MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); } return CaretPoint(pointToPutCaret); } bool HTMLEditor::AutoInlineStyleSetter::ContentIsElementSettingTheStyle( const HTMLEditor& aHTMLEditor, nsIContent& aContent) const { Element* const element = Element::FromNode(&aContent); if (!element) { return false; } if (IsRepresentedBy(*element)) { return true; } Result<bool, nsresult> specified = IsSpecifiedBy(aHTMLEditor, *element); NS_WARNING_ASSERTION(specified.isOk(), "EditorInlineStyle::IsSpecified() failed, but ignored"); return specified.unwrapOr(false); } // static nsIContent* HTMLEditor::AutoInlineStyleSetter::GetNextEditableInlineContent( const nsIContent& aContent, const nsINode* aLimiter) { auto* const nextContentInRange = [&]() -> nsIContent* { for (nsIContent* parent : aContent.InclusiveAncestorsOfType<nsIContent>()) { if (parent == aLimiter || !EditorUtils::IsEditableContent(*parent, EditorType::HTML) || (parent->IsElement() && (HTMLEditUtils::IsBlockElement( *parent->AsElement(), BlockInlineCheck::UseComputedDisplayOutsideStyle) || HTMLEditUtils::IsDisplayInsideFlowRoot(*parent->AsElement())))) { return nullptr; } if (nsIContent* nextSibling = parent->GetNextSibling()) { return nextSibling; } } return nullptr; }(); return nextContentInRange && EditorUtils::IsEditableContent(*nextContentInRange, EditorType::HTML) && !HTMLEditUtils::IsBlockElement( *nextContentInRange, BlockInlineCheck::UseComputedDisplayOutsideStyle) ? nextContentInRange : nullptr; } // static nsIContent* HTMLEditor::AutoInlineStyleSetter::GetPreviousEditableInlineContent( const nsIContent& aContent, const nsINode* aLimiter) { auto* const previousContentInRange = [&]() -> nsIContent* { for (nsIContent* parent : aContent.InclusiveAncestorsOfType<nsIContent>()) { if (parent == aLimiter || !EditorUtils::IsEditableContent(*parent, EditorType::HTML) || (parent->IsElement() && (HTMLEditUtils::IsBlockElement( *parent->AsElement(), BlockInlineCheck::UseComputedDisplayOutsideStyle) || HTMLEditUtils::IsDisplayInsideFlowRoot(*parent->AsElement())))) { return nullptr; } if (nsIContent* previousSibling = parent->GetPreviousSibling()) { return previousSibling; } } return nullptr; }(); return previousContentInRange && EditorUtils::IsEditableContent(*previousContentInRange, EditorType::HTML) && !HTMLEditUtils::IsBlockElement( *previousContentInRange, BlockInlineCheck::UseComputedDisplayOutsideStyle) ? previousContentInRange : nullptr; } EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::GetShrunkenRangeStart( const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange, const nsINode& aCommonAncestorOfRange, const nsIContent* aFirstEntirelySelectedContentNodeInRange) const { const EditorDOMPoint& startRef = aRange.StartRef(); // <a> cannot be nested and it should be represented with one element as far // as possible. Therefore, we don't need to shrink the range. if (IsStyleOfAnchorElement()) { return startRef.To<EditorRawDOMPoint>(); } // If the start boundary is at end of a node, we need to shrink the range // to next content, e.g., `abc[<b>def` should be `abc<b>[def` unless the // <b> is not entirely selected. auto* const nextContentOrStartContainer = [&]() -> nsIContent* { if (!startRef.IsInContentNode()) { return nullptr; } if (!startRef.IsEndOfContainer()) { return startRef.ContainerAs<nsIContent>(); } nsIContent* const nextContent = AutoInlineStyleSetter::GetNextEditableInlineContent( *startRef.ContainerAs<nsIContent>(), &aCommonAncestorOfRange); return nextContent ? nextContent : startRef.ContainerAs<nsIContent>(); }(); if (MOZ_UNLIKELY(!nextContentOrStartContainer)) { return startRef.To<EditorRawDOMPoint>(); } EditorRawDOMPoint startPoint = nextContentOrStartContainer != startRef.ContainerAs<nsIContent>() ? EditorRawDOMPoint(nextContentOrStartContainer) : startRef.To<EditorRawDOMPoint>(); MOZ_ASSERT(startPoint.IsSet()); // If the start point points a content node, let's try to move it down to // start of the child recursively. while (nsIContent* child = startPoint.GetChild()) { // We shouldn't cross editable and block boundary. if (!EditorUtils::IsEditableContent(*child, EditorType::HTML) || HTMLEditUtils::IsBlockElement( *child, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { break; } // If we reach a text node, the minimized range starts from start of it. if (child->IsText()) { startPoint.Set(child, 0u); break; } // Don't shrink the range into element which applies the style to children // because we want to update the element. E.g., if we are setting // background color, we want to update style attribute of an element which // specifies background color with `style` attribute. if (child == aFirstEntirelySelectedContentNodeInRange) { break; } // We should not start from an atomic element such as <br>, <img>, etc. if (!HTMLEditUtils::IsContainerNode(*child)) { break; } // If the element specifies the style, we should update it. Therefore, we // need to wrap it in the range. if (ContentIsElementSettingTheStyle(aHTMLEditor, *child)) { break; } // If the child is an `<a>`, we should not shrink the range into it // because user may not want to keep editing in the link except when user // tries to update selection into it obviously. if (child->IsHTMLElement(nsGkAtoms::a)) { break; } startPoint.Set(child, 0u); } return startPoint; } EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::GetShrunkenRangeEnd( const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange, const nsINode& aCommonAncestorOfRange, const nsIContent* aLastEntirelySelectedContentNodeInRange) const { const EditorDOMPoint& endRef = aRange.EndRef(); // <a> cannot be nested and it should be represented with one element as far // as possible. Therefore, we don't need to shrink the range. if (IsStyleOfAnchorElement()) { return endRef.To<EditorRawDOMPoint>(); } // If the end boundary is at start of a node, we need to shrink the range // to previous content, e.g., `abc</b>]def` should be `abc]</b>def` unless // the <b> is not entirely selected. auto* const previousContentOrEndContainer = [&]() -> nsIContent* { if (!endRef.IsInContentNode()) { return nullptr; } if (!endRef.IsStartOfContainer()) { return endRef.ContainerAs<nsIContent>(); } nsIContent* const previousContent = AutoInlineStyleSetter::GetPreviousEditableInlineContent( *endRef.ContainerAs<nsIContent>(), &aCommonAncestorOfRange); return previousContent ? previousContent : endRef.ContainerAs<nsIContent>(); }(); if (MOZ_UNLIKELY(!previousContentOrEndContainer)) { return endRef.To<EditorRawDOMPoint>(); } EditorRawDOMPoint endPoint = previousContentOrEndContainer != endRef.ContainerAs<nsIContent>() ? EditorRawDOMPoint::After(*previousContentOrEndContainer) : endRef.To<EditorRawDOMPoint>(); MOZ_ASSERT(endPoint.IsSet()); // If the end point points after a content node, let's try to move it down // to end of the child recursively. while (nsIContent* child = endPoint.GetPreviousSiblingOfChild()) { // We shouldn't cross editable and block boundary. if (!EditorUtils::IsEditableContent(*child, EditorType::HTML) || HTMLEditUtils::IsBlockElement( *child, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { break; } // If we reach a text node, the minimized range starts from start of it. if (child->IsText()) { endPoint.SetToEndOf(child); break; } // Don't shrink the range into element which applies the style to children // because we want to update the element. E.g., if we are setting // background color, we want to update style attribute of an element which // specifies background color with `style` attribute. if (child == aLastEntirelySelectedContentNodeInRange) { break; } // We should not end in an atomic element such as <br>, <img>, etc. if (!HTMLEditUtils::IsContainerNode(*child)) { break; } // If the element specifies the style, we should update it. Therefore, we // need to wrap it in the range. if (ContentIsElementSettingTheStyle(aHTMLEditor, *child)) { break; } // If the child is an `<a>`, we should not shrink the range into it // because user may not want to keep editing in the link except when user // tries to update selection into it obviously. if (child->IsHTMLElement(nsGkAtoms::a)) { break; } endPoint.SetToEndOf(child); } return endPoint; } EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter:: GetExtendedRangeStartToWrapAncestorApplyingSameStyle( const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aStartPoint) const { MOZ_ASSERT(aStartPoint.IsSetAndValid()); EditorRawDOMPoint startPoint = aStartPoint; // FIXME: This should handle ignore invisible white-spaces before the position // if it's in a text node or invisible white-spaces. if (!startPoint.IsStartOfContainer() || startPoint.GetContainer()->GetPreviousSibling()) { return startPoint; } // FYI: Currently, we don't support setting `font-size` even in the CSS mode. // Therefore, if the style is <font size="...">, we always set a <font>. const bool isSettingFontElement = IsStyleOfFontSize() || (!aHTMLEditor.IsCSSEnabled() && IsStyleOfFontElement()); Element* mostDistantStartParentHavingStyle = nullptr; for (Element* parent : startPoint.GetContainer()->InclusiveAncestorsOfType<Element>()) { if (!EditorUtils::IsEditableContent(*parent, EditorType::HTML) || HTMLEditUtils::IsBlockElement( *parent, BlockInlineCheck::UseComputedDisplayOutsideStyle) || HTMLEditUtils::IsDisplayInsideFlowRoot(*parent)) { break; } if (ContentIsElementSettingTheStyle(aHTMLEditor, *parent)) { mostDistantStartParentHavingStyle = parent; } // If we're setting <font> element and there is a <font> element which is // entirely selected, we should use it. else if (isSettingFontElement && parent->IsHTMLElement(nsGkAtoms::font)) { mostDistantStartParentHavingStyle = parent; } if (parent->GetPreviousSibling()) { break; // The parent is not first element in its parent, stop climbing. } } if (mostDistantStartParentHavingStyle) { startPoint.Set(mostDistantStartParentHavingStyle); } return startPoint; } EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter:: GetExtendedRangeEndToWrapAncestorApplyingSameStyle( const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aEndPoint) const { MOZ_ASSERT(aEndPoint.IsSetAndValid()); EditorRawDOMPoint endPoint = aEndPoint; // FIXME: This should ignore invisible white-spaces after the position if it's // in a text node, invisible <br> or following invisible text nodes. if (!endPoint.IsEndOfContainer() || endPoint.GetContainer()->GetNextSibling()) { return endPoint; } // FYI: Currently, we don't support setting `font-size` even in the CSS mode. // Therefore, if the style is <font size="...">, we always set a <font>. const bool isSettingFontElement = IsStyleOfFontSize() || (!aHTMLEditor.IsCSSEnabled() && IsStyleOfFontElement()); Element* mostDistantEndParentHavingStyle = nullptr; for (Element* parent : endPoint.GetContainer()->InclusiveAncestorsOfType<Element>()) { if (!EditorUtils::IsEditableContent(*parent, EditorType::HTML) || HTMLEditUtils::IsBlockElement( *parent, BlockInlineCheck::UseComputedDisplayOutsideStyle) || HTMLEditUtils::IsDisplayInsideFlowRoot(*parent)) { break; } if (ContentIsElementSettingTheStyle(aHTMLEditor, *parent)) { mostDistantEndParentHavingStyle = parent; } // If we're setting <font> element and there is a <font> element which is // entirely selected, we should use it. else if (isSettingFontElement && parent->IsHTMLElement(nsGkAtoms::font)) { mostDistantEndParentHavingStyle = parent; } if (parent->GetNextSibling()) { break; // The parent is not last element in its parent, stop climbing. } } if (mostDistantEndParentHavingStyle) { endPoint.SetAfter(mostDistantEndParentHavingStyle); } return endPoint; } EditorRawDOMRange HTMLEditor::AutoInlineStyleSetter:: GetExtendedRangeToMinimizeTheNumberOfNewElements( const HTMLEditor& aHTMLEditor, const nsINode& aCommonAncestor, EditorRawDOMPoint&& aStartPoint, EditorRawDOMPoint&& aEndPoint) const { MOZ_ASSERT(aStartPoint.IsSet()); MOZ_ASSERT(aEndPoint.IsSet()); // For minimizing the number of new elements, we should extend the range as // far as possible. E.g., `<span>[abc</span> <span>def]</span>` should be // styled as `<b><span>abc</span> <span>def</span></b>`. // Similarly, if the range crosses a block boundary, we should do same thing. // I.e., `<p><span>[abc</span></p><p><span>def]</span></p>` should become // `<p><b><span>abc</span></b></p><p><b><span>def</span></b></p>`. if (aStartPoint.GetContainer() != aEndPoint.GetContainer()) { while (aStartPoint.GetContainer() != &aCommonAncestor && aStartPoint.IsInContentNode() && aStartPoint.GetContainerParent() && aStartPoint.IsStartOfContainer()) { if (!EditorUtils::IsEditableContent( *aStartPoint.ContainerAs<nsIContent>(), EditorType::HTML) || (aStartPoint.ContainerAs<nsIContent>()->IsElement() && (HTMLEditUtils::IsBlockElement( *aStartPoint.ContainerAs<Element>(), BlockInlineCheck::UseComputedDisplayOutsideStyle) || HTMLEditUtils::IsDisplayInsideFlowRoot( *aStartPoint.ContainerAs<Element>())))) { break; } aStartPoint = aStartPoint.ParentPoint(); } while (aEndPoint.GetContainer() != &aCommonAncestor && aEndPoint.IsInContentNode() && aEndPoint.GetContainerParent() && aEndPoint.IsEndOfContainer()) { if (!EditorUtils::IsEditableContent(*aEndPoint.ContainerAs<nsIContent>(), EditorType::HTML) || (aEndPoint.ContainerAs<nsIContent>()->IsElement() && (HTMLEditUtils::IsBlockElement( *aEndPoint.ContainerAs<Element>(), BlockInlineCheck::UseComputedDisplayOutsideStyle) || HTMLEditUtils::IsDisplayInsideFlowRoot( *aEndPoint.ContainerAs<Element>())))) { break; } aEndPoint.SetAfter(aEndPoint.ContainerAs<nsIContent>()); } } // Additionally, if we'll set a CSS style, we want to wrap elements which // should have the new style into the range to avoid creating new <span> // element. if (!IsRepresentableWithHTML() || (aHTMLEditor.IsCSSEnabled() && IsCSSSettable(*nsGkAtoms::span))) { // First, if pointing in a text node, use parent point. if (aStartPoint.IsInContentNode() && aStartPoint.IsStartOfContainer() && aStartPoint.GetContainerParentAs<nsIContent>() && EditorUtils::IsEditableContent( *aStartPoint.ContainerParentAs<nsIContent>(), EditorType::HTML) && (!aStartPoint.GetContainerAs<Element>() || !HTMLEditUtils::IsContainerNode( *aStartPoint.ContainerAs<nsIContent>())) && EditorUtils::IsEditableContent(*aStartPoint.ContainerAs<nsIContent>(), EditorType::HTML)) { aStartPoint = aStartPoint.ParentPoint(); MOZ_ASSERT(aStartPoint.IsSet()); } if (aEndPoint.IsInContentNode() && aEndPoint.IsEndOfContainer() && aEndPoint.GetContainerParentAs<nsIContent>() && EditorUtils::IsEditableContent( *aEndPoint.ContainerParentAs<nsIContent>(), EditorType::HTML) && (!aEndPoint.GetContainerAs<Element>() || !HTMLEditUtils::IsContainerNode( *aEndPoint.ContainerAs<nsIContent>())) && EditorUtils::IsEditableContent(*aEndPoint.ContainerAs<nsIContent>(), EditorType::HTML)) { aEndPoint.SetAfter(aEndPoint.GetContainer()); MOZ_ASSERT(aEndPoint.IsSet()); } // Then, wrap the container if it's a good element to set a CSS property. if (aStartPoint.IsInContentNode() && aStartPoint.GetContainerParent() && // The point must be start of the container aStartPoint.IsStartOfContainer() && // only if the pointing first child node cannot have `style` attribute (!aStartPoint.GetChildAs<nsStyledElement>() || !ElementIsGoodContainerToSetStyle( *aStartPoint.ChildAs<nsStyledElement>())) && // but don't cross block boundary at climbing up the tree !HTMLEditUtils::IsBlockElement( *aStartPoint.ContainerAs<nsIContent>(), BlockInlineCheck::UseComputedDisplayOutsideStyle) && // and the container is a good editable element to set CSS style aStartPoint.GetContainerAs<nsStyledElement>() && ElementIsGoodContainerToSetStyle( *aStartPoint.ContainerAs<nsStyledElement>())) { aStartPoint = aStartPoint.ParentPoint(); MOZ_ASSERT(aStartPoint.IsSet()); } if (aEndPoint.IsInContentNode() && aEndPoint.GetContainerParent() && // The point must be end of the container aEndPoint.IsEndOfContainer() && // only if the pointing last child node cannot have `style` attribute (aEndPoint.IsStartOfContainer() || !aEndPoint.GetPreviousSiblingOfChildAs<nsStyledElement>() || !ElementIsGoodContainerToSetStyle( *aEndPoint.GetPreviousSiblingOfChildAs<nsStyledElement>())) && // but don't cross block boundary at climbing up the tree !HTMLEditUtils::IsBlockElement( *aEndPoint.ContainerAs<nsIContent>(), BlockInlineCheck::UseComputedDisplayOutsideStyle) && // and the container is a good editable element to set CSS style aEndPoint.GetContainerAs<nsStyledElement>() && ElementIsGoodContainerToSetStyle( *aEndPoint.ContainerAs<nsStyledElement>())) { aEndPoint.SetAfter(aEndPoint.GetContainer()); MOZ_ASSERT(aEndPoint.IsSet()); } } return EditorRawDOMRange(std::move(aStartPoint), std::move(aEndPoint)); } Result<EditorRawDOMRange, nsresult> HTMLEditor::AutoInlineStyleSetter::ExtendOrShrinkRangeToApplyTheStyle( const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange, const Element& aEditingHost) const { if (NS_WARN_IF(!aRange.IsPositioned())) { return Err(NS_ERROR_FAILURE); } // For avoiding assertion hits in the utility methods, check whether the // range is in same subtree, first. Even if the range crosses a subtree // boundary, it's not a bug of this module. nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor(); if (NS_WARN_IF(!commonAncestor)) { return Err(NS_ERROR_FAILURE); } // If the range does not select only invisible <br> element, let's extend the // range to contain the <br> element. EditorDOMRange range(aRange); if (range.EndRef().IsInContentNode()) { WSScanResult nextContentData = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( &aEditingHost, range.EndRef(), BlockInlineCheck::UseComputedDisplayOutsideStyle); if (nextContentData.ReachedInvisibleBRElement() && nextContentData.BRElementPtr()->GetParentElement() && HTMLEditUtils::IsInlineContent( *nextContentData.BRElementPtr()->GetParentElement(), BlockInlineCheck::UseComputedDisplayOutsideStyle)) { range.SetEnd(EditorDOMPoint::After(*nextContentData.BRElementPtr())); MOZ_ASSERT(range.EndRef().IsSet()); commonAncestor = range.GetClosestCommonInclusiveAncestor(); if (NS_WARN_IF(!commonAncestor)) { return Err(NS_ERROR_FAILURE); } } } // If the range is collapsed, we don't want to replace ancestors unless it's // in an empty element. if (range.Collapsed() && range.StartRef().GetContainer()->Length()) { return EditorRawDOMRange(range); } // First, shrink the given range to minimize new style applied contents. // However, we should not shrink the range into entirely selected element. // E.g., if `abc[<i>def</i>]ghi`, shouldn't shrink it as // `abc<i>[def]</i>ghi`. EditorRawDOMPoint startPoint, endPoint; if (range.Collapsed()) { startPoint = endPoint = range.StartRef().To<EditorRawDOMPoint>(); } else { ContentSubtreeIterator iter; if (NS_FAILED(iter.Init(range.StartRef().ToRawRangeBoundary(), range.EndRef().ToRawRangeBoundary()))) { NS_WARNING("ContentSubtreeIterator::Init() failed"); return Err(NS_ERROR_FAILURE); } nsIContent* const firstContentEntirelyInRange = nsIContent::FromNodeOrNull(iter.GetCurrentNode()); nsIContent* const lastContentEntirelyInRange = [&]() { iter.Last(); return nsIContent::FromNodeOrNull(iter.GetCurrentNode()); }(); // Compute the shrunken range boundaries. startPoint = GetShrunkenRangeStart(aHTMLEditor, range, *commonAncestor, firstContentEntirelyInRange); MOZ_ASSERT(startPoint.IsSet()); endPoint = GetShrunkenRangeEnd(aHTMLEditor, range, *commonAncestor, lastContentEntirelyInRange); MOZ_ASSERT(endPoint.IsSet()); // If shrunken range is swapped, it could like this case: // `abc[</span><span>]def`, starts at very end of a node and ends at // very start of immediately next node. In this case, we should use // the original range instead. if (MOZ_UNLIKELY(!startPoint.EqualsOrIsBefore(endPoint))) { startPoint = range.StartRef().To<EditorRawDOMPoint>(); endPoint = range.EndRef().To<EditorRawDOMPoint>(); } } // Then, we may need to extend the range to wrap parent inline elements // which specify same style since we need to remove same style elements to // apply new value. E.g., abc // <span style="background-color: red"> // <span style="background-color: blue">[def]</span> // </span> // ghi // In this case, we need to wrap the other <span> element if setting // background color. Then, the inner <span> element is removed and the // other <span> element's style attribute will be updated rather than // inserting new <span> element. startPoint = GetExtendedRangeStartToWrapAncestorApplyingSameStyle(aHTMLEditor, startPoint); MOZ_ASSERT(startPoint.IsSet()); endPoint = GetExtendedRangeEndToWrapAncestorApplyingSameStyle(aHTMLEditor, endPoint); MOZ_ASSERT(endPoint.IsSet()); // Finally, we need to extend the range unless the range is in an element to // reduce the number of creating new elements. E.g., if now selects // `<span>[abc</span><span>def]</span>`, we should make it // `<b><span>abc</span><span>def</span></b>` rather than // `<span><b>abc</b></span><span><b>def</b></span>`. EditorRawDOMRange finalRange = GetExtendedRangeToMinimizeTheNumberOfNewElements( aHTMLEditor, *commonAncestor, std::move(startPoint), std::move(endPoint)); #if 0 fprintf(stderr, "ExtendOrShrinkRangeToApplyTheStyle:\n" " Result: {(\n %s\n ) - (\n %s\n )},\n" " Input: {(\n %s\n ) - (\n %s\n )}\n", ToString(finalRange.StartRef()).c_str(), ToString(finalRange.EndRef()).c_str(), ToString(aRange.StartRef()).c_str(), ToString(aRange.EndRef()).c_str()); #endif return finalRange; } Result<SplitRangeOffResult, nsresult> HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges( const EditorDOMRange& aRange, const EditorInlineStyle& aStyle, SplitAtEdges aSplitAtEdges) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!aRange.IsPositioned())) { return Err(NS_ERROR_FAILURE); } EditorDOMRange range(aRange); // split any matching style nodes above the start of range auto resultAtStart = [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> { AutoTrackDOMRange tracker(RangeUpdaterRef(), &range); Result<SplitNodeResult, nsresult> result = SplitAncestorStyledInlineElementsAt(range.StartRef(), aStyle, aSplitAtEdges); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed"); return result; } tracker.FlushAndStopTracking(); if (result.inspect().Handled()) { auto startOfRange = result.inspect().AtSplitPoint<EditorDOMPoint>(); if (!startOfRange.IsSet()) { result.inspect().IgnoreCaretPointSuggestion(); NS_WARNING( "HTMLEditor::SplitAncestorStyledInlineElementsAt() didn't return " "split point"); return Err(NS_ERROR_FAILURE); } range.SetStart(std::move(startOfRange)); } else if (MOZ_UNLIKELY(!range.IsPositioned())) { NS_WARNING( "HTMLEditor::SplitAncestorStyledInlineElementsAt() caused unexpected " "DOM tree"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return result; }(); if (MOZ_UNLIKELY(resultAtStart.isErr())) { return resultAtStart.propagateErr(); } SplitNodeResult unwrappedResultAtStart = resultAtStart.unwrap(); // second verse, same as the first... auto resultAtEnd = [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> { AutoTrackDOMRange tracker(RangeUpdaterRef(), &range); Result<SplitNodeResult, nsresult> result = SplitAncestorStyledInlineElementsAt(range.EndRef(), aStyle, aSplitAtEdges); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed"); return result; } tracker.FlushAndStopTracking(); if (NS_WARN_IF(result.inspect().Handled())) { auto endOfRange = result.inspect().AtSplitPoint<EditorDOMPoint>(); if (!endOfRange.IsSet()) { result.inspect().IgnoreCaretPointSuggestion(); NS_WARNING( "HTMLEditor::SplitAncestorStyledInlineElementsAt() didn't return " "split point"); return Err(NS_ERROR_FAILURE); } range.SetEnd(std::move(endOfRange)); } else if (MOZ_UNLIKELY(!range.IsPositioned())) { NS_WARNING( "HTMLEditor::SplitAncestorStyledInlineElementsAt() caused unexpected " "DOM tree"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return result; }(); if (MOZ_UNLIKELY(resultAtEnd.isErr())) { unwrappedResultAtStart.IgnoreCaretPointSuggestion(); return resultAtEnd.propagateErr(); } return SplitRangeOffResult(std::move(range), std::move(unwrappedResultAtStart), resultAtEnd.unwrap()); } Result<SplitNodeResult, nsresult> HTMLEditor::SplitAncestorStyledInlineElementsAt( const EditorDOMPoint& aPointToSplit, const EditorInlineStyle& aStyle, SplitAtEdges aSplitAtEdges) { // If the point is in a non-content node, e.g., in the document node, we // should split nothing. if (MOZ_UNLIKELY(!aPointToSplit.IsInContentNode())) { return SplitNodeResult::NotHandled(aPointToSplit); } // 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(). const bool handleCSS = aStyle.mHTMLProperty != nsGkAtoms::tt || IsCSSEnabled(); AutoTArray<OwningNonNull<Element>, 24> arrayOfParents; for (Element* element : aPointToSplit.GetContainer()->InclusiveAncestorsOfType<Element>()) { if (element->IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::head, nsGkAtoms::html) || HTMLEditUtils::IsBlockElement( *element, BlockInlineCheck::UseComputedDisplayOutsideStyle) || !element->GetParent() || !EditorUtils::IsEditableContent(*element->GetParent(), EditorType::HTML) || NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(*element))) { break; } arrayOfParents.AppendElement(*element); } // Split any matching style nodes above the point. SplitNodeResult result = SplitNodeResult::NotHandled(aPointToSplit); MOZ_ASSERT(!result.Handled()); EditorDOMPoint pointToPutCaret; for (OwningNonNull<Element>& element : arrayOfParents) { auto isSetByCSSOrError = [&]() -> Result<bool, nsresult> { if (!handleCSS) { return false; } // The HTML style defined by aStyle has a CSS equivalence in this // implementation for the node; let's check if it carries those CSS // styles if (aStyle.IsCSSRemovable(*element)) { nsAutoString firstValue; Result<bool, nsresult> isSpecifiedByCSSOrError = CSSEditUtils::IsSpecifiedCSSEquivalentTo(*this, *element, aStyle, firstValue); if (MOZ_UNLIKELY(isSpecifiedByCSSOrError.isErr())) { result.IgnoreCaretPointSuggestion(); NS_WARNING("CSSEditUtils::IsSpecifiedCSSEquivalentTo() failed"); return isSpecifiedByCSSOrError; } if (isSpecifiedByCSSOrError.unwrap()) { return true; } } // If this is <sub> or <sup>, we won't use vertical-align CSS property // because <sub>/<sup> changes font size but neither `vertical-align: // sub` nor `vertical-align: super` changes it (bug 394304 comment 2). // Therefore, they are not equivalents. However, they're obviously // conflict with vertical-align style. Thus, we need to remove ancestor // elements having vertical-align style. if (aStyle.IsStyleConflictingWithVerticalAlign()) { nsAutoString value; nsresult rv = CSSEditUtils::GetSpecifiedProperty( *element, *nsGkAtoms::vertical_align, value); if (NS_FAILED(rv)) { NS_WARNING("CSSEditUtils::GetSpecifiedProperty() failed"); result.IgnoreCaretPointSuggestion(); return Err(rv); } if (!value.IsEmpty()) { return true; } } return false; }(); if (MOZ_UNLIKELY(isSetByCSSOrError.isErr())) { return isSetByCSSOrError.propagateErr(); } if (!isSetByCSSOrError.inspect()) { if (!aStyle.IsStyleToClearAllInlineStyles()) { // If we're removing a link style and the element is an <a href>, we // need to split it. if (aStyle.mHTMLProperty == nsGkAtoms::href && HTMLEditUtils::IsLink(element)) { } // If we're removing HTML style, we should split only the element // which represents the style. else if (!element->IsHTMLElement(aStyle.mHTMLProperty) || (aStyle.mAttribute && !element->HasAttr(aStyle.mAttribute))) { continue; } // If we're setting <font> related styles, it means that we're not // toggling the style. In this case, we need to remove parent <font> // elements and/or update parent <font> elements if there are some // elements which have the attribute. However, we should not touch if // the value is same as what the caller setting to keep the DOM tree // as-is as far as possible. if (aStyle.IsStyleOfFontElement() && aStyle.MaybeHasValue()) { const nsAttrValue* const attrValue = element->GetParsedAttr(aStyle.mAttribute); if (attrValue) { if (aStyle.mAttribute == nsGkAtoms::size) { if (nsContentUtils::ParseLegacyFontSize( aStyle.AsInlineStyleAndValue().mAttributeValue) == attrValue->GetIntegerValue()) { continue; } } else if (aStyle.mAttribute == nsGkAtoms::color) { nsAttrValue newValue; nscolor oldColor, newColor; if (attrValue->GetColorValue(oldColor) && newValue.ParseColor( aStyle.AsInlineStyleAndValue().mAttributeValue) && newValue.GetColorValue(newColor) && oldColor == newColor) { continue; } } else if (attrValue->Equals( aStyle.AsInlineStyleAndValue().mAttributeValue, eIgnoreCase)) { continue; } } } } // If aProperty is nullptr, we need to split any style. else if (!EditorUtils::IsEditableContent(element, EditorType::HTML) || !HTMLEditUtils::IsRemovableInlineStyleElement(*element)) { 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. AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret); Result<SplitNodeResult, nsresult> splitNodeResult = SplitNodeDeepWithTransaction(MOZ_KnownLive(element), result.AtSplitPoint<EditorDOMPoint>(), aSplitAtEdges); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed"); return splitNodeResult; } SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap(); trackPointToPutCaret.FlushAndStopTracking(); unwrappedSplitNodeResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); // If it's not handled, it means that `content` is not a splittable node // like a void element even if it has some children, and the split point // is middle of it. if (!unwrappedSplitNodeResult.Handled()) { continue; } // Respect the last split result which actually did it. if (!result.DidSplit() || unwrappedSplitNodeResult.DidSplit()) { result = unwrappedSplitNodeResult.ToHandledResult(); } MOZ_ASSERT(result.Handled()); } return pointToPutCaret.IsSet() ? SplitNodeResult(std::move(result), std::move(pointToPutCaret)) : std::move(result); } Result<EditorDOMPoint, nsresult> HTMLEditor::ClearStyleAt( const EditorDOMPoint& aPoint, const EditorInlineStyle& aStyleToRemove, SpecifiedStyle aSpecifiedStyle) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!aPoint.IsSet())) { return Err(NS_ERROR_INVALID_ARG); } // TODO: We should rewrite this to stop unnecessary element creation and // deleting it later because it causes the original element may be // removed from the DOM tree even if same element is still in the // DOM tree from point of view of users. // First, split inline elements at the point. // E.g., if aStyleToRemove.mHTMLProperty 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>`. EditorDOMPoint pointToPutCaret(aPoint); AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret); Result<SplitNodeResult, nsresult> splitNodeResult = SplitAncestorStyledInlineElementsAt( aPoint, aStyleToRemove, SplitAtEdges::eAllowToCreateEmptyContainer); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed"); return splitNodeResult.propagateErr(); } trackPointToPutCaret.FlushAndStopTracking(); SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap(); unwrappedSplitNodeResult.MoveCaretPointTo( pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); // If there is no styled inline elements of aStyleToRemove, we just return the // given point. // E.g., `<p><i>a[]bc</i></p>` for nsGkAtoms::b. if (!unwrappedSplitNodeResult.Handled()) { return pointToPutCaret; } // 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. Then, we'll get the following DOM tree if there is no "a" in the // above case: // <p><b><i>bc</i></b></p> // ^^ if (unwrappedSplitNodeResult.GetPreviousContent() && HTMLEditUtils::IsEmptyNode( *unwrappedSplitNodeResult.GetPreviousContent(), {EmptyCheckOption::TreatSingleBRElementAsVisible, EmptyCheckOption::TreatListItemAsVisible, EmptyCheckOption::TreatTableCellAsVisible})) { AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret); // Delete previous node if it's empty. // MOZ_KnownLive(unwrappedSplitNodeResult.GetPreviousContent()): // It's grabbed by unwrappedSplitNodeResult. nsresult rv = DeleteNodeWithTransaction( MOZ_KnownLive(*unwrappedSplitNodeResult.GetPreviousContent())); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(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 (!unwrappedSplitNodeResult.GetNextContent()) { return pointToPutCaret; } // Otherwise, the next node is topmost ancestor inline element which has // the style. We want to put caret between the split nodes, but we need // to keep other styles. Therefore, next, we need to split at start of // the next node. The first example should become // `<p><b><i>a</i></b><b><i></i></b><b><i>bc</i></b></p>`. // ^^^^^^^^^^^^^^ nsIContent* firstLeafChildOfNextNode = HTMLEditUtils::GetFirstLeafContent( *unwrappedSplitNodeResult.GetNextContent(), {LeafNodeType::OnlyLeafNode}); EditorDOMPoint atStartOfNextNode( firstLeafChildOfNextNode ? firstLeafChildOfNextNode : unwrappedSplitNodeResult.GetNextContent(), 0); RefPtr<HTMLBRElement> brElement; // But don't try to split non-containers like `<br>`, `<hr>` and `<img>` // element. if (!atStartOfNextNode.IsInContentNode() || !HTMLEditUtils::IsContainerNode( *atStartOfNextNode.ContainerAs<nsIContent>())) { // If it's a `<br>` element, let's move it into new node later. brElement = HTMLBRElement::FromNode(atStartOfNextNode.GetContainer()); if (!atStartOfNextNode.GetContainerParentAs<nsIContent>()) { NS_WARNING("atStartOfNextNode was in an orphan node"); return Err(NS_ERROR_FAILURE); } atStartOfNextNode.Set(atStartOfNextNode.GetContainerParent(), 0); } AutoTrackDOMPoint trackPointToPutCaret2(RangeUpdaterRef(), &pointToPutCaret); Result<SplitNodeResult, nsresult> splitResultAtStartOfNextNode = SplitAncestorStyledInlineElementsAt( atStartOfNextNode, aStyleToRemove, SplitAtEdges::eAllowToCreateEmptyContainer); if (MOZ_UNLIKELY(splitResultAtStartOfNextNode.isErr())) { NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed"); return splitResultAtStartOfNextNode.propagateErr(); } trackPointToPutCaret2.FlushAndStopTracking(); SplitNodeResult unwrappedSplitResultAtStartOfNextNode = splitResultAtStartOfNextNode.unwrap(); unwrappedSplitResultAtStartOfNextNode.MoveCaretPointTo( pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (unwrappedSplitResultAtStartOfNextNode.Handled() && unwrappedSplitResultAtStartOfNextNode.GetNextContent()) { // If the right inline elements are empty, we should remove them. E.g., // if the split point is at end of a text node (or end of an inline // element), e.g., <div><b><i>abc[]</i></b></div>, then now, it's been // changed to: // <div><b><i>abc</i></b><b><i>[]</i></b><b><i></i></b></div> // ^^^^^^^^^^^^^^ // We will change it to: // <div><b><i>abc</i></b><b><i>[]</i></b></div> // ^^ // And if it has only padding <br> element, we should move it into the // previous <i> which will have new content. bool seenBR = false; if (HTMLEditUtils::IsEmptyNode( *unwrappedSplitResultAtStartOfNextNode.GetNextContent(), {EmptyCheckOption::TreatListItemAsVisible, EmptyCheckOption::TreatTableCellAsVisible}, &seenBR)) { if (seenBR && !brElement) { brElement = HTMLEditUtils::GetFirstBRElement( *unwrappedSplitResultAtStartOfNextNode.GetNextContentAs<Element>()); } // Once we remove <br> element's parent, we lose the rights to remove it // from the parent because the parent becomes not editable. Therefore, we // need to delete the <br> element before removing its parents for reusing // it later. if (brElement) { nsresult rv = DeleteNodeWithTransaction(*brElement); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } // Delete next node if it's empty. // MOZ_KnownLive because of grabbed by // unwrappedSplitResultAtStartOfNextNode. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive( *unwrappedSplitResultAtStartOfNextNode.GetNextContent())); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } } if (!unwrappedSplitResultAtStartOfNextNode.Handled()) { return std::move(pointToPutCaret); } // If there is no content, we should return here. // XXX Is this possible case without mutation event listener? if (!unwrappedSplitResultAtStartOfNextNode.GetPreviousContent()) { // XXX This is really odd, but we return this value... const auto splitPoint = unwrappedSplitNodeResult.AtSplitPoint<EditorRawDOMPoint>(); const auto splitPointAtStartOfNextNode = unwrappedSplitResultAtStartOfNextNode.AtSplitPoint<EditorRawDOMPoint>(); return EditorDOMPoint(splitPoint.GetContainer(), splitPointAtStartOfNextNode.Offset()); } // Now, we want to put `<br>` element into the empty split node if // it was in next node of the first split. // E.g., `<p><b><i>a</i></b><b><i><br></i></b><b><i>bc</i></b></p>` nsIContent* firstLeafChildOfPreviousNode = HTMLEditUtils::GetFirstLeafContent( *unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(), {LeafNodeType::OnlyLeafNode}); pointToPutCaret.Set( firstLeafChildOfPreviousNode ? firstLeafChildOfPreviousNode : unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(), 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) { if (brElement->GetParentNode()) { Result<MoveNodeResult, nsresult> moveBRElementResult = MoveNodeWithTransaction(*brElement, pointToPutCaret); if (MOZ_UNLIKELY(moveBRElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveBRElementResult.propagateErr(); } moveBRElementResult.unwrap().MoveCaretPointTo( pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); } else { Result<CreateElementResult, nsresult> insertBRElementResult = InsertNodeWithTransaction<Element>(*brElement, pointToPutCaret); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertBRElementResult.propagateErr(); } insertBRElementResult.unwrap().MoveCaretPointTo( pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); } if (unwrappedSplitResultAtStartOfNextNode.GetNextContent() && unwrappedSplitResultAtStartOfNextNode.GetNextContent() ->IsInComposedDoc()) { // If we split inline elements at immediately before <br> element which is // the last visible content in the right element, we don't need the right // element anymore. Otherwise, we'll create the following DOM tree: // - <b>abc</b>{}<br><b></b> // ^^^^^^^ // - <b><i>abc</i></b><i><br></i><b></b> // ^^^^^^^ if (HTMLEditUtils::IsEmptyNode( *unwrappedSplitResultAtStartOfNextNode.GetNextContent(), {EmptyCheckOption::TreatSingleBRElementAsVisible, EmptyCheckOption::TreatListItemAsVisible, EmptyCheckOption::TreatTableCellAsVisible})) { // MOZ_KnownLive because the result is grabbed by // unwrappedSplitResultAtStartOfNextNode. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive( *unwrappedSplitResultAtStartOfNextNode.GetNextContent())); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } // If the next content has only one <br> element, there may be empty // inline elements around it. We don't need them anymore because user // cannot put caret into them. E.g., <b><i>abc[]<br></i><br></b> has // been changed to <b><i>abc</i></b><i>{}<br></i><b><i></i><br></b> now. // ^^^^^^^^^^^^^^^^^^ // We don't need the empty <i>. else if (HTMLEditUtils::IsEmptyNode( *unwrappedSplitResultAtStartOfNextNode.GetNextContent(), {EmptyCheckOption::TreatListItemAsVisible, EmptyCheckOption::TreatTableCellAsVisible})) { AutoTArray<OwningNonNull<nsIContent>, 4> emptyInlineContainerElements; HTMLEditUtils::CollectEmptyInlineContainerDescendants( *unwrappedSplitResultAtStartOfNextNode.GetNextContentAs<Element>(), emptyInlineContainerElements, {EmptyCheckOption::TreatSingleBRElementAsVisible, EmptyCheckOption::TreatListItemAsVisible, EmptyCheckOption::TreatTableCellAsVisible}, BlockInlineCheck::UseComputedDisplayOutsideStyle); for (const OwningNonNull<nsIContent>& emptyInlineContainerElement : emptyInlineContainerElements) { // MOZ_KnownLive(emptyInlineContainerElement) due to bug 1622253. nsresult rv = DeleteNodeWithTransaction( MOZ_KnownLive(emptyInlineContainerElement)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(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 (auto* const previousElementOfSplitPoint = unwrappedSplitResultAtStartOfNextNode .GetPreviousContentAs<Element>()) { // 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); // MOZ_KnownLive(previousElementOfSplitPoint): // It's grabbed by unwrappedSplitResultAtStartOfNextNode. Result<EditorDOMPoint, nsresult> removeStyleResult = RemoveStyleInside(MOZ_KnownLive(*previousElementOfSplitPoint), aStyleToRemove, aSpecifiedStyle); if (MOZ_UNLIKELY(removeStyleResult.isErr())) { NS_WARNING("HTMLEditor::RemoveStyleInside() failed"); return removeStyleResult; } // We've already computed a suggested caret position at start of first leaf // which is stored in pointToPutCaret, so we don't need to update it here. } return pointToPutCaret; } Result<EditorDOMPoint, nsresult> HTMLEditor::RemoveStyleInside( Element& aElement, const EditorInlineStyle& aStyleToRemove, SpecifiedStyle aSpecifiedStyle) { // First, handle all descendants. AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfChildContents; HTMLEditUtils::CollectAllChildren(aElement, arrayOfChildContents); EditorDOMPoint pointToPutCaret; for (const OwningNonNull<nsIContent>& child : arrayOfChildContents) { if (!child->IsElement()) { continue; } Result<EditorDOMPoint, nsresult> removeStyleResult = RemoveStyleInside( MOZ_KnownLive(*child->AsElement()), aStyleToRemove, aSpecifiedStyle); if (MOZ_UNLIKELY(removeStyleResult.isErr())) { NS_WARNING("HTMLEditor::RemoveStyleInside() failed"); return removeStyleResult; } if (removeStyleResult.inspect().IsSet()) { pointToPutCaret = removeStyleResult.unwrap(); } } // TODO: It seems that if aElement is not editable, we should insert new // container to remove the style if possible. if (!EditorUtils::IsEditableContent(aElement, EditorType::HTML)) { return pointToPutCaret; } // Next, remove CSS style first. Then, `style` attribute will be removed if // the corresponding CSS property is last one. auto isStyleSpecifiedOrError = [&]() -> Result<bool, nsresult> { if (!aStyleToRemove.IsCSSRemovable(aElement)) { return false; } MOZ_ASSERT(!aStyleToRemove.IsStyleToClearAllInlineStyles()); Result<bool, nsresult> elementHasSpecifiedCSSEquivalentStylesOrError = CSSEditUtils::HaveSpecifiedCSSEquivalentStyles(*this, aElement, aStyleToRemove); NS_WARNING_ASSERTION( elementHasSpecifiedCSSEquivalentStylesOrError.isOk(), "CSSEditUtils::HaveSpecifiedCSSEquivalentStyles() failed"); return elementHasSpecifiedCSSEquivalentStylesOrError; }(); if (MOZ_UNLIKELY(isStyleSpecifiedOrError.isErr())) { return isStyleSpecifiedOrError.propagateErr(); } bool styleSpecified = isStyleSpecifiedOrError.unwrap(); if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) { if (styleSpecified) { // MOZ_KnownLive(*styledElement) because it's an alias of aElement. nsresult rv = CSSEditUtils::RemoveCSSEquivalentToStyle( WithTransaction::Yes, *this, MOZ_KnownLive(*styledElement), aStyleToRemove, nullptr); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "CSSEditUtils::RemoveCSSEquivalentToStyle() failed, but ignored"); } // If the style is <sub> or <sup>, we won't use vertical-align CSS // property because <sub>/<sup> changes font size but neither // `vertical-align: sub` nor `vertical-align: super` changes it // (bug 394304 comment 2). Therefore, they are not equivalents. However, // they're obviously conflict with vertical-align style. Thus, we need to // remove the vertical-align style from elements. if (aStyleToRemove.IsStyleConflictingWithVerticalAlign()) { nsAutoString value; nsresult rv = CSSEditUtils::GetSpecifiedProperty( aElement, *nsGkAtoms::vertical_align, value); if (NS_FAILED(rv)) { NS_WARNING("CSSEditUtils::GetSpecifiedProperty() failed"); return Err(rv); } if (!value.IsEmpty()) { // MOZ_KnownLive(*styledElement) because it's an alias of aElement. nsresult rv = CSSEditUtils::RemoveCSSPropertyWithTransaction( *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::vertical_align, value); if (NS_FAILED(rv)) { NS_WARNING("CSSEditUtils::RemoveCSSPropertyWithTransaction() failed"); return Err(rv); } styleSpecified = true; } } } // Then, if we could and should remove or replace aElement, let's do it. Or // just remove attribute. const bool isStyleRepresentedByElement = !aStyleToRemove.IsStyleToClearAllInlineStyles() && aStyleToRemove.IsRepresentedBy(aElement); auto ShouldUpdateDOMTree = [&]() { // If we're removing any inline styles and aElement is an inline style // element, we can remove or replace it. if (aStyleToRemove.IsStyleToClearAllInlineStyles() && HTMLEditUtils::IsRemovableInlineStyleElement(aElement)) { return true; } // If we're a specific style and aElement represents it, we can remove or // replace the element or remove the corresponding attribute. if (isStyleRepresentedByElement) { return true; } // If we've removed a CSS style from the `style` attribute of aElement, we // could remove the element. return aElement.IsHTMLElement(nsGkAtoms::span) && styleSpecified; }; if (!ShouldUpdateDOMTree()) { return pointToPutCaret; } const bool elementHasNecessaryAttributes = [&]() { // If we're not removing nor replacing aElement itself, we don't need to // take care of its `style` and `class` attributes even if aSpecifiedStyle // is `Discard` because aSpecifiedStyle is not intended to be used in this // case. if (!isStyleRepresentedByElement) { return HTMLEditUtils::ElementHasAttributeExcept(aElement, *nsGkAtoms::_empty); } // If we're removing links, we don't need to keep <a> even if it has some // specific attributes because it cannot be nested. However, if and only if // it has `style` attribute and aSpecifiedStyle is not `Discard`, we need to // replace it with new <span> to keep the style. if (aStyleToRemove.IsStyleOfAnchorElement()) { return aSpecifiedStyle == SpecifiedStyle::Preserve && (aElement.HasNonEmptyAttr(nsGkAtoms::style) || aElement.HasNonEmptyAttr(nsGkAtoms::_class)); } nsAtom& attrKeepStaying = aStyleToRemove.mAttribute ? *aStyleToRemove.mAttribute : *nsGkAtoms::_empty; return aSpecifiedStyle == SpecifiedStyle::Preserve // If we're try to remove the element but the caller wants to // preserve the style, check whether aElement has attributes // except the removing attribute since `style` and `class` should // keep existing to preserve the style. ? HTMLEditUtils::ElementHasAttributeExcept(aElement, attrKeepStaying) // If we're try to remove the element and the caller wants to // discard the style specified to the element, check whether // aElement has attributes except the removing attribute, `style` // and `class` since we don't want to keep these attributes. : HTMLEditUtils::ElementHasAttributeExcept( aElement, attrKeepStaying, *nsGkAtoms::style, *nsGkAtoms::_class); }(); // If the element is not a <span> and still has some attributes, we should // replace it with new <span>. auto ReplaceWithNewSpan = [&]() { if (aStyleToRemove.IsStyleToClearAllInlineStyles()) { return false; // Remove it even if it has attributes. } if (aElement.IsHTMLElement(nsGkAtoms::span)) { return false; // Don't replace <span> with new <span>. } if (!isStyleRepresentedByElement) { return false; // Keep non-related element as-is. } if (!elementHasNecessaryAttributes) { return false; // Should remove it instead of replacing it. } if (aElement.IsHTMLElement(nsGkAtoms::font)) { // Replace <font> if it won't have its specific attributes. return (aStyleToRemove.mHTMLProperty == nsGkAtoms::color || !aElement.HasAttr(nsGkAtoms::color)) && (aStyleToRemove.mHTMLProperty == nsGkAtoms::face || !aElement.HasAttr(nsGkAtoms::face)) && (aStyleToRemove.mHTMLProperty == nsGkAtoms::size || !aElement.HasAttr(nsGkAtoms::size)); } // The styled element has only global attributes, let's replace it with new // <span> with cloning the attributes. return true; }; if (ReplaceWithNewSpan()) { // Before cloning the attribute to new element, let's remove it. if (aStyleToRemove.mAttribute) { nsresult rv = RemoveAttributeWithTransaction(aElement, *aStyleToRemove.mAttribute); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::RemoveAttributeWithTransaction() failed"); return Err(rv); } } if (aSpecifiedStyle == SpecifiedStyle::Discard) { nsresult rv = RemoveAttributeWithTransaction(aElement, *nsGkAtoms::style); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::style) " "failed"); return Err(rv); } rv = RemoveAttributeWithTransaction(aElement, *nsGkAtoms::_class); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::_class) " "failed"); return Err(rv); } } // Move `style` attribute and `class` element to span element before // removing aElement from the tree. auto replaceWithSpanResult = [&]() MOZ_CAN_RUN_SCRIPT -> Result<CreateElementResult, nsresult> { if (!aStyleToRemove.IsStyleOfAnchorElement()) { return ReplaceContainerAndCloneAttributesWithTransaction( aElement, *nsGkAtoms::span); } nsString styleValue; // Use nsString to avoid copying the buffer at // setting the attribute. aElement.GetAttr(nsGkAtoms::style, styleValue); return ReplaceContainerWithTransaction(aElement, *nsGkAtoms::span, *nsGkAtoms::style, styleValue); }(); if (MOZ_UNLIKELY(replaceWithSpanResult.isErr())) { NS_WARNING( "HTMLEditor::ReplaceContainerWithTransaction(nsGkAtoms::span) " "failed"); return replaceWithSpanResult.propagateErr(); } CreateElementResult unwrappedReplaceWithSpanResult = replaceWithSpanResult.unwrap(); if (AllowsTransactionsToChangeSelection()) { unwrappedReplaceWithSpanResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); } else { unwrappedReplaceWithSpanResult.IgnoreCaretPointSuggestion(); } return pointToPutCaret; } auto RemoveElement = [&]() { if (aStyleToRemove.IsStyleToClearAllInlineStyles()) { MOZ_ASSERT(HTMLEditUtils::IsRemovableInlineStyleElement(aElement)); return true; } // If the element still has some attributes, we should not remove it to keep // current presentation and/or semantics. if (elementHasNecessaryAttributes) { return false; } // If the style is represented by the element, let's remove it. if (isStyleRepresentedByElement) { return true; } // If we've removed a CSS style and that made the <span> element have no // attributes, we can delete it. if (styleSpecified && aElement.IsHTMLElement(nsGkAtoms::span)) { return true; } return false; }; if (RemoveElement()) { Result<EditorDOMPoint, nsresult> unwrapElementResult = RemoveContainerWithTransaction(aElement); if (MOZ_UNLIKELY(unwrapElementResult.isErr())) { NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed"); return unwrapElementResult.propagateErr(); } if (AllowsTransactionsToChangeSelection() && unwrapElementResult.inspect().IsSet()) { pointToPutCaret = unwrapElementResult.unwrap(); } return pointToPutCaret; } // If the element needs to keep having some attributes, just remove the // attribute. Note that we don't need to remove `style` attribute here when // aSpecifiedStyle is `Discard` because we've already removed unnecessary // CSS style above. if (isStyleRepresentedByElement && aStyleToRemove.mAttribute) { nsresult rv = RemoveAttributeWithTransaction(aElement, *aStyleToRemove.mAttribute); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::RemoveAttributeWithTransaction() failed"); return Err(rv); } } return pointToPutCaret; } EditorRawDOMRange HTMLEditor::GetExtendedRangeWrappingNamedAnchor( const EditorRawDOMRange& aRange) const { MOZ_ASSERT(aRange.StartRef().IsSet()); MOZ_ASSERT(aRange.EndRef().IsSet()); // FYI: We don't want to stop at ancestor block boundaries to extend the range // because <a name> can have block elements with low level DOM API. We want // to remove any <a name> ancestors to remove the style. EditorRawDOMRange newRange(aRange); for (Element* element : aRange.StartRef().GetContainer()->InclusiveAncestorsOfType<Element>()) { if (!HTMLEditUtils::IsNamedAnchor(element)) { continue; } newRange.SetStart(EditorRawDOMPoint(element)); } for (Element* element : aRange.EndRef().GetContainer()->InclusiveAncestorsOfType<Element>()) { if (!HTMLEditUtils::IsNamedAnchor(element)) { continue; } newRange.SetEnd(EditorRawDOMPoint::After(*element)); } return newRange; } EditorRawDOMRange HTMLEditor::GetExtendedRangeWrappingEntirelySelectedElements( const EditorRawDOMRange& aRange) const { MOZ_ASSERT(aRange.StartRef().IsSet()); MOZ_ASSERT(aRange.EndRef().IsSet()); // FYI: We don't want to stop at ancestor block boundaries to extend the range // because the style may come from inline parents of block elements which may // occur in invalid DOM tree. We want to split any (even invalid) ancestors // at removing the styles. EditorRawDOMRange newRange(aRange); while (newRange.StartRef().IsInContentNode() && newRange.StartRef().IsStartOfContainer()) { if (!EditorUtils::IsEditableContent( *newRange.StartRef().ContainerAs<nsIContent>(), EditorType::HTML)) { break; } newRange.SetStart(newRange.StartRef().ParentPoint()); } while (newRange.EndRef().IsInContentNode() && newRange.EndRef().IsEndOfContainer()) { if (!EditorUtils::IsEditableContent( *newRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)) { break; } newRange.SetEnd( EditorRawDOMPoint::After(*newRange.EndRef().ContainerAs<nsIContent>())); } return newRange; } nsresult HTMLEditor::GetInlinePropertyBase(const EditorInlineStyle& aStyle, const nsAString* aValue, bool* aFirst, bool* aAny, bool* aAll, nsAString* outValue) const { MOZ_ASSERT(!aStyle.IsStyleToClearAllInlineStyles()); MOZ_ASSERT(IsEditActionDataAvailable()); *aAny = false; *aAll = true; *aFirst = false; bool first = true; const bool isCollapsed = SelectionRef().IsCollapsed(); RefPtr<nsRange> range = SelectionRef().GetRangeAt(0); // XXX: Should be a while loop, to get each separate range // XXX: ERROR_HANDLING can currentItem be null? if (range) { // For each range, set a flag bool firstNodeInRange = true; if (isCollapsed) { if (NS_WARN_IF(!range->GetStartContainer())) { return NS_ERROR_FAILURE; } nsString tOutString; const PendingStyleState styleState = [&]() { if (aStyle.mAttribute) { auto state = mPendingStylesToApplyToNewContent->GetStyleState( *aStyle.mHTMLProperty, aStyle.mAttribute, &tOutString); if (outValue) { outValue->Assign(tOutString); } return state; } return mPendingStylesToApplyToNewContent->GetStyleState( *aStyle.mHTMLProperty); }(); if (styleState != PendingStyleState::NotUpdated) { *aFirst = *aAny = *aAll = (styleState == PendingStyleState::BeingPreserved); return NS_OK; } nsIContent* const collapsedContent = nsIContent::FromNode(range->GetStartContainer()); if (MOZ_LIKELY(collapsedContent && collapsedContent->GetAsElementOrParentElement()) && aStyle.IsCSSSettable( *collapsedContent->GetAsElementOrParentElement())) { if (aValue) { tOutString.Assign(*aValue); } Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError = CSSEditUtils::IsComputedCSSEquivalentTo( *this, MOZ_KnownLive(*collapsedContent), aStyle, tOutString); if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) { NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed"); return isComputedCSSEquivalentToStyleOrError.unwrapErr(); } *aFirst = *aAny = *aAll = isComputedCSSEquivalentToStyleOrError.unwrap(); if (outValue) { outValue->Assign(tOutString); } return NS_OK; } *aFirst = *aAny = *aAll = collapsedContent && HTMLEditUtils::IsInlineStyleSetByElement( *collapsedContent, aStyle, aValue, outValue); return NS_OK; } // Non-collapsed selection nsAutoString firstValue, theValue; nsCOMPtr<nsINode> endNode = range->GetEndContainer(); uint32_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()->IsHTMLElement(nsGkAtoms::body)) { break; } RefPtr<Text> textNode = Text::FromNode(postOrderIter.GetCurrentNode()); if (!textNode) { continue; } // just ignore any non-editable nodes if (!EditorUtils::IsEditableContent(*textNode, EditorType::HTML) || !HTMLEditUtils::IsVisibleTextNode(*textNode)) { continue; } if (!isCollapsed && first && firstNodeInRange) { firstNodeInRange = false; if (range->StartOffset() == textNode->TextDataLength()) { continue; } } else if (textNode == endNode && !endOffset) { continue; } const RefPtr<Element> element = textNode->GetParentElement(); bool isSet = false; if (first) { if (element) { if (aStyle.IsCSSSettable(*element)) { // 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); } Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError = CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle, firstValue); if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) { NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed"); return isComputedCSSEquivalentToStyleOrError.unwrapErr(); } isSet = isComputedCSSEquivalentToStyleOrError.unwrap(); } else { isSet = HTMLEditUtils::IsInlineStyleSetByElement( *element, aStyle, aValue, &firstValue); } } *aFirst = isSet; first = false; if (outValue) { *outValue = firstValue; } } else { if (element) { if (aStyle.IsCSSSettable(*element)) { // 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); } Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError = CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle, theValue); if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) { NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed"); return isComputedCSSEquivalentToStyleOrError.unwrapErr(); } isSet = isComputedCSSEquivalentToStyleOrError.unwrap(); } else { isSet = HTMLEditUtils::IsInlineStyleSetByElement(*element, aStyle, 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. (!aStyle.IsStyleOfTextDecoration( EditorInlineStyle::IgnoreSElement::Yes) || *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; } nsresult HTMLEditor::GetInlineProperty(nsStaticAtom& aHTMLProperty, nsAtom* aAttribute, const nsAString& aValue, bool* aFirst, bool* aAny, bool* aAll) const { if (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(EditorInlineStyle(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) { nsStaticAtom* property = NS_GetStaticAtom(aHTMLProperty); if (NS_WARN_IF(!property)) { return NS_ERROR_INVALID_ARG; } nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute); // MOZ_KnownLive because nsStaticAtom is available until shutting down. nsresult rv = GetInlinePropertyWithAttrValue(MOZ_KnownLive(*property), MOZ_KnownLive(attribute), aValue, aFirst, aAny, aAll, outValue); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::GetInlinePropertyWithAttrValue() failed"); return rv; } nsresult HTMLEditor::GetInlinePropertyWithAttrValue( nsStaticAtom& aHTMLProperty, nsAtom* aAttribute, const nsAString& aValue, bool* aFirst, bool* aAny, bool* aAll, nsAString& outValue) { if (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(EditorInlineStyle(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, __FUNCTION__); 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"); AutoTArray<EditorInlineStyle, 1> removeAllInlineStyles; removeAllInlineStyles.AppendElement(EditorInlineStyle::RemoveAllStyles()); rv = RemoveInlinePropertiesAsSubAction(removeAllInlineStyles); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::RemoveInlinePropertiesAsSubAction() 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); } AutoTArray<EditorInlineStyle, 8> removeInlineStyleAndRelatedElements; AppendInlineStyleAndRelatedStyle(EditorInlineStyle(aHTMLProperty, aAttribute), removeInlineStyleAndRelatedElements); rv = RemoveInlinePropertiesAsSubAction(removeInlineStyleAndRelatedElements); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::RemoveInlinePropertiesAsSubAction() 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); } AutoTArray<EditorInlineStyle, 1> removeOneInlineStyle; removeOneInlineStyle.AppendElement(EditorInlineStyle(*property, attribute)); rv = RemoveInlinePropertiesAsSubAction(removeOneInlineStyle); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::RemoveInlinePropertiesAsSubAction() failed"); return EditorBase::ToGenericNSResult(rv); } void HTMLEditor::AppendInlineStyleAndRelatedStyle( const EditorInlineStyle& aStyleToRemove, nsTArray<EditorInlineStyle>& aStylesToRemove) const { if (nsStaticAtom* similarElementName = aStyleToRemove.GetSimilarElementNameAtom()) { EditorInlineStyle anotherStyle(*similarElementName); if (!aStylesToRemove.Contains(anotherStyle)) { aStylesToRemove.AppendElement(std::move(anotherStyle)); } } else if (aStyleToRemove.mHTMLProperty == nsGkAtoms::font) { if (aStyleToRemove.mAttribute == nsGkAtoms::size) { EditorInlineStyle big(*nsGkAtoms::big), small(*nsGkAtoms::small); if (!aStylesToRemove.Contains(big)) { aStylesToRemove.AppendElement(std::move(big)); } if (!aStylesToRemove.Contains(small)) { aStylesToRemove.AppendElement(std::move(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 (aStyleToRemove.mAttribute == nsGkAtoms::face && !GetEditActionPrincipal()) { EditorInlineStyle tt(*nsGkAtoms::tt); if (!aStylesToRemove.Contains(tt)) { aStylesToRemove.AppendElement(std::move(tt)); } } } if (!aStylesToRemove.Contains(aStyleToRemove)) { aStylesToRemove.AppendElement(aStyleToRemove); } } nsresult HTMLEditor::RemoveInlinePropertiesAsSubAction( const nsTArray<EditorInlineStyle>& aStylesToRemove) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!aStylesToRemove.IsEmpty()); DebugOnly<nsresult> rvIgnored = CommitComposition(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "EditorBase::CommitComposition() failed, but ignored"); if (SelectionRef().IsCollapsed()) { // Manipulating text attributes on a collapsed selection only sets state // for the next text insertion mPendingStylesToApplyToNewContent->ClearStyles(aStylesToRemove); return NS_OK; } // XXX Shouldn't we quit before calling `CommitComposition()`? if (IsPlaintextMailComposer()) { return NS_OK; } { Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); 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"); // TODO: We don't need AutoTransactionsConserveSelection here in the normal // cases, but removing this may cause the behavior with the legacy // mutation event listeners. We should try to delete this in a bug. AutoTransactionsConserveSelection dontChangeMySelection(*this); AutoRangeArray selectionRanges(SelectionRef()); for (const EditorInlineStyle& styleToRemove : aStylesToRemove) { // The ranges may be updated by changing the DOM tree. In strictly // speaking, we should save and restore the ranges at every range loop, // but we've never done so and it may be expensive if there are a lot of // ranges. Therefore, we should do it for every style handling for now. // TODO: We should collect everything required for removing the style before // touching the DOM tree. Then, we need to save and restore the // ranges only once. Maybe<AutoInlineStyleSetter> styleInverter; if (styleToRemove.IsInvertibleWithCSS()) { styleInverter.emplace(EditorInlineStyleAndValue::ToInvert(styleToRemove)); } for (OwningNonNull<nsRange>& selectionRange : selectionRanges.Ranges()) { AutoTrackDOMRange trackSelectionRange(RangeUpdaterRef(), &selectionRange); // If we're removing <a name>, we don't want to split ancestors because // the split fragment will keep working as named anchor. Therefore, we // need to remove all <a name> elements which the selection range even // partially contains. const EditorDOMRange range( styleToRemove.mHTMLProperty == nsGkAtoms::name ? GetExtendedRangeWrappingNamedAnchor( EditorRawDOMRange(selectionRange)) : GetExtendedRangeWrappingEntirelySelectedElements( EditorRawDOMRange(selectionRange))); if (NS_WARN_IF(!range.IsPositioned())) { continue; } // Remove this style from ancestors of our range endpoints, splitting // them as appropriate Result<SplitRangeOffResult, nsresult> splitRangeOffResult = SplitAncestorStyledInlineElementsAtRangeEdges( range, styleToRemove, SplitAtEdges::eAllowToCreateEmptyContainer); if (MOZ_UNLIKELY(splitRangeOffResult.isErr())) { NS_WARNING( "HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges() " "failed"); return splitRangeOffResult.unwrapErr(); } // There is AutoTransactionsConserveSelection, so we don't need to // update selection here. splitRangeOffResult.inspect().IgnoreCaretPointSuggestion(); // 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 EditorDOMRange& splitRange = splitRangeOffResult.inspect().RangeRef(); if (NS_WARN_IF(!splitRange.IsPositioned())) { continue; } AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContentsToInvertStyle; { // Collect top level children in the range first. // TODO: Perhaps, HTMLEditUtils::IsSplittableNode should be used here // instead of EditorUtils::IsEditableContent. AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContentsAroundRange; if (splitRange.InSameContainer() && splitRange.StartRef().IsInTextNode()) { if (!EditorUtils::IsEditableContent( *splitRange.StartRef().ContainerAs<Text>(), EditorType::HTML)) { continue; } arrayOfContentsAroundRange.AppendElement( *splitRange.StartRef().ContainerAs<Text>()); } else if (splitRange.IsInTextNodes() && splitRange.InAdjacentSiblings()) { // Adjacent siblings are in a same element, so the editable state of // both text nodes are always same. if (!EditorUtils::IsEditableContent( *splitRange.StartRef().ContainerAs<Text>(), EditorType::HTML)) { continue; } arrayOfContentsAroundRange.AppendElement( *splitRange.StartRef().ContainerAs<Text>()); arrayOfContentsAroundRange.AppendElement( *splitRange.EndRef().ContainerAs<Text>()); } else { // Append first node if it's a text node but selected not entirely. if (splitRange.StartRef().IsInTextNode() && !splitRange.StartRef().IsStartOfContainer() && EditorUtils::IsEditableContent( *splitRange.StartRef().ContainerAs<Text>(), EditorType::HTML)) { arrayOfContentsAroundRange.AppendElement( *splitRange.StartRef().ContainerAs<Text>()); } // Append all entirely selected nodes. ContentSubtreeIterator subtreeIter; if (NS_SUCCEEDED( subtreeIter.Init(splitRange.StartRef().ToRawRangeBoundary(), splitRange.EndRef().ToRawRangeBoundary()))) { 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)) { arrayOfContentsAroundRange.AppendElement(*node->AsContent()); } } } // Append last node if it's a text node but selected not entirely. if (!splitRange.InSameContainer() && splitRange.EndRef().IsInTextNode() && !splitRange.EndRef().IsEndOfContainer() && EditorUtils::IsEditableContent( *splitRange.EndRef().ContainerAs<Text>(), EditorType::HTML)) { arrayOfContentsAroundRange.AppendElement( *splitRange.EndRef().ContainerAs<Text>()); } } if (styleToRemove.IsInvertibleWithCSS()) { arrayOfContentsToInvertStyle.SetCapacity( arrayOfContentsAroundRange.Length()); } for (OwningNonNull<nsIContent>& content : arrayOfContentsAroundRange) { // We should remove style from the element and its descendants. if (content->IsElement()) { Result<EditorDOMPoint, nsresult> removeStyleResult = RemoveStyleInside(MOZ_KnownLive(*content->AsElement()), styleToRemove, SpecifiedStyle::Preserve); if (MOZ_UNLIKELY(removeStyleResult.isErr())) { NS_WARNING("HTMLEditor::RemoveStyleInside() failed"); return removeStyleResult.unwrapErr(); } // There is AutoTransactionsConserveSelection, so we don't need to // update selection here. // If the element was removed from the DOM tree by // RemoveStyleInside, we need to do nothing for it anymore. if (!content->GetParentNode()) { continue; } } if (styleToRemove.IsInvertibleWithCSS()) { arrayOfContentsToInvertStyle.AppendElement(content); } } // for-loop for arrayOfContentsAroundRange } auto FlushAndStopTrackingAndShrinkSelectionRange = [&]() MOZ_CAN_RUN_SCRIPT { trackSelectionRange.FlushAndStopTracking(); if (NS_WARN_IF(!selectionRange->IsPositioned())) { return; } EditorRawDOMRange range(selectionRange); nsINode* const commonAncestor = range.GetClosestCommonInclusiveAncestor(); // Shrink range for compatibility between browsers. nsIContent* const maybeNextContent = range.StartRef().IsInContentNode() && range.StartRef().IsEndOfContainer() ? AutoInlineStyleSetter::GetNextEditableInlineContent( *range.StartRef().ContainerAs<nsIContent>(), commonAncestor) : nullptr; nsIContent* const maybePreviousContent = range.EndRef().IsInContentNode() && range.EndRef().IsStartOfContainer() ? AutoInlineStyleSetter::GetPreviousEditableInlineContent( *range.EndRef().ContainerAs<nsIContent>(), commonAncestor) : nullptr; if (!maybeNextContent && !maybePreviousContent) { return; } const auto startPoint = maybeNextContent && maybeNextContent != selectionRange->GetStartContainer() ? HTMLEditUtils::GetDeepestEditableStartPointOf< EditorRawDOMPoint>(*maybeNextContent) : range.StartRef(); const auto endPoint = maybePreviousContent && maybePreviousContent != selectionRange->GetEndContainer() ? HTMLEditUtils::GetDeepestEditableEndPointOf< EditorRawDOMPoint>(*maybePreviousContent) : range.EndRef(); DebugOnly<nsresult> rvIgnored = selectionRange->SetStartAndEnd( startPoint.ToRawRangeBoundary(), endPoint.ToRawRangeBoundary()); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "nsRange::SetStartAndEnd() failed, but ignored"); }; if (arrayOfContentsToInvertStyle.IsEmpty()) { FlushAndStopTrackingAndShrinkSelectionRange(); continue; } MOZ_ASSERT(styleToRemove.IsInvertibleWithCSS()); // If the style is specified in parent block and we can remove the // style with inserting new <span> element, we should do it. for (OwningNonNull<nsIContent>& content : arrayOfContentsToInvertStyle) { if (Element* element = Element::FromNode(content)) { // 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. nsresult rv = styleInverter->InvertStyleIfApplied( *this, MOZ_KnownLive(*element)); if (NS_FAILED(rv)) { if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "AutoInlineStyleSetter::InvertStyleIfApplied() failed"); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "AutoInlineStyleSetter::InvertStyleIfApplied() failed, but " "ignored"); } continue; } // 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. if (Text* textNode = Text::FromNode(content)) { const uint32_t startOffset = content == splitRange.StartRef().GetContainer() ? splitRange.StartRef().Offset() : 0u; const uint32_t endOffset = content == splitRange.EndRef().GetContainer() ? splitRange.EndRef().Offset() : textNode->TextDataLength(); Result<SplitRangeOffFromNodeResult, nsresult> wrapTextInStyledElementResult = styleInverter->InvertStyleIfApplied( *this, MOZ_KnownLive(*textNode), startOffset, endOffset); if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) { NS_WARNING("AutoInlineStyleSetter::InvertStyleIfApplied() failed"); return wrapTextInStyledElementResult.unwrapErr(); } SplitRangeOffFromNodeResult unwrappedWrapTextInStyledElementResult = wrapTextInStyledElementResult.unwrap(); // There is AutoTransactionsConserveSelection, so we don't need to // update selection here. unwrappedWrapTextInStyledElementResult.IgnoreCaretPointSuggestion(); // If we've split the content, let's swap content in // arrayOfContentsToInvertStyle with the text node which is applied // the style. if (unwrappedWrapTextInStyledElementResult.DidSplit() && styleToRemove.IsInvertibleWithCSS()) { MOZ_ASSERT(unwrappedWrapTextInStyledElementResult .GetMiddleContentAs<Text>()); if (Text* styledTextNode = unwrappedWrapTextInStyledElementResult .GetMiddleContentAs<Text>()) { if (styledTextNode != content) { arrayOfContentsToInvertStyle.ReplaceElementAt( arrayOfContentsToInvertStyle.Length() - 1, OwningNonNull<nsIContent>(*styledTextNode)); } } } continue; } // If the node is not an element nor a text node, it's invisible. // In this case, we don't need to make it wrapped in new element. } // Finally, we should remove the style from all leaf text nodes if // they still have the style. AutoTArray<OwningNonNull<Text>, 32> leafTextNodes; for (const OwningNonNull<nsIContent>& content : arrayOfContentsToInvertStyle) { // XXX Should we ignore content which has already removed from the // DOM tree by the previous for-loop? if (content->IsElement()) { CollectEditableLeafTextNodes(*content->AsElement(), leafTextNodes); } } for (const OwningNonNull<Text>& textNode : leafTextNodes) { Result<SplitRangeOffFromNodeResult, nsresult> wrapTextInStyledElementResult = styleInverter->InvertStyleIfApplied( *this, MOZ_KnownLive(*textNode), 0, textNode->TextLength()); if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) { NS_WARNING( "AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode() " "failed"); return wrapTextInStyledElementResult.unwrapErr(); } // There is AutoTransactionsConserveSelection, so we don't need to // update selection here. wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion(); } // for-loop of leafTextNodes // styleInverter may have touched a part of the range. Therefore, we // cannot adjust the range without comparing DOM node position and // first/last touched positions, but it may be too expensive. I think // that shrinking only the tracked range boundaries must be enough in most // cases. FlushAndStopTrackingAndShrinkSelectionRange(); } // for-loop of selectionRanges } // for-loop of styles MOZ_ASSERT(!selectionRanges.HasSavedRanges()); nsresult rv = selectionRanges.ApplyTo(SelectionRef()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed"); return rv; } nsresult HTMLEditor::AutoInlineStyleSetter::InvertStyleIfApplied( HTMLEditor& aHTMLEditor, Element& aElement) { MOZ_ASSERT(IsStyleToInvert()); Result<bool, nsresult> isRemovableParentStyleOrError = aHTMLEditor.IsRemovableParentStyleWithNewSpanElement(aElement, *this); if (MOZ_UNLIKELY(isRemovableParentStyleOrError.isErr())) { NS_WARNING("HTMLEditor::IsRemovableParentStyleWithNewSpanElement() failed"); return isRemovableParentStyleOrError.unwrapErr(); } if (!isRemovableParentStyleOrError.unwrap()) { // E.g., text-decoration cannot be override visually in children. // In such cases, we can do nothing. return NS_OK; } // Wrap it into a new element, move it into direct child which has same style, // or specify the style to its parent. Result<CaretPoint, nsresult> pointToPutCaretOrError = ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(aHTMLEditor, aElement); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING( "AutoInlineStyleSetter::" "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed"); return pointToPutCaretOrError.unwrapErr(); } // The caller must update `Selection` later so that we don't need this. pointToPutCaretOrError.unwrap().IgnoreCaretPointSuggestion(); return NS_OK; } Result<SplitRangeOffFromNodeResult, nsresult> HTMLEditor::AutoInlineStyleSetter::InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Text& aTextNode, uint32_t aStartOffset, uint32_t aEndOffset) { MOZ_ASSERT(IsStyleToInvert()); Result<bool, nsresult> isRemovableParentStyleOrError = aHTMLEditor.IsRemovableParentStyleWithNewSpanElement(aTextNode, *this); if (MOZ_UNLIKELY(isRemovableParentStyleOrError.isErr())) { NS_WARNING("HTMLEditor::IsRemovableParentStyleWithNewSpanElement() failed"); return isRemovableParentStyleOrError.propagateErr(); } if (!isRemovableParentStyleOrError.unwrap()) { // E.g., text-decoration cannot be override visually in children. // In such cases, we can do nothing. return SplitRangeOffFromNodeResult(nullptr, &aTextNode, nullptr); } // We need to use new `<span>` element or existing element if it's available // to overwrite parent style. Result<SplitRangeOffFromNodeResult, nsresult> wrapTextInStyledElementResult = SplitTextNodeAndApplyStyleToMiddleNode(aHTMLEditor, aTextNode, aStartOffset, aEndOffset); NS_WARNING_ASSERTION( wrapTextInStyledElementResult.isOk(), "AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode() failed"); return wrapTextInStyledElementResult; } Result<bool, nsresult> HTMLEditor::IsRemovableParentStyleWithNewSpanElement( nsIContent& aContent, const EditorInlineStyle& aStyle) const { // We don't support to remove all inline styles with this path. if (aStyle.IsStyleToClearAllInlineStyles()) { return false; } // First check whether the style is invertible since this is the fastest // check. if (!aStyle.IsInvertibleWithCSS()) { return false; } // If aContent is not an element and it's not in an element, it means that // aContent is disconnected non-element node. In this case, it's never // applied any styles which are invertible. const RefPtr<Element> element = aContent.GetAsElementOrParentElement(); if (MOZ_UNLIKELY(!element)) { return false; } // If parent block has invertible style, we should remove the style with // creating new `<span>` element even in HTML mode because Chrome does it. if (!aStyle.IsCSSSettable(*element)) { return false; } nsAutoString emptyString; Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError = CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle, emptyString); NS_WARNING_ASSERTION(isComputedCSSEquivalentToStyleOrError.isOk(), "CSSEditUtils::IsComputedCSSEquivalentTo() failed"); return isComputedCSSEquivalentToStyleOrError; } 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 = IncrementOrDecrementFontSizeAsSubAction(FontSize::incr); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::IncrementOrDecrementFontSizeAsSubAction(" "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 = IncrementOrDecrementFontSizeAsSubAction(FontSize::decr); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::IncrementOrDecrementFontSizeAsSubAction(" "FontSize::decr) failed"); return EditorBase::ToGenericNSResult(rv); } nsresult HTMLEditor::IncrementOrDecrementFontSizeAsSubAction( FontSize aIncrementOrDecrement) { MOZ_ASSERT(IsEditActionDataAvailable()); // Committing composition and changing font size should be undone together. AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); DebugOnly<nsresult> rvIgnored = CommitComposition(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "EditorBase::CommitComposition() failed, but ignored"); // If selection is collapsed, set typing state if (SelectionRef().IsCollapsed()) { nsStaticAtom& bigOrSmallTagName = aIncrementOrDecrement == FontSize::incr ? *nsGkAtoms::big : *nsGkAtoms::small; // Let's see in what kind of element the selection is if (!SelectionRef().RangeCount()) { return NS_OK; } const auto firstRangeStartPoint = EditorBase::GetFirstSelectionStartPoint<EditorRawDOMPoint>(); if (NS_WARN_IF(!firstRangeStartPoint.IsSet())) { return NS_OK; } Element* element = firstRangeStartPoint.GetContainerOrContainerParentElement(); if (NS_WARN_IF(!element)) { return NS_OK; } if (!HTMLEditUtils::CanNodeContain(*element, bigOrSmallTagName)) { return NS_OK; } // Manipulating text attributes on a collapsed selection only sets state // for the next text insertion mPendingStylesToApplyToNewContent->PreserveStyle(bigOrSmallTagName, nullptr, u""_ns); return NS_OK; } 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"); // TODO: We don't need AutoTransactionsConserveSelection here in the normal // cases, but removing this may cause the behavior with the legacy // mutation event listeners. We should try to delete this in a bug. AutoTransactionsConserveSelection dontChangeMySelection(*this); AutoRangeArray selectionRanges(SelectionRef()); MOZ_ALWAYS_TRUE(selectionRanges.SaveAndTrackRanges(*this)); for (const OwningNonNull<nsRange>& domRange : selectionRanges.Ranges()) { // TODO: We should stop extending the range outside ancestor blocks because // we don't need to do it for setting inline styles. However, here is // chrome only handling path. Therefore, we don't need to fix here // soon. const EditorDOMRange range(GetExtendedRangeWrappingEntirelySelectedElements( EditorRawDOMRange(domRange))); if (NS_WARN_IF(!range.IsPositioned())) { continue; } if (range.InSameContainer() && range.StartRef().IsInTextNode()) { Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult = SetFontSizeOnTextNode( MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()), range.StartRef().Offset(), range.EndRef().Offset(), aIncrementOrDecrement); if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) { NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed"); return wrapInBigOrSmallElementResult.unwrapErr(); } // There is an AutoTransactionsConserveSelection instance so that we don't // need to update selection for this change. wrapInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion(); continue; } // 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.StartRef().ToRawRangeBoundary(), range.EndRef().ToRawRangeBoundary()))) { 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 of bug 1622253 Result<EditorDOMPoint, nsresult> fontChangeOnNodeResult = SetFontSizeWithBigOrSmallElement(MOZ_KnownLive(content), aIncrementOrDecrement); if (MOZ_UNLIKELY(fontChangeOnNodeResult.isErr())) { NS_WARNING("HTMLEditor::SetFontSizeWithBigOrSmallElement() failed"); return fontChangeOnNodeResult.unwrapErr(); } // There is an AutoTransactionsConserveSelection, so we don't need to // update selection here. } } // 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 (range.StartRef().IsInTextNode() && !range.StartRef().IsEndOfContainer() && EditorUtils::IsEditableContent(*range.StartRef().ContainerAs<Text>(), EditorType::HTML)) { Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult = SetFontSizeOnTextNode( MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()), range.StartRef().Offset(), range.StartRef().ContainerAs<Text>()->TextDataLength(), aIncrementOrDecrement); if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) { NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed"); return wrapInBigOrSmallElementResult.unwrapErr(); } // There is an AutoTransactionsConserveSelection instance so that we // don't need to update selection for this change. wrapInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion(); } if (range.EndRef().IsInTextNode() && !range.EndRef().IsStartOfContainer() && EditorUtils::IsEditableContent(*range.EndRef().ContainerAs<Text>(), EditorType::HTML)) { Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult = SetFontSizeOnTextNode( MOZ_KnownLive(*range.EndRef().ContainerAs<Text>()), 0u, range.EndRef().Offset(), aIncrementOrDecrement); if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) { NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed"); return wrapInBigOrSmallElementResult.unwrapErr(); } // There is an AutoTransactionsConserveSelection instance so that we // don't need to update selection for this change. wrapInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion(); } } MOZ_ASSERT(selectionRanges.HasSavedRanges()); selectionRanges.RestoreFromSavedRanges(); nsresult rv = selectionRanges.ApplyTo(SelectionRef()); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed"); return rv; } Result<CreateElementResult, nsresult> HTMLEditor::SetFontSizeOnTextNode( Text& aTextNode, uint32_t aStartOffset, uint32_t aEndOffset, FontSize aIncrementOrDecrement) { // Don't need to do anything if no characters actually selected if (aStartOffset == aEndOffset) { return CreateElementResult::NotHandled(); } if (!aTextNode.GetParentNode() || !HTMLEditUtils::CanNodeContain(*aTextNode.GetParentNode(), *nsGkAtoms::big)) { return CreateElementResult::NotHandled(); } aEndOffset = std::min(aTextNode.Length(), aEndOffset); // Make the range an independent node. RefPtr<Text> textNodeForTheRange = &aTextNode; EditorDOMPoint pointToPutCaret; { auto pointToPutCaretOrError = [&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMPoint, nsresult> { EditorDOMPoint pointToPutCaret; // Split at the end of the range. EditorDOMPoint atEnd(textNodeForTheRange, aEndOffset); if (!atEnd.IsEndOfContainer()) { // We need to split off back of text node Result<SplitNodeResult, nsresult> splitAtEndResult = SplitNodeWithTransaction(atEnd); if (MOZ_UNLIKELY(splitAtEndResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return splitAtEndResult.propagateErr(); } SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap(); if (MOZ_UNLIKELY( !unwrappedSplitAtEndResult.HasCaretPointSuggestion())) { NS_WARNING( "HTMLEditor::SplitNodeWithTransaction() didn't suggest caret " "point"); return Err(NS_ERROR_FAILURE); } unwrappedSplitAtEndResult.MoveCaretPointTo(pointToPutCaret, *this, {}); MOZ_ASSERT_IF(AllowsTransactionsToChangeSelection(), pointToPutCaret.IsSet()); textNodeForTheRange = unwrappedSplitAtEndResult.GetPreviousContentAs<Text>(); MOZ_DIAGNOSTIC_ASSERT(textNodeForTheRange); } // Split at the start of the range. EditorDOMPoint atStart(textNodeForTheRange, aStartOffset); if (!atStart.IsStartOfContainer()) { // We need to split off front of text node Result<SplitNodeResult, nsresult> splitAtStartResult = SplitNodeWithTransaction(atStart); if (MOZ_UNLIKELY(splitAtStartResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return splitAtStartResult.propagateErr(); } SplitNodeResult unwrappedSplitAtStartResult = splitAtStartResult.unwrap(); if (MOZ_UNLIKELY( !unwrappedSplitAtStartResult.HasCaretPointSuggestion())) { NS_WARNING( "HTMLEditor::SplitNodeWithTransaction() didn't suggest caret " "point"); return Err(NS_ERROR_FAILURE); } unwrappedSplitAtStartResult.MoveCaretPointTo(pointToPutCaret, *this, {}); MOZ_ASSERT_IF(AllowsTransactionsToChangeSelection(), pointToPutCaret.IsSet()); textNodeForTheRange = unwrappedSplitAtStartResult.GetNextContentAs<Text>(); MOZ_DIAGNOSTIC_ASSERT(textNodeForTheRange); } return pointToPutCaret; }(); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { // Don't warn here since it should be done in the lambda. return pointToPutCaretOrError.propagateErr(); } pointToPutCaret = pointToPutCaretOrError.unwrap(); } // Look for siblings that are correct type of node nsStaticAtom* const bigOrSmallTagName = aIncrementOrDecrement == FontSize::incr ? nsGkAtoms::big : nsGkAtoms::small; nsCOMPtr<nsIContent> sibling = HTMLEditUtils::GetPreviousSibling( *textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode}); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { // Previous sib is already right kind of inline node; slide this over Result<MoveNodeResult, nsresult> moveTextNodeResult = MoveNodeToEndWithTransaction(*textNodeForTheRange, *sibling); if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveTextNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveTextNodeResult = moveTextNodeResult.unwrap(); unwrappedMoveTextNodeResult.MoveCaretPointTo( pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion}); // XXX Should we return the new container? return CreateElementResult::NotHandled(std::move(pointToPutCaret)); } sibling = HTMLEditUtils::GetNextSibling( *textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode}); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { // Following sib is already right kind of inline node; slide this over Result<MoveNodeResult, nsresult> moveTextNodeResult = MoveNodeWithTransaction(*textNodeForTheRange, EditorDOMPoint(sibling, 0u)); if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveTextNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveTextNodeResult = moveTextNodeResult.unwrap(); unwrappedMoveTextNodeResult.MoveCaretPointTo( pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion}); // XXX Should we return the new container? return CreateElementResult::NotHandled(std::move(pointToPutCaret)); } // Else wrap the node inside font node with appropriate relative size Result<CreateElementResult, nsresult> wrapTextInBigOrSmallElementResult = InsertContainerWithTransaction(*textNodeForTheRange, MOZ_KnownLive(*bigOrSmallTagName)); if (wrapTextInBigOrSmallElementResult.isErr()) { NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed"); return wrapTextInBigOrSmallElementResult; } CreateElementResult unwrappedWrapTextInBigOrSmallElementResult = wrapTextInBigOrSmallElementResult.unwrap(); unwrappedWrapTextInBigOrSmallElementResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); return CreateElementResult( unwrappedWrapTextInBigOrSmallElementResult.UnwrapNewNode(), std::move(pointToPutCaret)); } Result<EditorDOMPoint, nsresult> HTMLEditor::SetFontSizeOfFontElementChildren( nsIContent& aContent, FontSize aIncrementOrDecrement) { // 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. // If this is a font node with size, put big/small inside it. if (aContent.IsHTMLElement(nsGkAtoms::font) && aContent.AsElement()->HasAttr(nsGkAtoms::size)) { EditorDOMPoint pointToPutCaret; // Cycle through children and adjust relative font size. AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents; HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents); for (const auto& child : arrayOfContents) { // MOZ_KnownLive because of bug 1622253 Result<EditorDOMPoint, nsresult> setFontSizeOfChildResult = SetFontSizeWithBigOrSmallElement(MOZ_KnownLive(child), aIncrementOrDecrement); if (MOZ_UNLIKELY(setFontSizeOfChildResult.isErr())) { NS_WARNING("HTMLEditor::WrapContentInBigOrSmallElement() failed"); return setFontSizeOfChildResult; } if (setFontSizeOfChildResult.inspect().IsSet()) { pointToPutCaret = setFontSizeOfChildResult.unwrap(); } } // WrapContentInBigOrSmallElement already calls us recursively, // so we don't need to check our children again. return pointToPutCaret; } // Otherwise cycle through the children. EditorDOMPoint pointToPutCaret; AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents; HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents); for (const auto& child : arrayOfContents) { // MOZ_KnownLive because of bug 1622253 Result<EditorDOMPoint, nsresult> fontSizeChangeResult = SetFontSizeOfFontElementChildren(MOZ_KnownLive(child), aIncrementOrDecrement); if (MOZ_UNLIKELY(fontSizeChangeResult.isErr())) { NS_WARNING("HTMLEditor::SetFontSizeOfFontElementChildren() failed"); return fontSizeChangeResult; } if (fontSizeChangeResult.inspect().IsSet()) { pointToPutCaret = fontSizeChangeResult.unwrap(); } } return pointToPutCaret; } Result<EditorDOMPoint, nsresult> HTMLEditor::SetFontSizeWithBigOrSmallElement( nsIContent& aContent, FontSize aIncrementOrDecrement) { nsStaticAtom* const bigOrSmallTagName = aIncrementOrDecrement == FontSize::incr ? nsGkAtoms::big : nsGkAtoms::small; // Is aContent the opposite of what we want? if ((aIncrementOrDecrement == FontSize::incr && aContent.IsHTMLElement(nsGkAtoms::small)) || (aIncrementOrDecrement == FontSize::decr && aContent.IsHTMLElement(nsGkAtoms::big))) { // First, populate any nested font elements that have the size attr set Result<EditorDOMPoint, nsresult> fontSizeChangeOfDescendantsResult = SetFontSizeOfFontElementChildren(aContent, aIncrementOrDecrement); if (MOZ_UNLIKELY(fontSizeChangeOfDescendantsResult.isErr())) { NS_WARNING("HTMLEditor::SetFontSizeOfFontElementChildren() failed"); return fontSizeChangeOfDescendantsResult; } EditorDOMPoint pointToPutCaret = fontSizeChangeOfDescendantsResult.unwrap(); // In that case, just unwrap the <big> or <small> element. Result<EditorDOMPoint, nsresult> unwrapBigOrSmallElementResult = RemoveContainerWithTransaction(MOZ_KnownLive(*aContent.AsElement())); if (MOZ_UNLIKELY(unwrapBigOrSmallElementResult.isErr())) { NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed"); return unwrapBigOrSmallElementResult; } if (unwrapBigOrSmallElementResult.inspect().IsSet()) { pointToPutCaret = unwrapBigOrSmallElementResult.unwrap(); } return pointToPutCaret; } if (HTMLEditUtils::CanNodeContain(*bigOrSmallTagName, aContent)) { // First, populate any nested font tags that have the size attr set Result<EditorDOMPoint, nsresult> fontSizeChangeOfDescendantsResult = SetFontSizeOfFontElementChildren(aContent, aIncrementOrDecrement); if (MOZ_UNLIKELY(fontSizeChangeOfDescendantsResult.isErr())) { NS_WARNING("HTMLEditor::SetFontSizeOfFontElementChildren() failed"); return fontSizeChangeOfDescendantsResult; } EditorDOMPoint pointToPutCaret = fontSizeChangeOfDescendantsResult.unwrap(); // Next, if next or previous is <big> or <small>, move aContent into it. nsCOMPtr<nsIContent> sibling = HTMLEditUtils::GetPreviousSibling( aContent, {WalkTreeOption::IgnoreNonEditableNode}); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { Result<MoveNodeResult, nsresult> moveNodeResult = MoveNodeToEndWithTransaction(aContent, *sibling); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); unwrappedMoveNodeResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); return pointToPutCaret; } sibling = HTMLEditUtils::GetNextSibling( aContent, {WalkTreeOption::IgnoreNonEditableNode}); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { Result<MoveNodeResult, nsresult> moveNodeResult = MoveNodeWithTransaction(aContent, EditorDOMPoint(sibling, 0u)); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); unwrappedMoveNodeResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); return pointToPutCaret; } // Otherwise, wrap aContent in new <big> or <small> Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult = InsertContainerWithTransaction(aContent, MOZ_KnownLive(*bigOrSmallTagName)); if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed"); return Err(wrapInBigOrSmallElementResult.unwrapErr()); } CreateElementResult unwrappedWrapInBigOrSmallElementResult = wrapInBigOrSmallElementResult.unwrap(); MOZ_ASSERT(unwrappedWrapInBigOrSmallElementResult.GetNewNode()); unwrappedWrapInBigOrSmallElementResult.MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); return pointToPutCaret; } // 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. EditorDOMPoint pointToPutCaret; AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents; HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents); for (const auto& child : arrayOfContents) { // MOZ_KnownLive because of bug 1622253 Result<EditorDOMPoint, nsresult> setFontSizeOfChildResult = SetFontSizeWithBigOrSmallElement(MOZ_KnownLive(child), aIncrementOrDecrement); if (MOZ_UNLIKELY(setFontSizeOfChildResult.isErr())) { NS_WARNING("HTMLEditor::SetFontSizeWithBigOrSmallElement() failed"); return setFontSizeOfChildResult; } if (setFontSizeOfChildResult.inspect().IsSet()) { pointToPutCaret = setFontSizeOfChildResult.unwrap(); } } return pointToPutCaret; } 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( EditorInlineStyle(*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(EditorInlineStyle(*nsGkAtoms::tt), 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( EditorInlineStyle(*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 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