/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 sw=2 et tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "HTMLEditor.h" #include #include #include "CSSEditUtils.h" #include "EditAction.h" #include "EditorDOMPoint.h" #include "EditorUtils.h" #include "HTMLEditHelpers.h" #include "HTMLEditUtils.h" #include "TypeInState.h" // for SpecifiedStyle #include "WSRunObject.h" #include "mozilla/Assertions.h" #include "mozilla/CheckedInt.h" #include "mozilla/ContentIterator.h" #include "mozilla/IntegerRange.h" #include "mozilla/InternalMutationEvent.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/Maybe.h" #include "mozilla/OwningNonNull.h" #include "mozilla/Preferences.h" #include "mozilla/RangeUtils.h" #include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_* #include "mozilla/TextComposition.h" #include "mozilla/UniquePtr.h" #include "mozilla/Unused.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/RangeBinding.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/StaticRange.h" #include "mozilla/mozalloc.h" #include "nsAString.h" #include "nsAlgorithm.h" #include "nsAtom.h" #include "nsCRT.h" #include "nsCRTGlue.h" #include "nsComponentManagerUtils.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsFrameSelection.h" #include "nsGkAtoms.h" #include "nsHTMLDocument.h" #include "nsIContent.h" #include "nsID.h" #include "nsIFrame.h" #include "nsINode.h" #include "nsLiteralString.h" #include "nsPrintfCString.h" #include "nsRange.h" #include "nsReadableUtils.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsStyledElement.h" #include "nsTArray.h" #include "nsTextNode.h" #include "nsThreadUtils.h" #include "nsUnicharUtils.h" // Workaround for windows headers #ifdef SetProp # undef SetProp #endif class nsISupports; namespace mozilla { using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using LeafNodeType = HTMLEditUtils::LeafNodeType; using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; using StyleDifference = HTMLEditUtils::StyleDifference; using WalkTextOption = HTMLEditUtils::WalkTextOption; using WalkTreeDirection = HTMLEditUtils::WalkTreeDirection; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; /******************************************************** * first some helpful functors we will use ********************************************************/ static bool IsStyleCachePreservingSubAction(EditSubAction aEditSubAction) { switch (aEditSubAction) { case EditSubAction::eDeleteSelectedContent: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::eCreateOrChangeList: case EditSubAction::eIndent: case EditSubAction::eOutdent: case EditSubAction::eSetOrClearAlignment: case EditSubAction::eCreateOrRemoveBlock: case EditSubAction::eMergeBlockContents: case EditSubAction::eRemoveList: case EditSubAction::eCreateOrChangeDefinitionListItem: case EditSubAction::eInsertElement: case EditSubAction::eInsertQuotation: case EditSubAction::eInsertQuotedText: return true; default: return false; } } template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock( EditorDOMPoint& aStartPoint, EditorDOMPoint& aEndPoint, const Element& aEditingHost) const; template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock( EditorRawDOMPoint& aStartPoint, EditorDOMPoint& aEndPoint, const Element& aEditingHost) const; template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock( EditorDOMPoint& aStartPoint, EditorRawDOMPoint& aEndPoint, const Element& aEditingHost) const; template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock( EditorRawDOMPoint& aStartPoint, EditorRawDOMPoint& aEndPoint, const Element& aEditingHost) const; template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorDOMRange& aRange); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorRawDOMRange& aRange); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorRawDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces( const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint); template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const EditorDOMRange& aRange, EditSubAction aEditSubAction) const; template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const EditorRawDOMRange& aRange, EditSubAction aEditSubAction) const; template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint, EditSubAction aEditSubAction) const; template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const EditorRawDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint, EditSubAction aEditSubAction) const; template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const EditorDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint, EditSubAction aEditSubAction) const; template already_AddRefed HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd( const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint, EditSubAction aEditSubAction) const; template EditorDOMPoint HTMLEditor::GetCurrentHardLineStartPoint( const EditorDOMPoint& aPoint, EditSubAction aEditSubAction, const Element& aEditingHost) const; template EditorDOMPoint HTMLEditor::GetCurrentHardLineStartPoint( const EditorRawDOMPoint& aPoint, EditSubAction aEditSubAction, const Element& aEditingHost) const; template EditorDOMPoint HTMLEditor::GetCurrentHardLineEndPoint( const EditorDOMPoint& aPoint, const Element& aEditingHost) const; template EditorDOMPoint HTMLEditor::GetCurrentHardLineEndPoint( const EditorRawDOMPoint& aPoint, const Element& aEditingHost) const; nsresult HTMLEditor::InitEditorContentAndSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); nsresult rv = EditorBase::InitEditorContentAndSelection(); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::InitEditorContentAndSelection() failed"); return rv; } Element* bodyOrDocumentElement = GetRoot(); if (NS_WARN_IF(!bodyOrDocumentElement && !GetDocument())) { return NS_ERROR_FAILURE; } if (!bodyOrDocumentElement) { return NS_OK; } rv = InsertBRElementToEmptyListItemsAndTableCellsInRange( RawRangeBoundary(bodyOrDocumentElement, 0u), RawRangeBoundary(bodyOrDocumentElement, bodyOrDocumentElement->GetChildCount())); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange() " "failed, but ignored"); return NS_OK; } void HTMLEditor::OnStartToHandleTopLevelEditSubAction( EditSubAction aTopLevelEditSubAction, nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!aRv.Failed()); EditorBase::OnStartToHandleTopLevelEditSubAction( aTopLevelEditSubAction, aDirectionOfTopLevelEditSubAction, aRv); MOZ_ASSERT(GetTopLevelEditSubAction() == aTopLevelEditSubAction); MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == aDirectionOfTopLevelEditSubAction); if (NS_WARN_IF(Destroyed())) { aRv.Throw(NS_ERROR_EDITOR_DESTROYED); return; } if (!mInitSucceeded) { return; // We should do nothing if we're being initialized. } NS_WARNING_ASSERTION( !aRv.Failed(), "EditorBase::OnStartToHandleTopLevelEditSubAction() failed"); // Remember where our selection was before edit action took place: const auto atCompositionStart = GetFirstIMESelectionStartPoint(); if (atCompositionStart.IsSet()) { // If there is composition string, let's remember current composition // range. TopLevelEditSubActionDataRef().mSelectedRange->StoreRange( atCompositionStart, GetLastIMESelectionEndPoint()); } else { // Get the selection location // XXX This may occur so that I think that we shouldn't throw exception // in this case. if (NS_WARN_IF(!SelectionRef().RangeCount())) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } if (const nsRange* range = SelectionRef().GetRangeAt(0)) { TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(*range); } } // Register with range updater to track this as we perturb the doc RangeUpdaterRef().RegisterRangeItem( *TopLevelEditSubActionDataRef().mSelectedRange); // Remember current inline styles for deletion and normal insertion ops bool cacheInlineStyles; switch (aTopLevelEditSubAction) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: cacheInlineStyles = true; break; default: cacheInlineStyles = IsStyleCachePreservingSubAction(aTopLevelEditSubAction); break; } if (cacheInlineStyles) { nsCOMPtr containerContent = nsIContent::FromNodeOrNull( aDirectionOfTopLevelEditSubAction == nsIEditor::eNext ? TopLevelEditSubActionDataRef().mSelectedRange->mEndContainer : TopLevelEditSubActionDataRef().mSelectedRange->mStartContainer); if (NS_WARN_IF(!containerContent)) { aRv.Throw(NS_ERROR_FAILURE); return; } nsresult rv = CacheInlineStyles(*containerContent); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CacheInlineStyles() failed"); aRv.Throw(rv); return; } } // Stabilize the document against contenteditable count changes Document* document = GetDocument(); if (NS_WARN_IF(!document)) { aRv.Throw(NS_ERROR_FAILURE); return; } if (document->GetEditingState() == Document::EditingState::eContentEditable) { document->ChangeContentEditableCount(nullptr, +1); TopLevelEditSubActionDataRef().mRestoreContentEditableCount = true; } // Check that selection is in subtree defined by body node nsresult rv = EnsureSelectionInBodyOrDocumentElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { aRv.Throw(NS_ERROR_EDITOR_DESTROYED); return; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() " "failed, but ignored"); } nsresult HTMLEditor::OnEndHandlingTopLevelEditSubAction() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); nsresult rv; while (true) { if (NS_WARN_IF(Destroyed())) { rv = NS_ERROR_EDITOR_DESTROYED; break; } if (!mInitSucceeded) { rv = NS_OK; // We should do nothing if we're being initialized. break; } // Do all the tricky stuff rv = OnEndHandlingTopLevelEditSubActionInternal(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() failied"); // Perhaps, we need to do the following jobs even if the editor has been // destroyed since they adjust some states of HTML document but don't // modify the DOM tree nor Selection. // Free up selectionState range item if (TopLevelEditSubActionDataRef().mSelectedRange) { RangeUpdaterRef().DropRangeItem( *TopLevelEditSubActionDataRef().mSelectedRange); } // Reset the contenteditable count to its previous value if (TopLevelEditSubActionDataRef().mRestoreContentEditableCount) { Document* document = GetDocument(); if (NS_WARN_IF(!document)) { rv = NS_ERROR_FAILURE; break; } if (document->GetEditingState() == Document::EditingState::eContentEditable) { document->ChangeContentEditableCount(nullptr, -1); } } break; } DebugOnly rvIgnored = EditorBase::OnEndHandlingTopLevelEditSubAction(); NS_WARNING_ASSERTION( NS_FAILED(rv) || NS_SUCCEEDED(rvIgnored), "EditorBase::OnEndHandlingTopLevelEditSubAction() failed, but ignored"); MOZ_ASSERT(!GetTopLevelEditSubAction()); MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == eNone); return rv; } nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); nsresult rv = EnsureSelectionInBodyOrDocumentElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() " "failed, but ignored"); switch (GetTopLevelEditSubAction()) { case EditSubAction::eReplaceHeadWithHTMLSource: case EditSubAction::eCreatePaddingBRElementForEmptyEditor: return NS_OK; default: break; } if (TopLevelEditSubActionDataRef().mChangedRange->IsPositioned() && GetTopLevelEditSubAction() != EditSubAction::eUndo && GetTopLevelEditSubAction() != EditSubAction::eRedo) { // don't let any txns in here move the selection around behind our back. // Note that this won't prevent explicit selection setting from working. AutoTransactionsConserveSelection dontChangeMySelection(*this); { EditorRawDOMRange changedRange( *TopLevelEditSubActionDataRef().mChangedRange); if (changedRange.IsPositioned() && changedRange.EnsureNotInNativeAnonymousSubtree()) { switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::eDeleteText: { // XXX We should investigate whether this is really needed because // it seems that the following code does not handle the // white-spaces. RefPtr extendedChangedRange = CreateRangeIncludingAdjuscentWhiteSpaces(changedRange); if (extendedChangedRange) { MOZ_ASSERT(extendedChangedRange->IsPositioned()); // Use extended range temporarily. TopLevelEditSubActionDataRef().mChangedRange = std::move(extendedChangedRange); } break; } default: { RefPtr extendedChangedRange = CreateRangeExtendedToHardLineStartAndEnd( changedRange, GetTopLevelEditSubAction()); if (extendedChangedRange) { MOZ_ASSERT(extendedChangedRange->IsPositioned()); // Use extended range temporarily. TopLevelEditSubActionDataRef().mChangedRange = std::move(extendedChangedRange); } break; } } } } // if we did a ranged deletion or handling backspace key, make sure we have // a place to put caret. // Note we only want to do this if the overall operation was deletion, // not if deletion was done along the way for // EditSubAction::eInsertHTMLSource, EditSubAction::eInsertText, etc. // That's why this is here rather than DeleteSelectionAsSubAction(). // However, we shouldn't insert
elements if we've already removed // empty block parents because users may want to disappear the line by // the deletion. // XXX We should make HandleDeleteSelection() store expected container // for handling this here since we cannot trust current selection is // collapsed at deleted point. if (GetTopLevelEditSubAction() == EditSubAction::eDeleteSelectedContent && TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange && !TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks) { const auto newCaretPosition = GetFirstSelectionStartPoint(); if (!newCaretPosition.IsSet()) { NS_WARNING("There was no selection range"); return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } nsresult rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( newCaretPosition); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() " "failed"); return rv; } } // add in any needed
s, and remove any unneeded ones. nsresult rv = InsertBRElementToEmptyListItemsAndTableCellsInRange( TopLevelEditSubActionDataRef().mChangedRange->StartRef().AsRaw(), TopLevelEditSubActionDataRef().mChangedRange->EndRef().AsRaw()); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange()" " failed, but ignored"); // merge any adjacent text nodes switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: break; default: { nsresult rv = CollapseAdjacentTextNodes( MOZ_KnownLive(*TopLevelEditSubActionDataRef().mChangedRange)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CollapseAdjacentTextNodes() failed"); return rv; } break; } } // Clean up any empty nodes in the changed range unless they are inserted // intentionally. if (TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements) { nsresult rv = RemoveEmptyNodesIn( MOZ_KnownLive(*TopLevelEditSubActionDataRef().mChangedRange)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::RemoveEmptyNodesIn() failed"); return rv; } } // attempt to transform any unneeded nbsp's into spaces after doing various // operations switch (GetTopLevelEditSubAction()) { case EditSubAction::eDeleteSelectedContent: if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) { break; } [[fallthrough]]; case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::ePasteHTMLContent: case EditSubAction::eInsertHTMLSource: { // TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII // white-spaces with NPSPs and then, we'll replace them with ASCII // white-spaces here. We should avoid this overwriting things as // far as possible because replacing characters in text nodes // causes running mutation event listeners which are really // expensive. // Adjust end of composition string if there is composition string. auto pointToAdjust = GetLastIMESelectionEndPoint(); if (!pointToAdjust.IsInContentNode()) { // Otherwise, adjust current selection start point. pointToAdjust = GetFirstSelectionStartPoint(); if (NS_WARN_IF(!pointToAdjust.IsInContentNode())) { return NS_ERROR_FAILURE; } } if (EditorUtils::IsEditableContent(*pointToAdjust.ContainerAsContent(), EditorType::HTML)) { AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(), &pointToAdjust); nsresult rv = WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( *this, pointToAdjust); if (NS_FAILED(rv)) { NS_WARNING( "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "failed"); return rv; } } // also do this for original selection endpoints. // XXX Hmm, if `NormalizeVisibleWhiteSpacesAt()` runs mutation event // listener and that causes changing `mSelectedRange`, what we // should do? if (NS_WARN_IF(!TopLevelEditSubActionDataRef() .mSelectedRange->IsPositioned())) { return NS_ERROR_FAILURE; } EditorDOMPoint atStart = TopLevelEditSubActionDataRef().mSelectedRange->StartPoint(); if (atStart != pointToAdjust && atStart.IsInContentNode() && EditorUtils::IsEditableContent(*atStart.ContainerAsContent(), EditorType::HTML)) { AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(), &pointToAdjust); AutoTrackDOMPoint trackStartPoint(RangeUpdaterRef(), &atStart); nsresult rv = WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt( *this, atStart); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "failed, but ignored"); } // we only need to handle old selection endpoint if it was different // from start EditorDOMPoint atEnd = TopLevelEditSubActionDataRef().mSelectedRange->EndPoint(); if (!TopLevelEditSubActionDataRef().mSelectedRange->Collapsed() && atEnd != pointToAdjust && atEnd != atStart && atEnd.IsInContentNode() && EditorUtils::IsEditableContent(*atEnd.ContainerAsContent(), EditorType::HTML)) { nsresult rv = WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(*this, atEnd); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() " "failed, but ignored"); } break; } default: break; } // If we created a new block, make sure caret is in it. if (TopLevelEditSubActionDataRef().mNewBlockElement && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretInBlockElement( MOZ_KnownLive(*TopLevelEditSubActionDataRef().mNewBlockElement)); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::EnsureSelectionInBlockElement() failed, but ignored"); } // Adjust selection for insert text, html paste, and delete actions if // we haven't removed new empty blocks. Note that if empty block parents // are removed, Selection should've been adjusted by the method which // did it. if (!TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks && SelectionRef().IsCollapsed()) { switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: case EditSubAction::eInsertLineBreak: case EditSubAction::eInsertParagraphSeparator: case EditSubAction::ePasteHTMLContent: case EditSubAction::eInsertHTMLSource: // XXX AdjustCaretPositionAndEnsurePaddingBRElement() intentionally // does not create padding `
` element for empty editor. // Investigate which is better that whether this should does it // or wait MaybeCreatePaddingBRElementForEmptyEditor(). rv = AdjustCaretPositionAndEnsurePaddingBRElement( GetDirectionOfTopLevelEditSubAction()); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::AdjustCaretPositionAndEnsurePaddingBRElement() " "failed"); return rv; } break; default: break; } } // check for any styles which were removed inappropriately bool reapplyCachedStyle; switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: reapplyCachedStyle = true; break; default: reapplyCachedStyle = IsStyleCachePreservingSubAction(GetTopLevelEditSubAction()); break; } // If the selection is in empty inline HTML elements, we should delete // them unless it's inserted intentionally. if (mPlaceholderBatch && TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements && SelectionRef().IsCollapsed() && SelectionRef().GetFocusNode()) { RefPtr mostDistantEmptyInlineAncestor = nullptr; for (Element* ancestor : SelectionRef().GetFocusNode()->InclusiveAncestorsOfType()) { if (!ancestor->IsHTMLElement() || !HTMLEditUtils::IsRemovableFromParentNode(*ancestor) || !HTMLEditUtils::IsEmptyInlineContent(*ancestor)) { break; } mostDistantEmptyInlineAncestor = ancestor; } if (mostDistantEmptyInlineAncestor) { nsresult rv = DeleteNodeWithTransaction(*mostDistantEmptyInlineAncestor); if (Destroyed()) { NS_WARNING( "HTMLEditor::DeleteNodeWithTransaction() caused destroying the " "editor at deleting empty inline ancestors"); return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::DeleteNodeWithTransaction() failed at deleting " "empty inline ancestors"); return rv; } } } // But the cached inline styles should be restored from type-in-state later. if (reapplyCachedStyle) { DebugOnly rvIgnored = mTypeInState->UpdateSelState(*this); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "TypeInState::UpdateSelState() failed, but ignored"); rvIgnored = ReapplyCachedStyles(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "HTMLEditor::ReapplyCachedStyles() failed, but ignored"); TopLevelEditSubActionDataRef().mCachedInlineStyles->Clear(); } } rv = HandleInlineSpellCheck( TopLevelEditSubActionDataRef().mSelectedRange->StartPoint(), TopLevelEditSubActionDataRef().mChangedRange); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::HandleInlineSpellCheck() failed"); return rv; } // detect empty doc // XXX Need to investigate when the padding
element is removed because // I don't see the
element with testing manually. If it won't be // used, we can get rid of this cost. rv = MaybeCreatePaddingBRElementForEmptyEditor(); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed"); return rv; } // adjust selection HINT if needed if (!TopLevelEditSubActionDataRef().mDidExplicitlySetInterLine && SelectionRef().IsCollapsed()) { SetSelectionInterlinePosition(); } return NS_OK; } EditActionResult HTMLEditor::CanHandleHTMLEditSubAction() const { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } // If there is not selection ranges, we should ignore the result. if (!SelectionRef().RangeCount()) { return EditActionCanceled(); } const nsRange* range = SelectionRef().GetRangeAt(0); nsINode* selStartNode = range->GetStartContainer(); if (NS_WARN_IF(!selStartNode) || NS_WARN_IF(!selStartNode->IsContent())) { return EditActionResult(NS_ERROR_FAILURE); } if (!HTMLEditUtils::IsSimplyEditableNode(*selStartNode) || HTMLEditUtils::IsNonEditableReplacedContent(*selStartNode->AsContent())) { return EditActionCanceled(); } nsINode* selEndNode = range->GetEndContainer(); if (NS_WARN_IF(!selEndNode) || NS_WARN_IF(!selEndNode->IsContent())) { return EditActionResult(NS_ERROR_FAILURE); } if (selStartNode == selEndNode) { return EditActionIgnored(); } if (!HTMLEditUtils::IsSimplyEditableNode(*selEndNode) || HTMLEditUtils::IsNonEditableReplacedContent(*selEndNode->AsContent())) { return EditActionCanceled(); } // XXX What does it mean the common ancestor is editable? I have no idea. // It should be in same (active) editing host, and even if it's editable, // there may be non-editable contents in the range. nsINode* commonAncestor = range->GetClosestCommonInclusiveAncestor(); if (!commonAncestor) { NS_WARNING( "AbstractRange::GetClosestCommonInclusiveAncestor() returned nullptr"); return EditActionResult(NS_ERROR_FAILURE); } return HTMLEditUtils::IsSimplyEditableNode(*commonAncestor) ? EditActionIgnored() : EditActionCanceled(); } MOZ_CAN_RUN_SCRIPT static nsStaticAtom& MarginPropertyAtomForIndent( nsIContent& aContent) { nsAutoString direction; DebugOnly rvIgnored = CSSEditUtils::GetComputedProperty( aContent, *nsGkAtoms::direction, direction); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetComputedProperty(nsGkAtoms::direction)" " failed, but ignored"); return direction.EqualsLiteral("rtl") ? *nsGkAtoms::marginRight : *nsGkAtoms::marginLeft; } nsresult HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(SelectionRef().IsCollapsed()); // If we are after a padding `
` element for empty last line in the same // block, then move selection to be before it const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorRawDOMPoint atSelectionStart(firstRange->StartRef()); if (NS_WARN_IF(!atSelectionStart.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atSelectionStart.IsSetAndValid()); if (!atSelectionStart.IsInContentNode()) { return NS_OK; } Element* editingHost = GetActiveEditingHost(); if (!editingHost) { NS_WARNING( "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() did nothing " "because of no editing host"); return NS_OK; } nsIContent* previousBRElement = HTMLEditUtils::GetPreviousContent(atSelectionStart, {}, editingHost); if (!previousBRElement || !previousBRElement->IsHTMLElement(nsGkAtoms::br) || !previousBRElement->GetParent() || !EditorUtils::IsEditableContent(*previousBRElement->GetParent(), EditorType::HTML) || !HTMLEditUtils::IsInvisibleBRElement(*previousBRElement)) { return NS_OK; } const RefPtr blockElementAtSelectionStart = HTMLEditUtils::GetInclusiveAncestorElement( *atSelectionStart.ContainerAsContent(), HTMLEditUtils::ClosestBlockElement); const RefPtr parentBlockElementOfBRElement = HTMLEditUtils::GetAncestorElement(*previousBRElement, HTMLEditUtils::ClosestBlockElement); if (!blockElementAtSelectionStart || blockElementAtSelectionStart != parentBlockElementOfBRElement) { return NS_OK; } // If we are here then the selection is right after a padding
// element for empty last line that is in the same block as the // selection. We need to move the selection start to be before the // padding
element. EditorRawDOMPoint atInvisibleBRElement(previousBRElement); nsresult rv = CollapseSelectionTo(atInvisibleBRElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } nsresult HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() { MOZ_ASSERT(IsEditActionDataAvailable()); if (mPaddingBRElementForEmptyEditor) { return NS_OK; } IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eCreatePaddingBRElementForEmptyEditor, nsIEditor::eNone, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return ignoredError.StealNSResult(); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); ignoredError.SuppressException(); RefPtr rootElement = GetRoot(); if (!rootElement) { return NS_OK; } // Now we've got the body element. Iterate over the body element's children, // looking for editable content. If no editable content is found, insert the // padding
element. EditorType editorType = GetEditorType(); bool isRootEditable = EditorUtils::IsEditableContent(*rootElement, editorType); for (nsIContent* rootChild = rootElement->GetFirstChild(); rootChild; rootChild = rootChild->GetNextSibling()) { if (EditorUtils::IsPaddingBRElementForEmptyEditor(*rootChild) || !isRootEditable || EditorUtils::IsEditableContent(*rootChild, editorType) || HTMLEditUtils::IsBlockElement(*rootChild)) { return NS_OK; } } // Skip adding the padding
element for empty editor if body // is read-only. if (IsHTMLEditor() && !HTMLEditUtils::IsSimplyEditableNode(*rootElement)) { return NS_OK; } // Create a br. RefPtr newBRElement = CreateHTMLContent(nsGkAtoms::br); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_WARN_IF(!newBRElement)) { return NS_ERROR_FAILURE; } mPaddingBRElementForEmptyEditor = static_cast(newBRElement.get()); // Give it a special attribute. newBRElement->SetFlags(NS_PADDING_FOR_EMPTY_EDITOR); // Put the node in the document. CreateElementResult insertBRElementResult = InsertNodeWithTransaction(*newBRElement, EditorDOMPoint(rootElement, 0u)); if (insertBRElementResult.isErr()) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertBRElementResult.unwrapErr(); } // Set selection. insertBRElementResult.IgnoreCaretPointSuggestion(); nsresult rv = CollapseSelectionToStartOf(*rootElement); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "EditorBase::CollapseSelectionToStartOf() caused destroying the " "editor"); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed, but ignored"); return NS_OK; } nsresult HTMLEditor::EnsureNoPaddingBRElementForEmptyEditor() { MOZ_ASSERT(IsEditActionDataAvailable()); if (!mPaddingBRElementForEmptyEditor) { return NS_OK; } // If we're an HTML editor, a mutation event listener may recreate padding //
element for empty editor again during the call of // DeleteNodeWithTransaction(). So, move it first. RefPtr paddingBRElement( std::move(mPaddingBRElementForEmptyEditor)); nsresult rv = DeleteNodeWithTransaction(*paddingBRElement); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } nsresult HTMLEditor::ReflectPaddingBRElementForEmptyEditor() { if (NS_WARN_IF(!mRootElement)) { NS_WARNING("Failed to handle padding BR element due to no root element"); return NS_ERROR_FAILURE; } // The idea here is to see if the magic empty node has suddenly reappeared. If // it has, set our state so we remember it. There is a tradeoff between doing // here and at redo, or doing it everywhere else that might care. Since undo // and redo are relatively rare, it makes sense to take the (small) // performance hit here. nsIContent* firstLeafChild = HTMLEditUtils::GetFirstLeafContent( *mRootElement, {LeafNodeType::OnlyLeafNode}); if (firstLeafChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) { mPaddingBRElementForEmptyEditor = static_cast(firstLeafChild); } else { mPaddingBRElementForEmptyEditor = nullptr; } return NS_OK; } nsresult HTMLEditor::PrepareInlineStylesForCaret() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(SelectionRef().IsCollapsed()); // XXX This method works with the top level edit sub-action, but this // must be wrong if we are handling nested edit action. if (TopLevelEditSubActionDataRef().mDidDeleteSelection) { switch (GetTopLevelEditSubAction()) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: { nsresult rv = ReapplyCachedStyles(); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReapplyCachedStyles() failed"); return rv; } break; } default: break; } } // For most actions we want to clear the cached styles, but there are // exceptions if (!IsStyleCachePreservingSubAction(GetTopLevelEditSubAction())) { TopLevelEditSubActionDataRef().mCachedInlineStyles->Clear(); } return NS_OK; } EditActionResult HTMLEditor::HandleInsertText( EditSubAction aEditSubAction, const nsAString& aInsertionString, SelectionHandling aSelectionHandling) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText || aEditSubAction == EditSubAction::eInsertTextComingFromIME); MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore, aEditSubAction == EditSubAction::eInsertTextComingFromIME); EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } UndefineCaretBidiLevel(); // If the selection isn't collapsed, delete it. Don't delete existing inline // tags, because we're hopefully going to insert text (bug 787432). if (!SelectionRef().IsCollapsed() && aSelectionHandling == SelectionHandling::Delete) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(nsIEditor::eNone, " "nsIEditor::eNoStrip) failed"); return EditActionHandled(rv); } } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { return EditActionHandled(NS_ERROR_FAILURE); } RefPtr firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionHandled(NS_ERROR_FAILURE); } // for every property that is set, insert a new inline style node // XXX CreateStyleForInsertText() adjusts selection automatically, but // it should just return the insertion point instead. rv = CreateStyleForInsertText(*firstRange); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return EditActionHandled(rv); } firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionHandled(NS_ERROR_FAILURE); } EditorDOMPoint pointToInsert(firstRange->StartRef()); if (NS_WARN_IF(!pointToInsert.IsSet()) || NS_WARN_IF(!pointToInsert.IsInContentNode())) { return EditActionHandled(NS_ERROR_FAILURE); } MOZ_ASSERT(pointToInsert.IsSetAndValid()); // If the point is not in an element which can contain text nodes, climb up // the DOM tree. if (!pointToInsert.IsInTextNode()) { Element* editingHost = GetActiveEditingHost(GetDocument()->IsXMLDocument() ? LimitInBodyElement::No : LimitInBodyElement::Yes); if (NS_WARN_IF(!editingHost)) { return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } while (!HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(), *nsGkAtoms::textTagName)) { if (NS_WARN_IF(pointToInsert.GetContainer() == editingHost) || NS_WARN_IF(!pointToInsert.GetContainerParentAsContent())) { NS_WARNING("Selection start point couldn't have text nodes"); return EditActionHandled(NS_ERROR_FAILURE); } pointToInsert.Set(pointToInsert.ContainerAsContent()); } } if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) { auto compositionStartPoint = GetFirstIMESelectionStartPoint(); if (!compositionStartPoint.IsSet()) { compositionStartPoint = pointToInsert; } if (aInsertionString.IsEmpty()) { // Right now the WhiteSpaceVisibilityKeeper code bails on empty strings, // but IME needs the InsertTextWithTransaction() call to still happen // since empty strings are meaningful there. Result insertTextResult = InsertTextWithTransaction(*document, aInsertionString, compositionStartPoint); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return EditActionResult(insertTextResult.unwrapErr()); } return EditActionHandled(); } auto compositionEndPoint = GetLastIMESelectionEndPoint(); if (!compositionEndPoint.IsSet()) { compositionEndPoint = compositionStartPoint; } Result replaceTextResult = WhiteSpaceVisibilityKeeper::ReplaceText( *this, aInsertionString, EditorDOMRange(compositionStartPoint, compositionEndPoint)); if (MOZ_UNLIKELY(replaceTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed"); return EditActionHandled(replaceTextResult.unwrapErr()); } compositionStartPoint = GetFirstIMESelectionStartPoint(); compositionEndPoint = GetLastIMESelectionEndPoint(); if (NS_WARN_IF(!compositionStartPoint.IsSet()) || NS_WARN_IF(!compositionEndPoint.IsSet())) { // Mutation event listener has changed the DOM tree... return EditActionHandled(); } nsresult rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( compositionStartPoint.ToRawRangeBoundary(), compositionEndPoint.ToRawRangeBoundary()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed"); return EditActionHandled(rv); } MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText); // find where we are EditorDOMPoint currentPoint(pointToInsert); // is our text going to be PREformatted? // We remember this so that we know how to handle tabs. const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted( *pointToInsert.ContainerAsContent()); // turn off the edit listener: we know how to // build the "doc changed range" ourselves, and it's // must faster to do it once here than to track all // the changes one at a time. AutoRestore disableListener( EditSubActionDataRef().mAdjustChangedRangeFromListener); EditSubActionDataRef().mAdjustChangedRangeFromListener = false; // don't change my selection in subtransactions AutoTransactionsConserveSelection dontChangeMySelection(*this); int32_t pos = 0; constexpr auto newlineStr = NS_LITERAL_STRING_FROM_CSTRING(LFSTR); { AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert); // for efficiency, break out the pre case separately. This is because // its a lot cheaper to search the input string for only newlines than // it is to search for both tabs and newlines. if (!isWhiteSpaceCollapsible || IsInPlaintextMode()) { while (pos != -1 && pos < AssertedCast(aInsertionString.Length())) { int32_t oldPos = pos; int32_t subStrLen; pos = aInsertionString.FindChar(nsCRT::LF, oldPos); if (pos != -1) { subStrLen = pos - oldPos; // if first char is newline, then use just it if (!subStrLen) { subStrLen = 1; } } else { subStrLen = aInsertionString.Length() - oldPos; pos = aInsertionString.Length(); } nsDependentSubstring subStr(aInsertionString, oldPos, subStrLen); // is it a return? if (subStr.Equals(newlineStr)) { CreateElementResult insertBRElementResult = InsertBRElement(WithTransaction::Yes, currentPoint); if (insertBRElementResult.isErr()) { NS_WARNING( "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return EditActionHandled(insertBRElementResult.unwrapErr()); } // We don't want to update selection here because we've blocked // InsertNodeTransaction updating selection with // dontChangeMySelection. insertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(!AllowsTransactionsToChangeSelection()); pos++; RefPtr brElement = insertBRElementResult.UnwrapNewNode(); if (brElement->GetNextSibling()) { pointToInsert.Set(brElement->GetNextSibling()); } else { pointToInsert.SetToEndOf(currentPoint.GetContainer()); } // XXX In most cases, pointToInsert and currentPoint are same here. // But if the
element has been moved to different point by // mutation observer, those points become different. currentPoint.SetAfter(brElement); NS_WARNING_ASSERTION(currentPoint.IsSet(), "Failed to set after the
element"); NS_WARNING_ASSERTION(currentPoint == pointToInsert, "Perhaps,
element position has been moved " "to different point " "by mutation observer"); } else { Result insertTextResult = InsertTextWithTransaction(*document, subStr, currentPoint); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return EditActionHandled(insertTextResult.unwrapErr()); } currentPoint = insertTextResult.inspect(); pointToInsert = insertTextResult.unwrap(); } } } else { const RefPtr editingHost = GetActiveEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { return EditActionHandled(NS_ERROR_FAILURE); } constexpr auto tabStr = u"\t"_ns; constexpr auto spacesStr = u" "_ns; char specialChars[] = {TAB, nsCRT::LF, 0}; nsAutoString insertionString(aInsertionString); // For FindCharInSet(). while (pos != -1 && pos < AssertedCast(insertionString.Length())) { int32_t oldPos = pos; int32_t subStrLen; pos = insertionString.FindCharInSet(specialChars, oldPos); if (pos != -1) { subStrLen = pos - oldPos; // if first char is newline, then use just it if (!subStrLen) { subStrLen = 1; } } else { subStrLen = insertionString.Length() - oldPos; pos = insertionString.Length(); } nsDependentSubstring subStr(insertionString, oldPos, subStrLen); // is it a tab? if (subStr.Equals(tabStr)) { Result insertTextResult = WhiteSpaceVisibilityKeeper::InsertText(*this, spacesStr, currentPoint); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed"); return EditActionHandled(insertTextResult.unwrapErr()); } pos++; MOZ_ASSERT(insertTextResult.inspect().IsSet()); currentPoint = insertTextResult.inspect(); pointToInsert = insertTextResult.unwrap(); } // is it a return? else if (subStr.Equals(newlineStr)) { CreateElementResult insertBRElementResult = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, currentPoint, *editingHost); if (insertBRElementResult.isErr()) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return EditActionHandled(insertBRElementResult.unwrapErr()); } // TODO: Some methods called for handling non-preformatted text use // GetActiveEditingHost(). Therefore, they depend on the latest // selection. So we cannot skip updating selection here. nsresult rv = insertBRElementResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CareateElementResult::SuggestCaretPointTo() failed"); return EditActionHandled(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CreateElementResult::SuggestCaretPointTo() failed, but ignored"); pos++; RefPtr newBRElement = insertBRElementResult.UnwrapNewNode(); MOZ_DIAGNOSTIC_ASSERT(newBRElement); if (newBRElement->GetNextSibling()) { pointToInsert.Set(newBRElement->GetNextSibling()); } else { pointToInsert.SetToEndOf(currentPoint.GetContainer()); } currentPoint.SetAfter(newBRElement); NS_WARNING_ASSERTION(currentPoint.IsSet(), "Failed to set after the new
element"); // XXX If the newBRElement has been moved or removed by mutation // observer, we hit this assert. We need to check if // newBRElement is in expected point, though, we must have // a lot of same bugs... NS_WARNING_ASSERTION( currentPoint == pointToInsert, "Perhaps, newBRElement has been moved or removed unexpectedly"); } else { Result insertTextResult = WhiteSpaceVisibilityKeeper::InsertText(*this, subStr, currentPoint); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed"); return EditActionHandled(insertTextResult.unwrapErr()); } MOZ_ASSERT(insertTextResult.inspect().IsSet()); currentPoint = insertTextResult.inspect(); pointToInsert = insertTextResult.unwrap(); } } } // After this block, pointToInsert is updated by AutoTrackDOMPoint. } if (currentPoint.IsSet()) { currentPoint.SetInterlinePosition(InterlinePosition::EndOfLine); nsresult rv = CollapseSelectionTo(currentPoint); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "EditorBase::CollapseSelectionTo() caused destroying the editor"); return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Selection::Collapse() failed, but ignored"); // manually update the doc changed range so that AfterEdit will clean up // the correct portion of the document. rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( pointToInsert.ToRawRangeBoundary(), currentPoint.ToRawRangeBoundary()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed"); return EditActionHandled(rv); } DebugOnly rvIgnored = SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Selection::SetInterlinePosition(InterlinePosition::" "EndOfLine) failed, but ignored"); rv = TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(pointToInsert); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::CollapseTo() failed"); return EditActionHandled(rv); } nsresult HTMLEditor::InsertLineBreakAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.Rv(); } // XXX This may be called by execCommand() with "insertLineBreak". // In such case, naming the transaction "TypingTxnName" is odd. AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName, ScrollSelectionIntoView::Yes, __FUNCTION__); // calling it text insertion to trigger moz br treatment by rules // XXX Why do we use EditSubAction::eInsertText here? Looks like // EditSubAction::eInsertLineBreak or EditSubAction::eInsertNode // is better. IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertText, 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"); UndefineCaretBidiLevel(); // If the selection isn't collapsed, delete it. if (!SelectionRef().IsCollapsed()) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed"); return rv; } } const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atStartOfSelection.IsSetAndValid()); RefPtr editingHost = GetActiveEditingHost(); if (NS_WARN_IF(!editingHost)) { return NS_ERROR_FAILURE; } // For backward compatibility, we should not insert a linefeed if // paragraph separator is set to "br" which is Gecko-specific mode. if (GetDefaultParagraphSeparator() == ParagraphSeparator::br || !HTMLEditUtils::ShouldInsertLinefeedCharacter(atStartOfSelection, *editingHost)) { CreateElementResult insertBRElementResult = InsertBRElement( WithTransaction::Yes, atStartOfSelection, nsIEditor::eNext); if (insertBRElementResult.isErr()) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.unwrapErr(); } nsresult rv = insertBRElementResult.SuggestCaretPointTo(*this, {}); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "CreateElementResult::SuggestCaretPointTo() failed"); MOZ_ASSERT(insertBRElementResult.GetNewNode()); return rv; } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } atStartOfSelection = EditorDOMPoint(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atStartOfSelection.IsSetAndValid()); // Do nothing if the node is read-only if (!HTMLEditUtils::IsSimplyEditableNode( *atStartOfSelection.GetContainer())) { return NS_SUCCESS_DOM_NO_OPERATION; } rv = HandleInsertLinefeed(atStartOfSelection, *editingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::HandleInsertLinefeed() failed"); return rv; } EditActionResult HTMLEditor::InsertParagraphSeparatorAsSubAction() { if (NS_WARN_IF(!mInitSucceeded)) { return EditActionIgnored(NS_ERROR_NOT_INITIALIZED); } EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } // XXX This may be called by execCommand() with "insertParagraph". // In such case, naming the transaction "TypingTxnName" is odd. AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eInsertParagraphSeparator, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditActionResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); UndefineCaretBidiLevel(); // If the selection isn't collapsed, delete it. if (!SelectionRef().IsCollapsed()) { nsresult rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed"); return EditActionIgnored(rv); } } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } RefPtr editingHost = GetActiveEditingHost(); if (NS_WARN_IF(!editingHost)) { return EditActionIgnored(NS_ERROR_FAILURE); } if (IsMailEditor()) { const auto pointToSplit = GetFirstSelectionStartPoint(); if (NS_WARN_IF(!pointToSplit.IsInContentNode())) { return EditActionIgnored(NS_ERROR_FAILURE); } if (RefPtr mailCiteElement = GetMostDistantAncestorMailCiteElement( *pointToSplit.ContainerAsContent())) { // Split any mailcites in the way. Should we abort this if we encounter // table cell boundaries? Result atNewBRElementOrError = HandleInsertParagraphInMailCiteElement(*mailCiteElement, pointToSplit, *editingHost); if (MOZ_UNLIKELY(atNewBRElementOrError.isErr())) { NS_WARNING( "HTMLEditor::HandleInsertParagraphInMailCiteElement() failed"); return EditActionHandled(atNewBRElementOrError.unwrapErr()); } EditorDOMPoint pointToPutCaret = atNewBRElementOrError.unwrap(); MOZ_ASSERT(pointToPutCaret.IsSet()); pointToPutCaret.SetInterlinePosition(InterlinePosition::StartOfNextLine); MOZ_ASSERT(pointToPutCaret.GetChild()); MOZ_ASSERT(pointToPutCaret.GetChild()->IsHTMLElement(nsGkAtoms::br)); nsresult rv = CollapseSelectionTo(pointToPutCaret); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return EditActionHandled(rv); } } // Smart splitting rules const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionIgnored(NS_ERROR_FAILURE); } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return EditActionIgnored(NS_ERROR_FAILURE); } MOZ_ASSERT(atStartOfSelection.IsSetAndValid()); // Do nothing if the node is read-only if (!HTMLEditUtils::IsSimplyEditableNode( *atStartOfSelection.GetContainer())) { return EditActionCanceled(); } // If the active editing host is an inline element, or if the active editing // host is the block parent itself and we're configured to use
as a // paragraph separator, just append a
. // If the editing host parent element is editable, it means that the editing // host must be a element and the selection may be outside the body // element. If the selection is outside the editing host, we should not // insert new paragraph nor
element. // XXX Currently, we don't support editing outside element, but Blink // does it. if (editingHost->GetParentElement() && HTMLEditUtils::IsSimplyEditableNode(*editingHost->GetParentElement()) && (!atStartOfSelection.IsInContentNode() || !nsContentUtils::ContentIsFlattenedTreeDescendantOf( atStartOfSelection.ContainerAsContent(), editingHost))) { return EditActionHandled(NS_ERROR_EDITOR_NO_EDITABLE_RANGE); } // Look for the nearest parent block. However, don't return error even if // there is no block parent here because in such case, i.e., editing host // is an inline element, we should insert
simply. RefPtr editableBlockElement = atStartOfSelection.IsInContentNode() ? HTMLEditUtils::GetInclusiveAncestorElement( *atStartOfSelection.ContainerAsContent(), HTMLEditUtils::ClosestEditableBlockElement) : nullptr; ParagraphSeparator separator = GetDefaultParagraphSeparator(); bool insertLineBreak; // If there is no block parent in the editing host, i.e., the editing host // itself is also a non-block element, we should insert a line break. if (!editableBlockElement) { // XXX Chromium checks if the CSS box of the editing host is a block. insertLineBreak = true; } // If the editable block element is not splittable, e.g., it's an editing // host, and the default paragraph separator is
or the element cannot // contain a

element, we should insert a
element. else if (!HTMLEditUtils::IsSplittableNode(*editableBlockElement)) { insertLineBreak = separator == ParagraphSeparator::br || !HTMLEditUtils::CanElementContainParagraph(*editingHost) || HTMLEditUtils::ShouldInsertLinefeedCharacter(atStartOfSelection, *editingHost); } // If the nearest block parent is a single-line container declared in // the execCommand spec and not the editing host, we should separate the // block even if the default paragraph separator is
element. else if (HTMLEditUtils::IsSingleLineContainer(*editableBlockElement)) { insertLineBreak = false; } // Otherwise, unless there is no block ancestor which can contain

// element, we shouldn't insert a line break here. else { insertLineBreak = true; for (const Element* editableBlockAncestor = editableBlockElement; editableBlockAncestor && insertLineBreak; editableBlockAncestor = HTMLEditUtils::GetAncestorElement( *editableBlockAncestor, HTMLEditUtils::ClosestEditableBlockElement)) { insertLineBreak = !HTMLEditUtils::CanElementContainParagraph(*editableBlockAncestor); } } // If we cannot insert a

/

element at the selection, we should insert // a
element or a linefeed instead. if (insertLineBreak) { // For backward compatibility, we should not insert a linefeed if // paragraph separator is set to "br" which is Gecko-specific mode. if (separator != ParagraphSeparator::br && HTMLEditUtils::ShouldInsertLinefeedCharacter(atStartOfSelection, *editingHost)) { nsresult rv = HandleInsertLinefeed(atStartOfSelection, *editingHost); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::HandleInsertLinefeed() failed"); return EditActionResult(rv); } return EditActionHandled(); } CreateElementResult insertBRElementResult = HandleInsertBRElement(atStartOfSelection, *editingHost); if (insertBRElementResult.isErr()) { NS_WARNING("HTMLEditor::HandleInsertBRElement() failed"); return EditActionHandled(insertBRElementResult.unwrapErr()); } nsresult rv = insertBRElementResult.SuggestCaretPointTo(*this, {}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return EditActionHandled(rv); } return EditActionHandled(); } if (!HTMLEditUtils::IsSplittableNode(*editableBlockElement) && separator != ParagraphSeparator::br) { // Insert a new block first MOZ_ASSERT(separator == ParagraphSeparator::div || separator == ParagraphSeparator::p); // FormatBlockContainerWithTransaction() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, editor might have been destroyed // at restoring Selection. nsresult rv = FormatBlockContainerWithTransaction( MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator))); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED) || NS_WARN_IF(Destroyed())) { return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED); } // We warn on failure, but don't handle it, because it might be harmless. // Instead we just check that a new block was actually created. NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::FormatBlockContainerWithTransaction() " "failed, but ignored"); firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionIgnored(NS_ERROR_FAILURE); } atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsInContentNode())) { return EditActionIgnored(NS_ERROR_FAILURE); } MOZ_ASSERT(atStartOfSelection.IsSetAndValid()); editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *atStartOfSelection.ContainerAsContent(), HTMLEditUtils::ClosestEditableBlockElement); if (NS_WARN_IF(!editableBlockElement)) { return EditActionIgnored(NS_ERROR_UNEXPECTED); } if (NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(*editableBlockElement))) { // Didn't create a new block for some reason, fall back to
CreateElementResult insertBRElementResult = HandleInsertBRElement(atStartOfSelection, *editingHost); if (insertBRElementResult.isErr()) { NS_WARNING("HTMLEditor::HandleInsertBRElement() failed"); return EditActionResult(insertBRElementResult.unwrapErr()); } nsresult rv = insertBRElementResult.SuggestCaretPointTo(*this, {}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return EditActionHandled(rv); } return EditActionHandled(); } // Now, mNewBlockElement is last created block element for wrapping inline // elements around the caret position and AfterEditInner() will move // caret into it. However, it may be different from block parent of // the caret position. E.g., FormatBlockContainerWithTransaction() may // wrap following inline elements of a
element which is next sibling // of container of the caret. So, we need to adjust mNewBlockElement here // for avoiding jumping caret to odd position. TopLevelEditSubActionDataRef().mNewBlockElement = editableBlockElement; } // If block is empty, populate with br. (For example, imagine a div that // contains the word "text". The user selects "text" and types return. // "Text" is deleted leaving an empty block. We want to put in one br to // make block have a line. Then code further below will put in a second br.) if (HTMLEditUtils::IsEmptyBlockElement( *editableBlockElement, {EmptyCheckOption::TreatSingleBRElementAsVisible})) { AutoEditorDOMPointChildInvalidator lockOffset(atStartOfSelection); EditorDOMPoint endOfBlockParent; endOfBlockParent.SetToEndOf(editableBlockElement); const CreateElementResult insertBRElementResult = InsertBRElement(WithTransaction::Yes, endOfBlockParent); if (insertBRElementResult.isErr()) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return EditActionIgnored(insertBRElementResult.unwrapErr()); } // XXX Is this intentional selection change? nsresult rv = insertBRElementResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return EditActionHandled(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CreateElementResult::SuggestCaretPointTo() failed, but ignored"); MOZ_ASSERT(insertBRElementResult.GetNewNode()); } RefPtr maybeNonEditableListItem = HTMLEditUtils::GetClosestAncestorListItemElement(*editableBlockElement, editingHost); if (maybeNonEditableListItem && HTMLEditUtils::IsSplittableNode(*maybeNonEditableListItem)) { Result pointToPutCaretOrError = HandleInsertParagraphInListItemElement( *maybeNonEditableListItem, atStartOfSelection, *editingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (NS_WARN_IF(pointToPutCaretOrError.unwrapErr() == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING( "HTMLEditor::HandleInsertParagraphInListItemElement() failed, but " "ignored"); return EditActionHandled(); } nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.unwrap()); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "EditorBase::CollapseSelectionTo() caused destroying the editor"); return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed, but ignored"); return EditActionHandled(); } if (HTMLEditUtils::IsHeader(*editableBlockElement)) { // Headers: close (or split) header Result pointToPutCaretOrError = HandleInsertParagraphInHeadingElement(*editableBlockElement, atStartOfSelection); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (NS_WARN_IF(pointToPutCaretOrError.unwrapErr() == NS_ERROR_EDITOR_DESTROYED)) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING( "HTMLEditor::HandleInsertParagraphInHeadingElement() failed, but " "ignored"); return EditActionHandled(); } nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.unwrap()); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "EditorBase::CollapseSelectionTo() caused destroying the editor"); return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed, but ignored"); return EditActionHandled(); } // XXX Ideally, we should take same behavior with both

container and //

container. However, we are still using
as default // paragraph separator (non-standard) and we've split only

container // long time. Therefore, some web apps may depend on this behavior like // Gmail. So, let's use traditional odd behavior only when the default // paragraph separator is
. Otherwise, take consistent behavior // between

container and

container. if ((separator == ParagraphSeparator::br && editableBlockElement->IsHTMLElement(nsGkAtoms::p)) || (separator != ParagraphSeparator::br && editableBlockElement->IsAnyOfHTMLElements(nsGkAtoms::p, nsGkAtoms::div))) { AutoEditorDOMPointChildInvalidator lockOffset(atStartOfSelection); // Paragraphs: special rules to look for
s EditActionResult result = HandleInsertParagraphInParagraph(*editableBlockElement); if (result.Failed()) { NS_WARNING("HTMLEditor::HandleInsertParagraphInParagraph() failed"); return result; } if (result.Handled()) { // Now, atStartOfSelection may be invalid because the left paragraph // may have less children than its offset. For avoiding warnings of // validation of EditorDOMPoint, we should not touch it anymore. lockOffset.Cancel(); return result; } // Fall through, if HandleInsertParagraphInParagraph() didn't handle it. MOZ_ASSERT(!result.Canceled(), "HandleInsertParagraphInParagraph() canceled this edit action, " "InsertParagraphSeparatorAsSubAction() needs to handle this " "action instead"); } // If nobody handles this edit action, let's insert new
at the selection. CreateElementResult insertBRElementResult = HandleInsertBRElement(atStartOfSelection, *editingHost); if (insertBRElementResult.isErr()) { NS_WARNING("HTMLEditor::HandleInsertBRElement() failed"); return EditActionIgnored(insertBRElementResult.unwrapErr()); } rv = insertBRElementResult.SuggestCaretPointTo(*this, {}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return EditActionHandled(rv); } return EditActionHandled(); } CreateElementResult HTMLEditor::HandleInsertBRElement( const EditorDOMPoint& aPointToBreak, Element& aEditingHost) { MOZ_ASSERT(aPointToBreak.IsSet()); MOZ_ASSERT(IsEditActionDataAvailable()); bool brElementIsAfterBlock = false, brElementIsBeforeBlock = false; // First, insert a
element. RefPtr brElement; if (IsInPlaintextMode()) { CreateElementResult insertBRElementResult = InsertBRElement(WithTransaction::Yes, aPointToBreak); if (insertBRElementResult.isErr()) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return CreateElementResult(insertBRElementResult.unwrapErr()); } // We'll return with suggesting new caret position and nobody refers // selection after here. So we don't need to update selection here. insertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertBRElementResult.GetNewNode()); brElement = insertBRElementResult.UnwrapNewNode(); } else { EditorDOMPoint pointToBreak(aPointToBreak); WSRunScanner wsRunScanner(&aEditingHost, pointToBreak); WSScanResult backwardScanResult = wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToBreak); if (MOZ_UNLIKELY(backwardScanResult.Failed())) { NS_WARNING( "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed"); return CreateElementResult(NS_ERROR_FAILURE); } brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary(); WSScanResult forwardScanResult = wsRunScanner.ScanNextVisibleNodeOrBlockBoundaryFrom(pointToBreak); if (MOZ_UNLIKELY(forwardScanResult.Failed())) { NS_WARNING( "WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); return CreateElementResult(NS_ERROR_FAILURE); } brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary(); // If the container of the break is a link, we need to split it and // insert new
between the split links. RefPtr linkNode = HTMLEditor::GetLinkElement(pointToBreak.GetContainer()); if (linkNode) { const SplitNodeResult splitLinkNodeResult = SplitNodeDeepWithTransaction( *linkNode, pointToBreak, SplitAtEdges::eDoNotCreateEmptyContainer); if (splitLinkNodeResult.isErr()) { NS_WARNING( "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::" "eDoNotCreateEmptyContainer) failed"); return CreateElementResult(splitLinkNodeResult.unwrapErr()); } // TODO: Some methods called by // WhiteSpaceVisibilityKeeper::InsertBRElement() use // GetActiveEditingHost() which depends on selection. Therefore, // we cannot skip updating selection here. nsresult rv = splitLinkNodeResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed"); return CreateElementResult(rv); } pointToBreak = splitLinkNodeResult.AtSplitPoint(); // When adding caret suggestion to SplitNodeResult, here didn't change // selection so that just ignore it. splitLinkNodeResult.IgnoreCaretPointSuggestion(); } CreateElementResult insertBRElementResult = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, pointToBreak, aEditingHost); if (insertBRElementResult.isErr()) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return CreateElementResult(insertBRElementResult.unwrapErr()); } // We'll return with suggesting new caret position and nobody refers // selection after here. So we don't need to update selection here. insertBRElementResult.IgnoreCaretPointSuggestion(); brElement = insertBRElementResult.UnwrapNewNode(); MOZ_ASSERT(brElement); } if (MOZ_UNLIKELY(!brElement->GetParentNode())) { NS_WARNING("Inserted
element was removed by the web app"); return CreateElementResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } if (brElementIsAfterBlock && brElementIsBeforeBlock) { // We just placed a
between block boundaries. This is the one case // where we want the selection to be before the br we just placed, as the // br will be on a new line, rather than at end of prior line. // XXX brElementIsAfterBlock and brElementIsBeforeBlock were set before // modifying the DOM tree. So, now, the
element may not be // between blocks. return CreateElementResult( std::move(brElement), EditorDOMPoint(brElement, InterlinePosition::StartOfNextLine)); } auto afterBRElement = EditorDOMPoint::After(brElement); WSScanResult forwardScanFromAfterBRElementResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(&aEditingHost, afterBRElement); if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return CreateElementResult(NS_ERROR_FAILURE); } if (forwardScanFromAfterBRElementResult.ReachedBRElement()) { // The next thing after the break we inserted is another break. Move the // second break to be the first break's sibling. This will prevent them // from being in different inline nodes, which would break // SetInterlinePosition(). It will also assure that if the user clicks // away and then clicks back on their new blank line, they will still get // the style from the line above. if (brElement->GetNextSibling() != forwardScanFromAfterBRElementResult.BRElementPtr()) { MOZ_ASSERT(forwardScanFromAfterBRElementResult.BRElementPtr()); nsresult rv = MoveNodeWithTransaction( MOZ_KnownLive(*forwardScanFromAfterBRElementResult.BRElementPtr()), afterBRElement); if (MOZ_UNLIKELY((Destroyed()))) { NS_WARNING( "HTMLEditor::MoveNodeWithTransaction() caused destroying the " "editor"); return CreateElementResult(NS_ERROR_EDITOR_DESTROYED); } if (MOZ_UNLIKELY(NS_FAILED(rv))) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return CreateElementResult(rv); } } } // We want the caret to stick to whatever is past the break. This is because // the break is on the same line we were on, but the next content will be on // the following line. // An exception to this is if the break has a next sibling that is a block // node. Then we stick to the left to avoid an uber caret. nsIContent* nextSiblingOfBRElement = brElement->GetNextSibling(); afterBRElement.SetInterlinePosition( nextSiblingOfBRElement && HTMLEditUtils::IsBlockElement(*nextSiblingOfBRElement) ? InterlinePosition::EndOfLine : InterlinePosition::StartOfNextLine); return CreateElementResult(std::move(brElement), afterBRElement); } nsresult HTMLEditor::HandleInsertLinefeed(const EditorDOMPoint& aPointToBreak, Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!aPointToBreak.IsSet())) { return NS_ERROR_INVALID_ARG; } // TODO: The following code is duplicated from `HandleInsertText`. They // should be merged when we fix bug 92921. RefPtr caretRange = nsRange::Create(aPointToBreak.ToRawRangeBoundary(), aPointToBreak.ToRawRangeBoundary(), IgnoreErrors()); if (NS_WARN_IF(!caretRange)) { return NS_ERROR_FAILURE; } nsresult rv = CreateStyleForInsertText(*caretRange); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return rv; } caretRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!caretRange)) { return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } EditorDOMPoint pointToInsert(caretRange->StartRef()); if (NS_WARN_IF(!pointToInsert.IsSet()) || NS_WARN_IF(!pointToInsert.IsInContentNode())) { return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } MOZ_ASSERT(pointToInsert.IsSetAndValid()); // The node may not be able to have a text node so that we need to check it // here. if (!pointToInsert.IsInTextNode() && !HTMLEditUtils::CanNodeContain(*pointToInsert.ContainerAsContent(), *nsGkAtoms::textTagName)) { NS_WARNING( "HTMLEditor::HandleInsertLinefeed() couldn't insert a linefeed because " "the insertion position couldn't have text nodes"); return NS_ERROR_EDITOR_NO_EDITABLE_RANGE; } RefPtr document = GetDocument(); MOZ_ASSERT(document); if (NS_WARN_IF(!document)) { return NS_ERROR_FAILURE; } AutoRestore disableListener( EditSubActionDataRef().mAdjustChangedRangeFromListener); EditSubActionDataRef().mAdjustChangedRangeFromListener = false; AutoTransactionsConserveSelection dontChangeMySelection(*this); EditorRawDOMPoint caretAfterInsert; { AutoTrackDOMPoint trackingInsertingPosition(RangeUpdaterRef(), &pointToInsert); Result insertTextResult = InsertTextWithTransaction(*document, u"\n"_ns, pointToInsert); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return insertTextResult.unwrapErr(); } caretAfterInsert = insertTextResult.unwrap().To(); } // Insert a padding
element at the end of the block element if there is // no content between the inserted linefeed and the following block boundary // to make sure that the last line is visible. // XXX Blink/WebKit inserts another linefeed character in this case. However, // for doing it, we need more work, e.g., updating serializer, deleting // unnecessary padding
element at modifying the last line. if (caretAfterInsert.IsInContentNode() && caretAfterInsert.IsEndOfContainer()) { WSRunScanner wsScannerAtCaret(&aEditingHost, caretAfterInsert); if (wsScannerAtCaret.StartsFromPreformattedLineBreak() && wsScannerAtCaret.EndsByBlockBoundary() && HTMLEditUtils::CanNodeContain(*wsScannerAtCaret.GetEndReasonContent(), *nsGkAtoms::br)) { auto newCaretPosition = caretAfterInsert.To(); { AutoTrackDOMPoint trackingInsertedPosition(RangeUpdaterRef(), &pointToInsert); AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &newCaretPosition); CreateElementResult insertBRElementResult = InsertBRElement(WithTransaction::Yes, newCaretPosition); if (insertBRElementResult.isErr()) { NS_WARNING( "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.unwrapErr(); } // We're tracking next caret position with newCaretPosition. Therefore, // we don't need to update selection here. insertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertBRElementResult.GetNewNode()); } caretAfterInsert = newCaretPosition.To(); } } // manually update the doc changed range so that // OnEndHandlingTopLevelEditSubActionInternal will clean up the correct // portion of the document. if (NS_WARN_IF(!caretAfterInsert.IsSet())) { DebugOnly rvIgnored = SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Selection::SetInterlinePosition(InterlinePosition::" "EndOfLine) failed, but ignored"); if (NS_FAILED(TopLevelEditSubActionDataRef().mChangedRange->CollapseTo( pointToInsert))) { NS_WARNING("nsRange::CollapseTo() failed"); return NS_ERROR_FAILURE; } // XXX Here is odd. We did mChangedRange->SetStartAndEnd(pointToInsert, // caretAfterInsert), but it always fail so that returning // NS_ERROR_FAILURE from here is the traditional behavior... return NS_ERROR_FAILURE; } if (MOZ_UNLIKELY(NS_FAILED( TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( pointToInsert.ToRawRangeBoundary(), caretAfterInsert.ToRawRangeBoundary())))) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return NS_ERROR_FAILURE; } caretAfterInsert.SetInterlinePosition(InterlinePosition::EndOfLine); rv = CollapseSelectionTo(caretAfterInsert); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } Result HTMLEditor::HandleInsertParagraphInMailCiteElement( Element& aMailCiteElement, const EditorDOMPoint& aPointToSplit, Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToSplit.IsSet()); NS_ASSERTION(!HTMLEditUtils::IsEmptyNode(aMailCiteElement), "The mail-cite element will be deleted, does it expected result " "for you?"); const SplitNodeResult splitCiteElementResult = [&]() MOZ_CAN_RUN_SCRIPT { EditorDOMPoint pointToSplit(aPointToSplit); // If our selection is just before a break, nudge it to be just after // it. This does two things for us. It saves us the trouble of having // to add a break here ourselves to preserve the "blockness" of the // inline span mailquote (in the inline case), and : it means the break // won't end up making an empty line that happens to be inside a // mailquote (in either inline or block case). The latter can confuse a // user if they click there and start typing, because being in the // mailquote may affect wrapping behavior, or font color, etc. WSScanResult forwardScanFromPointToSplitResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(&aEditingHost, pointToSplit); if (forwardScanFromPointToSplitResult.Failed()) { return SplitNodeResult(NS_ERROR_FAILURE); } // If selection start point is before a break and it's inside the // mailquote, let's split it after the visible node. if (forwardScanFromPointToSplitResult.ReachedBRElement() && forwardScanFromPointToSplitResult.BRElementPtr() != &aMailCiteElement && aMailCiteElement.Contains( forwardScanFromPointToSplitResult.BRElementPtr())) { pointToSplit = forwardScanFromPointToSplitResult.PointAfterContent(); } if (NS_WARN_IF(!pointToSplit.GetContainerAsContent())) { return SplitNodeResult(NS_ERROR_FAILURE); } SplitNodeResult splitResult = SplitNodeDeepWithTransaction(aMailCiteElement, pointToSplit, SplitAtEdges::eDoNotCreateEmptyContainer); if (splitResult.isErr()) { NS_WARNING( "HTMLEditor::SplitNodeDeepWithTransaction(aMailCiteElement, " "SplitAtEdges::eDoNotCreateEmptyContainer) failed"); return splitResult; } nsresult rv = splitResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed"); return SplitNodeResult(rv); } return splitResult; }(); if (splitCiteElementResult.isErr()) { NS_WARNING("Failed to split a mail-cite element"); return Err(splitCiteElementResult.unwrapErr()); } // When adding caret suggestion to SplitNodeResult, here didn't change // selection so that just ignore it. splitCiteElementResult.IgnoreCaretPointSuggestion(); // Add an invisible
to the end of left cite node if it was a of // style="display: block". This is important, since when serializing the cite // to plain text, the span which caused the visual break is discarded. So the // added
will guarantee that the serializer will insert a break where the // user saw one. // FYI: splitCiteElementResult grabs the previous node and the next node with // nsCOMPtr or EditorDOMPoint. So, it's safe to access leftCiteElement // and rightCiteElement even after changing the DOM tree and/or selection // even though it's raw pointer. Element* const leftCiteElement = Element::FromNodeOrNull(splitCiteElementResult.GetPreviousContent()); Element* const rightCiteElement = Element::FromNodeOrNull(splitCiteElementResult.GetNextContent()); if (leftCiteElement && leftCiteElement->IsHTMLElement(nsGkAtoms::span) && // XXX Oh, this depends on layout information of new element, and it's // created by the hacky flush in DoSplitNode(). So we need to // redesign around this for bug 1710784. leftCiteElement->GetPrimaryFrame() && leftCiteElement->GetPrimaryFrame()->IsBlockFrameOrSubclass()) { nsIContent* lastChild = leftCiteElement->GetLastChild(); if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) { const CreateElementResult insertInvisibleBRElementResult = InsertBRElement(WithTransaction::Yes, EditorDOMPoint::AtEndOf(*leftCiteElement)); if (insertInvisibleBRElementResult.isErr()) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return Err(insertInvisibleBRElementResult.unwrapErr()); } // We don't need to update selection here because we'll do another // InsertBRElement call soon. insertInvisibleBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertInvisibleBRElementResult.GetNewNode()); } } // In most cases,
should be inserted after current cite. However, if // left cite hasn't been created because the split point was start of the // cite node,
should be inserted before the current cite. CreateElementResult insertBRElementResult = InsertBRElement(WithTransaction::Yes, splitCiteElementResult.AtSplitPoint()); if (insertBRElementResult.isErr()) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return Err(insertBRElementResult.unwrapErr()); } // We'll return with suggesting caret position. Therefore, we don't need // to update selection here. insertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertBRElementResult.GetNewNode()); // if aMailCiteElement wasn't a block, we might also want another break before // it. We need to examine the content both before the br we just added and // also just after it. If we don't have another br or block boundary // adjacent, then we will need a 2nd br added to achieve blank line that user // expects. if (HTMLEditUtils::IsInlineElement(aMailCiteElement)) { nsresult rvOfInsertingBRElement = [&]() MOZ_CAN_RUN_SCRIPT { EditorDOMPoint pointToCreateNewBRElement( insertBRElementResult.GetNewNode()); // XXX Cannot we replace this complicated check with just a call of // HTMLEditUtils::IsVisibleBRElement with // resultOfInsertingBRElement.inspect()? WSScanResult backwardScanFromPointToCreateNewBRElementResult = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( &aEditingHost, pointToCreateNewBRElement); if (MOZ_UNLIKELY( backwardScanFromPointToCreateNewBRElementResult.Failed())) { NS_WARNING( "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() " "failed"); return NS_ERROR_FAILURE; } if (!backwardScanFromPointToCreateNewBRElementResult .InVisibleOrCollapsibleCharacters() && !backwardScanFromPointToCreateNewBRElementResult .ReachedSpecialContent()) { return NS_SUCCESS_DOM_NO_OPERATION; } WSScanResult forwardScanFromPointAfterNewBRElementResult = WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( &aEditingHost, EditorRawDOMPoint::After(pointToCreateNewBRElement)); if (MOZ_UNLIKELY(forwardScanFromPointAfterNewBRElementResult.Failed())) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return NS_ERROR_FAILURE; } if (!forwardScanFromPointAfterNewBRElementResult .InVisibleOrCollapsibleCharacters() && !forwardScanFromPointAfterNewBRElementResult .ReachedSpecialContent() && // In case we're at the very end. !forwardScanFromPointAfterNewBRElementResult .ReachedCurrentBlockBoundary()) { return NS_SUCCESS_DOM_NO_OPERATION; } CreateElementResult insertBRElementResult = InsertBRElement(WithTransaction::Yes, pointToCreateNewBRElement); if (insertBRElementResult.isErr()) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.unwrapErr(); } insertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertBRElementResult.GetNewNode()); return NS_OK; }(); if (NS_FAILED(rvOfInsertingBRElement)) { NS_WARNING( "Failed to insert additional
element before the inline right " "mail-cite element"); return Err(rvOfInsertingBRElement); } } if (leftCiteElement && HTMLEditUtils::IsEmptyNode(*leftCiteElement)) { // MOZ_KnownLive(leftCiteElement) because it's grabbed by // splitCiteElementResult. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*leftCiteElement)); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return Err(rv); } } if (rightCiteElement && HTMLEditUtils::IsEmptyNode(*rightCiteElement)) { // MOZ_KnownLive(rightCiteElement) because it's grabbed by // splitCiteElementResult. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*rightCiteElement)); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return Err(rv); } } if (MOZ_UNLIKELY(!insertBRElementResult.GetNewNode()->GetParent())) { NS_WARNING("Inserted
shouldn't become an orphan node"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return EditorDOMPoint(insertBRElementResult.GetNewNode()); } HTMLEditor::CharPointData HTMLEditor::GetPreviousCharPointDataForNormalizingWhiteSpaces( const EditorDOMPointInText& aPoint) const { MOZ_ASSERT(aPoint.IsSetAndValid()); if (!aPoint.IsStartOfContainer()) { return CharPointData::InSameTextNode( HTMLEditor::GetPreviousCharPointType(aPoint)); } const auto previousCharPoint = WSRunScanner::GetPreviousEditableCharPoint( GetActiveEditingHost(), aPoint); if (!previousCharPoint.IsSet()) { return CharPointData::InDifferentTextNode(CharPointType::TextEnd); } return CharPointData::InDifferentTextNode( HTMLEditor::GetCharPointType(previousCharPoint)); } HTMLEditor::CharPointData HTMLEditor::GetInclusiveNextCharPointDataForNormalizingWhiteSpaces( const EditorDOMPointInText& aPoint) const { MOZ_ASSERT(aPoint.IsSetAndValid()); if (!aPoint.IsEndOfContainer()) { return CharPointData::InSameTextNode(HTMLEditor::GetCharPointType(aPoint)); } const auto nextCharPoint = WSRunScanner::GetInclusiveNextEditableCharPoint( GetActiveEditingHost(), aPoint); if (!nextCharPoint.IsSet()) { return CharPointData::InDifferentTextNode(CharPointType::TextEnd); } return CharPointData::InDifferentTextNode( HTMLEditor::GetCharPointType(nextCharPoint)); } // static void HTMLEditor::GenerateWhiteSpaceSequence( nsAString& aResult, uint32_t aLength, const CharPointData& aPreviousCharPointData, const CharPointData& aNextCharPointData) { MOZ_ASSERT(aResult.IsEmpty()); MOZ_ASSERT(aLength); // For now, this method does not assume that result will be append to // white-space sequence in the text node. MOZ_ASSERT(aPreviousCharPointData.AcrossTextNodeBoundary() || !aPreviousCharPointData.IsCollapsibleWhiteSpace()); // For now, this method does not assume that the result will be inserted // into white-space sequence nor start of white-space sequence. MOZ_ASSERT(aNextCharPointData.AcrossTextNodeBoundary() || !aNextCharPointData.IsCollapsibleWhiteSpace()); if (aLength == 1) { // Even if previous/next char is in different text node, we should put // an ASCII white-space between visible characters. // XXX This means that this does not allow to put an NBSP in HTML editor // without preformatted style. However, Chrome has same issue too. if (aPreviousCharPointData.Type() == CharPointType::VisibleChar && aNextCharPointData.Type() == CharPointType::VisibleChar) { aResult.Assign(HTMLEditUtils::kSpace); return; } // If it's start or end of text, put an NBSP. if (aPreviousCharPointData.Type() == CharPointType::TextEnd || aNextCharPointData.Type() == CharPointType::TextEnd) { aResult.Assign(HTMLEditUtils::kNBSP); return; } // If the character is next to a preformatted linefeed, we need to put // an NBSP for avoiding collapsed into the linefeed. if (aPreviousCharPointData.Type() == CharPointType::PreformattedLineBreak || aNextCharPointData.Type() == CharPointType::PreformattedLineBreak) { aResult.Assign(HTMLEditUtils::kNBSP); return; } // Now, the white-space will be inserted to a white-space sequence, but not // end of text. We can put an ASCII white-space only when both sides are // not ASCII white-spaces. aResult.Assign( aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace || aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace); return; } // Generate pairs of NBSP and ASCII white-space. aResult.SetLength(aLength); bool appendNBSP = true; // Basically, starts with an NBSP. char16_t* lastChar = aResult.EndWriting() - 1; for (char16_t* iter = aResult.BeginWriting(); iter != lastChar; iter++) { *iter = appendNBSP ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace; appendNBSP = !appendNBSP; } // If the final one is expected to an NBSP, we can put an NBSP simply. if (appendNBSP) { *lastChar = HTMLEditUtils::kNBSP; return; } // If next char point is end of text node, an ASCII white-space or // preformatted linefeed, we need to put an NBSP. *lastChar = aNextCharPointData.AcrossTextNodeBoundary() || aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace || aNextCharPointData.Type() == CharPointType::PreformattedLineBreak ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace; } void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces( EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete, nsAString& aNormalizedWhiteSpacesInStartNode, nsAString& aNormalizedWhiteSpacesInEndNode) const { MOZ_ASSERT(aStartToDelete.IsSetAndValid()); MOZ_ASSERT(aEndToDelete.IsSetAndValid()); MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete)); MOZ_ASSERT(aNormalizedWhiteSpacesInStartNode.IsEmpty()); MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.IsEmpty()); // First, check whether there is surrounding white-spaces or not, and if there // are, check whether they are collapsible or not. Note that we shouldn't // touch white-spaces in different text nodes for performance, but we need // adjacent text node's first or last character information in some cases. Element* editingHost = GetActiveEditingHost(); const EditorDOMPointInText precedingCharPoint = WSRunScanner::GetPreviousEditableCharPoint(editingHost, aStartToDelete); const EditorDOMPointInText followingCharPoint = WSRunScanner::GetInclusiveNextEditableCharPoint(editingHost, aEndToDelete); // Blink-compat: Normalize white-spaces in first node only when not removing // its last character or no text nodes follow the first node. // If removing last character of first node and there are // following text nodes, white-spaces in following text node are // normalized instead. const bool removingLastCharOfStartNode = aStartToDelete.ContainerAsText() != aEndToDelete.ContainerAsText() || (aEndToDelete.IsEndOfContainer() && followingCharPoint.IsSet()); const bool maybeNormalizePrecedingWhiteSpaces = !removingLastCharOfStartNode && precedingCharPoint.IsSet() && !precedingCharPoint.IsEndOfContainer() && precedingCharPoint.ContainerAsText() == aStartToDelete.ContainerAsText() && precedingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP(); const bool maybeNormalizeFollowingWhiteSpaces = followingCharPoint.IsSet() && !followingCharPoint.IsEndOfContainer() && (followingCharPoint.ContainerAsText() == aEndToDelete.ContainerAsText() || removingLastCharOfStartNode) && followingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP(); if (!maybeNormalizePrecedingWhiteSpaces && !maybeNormalizeFollowingWhiteSpaces) { return; // There are no white-spaces. } // Next, consider the range to normalize. EditorDOMPointInText startToNormalize, endToNormalize; if (maybeNormalizePrecedingWhiteSpaces) { Maybe previousCharOffsetOfWhiteSpaces = HTMLEditUtils::GetPreviousNonCollapsibleCharOffset( precedingCharPoint, {WalkTextOption::TreatNBSPsCollapsible}); startToNormalize.Set(precedingCharPoint.ContainerAsText(), previousCharOffsetOfWhiteSpaces.isSome() ? previousCharOffsetOfWhiteSpaces.value() + 1 : 0); MOZ_ASSERT(!startToNormalize.IsEndOfContainer()); } if (maybeNormalizeFollowingWhiteSpaces) { Maybe nextCharOffsetOfWhiteSpaces = HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset( followingCharPoint, {WalkTextOption::TreatNBSPsCollapsible}); if (nextCharOffsetOfWhiteSpaces.isSome()) { endToNormalize.Set(followingCharPoint.ContainerAsText(), nextCharOffsetOfWhiteSpaces.value()); } else { endToNormalize.SetToEndOf(followingCharPoint.ContainerAsText()); } MOZ_ASSERT(!endToNormalize.IsStartOfContainer()); } // Next, retrieve surrounding information of white-space sequence. // If we're removing first text node's last character, we need to // normalize white-spaces starts from another text node. In this case, // we need to lie for avoiding assertion in GenerateWhiteSpaceSequence(). CharPointData previousCharPointData = removingLastCharOfStartNode ? CharPointData::InDifferentTextNode(CharPointType::TextEnd) : GetPreviousCharPointDataForNormalizingWhiteSpaces( startToNormalize.IsSet() ? startToNormalize : aStartToDelete); CharPointData nextCharPointData = GetInclusiveNextCharPointDataForNormalizingWhiteSpaces( endToNormalize.IsSet() ? endToNormalize : aEndToDelete); // Next, compute number of white-spaces in start/end node. uint32_t lengthInStartNode = 0, lengthInEndNode = 0; if (startToNormalize.IsSet()) { MOZ_ASSERT(startToNormalize.ContainerAsText() == aStartToDelete.ContainerAsText()); lengthInStartNode = aStartToDelete.Offset() - startToNormalize.Offset(); MOZ_ASSERT(lengthInStartNode); } if (endToNormalize.IsSet()) { lengthInEndNode = endToNormalize.ContainerAsText() == aEndToDelete.ContainerAsText() ? endToNormalize.Offset() - aEndToDelete.Offset() : endToNormalize.Offset(); MOZ_ASSERT(lengthInEndNode); // If we normalize white-spaces in a text node, we can replace all of them // with one ReplaceTextTransaction. if (endToNormalize.ContainerAsText() == aStartToDelete.ContainerAsText()) { lengthInStartNode += lengthInEndNode; lengthInEndNode = 0; } } MOZ_ASSERT(lengthInStartNode + lengthInEndNode); // Next, generate normalized white-spaces. if (!lengthInEndNode) { HTMLEditor::GenerateWhiteSpaceSequence( aNormalizedWhiteSpacesInStartNode, lengthInStartNode, previousCharPointData, nextCharPointData); } else if (!lengthInStartNode) { HTMLEditor::GenerateWhiteSpaceSequence( aNormalizedWhiteSpacesInEndNode, lengthInEndNode, previousCharPointData, nextCharPointData); } else { // For making `GenerateWhiteSpaceSequence()` simpler, we should create // whole white-space sequence first, then, copy to the out params. nsAutoString whiteSpaces; HTMLEditor::GenerateWhiteSpaceSequence( whiteSpaces, lengthInStartNode + lengthInEndNode, previousCharPointData, nextCharPointData); aNormalizedWhiteSpacesInStartNode = Substring(whiteSpaces, 0, lengthInStartNode); aNormalizedWhiteSpacesInEndNode = Substring(whiteSpaces, lengthInStartNode); MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.Length() == lengthInEndNode); } // TODO: Shrink the replacing range and string as far as possible because // this may run a lot, i.e., HTMLEditor creates ReplaceTextTransaction // a lot for normalizing white-spaces. Then, each transaction shouldn't // have all white-spaces every time because once it's normalized, we // don't need to normalize all of the sequence again, but currently // we do. // Finally, extend the range. if (startToNormalize.IsSet()) { aStartToDelete = startToNormalize; } if (endToNormalize.IsSet()) { aEndToDelete = endToNormalize; } } Result HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces( const EditorDOMPointInText& aStartToDelete, const EditorDOMPointInText& aEndToDelete, TreatEmptyTextNodes aTreatEmptyTextNodes, DeleteDirection aDeleteDirection) { MOZ_ASSERT(aStartToDelete.IsSetAndValid()); MOZ_ASSERT(aEndToDelete.IsSetAndValid()); MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete)); // Use nsString for these replacing string because we should avoid to copy // the buffer from auto storange to ReplaceTextTransaction. nsString normalizedWhiteSpacesInFirstNode, normalizedWhiteSpacesInLastNode; // First, check whether we need to normalize white-spaces after deleting // the given range. EditorDOMPointInText startToDelete(aStartToDelete); EditorDOMPointInText endToDelete(aEndToDelete); ExtendRangeToDeleteWithNormalizingWhiteSpaces( startToDelete, endToDelete, normalizedWhiteSpacesInFirstNode, normalizedWhiteSpacesInLastNode); // If extended range is still collapsed, i.e., the caller just wants to // normalize white-space sequence, but there is no white-spaces which need to // be replaced, we need to do nothing here. if (startToDelete == endToDelete) { return aStartToDelete.To(); } // Note that the container text node of startToDelete may be removed from // the tree if it becomes empty. Therefore, we need to track the point. EditorDOMPoint newCaretPosition; if (aStartToDelete.ContainerAsText() == aEndToDelete.ContainerAsText()) { newCaretPosition = aEndToDelete.To(); } else if (aDeleteDirection == DeleteDirection::Forward) { newCaretPosition.SetToEndOf(aStartToDelete.ContainerAsText()); } else { newCaretPosition.Set(aEndToDelete.ContainerAsText(), 0u); } // Then, modify the text nodes in the range. while (true) { AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &newCaretPosition); // Use ReplaceTextTransaction if we need to normalize white-spaces in // the first text node. if (!normalizedWhiteSpacesInFirstNode.IsEmpty()) { EditorDOMPoint trackingEndToDelete(endToDelete.ContainerAsText(), endToDelete.Offset()); { AutoTrackDOMPoint trackEndToDelete(RangeUpdaterRef(), &trackingEndToDelete); uint32_t lengthToReplaceInFirstTextNode = startToDelete.ContainerAsText() == trackingEndToDelete.ContainerAsText() ? trackingEndToDelete.Offset() - startToDelete.Offset() : startToDelete.ContainerAsText()->TextLength() - startToDelete.Offset(); nsresult rv = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAsText()), startToDelete.Offset(), lengthToReplaceInFirstTextNode, normalizedWhiteSpacesInFirstNode); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return Err(rv); } if (startToDelete.ContainerAsText() == trackingEndToDelete.ContainerAsText()) { MOZ_ASSERT(normalizedWhiteSpacesInLastNode.IsEmpty()); break; // There is no more text which we need to delete. } } if (MayHaveMutationEventListeners( NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) && (NS_WARN_IF(!trackingEndToDelete.IsSetAndValid()) || NS_WARN_IF(!trackingEndToDelete.IsInTextNode()))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } MOZ_ASSERT(trackingEndToDelete.IsInTextNode()); endToDelete.Set(trackingEndToDelete.ContainerAsText(), trackingEndToDelete.Offset()); // If the remaining range was modified by mutation event listener, // we should stop handling the deletion. startToDelete = EditorDOMPointInText::AtEndOf(*startToDelete.ContainerAsText()); if (MayHaveMutationEventListeners( NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) && NS_WARN_IF(!startToDelete.IsBefore(endToDelete))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } // Delete ASCII whiteSpaces in the range simpley if there are some text // nodes which we don't need to replace their text. if (normalizedWhiteSpacesInLastNode.IsEmpty() || startToDelete.ContainerAsText() != endToDelete.ContainerAsText()) { // If we need to replace text in the last text node, we should // delete text before its previous text node. EditorDOMPointInText endToDeleteExceptReplaceRange = normalizedWhiteSpacesInLastNode.IsEmpty() ? endToDelete : EditorDOMPointInText(endToDelete.ContainerAsText(), 0); if (startToDelete != endToDeleteExceptReplaceRange) { nsresult rv = DeleteTextAndTextNodesWithTransaction( startToDelete, endToDeleteExceptReplaceRange, aTreatEmptyTextNodes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed"); return Err(rv); } if (normalizedWhiteSpacesInLastNode.IsEmpty()) { break; // There is no more text which we need to delete. } if (MayHaveMutationEventListeners( NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED | NS_EVENT_BITS_MUTATION_NODEREMOVED | NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT | NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED) && (NS_WARN_IF(!endToDeleteExceptReplaceRange.IsSetAndValid()) || NS_WARN_IF(!endToDelete.IsSetAndValid()) || NS_WARN_IF(endToDelete.IsStartOfContainer()))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // Then, replace the text in the last text node. startToDelete = endToDeleteExceptReplaceRange; } } // Replace ASCII whiteSpaces in the range and following character in the // last text node. MOZ_ASSERT(!normalizedWhiteSpacesInLastNode.IsEmpty()); MOZ_ASSERT(startToDelete.ContainerAsText() == endToDelete.ContainerAsText()); nsresult rv = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAsText()), startToDelete.Offset(), endToDelete.Offset() - startToDelete.Offset(), normalizedWhiteSpacesInLastNode); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return Err(rv); } break; } if (!newCaretPosition.IsSetAndValid() || !newCaretPosition.GetContainer()->IsInComposedDoc()) { NS_WARNING( "HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces() got lost " "the modifying line"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // Look for leaf node to put caret if we remove some empty inline ancestors // at new caret position. if (!newCaretPosition.IsInTextNode()) { if (const Element* editableBlockElementOrInlineEditingHost = HTMLEditUtils::GetInclusiveAncestorElement( *newCaretPosition.ContainerAsContent(), HTMLEditUtils:: ClosestEditableBlockElementOrInlineEditingHost)) { Element* editingHost = GetActiveEditingHost(); // Try to put caret next to immediately after previous editable leaf. nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( newCaretPosition, *editableBlockElementOrInlineEditingHost, {LeafNodeType::LeafNodeOrNonEditableNode}, editingHost); if (previousContent && !HTMLEditUtils::IsBlockElement(*previousContent)) { newCaretPosition = previousContent->IsText() || HTMLEditUtils::IsContainerNode(*previousContent) ? EditorDOMPoint::AtEndOf(*previousContent) : EditorDOMPoint::After(*previousContent); } // But if the point is very first of a block element or immediately after // a child block, look for next editable leaf instead. else if (nsIContent* nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( newCaretPosition, *editableBlockElementOrInlineEditingHost, {LeafNodeType::LeafNodeOrNonEditableNode}, editingHost)) { newCaretPosition = nextContent->IsText() || HTMLEditUtils::IsContainerNode(*nextContent) ? EditorDOMPoint(nextContent, 0) : EditorDOMPoint(nextContent); } } } // For compatibility with Blink, we should move caret to end of previous // text node if it's direct previous sibling of the first text node in the // range. if (newCaretPosition.IsStartOfContainer() && newCaretPosition.IsInTextNode() && newCaretPosition.GetContainer()->GetPreviousSibling() && newCaretPosition.GetContainer()->GetPreviousSibling()->IsText()) { newCaretPosition.SetToEndOf( newCaretPosition.GetContainer()->GetPreviousSibling()->AsText()); } { AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &newCaretPosition); nsresult rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( newCaretPosition); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed"); return Err(rv); } } if (!newCaretPosition.IsSetAndValid()) { NS_WARNING("Inserting
element caused unexpected DOM tree"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return newCaretPosition; } nsresult HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( const EditorDOMPoint& aPointToInsert) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToInsert.IsSet()); if (!aPointToInsert.GetContainerAsContent()) { return NS_OK; } // If container of the point is not in a block, we don't need to put a // `
` element here. if (!HTMLEditUtils::IsBlockElement(*aPointToInsert.ContainerAsContent())) { return NS_OK; } WSRunScanner wsRunScanner(GetActiveEditingHost(), aPointToInsert); // If the point is not start of a hard line, we don't need to put a `
` // element here. if (!wsRunScanner.StartsFromHardLineBreak()) { return NS_OK; } // If the point is not end of a hard line or the hard line does not end with // block boundary, we don't need to put a `
` element here. if (!wsRunScanner.EndsByBlockBoundary()) { return NS_OK; } // If we cannot insert a `
` element here, do nothing. if (!HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(), *nsGkAtoms::br)) { return NS_OK; } CreateElementResult insertBRElementResult = InsertBRElement( WithTransaction::Yes, aPointToInsert, nsIEditor::ePrevious); if (insertBRElementResult.isErr()) { NS_WARNING( "HTMLEditor::InsertBRElement(WithTransaction::Yes, ePrevious) failed"); return insertBRElementResult.unwrapErr(); } nsresult rv = insertBRElementResult.SuggestCaretPointTo(*this, {}); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "CreateElementResult::SuggestCaretPointTo() failed"); return rv; } EditActionResult HTMLEditor::MakeOrChangeListAndListItemAsSubAction( nsAtom& aListElementOrListItemElementTagName, const nsAString& aBulletType, SelectAllOfCurrentList aSelectAllOfCurrentList) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(&aListElementOrListItemElementTagName == nsGkAtoms::ul || &aListElementOrListItemElementTagName == nsGkAtoms::ol || &aListElementOrListItemElementTagName == nsGkAtoms::dl || &aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt); if (NS_WARN_IF(!mInitSucceeded)) { return EditActionIgnored(NS_ERROR_NOT_INITIALIZED); } EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionIgnored(); } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); // XXX EditSubAction::eCreateOrChangeDefinitionListItem and // EditSubAction::eCreateOrChangeList are treated differently in // HTMLEditor::MaybeSplitElementsAtEveryBRElement(). Only when // EditSubAction::eCreateOrChangeList, it splits inline nodes. // Currently, it shouldn't be done when we called for formatting // `
` or `
` by // HTMLEditor::MakeDefinitionListItemWithTransaction(). But this // difference may be a bug. We should investigate this later. IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, &aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt ? EditSubAction::eCreateOrChangeDefinitionListItem : EditSubAction::eCreateOrChangeList, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditActionResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } nsAtom* listTagName = nullptr; nsAtom* listItemTagName = nullptr; if (&aListElementOrListItemElementTagName == nsGkAtoms::ul || &aListElementOrListItemElementTagName == nsGkAtoms::ol) { listTagName = &aListElementOrListItemElementTagName; listItemTagName = nsGkAtoms::li; } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dl) { listTagName = &aListElementOrListItemElementTagName; listItemTagName = nsGkAtoms::dd; } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt) { listTagName = nsGkAtoms::dl; listItemTagName = &aListElementOrListItemElementTagName; } else { NS_WARNING( "aListElementOrListItemElementTagName was neither list element name " "nor " "definition listitem element name"); return EditActionResult(NS_ERROR_INVALID_ARG); } // Expands selection range to include the immediate block parent, and then // further expands to include any ancestors whose children are all in the // range. if (!SelectionRef().IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return EditActionResult(rv); } } // ChangeSelectedHardLinesToList() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, editor might have been destroyed // at restoring Selection. result = ChangeSelectedHardLinesToList(MOZ_KnownLive(*listTagName), MOZ_KnownLive(*listItemTagName), aBulletType, aSelectAllOfCurrentList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::ChangeSelectedHardLinesToList() failed"); return result; } EditActionResult HTMLEditor::ChangeSelectedHardLinesToList( nsAtom& aListElementTagName, nsAtom& aListItemElementTagName, const nsAString& aBulletType, SelectAllOfCurrentList aSelectAllOfCurrentList) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); RefPtr editingHost = GetActiveEditingHost(); if (MOZ_UNLIKELY(NS_WARN_IF(!editingHost))) { return EditActionResult(NS_ERROR_FAILURE); } AutoSelectionRestorer restoreSelectionLater(*this); AutoTArray, 64> arrayOfContents; Element* parentListElement = aSelectAllOfCurrentList == SelectAllOfCurrentList::Yes ? GetParentListElementAtSelection() : nullptr; if (parentListElement) { arrayOfContents.AppendElement( OwningNonNull(*parentListElement)); } else { AutoTransactionsConserveSelection dontChangeMySelection(*this); nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eCreateOrChangeList, CollectNonEditableNodes::No); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(" "eCreateOrChangeList, CollectNonEditableNodes::No) failed"); return EditActionResult(rv); } } // check if all our nodes are
s, or empty inlines bool bOnlyBreaks = true; for (auto& content : arrayOfContents) { // if content is not a Break or empty inline, we're done if (!content->IsHTMLElement(nsGkAtoms::br) && !HTMLEditUtils::IsEmptyInlineContent(content)) { bOnlyBreaks = false; break; } } // if no nodes, we make empty list. Ditto if the user tried to make a list // of some # of breaks. if (arrayOfContents.IsEmpty() || bOnlyBreaks) { // if only breaks, delete them if (bOnlyBreaks) { for (auto& content : arrayOfContents) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return EditActionResult(rv); } } } const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionResult(NS_ERROR_FAILURE); } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return EditActionResult(NS_ERROR_FAILURE); } // Make sure we can put a list here. if (!HTMLEditUtils::CanNodeContain(*atStartOfSelection.GetContainer(), aListElementTagName)) { return EditActionCanceled(); } RefPtr newListItemElement; CreateElementResult createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( aListElementTagName, atStartOfSelection, BRElementNextToSplitPoint::Keep, *editingHost, // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868 [&newListItemElement, &aListItemElementTagName]( HTMLEditor& aHTMLEditor, Element& aListElement, const EditorDOMPoint& aPointToInsert) MOZ_CAN_RUN_SCRIPT_BOUNDARY { const auto withTransaction = aListElement.IsInComposedDoc() ? WithTransaction::Yes : WithTransaction::No; CreateElementResult createNewListItemElementResult = aHTMLEditor.CreateAndInsertElement( withTransaction, aListItemElementTagName, EditorDOMPoint(&aListElement, 0u)); if (createNewListItemElementResult.isErr()) { NS_WARNING( nsPrintfCString( "HTMLEditor::CreateAndInsertElement(%s) failed", ToString(withTransaction).c_str()) .get()); return createNewListItemElementResult.unwrapErr(); } // There is AutoSelectionRestorer in this method so that it'll // be restored or updated with making it abort. Therefore, // we don't need to update selection here. // XXX I'd like to check restoreSelectionLater here, but it // requires ifdefs to avoid bustage of opt builds caused // by unused warning... createNewListItemElementResult.IgnoreCaretPointSuggestion(); newListItemElement = createNewListItemElementResult.UnwrapNewNode(); MOZ_ASSERT(newListItemElement); return NS_OK; }); if (createNewListElementResult.isErr()) { NS_WARNING( nsPrintfCString( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "%s) failed", nsAtomCString(&aListElementTagName).get()) .get()); return EditActionResult(createNewListElementResult.unwrapErr()); } MOZ_ASSERT(createNewListElementResult.GetNewNode()); // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = newListItemElement; // Put selection in new list item and don't restore the Selection. createNewListElementResult.IgnoreCaretPointSuggestion(); restoreSelectionLater.Abort(); nsresult rv = CollapseSelectionToStartOf(*newListItemElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed"); return EditActionResult(rv); } // if there is only one node in the array, and it is a list, div, or // blockquote, then look inside of it until we find inner list or content. if (arrayOfContents.Length() == 1) { if (Element* deepestDivBlockquoteOrListElement = HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild( arrayOfContents[0], {WalkTreeOption::IgnoreNonEditableNode}, nsGkAtoms::div, nsGkAtoms::blockquote, nsGkAtoms::ul, nsGkAtoms::ol, nsGkAtoms::dl)) { if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements( nsGkAtoms::div, nsGkAtoms::blockquote)) { arrayOfContents.Clear(); CollectChildren(*deepestDivBlockquoteOrListElement, arrayOfContents, 0, CollectListChildren::No, CollectTableChildren::No, CollectNonEditableNodes::Yes); } else { arrayOfContents.ReplaceElementAt( 0, OwningNonNull(*deepestDivBlockquoteOrListElement)); } } } // Ok, now go through all the nodes and put then in the list, // or whatever is approriate. Wohoo! uint32_t countOfCollectedContents = arrayOfContents.Length(); RefPtr curList, prevListItem; for (uint32_t i = 0; i < countOfCollectedContents; i++) { // here's where we actually figure out what to do OwningNonNull content = arrayOfContents[i]; // make sure we don't assemble content that is in different table cells // into the same list. respect table cell boundaries when listifying. if (curList && HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*curList) != HTMLEditUtils::GetInclusiveAncestorAnyTableElement(content)) { curList = nullptr; } // If current node is a `
` element, delete it and forget previous // list item element. // If current node is an empty inline node, just delete it. if (EditorUtils::IsEditableContent(content, EditorType::HTML) && (content->IsHTMLElement(nsGkAtoms::br) || HTMLEditUtils::IsEmptyInlineContent(content))) { nsresult rv = DeleteNodeWithTransaction(*content); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return EditActionResult(rv); } if (content->IsHTMLElement(nsGkAtoms::br)) { prevListItem = nullptr; } continue; } if (HTMLEditUtils::IsAnyListElement(content)) { // If we met a list element and current list element is not a descendant // of the list, append current node to end of the current list element. // Then, wrap it with list item element and delete the old container. if (curList && !EditorUtils::IsDescendantOf(*content, *curList)) { nsresult rv = MoveNodeToEndWithTransaction(*content, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } CreateElementResult convertListTypeResult = ChangeListElementType(MOZ_KnownLive(*content->AsElement()), aListElementTagName, aListItemElementTagName); if (convertListTypeResult.isErr()) { NS_WARNING("HTMLEditor::ChangeListElementType() failed"); return EditActionResult(convertListTypeResult.unwrapErr()); } rv = RemoveBlockContainerWithTransaction( MOZ_KnownLive(*convertListTypeResult.GetNewNode())); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::RemoveBlockContainerWithTransaction() failed"); return EditActionResult(rv); } prevListItem = nullptr; continue; } // If current list element is in found list element or we've not met a // list element, convert current list element to proper type. CreateElementResult convertListTypeResult = ChangeListElementType(MOZ_KnownLive(*content->AsElement()), aListElementTagName, aListItemElementTagName); if (convertListTypeResult.isErr()) { NS_WARNING("HTMLEditor::ChangeListElementType() failed"); return EditActionResult(convertListTypeResult.unwrapErr()); } curList = convertListTypeResult.UnwrapNewNode(); prevListItem = nullptr; continue; } EditorDOMPoint atContent(content); if (NS_WARN_IF(!atContent.IsSet())) { return EditActionResult(NS_ERROR_FAILURE); } MOZ_ASSERT(atContent.IsSetAndValid()); if (HTMLEditUtils::IsListItem(content)) { // If current list item element is not in proper list element, we need // to conver the list element. if (!atContent.IsContainerHTMLElement(&aListElementTagName)) { // If we've not met a list element or current node is not in current // list element, insert a list element at current node and set // current list element to the new one. if (!curList || EditorUtils::IsDescendantOf(*content, *curList)) { if (NS_WARN_IF(!atContent.GetContainerAsContent())) { return EditActionResult(NS_ERROR_FAILURE); } const SplitNodeResult splitListItemParentResult = SplitNodeWithTransaction(atContent); if (splitListItemParentResult.isErr()) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return EditActionResult(splitListItemParentResult.unwrapErr()); } MOZ_ASSERT(splitListItemParentResult.DidSplit()); // We'll update selection after creating new list element below. // Therefore, we don't need to handle selection now. splitListItemParentResult.IgnoreCaretPointSuggestion(); CreateElementResult createNewListElementResult = CreateAndInsertElement( WithTransaction::Yes, aListElementTagName, splitListItemParentResult.AtNextContent()); if (createNewListElementResult.isErr()) { NS_WARNING( "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) " "failed"); return EditActionResult(createNewListElementResult.unwrapErr()); } nsresult rv = createNewListElementResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CreateElementResult::SuggestCaretPointTo() failed, but ignored"); curList = createNewListElementResult.UnwrapNewNode(); MOZ_ASSERT(curList); } // Then, move current node into current list element. nsresult rv = MoveNodeToEndWithTransaction(*content, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } // Convert list item type if current node is different list item type. if (!content->IsHTMLElement(&aListItemElementTagName)) { RefPtr newListItemElement = ReplaceContainerWithTransaction( MOZ_KnownLive(*content->AsElement()), aListItemElementTagName); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!newListItemElement) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } } } else { // If we've not met a list element, set current list element to the // parent of current list item element. if (!curList) { curList = atContent.GetContainerAsElement(); NS_WARNING_ASSERTION( HTMLEditUtils::IsAnyListElement(curList), "Current list item parent is not a list element"); } // If current list item element is not a child of current list element, // move it into current list item. else if (atContent.GetContainer() != curList) { nsresult rv = MoveNodeToEndWithTransaction(*content, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } } // Then, if current list item element is not proper type for current // list element, convert list item element to proper element. if (!content->IsHTMLElement(&aListItemElementTagName)) { RefPtr newListItemElement = ReplaceContainerWithTransaction( MOZ_KnownLive(*content->AsElement()), aListItemElementTagName); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!newListItemElement) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } } } Element* element = Element::FromNode(content); if (NS_WARN_IF(!element)) { return EditActionResult(NS_ERROR_FAILURE); } // If bullet type is specified, set list type attribute. // XXX Cannot we set type attribute before inserting the list item // element into the DOM tree? if (!aBulletType.IsEmpty()) { nsresult rv = SetAttributeWithTransaction( MOZ_KnownLive(*element), *nsGkAtoms::type, aBulletType); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::SetAttributeWithTransaction(nsGkAtoms::type) " "failed"); return EditActionResult(rv); } continue; } // Otherwise, remove list type attribute if there is. if (!element->HasAttr(nsGkAtoms::type)) { continue; } nsresult rv = RemoveAttributeWithTransaction(MOZ_KnownLive(*element), *nsGkAtoms::type); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::type) " "failed"); return EditActionResult(rv); } continue; } MOZ_ASSERT(!HTMLEditUtils::IsAnyListElement(content) && !HTMLEditUtils::IsListItem(content)); // If current node is a `
` element, replace it in the array with // its children. // XXX I think that this should be done when we collect the nodes above. // Then, we can change this `for` loop to ranged-for loop. if (content->IsHTMLElement(nsGkAtoms::div)) { prevListItem = nullptr; CollectChildren(*content, arrayOfContents, i + 1, CollectListChildren::Yes, CollectTableChildren::Yes, CollectNonEditableNodes::Yes); nsresult rv = RemoveContainerWithTransaction(MOZ_KnownLive(*content->AsElement())); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed"); return EditActionResult(rv); } // Extend the loop length to handle all children collected here. countOfCollectedContents = arrayOfContents.Length(); continue; } // If we've not met a list element, create a list element and make it // current list element. if (!curList) { prevListItem = nullptr; CreateElementResult createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( aListElementTagName, atContent, BRElementNextToSplitPoint::Keep, *editingHost); if (createNewListElementResult.isErr()) { NS_WARNING( nsPrintfCString( "HTMLEditor::" "InsertElementWithSplittingAncestorsWithTransaction(%s) failed", nsAtomCString(&aListElementTagName).get()) .get()); return EditActionResult(createNewListElementResult.unwrapErr()); } // We'll restore selection so that we don't need to update selection now. createNewListElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(restoreSelectionLater.MaybeRestoreSelectionLater()); MOZ_ASSERT(createNewListElementResult.GetNewNode()); curList = createNewListElementResult.UnwrapNewNode(); // Set new block element of top level edit sub-action to the new list // element for setting selection into it. // XXX This must be wrong. If we're handling nested edit action, // we shouldn't overwrite the new block element. TopLevelEditSubActionDataRef().mNewBlockElement = curList; // atContent is now referring the right node with mOffset but // referring the left node with mRef. So, invalidate it now. atContent.Clear(); } // If we're currently handling contents of a list item and current node // is not a block element, move current node into the list item. if (HTMLEditUtils::IsInlineElement(content) && prevListItem) { nsresult rv = MoveNodeToEndWithTransaction(*content, *prevListItem); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } continue; } // If current node is a paragraph, that means that it does not contain // block children so that we can just replace it with new list item // element and move it into current list element. // XXX This is too rough handling. If web apps modifies DOM tree directly, // any elements can have block elements as children. if (content->IsHTMLElement(nsGkAtoms::p)) { RefPtr newListItemElement = ReplaceContainerWithTransaction( MOZ_KnownLive(*content->AsElement()), aListItemElementTagName); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!newListItemElement) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } prevListItem = nullptr; nsresult rv = MoveNodeToEndWithTransaction(*newListItemElement, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } // XXX Why don't we set `type` attribute here?? continue; } // If current node is not a paragraph, wrap current node with new list // item element and move it into current list element. RefPtr newListItemElement = InsertContainerWithTransaction(*content, aListItemElementTagName); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (!newListItemElement) { NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed"); return EditActionResult(NS_ERROR_FAILURE); } // If current node is not a block element, new list item should have // following inline nodes too. if (HTMLEditUtils::IsInlineElement(content)) { prevListItem = newListItemElement; } else { prevListItem = nullptr; } nsresult rv = MoveNodeToEndWithTransaction(*newListItemElement, *curList); if (NS_WARN_IF(Destroyed())) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return EditActionResult(rv); } // XXX Why don't we set `type` attribute here?? } return EditActionHandled(); } nsresult HTMLEditor::RemoveListAtSelectionAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.Rv(); } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eRemoveList, 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"); if (!SelectionRef().IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return rv; } } AutoSelectionRestorer restoreSelectionLater(*this); AutoTArray, 64> arrayOfContents; { AutoTransactionsConserveSelection dontChangeMySelection(*this); nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eCreateOrChangeList, CollectNonEditableNodes::No); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(" "eCreateOrChangeList, CollectNonEditableNodes::No) failed"); return rv; } } // Remove all non-editable nodes. Leave them be. // XXX SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges() // should return only editable contents when it's called with // CollectNonEditableNodes::No. for (int32_t i = arrayOfContents.Length() - 1; i >= 0; i--) { OwningNonNull& content = arrayOfContents[i]; if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) { arrayOfContents.RemoveElementAt(i); } } // Only act on lists or list items in the array for (auto& content : arrayOfContents) { // here's where we actually figure out what to do if (HTMLEditUtils::IsListItem(content)) { // unlist this listitem nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()), LiftUpFromAllParentListElements::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:" ":Yes) failed"); return rv; } continue; } if (HTMLEditUtils::IsAnyListElement(content)) { // node is a list, move list items out nsresult rv = DestroyListStructureRecursively(MOZ_KnownLive(*content->AsElement())); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DestroyListStructureRecursively() failed"); return rv; } continue; } } return NS_OK; } nsresult HTMLEditor::FormatBlockContainerWithTransaction(nsAtom& blockType) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); RefPtr editingHost = GetActiveEditingHost(); if (MOZ_UNLIKELY(NS_WARN_IF(!editingHost))) { return NS_ERROR_FAILURE; } if (!SelectionRef().IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return rv; } } AutoSelectionRestorer restoreSelectionLater(*this); AutoTransactionsConserveSelection dontChangeMySelection(*this); AutoTArray, 64> arrayOfContents; nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eCreateOrRemoveBlock, CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(" "eCreateOrRemoveBlock, CollectNonEditableNodes::Yes) failed"); return rv; } // If there is no visible and editable nodes in the edit targets, make an // empty block. // XXX Isn't this odd if there are only non-editable visible nodes? if (HTMLEditUtils::IsEmptyOneHardLine(arrayOfContents)) { const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorDOMPoint pointToInsertBlock(firstRange->StartRef()); if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { if (!pointToInsertBlock.IsInContentNode()) { NS_WARNING( "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " "block parent because container of the point is not content"); return NS_ERROR_FAILURE; } // We are removing blocks (going to "body text") const RefPtr editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *pointToInsertBlock.ContainerAsContent(), HTMLEditUtils::ClosestEditableBlockElement); if (!editableBlockElement) { NS_WARNING( "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " "block parent"); return NS_ERROR_FAILURE; } if (!HTMLEditUtils::IsFormatNode(editableBlockElement)) { return NS_OK; } // If the first editable node after selection is a br, consume it. // Otherwise it gets pushed into a following block after the split, // which is visually bad. if (nsCOMPtr brContent = HTMLEditUtils::GetNextContent( pointToInsertBlock, {WalkTreeOption::IgnoreNonEditableNode}, editingHost)) { if (brContent && brContent->IsHTMLElement(nsGkAtoms::br)) { AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock); rv = DeleteNodeWithTransaction(*brContent); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } } } // Do the splits! const SplitNodeResult splitNodeResult = SplitNodeDeepWithTransaction( *editableBlockElement, pointToInsertBlock, SplitAtEdges::eDoNotCreateEmptyContainer); if (splitNodeResult.isErr()) { NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed"); return splitNodeResult.unwrapErr(); } // Put a
element at the split point const CreateElementResult insertBRElementResult = InsertBRElement( WithTransaction::Yes, splitNodeResult.AtSplitPoint()); if (insertBRElementResult.isErr()) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.unwrapErr(); } MOZ_ASSERT(insertBRElementResult.GetNewNode()); // Don't restore the selection restoreSelectionLater.Abort(); // Put selection at the split point splitNodeResult.IgnoreCaretPointSuggestion(); nsresult rv = CollapseSelectionTo( EditorRawDOMPoint(insertBRElementResult.GetNewNode())); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } // We are making a block. Consume a br, if needed. if (nsCOMPtr maybeBRContent = HTMLEditUtils::GetNextContent( pointToInsertBlock, {WalkTreeOption::IgnoreNonEditableNode, WalkTreeOption::StopAtBlockBoundary}, editingHost)) { if (maybeBRContent->IsHTMLElement(nsGkAtoms::br)) { AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock); rv = DeleteNodeWithTransaction(*maybeBRContent); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } // We don't need to act on this node any more arrayOfContents.RemoveElement(maybeBRContent); } } // Make sure we can put a block here. CreateElementResult createNewBlockElementResult = InsertElementWithSplittingAncestorsWithTransaction( blockType, pointToInsertBlock, BRElementNextToSplitPoint::Keep, *editingHost); if (createNewBlockElementResult.isErr()) { NS_WARNING( nsPrintfCString( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "%s) failed", nsAtomCString(&blockType).get()) .get()); return createNewBlockElementResult.unwrapErr(); } // We'll update selection after deleting the content nodes and nobody refers // selection until then. Therefore, we don't need to update selection here. createNewBlockElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(restoreSelectionLater.MaybeRestoreSelectionLater()); MOZ_ASSERT(createNewBlockElementResult.GetNewNode()); // Remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = createNewBlockElementResult.GetNewNode(); // Delete anything that was in the list of nodes while (!arrayOfContents.IsEmpty()) { OwningNonNull& content = arrayOfContents[0]; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } arrayOfContents.RemoveElementAt(0); } // Don't restore the selection restoreSelectionLater.Abort(); // Put selection in new block // MOZ_KnownLive(createNewBlockElementResult.GetNewNode()) because it's // grabbed by createNewBlockElementResult. rv = CollapseSelectionToStartOf( MOZ_KnownLive(*createNewBlockElementResult.GetNewNode())); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed"); return rv; } // Okay, now go through all the nodes and make the right kind of blocks, or // whatever is approriate. Woohoo! Note: blockquote is handled a little // differently. if (&blockType == nsGkAtoms::blockquote) { nsresult rv = MoveNodesIntoNewBlockquoteElement(arrayOfContents); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::MoveNodesIntoNewBlockquoteElement() failed"); return rv; } if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { nsresult rv = RemoveBlockContainerElements(arrayOfContents); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::RemoveBlockContainerElements() failed"); return rv; } rv = CreateOrChangeBlockContainerElement(arrayOfContents, blockType); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::CreateOrChangeBlockContainerElement() failed"); return rv; } nsresult HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (!SelectionRef().IsCollapsed()) { return NS_OK; } const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } if (!atStartOfSelection.Container()->IsElement()) { return NS_OK; } OwningNonNull startContainerElement = *atStartOfSelection.Container()->AsElement(); nsresult rv = InsertPaddingBRElementForEmptyLastLineIfNeeded(startContainerElement); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::InsertPaddingBRElementForEmptyLastLineIfNeeded() failed"); return rv; } EditActionResult HTMLEditor::IndentAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eIndent, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditActionResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionIgnored(); } result |= HandleIndentAtSelection(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::HandleIndentAtSelection() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Mutation event listener might have changed selection"); return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() failed"); return result.SetResult(rv); } // Helper for Handle[CSS|HTML]IndentAtSelectionInternal nsresult HTMLEditor::IndentListChild(RefPtr* aCurList, const EditorDOMPoint& aCurPoint, nsIContent& aContent) { MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(aCurPoint.GetContainer()), "unexpected container"); MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); RefPtr editingHost = GetActiveEditingHost(); if (MOZ_UNLIKELY(NS_WARN_IF(!editingHost))) { return NS_ERROR_FAILURE; } // some logic for putting list items into nested lists... // Check for whether we should join a list that follows aContent. // We do this if the next element is a list, and the list is of the // same type (li/ol) as aContent was a part it. if (nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling( aContent, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, WalkTreeOption::IgnoreNonEditableNode})) { if (HTMLEditUtils::IsAnyListElement(nextEditableSibling) && aCurPoint.GetContainer()->NodeInfo()->NameAtom() == nextEditableSibling->NodeInfo()->NameAtom() && aCurPoint.GetContainer()->NodeInfo()->NamespaceID() == nextEditableSibling->NodeInfo()->NamespaceID()) { nsresult rv = MoveNodeWithTransaction( aContent, EditorDOMPoint(nextEditableSibling, 0)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EdigtorBase::MoveNodeWithTransaction() failed"); return rv; } } // Check for whether we should join a list that preceeds aContent. // We do this if the previous element is a list, and the list is of // the same type (li/ol) as aContent was a part of. if (nsCOMPtr previousEditableSibling = HTMLEditUtils::GetPreviousSibling( aContent, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, WalkTreeOption::IgnoreNonEditableNode})) { if (HTMLEditUtils::IsAnyListElement(previousEditableSibling) && aCurPoint.GetContainer()->NodeInfo()->NameAtom() == previousEditableSibling->NodeInfo()->NameAtom() && aCurPoint.GetContainer()->NodeInfo()->NamespaceID() == previousEditableSibling->NodeInfo()->NamespaceID()) { nsresult rv = MoveNodeToEndWithTransaction(aContent, *previousEditableSibling); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } } // check to see if aCurList is still appropriate. Which it is if // aContent is still right after it in the same list. nsIContent* previousEditableSibling = *aCurList ? HTMLEditUtils::GetPreviousSibling( aContent, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, WalkTreeOption::IgnoreNonEditableNode}) : nullptr; if (!*aCurList || (previousEditableSibling && previousEditableSibling != *aCurList)) { nsAtom* containerName = aCurPoint.GetContainer()->NodeInfo()->NameAtom(); // Create a new nested list of correct type. CreateElementResult createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( MOZ_KnownLive(*containerName), aCurPoint, BRElementNextToSplitPoint::Keep, *editingHost); if (createNewListElementResult.isErr()) { NS_WARNING( nsPrintfCString( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "%s) failed", nsAtomCString(containerName).get()) .get()); return createNewListElementResult.unwrapErr(); } nsresult rv = createNewListElementResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return rv; } MOZ_ASSERT(createNewListElementResult.GetNewNode()); // aCurList is now the correct thing to put aContent in // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = createNewListElementResult.GetNewNode(); *aCurList = createNewListElementResult.UnwrapNewNode(); } // tuck the node into the end of the active list RefPtr container = *aCurList; nsresult rv = MoveNodeToEndWithTransaction(aContent, *container); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } EditActionResult HTMLEditor::HandleIndentAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() " "failed, but ignored"); if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) { nsresult rv = EnsureCaretNotAfterInvisibleBRElement(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() " "failed, but ignored"); if (NS_SUCCEEDED(rv)) { nsresult rv = PrepareInlineStylesForCaret(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return EditActionResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Mutation event listener might have changed the selection"); return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } if (IsCSSEnabled()) { nsresult rv = HandleCSSIndentAtSelection(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::HandleCSSIndentAtSelection() failed"); return EditActionHandled(rv); } rv = HandleHTMLIndentAtSelection(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::HandleHTMLIndent() failed"); return EditActionHandled(rv); } nsresult HTMLEditor::HandleCSSIndentAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (!SelectionRef().IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return rv; } } // HandleCSSIndentAtSelectionInternal() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, editor might have been destroyed // at restoring Selection. nsresult rv = HandleCSSIndentAtSelectionInternal(); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::HandleCSSIndentAtSelectionInternal() failed"); return rv; } nsresult HTMLEditor::HandleCSSIndentAtSelectionInternal() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); RefPtr editingHost = GetActiveEditingHost(); if (MOZ_UNLIKELY(NS_WARN_IF(!editingHost))) { return NS_ERROR_FAILURE; } AutoSelectionRestorer restoreSelectionLater(*this); AutoTArray, 64> arrayOfContents; // short circuit: detect case of collapsed selection inside an
  • . // just sublist that
  • . This prevents bug 97797. if (SelectionRef().IsCollapsed()) { const auto atCaret = GetFirstSelectionStartPoint(); if (NS_WARN_IF(!atCaret.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atCaret.IsInContentNode()); Element* const editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *atCaret.ContainerAsContent(), HTMLEditUtils::ClosestEditableBlockElement); if (editableBlockElement && HTMLEditUtils::IsListItem(editableBlockElement)) { arrayOfContents.AppendElement(*editableBlockElement); } } if (arrayOfContents.IsEmpty()) { nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eIndent, CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(" "eIndent, CollectNonEditableNodes::Yes) failed"); return rv; } } // If there is no visible and editable nodes in the edit targets, make an // empty block. // XXX Isn't this odd if there are only non-editable visible nodes? if (HTMLEditUtils::IsEmptyOneHardLine(arrayOfContents)) { // get selection location const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } // make sure we can put a block here CreateElementResult createNewDivElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::div, atStartOfSelection, BRElementNextToSplitPoint::Keep, *editingHost); if (createNewDivElementResult.isErr()) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::div) failed"); return createNewDivElementResult.unwrapErr(); } // We'll update selection below, and nobody refers selection until then. // Therefore, we don't need to touch selection here. createNewDivElementResult.IgnoreCaretPointSuggestion(); const RefPtr newDivElement = createNewDivElementResult.UnwrapNewNode(); MOZ_ASSERT(newDivElement); // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = newDivElement; nsresult rv = ChangeMarginStart(*newDivElement, ChangeMargin::Increase); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::ChangeMarginStart() failed, but ignored"); // delete anything that was in the list of nodes // XXX We don't need to remove the nodes from the array for performance. while (!arrayOfContents.IsEmpty()) { OwningNonNull& content = arrayOfContents[0]; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } arrayOfContents.RemoveElementAt(0); } // Don't restore the selection restoreSelectionLater.Abort(); // put selection in new block rv = CollapseSelectionToStartOf(*newDivElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed"); return rv; } // Ok, now go through all the nodes and put them in a blockquote, // or whatever is appropriate. RefPtr curList, curQuote; for (OwningNonNull& content : arrayOfContents) { // Here's where we actually figure out what to do. EditorDOMPoint atContent(content); if (NS_WARN_IF(!atContent.IsSet())) { continue; } // Ignore all non-editable nodes. Leave them be. // XXX We ignore non-editable nodes here, but not so in the above block. if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) { continue; } if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = IndentListChild(&curList, atContent, MOZ_KnownLive(content)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::IndentListChild() failed"); return rv; } continue; } // Not a list item. if (HTMLEditUtils::IsBlockElement(content)) { nsresult rv = ChangeMarginStart(MOZ_KnownLive(*content->AsElement()), ChangeMargin::Increase); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::ChangeMarginStart() failed, but ignored"); curQuote = nullptr; continue; } if (!curQuote) { // First, check that our element can contain a div. if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(), *nsGkAtoms::div)) { return NS_OK; // cancelled } CreateElementResult createNewDivElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep, *editingHost); if (createNewDivElementResult.isErr()) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::div) failed"); return createNewDivElementResult.unwrapErr(); } nsresult rv = createNewDivElementResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return rv; } RefPtr newDivElement = createNewDivElementResult.UnwrapNewNode(); MOZ_ASSERT(newDivElement); rv = ChangeMarginStart(*newDivElement, ChangeMargin::Increase); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::ChangeMarginStart() failed, but ignored"); // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = newDivElement; curQuote = std::move(newDivElement); // curQuote is now the correct thing to put content in } // tuck the node into the end of the active blockquote // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *curQuote); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } } return NS_OK; } nsresult HTMLEditor::HandleHTMLIndentAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (!SelectionRef().IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() " "failed"); return rv; } } // HandleHTMLIndentAtSelectionInternal() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, editor might have been destroyed // at restoring Selection. nsresult rv = HandleHTMLIndentAtSelectionInternal(); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::HandleHTMLIndentAtSelectionInternal() failed"); return rv; } nsresult HTMLEditor::HandleHTMLIndentAtSelectionInternal() { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); RefPtr editingHost = GetActiveEditingHost(); if (MOZ_UNLIKELY(NS_WARN_IF(!editingHost))) { return NS_ERROR_FAILURE; } AutoSelectionRestorer restoreSelectionLater(*this); // convert the selection ranges into "promoted" selection ranges: // this basically just expands the range to include the immediate // block parent, and then further expands to include any ancestors // whose children are all in the range AutoTArray, 4> arrayOfRanges; GetSelectionRangesExtendedToHardLineStartAndEnd(arrayOfRanges, EditSubAction::eIndent); // use these ranges to construct a list of nodes to act on. AutoTArray, 64> arrayOfContents; nsresult rv = SplitInlinesAndCollectEditTargetNodes( arrayOfRanges, arrayOfContents, EditSubAction::eIndent, CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::SplitInlinesAndCollectEditTargetNodes(eIndent, " "CollectNonEditableNodes::Yes) failed"); return rv; } // If there is no visible and editable nodes in the edit targets, make an // empty block. // XXX Isn't this odd if there are only non-editable visible nodes? if (HTMLEditUtils::IsEmptyOneHardLine(arrayOfContents)) { const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return NS_ERROR_FAILURE; } EditorDOMPoint atStartOfSelection(firstRange->StartRef()); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return NS_ERROR_FAILURE; } // Make sure we can put a block here. CreateElementResult createNewBlockQuoteElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::blockquote, atStartOfSelection, BRElementNextToSplitPoint::Keep, *editingHost); if (createNewBlockQuoteElementResult.isErr()) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::blockquote) failed"); return createNewBlockQuoteElementResult.unwrapErr(); } // We'll update selection below after deleting the content nodes and nobody // refers selection until then. Therefore, we don't need to update // selection here. createNewBlockQuoteElementResult.IgnoreCaretPointSuggestion(); RefPtr newBlockQuoteElement = createNewBlockQuoteElementResult.UnwrapNewNode(); MOZ_ASSERT(newBlockQuoteElement); // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = newBlockQuoteElement; // delete anything that was in the list of nodes // XXX We don't need to remove the nodes from the array for performance. for (const OwningNonNull& content : arrayOfContents) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } } // Don't restore the selection restoreSelectionLater.Abort(); nsresult rv = CollapseSelectionToStartOf(*newBlockQuoteElement); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed"); return rv; } // Ok, now go through all the nodes and put them in a blockquote, // or whatever is appropriate. Wohoo! RefPtr curList, curQuote, indentedLI; for (OwningNonNull& content : arrayOfContents) { // Here's where we actually figure out what to do. EditorDOMPoint atContent(content); if (NS_WARN_IF(!atContent.IsSet())) { continue; } // Ignore all non-editable nodes. Leave them be. // XXX We ignore non-editable nodes here, but not so in the above block. if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) { continue; } if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) { // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. nsresult rv = IndentListChild(&curList, atContent, MOZ_KnownLive(content)); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::IndentListChild() failed"); return rv; } // forget curQuote, if any curQuote = nullptr; continue; } // Not a list item, use blockquote? // if we are inside a list item, we don't want to blockquote, we want // to sublist the list item. We may have several nodes listed in the // array of nodes to act on, that are in the same list item. Since // we only want to indent that li once, we must keep track of the most // recent indented list item, and not indent it if we find another node // to act on that is still inside the same li. if (RefPtr listItem = HTMLEditUtils::GetClosestAncestorListItemElement(content, editingHost)) { if (indentedLI == listItem) { // already indented this list item continue; } // check to see if curList is still appropriate. Which it is if // content is still right after it in the same list. nsIContent* previousEditableSibling = curList ? HTMLEditUtils::GetPreviousSibling( *listItem, {WalkTreeOption::IgnoreNonEditableNode}) : nullptr; if (!curList || (previousEditableSibling && previousEditableSibling != curList)) { EditorDOMPoint atListItem(listItem); if (NS_WARN_IF(!listItem)) { return NS_ERROR_FAILURE; } nsAtom* containerName = atListItem.GetContainer()->NodeInfo()->NameAtom(); // Create a new nested list of correct type. CreateElementResult createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( MOZ_KnownLive(*containerName), atListItem, BRElementNextToSplitPoint::Keep, *editingHost); if (createNewListElementResult.isErr()) { NS_WARNING(nsPrintfCString("HTMLEditor::" "InsertElementWithSplittingAncestorsWithTr" "ansaction(%s) failed", nsAtomCString(containerName).get()) .get()); return createNewListElementResult.unwrapErr(); } nsresult rv = createNewListElementResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return rv; } MOZ_ASSERT(createNewListElementResult.GetNewNode()); curList = createNewListElementResult.UnwrapNewNode(); } rv = MoveNodeToEndWithTransaction(*listItem, *curList); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } // remember we indented this li indentedLI = listItem; continue; } // need to make a blockquote to put things in if we haven't already, // or if this node doesn't go in blockquote we used earlier. // One reason it might not go in prio blockquote is if we are now // in a different table cell. if (curQuote && HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*curQuote) != HTMLEditUtils::GetInclusiveAncestorAnyTableElement(content)) { curQuote = nullptr; } if (!curQuote) { // First, check that our element can contain a blockquote. if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(), *nsGkAtoms::blockquote)) { return NS_OK; // cancelled } CreateElementResult createNewBlockQuoteElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::blockquote, atContent, BRElementNextToSplitPoint::Keep, *editingHost); if (createNewBlockQuoteElementResult.isErr()) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::blockquote) failed"); return createNewBlockQuoteElementResult.unwrapErr(); } nsresult rv = createNewBlockQuoteElementResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return rv; } RefPtr newBlockQuoteElement = createNewBlockQuoteElementResult.UnwrapNewNode(); MOZ_ASSERT(newBlockQuoteElement); // remember our new block for postprocessing TopLevelEditSubActionDataRef().mNewBlockElement = newBlockQuoteElement; curQuote = std::move(newBlockQuoteElement); // curQuote is now the correct thing to put curNode in } // tuck the node into the end of the active blockquote // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. rv = MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *curQuote); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return rv; } // forget curList, if any curList = nullptr; } return NS_OK; } EditActionResult HTMLEditor::OutdentAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eOutdent, nsIEditor::eNext, ignoredError); if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return EditActionResult(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); EditActionResult result = CanHandleHTMLEditSubAction(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionIgnored(); } result |= HandleOutdentAtSelection(); if (result.Failed() || result.Canceled()) { NS_WARNING_ASSERTION(result.Succeeded(), "HTMLEditor::HandleOutdentAtSelection() failed"); return result; } if (IsSelectionRangeContainerNotContent()) { NS_WARNING("Mutation event listener might have changed the selection"); return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() " "failed"); return result.SetResult(rv); } EditActionResult HTMLEditor::HandleOutdentAtSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (!SelectionRef().IsCollapsed()) { nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction(); if (NS_WARN_IF(NS_FAILED(rv))) { return EditActionHandled(rv); } } // HandleOutdentAtSelectionInternal() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, the editor might have been destroyed // at restoring Selection. SplitRangeOffFromNodeResult outdentResult = HandleOutdentAtSelectionInternal(); if (NS_WARN_IF(Destroyed())) { return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } if (outdentResult.isErr()) { NS_WARNING("HTMLEditor::HandleOutdentAtSelectionInternal() failed"); return EditActionHandled(outdentResult.unwrapErr()); } // Make sure selection didn't stick to last piece of content in old bq (only // a problem for collapsed selections) if (!outdentResult.GetLeftContent() && !outdentResult.GetRightContent()) { return EditActionHandled(); } if (!SelectionRef().IsCollapsed()) { return EditActionHandled(); } // Push selection past end of left element of last split indented element. if (outdentResult.GetLeftContent()) { const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionHandled(); } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return EditActionHandled(NS_ERROR_FAILURE); } if (atStartOfSelection.Container() == outdentResult.GetLeftContent() || EditorUtils::IsDescendantOf(*atStartOfSelection.Container(), *outdentResult.GetLeftContent())) { // Selection is inside the left node - push it past it. EditorRawDOMPoint afterRememberedLeftBQ( EditorRawDOMPoint::After(*outdentResult.GetLeftContent())); NS_WARNING_ASSERTION( afterRememberedLeftBQ.IsSet(), "Failed to set after remembered left blockquote element"); nsresult rv = CollapseSelectionTo(afterRememberedLeftBQ); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "EditorBase::CollapseSelectionTo() caused destroying the editor"); return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed, but ignored"); } } // And pull selection before beginning of right element of last split // indented element. if (outdentResult.GetRightContent()) { const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionHandled(); } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return EditActionHandled(NS_ERROR_FAILURE); } if (atStartOfSelection.Container() == outdentResult.GetRightContent() || EditorUtils::IsDescendantOf(*atStartOfSelection.Container(), *outdentResult.GetRightContent())) { // Selection is inside the right element - push it before it. EditorRawDOMPoint atRememberedRightBQ(outdentResult.GetRightContent()); nsresult rv = CollapseSelectionTo(atRememberedRightBQ); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "EditorBase::CollapseSelectionTo() caused destroying the editor"); return EditActionHandled(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed, but ignored"); } } return EditActionHandled(); } SplitRangeOffFromNodeResult HTMLEditor::HandleOutdentAtSelectionInternal() { MOZ_ASSERT(IsEditActionDataAvailable()); AutoSelectionRestorer restoreSelectionLater(*this); bool useCSS = IsCSSEnabled(); // Convert the selection ranges into "promoted" selection ranges: this // basically just expands the range to include the immediate block parent, // and then further expands to include any ancestors whose children are all // in the range AutoTArray, 64> arrayOfContents; nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges( arrayOfContents, EditSubAction::eOutdent, CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::" "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges() " "failed"); return SplitRangeOffFromNodeResult(rv); } nsCOMPtr leftContentOfLastOutdented; nsCOMPtr middleContentOfLastOutdented; nsCOMPtr rightContentOfLastOutdented; RefPtr indentedParentElement; nsCOMPtr firstContentToBeOutdented, lastContentToBeOutdented; BlockIndentedWith indentedParentIndentedWith = BlockIndentedWith::HTML; for (OwningNonNull& content : arrayOfContents) { // Here's where we actually figure out what to do EditorDOMPoint atContent(content); if (!atContent.IsSet()) { continue; } // If it's a `
    `, remove it to outdent its children. if (content->IsHTMLElement(nsGkAtoms::blockquote)) { // If we've already found an ancestor block element indented, we need to // split it and remove the block element first. if (indentedParentElement) { MOZ_ASSERT(indentedParentElement == content); SplitRangeOffFromNodeResult outdentResult = OutdentPartOfBlock( *indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith); if (outdentResult.isErr()) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } leftContentOfLastOutdented = outdentResult.GetLeftContent(); middleContentOfLastOutdented = outdentResult.GetMiddleContent(); rightContentOfLastOutdented = outdentResult.GetRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; indentedParentIndentedWith = BlockIndentedWith::HTML; } nsresult rv = RemoveBlockContainerWithTransaction( MOZ_KnownLive(*content->AsElement())); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed"); return SplitRangeOffFromNodeResult(rv); } continue; } // If we're using CSS and the node is a block element, check its start // margin whether it's indented with CSS. if (useCSS && HTMLEditUtils::IsBlockElement(content)) { nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(MOZ_KnownLive(content)); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } nsAutoString value; DebugOnly rvIgnored = CSSEditUtils::GetSpecifiedProperty(content, marginProperty, value); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetSpecifiedProperty() failed, but ignored"); float startMargin = 0; RefPtr unit; CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit)); // If indented with CSS, we should decrease the start mergin. if (startMargin > 0) { nsresult rv = ChangeMarginStart(MOZ_KnownLive(*content->AsElement()), ChangeMargin::Decrease); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "HTMLEditor::ChangeMarginStart(ChangeMargin::" "Decrease) failed, but ignored"); continue; } } // If it's a list item, we should treat as that it "indents" its children. if (HTMLEditUtils::IsListItem(content)) { // If it is a list item, that means we are not outdenting whole list. // XXX I don't understand this sentence... We may meet parent list // element, no? if (indentedParentElement) { SplitRangeOffFromNodeResult outdentResult = OutdentPartOfBlock( *indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith); if (outdentResult.isErr()) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } leftContentOfLastOutdented = outdentResult.GetLeftContent(); middleContentOfLastOutdented = outdentResult.GetMiddleContent(); rightContentOfLastOutdented = outdentResult.GetRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; indentedParentIndentedWith = BlockIndentedWith::HTML; } // XXX `content` could become different element since // `OutdentPartOfBlock()` may run mutation event listeners. rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()), LiftUpFromAllParentListElements::No); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:" ":No) failed"); return SplitRangeOffFromNodeResult(rv); } continue; } // If we've found an ancestor block element which indents its children // and the current node is NOT a descendant of it, we should remove it to // outdent its children. Otherwise, i.e., current node is a descendant of // it, we meet new node which should be outdented when the indented parent // is removed. if (indentedParentElement) { if (EditorUtils::IsDescendantOf(*content, *indentedParentElement)) { // Extend the range to be outdented at removing the // indentedParentElement. lastContentToBeOutdented = content; continue; } SplitRangeOffFromNodeResult outdentResult = OutdentPartOfBlock( *indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith); if (outdentResult.isErr()) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } leftContentOfLastOutdented = outdentResult.GetLeftContent(); middleContentOfLastOutdented = outdentResult.GetMiddleContent(); rightContentOfLastOutdented = outdentResult.GetRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; // curBlockIndentedWith = HTMLEditor::BlockIndentedWith::HTML; // Then, we need to look for next indentedParentElement. } indentedParentIndentedWith = BlockIndentedWith::HTML; RefPtr editingHost = GetActiveEditingHost(); for (nsCOMPtr parentContent = content->GetParent(); parentContent && !parentContent->IsHTMLElement(nsGkAtoms::body) && parentContent != editingHost && (parentContent->IsHTMLElement(nsGkAtoms::table) || !HTMLEditUtils::IsAnyTableElement(parentContent)); parentContent = parentContent->GetParent()) { // If we reach a `
    ` ancestor, it should be split at next // time at least for outdenting current node. if (parentContent->IsHTMLElement(nsGkAtoms::blockquote)) { indentedParentElement = parentContent->AsElement(); firstContentToBeOutdented = content; lastContentToBeOutdented = content; break; } if (!useCSS) { continue; } nsCOMPtr grandParentNode = parentContent->GetParentNode(); nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(MOZ_KnownLive(content)); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } if (NS_WARN_IF(grandParentNode != parentContent->GetParentNode())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsAutoString value; DebugOnly rvIgnored = CSSEditUtils::GetSpecifiedProperty( *parentContent, marginProperty, value); if (NS_WARN_IF(Destroyed())) { return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "CSSEditUtils::GetSpecifiedProperty() failed, but ignored"); // XXX Now, editing host may become different element. If so, shouldn't // we stop this handling? float startMargin; RefPtr unit; CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit)); // If we reach a block element which indents its children with start // margin, we should remove it at next time. if (startMargin > 0 && !(HTMLEditUtils::IsAnyListElement(atContent.GetContainer()) && HTMLEditUtils::IsAnyListElement(content))) { indentedParentElement = parentContent->AsElement(); firstContentToBeOutdented = content; lastContentToBeOutdented = content; indentedParentIndentedWith = BlockIndentedWith::CSS; break; } } if (indentedParentElement) { continue; } // If we don't have any block elements which indents current node and // both current node and its parent are list element, remove current // node to move all its children to the parent list. // XXX This is buggy. When both lists' item types are different, // we create invalid tree. E.g., `