/* -*- 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& aStylesToSet); template nsresult HTMLEditor::SetInlinePropertiesAsSubAction( const AutoTArray& aStylesToSet); template nsresult HTMLEditor::SetInlinePropertiesAroundRanges( AutoRangeArray& aRanges, const AutoTArray& aStylesToSet, const Element& aEditingHost); template nsresult HTMLEditor::SetInlinePropertiesAroundRanges( AutoRangeArray& aRanges, const AutoTArray& 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 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 `` 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 `` 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 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 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 nsresult HTMLEditor::SetInlinePropertiesAsSubAction( const AutoTArray& aStylesToSet) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!aStylesToSet.IsEmpty()); DebugOnly 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 result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } RefPtr 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 nsresult HTMLEditor::SetInlinePropertiesAroundRanges( AutoRangeArray& aRanges, const AutoTArray& aStylesToSet, const Element& aEditingHost) { MOZ_ASSERT(!aRanges.HasSavedRanges()); for (const EditorInlineStyleAndValue& styleToSet : aStylesToSet) { AutoInlineStyleSetter inlineStyleSetter(styleToSet); for (OwningNonNull& domRange : aRanges.Ranges()) { inlineStyleSetter.Reset(); auto rangeOrError = [&]() MOZ_CAN_RUN_SCRIPT -> Result { EditorDOMRange range(domRange); // If we're setting , we want to remove ancestors which set // `font-size` or recursively. Therefore, for // extending the ranges to contain all ancestors in the range, we need // to split ancestors first. // XXX: Blink and WebKit inserts elements to inner most // elements, however, we cannot do it under current design because // once we contain ancestors which have `font-size` or are // , 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 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 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, 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 emptyTextNode = emptyTextNodeOrError.unwrap(); Result caretPointOrError = inlineStyleSetter .ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle( *this, *emptyTextNode); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "AutoInlineStyleSetter::" "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed"); return caretPointOrError.unwrapErr(); } DebugOnly 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(&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() : HTMLEditUtils::GetDeepestEditableStartPointOf< EditorRawDOMPoint>( *inlineStyleSetter.FirstHandledPointRef() .ContainerAs()); const auto endPoint = !inlineStyleSetter.LastHandledPointRef().IsEndOfContainer() ? inlineStyleSetter.LastHandledPointRef() .To() : HTMLEditUtils::GetDeepestEditableEndPointOf< EditorRawDOMPoint>( *inlineStyleSetter.LastHandledPointRef() .ContainerAs()); 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()) because of grabbed by `range`. // MOZ_KnownLive(styleToSet.*) due to bug 1622253. Result wrapTextInStyledElementResult = inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode( *this, MOZ_KnownLive(*range.StartRef().ContainerAs()), 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, 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(), EditorType::HTML)) { // MOZ_KnownLive(...ContainerAs()) because of grabbed by `range`. // MOZ_KnownLive(styleToSet.*) due to bug 1622253. Result wrapTextInStyledElementResult = inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode( *this, MOZ_KnownLive(*range.StartRef().ContainerAs()), range.StartRef().Offset(), range.StartRef().ContainerAs()->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 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(), EditorType::HTML)) { // MOZ_KnownLive(...ContainerAs()) because of grabbed by `range`. // MOZ_KnownLive(styleToSet.mAttribute) due to bug 1622253. Result wrapTextInStyledElementResult = inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode( *this, MOZ_KnownLive(*range.EndRef().ContainerAs()), 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, nsresult> HTMLEditor::AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCandidatePointToInsert, const Element& aEditingHost) { auto pointToInsertNewText = HTMLEditUtils::GetBetterCaretPositionToInsertText( aCandidatePointToInsert, aEditingHost); if (MOZ_UNLIKELY(!pointToInsertNewText.IsSet())) { return RefPtr(); // cannot insert text there } auto pointToInsertNewStyleOrError = [&]() MOZ_CAN_RUN_SCRIPT -> Result { if (!pointToInsertNewText.IsInTextNode()) { return pointToInsertNewText; } if (!pointToInsertNewText.ContainerAs()->TextDataLength()) { return pointToInsertNewText; // Use it } if (pointToInsertNewText.IsStartOfContainer()) { return pointToInsertNewText.ParentPoint(); } if (pointToInsertNewText.IsEndOfContainer()) { return EditorDOMPoint::After(*pointToInsertNewText.ContainerAs()); } Result 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(); }(); 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( pointToInsertNewStyleOrError.inspect().ContainerAs()); } // Otherwise, we need an empty text node to create new inline style. RefPtr newEmptyTextNode = aHTMLEditor.CreateTextNode(u""_ns); if (MOZ_UNLIKELY(!newEmptyTextNode)) { NS_WARNING("EditorBase::CreateTextNode() failed"); return Err(NS_ERROR_FAILURE); } Result insertNewTextNodeResult = aHTMLEditor.InsertNodeWithTransaction( *newEmptyTextNode, pointToInsertNewStyleOrError.inspect()); if (MOZ_UNLIKELY(insertNewTextNodeResult.isErr())) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertNewTextNodeResult.propagateErr(); } insertNewTextNodeResult.inspect().IgnoreCaretPointSuggestion(); return newEmptyTextNode; } Result 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 , , etc. if (aElement.IsHTMLElement(&HTMLPropertyRef()) && !HTMLEditUtils::ElementHasAttribute(aElement) && !mAttribute) { return true; } // Now look for things like 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 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 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 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, or , 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 // for compatibility with the other browsers. if (&HTMLPropertyRef() == nsGkAtoms::font && aStyledElement.IsHTMLElement(nsGkAtoms::font)) { return true; } // If the element has one or more
(even if it's invisible), we don't // want to use the 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 when // `

{ abc }

`. 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
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 for making // `[abc def]` become // `abc def` rather // than `abc def`. return false; } Result HTMLEditor::AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode( HTMLEditor& aHTMLEditor, Text& aText, uint32_t aStartOffset, uint32_t aEndOffset) { const RefPtr 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 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 { EditorDOMPoint atEnd(&aText, aEndOffset); if (atEnd.IsEndOfContainer()) { return SplitNodeResult::NotHandled(atEnd); } // We need to split off back of text node Result 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 { 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 = 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() : nullptr; Text* const middleTextNode = unwrappedSplitAtStartResult.DidSplit() ? unwrappedSplitAtStartResult.GetNextContentAs() : (unwrappedSplitAtEndResult.DidSplit() ? unwrappedSplitAtEndResult.GetPreviousContentAs() : &aText); Text* const rightTextNode = unwrappedSplitAtEndResult.DidSplit() ? unwrappedSplitAtEndResult.GetNextContentAs() : 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(*sibling->AsElement()); Result 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 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(*sibling->AsElement()); Result 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 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 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 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, 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& content : arrayOfContents) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result 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 previousSibling = HTMLEditUtils::GetPreviousSibling( aContent, {WalkTreeOption::IgnoreNonEditableNode}); nsCOMPtr nextSibling = HTMLEditUtils::GetNextSibling( aContent, {WalkTreeOption::IgnoreNonEditableNode}); if (RefPtr previousElement = Element::FromNodeOrNull(previousSibling)) { Result canMoveIntoPreviousSibling = ElementIsGoodContainerForTheStyle(aHTMLEditor, *previousElement); if (MOZ_UNLIKELY(canMoveIntoPreviousSibling.isErr())) { NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed"); return canMoveIntoPreviousSibling.propagateErr(); } if (canMoveIntoPreviousSibling.inspect()) { Result moveNodeResult = aHTMLEditor.MoveNodeToEndWithTransaction(aContent, *previousSibling); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); RefPtr nextElement = Element::FromNodeOrNull(nextSibling); if (!nextElement) { OnHandled(aContent); return CaretPoint(unwrappedMoveNodeResult.UnwrapCaretPoint()); } Result 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 = 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()); } } if (RefPtr nextElement = Element::FromNodeOrNull(nextSibling)) { Result canMoveIntoNextSibling = ElementIsGoodContainerForTheStyle(aHTMLEditor, *nextElement); if (MOZ_UNLIKELY(canMoveIntoNextSibling.isErr())) { NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed"); return canMoveIntoNextSibling.propagateErr(); } if (canMoveIntoNextSibling.inspect()) { Result 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 = aContent.GetAsElementOrParentElement()) { if (IsCSSSettable(*element)) { nsAutoString value(mAttributeValue); // MOZ_KnownLive(element) because it's aContent. Result 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 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 result = ApplyCSSTextDecoration(aHTMLEditor, aContent); NS_WARNING_ASSERTION( result.isOk(), "AutoInlineStyleSetter::ApplyCSSTextDecoration() failed"); return result; } EditorDOMPoint pointToPutCaret; RefPtr 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 . if (!styledElement) { Result 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 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 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 HTMLEditor::AutoInlineStyleSetter::ApplyCSSTextDecoration( HTMLEditor& aHTMLEditor, nsIContent& aContent) { MOZ_ASSERT(IsStyleOfTextDecoration(IgnoreSElement::No)); EditorDOMPoint pointToPutCaret; RefPtr 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 . if (styledElement && styledElement->IsAnyOfHTMLElements( nsGkAtoms::u, nsGkAtoms::s, nsGkAtoms::strike)) { Result 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 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 element. else { Result 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 HTMLEditor::AutoInlineStyleSetter:: ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(HTMLEditor& aHTMLEditor, nsIContent& aContent) { if (NS_WARN_IF(!aContent.GetParentNode())) { return Err(NS_ERROR_FAILURE); } OwningNonNull parent = *aContent.GetParentNode(); nsCOMPtr previousSibling = aContent.GetPreviousSibling(), nextSibling = aContent.GetNextSibling(); EditorDOMPoint pointToPutCaret; if (aContent.IsElement()) { Result 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 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 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, 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& content : nodesToSet) { // MOZ_KnownLive because 'nodesToSet' is guaranteed to // keep it alive. Result 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 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()) { 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()) { 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(); // 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(); } // If the start boundary is at end of a node, we need to shrink the range // to next content, e.g., `abc[def` should be `abc[def` unless the // is not entirely selected. auto* const nextContentOrStartContainer = [&]() -> nsIContent* { if (!startRef.IsInContentNode()) { return nullptr; } if (!startRef.IsEndOfContainer()) { return startRef.ContainerAs(); } nsIContent* const nextContent = AutoInlineStyleSetter::GetNextEditableInlineContent( *startRef.ContainerAs(), &aCommonAncestorOfRange); return nextContent ? nextContent : startRef.ContainerAs(); }(); if (MOZ_UNLIKELY(!nextContentOrStartContainer)) { return startRef.To(); } EditorRawDOMPoint startPoint = nextContentOrStartContainer != startRef.ContainerAs() ? EditorRawDOMPoint(nextContentOrStartContainer) : startRef.To(); 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
, , 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 `
`, 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(); // 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(); } // If the end boundary is at start of a node, we need to shrink the range // to previous content, e.g., `abc]def` should be `abc]def` unless // the is not entirely selected. auto* const previousContentOrEndContainer = [&]() -> nsIContent* { if (!endRef.IsInContentNode()) { return nullptr; } if (!endRef.IsStartOfContainer()) { return endRef.ContainerAs(); } nsIContent* const previousContent = AutoInlineStyleSetter::GetPreviousEditableInlineContent( *endRef.ContainerAs(), &aCommonAncestorOfRange); return previousContent ? previousContent : endRef.ContainerAs(); }(); if (MOZ_UNLIKELY(!previousContentOrEndContainer)) { return endRef.To(); } EditorRawDOMPoint endPoint = previousContentOrEndContainer != endRef.ContainerAs() ? EditorRawDOMPoint::After(*previousContentOrEndContainer) : endRef.To(); 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
, , 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 `
`, 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 , we always set a . const bool isSettingFontElement = IsStyleOfFontSize() || (!aHTMLEditor.IsCSSEnabled() && IsStyleOfFontElement()); Element* mostDistantStartParentHavingStyle = nullptr; for (Element* parent : startPoint.GetContainer()->InclusiveAncestorsOfType()) { 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 element and there is a 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
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 , we always set a . const bool isSettingFontElement = IsStyleOfFontSize() || (!aHTMLEditor.IsCSSEnabled() && IsStyleOfFontElement()); Element* mostDistantEndParentHavingStyle = nullptr; for (Element* parent : endPoint.GetContainer()->InclusiveAncestorsOfType()) { 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 element and there is a 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., `[abc def]` should be // styled as `abc def`. // Similarly, if the range crosses a block boundary, we should do same thing. // I.e., `

[abc

def]

` should become // `

abc

def

`. if (aStartPoint.GetContainer() != aEndPoint.GetContainer()) { while (aStartPoint.GetContainer() != &aCommonAncestor && aStartPoint.IsInContentNode() && aStartPoint.GetContainerParent() && aStartPoint.IsStartOfContainer()) { if (!EditorUtils::IsEditableContent( *aStartPoint.ContainerAs(), EditorType::HTML) || (aStartPoint.ContainerAs()->IsElement() && (HTMLEditUtils::IsBlockElement( *aStartPoint.ContainerAs(), BlockInlineCheck::UseComputedDisplayOutsideStyle) || HTMLEditUtils::IsDisplayInsideFlowRoot( *aStartPoint.ContainerAs())))) { break; } aStartPoint = aStartPoint.ParentPoint(); } while (aEndPoint.GetContainer() != &aCommonAncestor && aEndPoint.IsInContentNode() && aEndPoint.GetContainerParent() && aEndPoint.IsEndOfContainer()) { if (!EditorUtils::IsEditableContent(*aEndPoint.ContainerAs(), EditorType::HTML) || (aEndPoint.ContainerAs()->IsElement() && (HTMLEditUtils::IsBlockElement( *aEndPoint.ContainerAs(), BlockInlineCheck::UseComputedDisplayOutsideStyle) || HTMLEditUtils::IsDisplayInsideFlowRoot( *aEndPoint.ContainerAs())))) { break; } aEndPoint.SetAfter(aEndPoint.ContainerAs()); } } // 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 // 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() && EditorUtils::IsEditableContent( *aStartPoint.ContainerParentAs(), EditorType::HTML) && (!aStartPoint.GetContainerAs() || !HTMLEditUtils::IsContainerNode( *aStartPoint.ContainerAs())) && EditorUtils::IsEditableContent(*aStartPoint.ContainerAs(), EditorType::HTML)) { aStartPoint = aStartPoint.ParentPoint(); MOZ_ASSERT(aStartPoint.IsSet()); } if (aEndPoint.IsInContentNode() && aEndPoint.IsEndOfContainer() && aEndPoint.GetContainerParentAs() && EditorUtils::IsEditableContent( *aEndPoint.ContainerParentAs(), EditorType::HTML) && (!aEndPoint.GetContainerAs() || !HTMLEditUtils::IsContainerNode( *aEndPoint.ContainerAs())) && EditorUtils::IsEditableContent(*aEndPoint.ContainerAs(), 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() || !ElementIsGoodContainerToSetStyle( *aStartPoint.ChildAs())) && // but don't cross block boundary at climbing up the tree !HTMLEditUtils::IsBlockElement( *aStartPoint.ContainerAs(), BlockInlineCheck::UseComputedDisplayOutsideStyle) && // and the container is a good editable element to set CSS style aStartPoint.GetContainerAs() && ElementIsGoodContainerToSetStyle( *aStartPoint.ContainerAs())) { 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() || !ElementIsGoodContainerToSetStyle( *aEndPoint.GetPreviousSiblingOfChildAs())) && // but don't cross block boundary at climbing up the tree !HTMLEditUtils::IsBlockElement( *aEndPoint.ContainerAs(), BlockInlineCheck::UseComputedDisplayOutsideStyle) && // and the container is a good editable element to set CSS style aEndPoint.GetContainerAs() && ElementIsGoodContainerToSetStyle( *aEndPoint.ContainerAs())) { aEndPoint.SetAfter(aEndPoint.GetContainer()); MOZ_ASSERT(aEndPoint.IsSet()); } } return EditorRawDOMRange(std::move(aStartPoint), std::move(aEndPoint)); } Result 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
element, let's extend the // range to contain the
element. EditorDOMRange range(aRange); if (range.EndRef().IsInContentNode()) { const WSScanResult nextContentData = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &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[def]ghi`, shouldn't shrink it as // `abc[def]ghi`. EditorRawDOMPoint startPoint, endPoint; if (range.Collapsed()) { startPoint = endPoint = range.StartRef().To(); } 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[
]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(); endPoint = range.EndRef().To(); } } // 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 // // [def] // // ghi // In this case, we need to wrap the other element if setting // background color. Then, the inner element is removed and the // other element's style attribute will be updated rather than // inserting new 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 // `[abcdef]`, we should make it // `abcdef` rather than // `abcdef`. 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 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 { AutoTrackDOMRange tracker(RangeUpdaterRef(), &range); Result 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(); 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 { AutoTrackDOMRange tracker(RangeUpdaterRef(), &range); Result result = SplitAncestorStyledInlineElementsAt(range.EndRef(), aStyle, aSplitAtEdges); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed"); return result; } tracker.FlushAndStopTracking(); if (result.inspect().Handled()) { auto endOfRange = result.inspect().AtSplitPoint(); 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 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 `` element // and CSS. For example, `

` 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, 24> arrayOfParents; for (Element* element : aPointToSplit.GetContainer()->InclusiveAncestorsOfType()) { 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 : arrayOfParents) { auto isSetByCSSOrError = [&]() -> Result { 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 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 or , we won't use vertical-align CSS property // because / 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 , 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 related styles, it means that we're not // toggling the style. In this case, we need to remove parent // elements and/or update parent 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 = SplitNodeDeepWithTransaction(MOZ_KnownLive(element), result.AtSplitPoint(), 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 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 // `

a[]bc

`, we want to make it as // `

abc

`. EditorDOMPoint pointToPutCaret(aPoint); AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret); Result 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., `

a[]bc

` 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: //

bc

// ^^ 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., `

a[]bc

` for nsGkAtoms::b and // we're in CSS mode. // XXX Chrome resets block style and creates `` 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 // `

abc

`. // ^^^^^^^^^^^^^^ nsIContent* firstLeafChildOfNextNode = HTMLEditUtils::GetFirstLeafContent( *unwrappedSplitNodeResult.GetNextContent(), {LeafNodeType::OnlyLeafNode}); EditorDOMPoint atStartOfNextNode( firstLeafChildOfNextNode ? firstLeafChildOfNextNode : unwrappedSplitNodeResult.GetNextContent(), 0); RefPtr brElement; // But don't try to split non-containers like `
`, `
` and `` // element. if (!atStartOfNextNode.IsInContentNode() || !HTMLEditUtils::IsContainerNode( *atStartOfNextNode.ContainerAs())) { // If it's a `
` element, let's move it into new node later. brElement = HTMLBRElement::FromNode(atStartOfNextNode.GetContainer()); if (!atStartOfNextNode.GetContainerParentAs()) { NS_WARNING("atStartOfNextNode was in an orphan node"); return Err(NS_ERROR_FAILURE); } atStartOfNextNode.Set(atStartOfNextNode.GetContainerParent(), 0); } AutoTrackDOMPoint trackPointToPutCaret2(RangeUpdaterRef(), &pointToPutCaret); Result 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.,
abc[]
, then now, it's been // changed to: //
abc[]
// ^^^^^^^^^^^^^^ // We will change it to: //
abc[]
// ^^ // And if it has only padding
element, we should move it into the // previous 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()); } // Once we remove
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
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(); const auto splitPointAtStartOfNextNode = unwrappedSplitResultAtStartOfNextNode.AtSplitPoint(); return EditorDOMPoint(splitPoint.GetContainer(), splitPointAtStartOfNextNode.Offset()); } // Now, we want to put `
` element into the empty split node if // it was in next node of the first split. // E.g., `

a
bc

` nsIContent* firstLeafChildOfPreviousNode = HTMLEditUtils::GetFirstLeafContent( *unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(), {LeafNodeType::OnlyLeafNode}); pointToPutCaret.Set( firstLeafChildOfPreviousNode ? firstLeafChildOfPreviousNode : unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(), 0); // If the right node starts with a `
`, 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 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 insertBRElementResult = InsertNodeWithTransaction(*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
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: // - abc{}
// ^^^^^^^ // - abc
// ^^^^^^^ 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
element, there may be empty // inline elements around it. We don't need them anymore because user // cannot put caret into them. E.g., abc[]

has // been changed to abc{}

now. // ^^^^^^^^^^^^^^^^^^ // We don't need the empty . else if (HTMLEditUtils::IsEmptyNode( *unwrappedSplitResultAtStartOfNextNode.GetNextContent(), {EmptyCheckOption::TreatListItemAsVisible, EmptyCheckOption::TreatTableCellAsVisible})) { AutoTArray, 4> emptyInlineContainerElements; HTMLEditUtils::CollectEmptyInlineContainerDescendants( *unwrappedSplitResultAtStartOfNextNode.GetNextContentAs(), emptyInlineContainerElements, {EmptyCheckOption::TreatSingleBRElementAsVisible, EmptyCheckOption::TreatListItemAsVisible, EmptyCheckOption::TreatTableCellAsVisible}, BlockInlineCheck::UseComputedDisplayOutsideStyle); for (const OwningNonNull& 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: // `

a[]bc

` // ^^^^^^^^^ if (auto* const previousElementOfSplitPoint = unwrappedSplitResultAtStartOfNextNode .GetPreviousContentAs()) { // 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 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 HTMLEditor::RemoveStyleInside( Element& aElement, const EditorInlineStyle& aStyleToRemove, SpecifiedStyle aSpecifiedStyle) { // First, handle all descendants. AutoTArray, 32> arrayOfChildContents; HTMLEditUtils::CollectAllChildren(aElement, arrayOfChildContents); EditorDOMPoint pointToPutCaret; for (const OwningNonNull& child : arrayOfChildContents) { if (!child->IsElement()) { continue; } Result 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 { if (!aStyleToRemove.IsCSSRemovable(aElement)) { return false; } MOZ_ASSERT(!aStyleToRemove.IsStyleToClearAllInlineStyles()); Result 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 or , we won't use vertical-align CSS // property because / 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
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 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 and still has some attributes, we should // replace it with new . auto ReplaceWithNewSpan = [&]() { if (aStyleToRemove.IsStyleToClearAllInlineStyles()) { return false; // Remove it even if it has attributes. } if (aElement.IsHTMLElement(nsGkAtoms::span)) { return false; // Don't replace with new . } 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 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 // 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 { 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 element have no // attributes, we can delete it. if (styleSpecified && aElement.IsHTMLElement(nsGkAtoms::span)) { return true; } return false; }; if (RemoveElement()) { Result 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 can have block elements with low level DOM API. We want // to remove any ancestors to remove the style. EditorRawDOMRange newRange(aRange); for (Element* element : aRange.StartRef().GetContainer()->InclusiveAncestorsOfType()) { if (!HTMLEditUtils::IsNamedAnchor(element)) { continue; } newRange.SetStart(EditorRawDOMPoint(element)); } for (Element* element : aRange.EndRef().GetContainer()->InclusiveAncestorsOfType()) { 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(), EditorType::HTML)) { break; } newRange.SetStart(newRange.StartRef().ParentPoint()); } while (newRange.EndRef().IsInContentNode() && newRange.EndRef().IsEndOfContainer()) { if (!EditorUtils::IsEditableContent( *newRange.EndRef().ContainerAs(), EditorType::HTML)) { break; } newRange.SetEnd( EditorRawDOMPoint::After(*newRange.EndRef().ContainerAs())); } 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 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 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 endNode = range->GetEndContainer(); uint32_t endOffset = range->EndOffset(); PostContentIterator postOrderIter; DebugOnly 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 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 = 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 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 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. and // , 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)" // uses blue color // These two values should be the same if we are checking ``. // 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 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 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 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& 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 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 // 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& aStylesToRemove) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!aStylesToRemove.IsEmpty()); DebugOnly 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 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 styleInverter; if (styleToRemove.IsInvertibleWithCSS()) { styleInverter.emplace(EditorInlineStyleAndValue::ToInvert(styleToRemove)); } for (OwningNonNull& selectionRange : selectionRanges.Ranges()) { AutoTrackDOMRange trackSelectionRange(RangeUpdaterRef(), &selectionRange); // If we're removing , we don't want to split ancestors because // the split fragment will keep working as named anchor. Therefore, we // need to remove all 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 = 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, 64> arrayOfContentsToInvertStyle; { // Collect top level children in the range first. // TODO: Perhaps, HTMLEditUtils::IsSplittableNode should be used here // instead of EditorUtils::IsEditableContent. AutoTArray, 64> arrayOfContentsAroundRange; if (splitRange.InSameContainer() && splitRange.StartRef().IsInTextNode()) { if (!EditorUtils::IsEditableContent( *splitRange.StartRef().ContainerAs(), EditorType::HTML)) { continue; } arrayOfContentsAroundRange.AppendElement( *splitRange.StartRef().ContainerAs()); } 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(), EditorType::HTML)) { continue; } arrayOfContentsAroundRange.AppendElement( *splitRange.StartRef().ContainerAs()); arrayOfContentsAroundRange.AppendElement( *splitRange.EndRef().ContainerAs()); } 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(), EditorType::HTML)) { arrayOfContentsAroundRange.AppendElement( *splitRange.StartRef().ContainerAs()); } // Append all entirely selected nodes. ContentSubtreeIterator subtreeIter; if (NS_SUCCEEDED( subtreeIter.Init(splitRange.StartRef().ToRawRangeBoundary(), splitRange.EndRef().ToRawRangeBoundary()))) { for (; !subtreeIter.IsDone(); subtreeIter.Next()) { nsCOMPtr 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(), EditorType::HTML)) { arrayOfContentsAroundRange.AppendElement( *splitRange.EndRef().ContainerAs()); } } if (styleToRemove.IsInvertibleWithCSS()) { arrayOfContentsToInvertStyle.SetCapacity( arrayOfContentsAroundRange.Length()); } for (OwningNonNull& content : arrayOfContentsAroundRange) { // We should remove style from the element and its descendants. if (content->IsElement()) { Result 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(), commonAncestor) : nullptr; nsIContent* const maybePreviousContent = range.EndRef().IsInContentNode() && range.EndRef().IsStartOfContainer() ? AutoInlineStyleSetter::GetPreviousEditableInlineContent( *range.EndRef().ContainerAs(), 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 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 element, we should do it. for (OwningNonNull& 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 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()); if (Text* styledTextNode = unwrappedWrapTextInStyledElementResult .GetMiddleContentAs()) { if (styledTextNode != content) { arrayOfContentsToInvertStyle.ReplaceElementAt( arrayOfContentsToInvertStyle.Length() - 1, OwningNonNull(*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, 32> leafTextNodes; for (const OwningNonNull& 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& textNode : leafTextNodes) { Result 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 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 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 HTMLEditor::AutoInlineStyleSetter::InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Text& aTextNode, uint32_t aStartOffset, uint32_t aEndOffset) { MOZ_ASSERT(IsStyleToInvert()); Result 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 `` element or existing element if it's available // to overwrite parent style. Result wrapTextInStyledElementResult = SplitTextNodeAndApplyStyleToMiddleNode(aHTMLEditor, aTextNode, aStartOffset, aEndOffset); NS_WARNING_ASSERTION( wrapTextInStyledElementResult.isOk(), "AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode() failed"); return wrapTextInStyledElementResult; } Result 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 = aContent.GetAsElementOrParentElement(); if (MOZ_UNLIKELY(!element)) { return false; } // If parent block has invertible style, we should remove the style with // creating new `` element even in HTML mode because Chrome does it. if (!aStyle.IsCSSSettable(*element)) { return false; } nsAutoString emptyString; Result isComputedCSSEquivalentToStyleOrError = CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle, emptyString); NS_WARNING_ASSERTION(isComputedCSSEquivalentToStyleOrError.isOk(), "CSSEditUtils::IsComputedCSSEquivalentTo() failed"); return isComputedCSSEquivalentToStyleOrError; } void HTMLEditor::CollectEditableLeafTextNodes( Element& aElement, nsTArray>& 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 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(); 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& 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 wrapInBigOrSmallElementResult = SetFontSizeOnTextNode( MOZ_KnownLive(*range.StartRef().ContainerAs()), 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> arrayOfContents; for (; !subtreeIter.IsDone(); subtreeIter.Next()) { if (NS_WARN_IF(!subtreeIter.GetCurrentNode()->IsContent())) { return NS_ERROR_FAILURE; } OwningNonNull 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& content : arrayOfContents) { // MOZ_KnownLive because of bug 1622253 Result 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(), EditorType::HTML)) { Result wrapInBigOrSmallElementResult = SetFontSizeOnTextNode( MOZ_KnownLive(*range.StartRef().ContainerAs()), range.StartRef().Offset(), range.StartRef().ContainerAs()->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(), EditorType::HTML)) { Result wrapInBigOrSmallElementResult = SetFontSizeOnTextNode( MOZ_KnownLive(*range.EndRef().ContainerAs()), 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 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 textNodeForTheRange = &aTextNode; EditorDOMPoint pointToPutCaret; { auto pointToPutCaretOrError = [&]() MOZ_CAN_RUN_SCRIPT -> Result { 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 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(); 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 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(); 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 sibling = HTMLEditUtils::GetPreviousSibling( *textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode}); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { // Previous sib is already right kind of inline node; slide this over Result 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 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 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 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, 32> arrayOfContents; HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents); for (const auto& child : arrayOfContents) { // MOZ_KnownLive because of bug 1622253 Result 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, 32> arrayOfContents; HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents); for (const auto& child : arrayOfContents) { // MOZ_KnownLive because of bug 1622253 Result 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 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 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 or element. Result 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 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 or , move aContent into it. nsCOMPtr sibling = HTMLEditUtils::GetPreviousSibling( aContent, {WalkTreeOption::IgnoreNonEditableNode}); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { Result 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 = 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 or Result 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, 32> arrayOfContents; HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents); for (const auto& child : arrayOfContents) { // MOZ_KnownLive because of bug 1622253 Result 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