From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- editor/libeditor/HTMLEditSubActionHandler.cpp | 11794 ++++++++++++++++++++++++ 1 file changed, 11794 insertions(+) create mode 100644 editor/libeditor/HTMLEditSubActionHandler.cpp (limited to 'editor/libeditor/HTMLEditSubActionHandler.cpp') diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp new file mode 100644 index 0000000000..8e89d26d46 --- /dev/null +++ b/editor/libeditor/HTMLEditSubActionHandler.cpp @@ -0,0 +1,11794 @@ +/* -*- 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/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::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 (IsInPlaintextMode()) { + // 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()) { + switch (GetTopLevelEditSubAction()) { + case EditSubAction::eInsertText: + case EditSubAction::eInsertTextComingFromIME: + case EditSubAction::eInsertLineBreak: + case EditSubAction::eInsertParagraphSeparator: + case EditSubAction::eDeleteText: { + // XXX We should investigate whether this is really needed because + // it seems that the following code does not handle the + // white-spaces. + RefPtr extendedChangedRange = + CreateRangeIncludingAdjuscentWhiteSpaces(changedRange); + if (extendedChangedRange) { + MOZ_ASSERT(extendedChangedRange->IsPositioned()); + // Use extended range temporarily. + TopLevelEditSubActionDataRef().mChangedRange = + std::move(extendedChangedRange); + } + break; + } + default: { + if (Element* editingHost = ComputeEditingHost()) { + if (RefPtr extendedChangedRange = AutoRangeArray:: + CreateRangeWrappingStartAndEndLinesContainingBoundaries( + changedRange, GetTopLevelEditSubAction(), + *editingHost)) { + 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})) { + 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, {}, 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); + const RefPtr parentBlockElementOfBRElement = + HTMLEditUtils::GetAncestorElement(*previousBRElement, + HTMLEditUtils::ClosestBlockElement); + + if (!blockElementAtSelectionStart || + blockElementAtSelectionStart != parentBlockElementOfBRElement) { + return NS_OK; + } + + // If we are here then the selection is right after a padding
+ // element for empty last line that is in the same block as the + // selection. We need to move the selection start to be before the + // padding
element. + EditorRawDOMPoint atInvisibleBRElement(previousBRElement); + nsresult rv = CollapseSelectionTo(atInvisibleBRElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; +} + +nsresult HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (mPaddingBRElementForEmptyEditor) { + return NS_OK; + } + + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eCreatePaddingBRElementForEmptyEditor, + nsIEditor::eNone, ignoredError); + if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) { + return ignoredError.StealNSResult(); + } + NS_WARNING_ASSERTION( + !ignoredError.Failed(), + "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored"); + ignoredError.SuppressException(); + + RefPtr rootElement = GetRoot(); + if (!rootElement) { + return NS_OK; + } + + // Now we've got the body element. Iterate over the body element's children, + // looking for editable content. If no editable content is found, insert the + // padding
element. + EditorType editorType = GetEditorType(); + bool isRootEditable = + EditorUtils::IsEditableContent(*rootElement, editorType); + for (nsIContent* rootChild = rootElement->GetFirstChild(); rootChild; + rootChild = rootChild->GetNextSibling()) { + if (EditorUtils::IsPaddingBRElementForEmptyEditor(*rootChild) || + !isRootEditable || + EditorUtils::IsEditableContent(*rootChild, editorType) || + HTMLEditUtils::IsBlockElement(*rootChild)) { + return NS_OK; + } + } + + // Skip adding the padding
element for empty editor if body + // is read-only. + if (IsHTMLEditor() && !HTMLEditUtils::IsSimplyEditableNode(*rootElement)) { + return NS_OK; + } + + // Create a br. + RefPtr newBRElement = CreateHTMLContent(nsGkAtoms::br); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + if (NS_WARN_IF(!newBRElement)) { + return NS_ERROR_FAILURE; + } + + mPaddingBRElementForEmptyEditor = + static_cast(newBRElement.get()); + + // Give it a special attribute. + newBRElement->SetFlags(NS_PADDING_FOR_EMPTY_EDITOR); + + // Put the node in the document. + Result insertBRElementResult = + InsertNodeWithTransaction(*newBRElement, + EditorDOMPoint(rootElement, 0u)); + if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertBRElementResult.unwrapErr(); + } + + // Set selection. + insertBRElementResult.inspect().IgnoreCaretPointSuggestion(); + nsresult rv = CollapseSelectionToStartOf(*rootElement); + if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { + NS_WARNING( + "EditorBase::CollapseSelectionToStartOf() caused destroying the " + "editor"); + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionToStartOf() failed, but ignored"); + return NS_OK; +} + +nsresult HTMLEditor::EnsureNoPaddingBRElementForEmptyEditor() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (!mPaddingBRElementForEmptyEditor) { + return NS_OK; + } + + // If we're an HTML editor, a mutation event listener may recreate padding + //
element for empty editor again during the call of + // DeleteNodeWithTransaction(). So, move it first. + RefPtr paddingBRElement( + std::move(mPaddingBRElementForEmptyEditor)); + nsresult rv = DeleteNodeWithTransaction(*paddingBRElement); + 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 || IsInPlaintextMode()) { + while (pos != -1 && + pos < AssertedCast(aInsertionString.Length())) { + int32_t oldPos = pos; + int32_t subStrLen; + pos = aInsertionString.FindChar(nsCRT::LF, oldPos); + + if (pos != -1) { + subStrLen = pos - oldPos; + // if first char is newline, then use just it + if (!subStrLen) { + subStrLen = 1; + } + } else { + subStrLen = aInsertionString.Length() - oldPos; + pos = aInsertionString.Length(); + } + + nsDependentSubstring subStr(aInsertionString, oldPos, subStrLen); + + // is it a return? + if (subStr.Equals(newlineStr)) { + 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()); + // Next inserting text should be inserted into styled inline elements if + // they have first visible thing in the new line. + auto pointToPutCaret = [&]() -> EditorDOMPoint { + WSScanResult forwardScanResult = + WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + editingHost, EditorRawDOMPoint::After( + *unwrappedInsertBRElementResult.GetNewNode())); + if (forwardScanResult.InVisibleOrCollapsibleCharacters()) { + unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); + return forwardScanResult.Point(); + } + if (forwardScanResult.ReachedSpecialContent()) { + unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); + return forwardScanResult.PointAtContent(); + } + return unwrappedInsertBRElementResult.UnwrapCaretPoint(); + }(); + + 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(aEditingHost) || + HTMLEditUtils::ShouldInsertLinefeedCharacter( + aCandidatePointToSplit, 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)) { + 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); + + // 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); + Result, nsresult> suggestBlockElementToPutCaretOrError = + FormatBlockContainerWithTransaction( + selectionRanges, + MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator)), + 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); + 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})) { + 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()); + + bool brElementIsAfterBlock = false, brElementIsBeforeBlock = false; + + // First, insert a
element. + RefPtr brElement; + if (IsInPlaintextMode()) { + 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); + WSRunScanner wsRunScanner(&aEditingHost, pointToBreak); + WSScanResult backwardScanResult = + wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToBreak); + if (MOZ_UNLIKELY(backwardScanResult.Failed())) { + NS_WARNING( + "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed"); + return Err(NS_ERROR_FAILURE); + } + brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary(); + WSScanResult forwardScanResult = + wsRunScanner.ScanNextVisibleNodeOrBlockBoundaryFrom(pointToBreak); + if (MOZ_UNLIKELY(forwardScanResult.Failed())) { + NS_WARNING( + "WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); + return Err(NS_ERROR_FAILURE); + } + brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary(); + // If the container of the break is a link, we need to split it and + // insert new
between the split links. + RefPtr linkNode = + HTMLEditor::GetLinkElement(pointToBreak.GetContainer()); + if (linkNode) { + 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); + } + + 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(brElement, + InterlinePosition::StartOfNextLine); + return CreateElementResult(std::move(brElement), + std::move(pointToPutCaret)); + } + + auto afterBRElement = EditorDOMPoint::After(brElement); + WSScanResult forwardScanFromAfterBRElementResult = + WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(&aEditingHost, + afterBRElement); + 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"); + } + } + + // We want the caret to stick to whatever is past the break. This is because + // the break is on the same line we were on, but the next content will be on + // the following line. + + // An exception to this is if the break has a next sibling that is a block + // node. Then we stick to the left to avoid an uber caret. + nsIContent* nextSiblingOfBRElement = brElement->GetNextSibling(); + afterBRElement.SetInterlinePosition( + nextSiblingOfBRElement && + HTMLEditUtils::IsBlockElement(*nextSiblingOfBRElement) + ? InterlinePosition::EndOfLine + : InterlinePosition::StartOfNextLine); + return CreateElementResult(std::move(brElement), afterBRElement); +} + +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); + if (wsScannerAtCaret.StartsFromPreformattedLineBreak() && + wsScannerAtCaret.EndsByBlockBoundary() && + 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), + "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. + WSScanResult forwardScanFromPointToSplitResult = + WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(&aEditingHost, + pointToSplit); + 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.PointAfterContent(); + } + + 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::IsInlineElement(aMailCiteElement)) { + 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()? + WSScanResult backwardScanFromPointToCreateNewBRElementResult = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + &aEditingHost, pointToCreateNewBRElement); + if (MOZ_UNLIKELY( + backwardScanFromPointToCreateNewBRElementResult.Failed())) { + NS_WARNING( + "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() " + "failed"); + return NS_ERROR_FAILURE; + } + if (!backwardScanFromPointToCreateNewBRElementResult + .InVisibleOrCollapsibleCharacters() && + !backwardScanFromPointToCreateNewBRElementResult + .ReachedSpecialContent()) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + WSScanResult forwardScanFromPointAfterNewBRElementResult = + WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + &aEditingHost, + EditorRawDOMPoint::After(pointToCreateNewBRElement)); + if (MOZ_UNLIKELY(forwardScanFromPointAfterNewBRElementResult.Failed())) { + NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); + return NS_ERROR_FAILURE; + } + if (!forwardScanFromPointAfterNewBRElementResult + .InVisibleOrCollapsibleCharacters() && + !forwardScanFromPointAfterNewBRElementResult + .ReachedSpecialContent() && + // In case we're at the very end. + !forwardScanFromPointAfterNewBRElementResult + .ReachedCurrentBlockBoundary()) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + 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)) { + // 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)) { + // 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); + 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); + 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); + const EditorDOMPointInText followingCharPoint = + WSRunScanner::GetInclusiveNextEditableCharPoint(editingHost, + aEndToDelete); + // Blink-compat: Normalize white-spaces in first node only when not removing + // its last character or no text nodes follow the first node. + // If removing last character of first node and there are + // following text nodes, white-spaces in following text node are + // normalized instead. + const bool removingLastCharOfStartNode = + aStartToDelete.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)) { + Element* editingHost = ComputeEditingHost(); + // Try to put caret next to immediately after previous editable leaf. + nsIContent* previousContent = + HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + newCaretPosition, *editableBlockElementOrInlineEditingHost, + {LeafNodeType::LeafNodeOrNonEditableNode}, editingHost); + if (previousContent && !HTMLEditUtils::IsBlockElement(*previousContent)) { + newCaretPosition = + previousContent->IsText() || + HTMLEditUtils::IsContainerNode(*previousContent) + ? EditorDOMPoint::AtEndOf(*previousContent) + : EditorDOMPoint::After(*previousContent); + } + // But if the point is very first of a block element or immediately after + // a child block, look for next editable leaf instead. + else if (nsIContent* nextContent = + HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + newCaretPosition, + *editableBlockElementOrInlineEditingHost, + {LeafNodeType::LeafNodeOrNonEditableNode}, + editingHost)) { + newCaretPosition = nextContent->IsText() || + HTMLEditUtils::IsContainerNode(*nextContent) + ? EditorDOMPoint(nextContent, 0) + : EditorDOMPoint(nextContent); + } + } + } + + // For compatibility with Blink, we should move caret to end of previous + // text node if it's direct previous sibling of the first text node in the + // range. + if (newCaretPosition.IsStartOfContainer() && + newCaretPosition.IsInTextNode() && + newCaretPosition.GetContainer()->GetPreviousSibling() && + newCaretPosition.GetContainer()->GetPreviousSibling()->IsText()) { + newCaretPosition.SetToEndOf( + newCaretPosition.GetContainer()->GetPreviousSibling()->AsText()); + } + + { + AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(), + &newCaretPosition); + 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())) { + return CaretPoint(EditorDOMPoint()); + } + + WSRunScanner wsRunScanner(ComputeEditingHost(), aPointToInsert); + // If the point is not start of a hard line, we don't need to put a `
` + // element here. + if (!wsRunScanner.StartsFromHardLineBreak()) { + 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()) { + 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eCreateOrChangeList, aEditingHost); + Result splitResult = + extendedRanges.SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + aHTMLEditor, 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})) { + 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}, + 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}))) { + 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. + // XXX Despite the name, HTMLEditUtils::IsInlineElement() returns true for + // non-element content nodes too. + if (HTMLEditUtils::IsInlineElement(aHandlingContent)) { + 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::IgnoreEditableState, + 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::IsInlineElement(aHandlingInlineContent)); + + // 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::IsInlineElement(aHandlingContent)) { + 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 + .ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eCreateOrChangeList, aEditingHost); + Result splitResult = + extendedSelectionRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, 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, nsAtom& blockType, + 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eCreateOrRemoveBlock, aEditingHost); + Result splitResult = + aSelectionRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, aEditingHost); + if (MOZ_UNLIKELY(splitResult.isErr())) { + NS_WARNING( + "AutoRangeArray::" + "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed"); + return splitResult.propagateErr(); + } + nsresult rv = aSelectionRanges.CollectEditTargetNodes( + *this, arrayOfContents, EditSubAction::eCreateOrRemoveBlock, + AutoRangeArray::CollectNonEditableNodes::Yes); + if (NS_FAILED(rv)) { + NS_WARNING( + "AutoRangeArray::CollectEditTargetNodes(EditSubAction::" + "eCreateOrRemoveBlock, CollectNonEditableNodes::No) failed"); + return Err(rv); + } + + Result splitAtBRElementsResult = + MaybeSplitElementsAtEveryBRElement(arrayOfContents, + EditSubAction::eCreateOrRemoveBlock); + if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { + NS_WARNING( + "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" + "eCreateOrRemoveBlock) 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)) { + if (NS_WARN_IF(aSelectionRanges.Ranges().IsEmpty())) { + return Err(NS_ERROR_FAILURE); + } + + auto pointToInsertBlock = + aSelectionRanges.GetFirstRangeStartPoint(); + if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { + if (!pointToInsertBlock.IsInContentNode()) { + NS_WARNING( + "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " + "block parent because container of the point is not content"); + return Err(NS_ERROR_FAILURE); + } + // We are removing blocks (going to "body text") + const RefPtr editableBlockElement = + HTMLEditUtils::GetInclusiveAncestorElement( + *pointToInsertBlock.ContainerAs(), + HTMLEditUtils::ClosestEditableBlockElement); + if (!editableBlockElement) { + NS_WARNING( + "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " + "block parent"); + return Err(NS_ERROR_FAILURE); + } + if (!HTMLEditUtils::IsFormatNode(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}, + &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}, + &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( + blockType, pointToInsertBlock, BRElementNextToSplitPoint::Keep, + aEditingHost); + if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) { + NS_WARNING( + nsPrintfCString( + "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" + "%s) failed", + nsAtomCString(&blockType).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(); + } + // 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 (&blockType == 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 (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { + Result removeBlockContainerElementsResult = + RemoveBlockContainerElementsWithTransaction(arrayOfContents); + if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) { + NS_WARNING( + "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed"); + return removeBlockContainerElementsResult.propagateErr(); + } + return RefPtr(); + } + Result wrapContentsInBlockElementResult = + CreateOrChangeBlockContainerElement(arrayOfContents, blockType, + aEditingHost); + if (MOZ_UNLIKELY(wrapContentsInBlockElementResult.isErr())) { + NS_WARNING("HTMLEditor::CreateOrChangeBlockContainerElement() 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); + if (editableBlockElement && + HTMLEditUtils::IsListItem(editableBlockElement)) { + arrayOfContents.AppendElement(*editableBlockElement); + } + } + + EditorDOMPoint pointToPutCaret; + if (arrayOfContents.IsEmpty()) { + { + AutoRangeArray extendedRanges(aRanges); + extendedRanges.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eIndent, aEditingHost); + Result splitResult = + extendedRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, 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)) { + 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)) { + 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eIndent, aEditingHost); + Result splitResult = + extendedRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, 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)) { + const EditorDOMPoint pointToInsertBlockquoteElement = + pointToPutCaret.IsSet() + ? std::move(pointToPutCaret) + : EditorBase::GetFirstSelectionStartPoint(); + if (NS_WARN_IF(!pointToInsertBlockquoteElement.IsSet())) { + return NS_ERROR_FAILURE; + } + + // Make sure we can put a block here. + 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 (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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eOutdent, 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)) { + 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., `