/* -*- 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 "HTMLEditorInlines.h" #include "HTMLEditorNestedClasses.h" #include #include #include "AutoRangeArray.h" #include "CSSEditUtils.h" #include "EditAction.h" #include "EditorDOMPoint.h" #include "EditorUtils.h" #include "HTMLEditHelpers.h" #include "HTMLEditUtils.h" #include "PendingStyles.h" // for SpecifiedStyle #include "WSRunObject.h" #include "ErrorList.h" #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/AutoRestore.h" #include "mozilla/CheckedInt.h" #include "mozilla/ContentIterator.h" #include "mozilla/EditorForwards.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/PresShell.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" class nsISupports; namespace mozilla { using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions; using LeafNodeType = HTMLEditUtils::LeafNodeType; using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; using WalkTextOption = HTMLEditUtils::WalkTextOption; using WalkTreeDirection = HTMLEditUtils::WalkTreeDirection; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; /******************************************************** * first some helpful functors we will use ********************************************************/ static bool IsPendingStyleCachePreservingSubAction( 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::eFormatBlockForHTMLCommand: case EditSubAction::eMergeBlockContents: case EditSubAction::eRemoveList: case EditSubAction::eCreateOrChangeDefinitionListItem: case EditSubAction::eInsertElement: case EditSubAction::eInsertQuotation: case EditSubAction::eInsertQuotedText: return true; default: return false; } } 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); nsresult HTMLEditor::InitEditorContentAndSelection() { MOZ_ASSERT(IsEditActionDataAvailable()); // We should do nothing with the result of GetRoot() if only a part of the // document is editable. if (!EntireDocumentIsEditable()) { return NS_OK; } nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() failed"); return rv; } // If the selection hasn't been set up yet, set it up collapsed to the end of // our editable content. // XXX I think that this shouldn't do it in `HTMLEditor` because it maybe // removed by the web app and if they call `Selection::AddRange()` without // checking the range count, it may cause multiple selection ranges. if (!SelectionRef().RangeCount()) { nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() " "failed"); return rv; } } if (IsPlaintextMailComposer()) { // XXX Should we do this in HTMLEditor? It's odd to guarantee that last // empty line is visible only when it's in the plain text mode. nsresult rv = EnsurePaddingBRElementInMultilineEditor(); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::EnsurePaddingBRElementInMultilineEditor() 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"); // Let's work with the latest layout information after (maybe) dispatching // `beforeinput` event. RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } document->FlushPendingNotifications(FlushType::Frames); if (NS_WARN_IF(Destroyed())) { aRv.Throw(NS_ERROR_EDITOR_DESTROYED); return; } // 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 const bool cacheInlineStyles = [&]() { switch (aTopLevelEditSubAction) { case EditSubAction::eInsertText: case EditSubAction::eInsertTextComingFromIME: case EditSubAction::eDeleteSelectedContent: return true; default: return IsPendingStyleCachePreservingSubAction(aTopLevelEditSubAction); } }(); if (cacheInlineStyles) { const RefPtr editingHost = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHost)) { aRv.Throw(NS_ERROR_FAILURE); return; } nsIContent* const startContainer = HTMLEditUtils::GetContentToPreserveInlineStyles( TopLevelEditSubActionDataRef() .mSelectedRange->StartPoint(), *editingHost); if (NS_WARN_IF(!startContainer)) { aRv.Throw(NS_ERROR_FAILURE); return; } if (const RefPtr startContainerElement = startContainer->GetAsElementOrParentElement()) { nsresult rv = CacheInlineStyles(*startContainerElement); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::CacheInlineStyles() failed"); aRv.Throw(rv); return; } } } // Stabilize the document against contenteditable count changes 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); { EditorDOMRange changedRange( *TopLevelEditSubActionDataRef().mChangedRange); if (changedRange.IsPositioned() && changedRange.EnsureNotInNativeAnonymousSubtree()) { bool isBlockLevelSubAction = false; 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; } case EditSubAction::eCreateOrChangeList: case EditSubAction::eCreateOrChangeDefinitionListItem: case EditSubAction::eRemoveList: case EditSubAction::eFormatBlockForHTMLCommand: case EditSubAction::eCreateOrRemoveBlock: case EditSubAction::eIndent: case EditSubAction::eOutdent: case EditSubAction::eSetOrClearAlignment: case EditSubAction::eSetPositionToAbsolute: case EditSubAction::eSetPositionToStatic: case EditSubAction::eDecreaseZIndex: case EditSubAction::eIncreaseZIndex: isBlockLevelSubAction = true; [[fallthrough]]; default: { Element* editingHost = ComputeEditingHost(); if (MOZ_UNLIKELY(!editingHost)) { break; } RefPtr extendedChangedRange = AutoRangeArray:: CreateRangeWrappingStartAndEndLinesContainingBoundaries( changedRange, GetTopLevelEditSubAction(), isBlockLevelSubAction ? BlockInlineCheck::UseHTMLDefaultStyle : BlockInlineCheck::UseComputedDisplayOutsideStyle, *editingHost); if (!extendedChangedRange) { break; } 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; } Result caretPointOrError = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( newCaretPosition); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "HTMLEditor::" "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() " "failed"); return caretPointOrError.unwrapErr(); } nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); return rv; } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CaretPoint::SuggestCaretPointTo() failed, but ignored"); } // 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( EditorDOMRange(*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: { // Due to the replacement of white-spaces in // WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(), // selection ranges may be changed since DOM ranges track the DOM // mutation by themselves. However, we want to keep selection as-is. // Therefore, we should restore `Selection` after replacing // white-spaces. AutoSelectionRestorer restoreSelection(*this); // 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.ContainerAs(), 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.ContainerAs(), 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.ContainerAs(), 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; } // 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 = IsPendingStyleCachePreservingSubAction(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::IsEmptyInlineContainer( *ancestor, {EmptyCheckOption::TreatSingleBRElementAsVisible}, BlockInlineCheck::UseComputedDisplayStyle)) { break; } mostDistantEmptyInlineAncestor = ancestor; } if (mostDistantEmptyInlineAncestor) { nsresult rv = DeleteNodeWithTransaction(*mostDistantEmptyInlineAncestor); if (NS_FAILED(rv)) { NS_WARNING( "EditorBase::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 = mPendingStylesToApplyToNewContent->UpdateSelState(*this); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "PendingStyles::UpdateSelState() failed, but ignored"); rvIgnored = ReapplyCachedStyles(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "HTMLEditor::ReapplyCachedStyles() failed, but ignored"); TopLevelEditSubActionDataRef().mCachedPendingStyles->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; } Result HTMLEditor::CanHandleHTMLEditSubAction( CheckSelectionInReplacedElement aCheckSelectionInReplacedElement /* = CheckSelectionInReplacedElement::Yes */) const { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } // If there is not selection ranges, we should ignore the result. if (!SelectionRef().RangeCount()) { return EditActionResult::CanceledResult(); } const nsRange* range = SelectionRef().GetRangeAt(0); nsINode* selStartNode = range->GetStartContainer(); if (NS_WARN_IF(!selStartNode) || NS_WARN_IF(!selStartNode->IsContent())) { return Err(NS_ERROR_FAILURE); } if (!HTMLEditUtils::IsSimplyEditableNode(*selStartNode)) { return EditActionResult::CanceledResult(); } nsINode* selEndNode = range->GetEndContainer(); if (NS_WARN_IF(!selEndNode) || NS_WARN_IF(!selEndNode->IsContent())) { return Err(NS_ERROR_FAILURE); } if (selStartNode == selEndNode) { if (aCheckSelectionInReplacedElement == CheckSelectionInReplacedElement::Yes && HTMLEditUtils::IsNonEditableReplacedContent( *selStartNode->AsContent())) { return EditActionResult::CanceledResult(); } return EditActionResult::IgnoredResult(); } if (HTMLEditUtils::IsNonEditableReplacedContent(*selStartNode->AsContent()) || HTMLEditUtils::IsNonEditableReplacedContent(*selEndNode->AsContent())) { return EditActionResult::CanceledResult(); } if (!HTMLEditUtils::IsSimplyEditableNode(*selEndNode)) { return EditActionResult::CanceledResult(); } // If anchor node is in an HTML element which has inert attribute, we should // do nothing. // XXX HTMLEditor typically uses first range instead of anchor/focus range. // Therefore, referring first range here is more reasonable than // anchor/focus range of Selection. nsIContent* const selAnchorContent = SelectionRef().GetDirection() == eDirNext ? nsIContent::FromNode(selStartNode) : nsIContent::FromNode(selEndNode); if (selAnchorContent && HTMLEditUtils::ContentIsInert(*selAnchorContent->AsContent())) { return EditActionResult::CanceledResult(); } // 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 (MOZ_UNLIKELY(!commonAncestor)) { NS_WARNING( "AbstractRange::GetClosestCommonInclusiveAncestor() returned nullptr"); return Err(NS_ERROR_FAILURE); } return HTMLEditUtils::IsSimplyEditableNode(*commonAncestor) ? EditActionResult::IgnoredResult() : EditActionResult::CanceledResult(); } 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 = ComputeEditingHost(); if (!editingHost) { NS_WARNING( "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() did nothing " "because of no editing host"); return NS_OK; } nsIContent* previousBRElement = HTMLEditUtils::GetPreviousContent( atSelectionStart, {}, BlockInlineCheck::UseComputedDisplayStyle, 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.ContainerAs(), HTMLEditUtils::ClosestBlockElement, BlockInlineCheck::UseComputedDisplayStyle); const RefPtr parentBlockElementOfBRElement = HTMLEditUtils::GetAncestorElement( *previousBRElement, HTMLEditUtils::ClosestBlockElement, BlockInlineCheck::UseComputedDisplayStyle); 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; } // XXX I think that we should not insert a
element if we're for a web // content. Probably, this is required only by chrome editors such as // the mail composer of Thunderbird and the composer of SeaMonkey. const RefPtr bodyOrDocumentElement = GetRoot(); if (!bodyOrDocumentElement) { return NS_OK; } // Skip adding the padding
element for empty editor if body // is read-only. if (!HTMLEditUtils::IsSimplyEditableNode(*bodyOrDocumentElement)) { 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(*bodyOrDocumentElement, editorType); for (nsIContent* child = bodyOrDocumentElement->GetFirstChild(); child; child = child->GetNextSibling()) { if (EditorUtils::IsPaddingBRElementForEmptyEditor(*child) || !isRootEditable || EditorUtils::IsEditableContent(*child, editorType) || HTMLEditUtils::IsBlockElement( *child, BlockInlineCheck::UseComputedDisplayStyle)) { 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(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); // 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. Result insertBRElementResult = InsertNodeWithTransaction( *newBRElement, EditorDOMPoint(bodyOrDocumentElement, 0u)); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); return insertBRElementResult.unwrapErr(); } // Set selection. insertBRElementResult.inspect().IgnoreCaretPointSuggestion(); nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement); 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); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::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 (!IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) { TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear(); } return NS_OK; } Result 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); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { 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 Err(rv); } } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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 Err(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 Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } RefPtr document = GetDocument(); if (NS_WARN_IF(!document)) { return Err(NS_ERROR_FAILURE); } const RefPtr editingHost = ComputeEditingHost( GetDocument()->IsXMLDocument() ? LimitInBodyElement::No : LimitInBodyElement::Yes); if (NS_WARN_IF(!editingHost)) { return Err(NS_ERROR_FAILURE); } auto pointToInsert = GetFirstSelectionStartPoint(); if (MOZ_UNLIKELY(!pointToInsert.IsSet())) { return Err(NS_ERROR_FAILURE); } // for every property that is set, insert a new inline style node Result setStyleResult = CreateStyleForInsertText(pointToInsert, *editingHost); if (MOZ_UNLIKELY(setStyleResult.isErr())) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return setStyleResult.propagateErr(); } if (setStyleResult.inspect().IsSet()) { pointToInsert = setStyleResult.unwrap(); } if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) || NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(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()) { while (!HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(), *nsGkAtoms::textTagName)) { if (NS_WARN_IF(pointToInsert.GetContainer() == editingHost) || NS_WARN_IF(!pointToInsert.GetContainerParentAs())) { NS_WARNING("Selection start point couldn't have text nodes"); return Err(NS_ERROR_FAILURE); } pointToInsert.Set(pointToInsert.ContainerAs()); } } 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 insertTextResult.propagateErr(); } nsresult rv = insertTextResult.unwrap().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CaretPoint::SuggestCaretPointTo() failed, but ignored"); return EditActionResult::HandledResult(); } auto compositionEndPoint = GetLastIMESelectionEndPoint(); if (!compositionEndPoint.IsSet()) { compositionEndPoint = compositionStartPoint; } Result replaceTextResult = WhiteSpaceVisibilityKeeper::ReplaceText( *this, aInsertionString, EditorDOMRange(compositionStartPoint, compositionEndPoint), *editingHost); if (MOZ_UNLIKELY(replaceTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed"); return replaceTextResult.propagateErr(); } // CompositionTransaction should've set selection so that we should ignore // caret suggestion. replaceTextResult.unwrap().IgnoreCaretPointSuggestion(); compositionStartPoint = GetFirstIMESelectionStartPoint(); compositionEndPoint = GetLastIMESelectionEndPoint(); if (NS_WARN_IF(!compositionStartPoint.IsSet()) || NS_WARN_IF(!compositionEndPoint.IsSet())) { // Mutation event listener has changed the DOM tree... return EditActionResult::HandledResult(); } nsresult rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( compositionStartPoint.ToRawRangeBoundary(), compositionEndPoint.ToRawRangeBoundary()); if (NS_FAILED(rv)) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(rv); } return EditActionResult::HandledResult(); } 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.ContainerAs()); // 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 || IsPlaintextMailComposer()) { 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)) { Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, currentPoint); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // We don't want to update selection here because we've blocked // InsertNodeTransaction updating selection with // dontChangeMySelection. unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(!AllowsTransactionsToChangeSelection()); pos++; RefPtr brElement = unwrappedInsertBRElementResult.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 insertTextResult.propagateErr(); } // Ignore the caret suggestion because of `dontChangeMySelection` // above. insertTextResult.inspect().IgnoreCaretPointSuggestion(); if (insertTextResult.inspect().Handled()) { pointToInsert = currentPoint = insertTextResult.unwrap() .EndOfInsertedTextRef() .To(); } else { pointToInsert = currentPoint; } } } } else { constexpr auto tabStr = u"\t"_ns; constexpr auto spacesStr = u" "_ns; nsAutoString insertionString(aInsertionString); // For FindCharInSet(). while (pos != -1 && pos < AssertedCast(insertionString.Length())) { int32_t oldPos = pos; int32_t subStrLen; pos = insertionString.FindCharInSet(u"\t\n", 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, *editingHost); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed"); return insertTextResult.propagateErr(); } // Ignore the caret suggestion because of `dontChangeMySelection` // above. insertTextResult.inspect().IgnoreCaretPointSuggestion(); pos++; if (insertTextResult.inspect().Handled()) { pointToInsert = currentPoint = insertTextResult.unwrap() .EndOfInsertedTextRef() .To(); MOZ_ASSERT(pointToInsert.IsSet()); } else { pointToInsert = currentPoint; MOZ_ASSERT(pointToInsert.IsSet()); } } // is it a return? else if (subStr.Equals(newlineStr)) { Result insertBRElementResult = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, currentPoint, *editingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // TODO: Some methods called for handling non-preformatted text use // ComputeEditingHost(). Therefore, they depend on the latest // selection. So we cannot skip updating selection here. nsresult rv = unwrappedInsertBRElementResult.SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CreateElementResult::SuggestCaretPointTo() failed, but ignored"); pos++; RefPtr newBRElement = unwrappedInsertBRElementResult.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, *editingHost); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed"); return insertTextResult.propagateErr(); } // Ignore the caret suggestion because of `dontChangeMySelection` // above. insertTextResult.inspect().IgnoreCaretPointSuggestion(); if (insertTextResult.inspect().Handled()) { pointToInsert = currentPoint = insertTextResult.unwrap() .EndOfInsertedTextRef() .To(); MOZ_ASSERT(pointToInsert.IsSet()); } else { pointToInsert = currentPoint; MOZ_ASSERT(pointToInsert.IsSet()); } } } } // After this block, pointToInsert is updated by AutoTrackDOMPoint. } if (currentPoint.IsSet()) { currentPoint.SetInterlinePosition(InterlinePosition::EndOfLine); nsresult rv = CollapseSelectionTo(currentPoint); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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()); if (NS_FAILED(rv)) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(rv); } return EditActionResult::HandledResult(); } DebugOnly rvIgnored = SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "Selection::SetInterlinePosition(InterlinePosition::" "EndOfLine) failed, but ignored"); rv = TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(pointToInsert); if (NS_FAILED(rv)) { NS_WARNING("nsRange::CollapseTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } nsresult HTMLEditor::InsertLineBreakAsSubAction() { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); if (NS_WARN_IF(!mInitSucceeded)) { return NS_ERROR_NOT_INITIALIZED; } { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } // 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 = ComputeEditingHost(); 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)) { Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, atStartOfSelection, nsIEditor::eNext); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.unwrapErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode()); unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); auto pointToPutCaret = EditorDOMPoint::After(*unwrappedInsertBRElementResult.GetNewNode()); if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) { NS_WARNING("Inserted
was unexpectedly removed"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } const WSScanResult backwardScanFromBeforeBRElementResult = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( editingHost, EditorDOMPoint(unwrappedInsertBRElementResult.GetNewNode()), BlockInlineCheck::UseComputedDisplayStyle); if (MOZ_UNLIKELY(backwardScanFromBeforeBRElementResult.Failed())) { NS_WARNING( "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() failed"); return Err(NS_ERROR_FAILURE); } const WSScanResult forwardScanFromAfterBRElementResult = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( editingHost, pointToPutCaret, BlockInlineCheck::UseComputedDisplayStyle); if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return Err(NS_ERROR_FAILURE); } const bool brElementIsAfterBlock = backwardScanFromBeforeBRElementResult.ReachedBlockBoundary() || // FIXME: This is wrong considering because the inline editing host may // be surrounded by visible inline content. However, WSRunScanner is // not aware of block boundary around it and stopping this change causes // starting to fail some WPT. Therefore, we need to keep doing this for // now. backwardScanFromBeforeBRElementResult .ReachedInlineEditingHostBoundary(); const bool brElementIsBeforeBlock = forwardScanFromAfterBRElementResult.ReachedBlockBoundary() || // FIXME: See above comment. forwardScanFromAfterBRElementResult.ReachedInlineEditingHostBoundary(); const bool isEmptyEditingHost = HTMLEditUtils::IsEmptyNode( *editingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible}); if (brElementIsBeforeBlock && (isEmptyEditingHost || !brElementIsAfterBlock)) { // Empty last line is invisible if it's immediately before either parent // or another block's boundary so that we need to put invisible
// element here for making it visible. Result invisibleAdditionalBRElementResult = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, pointToPutCaret, *editingHost); if (MOZ_UNLIKELY(invisibleAdditionalBRElementResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return invisibleAdditionalBRElementResult.unwrapErr(); } CreateElementResult unwrappedInvisibleAdditionalBRElement = invisibleAdditionalBRElementResult.unwrap(); pointToPutCaret.Set(unwrappedInvisibleAdditionalBRElement.GetNewNode()); unwrappedInvisibleAdditionalBRElement.IgnoreCaretPointSuggestion(); } else if (forwardScanFromAfterBRElementResult .InVisibleOrCollapsibleCharacters()) { pointToPutCaret = forwardScanFromAfterBRElementResult .PointAtReachedContent(); } else if (forwardScanFromAfterBRElementResult.ReachedSpecialContent()) { // Next inserting text should be inserted into styled inline elements if // they have first visible thing in the new line. pointToPutCaret = forwardScanFromAfterBRElementResult .PointAtReachedContent(); } nsresult rv = CollapseSelectionTo(pointToPutCaret); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "CreateElementResult::SuggestCaretPointTo() failed"); 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; } Result insertLineFeedResult = HandleInsertLinefeed(atStartOfSelection, *editingHost); if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertLinefeed() failed"); return insertLineFeedResult.unwrapErr(); } rv = CollapseSelectionTo(insertLineFeedResult.inspect()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed"); return rv; } Result HTMLEditor::InsertParagraphSeparatorAsSubAction(const Element& aEditingHost) { if (NS_WARN_IF(!mInitSucceeded)) { return Err(NS_ERROR_NOT_INITIALIZED); } { Result result = CanHandleHTMLEditSubAction( CheckSelectionInReplacedElement::OnlyWhenNotInSameNode); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { 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 Err(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 Err(rv); } } nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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 Err(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 Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } AutoRangeArray selectionRanges(SelectionRef()); { // If the editing host is the body element, the selection may be outside // aEditingHost. In the case, we should use the editing host outside the // only here for keeping our traditional behavior for now. // This should be fixed in bug 1634351. const Element* editingHostMaybeOutsideBody = &aEditingHost; if (aEditingHost.IsHTMLElement(nsGkAtoms::body)) { editingHostMaybeOutsideBody = ComputeEditingHost(LimitInBodyElement::No); if (NS_WARN_IF(!editingHostMaybeOutsideBody)) { return Err(NS_ERROR_FAILURE); } } selectionRanges.EnsureOnlyEditableRanges(*editingHostMaybeOutsideBody); if (NS_WARN_IF(selectionRanges.Ranges().IsEmpty())) { return Err(NS_ERROR_FAILURE); } } auto pointToInsert = selectionRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } while (true) { Element* element = pointToInsert.GetContainerOrContainerParentElement(); if (MOZ_UNLIKELY(!element)) { return Err(NS_ERROR_FAILURE); } // If the element can have a
element (it means that the element or its // container must be able to have
or

too), we can handle // insertParagraph at the point. if (HTMLEditUtils::CanNodeContain(*element, *nsGkAtoms::br)) { break; } // Otherwise, try to insert paragraph at the parent. pointToInsert = pointToInsert.ParentPoint(); } if (IsMailEditor()) { if (RefPtr mailCiteElement = GetMostDistantAncestorMailCiteElement( *pointToInsert.ContainerAs())) { // Split any mailcites in the way. Should we abort this if we encounter // table cell boundaries? Result atNewBRElementOrError = HandleInsertParagraphInMailCiteElement(*mailCiteElement, pointToInsert, aEditingHost); if (MOZ_UNLIKELY(atNewBRElementOrError.isErr())) { NS_WARNING( "HTMLEditor::HandleInsertParagraphInMailCiteElement() failed"); return atNewBRElementOrError.propagateErr(); } 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); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } } // 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 (aEditingHost.GetParentElement() && HTMLEditUtils::IsSimplyEditableNode(*aEditingHost.GetParentElement()) && !nsContentUtils::ContentIsFlattenedTreeDescendantOf( pointToInsert.ContainerAs(), &aEditingHost)) { return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE); } auto InsertLineBreakInstead = [](const Element* aEditableBlockElement, const EditorDOMPoint& aCandidatePointToSplit, ParagraphSeparator aDefaultParagraphSeparator, const Element& aEditingHost) { // 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 (!aEditableBlockElement) { // XXX Chromium checks if the CSS box of the editing host is a block. return 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. if (!HTMLEditUtils::IsSplittableNode(*aEditableBlockElement)) { return aDefaultParagraphSeparator == ParagraphSeparator::br || !HTMLEditUtils::CanElementContainParagraph( *aEditableBlockElement) || (HTMLEditUtils::ShouldInsertLinefeedCharacter( aCandidatePointToSplit, aEditingHost) && HTMLEditUtils::IsDisplayOutsideInline(aEditingHost)); } // 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. if (HTMLEditUtils::IsSingleLineContainer(*aEditableBlockElement)) { return false; } // Otherwise, unless there is no block ancestor which can contain

// element, we shouldn't insert a line break here. for (const Element* editableBlockAncestor = aEditableBlockElement; editableBlockAncestor; editableBlockAncestor = HTMLEditUtils::GetAncestorElement( *editableBlockAncestor, HTMLEditUtils::ClosestEditableBlockElementOrButtonElement, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (HTMLEditUtils::CanElementContainParagraph( *editableBlockAncestor)) { return false; } } return true; }; // 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 = HTMLEditUtils::GetInclusiveAncestorElement( *pointToInsert.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElementOrButtonElement, BlockInlineCheck::UseComputedDisplayOutsideStyle); // If we cannot insert a

/

element at the selection, we should insert // a
element or a linefeed instead. const ParagraphSeparator separator = GetDefaultParagraphSeparator(); if (InsertLineBreakInstead(editableBlockElement, pointToInsert, separator, aEditingHost)) { // 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(pointToInsert, aEditingHost)) { Result insertLineFeedResult = HandleInsertLinefeed(pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertLinefeed() failed"); return insertLineFeedResult.propagateErr(); } nsresult rv = CollapseSelectionTo(insertLineFeedResult.inspect()); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } Result insertBRElementResult = HandleInsertBRElement(pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertBRElement() failed"); return insertBRElementResult.propagateErr(); } nsresult rv = insertBRElementResult.inspect().SuggestCaretPointTo(*this, {}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } // If somebody wants to restrict caret position in a block element below, // we should guarantee it. Otherwise, we can put caret to the candidate // point. auto CollapseSelection = [this](const EditorDOMPoint& aCandidatePointToPutCaret, const Element* aBlockElementShouldHaveCaret, const SuggestCaretOptions& aOptions) MOZ_CAN_RUN_SCRIPT -> nsresult { if (!aCandidatePointToPutCaret.IsSet()) { if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion)) { return NS_OK; } return aOptions.contains(SuggestCaret::AndIgnoreTrivialError) ? NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR : NS_ERROR_FAILURE; } EditorDOMPoint pointToPutCaret(aCandidatePointToPutCaret); if (aBlockElementShouldHaveCaret) { Result pointToPutCaretOrError = HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< EditorDOMPoint>(*aBlockElementShouldHaveCaret, aCandidatePointToPutCaret); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING( "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() " "failed, but ignored"); } else if (pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } } nsresult rv = CollapseSelectionTo(pointToPutCaret); if (NS_FAILED(rv) && MOZ_LIKELY(rv != NS_ERROR_EDITOR_DESTROYED) && aOptions.contains(SuggestCaret::AndIgnoreTrivialError)) { rv = NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR; } return rv; }; RefPtr blockElementToPutCaret; // If the default paragraph separator is not
and selection is not in // a splittable block element, we should wrap selected contents in a new // paragraph, then, split it. if (!HTMLEditUtils::IsSplittableNode(*editableBlockElement) && separator != ParagraphSeparator::br) { MOZ_ASSERT(separator == ParagraphSeparator::div || separator == ParagraphSeparator::p); // FIXME: If there is no splittable block element, the other browsers wrap // the right nodes into new paragraph, but keep the left node as-is. // We should follow them to make here simpler and better compatibility. Result, nsresult> suggestBlockElementToPutCaretOrError = FormatBlockContainerWithTransaction( selectionRanges, MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator)), // For keeping the traditional behavior at insertParagraph command, // let's use the XUL paragraph state command targets even if we're // handling HTML insertParagraph command. FormatBlockMode::XULParagraphStateCommand, aEditingHost); if (MOZ_UNLIKELY(suggestBlockElementToPutCaretOrError.isErr())) { NS_WARNING("HTMLEditor::FormatBlockContainerWithTransaction() failed"); return suggestBlockElementToPutCaretOrError.propagateErr(); } if (selectionRanges.HasSavedRanges()) { selectionRanges.RestoreFromSavedRanges(); } pointToInsert = selectionRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } MOZ_ASSERT(pointToInsert.IsSetAndValid()); blockElementToPutCaret = suggestBlockElementToPutCaretOrError.unwrap(); editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *pointToInsert.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElementOrButtonElement, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (NS_WARN_IF(!editableBlockElement)) { return Err(NS_ERROR_UNEXPECTED); } if (NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(*editableBlockElement))) { // Didn't create a new block for some reason, fall back to
Result insertBRElementResult = HandleInsertBRElement(pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertBRElement() failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); EditorDOMPoint pointToPutCaret = unwrappedInsertBRElementResult.UnwrapCaretPoint(); if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) { NS_WARNING( "HTMLEditor::HandleInsertBRElement() didn't suggest a point to put " "caret"); return Err(NS_ERROR_FAILURE); } nsresult rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret, {}); if (NS_FAILED(rv)) { NS_WARNING("CollapseSelection() failed"); return Err(rv); } return EditActionResult::HandledResult(); } // We want to collapse selection in the editable block element. blockElementToPutCaret = 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.) RefPtr insertedPaddingBRElement; if (HTMLEditUtils::IsEmptyBlockElement( *editableBlockElement, {EmptyCheckOption::TreatSingleBRElementAsVisible}, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, EditorDOMPoint::AtEndOf(*editableBlockElement)); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode()); insertedPaddingBRElement = unwrappedInsertBRElementResult.UnwrapNewNode(); pointToInsert = selectionRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } } RefPtr maybeNonEditableListItem = HTMLEditUtils::GetClosestAncestorListItemElement(*editableBlockElement, &aEditingHost); if (maybeNonEditableListItem && HTMLEditUtils::IsSplittableNode(*maybeNonEditableListItem)) { Result insertParagraphInListItemResult = HandleInsertParagraphInListItemElement(*maybeNonEditableListItem, pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertParagraphInListItemResult.isErr())) { if (NS_WARN_IF(insertParagraphInListItemResult.unwrapErr() == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING( "HTMLEditor::HandleInsertParagraphInListItemElement() failed, but " "ignored"); return EditActionResult::HandledResult(); } InsertParagraphResult unwrappedInsertParagraphInListItemResult = insertParagraphInListItemResult.unwrap(); MOZ_ASSERT(unwrappedInsertParagraphInListItemResult.Handled()); MOZ_ASSERT(unwrappedInsertParagraphInListItemResult.GetNewNode()); const RefPtr listItemOrParagraphElement = unwrappedInsertParagraphInListItemResult.UnwrapNewNode(); const EditorDOMPoint pointToPutCaret = unwrappedInsertParagraphInListItemResult.UnwrapCaretPoint(); nsresult rv = CollapseSelection(pointToPutCaret, listItemOrParagraphElement, {SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CollapseSelection() failed"); return Err(rv); } NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CollapseSelection() failed, but ignored"); return EditActionResult::HandledResult(); } if (HTMLEditUtils::IsHeader(*editableBlockElement)) { Result insertParagraphInHeadingElementResult = HandleInsertParagraphInHeadingElement(*editableBlockElement, pointToInsert); if (MOZ_UNLIKELY(insertParagraphInHeadingElementResult.isErr())) { NS_WARNING( "HTMLEditor::HandleInsertParagraphInHeadingElement() failed, but " "ignored"); return EditActionResult::HandledResult(); } InsertParagraphResult unwrappedInsertParagraphInHeadingElementResult = insertParagraphInHeadingElementResult.unwrap(); if (unwrappedInsertParagraphInHeadingElementResult.Handled()) { MOZ_ASSERT(unwrappedInsertParagraphInHeadingElementResult.GetNewNode()); blockElementToPutCaret = unwrappedInsertParagraphInHeadingElementResult.UnwrapNewNode(); } const EditorDOMPoint pointToPutCaret = unwrappedInsertParagraphInHeadingElementResult.UnwrapCaretPoint(); nsresult rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CollapseSelection() failed"); return Err(rv); } NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CollapseSelection() failed, but ignored"); return EditActionResult::HandledResult(); } // 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))) { // Paragraphs: special rules to look for
s Result splitNodeResult = HandleInsertParagraphInParagraph( *editableBlockElement, insertedPaddingBRElement ? EditorDOMPoint(insertedPaddingBRElement) : pointToInsert, aEditingHost); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertParagraphInParagraph() failed"); return splitNodeResult.propagateErr(); } if (splitNodeResult.inspect().Handled()) { SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap(); const RefPtr rightParagraphElement = unwrappedSplitNodeResult.DidSplit() ? unwrappedSplitNodeResult.GetNextContentAs() : blockElementToPutCaret.get(); const EditorDOMPoint pointToPutCaret = unwrappedSplitNodeResult.UnwrapCaretPoint(); nsresult rv = CollapseSelection(pointToPutCaret, rightParagraphElement, {SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CollapseSelection() failed"); return Err(rv); } NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CollapseSelection() failed, but ignored"); return EditActionResult::HandledResult(); } MOZ_ASSERT(!splitNodeResult.inspect().HasCaretPointSuggestion()); // Fall through, if HandleInsertParagraphInParagraph() didn't handle it. MOZ_ASSERT(pointToInsert.IsSetAndValid(), "HTMLEditor::HandleInsertParagraphInParagraph() shouldn't touch " "the DOM tree if it returns not-handled state"); } // If nobody handles this edit action, let's insert new
at the selection. Result insertBRElementResult = HandleInsertBRElement(pointToInsert, aEditingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::HandleInsertBRElement() failed"); return insertBRElementResult.propagateErr(); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); EditorDOMPoint pointToPutCaret = unwrappedInsertBRElementResult.UnwrapCaretPoint(); rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret, {}); if (NS_FAILED(rv)) { NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } Result HTMLEditor::HandleInsertBRElement( const EditorDOMPoint& aPointToBreak, const Element& aEditingHost) { MOZ_ASSERT(aPointToBreak.IsSet()); MOZ_ASSERT(IsEditActionDataAvailable()); const bool editingHostIsEmpty = HTMLEditUtils::IsEmptyNode( aEditingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible}); WSRunScanner wsRunScanner(&aEditingHost, aPointToBreak, BlockInlineCheck::UseComputedDisplayStyle); const WSScanResult backwardScanResult = wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(aPointToBreak); if (MOZ_UNLIKELY(backwardScanResult.Failed())) { NS_WARNING( "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed"); return Err(NS_ERROR_FAILURE); } const bool brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary() || // FIXME: This is wrong considering because the inline editing host may // be surrounded by visible inline content. However, WSRunScanner is // not aware of block boundary around it and stopping this change causes // starting to fail some WPT. Therefore, we need to keep doing this for // now. backwardScanResult.ReachedInlineEditingHostBoundary(); const WSScanResult forwardScanResult = wsRunScanner.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( aPointToBreak); if (MOZ_UNLIKELY(forwardScanResult.Failed())) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); return Err(NS_ERROR_FAILURE); } const bool brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary() || // FIXME: See above comment forwardScanResult.ReachedInlineEditingHostBoundary(); // First, insert a
element. RefPtr brElement; if (IsPlaintextMailComposer()) { Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, aPointToBreak); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult; } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // We'll return with suggesting new caret position and nobody refers // selection after here. So we don't need to update selection here. unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode()); brElement = unwrappedInsertBRElementResult.UnwrapNewNode(); } else { EditorDOMPoint pointToBreak(aPointToBreak); // 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) { Result splitLinkNodeResult = SplitNodeDeepWithTransaction( *linkNode, pointToBreak, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(splitLinkNodeResult.isErr())) { NS_WARNING( "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::" "eDoNotCreateEmptyContainer) failed"); return splitLinkNodeResult.propagateErr(); } // TODO: Some methods called by // WhiteSpaceVisibilityKeeper::InsertBRElement() use // ComputeEditingHost() which depends on selection. Therefore, // we cannot skip updating selection here. nsresult rv = splitLinkNodeResult.inspect().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed"); return Err(rv); } pointToBreak = splitLinkNodeResult.inspect().AtSplitPoint(); } Result insertBRElementResult = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, pointToBreak, aEditingHost); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return insertBRElementResult; } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // We'll return with suggesting new caret position and nobody refers // selection after here. So we don't need to update selection here. unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); brElement = unwrappedInsertBRElementResult.UnwrapNewNode(); MOZ_ASSERT(brElement); } if (MOZ_UNLIKELY(!brElement->GetParentNode())) { NS_WARNING("Inserted
element was removed by the web app"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } auto afterBRElement = EditorDOMPoint::After(brElement); auto InsertAdditionalInvisibleLineBreak = [&]() MOZ_CAN_RUN_SCRIPT -> Result { // Empty last line is invisible if it's immediately before either parent or // another block's boundary so that we need to put invisible
element // here for making it visible. Result invisibleAdditionalBRElementResult = WhiteSpaceVisibilityKeeper::InsertBRElement(*this, afterBRElement, aEditingHost); if (MOZ_UNLIKELY(invisibleAdditionalBRElementResult.isErr())) { NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed"); return invisibleAdditionalBRElementResult; } // afterBRElement points after the first
with referring an old child. // Therefore, we need to update it with new child which is the new invisible //
. afterBRElement.Set( invisibleAdditionalBRElementResult.inspect().GetNewNode()); return invisibleAdditionalBRElementResult; }; 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. EditorDOMPoint pointToPutCaret; if (editingHostIsEmpty) { Result invisibleAdditionalBRElementResult = InsertAdditionalInvisibleLineBreak(); if (invisibleAdditionalBRElementResult.isErr()) { return invisibleAdditionalBRElementResult; } invisibleAdditionalBRElementResult.unwrap().IgnoreCaretPointSuggestion(); pointToPutCaret = std::move(afterBRElement); } else { pointToPutCaret = EditorDOMPoint(brElement, InterlinePosition::StartOfNextLine); } return CreateElementResult(std::move(brElement), std::move(pointToPutCaret)); } const WSScanResult forwardScanFromAfterBRElementResult = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &aEditingHost, afterBRElement, BlockInlineCheck::UseComputedDisplayStyle); if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) { NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); return Err(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()); Result moveBRElementResult = MoveNodeWithTransaction( MOZ_KnownLive( *forwardScanFromAfterBRElementResult.BRElementPtr()), afterBRElement); if (MOZ_UNLIKELY(moveBRElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveBRElementResult.propagateErr(); } nsresult rv = moveBRElementResult.inspect().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "MoveNodeResult::SuggestCaretPointTo() failed, but ignored"); } } else if ((forwardScanFromAfterBRElementResult.ReachedBlockBoundary() || // FIXME: This is wrong considering because the inline editing // host may be surrounded by visible inline content. However, // WSRunScanner is not aware of block boundary around it and // stopping this change causes starting to fail some WPT. // Therefore, we need to keep doing this for now. forwardScanFromAfterBRElementResult .ReachedInlineEditingHostBoundary()) && !brElementIsAfterBlock) { Result invisibleAdditionalBRElementResult = InsertAdditionalInvisibleLineBreak(); if (invisibleAdditionalBRElementResult.isErr()) { return invisibleAdditionalBRElementResult; } invisibleAdditionalBRElementResult.unwrap().IgnoreCaretPointSuggestion(); } // 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, BlockInlineCheck::UseComputedDisplayStyle) ? InterlinePosition::EndOfLine : InterlinePosition::StartOfNextLine); return CreateElementResult(std::move(brElement), afterBRElement); } Result HTMLEditor::HandleInsertLinefeed( const EditorDOMPoint& aPointToBreak, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); if (NS_WARN_IF(!aPointToBreak.IsSet())) { return Err(NS_ERROR_INVALID_ARG); } const RefPtr document = GetDocument(); MOZ_DIAGNOSTIC_ASSERT(document); if (NS_WARN_IF(!document)) { return Err(NS_ERROR_FAILURE); } // TODO: The following code is duplicated from `HandleInsertText`. They // should be merged when we fix bug 92921. Result setStyleResult = CreateStyleForInsertText(aPointToBreak, aEditingHost); if (MOZ_UNLIKELY(setStyleResult.isErr())) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return setStyleResult.propagateErr(); } EditorDOMPoint pointToInsert = setStyleResult.inspect().IsSet() ? setStyleResult.inspect() : aPointToBreak; if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) || NS_WARN_IF(!pointToInsert.IsInContentNode())) { return Err(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.ContainerAs(), *nsGkAtoms::textTagName)) { NS_WARNING( "HTMLEditor::HandleInsertLinefeed() couldn't insert a linefeed because " "the insertion position couldn't have text nodes"); return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE); } AutoRestore disableListener( EditSubActionDataRef().mAdjustChangedRangeFromListener); EditSubActionDataRef().mAdjustChangedRangeFromListener = false; // TODO: We don't need AutoTransactionsConserveSelection here in the normal // cases, but removing this may cause the behavior with the legacy // mutation event listeners. We should try to delete this in a bug. AutoTransactionsConserveSelection dontChangeMySelection(*this); EditorDOMPoint pointToPutCaret; { AutoTrackDOMPoint trackingInsertingPosition(RangeUpdaterRef(), &pointToInsert); Result insertTextResult = InsertTextWithTransaction(*document, u"\n"_ns, pointToInsert); if (MOZ_UNLIKELY(insertTextResult.isErr())) { NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed"); return insertTextResult.propagateErr(); } // Ignore the caret suggestion because of `dontChangeMySelection` above. insertTextResult.inspect().IgnoreCaretPointSuggestion(); pointToPutCaret = insertTextResult.inspect().Handled() ? insertTextResult.unwrap() .EndOfInsertedTextRef() .To() : pointToInsert; } // 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 (pointToPutCaret.IsInContentNode() && pointToPutCaret.IsEndOfContainer()) { WSRunScanner wsScannerAtCaret(&aEditingHost, pointToPutCaret, BlockInlineCheck::UseComputedDisplayStyle); if (wsScannerAtCaret.StartsFromPreformattedLineBreak() && (wsScannerAtCaret.EndsByBlockBoundary() || wsScannerAtCaret.EndsByInlineEditingHostBoundary()) && HTMLEditUtils::CanNodeContain(*wsScannerAtCaret.GetEndReasonContent(), *nsGkAtoms::br)) { AutoTrackDOMPoint trackingInsertedPosition(RangeUpdaterRef(), &pointToInsert); AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), &pointToPutCaret); Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, pointToPutCaret); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.propagateErr(); } // We're tracking next caret position with newCaretPosition. Therefore, // we don't need to update selection here. insertBRElementResult.inspect().IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode()); } } // manually update the doc changed range so that // OnEndHandlingTopLevelEditSubActionInternal will clean up the correct // portion of the document. MOZ_ASSERT(pointToPutCaret.IsSet()); if (NS_WARN_IF(!pointToPutCaret.IsSet())) { // XXX Here is odd. We did mChangedRange->SetStartAndEnd(pointToInsert, // pointToPutCaret), but it always fails because of the latter is unset. // Therefore, always returning NS_ERROR_FAILURE from here is the // traditional behavior... // TODO: Stop updating the interline position of Selection with fixing here // and returning expected point. 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 Err(NS_ERROR_FAILURE); } NS_WARNING( "We always return NS_ERROR_FAILURE here because of a failure of " "updating mChangedRange"); return Err(NS_ERROR_FAILURE); } if (NS_FAILED(TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd( pointToInsert.ToRawRangeBoundary(), pointToPutCaret.ToRawRangeBoundary()))) { NS_WARNING("nsRange::SetStartAndEnd() failed"); return Err(NS_ERROR_FAILURE); } pointToPutCaret.SetInterlinePosition(InterlinePosition::EndOfLine); return pointToPutCaret; } Result HTMLEditor::HandleInsertParagraphInMailCiteElement( Element& aMailCiteElement, const EditorDOMPoint& aPointToSplit, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToSplit.IsSet()); NS_ASSERTION(!HTMLEditUtils::IsEmptyNode( aMailCiteElement, {EmptyCheckOption::TreatNonEditableContentAsInvisible}), "The mail-cite element will be deleted, does it expected result " "for you?"); auto splitCiteElementResult = [&]() MOZ_CAN_RUN_SCRIPT -> Result { 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. const WSScanResult forwardScanFromPointToSplitResult = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &aEditingHost, pointToSplit, BlockInlineCheck::UseHTMLDefaultStyle); if (forwardScanFromPointToSplitResult.Failed()) { return Err(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 .PointAfterReachedContent(); } if (NS_WARN_IF(!pointToSplit.IsInContentNode())) { return Err(NS_ERROR_FAILURE); } Result splitResult = SplitNodeDeepWithTransaction(aMailCiteElement, pointToSplit, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "HTMLEditor::SplitNodeDeepWithTransaction(aMailCiteElement, " "SplitAtEdges::eDoNotCreateEmptyContainer) failed"); return splitResult; } nsresult rv = splitResult.inspect().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); if (NS_FAILED(rv)) { NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed"); return Err(rv); } return splitResult; }(); if (MOZ_UNLIKELY(splitCiteElementResult.isErr())) { NS_WARNING("Failed to split a mail-cite element"); return splitCiteElementResult.propagateErr(); } SplitNodeResult unwrappedSplitCiteElementResult = splitCiteElementResult.unwrap(); // When adding caret suggestion to SplitNodeResult, here didn't change // selection so that just ignore it. unwrappedSplitCiteElementResult.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: unwrappedSplitCiteElementResult 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. auto* const leftCiteElement = unwrappedSplitCiteElementResult.GetPreviousContentAs(); auto* const rightCiteElement = unwrappedSplitCiteElementResult.GetNextContentAs(); 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)) { Result insertInvisibleBRElementResult = InsertBRElement(WithTransaction::Yes, EditorDOMPoint::AtEndOf(*leftCiteElement)); if (MOZ_UNLIKELY(insertInvisibleBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertInvisibleBRElementResult.propagateErr(); } // We don't need to update selection here because we'll do another // InsertBRElement call soon. insertInvisibleBRElementResult.inspect().IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertInvisibleBRElementResult.inspect().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. Result insertBRElementResult = InsertBRElement( WithTransaction::Yes, unwrappedSplitCiteElementResult.AtSplitPoint()); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return Err(insertBRElementResult.unwrapErr()); } CreateElementResult unwrappedInsertBRElementResult = insertBRElementResult.unwrap(); // We'll return with suggesting caret position. Therefore, we don't need // to update selection here. unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedInsertBRElementResult.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::IsInlineContent( aMailCiteElement, BlockInlineCheck::UseComputedDisplayStyle)) { nsresult rvOfInsertingBRElement = [&]() MOZ_CAN_RUN_SCRIPT { EditorDOMPoint pointToCreateNewBRElement( unwrappedInsertBRElementResult.GetNewNode()); // XXX Cannot we replace this complicated check with just a call of // HTMLEditUtils::IsVisibleBRElement with // resultOfInsertingBRElement.inspect()? const WSScanResult backwardScanFromPointToCreateNewBRElementResult = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( &aEditingHost, pointToCreateNewBRElement, BlockInlineCheck::UseComputedDisplayStyle); 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; } const WSScanResult forwardScanFromPointAfterNewBRElementResult = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( &aEditingHost, EditorRawDOMPoint::After(pointToCreateNewBRElement), BlockInlineCheck::UseComputedDisplayStyle); 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; } Result insertBRElementResult = InsertBRElement(WithTransaction::Yes, pointToCreateNewBRElement); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.unwrapErr(); } insertBRElementResult.inspect().IgnoreCaretPointSuggestion(); MOZ_ASSERT(insertBRElementResult.inspect().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, {EmptyCheckOption::TreatNonEditableContentAsInvisible})) { // MOZ_KnownLive(leftCiteElement) because it's grabbed by // unwrappedSplitCiteElementResult. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*leftCiteElement)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } if (rightCiteElement && HTMLEditUtils::IsEmptyNode( *rightCiteElement, {EmptyCheckOption::TreatNonEditableContentAsInvisible})) { // MOZ_KnownLive(rightCiteElement) because it's grabbed by // unwrappedSplitCiteElementResult. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*rightCiteElement)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } if (MOZ_UNLIKELY(!unwrappedInsertBRElementResult.GetNewNode()->GetParent())) { NS_WARNING("Inserted
shouldn't become an orphan node"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return EditorDOMPoint(unwrappedInsertBRElementResult.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( ComputeEditingHost(), aPoint, BlockInlineCheck::UseComputedDisplayStyle); 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( ComputeEditingHost(), aPoint, BlockInlineCheck::UseComputedDisplayStyle); 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 = ComputeEditingHost(); const EditorDOMPointInText precedingCharPoint = WSRunScanner::GetPreviousEditableCharPoint( editingHost, aStartToDelete, BlockInlineCheck::UseComputedDisplayStyle); const EditorDOMPointInText followingCharPoint = WSRunScanner::GetInclusiveNextEditableCharPoint( editingHost, aEndToDelete, BlockInlineCheck::UseComputedDisplayStyle); // 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.ContainerAs() != aEndToDelete.ContainerAs() || (aEndToDelete.IsEndOfContainer() && followingCharPoint.IsSet()); const bool maybeNormalizePrecedingWhiteSpaces = !removingLastCharOfStartNode && precedingCharPoint.IsSet() && !precedingCharPoint.IsEndOfContainer() && precedingCharPoint.ContainerAs() == aStartToDelete.ContainerAs() && precedingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP(); const bool maybeNormalizeFollowingWhiteSpaces = followingCharPoint.IsSet() && !followingCharPoint.IsEndOfContainer() && (followingCharPoint.ContainerAs() == aEndToDelete.ContainerAs() || 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.ContainerAs(), 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.ContainerAs(), nextCharOffsetOfWhiteSpaces.value()); } else { endToNormalize.SetToEndOf(followingCharPoint.ContainerAs()); } 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.ContainerAs() == aStartToDelete.ContainerAs()); lengthInStartNode = aStartToDelete.Offset() - startToNormalize.Offset(); MOZ_ASSERT(lengthInStartNode); } if (endToNormalize.IsSet()) { lengthInEndNode = endToNormalize.ContainerAs() == aEndToDelete.ContainerAs() ? 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.ContainerAs() == aStartToDelete.ContainerAs()) { 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 CaretPoint(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.ContainerAs() == aEndToDelete.ContainerAs()) { newCaretPosition = aEndToDelete.To(); } else if (aDeleteDirection == DeleteDirection::Forward) { newCaretPosition.SetToEndOf(aStartToDelete.ContainerAs()); } else { newCaretPosition.Set(aEndToDelete.ContainerAs(), 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.ContainerAs(), endToDelete.Offset()); { AutoTrackDOMPoint trackEndToDelete(RangeUpdaterRef(), &trackingEndToDelete); uint32_t lengthToReplaceInFirstTextNode = startToDelete.ContainerAs() == trackingEndToDelete.ContainerAs() ? trackingEndToDelete.Offset() - startToDelete.Offset() : startToDelete.ContainerAs()->TextLength() - startToDelete.Offset(); Result replaceTextResult = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAs()), startToDelete.Offset(), lengthToReplaceInFirstTextNode, normalizedWhiteSpacesInFirstNode); if (MOZ_UNLIKELY(replaceTextResult.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceTextResult.propagateErr(); } // We'll return computed caret point, newCaretPosition, below. replaceTextResult.unwrap().IgnoreCaretPointSuggestion(); if (startToDelete.ContainerAs() == trackingEndToDelete.ContainerAs()) { 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.ContainerAs(), trackingEndToDelete.Offset()); // If the remaining range was modified by mutation event listener, // we should stop handling the deletion. startToDelete = EditorDOMPointInText::AtEndOf(*startToDelete.ContainerAs()); 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.ContainerAs() != endToDelete.ContainerAs()) { // 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.ContainerAs(), 0); if (startToDelete != endToDeleteExceptReplaceRange) { Result caretPointOrError = DeleteTextAndTextNodesWithTransaction(startToDelete, endToDeleteExceptReplaceRange, aTreatEmptyTextNodes); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed"); return caretPointOrError.propagateErr(); } nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo( *this, {SuggestCaret::OnlyIfHasSuggestion, SuggestCaret::OnlyIfTransactionsAllowedToDoIt, SuggestCaret::AndIgnoreTrivialError}); if (NS_FAILED(rv)) { NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); return Err(rv); } NS_WARNING_ASSERTION( rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR, "CaretPoint::SuggestCaretPointTo() failed, but ignored"); 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.ContainerAs() == endToDelete.ContainerAs()); Result replaceTextResult = ReplaceTextWithTransaction( MOZ_KnownLive(*startToDelete.ContainerAs()), startToDelete.Offset(), endToDelete.Offset() - startToDelete.Offset(), normalizedWhiteSpacesInLastNode); if (MOZ_UNLIKELY(replaceTextResult.isErr())) { NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed"); return replaceTextResult.propagateErr(); } // We'll return computed caret point, newCaretPosition, below. replaceTextResult.unwrap().IgnoreCaretPointSuggestion(); break; } if (NS_WARN_IF(!newCaretPosition.IsSetAndValid()) || NS_WARN_IF(!newCaretPosition.GetContainer()->IsInComposedDoc())) { 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.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost, BlockInlineCheck::UseComputedDisplayStyle)) { Element* editingHost = ComputeEditingHost(); // Try to put caret next to immediately after previous editable leaf. nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( newCaretPosition, *editableBlockElementOrInlineEditingHost, {LeafNodeType::LeafNodeOrNonEditableNode}, BlockInlineCheck::UseComputedDisplayStyle, editingHost); if (previousContent && !HTMLEditUtils::IsBlockElement( *previousContent, BlockInlineCheck::UseComputedDisplayStyle)) { 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}, BlockInlineCheck::UseComputedDisplayStyle, 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); Result caretPointOrError = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( newCaretPosition); if (MOZ_UNLIKELY(caretPointOrError.isErr())) { NS_WARNING( "HTMLEditor::" "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed"); return caretPointOrError; } } if (!newCaretPosition.IsSetAndValid()) { NS_WARNING("Inserting
element caused unexpected DOM tree"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } return CaretPoint(std::move(newCaretPosition)); } Result HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( const EditorDOMPoint& aPointToInsert) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToInsert.IsSet()); if (!aPointToInsert.IsInContentNode()) { return CaretPoint(EditorDOMPoint()); } // If container of the point is not in a block, we don't need to put a // `
` element here. if (!HTMLEditUtils::IsBlockElement( *aPointToInsert.ContainerAs(), BlockInlineCheck::UseComputedDisplayStyle)) { return CaretPoint(EditorDOMPoint()); } if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode( *aPointToInsert.ContainerAs()))) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } WSRunScanner wsRunScanner(ComputeEditingHost(), aPointToInsert, BlockInlineCheck::UseComputedDisplayStyle); // If the point is not start of a hard line, we don't need to put a `
` // element here. if (!wsRunScanner.StartsFromHardLineBreak() && !wsRunScanner.StartsFromInlineEditingHostBoundary()) { return CaretPoint(EditorDOMPoint()); } // 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() && !wsRunScanner.EndsByInlineEditingHostBoundary()) { return CaretPoint(EditorDOMPoint()); } // If we cannot insert a `
` element here, do nothing. if (!HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(), *nsGkAtoms::br)) { return CaretPoint(EditorDOMPoint()); } Result insertBRElementResult = InsertBRElement( WithTransaction::Yes, aPointToInsert, nsIEditor::ePrevious); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertBRElement(WithTransaction::Yes, ePrevious) failed"); return insertBRElementResult.propagateErr(); } return CaretPoint(insertBRElementResult.unwrap().UnwrapCaretPoint()); } Result HTMLEditor::MakeOrChangeListAndListItemAsSubAction( const nsStaticAtom& 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 Err(NS_ERROR_NOT_INITIALIZED); } { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionResult::IgnoredResult(); } 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 error; AutoEditSubActionNotifier startToHandleEditSubAction( *this, &aListElementOrListItemElementTagName == nsGkAtoms::dd || &aListElementOrListItemElementTagName == nsGkAtoms::dt ? EditSubAction::eCreateOrChangeDefinitionListItem : EditSubAction::eCreateOrChangeList, nsIEditor::eNext, error); if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return Err(error.StealNSResult()); } NS_WARNING_ASSERTION( !error.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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 Err(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 Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } const nsStaticAtom* listTagName = nullptr; const nsStaticAtom* 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 Err(NS_ERROR_INVALID_ARG); } const RefPtr editingHost = ComputeEditingHost(); if (MOZ_UNLIKELY(!editingHost)) { return EditActionResult::CanceledResult(); } // Expands selection range to include the immediate block parent, and then // further expands to include any ancestors whose children are all in the // range. // XXX Why do we do this only when there is only one selection range? if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( SelectionRef().GetRangeAt(0u), *editingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.propagateErr(); } // Note that end point may be prior to start point. So, we // cannot use Selection::SetStartAndEndInLimit() here. error.SuppressException(); SelectionRef().SetBaseAndExtentInLimiter( extendedRange.inspect().StartRef().ToRawRangeBoundary(), extendedRange.inspect().EndRef().ToRawRangeBoundary(), error); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (MOZ_UNLIKELY(error.Failed())) { NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed"); return Err(error.StealNSResult()); } } AutoListElementCreator listCreator(*listTagName, *listItemTagName, aBulletType); AutoRangeArray selectionRanges(SelectionRef()); Result result = listCreator.Run( *this, selectionRanges, aSelectAllOfCurrentList, *editingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::ConvertContentAroundRangesToList() failed"); // XXX Should we try to restore selection ranges in this case? return result; } rv = selectionRanges.ApplyTo(SelectionRef()); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::ApplyTo() failed"); return Err(rv); } return result.inspect().Ignored() ? EditActionResult::CanceledResult() : EditActionResult::HandledResult(); } Result HTMLEditor::AutoListElementCreator::Run( HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges, SelectAllOfCurrentList aSelectAllOfCurrentList, const Element& aEditingHost) const { MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!aHTMLEditor.IsSelectionRangeContainerNotContent()); if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(aHTMLEditor))) { return Err(NS_ERROR_FAILURE); } AutoContentNodeArray arrayOfContents; nsresult rv = SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList( aHTMLEditor, aRanges, aSelectAllOfCurrentList, aEditingHost, arrayOfContents); if (NS_FAILED(rv)) { NS_WARNING( "AutoListElementCreator::" "SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList() failed"); return Err(rv); } // check if all our nodes are
s, or empty inlines // if no nodes, we make empty list. Ditto if the user tried to make a list // of some # of breaks. if (AutoListElementCreator:: IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements( arrayOfContents)) { Result, nsresult> newListItemElementOrError = ReplaceContentNodesWithEmptyNewList(aHTMLEditor, aRanges, arrayOfContents, aEditingHost); if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) { NS_WARNING( "AutoListElementCreator::ReplaceContentNodesWithEmptyNewList() " "failed"); return newListItemElementOrError.propagateErr(); } if (MOZ_UNLIKELY(!newListItemElementOrError.inspect())) { aRanges.RestoreFromSavedRanges(); return EditActionResult::CanceledResult(); } aRanges.ClearSavedRanges(); nsresult rv = aRanges.Collapse( EditorRawDOMPoint(newListItemElementOrError.inspect(), 0u)); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::Collapse() failed"); return Err(rv); } return EditActionResult::IgnoredResult(); } Result, nsresult> listItemOrListToPutCaretOrError = WrapContentNodesIntoNewListElements(aHTMLEditor, aRanges, arrayOfContents, aEditingHost); if (MOZ_UNLIKELY(listItemOrListToPutCaretOrError.isErr())) { NS_WARNING( "AutoListElementCreator::WrapContentNodesIntoNewListElements() failed"); return listItemOrListToPutCaretOrError.propagateErr(); } MOZ_ASSERT(aRanges.HasSavedRanges()); aRanges.RestoreFromSavedRanges(); // If selection will be collapsed but not in listItemOrListToPutCaret, we need // to adjust the caret position into it. if (listItemOrListToPutCaretOrError.inspect()) { DebugOnly rvIgnored = EnsureCollapsedRangeIsInListItemOrListElement( *listItemOrListToPutCaretOrError.inspect(), aRanges); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "AutoListElementCreator::" "EnsureCollapsedRangeIsInListItemOrListElement() failed, but ignored"); } return EditActionResult::HandledResult(); } nsresult HTMLEditor::AutoListElementCreator:: SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList( HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges, SelectAllOfCurrentList aSelectAllOfCurrentList, const Element& aEditingHost, ContentNodeArray& aOutArrayOfContents) const { MOZ_ASSERT(aOutArrayOfContents.IsEmpty()); if (aSelectAllOfCurrentList == SelectAllOfCurrentList::Yes) { if (Element* parentListElementOfRanges = aRanges.GetClosestAncestorAnyListElementOfRange()) { aOutArrayOfContents.AppendElement( OwningNonNull(*parentListElementOfRanges)); return NS_OK; } } AutoRangeArray extendedRanges(aRanges); // TODO: We don't need AutoTransactionsConserveSelection here in the // normal cases, but removing this may cause the behavior with the // legacy mutation event listeners. We should try to delete this in // a bug. AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); extendedRanges.ExtendRangesToWrapLines(EditSubAction::eCreateOrChangeList, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost); Result splitResult = extendedRanges.SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( aHTMLEditor, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed"); return splitResult.unwrapErr(); } nsresult rv = extendedRanges.CollectEditTargetNodes( aHTMLEditor, aOutArrayOfContents, EditSubAction::eCreateOrChangeList, AutoRangeArray::CollectNonEditableNodes::No); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::" "eCreateOrChangeList, CollectNonEditableNodes::No) failed"); return rv; } Result splitAtBRElementsResult = aHTMLEditor.MaybeSplitElementsAtEveryBRElement( aOutArrayOfContents, EditSubAction::eCreateOrChangeList); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" "eCreateOrChangeList) failed"); return splitAtBRElementsResult.unwrapErr(); } return NS_OK; } // static bool HTMLEditor::AutoListElementCreator:: IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements( const ContentNodeArray& aArrayOfContents) { for (const OwningNonNull& content : aArrayOfContents) { // if content is not a
or empty inline, we're done // XXX Should we handle line breaks in preformatted text node? if (!content->IsHTMLElement(nsGkAtoms::br) && !HTMLEditUtils::IsEmptyInlineContainer( content, {EmptyCheckOption::TreatSingleBRElementAsVisible, EmptyCheckOption::TreatNonEditableContentAsInvisible}, BlockInlineCheck::UseComputedDisplayStyle)) { return false; } } return true; } Result, nsresult> HTMLEditor::AutoListElementCreator::ReplaceContentNodesWithEmptyNewList( HTMLEditor& aHTMLEditor, const AutoRangeArray& aRanges, const AutoContentNodeArray& aArrayOfContents, const Element& aEditingHost) const { // if only breaks, delete them for (const OwningNonNull& content : aArrayOfContents) { // MOZ_KnownLive because of bug 1620312 nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } const auto firstRangeStartPoint = aRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!firstRangeStartPoint.IsSet())) { return Err(NS_ERROR_FAILURE); } // Make sure we can put a list here. if (!HTMLEditUtils::CanNodeContain(*firstRangeStartPoint.GetContainer(), mListTagName)) { return RefPtr(); } RefPtr newListItemElement; Result createNewListElementResult = aHTMLEditor.InsertElementWithSplittingAncestorsWithTransaction( mListTagName, firstRangeStartPoint, BRElementNextToSplitPoint::Keep, aEditingHost, // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868 [&](HTMLEditor& aHTMLEditor, Element& aListElement, const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY { AutoHandlingState dummyState; Result createListItemElementResult = AppendListItemElement(aHTMLEditor, aListElement, dummyState); if (MOZ_UNLIKELY(createListItemElementResult.isErr())) { NS_WARNING( "AutoListElementCreator::AppendListItemElement() failed"); return createListItemElementResult.unwrapErr(); } CreateElementResult unwrappedResult = createListItemElementResult.unwrap(); // 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 aRanges.HasSavedRanges() here, but it // requires ifdefs to avoid bustage of opt builds caused // by unused warning... unwrappedResult.IgnoreCaretPointSuggestion(); newListItemElement = unwrappedResult.UnwrapNewNode(); MOZ_ASSERT(newListItemElement); return NS_OK; }); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING( nsPrintfCString( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "%s) failed", nsAtomCString(&mListTagName).get()) .get()); return createNewListElementResult.propagateErr(); } MOZ_ASSERT(createNewListElementResult.inspect().GetNewNode()); // Put selection in new list item and don't restore the Selection. createNewListElementResult.inspect().IgnoreCaretPointSuggestion(); return newListItemElement; } Result, nsresult> HTMLEditor::AutoListElementCreator::WrapContentNodesIntoNewListElements( HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges, AutoContentNodeArray& aArrayOfContents, const Element& aEditingHost) const { // 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 (aArrayOfContents.Length() == 1) { if (Element* deepestDivBlockquoteOrListElement = HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild( aArrayOfContents[0], {WalkTreeOption::IgnoreNonEditableNode}, BlockInlineCheck::UseHTMLDefaultStyle, nsGkAtoms::div, nsGkAtoms::blockquote, nsGkAtoms::ul, nsGkAtoms::ol, nsGkAtoms::dl)) { if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements( nsGkAtoms::div, nsGkAtoms::blockquote)) { aArrayOfContents.Clear(); HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement, aArrayOfContents, 0, {}); } else { aArrayOfContents.ReplaceElementAt( 0, OwningNonNull(*deepestDivBlockquoteOrListElement)); } } } // Ok, now go through all the nodes and put then in the list, // or whatever is appropriate. Wohoo! AutoHandlingState handlingState; for (const OwningNonNull& content : aArrayOfContents) { // MOZ_KnownLive because of bug 1620312 nsresult rv = HandleChildContent(aHTMLEditor, MOZ_KnownLive(content), handlingState, aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("AutoListElementCreator::HandleChildContent() failed"); return Err(rv); } } return std::move(handlingState.mListOrListItemElementToPutCaret); } nsresult HTMLEditor::AutoListElementCreator::HandleChildContent( HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent, AutoHandlingState& aState, const Element& aEditingHost) const { // make sure we don't assemble content that is in different table cells // into the same list. respect table cell boundaries when listifying. if (aState.mCurrentListElement && HTMLEditUtils::GetInclusiveAncestorAnyTableElement( *aState.mCurrentListElement) != HTMLEditUtils::GetInclusiveAncestorAnyTableElement( aHandlingContent)) { aState.mCurrentListElement = 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(aHandlingContent, EditorType::HTML) && (aHandlingContent.IsHTMLElement(nsGkAtoms::br) || HTMLEditUtils::IsEmptyInlineContainer( aHandlingContent, {EmptyCheckOption::TreatSingleBRElementAsVisible, EmptyCheckOption::TreatNonEditableContentAsInvisible}, BlockInlineCheck::UseHTMLDefaultStyle))) { nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aHandlingContent); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return rv; } if (aHandlingContent.IsHTMLElement(nsGkAtoms::br)) { aState.mPreviousListItemElement = nullptr; } return NS_OK; } // If we meet a list, we can reuse it or convert it to the expected type list. if (HTMLEditUtils::IsAnyListElement(&aHandlingContent)) { nsresult rv = HandleChildListElement( aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoListElementCreator::HandleChildListElement() failed"); return rv; } // We cannot handle nodes if not in element node. if (NS_WARN_IF(!aHandlingContent.GetParentElement())) { return NS_ERROR_FAILURE; } // If we meet a list item, we can just move it to current list element or new // list element. if (HTMLEditUtils::IsListItem(&aHandlingContent)) { nsresult rv = HandleChildListItemElement( aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoListElementCreator::HandleChildListItemElement() failed"); return rv; } // If we meet a
or a

, we want only its children to wrapping into // list element. Therefore, this call will call this recursively. if (aHandlingContent.IsAnyOfHTMLElements(nsGkAtoms::div, nsGkAtoms::p)) { nsresult rv = HandleChildDivOrParagraphElement( aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState, aEditingHost); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoListElementCreator::HandleChildDivOrParagraphElement() failed"); return rv; } // If we've not met a list element, create a list element and make it // current list element. if (!aState.mCurrentListElement) { nsresult rv = CreateAndUpdateCurrentListElement( aHTMLEditor, EditorDOMPoint(&aHandlingContent), EmptyListItem::NotCreate, aState, aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("AutoListElementCreator::HandleChildInlineElement() failed"); return rv; } } // If we meet an inline content, we want to move it to previously used list // item element or new list item element. if (HTMLEditUtils::IsInlineContent(aHandlingContent, BlockInlineCheck::UseHTMLDefaultStyle)) { nsresult rv = HandleChildInlineContent(aHTMLEditor, aHandlingContent, aState); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoListElementCreator::HandleChildInlineElement() failed"); return rv; } // Otherwise, we should wrap it into new list item element. nsresult rv = WrapContentIntoNewListItemElement(aHTMLEditor, aHandlingContent, aState); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoListElementCreator::WrapContentIntoNewListItemElement() failed"); return rv; } nsresult HTMLEditor::AutoListElementCreator::HandleChildListElement( HTMLEditor& aHTMLEditor, Element& aHandlingListElement, AutoHandlingState& aState) const { MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aHandlingListElement)); // 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 (aState.mCurrentListElement && !EditorUtils::IsDescendantOf(aHandlingListElement, *aState.mCurrentListElement)) { Result moveNodeResult = aHTMLEditor.MoveNodeToEndWithTransaction( aHandlingListElement, MOZ_KnownLive(*aState.mCurrentListElement)); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } moveNodeResult.inspect().IgnoreCaretPointSuggestion(); Result convertListTypeResult = aHTMLEditor.ChangeListElementType(aHandlingListElement, mListTagName, mListItemTagName); if (MOZ_UNLIKELY(convertListTypeResult.isErr())) { NS_WARNING("HTMLEditor::ChangeListElementType() failed"); return convertListTypeResult.propagateErr(); } convertListTypeResult.inspect().IgnoreCaretPointSuggestion(); Result unwrapNewListElementResult = aHTMLEditor.RemoveBlockContainerWithTransaction( MOZ_KnownLive(*convertListTypeResult.inspect().GetNewNode())); if (MOZ_UNLIKELY(unwrapNewListElementResult.isErr())) { NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed"); return unwrapNewListElementResult.propagateErr(); } aState.mPreviousListItemElement = nullptr; return NS_OK; } // If current list element is in found list element or we've not met a // list element, convert current list element to proper type. Result convertListTypeResult = aHTMLEditor.ChangeListElementType(aHandlingListElement, mListTagName, mListItemTagName); if (MOZ_UNLIKELY(convertListTypeResult.isErr())) { NS_WARNING("HTMLEditor::ChangeListElementType() failed"); return convertListTypeResult.propagateErr(); } CreateElementResult unwrappedConvertListTypeResult = convertListTypeResult.unwrap(); unwrappedConvertListTypeResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedConvertListTypeResult.GetNewNode()); aState.mCurrentListElement = unwrappedConvertListTypeResult.UnwrapNewNode(); aState.mPreviousListItemElement = nullptr; return NS_OK; } nsresult HTMLEditor::AutoListElementCreator::HandleChildListItemInDifferentTypeList( HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement, AutoHandlingState& aState) const { MOZ_ASSERT(HTMLEditUtils::IsListItem(&aHandlingListItemElement)); MOZ_ASSERT( !aHandlingListItemElement.GetParent()->IsHTMLElement(&mListTagName)); // 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 (!aState.mCurrentListElement || aHandlingListItemElement.IsInclusiveDescendantOf( aState.mCurrentListElement)) { EditorDOMPoint atListItem(&aHandlingListItemElement); MOZ_ASSERT(atListItem.IsInContentNode()); Result splitListItemParentResult = aHTMLEditor.SplitNodeWithTransaction(atListItem); if (MOZ_UNLIKELY(splitListItemParentResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); return splitListItemParentResult.propagateErr(); } SplitNodeResult unwrappedSplitListItemParentResult = splitListItemParentResult.unwrap(); MOZ_ASSERT(unwrappedSplitListItemParentResult.DidSplit()); unwrappedSplitListItemParentResult.IgnoreCaretPointSuggestion(); Result createNewListElementResult = aHTMLEditor.CreateAndInsertElement( WithTransaction::Yes, mListTagName, unwrappedSplitListItemParentResult.AtNextContent()); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING( "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) " "failed"); return createNewListElementResult.propagateErr(); } CreateElementResult unwrapCreateNewListElementResult = createNewListElementResult.unwrap(); unwrapCreateNewListElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrapCreateNewListElementResult.GetNewNode()); aState.mCurrentListElement = unwrapCreateNewListElementResult.UnwrapNewNode(); } // Then, move current node into current list element. Result moveNodeResult = aHTMLEditor.MoveNodeToEndWithTransaction( aHandlingListItemElement, MOZ_KnownLive(*aState.mCurrentListElement)); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } moveNodeResult.inspect().IgnoreCaretPointSuggestion(); // Convert list item type if current node is different list item type. if (aHandlingListItemElement.IsHTMLElement(&mListItemTagName)) { return NS_OK; } Result newListItemElementOrError = aHTMLEditor.ReplaceContainerAndCloneAttributesWithTransaction( aHandlingListItemElement, mListItemTagName); if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) { NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed"); return newListItemElementOrError.propagateErr(); } newListItemElementOrError.inspect().IgnoreCaretPointSuggestion(); return NS_OK; } nsresult HTMLEditor::AutoListElementCreator::HandleChildListItemElement( HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement, AutoHandlingState& aState) const { MOZ_ASSERT(aHandlingListItemElement.GetParentNode()); MOZ_ASSERT(HTMLEditUtils::IsListItem(&aHandlingListItemElement)); // If current list item element is not in proper list element, we need // to convert the list element. // XXX This check is not enough, if (!aHandlingListItemElement.GetParentNode()->IsHTMLElement(&mListTagName)) { nsresult rv = HandleChildListItemInDifferentTypeList( aHTMLEditor, aHandlingListItemElement, aState); if (NS_FAILED(rv)) { NS_WARNING( "AutoListElementCreator::HandleChildListItemInDifferentTypeList() " "failed"); return rv; } } else { nsresult rv = HandleChildListItemInSameTypeList( aHTMLEditor, aHandlingListItemElement, aState); if (NS_FAILED(rv)) { NS_WARNING( "AutoListElementCreator::HandleChildListItemInSameTypeList() failed"); return rv; } } // 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 (!mBulletType.IsEmpty()) { nsresult rv = aHTMLEditor.SetAttributeWithTransaction( aHandlingListItemElement, *nsGkAtoms::type, mBulletType); if (NS_WARN_IF(aHTMLEditor.Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::SetAttributeWithTransaction(nsGkAtoms::type) failed"); return rv; } // Otherwise, remove list type attribute if there is. if (!aHandlingListItemElement.HasAttr(nsGkAtoms::type)) { return NS_OK; } nsresult rv = aHTMLEditor.RemoveAttributeWithTransaction( aHandlingListItemElement, *nsGkAtoms::type); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::type) failed"); return rv; } nsresult HTMLEditor::AutoListElementCreator::HandleChildListItemInSameTypeList( HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement, AutoHandlingState& aState) const { MOZ_ASSERT(HTMLEditUtils::IsListItem(&aHandlingListItemElement)); MOZ_ASSERT( aHandlingListItemElement.GetParent()->IsHTMLElement(&mListTagName)); EditorDOMPoint atListItem(&aHandlingListItemElement); MOZ_ASSERT(atListItem.IsInContentNode()); // If we've not met a list element, set current list element to the // parent of current list item element. if (!aState.mCurrentListElement) { aState.mCurrentListElement = atListItem.GetContainerAs(); NS_WARNING_ASSERTION( HTMLEditUtils::IsAnyListElement(aState.mCurrentListElement), "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 (atListItem.GetContainer() != aState.mCurrentListElement) { Result moveNodeResult = aHTMLEditor.MoveNodeToEndWithTransaction( aHandlingListItemElement, MOZ_KnownLive(*aState.mCurrentListElement)); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } moveNodeResult.inspect().IgnoreCaretPointSuggestion(); } // Then, if current list item element is not proper type for current // list element, convert list item element to proper element. if (aHandlingListItemElement.IsHTMLElement(&mListItemTagName)) { return NS_OK; } // FIXME: Manage attribute cloning Result newListItemElementOrError = aHTMLEditor.ReplaceContainerAndCloneAttributesWithTransaction( aHandlingListItemElement, mListItemTagName); if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) { NS_WARNING( "HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction() " "failed"); return newListItemElementOrError.propagateErr(); } newListItemElementOrError.inspect().IgnoreCaretPointSuggestion(); return NS_OK; } nsresult HTMLEditor::AutoListElementCreator::HandleChildDivOrParagraphElement( HTMLEditor& aHTMLEditor, Element& aHandlingDivOrParagraphElement, AutoHandlingState& aState, const Element& aEditingHost) const { MOZ_ASSERT(aHandlingDivOrParagraphElement.IsAnyOfHTMLElements(nsGkAtoms::div, nsGkAtoms::p)); AutoRestore> previouslyReplacingBlockElement( aState.mReplacingBlockElement); aState.mReplacingBlockElement = &aHandlingDivOrParagraphElement; AutoRestore previouslyReplacingBlockElementIdCopied( aState.mMaybeCopiedReplacingBlockElementId); aState.mMaybeCopiedReplacingBlockElementId = false; // If the

or

is empty, we should replace it with a list element // and/or a list item element. if (HTMLEditUtils::IsEmptyNode(aHandlingDivOrParagraphElement, {EmptyCheckOption::TreatListItemAsVisible, EmptyCheckOption::TreatTableCellAsVisible})) { if (!aState.mCurrentListElement) { nsresult rv = CreateAndUpdateCurrentListElement( aHTMLEditor, EditorDOMPoint(&aHandlingDivOrParagraphElement), EmptyListItem::Create, aState, aEditingHost); if (NS_FAILED(rv)) { NS_WARNING( "AutoListElementCreator::CreateAndUpdateCurrentListElement(" "EmptyListItem::Create) failed"); return rv; } } else { Result createListItemElementResult = AppendListItemElement( aHTMLEditor, MOZ_KnownLive(*aState.mCurrentListElement), aState); if (MOZ_UNLIKELY(createListItemElementResult.isErr())) { NS_WARNING("AutoListElementCreator::AppendListItemElement() failed"); return createListItemElementResult.unwrapErr(); } CreateElementResult unwrappedResult = createListItemElementResult.unwrap(); unwrappedResult.IgnoreCaretPointSuggestion(); aState.mListOrListItemElementToPutCaret = unwrappedResult.UnwrapNewNode(); } nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aHandlingDivOrParagraphElement); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed"); return rv; } // We don't want new inline contents inserted into the new list item element // because we want to keep the line break at end of // aHandlingDivOrParagraphElement. aState.mPreviousListItemElement = nullptr; return NS_OK; } // If current node is a

element, replace it with its children and handle // them as same as topmost children in the range. AutoContentNodeArray arrayOfContentsInDiv; HTMLEditUtils::CollectChildren(aHandlingDivOrParagraphElement, arrayOfContentsInDiv, 0, {CollectChildrenOption::CollectListChildren, CollectChildrenOption::CollectTableChildren}); Result unwrapDivElementResult = aHTMLEditor.RemoveContainerWithTransaction( aHandlingDivOrParagraphElement); if (MOZ_UNLIKELY(unwrapDivElementResult.isErr())) { NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed"); return unwrapDivElementResult.unwrapErr(); } for (const OwningNonNull& content : arrayOfContentsInDiv) { // MOZ_KnownLive because of bug 1620312 nsresult rv = HandleChildContent(aHTMLEditor, MOZ_KnownLive(content), aState, aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("AutoListElementCreator::HandleChildContent() failed"); return rv; } } // We don't want new inline contents inserted into the new list item element // because we want to keep the line break at end of // aHandlingDivOrParagraphElement. aState.mPreviousListItemElement = nullptr; return NS_OK; } nsresult HTMLEditor::AutoListElementCreator::CreateAndUpdateCurrentListElement( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert, EmptyListItem aEmptyListItem, AutoHandlingState& aState, const Element& aEditingHost) const { MOZ_ASSERT(aPointToInsert.IsSetAndValid()); aState.mPreviousListItemElement = nullptr; RefPtr newListItemElement; auto initializer = // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868 [&](HTMLEditor&, Element& aListElement, const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY { // If the replacing element has `dir` attribute, the new list // element should take it to correct its list marker position. if (aState.mReplacingBlockElement) { nsString dirValue; if (aState.mReplacingBlockElement->GetAttr(nsGkAtoms::dir, dirValue) && !dirValue.IsEmpty()) { // We don't need to use transaction to set `dir` attribute here // because the element will be stored with the `dir` attribute // in InsertNodeTransaction. Therefore, undo should work. IgnoredErrorResult ignoredError; aListElement.SetAttr(nsGkAtoms::dir, dirValue, ignoredError); NS_WARNING_ASSERTION( !ignoredError.Failed(), "Element::SetAttr(nsGkAtoms::dir) failed, but ignored"); } } if (aEmptyListItem == EmptyListItem::Create) { Result createNewListItemResult = AppendListItemElement(aHTMLEditor, aListElement, aState); if (MOZ_UNLIKELY(createNewListItemResult.isErr())) { NS_WARNING( "HTMLEditor::AppendNewElementToInsertingElement()" " failed"); return createNewListItemResult.unwrapErr(); } CreateElementResult unwrappedResult = createNewListItemResult.unwrap(); unwrappedResult.IgnoreCaretPointSuggestion(); newListItemElement = unwrappedResult.UnwrapNewNode(); } return NS_OK; }; Result createNewListElementResult = aHTMLEditor.InsertElementWithSplittingAncestorsWithTransaction( mListTagName, aPointToInsert, BRElementNextToSplitPoint::Keep, aEditingHost, initializer); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING( nsPrintfCString( "HTMLEditor::" "InsertElementWithSplittingAncestorsWithTransaction(%s) failed", nsAtomCString(&mListTagName).get()) .get()); return createNewListElementResult.propagateErr(); } CreateElementResult unwrappedCreateNewListElementResult = createNewListElementResult.unwrap(); unwrappedCreateNewListElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode()); aState.mListOrListItemElementToPutCaret = newListItemElement ? newListItemElement.get() : unwrappedCreateNewListElementResult.GetNewNode(); aState.mCurrentListElement = unwrappedCreateNewListElementResult.UnwrapNewNode(); aState.mPreviousListItemElement = std::move(newListItemElement); return NS_OK; } // static nsresult HTMLEditor::AutoListElementCreator::MaybeCloneAttributesToNewListItem( HTMLEditor& aHTMLEditor, Element& aListItemElement, AutoHandlingState& aState) { if (!aState.mReplacingBlockElement) { return NS_OK; } // If we're replacing a block element, the list items should have attributes // of the replacing element. However, we don't want to copy `dir` attribute // because it does not affect content in list item element and setting // opposite direction from the parent list causes the marker invisible. // Therefore, we don't want to take it. Finally, we don't need to use // transaction to copy the attributes here because the element will be stored // with the attributes in InsertNodeTransaction. Therefore, undo should work. nsresult rv = aHTMLEditor.CopyAttributes( WithTransaction::No, aListItemElement, MOZ_KnownLive(*aState.mReplacingBlockElement), aState.mMaybeCopiedReplacingBlockElementId ? HTMLEditor::CopyAllAttributesExceptIdAndDir : HTMLEditor::CopyAllAttributesExceptDir); aState.mMaybeCopiedReplacingBlockElementId = true; if (NS_WARN_IF(aHTMLEditor.Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::CopyAttributes(WithTransaction::No) failed"); return rv; } Result HTMLEditor::AutoListElementCreator::AppendListItemElement( HTMLEditor& aHTMLEditor, const Element& aListElement, AutoHandlingState& aState) const { const WithTransaction withTransaction = aListElement.IsInComposedDoc() ? WithTransaction::Yes : WithTransaction::No; Result createNewListItemResult = aHTMLEditor.CreateAndInsertElement( withTransaction, mListItemTagName, EditorDOMPoint::AtEndOf(aListElement), !aState.mReplacingBlockElement ? HTMLEditor::DoNothingForNewElement // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868 : [&aState](HTMLEditor& aHTMLEditor, Element& aListItemElement, const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY { nsresult rv = AutoListElementCreator::MaybeCloneAttributesToNewListItem( aHTMLEditor, aListItemElement, aState); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoListElementCreator::" "MaybeCloneAttributesToNewListItem() failed"); return rv; }); NS_WARNING_ASSERTION(createNewListItemResult.isOk(), "HTMLEditor::CreateAndInsertElement() failed"); return createNewListItemResult; } nsresult HTMLEditor::AutoListElementCreator::HandleChildInlineContent( HTMLEditor& aHTMLEditor, nsIContent& aHandlingInlineContent, AutoHandlingState& aState) const { MOZ_ASSERT(HTMLEditUtils::IsInlineContent( aHandlingInlineContent, BlockInlineCheck::UseHTMLDefaultStyle)); // 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 (!aState.mPreviousListItemElement) { nsresult rv = WrapContentIntoNewListItemElement( aHTMLEditor, aHandlingInlineContent, aState); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoListElementCreator::WrapContentIntoNewListItemElement() failed"); return rv; } Result moveInlineElementResult = aHTMLEditor.MoveNodeToEndWithTransaction( aHandlingInlineContent, MOZ_KnownLive(*aState.mPreviousListItemElement)); if (MOZ_UNLIKELY(moveInlineElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveInlineElementResult.propagateErr(); } moveInlineElementResult.inspect().IgnoreCaretPointSuggestion(); return NS_OK; } nsresult HTMLEditor::AutoListElementCreator::WrapContentIntoNewListItemElement( HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent, AutoHandlingState& aState) const { // If current node is not a paragraph, wrap current node with new list // item element and move it into current list element. Result wrapContentInListItemElementResult = aHTMLEditor.InsertContainerWithTransaction( aHandlingContent, mListItemTagName, !aState.mReplacingBlockElement ? HTMLEditor::DoNothingForNewElement // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868 : [&aState](HTMLEditor& aHTMLEditor, Element& aListItemElement, const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY { nsresult rv = AutoListElementCreator::MaybeCloneAttributesToNewListItem( aHTMLEditor, aListItemElement, aState); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "AutoListElementCreator::" "MaybeCloneAttributesToNewListItem() failed"); return rv; }); if (MOZ_UNLIKELY(wrapContentInListItemElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed"); return wrapContentInListItemElementResult.unwrapErr(); } CreateElementResult unwrappedWrapContentInListItemElementResult = wrapContentInListItemElementResult.unwrap(); unwrappedWrapContentInListItemElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedWrapContentInListItemElementResult.GetNewNode()); // MOZ_KnownLive(unwrappedWrapContentInListItemElementResult.GetNewNode()): // The result is grabbed by unwrappedWrapContentInListItemElementResult. Result moveListItemElementResult = aHTMLEditor.MoveNodeToEndWithTransaction( MOZ_KnownLive( *unwrappedWrapContentInListItemElementResult.GetNewNode()), MOZ_KnownLive(*aState.mCurrentListElement)); if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveListItemElementResult.unwrapErr(); } moveListItemElementResult.inspect().IgnoreCaretPointSuggestion(); // If current node is not a block element, new list item should have // following inline nodes too. if (HTMLEditUtils::IsInlineContent(aHandlingContent, BlockInlineCheck::UseHTMLDefaultStyle)) { aState.mPreviousListItemElement = unwrappedWrapContentInListItemElementResult.UnwrapNewNode(); } else { aState.mPreviousListItemElement = nullptr; } // XXX Why don't we set `type` attribute here?? return NS_OK; } nsresult HTMLEditor::AutoListElementCreator:: EnsureCollapsedRangeIsInListItemOrListElement( Element& aListItemOrListToPutCaret, AutoRangeArray& aRanges) const { if (!aRanges.IsCollapsed() || aRanges.Ranges().IsEmpty()) { return NS_OK; } const auto firstRangeStartPoint = aRanges.GetFirstRangeStartPoint(); if (MOZ_UNLIKELY(!firstRangeStartPoint.IsSet())) { return NS_OK; } Result pointToPutCaretOrError = HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< EditorRawDOMPoint>(aListItemOrListToPutCaret, firstRangeStartPoint); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING("HTMLEditUtils::ComputePointToPutCaretInElementIfOutside()"); return pointToPutCaretOrError.unwrapErr(); } if (pointToPutCaretOrError.inspect().IsSet()) { nsresult rv = aRanges.Collapse(pointToPutCaretOrError.inspect()); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::Collapse() failed"); return rv; } } return NS_OK; } nsresult HTMLEditor::RemoveListAtSelectionAsSubAction( const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result.unwrapErr(); } if (result.inspect().Canceled()) { return NS_OK; } } AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult error; AutoEditSubActionNotifier startToHandleEditSubAction( *this, EditSubAction::eRemoveList, nsIEditor::eNext, error); if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { return error.StealNSResult(); } NS_WARNING_ASSERTION( !error.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); // XXX Why do we do this only when there is only one selection range? if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( SelectionRef().GetRangeAt(0u), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.unwrapErr(); } // Note that end point may be prior to start point. So, we // cannot use Selection::SetStartAndEndInLimit() here. error.SuppressException(); SelectionRef().SetBaseAndExtentInLimiter( extendedRange.inspect().StartRef().ToRawRangeBoundary(), extendedRange.inspect().EndRef().ToRawRangeBoundary(), error); if (NS_WARN_IF(Destroyed())) { return NS_ERROR_EDITOR_DESTROYED; } if (error.Failed()) { NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed"); return error.StealNSResult(); } } AutoSelectionRestorer restoreSelectionLater(*this); AutoTArray, 64> arrayOfContents; { // TODO: We don't need AutoTransactionsConserveSelection here in the normal // cases, but removing this may cause the behavior with the legacy // mutation event listeners. We should try to delete this in a bug. AutoTransactionsConserveSelection dontChangeMySelection(*this); { AutoRangeArray extendedSelectionRanges(SelectionRef()); extendedSelectionRanges.ExtendRangesToWrapLines( EditSubAction::eCreateOrChangeList, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost); Result splitResult = extendedSelectionRanges .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() " "failed"); return splitResult.unwrapErr(); } nsresult rv = extendedSelectionRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eCreateOrChangeList, AutoRangeArray::CollectNonEditableNodes::No); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::" "eCreateOrChangeList, CollectNonEditableNodes::No) failed"); return rv; } } const Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eCreateOrChangeList); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" "eCreateOrChangeList) failed"); return splitAtBRElementsResult.inspectErr(); } } // Remove all non-editable nodes. Leave them be. // XXX CollectEditTargetNodes() should return only editable contents when it's // called with CollectNonEditableNodes::No, but checking it here, looks // like just wasting the runtime cost. 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; } Result, nsresult> HTMLEditor::FormatBlockContainerWithTransaction( AutoRangeArray& aSelectionRanges, const nsStaticAtom& aNewFormatTagName, FormatBlockMode aFormatBlockMode, const Element& aEditingHost) { MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); // XXX Why do we do this only when there is only one selection range? if (!aSelectionRanges.IsCollapsed() && aSelectionRanges.Ranges().Length() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( aSelectionRanges.FirstRangeRef(), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.propagateErr(); } // Note that end point may be prior to start point. So, we // cannot use AutoRangeArray::SetStartAndEnd() here. if (NS_FAILED(aSelectionRanges.SetBaseAndExtent( extendedRange.inspect().StartRef(), extendedRange.inspect().EndRef()))) { NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed"); return Err(NS_ERROR_FAILURE); } } MOZ_ALWAYS_TRUE(aSelectionRanges.SaveAndTrackRanges(*this)); // TODO: We don't need AutoTransactionsConserveSelection here in the normal // cases, but removing this may cause the behavior with the legacy // mutation event listeners. We should try to delete this in a bug. AutoTransactionsConserveSelection dontChangeMySelection(*this); AutoTArray, 64> arrayOfContents; aSelectionRanges.ExtendRangesToWrapLines( aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand ? EditSubAction::eFormatBlockForHTMLCommand : EditSubAction::eCreateOrRemoveBlock, BlockInlineCheck::UseComputedDisplayOutsideStyle, aEditingHost); Result splitResult = aSelectionRanges .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( *this, BlockInlineCheck::UseComputedDisplayOutsideStyle, aEditingHost); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed"); return splitResult.propagateErr(); } nsresult rv = aSelectionRanges.CollectEditTargetNodes( *this, arrayOfContents, aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand ? EditSubAction::eFormatBlockForHTMLCommand : EditSubAction::eCreateOrRemoveBlock, AutoRangeArray::CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(CollectNonEditableNodes::No) " "failed"); return Err(rv); } Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement( arrayOfContents, aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand ? EditSubAction::eFormatBlockForHTMLCommand : EditSubAction::eCreateOrRemoveBlock); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING("HTMLEditor::MaybeSplitElementsAtEveryBRElement() failed"); return splitAtBRElementsResult.propagateErr(); } // 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, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (NS_WARN_IF(aSelectionRanges.Ranges().IsEmpty())) { return Err(NS_ERROR_FAILURE); } auto pointToInsertBlock = aSelectionRanges.GetFirstRangeStartPoint(); if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand && (&aNewFormatTagName == nsGkAtoms::normal || &aNewFormatTagName == nsGkAtoms::_empty)) { if (!pointToInsertBlock.IsInContentNode()) { NS_WARNING( "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " "block parent because container of the point is not content"); return Err(NS_ERROR_FAILURE); } // We are removing blocks (going to "body text") const RefPtr editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *pointToInsertBlock.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (!editableBlockElement) { NS_WARNING( "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " "block parent"); return Err(NS_ERROR_FAILURE); } if (editableBlockElement->IsAnyOfHTMLElements( nsGkAtoms::dd, nsGkAtoms::dl, nsGkAtoms::dt) || !HTMLEditUtils::IsFormatElementForParagraphStateCommand( *editableBlockElement)) { return RefPtr(); } // 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}, BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost)) { if (brContent && brContent->IsHTMLElement(nsGkAtoms::br)) { AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock); nsresult rv = DeleteNodeWithTransaction(*brContent); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } } } // Do the splits! Result splitNodeResult = SplitNodeDeepWithTransaction( *editableBlockElement, pointToInsertBlock, SplitAtEdges::eDoNotCreateEmptyContainer); if (MOZ_UNLIKELY(splitNodeResult.isErr())) { NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed"); return splitNodeResult.propagateErr(); } SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap(); unwrappedSplitNodeResult.IgnoreCaretPointSuggestion(); // Put a
element at the split point Result insertBRElementResult = InsertBRElement( WithTransaction::Yes, unwrappedSplitNodeResult.AtSplitPoint()); if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); return insertBRElementResult.propagateErr(); } MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode()); aSelectionRanges.ClearSavedRanges(); nsresult rv = aSelectionRanges.Collapse( EditorRawDOMPoint(insertBRElementResult.inspect().GetNewNode())); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::Collapse() failed"); return Err(rv); } return RefPtr(); } // We are making a block. Consume a br, if needed. if (nsCOMPtr maybeBRContent = HTMLEditUtils::GetNextContent( pointToInsertBlock, {WalkTreeOption::IgnoreNonEditableNode, WalkTreeOption::StopAtBlockBoundary}, BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost)) { if (maybeBRContent->IsHTMLElement(nsGkAtoms::br)) { AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock); nsresult rv = DeleteNodeWithTransaction(*maybeBRContent); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } // We don't need to act on this node any more arrayOfContents.RemoveElement(maybeBRContent); } } // Make sure we can put a block here. Result createNewBlockElementResult = InsertElementWithSplittingAncestorsWithTransaction( aNewFormatTagName, pointToInsertBlock, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) { NS_WARNING( nsPrintfCString( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "%s) failed", nsAtomCString(&aNewFormatTagName).get()) .get()); return createNewBlockElementResult.propagateErr(); } CreateElementResult unwrappedCreateNewBlockElementResult = createNewBlockElementResult.unwrap(); unwrappedCreateNewBlockElementResult.IgnoreCaretPointSuggestion(); MOZ_ASSERT(unwrappedCreateNewBlockElementResult.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. nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return Err(rv); } arrayOfContents.RemoveElementAt(0); } // Put selection in new block aSelectionRanges.ClearSavedRanges(); nsresult rv = aSelectionRanges.Collapse(EditorRawDOMPoint( unwrappedCreateNewBlockElementResult.GetNewNode(), 0u)); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::Collapse() failed"); return Err(rv); } return unwrappedCreateNewBlockElementResult.UnwrapNewNode(); } if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand) { // Okay, now go through all the nodes and make the right kind of blocks, or // whatever is appropriate. // Note: blockquote is handled a little differently. if (&aNewFormatTagName == nsGkAtoms::blockquote) { Result wrapContentsInBlockquoteElementsResult = WrapContentsInBlockquoteElementsWithTransaction(arrayOfContents, aEditingHost); if (MOZ_UNLIKELY(wrapContentsInBlockquoteElementsResult.isErr())) { NS_WARNING( "HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction() " "failed"); return wrapContentsInBlockquoteElementsResult.propagateErr(); } wrapContentsInBlockquoteElementsResult.inspect() .IgnoreCaretPointSuggestion(); return wrapContentsInBlockquoteElementsResult.unwrap().UnwrapNewNode(); } if (&aNewFormatTagName == nsGkAtoms::normal || &aNewFormatTagName == nsGkAtoms::_empty) { Result removeBlockContainerElementsResult = RemoveBlockContainerElementsWithTransaction( arrayOfContents, FormatBlockMode::XULParagraphStateCommand, BlockInlineCheck::UseComputedDisplayOutsideStyle); if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) { NS_WARNING( "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed"); return removeBlockContainerElementsResult.propagateErr(); } return RefPtr(); } } Result wrapContentsInBlockElementResult = CreateOrChangeFormatContainerElement(arrayOfContents, aNewFormatTagName, aFormatBlockMode, aEditingHost); if (MOZ_UNLIKELY(wrapContentsInBlockElementResult.isErr())) { NS_WARNING("HTMLEditor::CreateOrChangeFormatContainerElement() failed"); return wrapContentsInBlockElementResult.propagateErr(); } wrapContentsInBlockElementResult.inspect().IgnoreCaretPointSuggestion(); return wrapContentsInBlockElementResult.unwrap().UnwrapNewNode(); } 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; } Result HTMLEditor::IndentAsSubAction( const Element& aEditingHost) { 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 Err(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionResult::IgnoredResult(); } Result result = HandleIndentAtSelection(aEditingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::HandleIndentAtSelection() failed"); return result; } if (result.inspect().Canceled()) { return result; } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Mutation event listener might have changed selection"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } // TODO: Investigate when we need to put a `
` element after indenting // ranges. Then, we could stop calling this here, or maybe we need to // do it while moving content nodes. nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection(); if (NS_FAILED(rv)) { NS_WARNING( "MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() failed"); return Err(rv); } return result; } Result HTMLEditor::IndentListChildWithTransaction( RefPtr* aSubListElement, const EditorDOMPoint& aPointInListElement, nsIContent& aContentMovingToSubList, const Element& aEditingHost) { MOZ_ASSERT( HTMLEditUtils::IsAnyListElement(aPointInListElement.GetContainer()), "unexpected container"); MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); // some logic for putting list items into nested lists... // If aContentMovingToSubList is followed by a sub-list element whose tag is // same as the parent list element's tag, we can move it to start of the // sub-list. if (nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling( aContentMovingToSubList, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, WalkTreeOption::IgnoreNonEditableNode})) { if (HTMLEditUtils::IsAnyListElement(nextEditableSibling) && aPointInListElement.GetContainer()->NodeInfo()->NameAtom() == nextEditableSibling->NodeInfo()->NameAtom() && aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() == nextEditableSibling->NodeInfo()->NamespaceID()) { Result moveListElementResult = MoveNodeWithTransaction(aContentMovingToSubList, EditorDOMPoint(nextEditableSibling, 0u)); if (MOZ_UNLIKELY(moveListElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed"); return moveListElementResult.propagateErr(); } return moveListElementResult.unwrap().UnwrapCaretPoint(); } } // If aContentMovingToSubList follows a sub-list element whose tag is same // as the parent list element's tag, we can move it to end of the sub-list. if (nsCOMPtr previousEditableSibling = HTMLEditUtils::GetPreviousSibling( aContentMovingToSubList, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, WalkTreeOption::IgnoreNonEditableNode})) { if (HTMLEditUtils::IsAnyListElement(previousEditableSibling) && aPointInListElement.GetContainer()->NodeInfo()->NameAtom() == previousEditableSibling->NodeInfo()->NameAtom() && aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() == previousEditableSibling->NodeInfo()->NamespaceID()) { Result moveListElementResult = MoveNodeToEndWithTransaction(aContentMovingToSubList, *previousEditableSibling); if (MOZ_UNLIKELY(moveListElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveListElementResult.propagateErr(); } return moveListElementResult.unwrap().UnwrapCaretPoint(); } } // If aContentMovingToSubList does not follow aSubListElement, we need // to create new sub-list element. EditorDOMPoint pointToPutCaret; nsIContent* previousEditableSibling = *aSubListElement ? HTMLEditUtils::GetPreviousSibling( aContentMovingToSubList, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, WalkTreeOption::IgnoreNonEditableNode}) : nullptr; if (!*aSubListElement || (previousEditableSibling && previousEditableSibling != *aSubListElement)) { nsAtom* containerName = aPointInListElement.GetContainer()->NodeInfo()->NameAtom(); // Create a new nested list of correct type. Result createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( MOZ_KnownLive(*containerName), aPointInListElement, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING( nsPrintfCString( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "%s) failed", nsAtomCString(containerName).get()) .get()); return createNewListElementResult.propagateErr(); } CreateElementResult unwrappedCreateNewListElementResult = createNewListElementResult.unwrap(); MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode()); pointToPutCaret = unwrappedCreateNewListElementResult.UnwrapCaretPoint(); *aSubListElement = unwrappedCreateNewListElementResult.UnwrapNewNode(); } // Finally, we should move aContentMovingToSubList into aSubListElement. const RefPtr subListElement = *aSubListElement; Result moveNodeResult = MoveNodeToEndWithTransaction(aContentMovingToSubList, *subListElement); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.propagateErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint(); } return pointToPutCaret; } Result HTMLEditor::HandleIndentAtSelection( const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); nsresult rv = EnsureNoPaddingBRElementForEmptyEditor(); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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 Err(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 Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored"); } } AutoRangeArray selectionRanges(SelectionRef()); if (MOZ_UNLIKELY(!selectionRanges.IsInContent())) { NS_WARNING("Mutation event listener might have changed the selection"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } if (IsCSSEnabled()) { nsresult rv = HandleCSSIndentAroundRanges(selectionRanges, aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::HandleCSSIndentAroundRanges() failed"); return Err(rv); } } else { nsresult rv = HandleHTMLIndentAroundRanges(selectionRanges, aEditingHost); if (NS_FAILED(rv)) { NS_WARNING("HTMLEditor::HandleHTMLIndentAroundRanges() failed"); return Err(rv); } } rv = selectionRanges.ApplyTo(SelectionRef()); if (MOZ_UNLIKELY(Destroyed())) { NS_WARNING("AutoRangeArray::ApplyTo() caused destroying the editor"); return Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::ApplyTo() failed"); return Err(rv); } return EditActionResult::HandledResult(); } nsresult HTMLEditor::HandleCSSIndentAroundRanges(AutoRangeArray& aRanges, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!aRanges.Ranges().IsEmpty()); MOZ_ASSERT(aRanges.IsInContent()); if (aRanges.Ranges().IsEmpty()) { NS_WARNING("There is no selection range"); return NS_ERROR_FAILURE; } // XXX Why do we do this only when there is only one selection range? if (!aRanges.IsCollapsed() && aRanges.Ranges().Length() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( aRanges.FirstRangeRef(), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.unwrapErr(); } // Note that end point may be prior to start point. So, we // cannot use SetStartAndEnd() here. nsresult rv = aRanges.SetBaseAndExtent(extendedRange.inspect().StartRef(), extendedRange.inspect().EndRef()); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed"); return rv; } } if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) { return NS_ERROR_FAILURE; } AutoTArray, 64> arrayOfContents; // short circuit: detect case of collapsed selection inside an
  • . // just sublist that
  • . This prevents bug 97797. if (aRanges.IsCollapsed()) { const auto atCaret = aRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!atCaret.IsSet())) { return NS_ERROR_FAILURE; } MOZ_ASSERT(atCaret.IsInContentNode()); Element* const editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( *atCaret.ContainerAs(), HTMLEditUtils::ClosestEditableBlockElement, BlockInlineCheck::UseHTMLDefaultStyle); if (editableBlockElement && HTMLEditUtils::IsListItem(editableBlockElement)) { arrayOfContents.AppendElement(*editableBlockElement); } } EditorDOMPoint pointToPutCaret; if (arrayOfContents.IsEmpty()) { { AutoRangeArray extendedRanges(aRanges); extendedRanges.ExtendRangesToWrapLines( EditSubAction::eIndent, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost); Result splitResult = extendedRanges .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() " "failed"); return splitResult.unwrapErr(); } if (splitResult.inspect().IsSet()) { pointToPutCaret = splitResult.unwrap(); } nsresult rv = extendedRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eIndent, AutoRangeArray::CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eIndent, " "CollectNonEditableNodes::Yes) failed"); return rv; } } Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eIndent); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" "eIndent) failed"); return splitAtBRElementsResult.inspectErr(); } if (splitAtBRElementsResult.inspect().IsSet()) { pointToPutCaret = splitAtBRElementsResult.unwrap(); } } // 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, BlockInlineCheck::UseHTMLDefaultStyle)) { const EditorDOMPoint pointToInsertDivElement = pointToPutCaret.IsSet() ? std::move(pointToPutCaret) : aRanges.GetFirstRangeStartPoint(); if (NS_WARN_IF(!pointToInsertDivElement.IsSet())) { return NS_ERROR_FAILURE; } // make sure we can put a block here Result createNewDivElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::div, pointToInsertDivElement, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::div) failed"); return createNewDivElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewDivElementResult = createNewDivElementResult.unwrap(); // We'll collapse ranges below, so we don't need to touch the ranges here. unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion(); const RefPtr newDivElement = unwrappedCreateNewDivElementResult.UnwrapNewNode(); MOZ_ASSERT(newDivElement); const Result pointToPutCaretOrError = ChangeMarginStart(*newDivElement, ChangeMargin::Increase, aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) 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. for (const OwningNonNull& content : arrayOfContents) { // MOZ_KnownLive(content) due to bug 1622253 nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(content)); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return rv; } } aRanges.ClearSavedRanges(); nsresult rv = aRanges.Collapse(EditorDOMPoint(newDivElement, 0u)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed"); return rv; } RefPtr latestNewBlockElement; auto RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside = [&]() -> nsresult { MOZ_ASSERT(aRanges.HasSavedRanges()); aRanges.RestoreFromSavedRanges(); if (!latestNewBlockElement || !aRanges.IsCollapsed() || aRanges.Ranges().IsEmpty()) { return NS_OK; } const auto firstRangeStartRawPoint = aRanges.GetFirstRangeStartPoint(); if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) { return NS_OK; } Result pointInNewBlockElementOrError = HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< EditorRawDOMPoint>(*latestNewBlockElement, firstRangeStartRawPoint); if (MOZ_UNLIKELY(pointInNewBlockElementOrError.isErr())) { NS_WARNING( "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, " "but ignored"); return NS_OK; } if (!pointInNewBlockElementOrError.inspect().IsSet()) { return NS_OK; } return aRanges.Collapse(pointInNewBlockElementOrError.unwrap()); }; // Ok, now go through all the nodes and put them into sub-list element // elements and new
    elements which have start margin. RefPtr subListElement, divElement; 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())) { const RefPtr oldSubListElement = subListElement; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result pointToPutCaretOrError = IndentListChildWithTransaction(&subListElement, atContent, MOZ_KnownLive(content), aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING("HTMLEditor::IndentListChildWithTransaction() failed"); return pointToPutCaretOrError.unwrapErr(); } if (subListElement != oldSubListElement) { // New list element is created, so we should put caret into the new list // element. latestNewBlockElement = subListElement; } if (pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } continue; } // Not a list item. if (HTMLEditUtils::IsBlockElement(content, BlockInlineCheck::UseHTMLDefaultStyle)) { Result pointToPutCaretOrError = ChangeMarginStart(MOZ_KnownLive(*content->AsElement()), ChangeMargin::Increase, aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (MOZ_UNLIKELY(pointToPutCaretOrError.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed"); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but " "ignored"); } else if (pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } divElement = nullptr; continue; } if (!divElement) { // First, check that our element can contain a div. if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(), *nsGkAtoms::div)) { // XXX This is odd, why do we stop indenting remaining content nodes? // Perhaps, `continue` is better. nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RestoreSavedRangesAndCollapseInLatestBlockElement" "IfOutside() failed"); return rv; } Result createNewDivElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::div) failed"); return createNewDivElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewDivElementResult = createNewDivElementResult.unwrap(); pointToPutCaret = unwrappedCreateNewDivElementResult.UnwrapCaretPoint(); MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode()); divElement = unwrappedCreateNewDivElementResult.UnwrapNewNode(); Result pointToPutCaretOrError = ChangeMarginStart(*divElement, ChangeMargin::Increase, aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (MOZ_UNLIKELY(pointToPutCaretOrError.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed"); return NS_ERROR_EDITOR_DESTROYED; } NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but " "ignored"); } else if (AllowsTransactionsToChangeSelection() && pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } latestNewBlockElement = divElement; } // Move the content into the
    which has start margin. // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result moveNodeResult = MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *divElement); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.unwrapErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint(); } } nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside() failed"); return rv; } nsresult HTMLEditor::HandleHTMLIndentAroundRanges(AutoRangeArray& aRanges, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); MOZ_ASSERT(!aRanges.Ranges().IsEmpty()); MOZ_ASSERT(aRanges.IsInContent()); // XXX Why do we do this only when there is only one range? if (!aRanges.IsCollapsed() && aRanges.Ranges().Length() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( aRanges.FirstRangeRef(), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.unwrapErr(); } // Note that end point may be prior to start point. So, we cannot use // SetStartAndEnd() here. nsresult rv = aRanges.SetBaseAndExtent(extendedRange.inspect().StartRef(), extendedRange.inspect().EndRef()); if (NS_FAILED(rv)) { NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed"); return rv; } } if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) { return NS_ERROR_FAILURE; } EditorDOMPoint pointToPutCaret; // 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 // use these ranges to construct a list of nodes to act on. AutoTArray, 64> arrayOfContents; { AutoRangeArray extendedRanges(aRanges); extendedRanges.ExtendRangesToWrapLines( EditSubAction::eIndent, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost); Result splitResult = extendedRanges .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost); if (MOZ_UNLIKELY(splitResult.isErr())) { NS_WARNING( "AutoRangeArray::" "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() " "failed"); return splitResult.unwrapErr(); } if (splitResult.inspect().IsSet()) { pointToPutCaret = splitResult.unwrap(); } nsresult rv = extendedRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eIndent, AutoRangeArray::CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eIndent, " "CollectNonEditableNodes::Yes) failed"); return rv; } } // FIXME: Split ancestors when we consider to indent the range. Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eIndent); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::eIndent)" " failed"); return splitAtBRElementsResult.inspectErr(); } if (splitAtBRElementsResult.inspect().IsSet()) { pointToPutCaret = splitAtBRElementsResult.unwrap(); } // 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, BlockInlineCheck::UseHTMLDefaultStyle)) { const EditorDOMPoint pointToInsertBlockquoteElement = pointToPutCaret.IsSet() ? std::move(pointToPutCaret) : EditorBase::GetFirstSelectionStartPoint(); if (NS_WARN_IF(!pointToInsertBlockquoteElement.IsSet())) { return NS_ERROR_FAILURE; } // If there is no element which can have
    , abort. if (NS_WARN_IF(!HTMLEditUtils::GetInsertionPointInInclusiveAncestor( *nsGkAtoms::blockquote, pointToInsertBlockquoteElement, &aEditingHost) .IsSet())) { return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; } // Make sure we can put a block here. // XXX Unfortunately, this calls // MaybeSplitAncestorsForInsertWithTransaction() then, // HTMLEditUtils::GetInsertionPointInInclusiveAncestor() is called again. Result createNewBlockquoteElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::blockquote, pointToInsertBlockquoteElement, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::blockquote) failed"); return createNewBlockquoteElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewBlockquoteElementResult = createNewBlockquoteElementResult.unwrap(); unwrappedCreateNewBlockquoteElementResult.IgnoreCaretPointSuggestion(); RefPtr newBlockquoteElement = unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode(); MOZ_ASSERT(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_FAILED(rv)) { NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); return rv; } } aRanges.ClearSavedRanges(); nsresult rv = aRanges.Collapse(EditorRawDOMPoint(newBlockquoteElement, 0u)); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionToStartOf() failed"); return rv; } RefPtr latestNewBlockElement; auto RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside = [&]() -> nsresult { MOZ_ASSERT(aRanges.HasSavedRanges()); aRanges.RestoreFromSavedRanges(); if (!latestNewBlockElement || !aRanges.IsCollapsed() || aRanges.Ranges().IsEmpty()) { return NS_OK; } const auto firstRangeStartRawPoint = aRanges.GetFirstRangeStartPoint(); if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) { return NS_OK; } Result pointInNewBlockElementOrError = HTMLEditUtils::ComputePointToPutCaretInElementIfOutside< EditorRawDOMPoint>(*latestNewBlockElement, firstRangeStartRawPoint); if (MOZ_UNLIKELY(pointInNewBlockElementOrError.isErr())) { NS_WARNING( "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, " "but ignored"); return NS_OK; } if (!pointInNewBlockElementOrError.inspect().IsSet()) { return NS_OK; } return aRanges.Collapse(pointInNewBlockElementOrError.unwrap()); }; // Ok, now go through all the nodes and put them in a blockquote, // or whatever is appropriate. Wohoo! RefPtr subListElement, blockquoteElement, indentedListItemElement; 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) || !HTMLEditUtils::IsRemovableNode(content)) { continue; } // If the content has been moved to different place, ignore it. if (MOZ_UNLIKELY(!content->IsInclusiveDescendantOf(&aEditingHost))) { continue; } if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) { const RefPtr oldSubListElement = subListElement; // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result pointToPutCaretOrError = IndentListChildWithTransaction(&subListElement, atContent, MOZ_KnownLive(content), aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { NS_WARNING("HTMLEditor::IndentListChildWithTransaction() failed"); return pointToPutCaretOrError.unwrapErr(); } if (oldSubListElement != subListElement) { // New list element is created, so we should put caret into the new list // element. latestNewBlockElement = subListElement; } if (pointToPutCaretOrError.inspect().IsSet()) { pointToPutCaret = pointToPutCaretOrError.unwrap(); } blockquoteElement = 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, &aEditingHost)) { if (indentedListItemElement == listItem) { // already indented this list item continue; } // check to see if subListElement is still appropriate. Which it is if // content is still right after it in the same list. nsIContent* previousEditableSibling = subListElement ? HTMLEditUtils::GetPreviousSibling( *listItem, {WalkTreeOption::IgnoreNonEditableNode}) : nullptr; if (!subListElement || (previousEditableSibling && previousEditableSibling != subListElement)) { 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. Result createNewListElementResult = InsertElementWithSplittingAncestorsWithTransaction( MOZ_KnownLive(*containerName), atListItem, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewListElementResult.isErr())) { NS_WARNING(nsPrintfCString("HTMLEditor::" "InsertElementWithSplittingAncestorsWithTr" "ansaction(%s) failed", nsAtomCString(containerName).get()) .get()); return createNewListElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewListElementResult = createNewListElementResult.unwrap(); if (unwrappedCreateNewListElementResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedCreateNewListElementResult.UnwrapCaretPoint(); } MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode()); subListElement = unwrappedCreateNewListElementResult.UnwrapNewNode(); } Result moveListItemElementResult = MoveNodeToEndWithTransaction(*listItem, *subListElement); if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveListItemElementResult.unwrapErr(); } MoveNodeResult unwrappedMoveListItemElementResult = moveListItemElementResult.unwrap(); if (unwrappedMoveListItemElementResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedMoveListItemElementResult.UnwrapCaretPoint(); } // Remember the list item element which we indented now for ignoring its // children to avoid using
    in it. indentedListItemElement = std::move(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 (blockquoteElement && HTMLEditUtils::GetInclusiveAncestorAnyTableElement( *blockquoteElement) != HTMLEditUtils::GetInclusiveAncestorAnyTableElement(content)) { blockquoteElement = nullptr; } if (!blockquoteElement) { // First, check that our element can contain a blockquote. if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(), *nsGkAtoms::blockquote)) { // XXX This is odd, why do we stop indenting remaining content nodes? // Perhaps, `continue` is better. nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RestoreSavedRangesAndCollapseInLatestBlockElement" "IfOutside() failed"); return rv; } Result createNewBlockquoteElementResult = InsertElementWithSplittingAncestorsWithTransaction( *nsGkAtoms::blockquote, atContent, BRElementNextToSplitPoint::Keep, aEditingHost); if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) { NS_WARNING( "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" "nsGkAtoms::blockquote) failed"); return createNewBlockquoteElementResult.unwrapErr(); } CreateElementResult unwrappedCreateNewBlockquoteElementResult = createNewBlockquoteElementResult.unwrap(); if (unwrappedCreateNewBlockquoteElementResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedCreateNewBlockquoteElementResult.UnwrapCaretPoint(); } MOZ_ASSERT(unwrappedCreateNewBlockquoteElementResult.GetNewNode()); blockquoteElement = unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode(); latestNewBlockElement = blockquoteElement; } // tuck the node into the end of the active blockquote // MOZ_KnownLive because 'arrayOfContents' is guaranteed to // keep it alive. Result moveNodeResult = MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *blockquoteElement); if (MOZ_UNLIKELY(moveNodeResult.isErr())) { NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed"); return moveNodeResult.unwrapErr(); } MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) { pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint(); } subListElement = nullptr; } nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside(); NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside() failed"); return rv; } Result HTMLEditor::OutdentAsSubAction( const Element& aEditingHost) { 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 Err(ignoredError.StealNSResult()); } NS_WARNING_ASSERTION( !ignoredError.Failed(), "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); { Result result = CanHandleHTMLEditSubAction(); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed"); return result; } if (result.inspect().Canceled()) { return result; } } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Some selection containers are not content node, but ignored"); return EditActionResult::IgnoredResult(); } Result result = HandleOutdentAtSelection(aEditingHost); if (MOZ_UNLIKELY(result.isErr())) { NS_WARNING("HTMLEditor::HandleOutdentAtSelection() failed"); return result; } if (result.inspect().Canceled()) { return result; } if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) { NS_WARNING("Mutation event listener might have changed the selection"); return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection(); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() " "failed"); return Err(rv); } return result; } Result HTMLEditor::HandleOutdentAtSelection( const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(!IsSelectionRangeContainerNotContent()); // XXX Why do we do this only when there is only one selection range? if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) { Result extendedRange = GetRangeExtendedToHardLineEdgesForBlockEditAction( SelectionRef().GetRangeAt(0u), aEditingHost); if (MOZ_UNLIKELY(extendedRange.isErr())) { NS_WARNING( "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() " "failed"); return extendedRange.propagateErr(); } // Note that end point may be prior to start point. So, we // cannot use Selection::SetStartAndEndInLimit() here. IgnoredErrorResult error; SelectionRef().SetBaseAndExtentInLimiter( extendedRange.inspect().StartRef().ToRawRangeBoundary(), extendedRange.inspect().EndRef().ToRawRangeBoundary(), error); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (MOZ_UNLIKELY(error.Failed())) { NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed"); return Err(error.StealNSResult()); } } // HandleOutdentAtSelectionInternal() creates AutoSelectionRestorer. // Therefore, even if it returns NS_OK, the editor might have been destroyed // at restoring Selection. Result outdentResult = HandleOutdentAtSelectionInternal(aEditingHost); MOZ_ASSERT_IF(outdentResult.isOk(), !outdentResult.inspect().HasCaretPointSuggestion()); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } if (MOZ_UNLIKELY(outdentResult.isErr())) { NS_WARNING("HTMLEditor::HandleOutdentAtSelectionInternal() failed"); return outdentResult.propagateErr(); } SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap(); // Make sure selection didn't stick to last piece of content in old bq (only // a problem for collapsed selections) if (!unwrappedOutdentResult.GetLeftContent() && !unwrappedOutdentResult.GetRightContent()) { return EditActionResult::HandledResult(); } if (!SelectionRef().IsCollapsed()) { return EditActionResult::HandledResult(); } // Push selection past end of left element of last split indented element. if (unwrappedOutdentResult.GetLeftContent()) { const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionResult::HandledResult(); } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return Err(NS_ERROR_FAILURE); } if (atStartOfSelection.Container() == unwrappedOutdentResult.GetLeftContent() || EditorUtils::IsDescendantOf(*atStartOfSelection.Container(), *unwrappedOutdentResult.GetLeftContent())) { // Selection is inside the left node - push it past it. EditorRawDOMPoint afterRememberedLeftBQ( EditorRawDOMPoint::After(*unwrappedOutdentResult.GetLeftContent())); NS_WARNING_ASSERTION( afterRememberedLeftBQ.IsSet(), "Failed to set after remembered left blockquote element"); nsresult rv = CollapseSelectionTo(afterRememberedLeftBQ); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(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 (unwrappedOutdentResult.GetRightContent()) { const nsRange* firstRange = SelectionRef().GetRangeAt(0); if (NS_WARN_IF(!firstRange)) { return EditActionResult::HandledResult(); } const RangeBoundary& atStartOfSelection = firstRange->StartRef(); if (NS_WARN_IF(!atStartOfSelection.IsSet())) { return Err(NS_ERROR_FAILURE); } if (atStartOfSelection.Container() == unwrappedOutdentResult.GetRightContent() || EditorUtils::IsDescendantOf( *atStartOfSelection.Container(), *unwrappedOutdentResult.GetRightContent())) { // Selection is inside the right element - push it before it. EditorRawDOMPoint atRememberedRightBQ( unwrappedOutdentResult.GetRightContent()); nsresult rv = CollapseSelectionTo(atRememberedRightBQ); if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING_ASSERTION( NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed, but ignored"); } } return EditActionResult::HandledResult(); } Result HTMLEditor::HandleOutdentAtSelectionInternal(const Element& aEditingHost) { 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; { AutoRangeArray extendedSelectionRanges(SelectionRef()); extendedSelectionRanges.ExtendRangesToWrapLines( EditSubAction::eOutdent, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost); nsresult rv = extendedSelectionRanges.CollectEditTargetNodes( *this, arrayOfContents, EditSubAction::eOutdent, AutoRangeArray::CollectNonEditableNodes::Yes); if (NS_FAILED(rv)) { NS_WARNING( "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eOutdent, " "CollectNonEditableNodes::Yes) failed"); return Err(rv); } Result splitAtBRElementsResult = MaybeSplitElementsAtEveryBRElement(arrayOfContents, EditSubAction::eOutdent); if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { NS_WARNING( "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" "eOutdent) failed"); return splitAtBRElementsResult.propagateErr(); } if (AllowsTransactionsToChangeSelection() && splitAtBRElementsResult.inspect().IsSet()) { nsresult rv = CollapseSelectionTo(splitAtBRElementsResult.inspect()); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(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) { NS_WARNING_ASSERTION(indentedParentElement == content, "Indented parent element is not the
    "); Result outdentResult = OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith, aEditingHost); if (MOZ_UNLIKELY(outdentResult.isErr())) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap(); unwrappedOutdentResult.IgnoreCaretPointSuggestion(); leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent(); middleContentOfLastOutdented = unwrappedOutdentResult.UnwrapMiddleContent(); rightContentOfLastOutdented = unwrappedOutdentResult.UnwrapRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; indentedParentIndentedWith = BlockIndentedWith::HTML; } Result unwrapBlockquoteElementResult = RemoveBlockContainerWithTransaction( MOZ_KnownLive(*content->AsElement())); if (MOZ_UNLIKELY(unwrapBlockquoteElementResult.isErr())) { NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed"); return unwrapBlockquoteElementResult.propagateErr(); } const EditorDOMPoint& pointToPutCaret = unwrapBlockquoteElementResult.inspect(); if (AllowsTransactionsToChangeSelection() && pointToPutCaret.IsSet()) { nsresult rv = CollapseSelectionTo(pointToPutCaret); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(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, BlockInlineCheck::UseHTMLDefaultStyle)) { nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(MOZ_KnownLive(content)); if (NS_WARN_IF(Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } nsAutoString value; DebugOnly rvIgnored = CSSEditUtils::GetSpecifiedProperty(content, marginProperty, value); if (NS_WARN_IF(Destroyed())) { return Err(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 margin. if (startMargin > 0) { const Result pointToPutCaretOrError = ChangeMarginStart(MOZ_KnownLive(*content->AsElement()), ChangeMargin::Decrease, aEditingHost); if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) { if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) { return Err(NS_ERROR_EDITOR_DESTROYED); } NS_WARNING( "HTMLEditor::ChangeMarginStart(ChangeMargin::Decrease) failed, " "but ignored"); } else if (AllowsTransactionsToChangeSelection() && pointToPutCaretOrError.inspect().IsSet()) { nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect()); if (NS_FAILED(rv)) { NS_WARNING("EditorBase::CollapseSelectionTo() failed"); return Err(rv); } } 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) { Result outdentResult = OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith, aEditingHost); if (MOZ_UNLIKELY(outdentResult.isErr())) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap(); unwrappedOutdentResult.IgnoreCaretPointSuggestion(); leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent(); middleContentOfLastOutdented = unwrappedOutdentResult.UnwrapMiddleContent(); rightContentOfLastOutdented = unwrappedOutdentResult.UnwrapRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; indentedParentIndentedWith = BlockIndentedWith::HTML; } // XXX `content` could become different element since // `OutdentPartOfBlock()` may run mutation event listeners. nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()), LiftUpFromAllParentListElements::No); if (NS_FAILED(rv)) { NS_WARNING( "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:" ":No) failed"); return Err(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; } Result outdentResult = OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented, *lastContentToBeOutdented, indentedParentIndentedWith, aEditingHost); if (MOZ_UNLIKELY(outdentResult.isErr())) { NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed"); return outdentResult; } SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap(); unwrappedOutdentResult.IgnoreCaretPointSuggestion(); leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent(); middleContentOfLastOutdented = unwrappedOutdentResult.UnwrapMiddleContent(); rightContentOfLastOutdented = unwrappedOutdentResult.UnwrapRightContent(); indentedParentElement = nullptr; firstContentToBeOutdented = nullptr; lastContentToBeOutdented = nullptr; // curBlockIndentedWith = HTMLEditor::BlockIndentedWith::HTML; // Then, we need to look for next indentedParentElement. } indentedParentIndentedWith = BlockIndentedWith::HTML; for (nsCOMPtr parentContent = content->GetParent(); parentContent && !parentContent->IsHTMLElement(nsGkAtoms::body) && parentContent != &aEditingHost && (parentContent->IsHTMLElement(nsGkAtoms::table) || !HTMLEditUtils::IsAnyTableElement(parentContent)); parentContent = parentContent->GetParent()) { if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(*parentContent))) { continue; } // 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 Err(NS_ERROR_EDITOR_DESTROYED); } if (NS_WARN_IF(grandParentNode != parentContent->GetParentNode())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } nsAutoString value; DebugOnly rvIgnored = CSSEditUtils::GetSpecifiedProperty( *parentContent, marginProperty, value); if (NS_WARN_IF(Destroyed())) { return Err(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., `