Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  HTMLEditSubActionHandler.cpp

  Sprache: C
 

/* 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 "EditorBase.h"
#include "HTMLEditor.h"
#include "HTMLEditorInlines.h"
#include "HTMLEditorNestedClasses.h"

#include <fmt/format.h>
#include <utility>

#include "AutoClonedRangeArray.h"
#include "AutoSelectionRestorer.h"
#include "CSSEditUtils.h"
#include "EditAction.h"
#include "EditorDOMPoint.h"
#include "EditorLineBreak.h"
#include "EditorUtils.h"
#include "HTMLEditHelpers.h"
#include "HTMLEditUtils.h"
#include "PendingStyles.h"  // for SpecifiedStyle
#include "WhiteSpaceVisibilityKeeper.h"
#include "WSRunScanner.h"

#include "ErrorList.h"
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/EditorForwards.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/Logging.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Maybe.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_editor.h"
#include "mozilla/TextComposition.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/EditContext.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/ElementInlines.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/RangeBinding.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/StaticRange.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 "nsIContent.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"

class nsISupports;

namespace mozilla {

extern LazyLogModule gTextInputLog;  // Defined in EditorBase.cpp

using namespace dom;
using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions;
using LeafNodeOption = HTMLEditUtils::LeafNodeOption;
using LeafNodeOptions = HTMLEditUtils::LeafNodeOptions;
using WalkTextOption = HTMLEditUtils::WalkTextOption;
using WalkTreeDirection = HTMLEditUtils::WalkTreeDirection;

/********************************************************
 *  first some helpful functors we will use
 ********************************************************/


static bool IsPendingStyleCachePreservingSubAction(
    EditSubAction aEditSubAction) {
  switch (aEditSubAction) {
    case EditSubAction::eDeleteSelectedContent:
    case EditSubAction::eInsertLineBreak:
    case EditSubAction::eInsertParagraphSeparator:
    case EditSubAction::eCreateOrChangeList:
    case EditSubAction::eIndent:
    case EditSubAction::eOutdent:
    case EditSubAction::eSetOrClearAlignment:
    case EditSubAction::eCreateOrRemoveBlock:
    case EditSubAction::eFormatBlockForHTMLCommand:
    case EditSubAction::eMergeBlockContents:
    case EditSubAction::eRemoveList:
    case EditSubAction::eCreateOrChangeDefinitionListItem:
    case EditSubAction::eInsertElement:
    case EditSubAction::eInsertQuotation:
    case EditSubAction::eInsertQuotedText:
      return true;
    default:
      return false;
  }
}

template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const EditorDOMRange& aRange);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const EditorRawDOMRange& aRange);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const EditorRawDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const EditorDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint);

nsresult HTMLEditor::InitEditorContentAndSelection() {
  MOZ_ASSERT(IsEditActionDataAvailable());

  // We should do nothing with the result of GetRoot() if only a part of the
  // document is editable.
  if (!EntireDocumentIsEditable()) {
    return NS_OK;
  }

  nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor();
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() failed");
    return rv;
  }

  // If the selection hasn't been set up yet, set it up collapsed to the end of
  // our editable content.
  // XXX I think that this shouldn't do it in `HTMLEditor` because it maybe
  //     removed by the web app and if they call `Selection::AddRange()` without
  //     checking the range count, it may cause multiple selection ranges.
  if (!SelectionRef().RangeCount()) {
    nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument();
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() "
          "failed");
      return rv;
    }
  }

  if (IsPlaintextMailComposer()) {
    // XXX Should we do this in HTMLEditor?  It's odd to guarantee that last
    //     empty line is visible only when it's in the plain text mode.
    nsresult rv = EnsurePaddingBRElementInMultilineEditor();
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "EditorBase::EnsurePaddingBRElementInMultilineEditor() failed");
      return rv;
    }
  }

  Element* bodyOrDocumentElement = GetRoot();
  if (NS_WARN_IF(!bodyOrDocumentElement && !GetDocument())) {
    return NS_ERROR_FAILURE;
  }

  if (!bodyOrDocumentElement) {
    return NS_OK;
  }

  // FIXME: This is odd to update the DOM for making users can put caret in
  // empty table cells and list items.  We should make it possible without
  // the hacky <br>.
  rv = InsertBRElementToEmptyListItemsAndTableCellsInRange(
      RawRangeBoundary::StartOfParent(*bodyOrDocumentElement),
      RawRangeBoundary::EndOfParent(*bodyOrDocumentElement));
  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> 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<EditorRawDOMPoint>();
  if (atCompositionStart.IsSet()) {
    // If there is composition string, let's remember current composition
    // range.
    TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(
        atCompositionStart, GetLastIMESelectionEndPoint<EditorRawDOMPoint>());
  } 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<Element> editingHost =
        ComputeEditingHost(LimitInBodyElement::No);
    if (NS_WARN_IF(!editingHost)) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }

    nsIContent* const startContainer =
        HTMLEditUtils::GetContentToPreserveInlineStyles(
            TopLevelEditSubActionDataRef()
                .mSelectedRange->StartPoint<EditorRawDOMPoint>(),
            *editingHost);
    if (NS_WARN_IF(!startContainer)) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
    if (const RefPtr<Element> 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() failed");
    // 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<nsresult> 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());

  // If we just maintained the DOM tree for consistent behavior even after
  // web apps modified the DOM, we should not touch the DOM in this
  // post-processor.
  if (GetTopLevelEditSubAction() ==
      EditSubAction::eMaintainWhiteSpaceVisibility) {
    return NS_OK;
  }

  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");

  if (GetTopLevelEditSubAction() ==
      EditSubAction::eCreatePaddingBRElementForEmptyEditor) {
    return NS_OK;
  }

  if (TopLevelEditSubActionDataRef().mChangedRange->IsPositioned() &&
      GetTopLevelEditSubAction() != EditSubAction::eUndo &&
      GetTopLevelEditSubAction() != EditSubAction::eRedo) {
    // don't let any txns in here move the selection around behind our back.
    // Note that this won't prevent explicit selection setting from working.
    AutoTransactionsConserveSelection dontChangeMySelection(*this);

    {
      EditorDOMRange changedRange(
          *TopLevelEditSubActionDataRef().mChangedRange);
      if (changedRange.IsPositioned() &&
          changedRange.EnsureNotInNativeAnonymousSubtree()) {
        bool isBlockLevelSubAction = false;
        switch (GetTopLevelEditSubAction()) {
          case EditSubAction::eInsertText:
          case EditSubAction::eInsertTextComingFromIME:
          case EditSubAction::eInsertLineBreak:
          case EditSubAction::eInsertParagraphSeparator:
          case EditSubAction::eDeleteText: {
            // XXX We should investigate whether this is really needed because
            //     it seems that the following code does not handle the
            //     white-spaces.
            RefPtr<nsRange> extendedChangedRange =
                CreateRangeIncludingAdjuscentWhiteSpaces(changedRange);
            if (extendedChangedRange) {
              MOZ_ASSERT(extendedChangedRange->IsPositioned());
              // Use extended range temporarily.
              TopLevelEditSubActionDataRef().mChangedRange =
                  std::move(extendedChangedRange);
            }
            break;
          }
          case EditSubAction::eCreateOrChangeList:
          case EditSubAction::eCreateOrChangeDefinitionListItem:
          case EditSubAction::eRemoveList:
          case EditSubAction::eFormatBlockForHTMLCommand:
          case EditSubAction::eCreateOrRemoveBlock:
          case EditSubAction::eIndent:
          case EditSubAction::eOutdent:
          case EditSubAction::eSetOrClearAlignment:
          case EditSubAction::eSetPositionToAbsolute:
          case EditSubAction::eSetPositionToStatic:
          case EditSubAction::eDecreaseZIndex:
          case EditSubAction::eIncreaseZIndex:
            isBlockLevelSubAction = true;
            [[fallthrough]];
          default: {
            Element* editingHost = ComputeEditingHost();
            if (MOZ_UNLIKELY(!editingHost)) {
              break;
            }
            RefPtr<nsRange> extendedChangedRange = AutoClonedRangeArray::
                CreateRangeWrappingStartAndEndLinesContainingBoundaries(
                    changedRange, GetTopLevelEditSubAction(),
                    isBlockLevelSubAction
                        ? BlockInlineCheck::UseHTMLDefaultStyle
                        : BlockInlineCheck::UseComputedDisplayOutsideStyle,
                    *editingHost);
            if (!extendedChangedRange) {
              break;
            }
            MOZ_ASSERT(extendedChangedRange->IsPositioned());
            // Use extended range temporarily.
            TopLevelEditSubActionDataRef().mChangedRange =
                std::move(extendedChangedRange);
            break;
          }
        }
      }
    }

    // if we did a ranged deletion or handling backspace key, make sure we have
    // a place to put caret.
    // Note we only want to do this if the overall operation was deletion,
    // not if deletion was done along the way for
    // EditSubAction::eInsertHTMLSource, EditSubAction::eInsertText, etc.
    // That's why this is here rather than DeleteSelectionAsSubAction().
    // However, we shouldn't insert <br> 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<EditorDOMPoint>();
      if (!newCaretPosition.IsSet()) {
        NS_WARNING("There was no selection range");
        return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
      }
      Element* const editingHost = ComputeEditingHost(LimitInBodyElement::No);
      if (!editingHost) [[unlikely]] {
        return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
      }
      Result<CreateLineBreakResult, nsresult>
          insertPaddingBRElementResultOrError =
              InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded(
                  newCaretPosition, *editingHost);
      if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
        NS_WARNING(
            "HTMLEditor::"
            "InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded() failed");
        return insertPaddingBRElementResultOrError.unwrapErr();
      }
      nsresult rv =
          insertPaddingBRElementResultOrError.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 <br>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;
      }
    }

    // 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::eInsertLineBreak:
        case EditSubAction::eInsertParagraphSeparator:
        case EditSubAction::ePasteHTMLContent:
        case EditSubAction::eInsertHTMLSource:
          // XXX AdjustCaretPositionAndEnsurePaddingBRElement() intentionally
          //     does not create padding `<br>` 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<Element> mostDistantEmptyInlineAncestor = nullptr;
      for (Element* ancestor :
           SelectionRef().GetFocusNode()->InclusiveAncestorsOfType<Element>()) {
        if (!ancestor->IsHTMLElement() ||
            !HTMLEditUtils::IsRemovableFromParentNode(*ancestor) ||
            !HTMLEditUtils::IsEmptyInlineContainer(
                *ancestor, {EmptyCheckOption::TreatSingleBRElementAsVisible},
                BlockInlineCheck::UseComputedDisplayStyle)) {
          break;
        }
        mostDistantEmptyInlineAncestor = ancestor;
      }
      if (mostDistantEmptyInlineAncestor) {
        nsresult rv =
            DeleteNodeWithTransaction(*mostDistantEmptyInlineAncestor);
        if (NS_FAILED(rv)) {
          NS_WARNING(
              "EditorBase::DeleteNodeWithTransaction() failed at deleting "
              "empty inline ancestors");
          return rv;
        }
      }
    }

    // But the cached inline styles should be restored from type-in-state later.
    if (reapplyCachedStyle) {
      DebugOnly<nsresult> 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();
    }
  }

  // Browser should not handle spell check for EditContext
  if (!GetEditContext()) {
    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 <br> element is removed because
  //     I don't see the <br> 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<EditActionResult, nsresult> HTMLEditor::CanHandleHTMLEditSubAction(
    CheckSelectionInReplacedElement
        aCheckSelectionInReplacedElement /* = ::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);
  }

  using ReplaceOrVoidElementOption = HTMLEditUtils::ReplaceOrVoidElementOption;

  if (selStartNode == selEndNode) {
    if (aCheckSelectionInReplacedElement ==
            CheckSelectionInReplacedElement::Yes &&
        HTMLEditUtils::GetInclusiveAncestorReplacedOrVoidElement(
            *selStartNode->AsContent(),
            ReplaceOrVoidElementOption::LookForOnlyNonVoidReplacedElement)) {
      return EditActionResult::CanceledResult();
    }
    return EditActionResult::IgnoredResult();
  }

  if (aCheckSelectionInReplacedElement != CheckSelectionInReplacedElement::No &&
      (HTMLEditUtils::GetInclusiveAncestorReplacedOrVoidElement(
           *selStartNode->AsContent(),
           ReplaceOrVoidElementOption::LookForOnlyNonVoidReplacedElement) ||
       HTMLEditUtils::GetInclusiveAncestorReplacedOrVoidElement(
           *selEndNode->AsContent(),
           ReplaceOrVoidElementOption::LookForOnlyNonVoidReplacedElement))) {
    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<nsresult> 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(
    const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(SelectionRef().IsCollapsed());

  // If we are after a padding `<br>` 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;
  }

  const WSScanResult prevThing =
      WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
          {WSRunScanner::Option::StopAtVisibleEmptyInlineContainers},
          atSelectionStart, &aEditingHost);
  if (!prevThing.ReachedLineBreak()) {
    return NS_OK;
  }
  EditorRawLineBreak unnecessaryLineBreak =
      prevThing.CreateEditorLineBreak<EditorRawLineBreak>();
  if (!unnecessaryLineBreak.IsFollowedByBlockBoundary()) {
    return NS_OK;
  }
  if (!unnecessaryLineBreak.ContentRef().GetParent() ||
      !unnecessaryLineBreak.ContentRef().IsInclusiveDescendantOf(&aEditingHost))
      [[unlikely]] {
    return NS_OK;
  }
  if (unnecessaryLineBreak.IsPreformattedLineBreak() &&
      NS_WARN_IF(
          !HTMLEditUtils::IsSimplyEditableNode(
              unnecessaryLineBreak.ContentRef()) &&
          !unnecessaryLineBreak.IsPreformattedLineBreakAtStartOfText())) {
    // If the preceding unnecessary preformatted line break is a part of a
    // non-editable visible Text, we cannot put caret into it. Then, typing
    // something will cause a new line because the unnecessary line break
    // becomes visible.
    return NS_OK;
  }
  EditorRawDOMPoint pointToPutCaret =
      unnecessaryLineBreak.To<EditorRawDOMPoint>();
  for (nsIContent* container :
       pointToPutCaret.GetContainer()->InclusiveAncestorsOfType<nsIContent>()) {
    if (!HTMLEditUtils::IsSimplyEditableNode(*container)) [[unlikely]] {
      if (NS_WARN_IF(container->GetPreviousSibling())) {
        // If the non-editable node is not the first child, we need to put caret
        // too far. Therefore, we should keep current selection. Although typing
        // something will cause a new line because the unnecessary line break
        // becomes visible.
        return NS_OK;
      }
      continue;
    }
    if (container != pointToPutCaret.GetContainer()) {
      MOZ_ASSERT(container->GetFirstChild());
      MOZ_ASSERT(
          !HTMLEditUtils::IsSimplyEditableNode(*container->GetFirstChild()));
      pointToPutCaret = EditorRawDOMPoint(container, 0);
    }
    break;
  }
  MOZ_ASSERT(pointToPutCaret.IsSet());
  MOZ_ASSERT(
      pointToPutCaret.GetContainer()->IsInclusiveDescendantOf(&aEditingHost));

  // If we are here then the selection is right after a padding line break 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 line break node.
  // FIXME: We should return the position instead of updating the Selection.
  nsresult rv = CollapseSelectionTo(pointToPutCaret);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::CollapseSelectionTo() failed");
  return rv;
}

nsresult HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() {
  MOZ_ASSERT(IsEditActionDataAvailable());

  if (mPaddingBRElementForEmptyEditor) {
    return NS_OK;
  }

  // XXX I think that we should not insert a <br> element if we're for a web
  // content.  Probably, this is required only by chrome editors such as
  // the mail composer of Thunderbird and the composer of SeaMonkey.

  const RefPtr<Element> bodyOrDocumentElement = GetRoot();
  if (!bodyOrDocumentElement) {
    return NS_OK;
  }

  // Skip adding the padding <br> element for empty editor if body
  // is read-only.
  if (!HTMLEditUtils::IsSimplyEditableNode(*bodyOrDocumentElement)) {
    return NS_OK;
  }

  // Skip adding <br> element for EditContext editors.
  if (GetEditContext()) {
    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 <br> element.
  EditorType editorType = GetEditorType();
  bool isRootEditable =
      EditorUtils::IsEditableContent(*bodyOrDocumentElement, editorType);
  for (nsIContent* child = bodyOrDocumentElement->GetFirstChild(); child;
       child = child->GetNextSibling()) {
    if (EditorUtils::IsPaddingBRElementForEmptyEditor(*child) ||
        !isRootEditable || EditorUtils::IsEditableContent(*child, editorType) ||
        HTMLEditUtils::IsBlockElement(
            *child, BlockInlineCheck::UseComputedDisplayStyle)) {
      return NS_OK;
    }
  }

  IgnoredErrorResult ignoredError;
  AutoEditSubActionNotifier startToHandleEditSubAction(
      *this, EditSubAction::eCreatePaddingBRElementForEmptyEditor,
      nsIEditor::eNone, ignoredError);
  if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
    return ignoredError.StealNSResult();
  }
  NS_WARNING_ASSERTION(
      !ignoredError.Failed(),
      "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");

  Result<CreateElementResult, nsresult> insertPaddingBRElementResultOrError =
      InsertBRElement(WithTransaction::Yes,
                      BRElementType::PaddingForEmptyEditor,
                      EditorDOMPoint(bodyOrDocumentElement, 0u));
  if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
    NS_WARNING(
        "EditorBase::InsertBRElement(WithTransaction::Yes, "
        "BRElementType::PaddingForEmptyEditor) failed");
    return insertPaddingBRElementResultOrError.propagateErr();
  }
  CreateElementResult insertPaddingBRElementResult =
      insertPaddingBRElementResultOrError.unwrap();
  mPaddingBRElementForEmptyEditor =
      HTMLBRElement::FromNode(insertPaddingBRElementResult.GetNewNode());
  nsresult rv = insertPaddingBRElementResult.SuggestCaretPointTo(
      *this, {SuggestCaret::AndIgnoreTrivialError});
  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");
  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
  // <br> element for empty editor again during the call of
  // DeleteNodeWithTransaction().  So, move it first.
  RefPtr<HTMLBRElement> 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, {});
  if (firstLeafChild &&
      EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) {
    mPaddingBRElementForEmptyEditor =
        static_cast<HTMLBRElement*>(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<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
    const nsAString& aInsertionString, InsertTextFor aPurpose) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());

  MOZ_LOG(
      gTextInputLog, LogLevel::Info,
      ("%p HTMLEditor::HandleInsertText(aInsertionString=\"%s\", aPurpose=%s)",
       this, NS_ConvertUTF16toUTF8(aInsertionString).get(),
       ToString(aPurpose).c_str()));

  {
    Result<EditActionResult, nsresult> result =
        CanHandleHTMLEditSubAction(CheckSelectionInReplacedElement::No);
    if (MOZ_UNLIKELY(result.isErr())) {
      NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
      return result;
    }
    if (result.inspect().Canceled()) {
      return result;
    }
  }

  UndefineCaretBidiLevel();

  if (RefPtr editContext = GetEditContext()) {
    uint32_t start = editContext->SelectionStart();
    uint32_t end = editContext->SelectionEnd();
    if (InsertingTextForComposition(aPurpose)) {
      MOZ_ASSERT(mComposition);
      if (mComposition->GetContainerTextNode()) {
        start = mComposition->ClampedStartOffsetInTextNode();
        end = mComposition->ClampedEndOffsetInTextNode();
      }
      mComposition->OnUpdateCompositionInEditor(aInsertionString,
                                                editContext->TextNode(), start);
    }
    editContext->UpdateTextAndFireEvent(start, end, aInsertionString);
    if (NS_WARN_IF(Destroyed())) {
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
    if (editContext != GetEditContext()) {
      // textupdate handler deactivated this EditContext
      return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
    }
    return EditActionResult::HandledResult();
  }

  // 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() &&
      !InsertingTextForExtantComposition(aPurpose)) {
    nsresult rv =
        DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "EditorBase::DeleteSelectionAsSubAction(nsIEditor::eNone, "
          "nsIEditor::eNoStrip) failed");
      return Err(rv);
    }
  }

  const RefPtr<Element> editingHost =
      ComputeEditingHost(LimitInBodyElement::No);
  if (NS_WARN_IF(!editingHost)) {
    return Err(NS_ERROR_FAILURE);
  }

  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(*editingHost);
    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");
    }
  }

  EditorDOMPoint pointToInsert = [&]() {
    if (InsertingTextForExtantComposition(aPurpose)) {
      auto compositionStartPoint =
          GetFirstIMESelectionStartPoint<EditorDOMPoint>();
      if (MOZ_LIKELY(compositionStartPoint.IsSet())) {
        return compositionStartPoint;
      }
    }
    return GetFirstSelectionStartPoint<EditorDOMPoint>();
  }();

  MOZ_LOG(gTextInputLog, LogLevel::Info,
          ("%p HTMLEditor::HandleInsertText(), pointToInsert=%s"this,
           ToString(pointToInsert).c_str()));

  if (NS_WARN_IF(!pointToInsert.IsSet())) {
    return Err(NS_ERROR_FAILURE);
  }

  // for every property that is set, insert a new inline style node
  // XXX I think that if this is second or later composition update, we should
  // not change the style because we won't update composition with keeping
  // inline elements in composing range.
  Result<EditorDOMPoint, nsresult> 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.
  pointToInsert = HTMLEditUtils::GetPossiblePointToInsert(
      pointToInsert, *nsGkAtoms::textTagName, *editingHost);
  if (NS_WARN_IF(!pointToInsert.IsSet())) {
    return Err(NS_ERROR_FAILURE);
  }
  MOZ_ASSERT(pointToInsert.IsInContentNode());

  if (InsertingTextForComposition(aPurpose)) {
    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, nsresult> insertEmptyTextResultOrError =
          InsertTextWithTransaction(aInsertionString, pointToInsert,
                                    InsertTextTo::ExistingTextNodeIfAvailable);
      if (MOZ_UNLIKELY(insertEmptyTextResultOrError.isErr())) {
        NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
        return insertEmptyTextResultOrError.propagateErr();
      }
      InsertTextResult insertEmptyTextResult =
          insertEmptyTextResultOrError.unwrap();
      // InsertTextWithTransaction() doesn not suggest caret position if it's
      // called for IME composition. However, for the safety, let's ignore the
      // caret position explicitly.
      insertEmptyTextResult.IgnoreCaretPointSuggestion();
      nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
          insertEmptyTextResult.EndOfInsertedTextRef(),
          // When user inserting text, the web app may expect that nothing
          // extant content will be deleted. Therefore, we should preserve
          // preformatted linefeed at least.
          PreservePreformattedLineBreak::Yes, PaddingForEmptyBlock::Unnecessary,
          *editingHost);
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
        return Err(rv);
      }
      const EditorDOMPoint& endOfInsertedText =
          insertEmptyTextResult.EndOfInsertedTextRef();
      if (endOfInsertedText.IsInTextNode() &&
          !endOfInsertedText.IsStartOfContainer()) {
        nsresult rv = WhiteSpaceVisibilityKeeper::
            NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces(
                *this, endOfInsertedText.AsInText().PreviousPoint());
        if (NS_FAILED(rv)) {
          NS_WARNING(
              "WhiteSpaceVisibilityKeeper::"
              "NormalizeVisibleWhiteSpacesWithoutDeletingInvisibleWhiteSpaces()"
              " failed");
          return Err(rv);
        }
        if (NS_WARN_IF(
                !endOfInsertedText.IsInContentNodeAndValidInComposedDoc())) {
          return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
        }
      }
      // If we replaced non-empty composition string with an empty string,
      // its preceding character may be a collapsible ASCII white-space.
      // Therefore, we may need to insert a padding <br> after the white-space.
      Result<CreateLineBreakResult, nsresult>
          insertPaddingBRElementResultOrError = InsertPaddingBRElementIfNeeded(
              insertEmptyTextResult.EndOfInsertedTextRef(), nsIEditor::eNoStrip,
              *editingHost);
      if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
        NS_WARNING(
            "HTMLEditor::InsertPaddingBRElementIfNeeded(eNoStrip) failed");
        return insertPaddingBRElementResultOrError.propagateErr();
      }
      insertPaddingBRElementResultOrError.unwrap().IgnoreCaretPointSuggestion();
      // Then, collapse caret after the empty text inserted position, i.e.,
      // whether the removed composition string was.
      if (AllowsTransactionsToChangeSelection()) {
        nsresult rv = CollapseSelectionTo(endOfInsertedText);
        if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
          return Err(rv);
        }
        NS_WARNING_ASSERTION(
            rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
            "CaretPoint::SuggestCaretPointTo() failed, but ignored");
      }
      return EditActionResult::HandledResult();
    }

    EditorDOMPoint endOfInsertedText;
    {
      AutoTrackDOMPoint trackPointToInsert(RangeUpdaterRef(), &pointToInsert);
      const auto compositionEndPoint =
          GetLastIMESelectionEndPoint<EditorDOMPoint>();
      Result<InsertTextResult, nsresult> replaceTextResult =
          WhiteSpaceVisibilityKeeper::InsertOrUpdateCompositionString(
              *this, aInsertionString,
              compositionEndPoint.IsSet()
                  ? EditorDOMRange(pointToInsert, compositionEndPoint)
                  : EditorDOMRange(pointToInsert),
              aPurpose, *editingHost);
      if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
        NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed");
        return replaceTextResult.propagateErr();
      }
      InsertTextResult unwrappedReplaceTextResult = replaceTextResult.unwrap();
      endOfInsertedText = unwrappedReplaceTextResult.EndOfInsertedTextRef();
      if (InsertingTextForCommittingComposition(aPurpose)) {
        // If we're committing the composition,
        // WhiteSpaceVisibilityKeeper::InsertOrUpdateCompositionString() may
        // replace the last character of the composition string when it's a
        // white-space.  Then, Selection will be moved before the last
        // character.  So, we need to adjust Selection here.
        nsresult rv = unwrappedReplaceTextResult.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::SuggestCaretPoint() failed, but ignored");
      } else {
        // CompositionTransaction should've set selection so that we should
        // ignore caret suggestion.
        unwrappedReplaceTextResult.IgnoreCaretPointSuggestion();
      }
    }

    if (!InsertingTextForCommittingComposition(aPurpose)) {
      const auto newCompositionStartPoint =
          GetFirstIMESelectionStartPoint<EditorDOMPoint>();
      const auto newCompositionEndPoint =
          GetLastIMESelectionEndPoint<EditorDOMPoint>();
      if (NS_WARN_IF(!newCompositionStartPoint.IsSet()) ||
          NS_WARN_IF(!newCompositionEndPoint.IsSet())) {
        // Mutation event listener has changed the DOM tree...
        return EditActionResult::HandledResult();
      }
      nsresult rv =
          TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
              newCompositionStartPoint.ToRawRangeBoundary(),
              newCompositionEndPoint.ToRawRangeBoundary());
      if (NS_FAILED(rv)) {
        NS_WARNING("nsRange::SetStartAndEnd() failed");
        return Err(rv);
      }
    } else {
      if (NS_WARN_IF(!endOfInsertedText.IsSetAndValidInComposedDoc()) ||
          NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc())) {
        return EditActionResult::HandledResult();
      }
      nsresult rv =
          TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
              pointToInsert.ToRawRangeBoundary(),
              endOfInsertedText.ToRawRangeBoundary());
      if (NS_FAILED(rv)) {
        NS_WARNING("nsRange::SetStartAndEnd() failed");
        return Err(rv);
      }
    }
    return EditActionResult::HandledResult();
  }

  MOZ_ASSERT(!InsertingTextForComposition(aPurpose));

  // 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<nsIContent>());
  const Maybe<LineBreakType> lineBreakType = GetPreferredLineBreakType(
      *pointToInsert.ContainerAs<nsIContent>(), *editingHost);
  if (NS_WARN_IF(lineBreakType.isNothing())) {
    return Err(NS_ERROR_FAILURE);
  }

  // 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<bool> disableListener(
      EditSubActionDataRef().mAdjustChangedRangeFromListener);
  EditSubActionDataRef().mAdjustChangedRangeFromListener = false;

  // don't change my selection in subtransactions
  AutoTransactionsConserveSelection dontChangeMySelection(*this);
  {
    AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert);

    const auto GetInsertTextTo = [](int32_t aInclusiveNextLinefeedOffset,
                                    uint32_t aLineStartOffset) {
      if (aInclusiveNextLinefeedOffset > 0) {
        return aLineStartOffset > 0
                   // If we'll insert a <br> and we're inserting 2nd or later
                   // line, we should always create new `Text` since it'll be
                   // between 2 <br> elements.
                   ? InsertTextTo::AlwaysCreateNewTextNode
                   // If we'll insert a <br> and we're inserting first line,
                   // we should append text to preceding text node, but
                   // we don't want to insert it to a a following text node
                   // because of avoiding to split the `Text`.
                   : InsertTextTo::ExistingTextNodeIfAvailableAndNotStart;
      }
      // If we're inserting the last line, the text should be inserted to
      // start of the following `Text` if there is or middle of the `Text`
      // at insertion position if we're inserting only the line.
      return InsertTextTo::ExistingTextNodeIfAvailable;
    };

    // for efficiency, break out the pre case separately.  This is because
    // its a lot cheaper to search the input string for only newlines than
    // it is to search for both tabs and newlines.
    if (!isWhiteSpaceCollapsible || IsPlaintextMailComposer()) {
      if (!aInsertionString.IsEmpty()) [[likely]] {
        // If the inserting string is not empty, we need to delete padding
        // line break after the insertion point first because X (Twitter)
        // expects that character data change will be notified at last.
        const WSScanResult nextThing = HTMLEditUtils::
            ScanInclusiveNextThingWithIgnoringUnnecessaryLineBreak(
                currentPoint, PaddingForEmptyBlock::Unnecessary, *editingHost);
        if (nextThing.MaybeIgnoredLineBreak().isSome()) {
          const EditorLineBreak& lineBreak =
              nextThing.MaybeIgnoredLineBreak().ref();
          // When user inserting content, the web app may expect that nothing
          // extant content will be deleted. Therefore, we should preserve
          // preformatted linefeed at least. However, we should delete it if
          // it's a padding for empty block for the compatibility with the other
          // browsers.
          if (lineBreak.IsHTMLBRElement() ||
              lineBreak.IsPaddingForEmptyBlock()) {
            const RefPtr<Element> ancestorLimiterToDeleteEmptyInlines =
                lineBreak.ContentRef().IsInclusiveDescendantOf(
                    currentPoint.GetContainer())
                    ? currentPoint.GetContainerOrContainerParentElement()
                    : editingHost.get();
            {
              AutoTrackDOMPoint trackCurrentPoint(RangeUpdaterRef(),
                                                  ¤tPoint);
              Result<EditorDOMPoint, nsresult> deleteLineBreakResultOrError =
                  DeleteLineBreakWithTransaction(
                      lineBreak, nsIEditor::eStrip,
                      *ancestorLimiterToDeleteEmptyInlines);
              if (deleteLineBreakResultOrError.isErr()) [[unlikely]] {
                NS_WARNING(
                    "HTMLEditor::DeleteLineBreakWithTransaction() failed");
                return deleteLineBreakResultOrError.propagateErr();
              }
            }
            if (NS_WARN_IF(!currentPoint.IsSetAndValidInComposedDoc())) {
              return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
            }
          }
        }
      }
      if (*lineBreakType == LineBreakType::Linefeed) {
        // Both Chrome and us inserts a preformatted linefeed with its own
        // `Text` node in various cases.  However, when inserting multiline
        // text, we should insert a `Text` because Chrome does so and the
        // comment field in https://discussions.apple.com/ handles the new
        // `Text` to split each line into a paragraph.  At that time, it's
        // not assumed that inserted text is split at every linefeed.
        MOZ_ASSERT(*lineBreakType == LineBreakType::Linefeed);
        Result<InsertTextResult, nsresult> insertTextResult =
            InsertTextWithTransaction(
                aInsertionString, currentPoint,
                InsertTextTo::ExistingTextNodeIfAvailable);
        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<EditorDOMPoint>();
        } else {
          pointToInsert = currentPoint;
        }
      } else {
        MOZ_ASSERT(*lineBreakType == LineBreakType::BRElement);
        uint32_t nextOffset = 0;
        while (nextOffset < aInsertionString.Length()) {
          const uint32_t lineStartOffset = nextOffset;
          const int32_t inclusiveNextLinefeedOffset = aInsertionString.FindChar(
              HTMLEditUtils::kNewLine, lineStartOffset);
          const uint32_t lineLength =
              inclusiveNextLinefeedOffset != -1
                  ? static_cast<uint32_t>(inclusiveNextLinefeedOffset) -
                        lineStartOffset
                  : aInsertionString.Length() - lineStartOffset;
          if (lineLength) {
            // lineText does not include the preformatted line break.
            const nsDependentSubstring lineText(aInsertionString,
                                                lineStartOffset, lineLength);
            Result<InsertTextResult, nsresult> insertTextResult =
                InsertTextWithTransaction(
                    lineText, currentPoint,
                    GetInsertTextTo(inclusiveNextLinefeedOffset,
                                    lineStartOffset));
            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<EditorDOMPoint>();
            } else {
              pointToInsert = currentPoint;
            }
            if (inclusiveNextLinefeedOffset < 0) {
              break;  // We reached the last line
            }
          }
          MOZ_ASSERT(inclusiveNextLinefeedOffset >= 0);
          Result<CreateLineBreakResult, nsresult> insertLineBreakResultOrError =
              InsertLineBreak(WithTransaction::Yes, *lineBreakType,
                              currentPoint);
          if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) {
            NS_WARNING(nsPrintfCString("HTMLEditor::InsertLineBreak("
                                       "WithTransaction::Yes, %s) failed",
                                       ToString(*lineBreakType).c_str())
                           .get());
            return insertLineBreakResultOrError.propagateErr();
          }
          CreateLineBreakResult insertLineBreakResult =
              insertLineBreakResultOrError.unwrap();
          // We don't want to update selection here because we've blocked
          // InsertNodeTransaction updating selection with
          // dontChangeMySelection.
          insertLineBreakResult.IgnoreCaretPointSuggestion();
          MOZ_ASSERT(!AllowsTransactionsToChangeSelection());

          nextOffset = inclusiveNextLinefeedOffset + 1;
          pointToInsert =
              insertLineBreakResult.AfterLineBreak<EditorDOMPoint>();
          currentPoint.SetAfter(&insertLineBreakResult.LineBreakContentRef());
          if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc()) ||
              NS_WARN_IF(!currentPoint.IsSetAndValidInComposedDoc())) {
            return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
          }
        }
      }
    } else {
      uint32_t nextOffset = 0;
      while (nextOffset < aInsertionString.Length()) {
        const uint32_t lineStartOffset = nextOffset;
        const int32_t inclusiveNextLinefeedOffset =
            aInsertionString.FindChar(HTMLEditUtils::kNewLine, lineStartOffset);
        const uint32_t lineLength =
            inclusiveNextLinefeedOffset != -1
                ? static_cast<uint32_t>(inclusiveNextLinefeedOffset) -
                      lineStartOffset
                : aInsertionString.Length() - lineStartOffset;

        if (lineLength) {
          auto insertTextResult =
              [&]() MOZ_CAN_RUN_SCRIPT -> Result<InsertTextResult, nsresult> {
            // lineText does not include the preformatted line break.
            const nsDependentSubstring lineText(aInsertionString,
                                                lineStartOffset, lineLength);
            if (!lineText.Contains(u'\t')) {
              return WhiteSpaceVisibilityKeeper::InsertText(
                  *this, lineText, currentPoint,
                  GetInsertTextTo(inclusiveNextLinefeedOffset, lineStartOffset),
                  *editingHost);
            }
            nsAutoString formattedLineText(lineText);
            formattedLineText.ReplaceSubstring(u"\t"_ns, u"    "_ns);
            return WhiteSpaceVisibilityKeeper::InsertText(
                *this, formattedLineText, currentPoint,
                GetInsertTextTo(inclusiveNextLinefeedOffset, lineStartOffset),
                *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();
          } else {
            pointToInsert = currentPoint;
          }
          if (inclusiveNextLinefeedOffset < 0) {
            break;  // We reached the last line
          }
        }

        Result<CreateLineBreakResult, nsresult> insertLineBreakResultOrError =
            WhiteSpaceVisibilityKeeper::InsertLineBreak(*lineBreakType, *this,
                                                        currentPoint);
        if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) {
          NS_WARNING(
              nsPrintfCString(
                  "WhiteSpaceVisibilityKeeper::InsertLineBreak(%s) failed",
                  ToString(*lineBreakType).c_str())
                  .get());
          return insertLineBreakResultOrError.propagateErr();
        }
        CreateLineBreakResult insertLineBreakResult =
            insertLineBreakResultOrError.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 = insertLineBreakResult.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");
        nextOffset = inclusiveNextLinefeedOffset + 1;
        pointToInsert = insertLineBreakResult.AfterLineBreak<EditorDOMPoint>();
        currentPoint.SetAfter(&insertLineBreakResult.LineBreakContentRef());
        if (NS_WARN_IF(!pointToInsert.IsSetAndValidInComposedDoc()) ||
            NS_WARN_IF(!currentPoint.IsSetAndValidInComposedDoc())) {
          return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
        }
      }
    }

    // After this block, pointToInsert is updated by AutoTrackDOMPoint.
  }

  if (currentPoint.IsSet()) {
    // If we appended a collapsible white-space to the end of the text node,
    // its following content may be removed by the web app.  Then, we need to
    // keep it visible even if it becomes immediately before a block boundary.
    // For referring the node from our mutation observer, we need to store the
    // text node temporarily.
    if (currentPoint.IsInTextNode() &&
        MOZ_LIKELY(!currentPoint.IsStartOfContainer()) &&
        currentPoint.IsEndOfContainer() &&
        currentPoint.IsPreviousCharCollapsibleASCIISpace()) {
      mLastCollapsibleWhiteSpaceAppendedTextNode =
          currentPoint.ContainerAs<Text>();
    }
    if (!aInsertionString.IsEmpty() &&
        aInsertionString.Last() == HTMLEditUtils::kNewLine) {
      Result<CreateLineBreakResult, nsresult> insertPaddingLineBreakResult =
          InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded(currentPoint,
                                                               *editingHost);
      if (insertPaddingLineBreakResult.isErr()) [[unlikely]] {
        NS_WARNING(
            "HTMLEditor::InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded()"
            " failed");
        return insertPaddingLineBreakResult.propagateErr();
      }
      if (insertPaddingLineBreakResult.inspect().HasCaretPointSuggestion()) {
        currentPoint = insertPaddingLineBreakResult.unwrap().UnwrapCaretPoint();
      }
    } else {
      nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
          currentPoint,
          // When user inserting text, the web app may expect that nothing
          // extant content will be deleted. Therefore, we should preserve
          // preformatted linefeed at least.
          PreservePreformattedLineBreak::Yes, PaddingForEmptyBlock::Unnecessary,
          *editingHost);
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
        return Err(rv);
      }
    }
    currentPoint.SetInterlinePosition(InterlinePosition::EndOfLine);
    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<nsresult> 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();
}

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::GetPreviousCharPoint<EditorRawDOMPointInText>(
          {WSRunScanner::Option::OnlyEditableNodes}, 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::GetInclusiveNextCharPoint<EditorRawDOMPointInText>(
          {WSRunScanner::Option::OnlyEditableNodes}, aPoint);
  if (!nextCharPoint.IsSet()) {
    return CharPointData::InDifferentTextNode(CharPointType::TextEnd);
  }
  return CharPointData::InDifferentTextNode(
      HTMLEditor::GetCharPointType(nextCharPoint));
}

// static
void HTMLEditor::NormalizeAllWhiteSpaceSequences(
    nsString& aResult, const CharPointData& aPreviousCharPointData,
    const CharPointData& aNextCharPointData, Linefeed aLinefeed) {
  MOZ_ASSERT(!aResult.IsEmpty());

  const auto IsCollapsibleChar = [&](char16_t aChar) {
    if (aChar == HTMLEditUtils::kNewLine) {
      return aLinefeed == Linefeed::Preformatted;
    }
    return nsCRT::IsAsciiSpace(aChar);
  };
  const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
    return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
  };

  const uint32_t length = aResult.Length();
  for (uint32_t offset = 0; offset < length; offset++) {
    const char16_t ch = aResult[offset];
    if (!IsCollapsibleCharOrNBSP(ch)) {
      continue;
    }
    const CharPointData previousCharData = [&]() {
      if (offset) {
        const char16_t prevChar = aResult[offset - 1u];
        return CharPointData::InSameTextNode(
            prevChar == HTMLEditUtils::kNewLine
                ? CharPointType::PreformattedLineBreak
                : CharPointType::VisibleChar);
      }
      return aPreviousCharPointData;
    }();
    const uint32_t endOffset = [&]() {
      for (const uint32_t i : IntegerRange(offset, length)) {
        if (IsCollapsibleCharOrNBSP(aResult[i])) {
          continue;
        }
        return i;
      }
      return length;
    }();
    const CharPointData nextCharData = [&]() {
      if (endOffset < length) {
        const char16_t nextChar = aResult[endOffset];
        return CharPointData::InSameTextNode(
            nextChar == HTMLEditUtils::kNewLine
                ? CharPointType::PreformattedLineBreak
                : CharPointType::VisibleChar);
      }
      return aNextCharPointData;
    }();
    HTMLEditor::ReplaceStringWithNormalizedWhiteSpaceSequence(
        aResult, offset, endOffset - offset, previousCharData, nextCharData);
    offset = endOffset;
  }
}

// static
void HTMLEditor::GenerateWhiteSpaceSequence(
    nsString& aResult, uint32_t aLength,
    const CharPointData& aPreviousCharPointData,
    const CharPointData& aNextCharPointData) {
  MOZ_ASSERT(aResult.IsEmpty());
  MOZ_ASSERT(aLength);

  aResult.SetLength(aLength);
  HTMLEditor::ReplaceStringWithNormalizedWhiteSpaceSequence(
      aResult, 0u, aLength, aPreviousCharPointData, aNextCharPointData);
}

// static
void HTMLEditor::ReplaceStringWithNormalizedWhiteSpaceSequence(
    nsString& aResult, uint32_t aOffset, uint32_t aLength,
    const CharPointData& aPreviousCharPointData,
    const CharPointData& aNextCharPointData) {
  MOZ_ASSERT(!aResult.IsEmpty());
  MOZ_ASSERT(aLength);
  MOZ_ASSERT(aOffset < aResult.Length());
  MOZ_ASSERT(aOffset + aLength <= aResult.Length());

  // 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.SetCharAt(HTMLEditUtils::kSpace, aOffset);
      return;
    }
    // If it's start or end of text, put an NBSP.
    if (aPreviousCharPointData.Type() == CharPointType::TextEnd ||
        aNextCharPointData.Type() == CharPointType::TextEnd) {
      aResult.SetCharAt(HTMLEditUtils::kNBSP, aOffset);
      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.SetCharAt(HTMLEditUtils::kNBSP, aOffset);
      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.SetCharAt(
        aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace ||
                aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace
            ? HTMLEditUtils::kNBSP
            : HTMLEditUtils::kSpace,
        aOffset);
    return;
  }

  // Generate pairs of NBSP and ASCII white-space.
  bool appendNBSP = true;  // Basically, starts with an NBSP.
  char16_t* const lastChar = aResult.BeginWriting() + aOffset + aLength - 1;
  for (char16_t* iter = aResult.BeginWriting() + aOffset; 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;
}

HTMLEditor::NormalizedStringToInsertText
HTMLEditor::NormalizeWhiteSpacesToInsertText(
    const EditorDOMPoint& aPointToInsert, const nsAString& aStringToInsert,
    NormalizeSurroundingWhiteSpaces aNormalizeSurroundingWhiteSpaces) const {
  MOZ_ASSERT(aPointToInsert.IsSet());

  // If white-spaces are preformatted, we don't need to normalize white-spaces.
  if (EditorUtils::IsWhiteSpacePreformatted(
          *aPointToInsert.ContainerAs<nsIContent>())) {
    return NormalizedStringToInsertText(aStringToInsert, aPointToInsert);
  }

  Text* const textNode = aPointToInsert.GetContainerAs<Text>();
  const CharacterDataBuffer* const characterDataBuffer =
      textNode ? &textNode->DataBuffer() : nullptr;
  const bool isNewLineCollapsible = !EditorUtils::IsNewLinePreformatted(
      *aPointToInsert.ContainerAs<nsIContent>());

  // We don't want to make invisible things visible with this normalization.
  // Therefore, we need to know whether there are invisible leading and/or
  // trailing white-spaces in the `Text`.

  // Then, compute visible white-space length before/after the insertion point.
  // Note that these lengths may contain invisible white-spaces.
  const uint32_t precedingWhiteSpaceLength = [&]() {
    if (!textNode || !aNormalizeSurroundingWhiteSpaces ||
        aPointToInsert.IsStartOfContainer()) {
      return 0u;
    }
    const auto nonWhiteSpaceOffset =
        HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
            *textNode, aPointToInsert.Offset(),
            {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
    const uint32_t firstWhiteSpaceOffset =
        nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u;
    return aPointToInsert.Offset() - firstWhiteSpaceOffset;
  }();
  const uint32_t followingWhiteSpaceLength = [&]() {
    if (!textNode || !aNormalizeSurroundingWhiteSpaces ||
        aPointToInsert.IsEndOfContainer()) {
      return 0u;
    }
    MOZ_ASSERT(characterDataBuffer);
    const auto nonWhiteSpaceOffset =
        HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
            *textNode, aPointToInsert.Offset(),
            {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
    MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(characterDataBuffer->GetLength()) >=
               aPointToInsert.Offset());
    return nonWhiteSpaceOffset.valueOr(characterDataBuffer->GetLength()) -
           aPointToInsert.Offset();
  }();

  // Now, we can know invisible white-space length in precedingWhiteSpaceLength
  // and followingWhiteSpaceLength.
  const uint32_t precedingInvisibleWhiteSpaceCount =
      textNode
          ? HTMLEditUtils::GetInvisibleWhiteSpaceCount(
                *textNode, aPointToInsert.Offset() - precedingWhiteSpaceLength,
                precedingWhiteSpaceLength)
          : 0u;
  MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount);
  const uint32_t newPrecedingWhiteSpaceLength =
      precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount;
  const uint32_t followingInvisibleSpaceCount =
      textNode
          ? HTMLEditUtils::GetInvisibleWhiteSpaceCount(
                *textNode, aPointToInsert.Offset(), followingWhiteSpaceLength)
          : 0u;
  MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount);
  const uint32_t newFollowingWhiteSpaceLength =
      followingWhiteSpaceLength - followingInvisibleSpaceCount;

  const nsAutoString stringToInsertWithSurroundingSpaces =
      [&]() -> nsAutoString {
    if (!newPrecedingWhiteSpaceLength && !newFollowingWhiteSpaceLength) {
      return nsAutoString(aStringToInsert);
    }
    nsAutoString str;
    str.SetCapacity(aStringToInsert.Length() + newPrecedingWhiteSpaceLength +
                    newFollowingWhiteSpaceLength);
    for ([[maybe_unused]] auto unused :
         IntegerRange(newPrecedingWhiteSpaceLength)) {
      str.Append(' ');
    }
    str.Append(aStringToInsert);
    for ([[maybe_unused]] auto unused :
         IntegerRange(newFollowingWhiteSpaceLength)) {
      str.Append(' ');
    }
    return str;
  }();

  const uint32_t insertionOffsetInTextNode =
      aPointToInsert.IsInTextNode() ? aPointToInsert.Offset() : 0u;
  NormalizedStringToInsertText result(
      stringToInsertWithSurroundingSpaces, insertionOffsetInTextNode,
      insertionOffsetInTextNode - precedingWhiteSpaceLength,  // replace start
      precedingWhiteSpaceLength + followingWhiteSpaceLength,  // replace length
      newPrecedingWhiteSpaceLength, newFollowingWhiteSpaceLength);

  // Now, normalize the inserting string.
  // Note that if the caller does not want to normalize the following
  // white-spaces, we always need to guarantee that neither the first character
  // nor the last character of the insertion string is not collapsible, i.e., if
  // each one is a collapsible white-space, we need to replace them an NBSP to
  // keep the visibility of the collapsible white-spaces.  Therefore, if
  // aNormalizeSurroundingWhiteSpaces is "No", we need to treat the insertion
  // string is the only characters in the `Text`.
  HTMLEditor::NormalizeAllWhiteSpaceSequences(
      result.mNormalizedString,
      CharPointData::InSameTextNode(
          !characterDataBuffer || !result.mReplaceStartOffset ||
                  !aNormalizeSurroundingWhiteSpaces
              ? CharPointType::TextEnd
              : (characterDataBuffer->CharAt(result.mReplaceStartOffset - 1u) ==
                         HTMLEditUtils::kNewLine
                     ? CharPointType::PreformattedLineBreak
                     : CharPointType::VisibleChar)),
      CharPointData::InSameTextNode(
          !characterDataBuffer ||
                  result.mReplaceEndOffset >=
                      characterDataBuffer->GetLength() ||
                  !aNormalizeSurroundingWhiteSpaces
              ? CharPointType::TextEnd
              : (characterDataBuffer->CharAt(result.mReplaceEndOffset) ==
                         HTMLEditUtils::kNewLine
                     ? CharPointType::PreformattedLineBreak
                     : CharPointType::VisibleChar)),
      isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted);
  return result;
}

HTMLEditor::ReplaceWhiteSpacesData HTMLEditor::GetNormalizedStringAt(
    const EditorDOMPointInText& aPoint) const {
  MOZ_ASSERT(aPoint.IsSet());

  // If white-spaces are preformatted, we don't need to normalize white-spaces.
  if (EditorUtils::IsWhiteSpacePreformatted(*aPoint.ContainerAs<Text>())) {
    return ReplaceWhiteSpacesData();
  }

  const Text& textNode = *aPoint.ContainerAs<Text>();
  const CharacterDataBuffer& characterDataBuffer = textNode.DataBuffer();

  // We don't want to make invisible things visible with this normalization.
  // Therefore, we need to know whether there are invisible leading and/or
  // trailing white-spaces in the `Text`.

  // Then, compute visible white-space length before/after the point.
  // Note that these lengths may contain invisible white-spaces.
  const uint32_t precedingWhiteSpaceLength = [&]() {
    if (aPoint.IsStartOfContainer()) {
      return 0u;
    }
    const auto nonWhiteSpaceOffset =
        HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
            textNode, aPoint.Offset(),
            {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
    const uint32_t firstWhiteSpaceOffset =
        nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u;
    return aPoint.Offset() - firstWhiteSpaceOffset;
  }();
  const uint32_t followingWhiteSpaceLength = [&]() {
    if (aPoint.IsEndOfContainer()) {
      return 0u;
    }
    const auto nonWhiteSpaceOffset =
        HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
            textNode, aPoint.Offset(),
            {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
    MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(characterDataBuffer.GetLength()) >=
               aPoint.Offset());
    return nonWhiteSpaceOffset.valueOr(characterDataBuffer.GetLength()) -
           aPoint.Offset();
  }();
  if (!precedingWhiteSpaceLength && !followingWhiteSpaceLength) {
    return ReplaceWhiteSpacesData();
  }

  // Now, we can know invisible white-space length in precedingWhiteSpaceLength
  // and followingWhiteSpaceLength.
  const uint32_t precedingInvisibleWhiteSpaceCount =
      HTMLEditUtils::GetInvisibleWhiteSpaceCount(
          textNode, aPoint.Offset() - precedingWhiteSpaceLength,
          precedingWhiteSpaceLength);
  MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount);
  const uint32_t newPrecedingWhiteSpaceLength =
      precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount;
  const uint32_t followingInvisibleSpaceCount =
      HTMLEditUtils::GetInvisibleWhiteSpaceCount(textNode, aPoint.Offset(),
                                                 followingWhiteSpaceLength);
  MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount);
  const uint32_t newFollowingWhiteSpaceLength =
      followingWhiteSpaceLength - followingInvisibleSpaceCount;

  nsAutoString stringToInsertWithSurroundingSpaces;
  if (newPrecedingWhiteSpaceLength || newFollowingWhiteSpaceLength) {
    stringToInsertWithSurroundingSpaces.SetLength(newPrecedingWhiteSpaceLength +
                                                  newFollowingWhiteSpaceLength);
    for (auto index : IntegerRange(newPrecedingWhiteSpaceLength +
                                   newFollowingWhiteSpaceLength)) {
      stringToInsertWithSurroundingSpaces.SetCharAt(' ', index);
    }
  }

  ReplaceWhiteSpacesData result(
      std::move(stringToInsertWithSurroundingSpaces),
      aPoint.Offset() - precedingWhiteSpaceLength,            // replace start
      precedingWhiteSpaceLength + followingWhiteSpaceLength,  // replace length
      // aPoint.Offset() after replacing the white-spaces
      aPoint.Offset() - precedingWhiteSpaceLength +
          newPrecedingWhiteSpaceLength);
  if (!result.mNormalizedString.IsEmpty()) {
    HTMLEditor::NormalizeAllWhiteSpaceSequences(
        result.mNormalizedString,
        CharPointData::InSameTextNode(
            !result.mReplaceStartOffset
                ? CharPointType::TextEnd
                : (characterDataBuffer.CharAt(result.mReplaceStartOffset -
                                              1u) == HTMLEditUtils::kNewLine
                       ? CharPointType::PreformattedLineBreak
                       : CharPointType::VisibleChar)),
        CharPointData::InSameTextNode(
            result.mReplaceEndOffset >= characterDataBuffer.GetLength()
                ? CharPointType::TextEnd
                : (characterDataBuffer.CharAt(result.mReplaceEndOffset) ==
                           HTMLEditUtils::kNewLine
                       ? CharPointType::PreformattedLineBreak
                       : CharPointType::VisibleChar)),
        EditorUtils::IsNewLinePreformatted(textNode) ? Linefeed::Collapsible
                                                     : Linefeed::Preformatted);
  }
  return result;
}

HTMLEditor::ReplaceWhiteSpacesData
HTMLEditor::GetFollowingNormalizedStringToSplitAt(
    const EditorDOMPointInText& aPointToSplit) const {
  MOZ_ASSERT(aPointToSplit.IsSet());

  if (EditorUtils::IsWhiteSpacePreformatted(
          *aPointToSplit.ContainerAs<Text>()) ||
      aPointToSplit.IsEndOfContainer()) {
    return ReplaceWhiteSpacesData();
  }
  const bool isNewLineCollapsible =
      !EditorUtils::IsNewLinePreformatted(*aPointToSplit.ContainerAs<Text>());
  const auto IsPreformattedLineBreak = [&](char16_t aChar) {
    return !isNewLineCollapsible && aChar == HTMLEditUtils::kNewLine;
  };
  const auto IsCollapsibleChar = [&](char16_t aChar) {
    return !IsPreformattedLineBreak(aChar) && nsCRT::IsAsciiSpace(aChar);
  };
  const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
    return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
  };
  const char16_t followingChar = aPointToSplit.Char();
  if (!IsCollapsibleCharOrNBSP(followingChar)) {
    return ReplaceWhiteSpacesData();
  }
  const uint32_t followingWhiteSpaceLength = [&]() {
    const auto nonWhiteSpaceOffset =
        HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
            *aPointToSplit.ContainerAs<Text>(), aPointToSplit.Offset(),
            {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
    MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(
                   aPointToSplit.ContainerAs<Text>()->TextDataLength()) >=
               aPointToSplit.Offset());
    return nonWhiteSpaceOffset.valueOr(
               aPointToSplit.ContainerAs<Text>()->TextDataLength()) -
           aPointToSplit.Offset();
  }();
  MOZ_ASSERT(followingWhiteSpaceLength);
  if (NS_WARN_IF(!followingWhiteSpaceLength) ||
      (followingWhiteSpaceLength == 1u &&
       followingChar == HTMLEditUtils::kNBSP)) {
    return ReplaceWhiteSpacesData();
  }

  const uint32_t followingInvisibleSpaceCount =
      HTMLEditUtils::GetInvisibleWhiteSpaceCount(
          *aPointToSplit.ContainerAs<Text>(), aPointToSplit.Offset(),
          followingWhiteSpaceLength);
  MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount);
  const uint32_t newFollowingWhiteSpaceLength =
      followingWhiteSpaceLength - followingInvisibleSpaceCount;
  nsAutoString followingWhiteSpaces;
  if (newFollowingWhiteSpaceLength) {
    followingWhiteSpaces.SetLength(newFollowingWhiteSpaceLength);
    for (const auto offset : IntegerRange(newFollowingWhiteSpaceLength)) {
      followingWhiteSpaces.SetCharAt(' ', offset);
    }
  }
  ReplaceWhiteSpacesData result(std::move(followingWhiteSpaces),
                                aPointToSplit.Offset(),
                                followingWhiteSpaceLength);
  if (!result.mNormalizedString.IsEmpty()) {
    const CharacterDataBuffer& characterDataBuffer =
        aPointToSplit.ContainerAs<Text>()->DataBuffer();
    HTMLEditor::NormalizeAllWhiteSpaceSequences(
        result.mNormalizedString,
        CharPointData::InSameTextNode(CharPointType::TextEnd),
        CharPointData::InSameTextNode(
            result.mReplaceEndOffset >= characterDataBuffer.GetLength()
                ? CharPointType::TextEnd
                : (characterDataBuffer.CharAt(result.mReplaceEndOffset) ==
                           HTMLEditUtils::kNewLine
                       ? CharPointType::PreformattedLineBreak
                       : CharPointType::VisibleChar)),
        isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted);
  }
  return result;
}

HTMLEditor::ReplaceWhiteSpacesData
HTMLEditor::GetPrecedingNormalizedStringToSplitAt(
    const EditorDOMPointInText& aPointToSplit) const {
  MOZ_ASSERT(aPointToSplit.IsSet());

  if (EditorUtils::IsWhiteSpacePreformatted(
          *aPointToSplit.ContainerAs<Text>()) ||
      aPointToSplit.IsStartOfContainer()) {
    return ReplaceWhiteSpacesData();
  }
  const bool isNewLineCollapsible =
      !EditorUtils::IsNewLinePreformatted(*aPointToSplit.ContainerAs<Text>());
  const auto IsPreformattedLineBreak = [&](char16_t aChar) {
    return !isNewLineCollapsible && aChar == HTMLEditUtils::kNewLine;
  };
  const auto IsCollapsibleChar = [&](char16_t aChar) {
    return !IsPreformattedLineBreak(aChar) && nsCRT::IsAsciiSpace(aChar);
  };
  const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
    return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
  };
  const char16_t precedingChar = aPointToSplit.PreviousChar();
  if (!IsCollapsibleCharOrNBSP(precedingChar)) {
    return ReplaceWhiteSpacesData();
  }
  const uint32_t precedingWhiteSpaceLength = [&]() {
    const auto nonWhiteSpaceOffset =
        HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
            *aPointToSplit.ContainerAs<Text>(), aPointToSplit.Offset(),
            {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
    const uint32_t firstWhiteSpaceOffset =
        nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u;
    return aPointToSplit.Offset() - firstWhiteSpaceOffset;
  }();
  MOZ_ASSERT(precedingWhiteSpaceLength);
  if (NS_WARN_IF(!precedingWhiteSpaceLength) ||
      (precedingWhiteSpaceLength == 1u &&
       precedingChar == HTMLEditUtils::kNBSP)) {
    return ReplaceWhiteSpacesData();
  }

  const uint32_t precedingInvisibleWhiteSpaceCount =
      HTMLEditUtils::GetInvisibleWhiteSpaceCount(
          *aPointToSplit.ContainerAs<Text>(),
          aPointToSplit.Offset() - precedingWhiteSpaceLength,
          precedingWhiteSpaceLength);
  MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount);
  const uint32_t newPrecedingWhiteSpaceLength =
      precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount;
  nsAutoString precedingWhiteSpaces;
  if (newPrecedingWhiteSpaceLength) {
    precedingWhiteSpaces.SetLength(newPrecedingWhiteSpaceLength);
    for (const auto offset : IntegerRange(newPrecedingWhiteSpaceLength)) {
      precedingWhiteSpaces.SetCharAt(' ', offset);
    }
  }
  ReplaceWhiteSpacesData result(
      std::move(precedingWhiteSpaces),
      aPointToSplit.Offset() - precedingWhiteSpaceLength,
      precedingWhiteSpaceLength);
  if (!result.mNormalizedString.IsEmpty()) {
    const CharacterDataBuffer& characterDataBuffer =
        aPointToSplit.ContainerAs<Text>()->DataBuffer();
    HTMLEditor::NormalizeAllWhiteSpaceSequences(
        result.mNormalizedString,
        CharPointData::InSameTextNode(
            !result.mReplaceStartOffset
                ? CharPointType::TextEnd
                : (characterDataBuffer.CharAt(result.mReplaceStartOffset -
                                              1u) == HTMLEditUtils::kNewLine
                       ? CharPointType::PreformattedLineBreak
                       : CharPointType::VisibleChar)),
        CharPointData::InSameTextNode(CharPointType::TextEnd),
        isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted);
  }
  return result;
}

HTMLEditor::ReplaceWhiteSpacesData
HTMLEditor::GetSurroundingNormalizedStringToDelete(const Text& aTextNode,
                                                   uint32_t aOffset,
                                                   uint32_t aLength) const {
  MOZ_ASSERT(aOffset <= aTextNode.TextDataLength());
  MOZ_ASSERT(aOffset + aLength <= aTextNode.TextDataLength());

  if (EditorUtils::IsWhiteSpacePreformatted(aTextNode) || !aLength ||
      (!aOffset && aLength >= aTextNode.TextDataLength())) {
    return ReplaceWhiteSpacesData();
  }
  const bool isNewLineCollapsible =
      !EditorUtils::IsNewLinePreformatted(aTextNode);
  const auto IsPreformattedLineBreak = [&](char16_t aChar) {
    return !isNewLineCollapsible && aChar == HTMLEditUtils::kNewLine;
  };
  const auto IsCollapsibleChar = [&](char16_t aChar) {
    return !IsPreformattedLineBreak(aChar) && nsCRT::IsAsciiSpace(aChar);
  };
  const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
    return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
  };
  const CharacterDataBuffer& characterDataBuffer = aTextNode.DataBuffer();
  const char16_t precedingChar = aOffset
                                     ? characterDataBuffer.CharAt(aOffset - 1u)
                                     : static_cast<char16_t>(0);
  const char16_t followingChar =
      aOffset + aLength < characterDataBuffer.GetLength()
          ? characterDataBuffer.CharAt(aOffset + aLength)
          : static_cast<char16_t>(0);
  // If there is no surrounding white-spaces, we need to do nothing here.
  if (!IsCollapsibleCharOrNBSP(precedingChar) &&
      !IsCollapsibleCharOrNBSP(followingChar)) {
    return ReplaceWhiteSpacesData();
  }
  const uint32_t precedingWhiteSpaceLength = [&]() {
    if (!IsCollapsibleCharOrNBSP(precedingChar)) {
      return 0u;
    }
    const auto nonWhiteSpaceOffset =
        HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
            aTextNode, aOffset,
            {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
    const uint32_t firstWhiteSpaceOffset =
        nonWhiteSpaceOffset ? *nonWhiteSpaceOffset + 1u : 0u;
    return aOffset - firstWhiteSpaceOffset;
  }();
  const uint32_t followingWhiteSpaceLength = [&]() {
    if (!IsCollapsibleCharOrNBSP(followingChar)) {
      return 0u;
    }
    const auto nonWhiteSpaceOffset =
        HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
            aTextNode, aOffset + aLength,
            {HTMLEditUtils::WalkTextOption::TreatNBSPsCollapsible});
    MOZ_ASSERT(nonWhiteSpaceOffset.valueOr(characterDataBuffer.GetLength()) >=
               aOffset + aLength);
    return nonWhiteSpaceOffset.valueOr(characterDataBuffer.GetLength()) -
           (aOffset + aLength);
  }();
  if (NS_WARN_IF(!precedingWhiteSpaceLength && !followingWhiteSpaceLength)) {
    return ReplaceWhiteSpacesData();
  }
  const uint32_t precedingInvisibleWhiteSpaceCount =
      HTMLEditUtils::GetInvisibleWhiteSpaceCount(
          aTextNode, aOffset - precedingWhiteSpaceLength,
          precedingWhiteSpaceLength);
  MOZ_ASSERT(precedingWhiteSpaceLength >= precedingInvisibleWhiteSpaceCount);
  const uint32_t followingInvisibleSpaceCount =
      HTMLEditUtils::GetInvisibleWhiteSpaceCount(aTextNode, aOffset + aLength,
                                                 followingWhiteSpaceLength);
  MOZ_ASSERT(followingWhiteSpaceLength >= followingInvisibleSpaceCount);

  // Let's try to return early if there is only one white-space around the
  // deleting range to avoid to run the expensive path.
  if (precedingWhiteSpaceLength == 1u && !precedingInvisibleWhiteSpaceCount &&
      !followingWhiteSpaceLength) {
    // If there is only one ASCII space and it'll be followed by a
    // non-collapsible character except preformatted linebreak after deletion,
    // we don't need to normalize the preceding white-space.
    if (precedingChar == HTMLEditUtils::kSpace && followingChar &&
        !IsPreformattedLineBreak(followingChar)) {
      return ReplaceWhiteSpacesData();
    }
    // If there is only one NBSP and it'll be the last character or will be
    // followed by a collapsible white-space, we don't need to normalize the
    // preceding white-space.
    if (precedingChar == HTMLEditUtils::kNBSP &&
        (!followingChar || IsPreformattedLineBreak(followingChar))) {
      return ReplaceWhiteSpacesData();
    }
  }
  if (followingWhiteSpaceLength == 1u && !followingInvisibleSpaceCount &&
      !precedingWhiteSpaceLength) {
    // If there is only one ASCII space and it'll follow by a non-collapsible
    // character after deletion, we don't need to normalize the following
    // white-space.
    if (followingChar == HTMLEditUtils::kSpace && precedingChar &&
        !IsPreformattedLineBreak(precedingChar)) {
      return ReplaceWhiteSpacesData();
    }
    // If there is only one NBSP and it'll be the first character or will
    // follow a preformatted line break, we don't need to normalize the
    // following white-space.
    if (followingChar == HTMLEditUtils::kNBSP &&
        (!precedingChar || IsPreformattedLineBreak(precedingChar))) {
      return ReplaceWhiteSpacesData();
    }
  }

  const uint32_t newPrecedingWhiteSpaceLength =
      precedingWhiteSpaceLength - precedingInvisibleWhiteSpaceCount;
  const uint32_t newFollowingWhiteSpaceLength =
      followingWhiteSpaceLength - followingInvisibleSpaceCount;
  nsAutoString surroundingWhiteSpaces;
  if (newPrecedingWhiteSpaceLength || newFollowingWhiteSpaceLength) {
    surroundingWhiteSpaces.SetLength(newPrecedingWhiteSpaceLength +
                                     newFollowingWhiteSpaceLength);
    for (const auto offset : IntegerRange(newPrecedingWhiteSpaceLength +
                                          newFollowingWhiteSpaceLength)) {
      surroundingWhiteSpaces.SetCharAt(' ', offset);
    }
  }
  ReplaceWhiteSpacesData result(
      std::move(surroundingWhiteSpaces), aOffset - precedingWhiteSpaceLength,
      precedingWhiteSpaceLength + aLength + followingWhiteSpaceLength,
      aOffset - precedingInvisibleWhiteSpaceCount);
  if (!result.mNormalizedString.IsEmpty()) {
    HTMLEditor::NormalizeAllWhiteSpaceSequences(
        result.mNormalizedString,
        CharPointData::InSameTextNode(
            !result.mReplaceStartOffset
                ? CharPointType::TextEnd
                : (characterDataBuffer.CharAt(result.mReplaceStartOffset -
                                              1u) == HTMLEditUtils::kNewLine
                       ? CharPointType::PreformattedLineBreak
                       : CharPointType::VisibleChar)),
        CharPointData::InSameTextNode(
            result.mReplaceEndOffset >= characterDataBuffer.GetLength()
                ? CharPointType::TextEnd
                : (characterDataBuffer.CharAt(result.mReplaceEndOffset) ==
                           HTMLEditUtils::kNewLine
                       ? CharPointType::PreformattedLineBreak
                       : CharPointType::VisibleChar)),
        isNewLineCollapsible ? Linefeed::Collapsible : Linefeed::Preformatted);
  }
  return result;
}

void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces(
    EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
    nsString& aNormalizedWhiteSpacesInStartNode,
    nsString& 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.
  const auto precedingCharPoint =
      WSRunScanner::GetPreviousCharPoint<EditorDOMPointInText>(
          {WSRunScanner::Option::OnlyEditableNodes}, aStartToDelete);
  const auto followingCharPoint =
      WSRunScanner::GetInclusiveNextCharPoint<EditorDOMPointInText>(
          {WSRunScanner::Option::OnlyEditableNodes}, 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<Text>() != aEndToDelete.ContainerAs<Text>() ||
      (aEndToDelete.IsEndOfContainer() && followingCharPoint.IsSet());
  const bool maybeNormalizePrecedingWhiteSpaces =
      !removingLastCharOfStartNode && precedingCharPoint.IsSet() &&
      !precedingCharPoint.IsEndOfContainer() &&
      precedingCharPoint.ContainerAs<Text>() ==
          aStartToDelete.ContainerAs<Text>() &&
      precedingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP();
  const bool maybeNormalizeFollowingWhiteSpaces =
      followingCharPoint.IsSet() && !followingCharPoint.IsEndOfContainer() &&
      (followingCharPoint.ContainerAs<Text>() ==
           aEndToDelete.ContainerAs<Text>() ||
       removingLastCharOfStartNode) &&
      followingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP();

  if (!maybeNormalizePrecedingWhiteSpaces &&
      !maybeNormalizeFollowingWhiteSpaces) {
    return;  // There are no white-spaces.
  }

  // Next, consider the range to normalize.
  EditorDOMPointInText startToNormalize, endToNormalize;
  if (maybeNormalizePrecedingWhiteSpaces) {
    Maybe<uint32_t> previousCharOffsetOfWhiteSpaces =
        HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
            precedingCharPoint, {WalkTextOption::TreatNBSPsCollapsible});
    startToNormalize.Set(precedingCharPoint.ContainerAs<Text>(),
                         previousCharOffsetOfWhiteSpaces.isSome()
                             ? previousCharOffsetOfWhiteSpaces.value() + 1
                             : 0);
    MOZ_ASSERT(!startToNormalize.IsEndOfContainer());
  }
  if (maybeNormalizeFollowingWhiteSpaces) {
    Maybe<uint32_t> nextCharOffsetOfWhiteSpaces =
        HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
            followingCharPoint, {WalkTextOption::TreatNBSPsCollapsible});
    if (nextCharOffsetOfWhiteSpaces.isSome()) {
      endToNormalize.Set(followingCharPoint.ContainerAs<Text>(),
                         nextCharOffsetOfWhiteSpaces.value());
    } else {
      endToNormalize.SetToEndOf(followingCharPoint.ContainerAs<Text>());
    }
    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<Text>() ==
               aStartToDelete.ContainerAs<Text>());
    lengthInStartNode = aStartToDelete.Offset() - startToNormalize.Offset();
    MOZ_ASSERT(lengthInStartNode);
  }
  if (endToNormalize.IsSet()) {
    lengthInEndNode =
        endToNormalize.ContainerAs<Text>() == aEndToDelete.ContainerAs<Text>()
            ? 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<Text>() ==
        aStartToDelete.ContainerAs<Text>()) {
      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<CaretPoint, nsresult>
HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces(
    const EditorDOMPointInText& aStartToDelete,
    const EditorDOMPointInText& aEndToDelete,
    TreatEmptyTextNodes aTreatEmptyTextNodes, DeleteDirection aDeleteDirection,
    const Element& aEditingHost) {
  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<EditorDOMPoint>());
  }

  // 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<Text>() == aEndToDelete.ContainerAs<Text>()) {
    newCaretPosition = aEndToDelete.To<EditorDOMPoint>();
  } else if (aDeleteDirection == DeleteDirection::Forward) {
    newCaretPosition.SetToEndOf(aStartToDelete.ContainerAs<Text>());
  } else {
    newCaretPosition.Set(aEndToDelete.ContainerAs<Text>(), 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<Text>(),
                                         endToDelete.Offset());
      {
        AutoTrackDOMPoint trackEndToDelete(RangeUpdaterRef(),
                                           &trackingEndToDelete);
        uint32_t lengthToReplaceInFirstTextNode =
            startToDelete.ContainerAs<Text>() ==
                    trackingEndToDelete.ContainerAs<Text>()
                ? trackingEndToDelete.Offset() - startToDelete.Offset()
                : startToDelete.ContainerAs<Text>()->TextLength() -
                      startToDelete.Offset();
        Result<InsertTextResult, nsresult> replaceTextResult =
            ReplaceTextWithTransaction(
                MOZ_KnownLive(*startToDelete.ContainerAs<Text>()),
                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<Text>() ==
            trackingEndToDelete.ContainerAs<Text>()) {
          MOZ_ASSERT(normalizedWhiteSpacesInLastNode.IsEmpty());
          break;  // There is no more text which we need to delete.
        }
      }
      MOZ_ASSERT(trackingEndToDelete.IsInTextNode());
      endToDelete.Set(trackingEndToDelete.ContainerAs<Text>(),
                      trackingEndToDelete.Offset());
      // If the remaining range was modified by mutation event listener,
      // we should stop handling the deletion.
      startToDelete =
          EditorDOMPointInText::AtEndOf(*startToDelete.ContainerAs<Text>());
    }
    // 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<Text>() != endToDelete.ContainerAs<Text>()) {
      // 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<Text>(), 0);
      if (startToDelete != endToDeleteExceptReplaceRange) {
        Result<CaretPoint, nsresult> 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 (MaybeNodeRemovalsObservedByDevTools() &&
            (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<Text>() ==
               endToDelete.ContainerAs<Text>());
    Result<InsertTextResult, nsresult> replaceTextResult =
        ReplaceTextWithTransaction(
            MOZ_KnownLive(*startToDelete.ContainerAs<Text>()),
            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<nsIContent>(),
                HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost,
                BlockInlineCheck::UseComputedDisplayStyle)) {
      // Try to put caret next to immediately after previous editable leaf.
      nsIContent* previousContent =
          HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
              newCaretPosition,
              {LeafNodeOption::TreatNonEditableNodeAsLeafNode},
              BlockInlineCheck::UseComputedDisplayStyle,
              editableBlockElementOrInlineEditingHost);
      if (previousContent &&
          HTMLEditUtils::IsSimplyEditableNode(*previousContent) &&
          !HTMLEditUtils::IsBlockElement(
              *previousContent, BlockInlineCheck::UseComputedDisplayStyle)) {
        newCaretPosition =
            previousContent->IsText() ||
                    HTMLEditUtils::IsContainerNode(*previousContent)
                ? EditorDOMPoint::AtEndOf(*previousContent)
                : EditorDOMPoint::After(*previousContent);
      }
      // But if the point is very first of a block element or immediately after
      // a child block, look for next editable leaf instead.
      else if (nsIContent* nextContent =
                   HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
                       newCaretPosition,
                       {LeafNodeOption::TreatNonEditableNodeAsLeafNode},
                       BlockInlineCheck::UseComputedDisplayStyle,
                       editableBlockElementOrInlineEditingHost)) {
        if (HTMLEditUtils::IsSimplyEditableNode(*nextContent) &&
            !HTMLEditUtils::IsBlockElement(
                *nextContent, BlockInlineCheck::UseComputedDisplayStyle)) {
          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()->IsEditable() &&
      newCaretPosition.GetContainer()->GetPreviousSibling()->IsText()) {
    newCaretPosition.SetToEndOf(
        newCaretPosition.GetContainer()->GetPreviousSibling()->AsText());
  }
  MOZ_ASSERT(HTMLEditUtils::IsSimplyEditableNode(
      *newCaretPosition.ContainerAs<nsIContent>()));

  {
    AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(),
                                           &newCaretPosition);
    nsresult rv = EnsureNoFollowingUnnecessaryLineBreak(
        newCaretPosition, PreservePreformattedLineBreak::No,
        PaddingForEmptyBlock::Significant, aEditingHost);
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::EnsureNoFollowingUnnecessaryLineBreak() failed");
      return Err(rv);
    }
    if (NS_WARN_IF(!newCaretPosition.IsSet())) {
      return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
    }
  }

  if (GetTopLevelEditSubAction() == EditSubAction::eDeleteSelectedContent) {
    AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(),
                                               &newCaretPosition);
    Result<CreateLineBreakResult, nsresult> insertPaddingBRElementOrError =
        InsertPaddingBRElementIfNeeded(
            newCaretPosition,
            aEditingHost.IsContentEditablePlainTextOnly() ? nsIEditor::eNoStrip
                                                          : nsIEditor::eStrip,
            aEditingHost);
    if (MOZ_UNLIKELY(insertPaddingBRElementOrError.isErr())) {
      NS_WARNING("HTMLEditor::InsertPaddingBRElementIfNeeded() failed");
      return insertPaddingBRElementOrError.propagateErr();
    }
    trackingNewCaretPosition.Flush(StopTracking::Yes);
    if (!newCaretPosition.IsInTextNode()) {
      insertPaddingBRElementOrError.unwrap().MoveCaretPointTo(
          newCaretPosition, {SuggestCaret::OnlyIfHasSuggestion});
    } else {
      insertPaddingBRElementOrError.unwrap().IgnoreCaretPointSuggestion();
    }
    if (!newCaretPosition.IsSetAndValid()) {
      NS_WARNING("Inserting <br> element caused unexpected DOM tree");
      return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
    }
  }
  return CaretPoint(std::move(newCaretPosition));
}

Result<JoinNodesResult, nsresult>
HTMLEditor::JoinTextNodesWithNormalizeWhiteSpaces(Text& aLeftText,
                                                  Text& aRightText) {
  if (EditorUtils::IsWhiteSpacePreformatted(aLeftText)) {
    Result<JoinNodesResult, nsresult> joinResultOrError =
        JoinNodesWithTransaction(aLeftText, aRightText);
    NS_WARNING_ASSERTION(joinResultOrError.isOk(),
                         "HTMLEditor::JoinNodesWithTransaction() failed");
    return joinResultOrError;
  }
  const bool isNewLinePreformatted =
      EditorUtils::IsNewLinePreformatted(aLeftText);
  const auto IsCollapsibleChar = [&](char16_t aChar) {
    return (aChar == HTMLEditUtils::kNewLine && !isNewLinePreformatted) ||
           nsCRT::IsAsciiSpace(aChar);
  };
  const auto IsCollapsibleCharOrNBSP = [&](char16_t aChar) {
    return aChar == HTMLEditUtils::kNBSP || IsCollapsibleChar(aChar);
  };
  const char16_t lastLeftChar = aLeftText.DataBuffer().SafeLastChar();
  char16_t firstRightChar = aRightText.DataBuffer().SafeFirstChar();
  const char16_t secondRightChar = aRightText.DataBuffer().GetLength() >= 2
                                       ? aRightText.DataBuffer().CharAt(1u)
                                       : static_cast<char16_t>(0);
  if (IsCollapsibleCharOrNBSP(firstRightChar)) {
    // If the right Text starts only with a collapsible white-space and it'll
    // follow a non-collapsible char, we should make it an ASCII white-space.
    if (secondRightChar && !IsCollapsibleCharOrNBSP(secondRightChar) &&
        lastLeftChar && !IsCollapsibleChar(lastLeftChar)) {
      if (firstRightChar != HTMLEditUtils::kSpace) {
        Result<InsertTextResult, nsresult> replaceWhiteSpaceResultOrError =
            ReplaceTextWithTransaction(aRightText, 0u, 1u, u" "_ns);
        if (MOZ_UNLIKELY(replaceWhiteSpaceResultOrError.isErr())) {
          NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
          return replaceWhiteSpaceResultOrError.propagateErr();
        }
        replaceWhiteSpaceResultOrError.unwrap().IgnoreCaretPointSuggestion();
        if (NS_WARN_IF(aLeftText.GetNextSibling() != &aRightText)) {
          return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
        }
        firstRightChar = HTMLEditUtils::kSpace;
      }
    }
    // Otherwise, normalize the white-spaces before join, i.e., it will start
    // with an NBSP.
    else {
      Result<EditorDOMPoint, nsresult> atFirstVisibleThingOrError =
          WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter(
              *this, EditorDOMPoint(&aRightText, 0u), {});
      if (MOZ_UNLIKELY(atFirstVisibleThingOrError.isErr())) {
        NS_WARNING(
            "WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter() failed");
        return atFirstVisibleThingOrError.propagateErr();
      }
      if (!aRightText.GetParentNode()) {
        return JoinNodesResult(EditorDOMPoint::AtEndOf(aLeftText), aRightText);
      }
    }
  } else if (IsCollapsibleCharOrNBSP(lastLeftChar) &&
             lastLeftChar != HTMLEditUtils::kSpace &&
             aLeftText.DataBuffer().GetLength() >= 2u) {
    // If the last char of the left `Text` is a single white-space but not an
    // ASCII space, let's replace it with an ASCII space.
    const char16_t secondLastChar =
        aLeftText.DataBuffer().CharAt(aLeftText.DataBuffer().GetLength() - 2u);
    if (!IsCollapsibleCharOrNBSP(secondLastChar) &&
        !IsCollapsibleCharOrNBSP(firstRightChar)) {
      Result<InsertTextResult, nsresult> replaceWhiteSpaceResultOrError =
          ReplaceTextWithTransaction(
              aLeftText, aLeftText.DataBuffer().GetLength() - 1u, 1u, u" "_ns);
      if (MOZ_UNLIKELY(replaceWhiteSpaceResultOrError.isErr())) {
        NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
        return replaceWhiteSpaceResultOrError.propagateErr();
      }
      replaceWhiteSpaceResultOrError.unwrap().IgnoreCaretPointSuggestion();
      if (NS_WARN_IF(aLeftText.GetNextSibling() != &aRightText)) {
        return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
      }
    }
  }
  Result<JoinNodesResult, nsresult> joinResultOrError =
      JoinNodesWithTransaction(aLeftText, aRightText);
  if (MOZ_UNLIKELY(joinResultOrError.isErr())) {
    NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
    return joinResultOrError;
  }
  JoinNodesResult joinResult = joinResultOrError.unwrap();
  const EditorDOMPointInText startOfRightTextData =
      joinResult.AtJoinedPoint<EditorRawDOMPoint>().GetAsInText();
  if (NS_WARN_IF(!startOfRightTextData.IsSet()) ||
      (firstRightChar &&
       (NS_WARN_IF(startOfRightTextData.IsEndOfContainer()) ||
        NS_WARN_IF(firstRightChar != startOfRightTextData.Char())))) {
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }
  return std::move(joinResult);
}

// static
bool HTMLEditor::CanInsertLineBreak(LineBreakType aLineBreakType,
                                    const nsIContent& aContent) {
  if (MOZ_UNLIKELY(!HTMLEditUtils::IsSimplyEditableNode(aContent))) {
    return false;
  }
  if (aLineBreakType == LineBreakType::BRElement) {
    return HTMLEditUtils::CanNodeContain(aContent, *nsGkAtoms::br);
  }
  MOZ_ASSERT(aLineBreakType == LineBreakType::Linefeed);
  const Element* const container = aContent.GetAsElementOrParentElement();
  return container &&
         HTMLEditUtils::CanNodeContain(*container, *nsGkAtoms::textTagName) &&
         EditorUtils::IsNewLinePreformatted(*container);
}

Result<CreateLineBreakResult, nsresult>
HTMLEditor::InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded(
    const EditorDOMPoint& aPointToInsert, const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aPointToInsert.IsSet());

  if (!aPointToInsert.IsInContentNode() ||
      NS_WARN_IF(!aPointToInsert.GetContainerOrContainerParentElement()))
      [[unlikely]] {
    return CreateLineBreakResult::NotHandled();
  }

  // FYI: We don't need to put <br> if it reaches an inline editing host because
  // editing host has at least one line height by default even if it's empty and
  // it's tested by WPT to no <br> element is inserted in the cases.

  // If the point is not start of a line, we don't need to put a line break
  // here.
  const WSScanResult previousThing =
      WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
          {WSRunScanner::Option::OnlyEditableNodes}, aPointToInsert,
          &aEditingHost);
  if (!previousThing.ReachedLineBoundary()) {
    return CreateLineBreakResult::NotHandled();
  }

  // If the point is not followed by a block boundary, we don't need to put a
  // line break here.
  const WSScanResult nextThing =
      WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
          {}, aPointToInsert,
          // FIXME: We should not limit the scan range into the editing host.
          // However, WSRunScanner does not check the visibility so that
          // following invisible text like the conent of <script> may change the
          // result. Fortunately, inline editing host is not used so widely.
          // We should treat the inline editing host boundary as a block
          // boundary.
          &aEditingHost);
  if (!nextThing.ReachedBlockBoundary() &&
      !nextThing.ReachedInlineEditingHostBoundary()) {
    return CreateLineBreakResult::NotHandled();
  }

  EditorDOMPoint pointToInsert(aPointToInsert);
  AutoTrackDOMPoint trackPointToInsert(RangeUpdaterRef(), &pointToInsert);

  // Okay, there is no meaningful content in the line. However, there might be
  // visible empty inline containers or some invisible nodes like Comment.
  // For the compatibility with the other browsers, we should put <br> as far as
  // near aPointToInsert.
  if (previousThing.ReachedPreformattedLineBreak() &&
      !EditorUtils::IsWhiteSpacePreformatted(*previousThing.TextPtr())) {
    const EditorDOMPoint pointAfterLineBreak =
        previousThing.PointAtReachedContent<EditorDOMPoint>();
    if (!pointAfterLineBreak.IsEndOfContainer()) [[unlikely]] {
      // If the previous thing is a preformatted line break but it's middle of a
      // Text, we want to delete the invisible trailing white-spaces.
      Result<CaretPoint, nsresult> caretPointOrError =
          WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces(
              *this, pointAfterLineBreak);
      if (caretPointOrError.isErr()) [[unlikely]] {
        NS_WARNING(
            "WhiteSpaceVisibilityKeeper::DeleteInvisibleASCIIWhiteSpaces() "
            "failed");
      }
      caretPointOrError.unwrap().IgnoreCaretPointSuggestion();
      trackPointToInsert.Flush(StopTracking::No);
    }
  }
  if (Element* const containerElement =
          pointToInsert.GetContainerOrContainerParentElement()) {
    if (!HTMLEditor::CanInsertLineBreak(LineBreakType::BRElement,
                                        *containerElement)) [[unlikely]] {
      // FIXME: We're deleting empty blocks at the post-processing after this
      // this called. Therefore, here may be in an empty list element.
      // Therefore, even if we cannot insert a <br>, we should return "not
      // handled" for now. We should make the delete handler delete empty blocks
      // by themselves and stop doing it in the post-processor. Then, return
      // error via PrepareToInsertLineBreak(). See bug 2019187.
      return CreateLineBreakResult::NotHandled();
    }
  } else {
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }
  Result<EditorDOMPoint, nsresult> pointToInsertOrError =
      PrepareToInsertLineBreak(LineBreakType::BRElement, pointToInsert);
  if (pointToInsertOrError.isErr()) [[unlikely]] {
    NS_WARNING(
        "HTMLEditor::PrepareToInsertLineBreak(LineBreakType::BRElement) "
        "failed");
    return pointToInsertOrError.propagateErr();
  }
  trackPointToInsert.Flush(StopTracking::Yes);
  EditorDOMPoint pointToPutCaret = pointToInsert;
  pointToInsert = pointToInsertOrError.unwrap();
  AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret);
  const BRElementType brElementType = [&]() {
    // The serializer requires a normal <br> in the empty block. See bug
    // 1385905.
    if (nextThing.ReachedCurrentBlockBoundary() &&
        previousThing.ReachedCurrentBlockBoundary()) {
      return BRElementType::Normal;
    }
    return BRElementType::PaddingForEmptyLastLine;
  }();
  Result<CreateElementResult, nsresult> insertLineBreakResultOrError =
      InsertBRElement(WithTransaction::Yes, brElementType, pointToInsert);
  if (insertLineBreakResultOrError.isErr()) [[unlikely]] {
    NS_WARNING(
        fmt::format(
            "HTMLEditor::InsertLineBreak(WithTransaction::Yes, {}) failed",
            brElementType)
            .c_str());
    return insertLineBreakResultOrError.propagateErr();
  }
  trackPointToPutCaret.Flush(StopTracking::Yes);
  return CreateLineBreakResult(insertLineBreakResultOrError.unwrap(),
                               std::move(pointToPutCaret));
}

Result<EditActionResult, nsresult>
HTMLEditor::MakeOrChangeListAndListItemAsSubAction(
    const nsStaticAtom& aListElementOrListItemElementTagName,
    const nsAString& aBulletType,
    SelectAllOfCurrentList aSelectAllOfCurrentList,
    const Element& aEditingHost) {
  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<EditActionResult, nsresult> 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
  //     `<dd>` or `<dt>` 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(aEditingHost);
    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);
  }

  // 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<EditorRawDOMRange, nsresult> 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.
    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);
  AutoClonedSelectionRangeArray selectionRanges(SelectionRef());
  Result<EditActionResult, nsresult> result = listCreator.Run(
      *this, selectionRanges, aSelectAllOfCurrentList, aEditingHost);
  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("AutoClonedSelectionRangeArray::ApplyTo() failed");
    return Err(rv);
  }
  return result.inspect().Ignored() ? EditActionResult::CanceledResult()
                                    : EditActionResult::HandledResult();
}

Result<EditActionResult, nsresult> HTMLEditor::AutoListElementCreator::Run(
    HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& 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 <br>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<RefPtr<Element>, 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("AutoClonedRangeArray::Collapse() failed");
      return Err(rv);
    }
    return EditActionResult::IgnoredResult();
  }

  Result<RefPtr<Element>, 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<nsresult> 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, AutoClonedRangeArray& 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<nsIContent>(*parentListElementOfRanges));
      return NS_OK;
    }
  }

  AutoClonedRangeArray extendedRanges(aRanges);

  // TODO: We don't need AutoTransactionsConserveSelection here in the
  //       normal cases, but removing this may cause the behavior with the
  //       legacy mutation event listeners.  We should try to delete this in
  //       a bug.
  AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);

  extendedRanges.ExtendRangesToWrapLines(EditSubAction::eCreateOrChangeList,
                                         BlockInlineCheck::UseHTMLDefaultStyle,
                                         aEditingHost);
  Result<EditorDOMPoint, nsresult> splitResult =
      extendedRanges.SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
          aHTMLEditor, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
  if (MOZ_UNLIKELY(splitResult.isErr())) {
    NS_WARNING(
        "AutoClonedRangeArray::"
        "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed");
    return splitResult.unwrapErr();
  }
  nsresult rv = extendedRanges.CollectEditTargetNodes(
      aHTMLEditor, aOutArrayOfContents, EditSubAction::eCreateOrChangeList,
      AutoClonedRangeArray::CollectNonEditableNodes::No);
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "AutoClonedRangeArray::CollectEditTargetNodes(EditSubAction::"
        "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
    return rv;
  }

  Result<EditorDOMPoint, nsresult> 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<nsIContent>& content : aArrayOfContents) {
    // if content is not a <br> or empty inline, we're done
    // XXX Should we handle line breaks in preformatted text node?
    if (!content->IsHTMLElement(nsGkAtoms::br) &&
        !HTMLEditUtils::IsEmptyInlineContainer(
            content,
            {EmptyCheckOption::TreatSingleBRElementAsVisible,
             EmptyCheckOption::TreatNonEditableContentAsInvisible},
            BlockInlineCheck::UseComputedDisplayStyle)) {
      return false;
    }
  }
  return true;
}

Result<RefPtr<Element>, nsresult>
HTMLEditor::AutoListElementCreator::ReplaceContentNodesWithEmptyNewList(
    HTMLEditor& aHTMLEditor, const AutoClonedRangeArray& aRanges,
    const AutoContentNodeArray& aArrayOfContents,
    const Element& aEditingHost) const {
  // if only breaks, delete them
  for (const OwningNonNull<nsIContent>& 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<EditorDOMPoint>();
  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<Element>();
  }

  RefPtr<Element> newListItemElement;
  Result<CreateElementResult, nsresult> 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<CreateElementResult, nsresult> 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<RefPtr<Element>, nsresult>
HTMLEditor::AutoListElementCreator::WrapContentNodesIntoNewListElements(
    HTMLEditor& aHTMLEditor, AutoClonedRangeArray& 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* const deepestDivBlockquoteOrListElement =
            HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild(
                aArrayOfContents[0], {LeafNodeOption::IgnoreNonEditableNode},
                BlockInlineCheck::UseHTMLDefaultStyle, nsGkAtoms::div,
                nsGkAtoms::blockquote, nsGkAtoms::ul, nsGkAtoms::ol,
                nsGkAtoms::dl)) {
      if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements(
              nsGkAtoms::div, nsGkAtoms::blockquote)) {
        aArrayOfContents.Clear();
        HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement,
                                       aArrayOfContents, 0, {});
      } else {
        aArrayOfContents.ReplaceElementAt(
            0, OwningNonNull<nsIContent>(*deepestDivBlockquoteOrListElement));
      }
    }
  }

  // Ok, now go through all the nodes and put then in the list,
  // or whatever is appropriate.  Wohoo!
  AutoHandlingState handlingState;
  for (const OwningNonNull<nsIContent>& 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 `<br>` element, delete it and forget previous
  // list item element.
  // If current node is an empty inline node, just delete it.
  if (EditorUtils::IsEditableContent(aHandlingContent, EditorType::HTML) &&
      (aHandlingContent.IsHTMLElement(nsGkAtoms::br) ||
       HTMLEditUtils::IsEmptyInlineContainer(
           aHandlingContent,
           {EmptyCheckOption::TreatSingleBRElementAsVisible,
            EmptyCheckOption::TreatNonEditableContentAsInvisible},
           BlockInlineCheck::UseHTMLDefaultStyle))) {
    nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aHandlingContent);
    if (NS_FAILED(rv)) {
      NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
      return rv;
    }
    if (aHandlingContent.IsHTMLElement(nsGkAtoms::br)) {
      aState.mPreviousListItemElement = nullptr;
    }
    return NS_OK;
  }

  // If we meet a list, we can reuse it or convert it to the expected type list.
  if (HTMLEditUtils::IsListElement(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::IsListItemElement(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 <div> or a <p>, we want only its children to wrapping into
  // list element.  Therefore, this call will call this recursively.
  if (aHandlingContent.IsAnyOfHTMLElements(nsGkAtoms::div, nsGkAtoms::p)) {
    nsresult rv = HandleChildDivOrParagraphElement(
        aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState,
        aEditingHost);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "AutoListElementCreator::HandleChildDivOrParagraphElement() failed");
    return rv;
  }

  // If we've not met a list element, create a list element and make it
  // current list element.
  if (!aState.mCurrentListElement) {
    nsresult rv = CreateAndUpdateCurrentListElement(
        aHTMLEditor, EditorDOMPoint(&aHandlingContent),
        EmptyListItem::NotCreate, aState, aEditingHost);
    if (NS_FAILED(rv)) {
      NS_WARNING("AutoListElementCreator::HandleChildInlineElement() failed");
      return rv;
    }
  }

  // If we meet an inline content, we want to move it to previously used list
  // item element or new list item element.
  if (HTMLEditUtils::IsInlineContent(aHandlingContent,
                                     BlockInlineCheck::UseHTMLDefaultStyle)) {
    nsresult rv =
        HandleChildInlineContent(aHTMLEditor, aHandlingContent, aState);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "AutoListElementCreator::HandleChildInlineElement() failed");
    return rv;
  }

  // Otherwise, we should wrap it into new list item element.
  nsresult rv =
      WrapContentIntoNewListItemElement(aHTMLEditor, aHandlingContent, aState);
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "AutoListElementCreator::WrapContentIntoNewListItemElement() failed");
  return rv;
}

nsresult HTMLEditor::AutoListElementCreator::HandleChildListElement(
    HTMLEditor& aHTMLEditor, Element& aHandlingListElement,
    AutoHandlingState& aState) const {
  MOZ_ASSERT(HTMLEditUtils::IsListElement(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, nsresult> 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<CreateElementResult, nsresult> convertListTypeResult =
        aHTMLEditor.ChangeListElementType(aHandlingListElement, mListTagName,
                                          mListItemTagName);
    if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
      NS_WARNING("HTMLEditor::ChangeListElementType() failed");
      return convertListTypeResult.propagateErr();
    }
    convertListTypeResult.inspect().IgnoreCaretPointSuggestion();

    Result<EditorDOMPoint, nsresult> 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<CreateElementResult, nsresult> 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::IsListItemElement(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<SplitNodeResult, nsresult> 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<CreateElementResult, nsresult> createNewListElementResult =
        aHTMLEditor.CreateAndInsertElement(
            WithTransaction::Yes, mListTagName,
            unwrappedSplitListItemParentResult.AtNextContent<EditorDOMPoint>());
    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, nsresult> 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<CreateElementResult, nsresult> 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::IsListItemElement(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::IsListItemElement(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<Element>();
    NS_WARNING_ASSERTION(
        HTMLEditUtils::IsListElement(*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, nsresult> 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<CreateElementResult, nsresult> 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<RefPtr<Element>> previouslyReplacingBlockElement(
      aState.mReplacingBlockElement);
  aState.mReplacingBlockElement = &aHandlingDivOrParagraphElement;
  AutoRestore<bool> previouslyReplacingBlockElementIdCopied(
      aState.mMaybeCopiedReplacingBlockElementId);
  aState.mMaybeCopiedReplacingBlockElementId = false;

  // If the <div> or <p> is empty, we should replace it with a list element
  // and/or a list item element.
  if (HTMLEditUtils::IsEmptyNode(aHandlingDivOrParagraphElement,
                                 {EmptyCheckOption::TreatListItemAsVisible,
                                  EmptyCheckOption::TreatTableCellAsVisible})) {
    if (!aState.mCurrentListElement) {
      nsresult rv = CreateAndUpdateCurrentListElement(
          aHTMLEditor, EditorDOMPoint(&aHandlingDivOrParagraphElement),
          EmptyListItem::Create, aState, aEditingHost);
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "AutoListElementCreator::CreateAndUpdateCurrentListElement("
            "EmptyListItem::Create) failed");
        return rv;
      }
    } else {
      Result<CreateElementResult, nsresult> 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 <div> 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<EditorDOMPoint, nsresult> unwrapDivElementResult =
      aHTMLEditor.RemoveContainerWithTransaction(
          aHandlingDivOrParagraphElement);
  if (MOZ_UNLIKELY(unwrapDivElementResult.isErr())) {
    NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
    return unwrapDivElementResult.unwrapErr();
  }

  for (const OwningNonNull<nsIContent>& 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<Element> 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<CreateElementResult, nsresult> 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<CreateElementResult, nsresult> 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<CreateElementResult, nsresult>
HTMLEditor::AutoListElementCreator::AppendListItemElement(
    HTMLEditor& aHTMLEditor, const Element& aListElement,
    AutoHandlingState& aState) const {
  const WithTransaction withTransaction = aListElement.IsInComposedDoc()
                                              ? WithTransaction::Yes
                                              : WithTransaction::No;
  Result<CreateElementResult, nsresult> createNewListItemResult =
      aHTMLEditor.CreateAndInsertElement(
          withTransaction, mListItemTagName,
          EditorDOMPoint::AtEndOf(aListElement),
          !aState.mReplacingBlockElement
              ? HTMLEditor::DoNothingForNewElement
              // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
              : [&aState](HTMLEditor& aHTMLEditor, Element& aListItemElement,
                          const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
                  nsresult rv =
                      AutoListElementCreator::MaybeCloneAttributesToNewListItem(
                          aHTMLEditor, aListItemElement, aState);
                  NS_WARNING_ASSERTION(
                      NS_SUCCEEDED(rv),
                      "AutoListElementCreator::"
                      "MaybeCloneAttributesToNewListItem() failed");
                  return rv;
                });
  NS_WARNING_ASSERTION(createNewListItemResult.isOk(),
                       "HTMLEditor::CreateAndInsertElement() failed");
  return createNewListItemResult;
}

nsresult HTMLEditor::AutoListElementCreator::HandleChildInlineContent(
    HTMLEditor& aHTMLEditor, nsIContent& aHandlingInlineContent,
    AutoHandlingState& aState) const {
  MOZ_ASSERT(HTMLEditUtils::IsInlineContent(
      aHandlingInlineContent, BlockInlineCheck::UseHTMLDefaultStyle));

  // If we're currently handling contents of a list item and current node
  // is not a block element, move current node into the list item.
  if (!aState.mPreviousListItemElement) {
    nsresult rv = WrapContentIntoNewListItemElement(
        aHTMLEditor, aHandlingInlineContent, aState);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "AutoListElementCreator::WrapContentIntoNewListItemElement() failed");
    return rv;
  }

  Result<MoveNodeResult, nsresult> 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<CreateElementResult, nsresult> 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<MoveNodeResult, nsresult> moveListItemElementResult =
      aHTMLEditor.MoveNodeToEndWithTransaction(
          MOZ_KnownLive(
              *unwrappedWrapContentInListItemElementResult.GetNewNode()),
          MOZ_KnownLive(*aState.mCurrentListElement));
  if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
    NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
    return moveListItemElementResult.unwrapErr();
  }
  moveListItemElementResult.inspect().IgnoreCaretPointSuggestion();

  // If current node is not a block element, new list item should have
  // following inline nodes too.
  if (HTMLEditUtils::IsInlineContent(aHandlingContent,
                                     BlockInlineCheck::UseHTMLDefaultStyle)) {
    aState.mPreviousListItemElement =
        unwrappedWrapContentInListItemElementResult.UnwrapNewNode();
  } else {
    aState.mPreviousListItemElement = nullptr;
  }

  // XXX Why don't we set `type` attribute here??
  return NS_OK;
}

nsresult HTMLEditor::AutoListElementCreator::
    EnsureCollapsedRangeIsInListItemOrListElement(
        Element& aListItemOrListToPutCaret,
        AutoClonedRangeArray& aRanges) const {
  if (!aRanges.IsCollapsed() || aRanges.Ranges().IsEmpty()) {
    return NS_OK;
  }

  const auto firstRangeStartPoint =
      aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
  if (MOZ_UNLIKELY(!firstRangeStartPoint.IsSet())) {
    return NS_OK;
  }
  Result<EditorRawDOMPoint, nsresult> 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("AutoClonedRangeArray::Collapse() failed");
      return rv;
    }
  }
  return NS_OK;
}

nsresult HTMLEditor::RemoveListAtSelectionAsSubAction(
    const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  {
    Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
    if (MOZ_UNLIKELY(result.isErr())) {
      NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
      return result.unwrapErr();
    }
    if (result.inspect().Canceled()) {
      return NS_OK;
    }
  }

  AutoPlaceholderBatch treatAsOneTransaction(
      *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
  IgnoredErrorResult 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<EditorRawDOMRange, nsresult> 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<OwningNonNull<nsIContent>, 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);

    {
      AutoClonedSelectionRangeArray extendedSelectionRanges(SelectionRef());
      extendedSelectionRanges.ExtendRangesToWrapLines(
          EditSubAction::eCreateOrChangeList,
          BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
      Result<EditorDOMPoint, nsresult> splitResult =
          extendedSelectionRanges
              .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
                  *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
      if (MOZ_UNLIKELY(splitResult.isErr())) {
        NS_WARNING(
            "AutoClonedRangeArray::"
            "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
            "failed");
        return splitResult.unwrapErr();
      }
      nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
          *this, arrayOfContents, EditSubAction::eCreateOrChangeList,
          AutoClonedRangeArray::CollectNonEditableNodes::No);
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "AutoClonedRangeArray::CollectEditTargetNodes(EditSubAction::"
            "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
        return rv;
      }
    }

    const Result<EditorDOMPoint, nsresult> 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--) {
    const OwningNonNull<nsIContent>& content = arrayOfContents[i];
    if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
      arrayOfContents.RemoveElementAt(i);
    }
  }

  // Only act on lists or list items in the array
  for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
    // here's where we actually figure out what to do
    if (HTMLEditUtils::IsListItemElement(*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::IsListElement(*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<RefPtr<Element>, nsresult>
HTMLEditor::FormatBlockContainerWithTransaction(
    AutoClonedSelectionRangeArray& aSelectionRanges,
    const nsStaticAtom& aNewFormatTagName, FormatBlockMode aFormatBlockMode,
    const Element& aEditingHost) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());

  // XXX Why do we do this only when there is only one selection range?
  if (!aSelectionRanges.IsCollapsed() &&
      aSelectionRanges.Ranges().Length() == 1u) {
    Result<EditorRawDOMRange, nsresult> 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 AutoClonedRangeArray::SetStartAndEnd() here.
    if (NS_FAILED(aSelectionRanges.SetBaseAndExtent(
            extendedRange.inspect().StartRef(),
            extendedRange.inspect().EndRef()))) {
      NS_WARNING("AutoClonedRangeArray::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<OwningNonNull<nsIContent>, 64> arrayOfContents;
  aSelectionRanges.ExtendRangesToWrapLines(
      aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
          ? EditSubAction::eFormatBlockForHTMLCommand
          : EditSubAction::eCreateOrRemoveBlock,
      BlockInlineCheck::UseComputedDisplayOutsideStyle, aEditingHost);
  Result<EditorDOMPoint, nsresult> splitResult =
      aSelectionRanges
          .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
              *this, BlockInlineCheck::UseComputedDisplayOutsideStyle,
              aEditingHost);
  if (MOZ_UNLIKELY(splitResult.isErr())) {
    NS_WARNING(
        "AutoClonedRangeArray::"
        "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed");
    return splitResult.propagateErr();
  }
  nsresult rv = aSelectionRanges.CollectEditTargetNodes(
      *this, arrayOfContents,
      aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
          ? EditSubAction::eFormatBlockForHTMLCommand
          : EditSubAction::eCreateOrRemoveBlock,
      AutoClonedRangeArray::CollectNonEditableNodes::Yes);
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "AutoClonedRangeArray::CollectEditTargetNodes(CollectNonEditableNodes::"
        "No) failed");
    return Err(rv);
  }

  Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
      MaybeSplitElementsAtEveryBRElement(
          arrayOfContents,
          aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
              ? EditSubAction::eFormatBlockForHTMLCommand
              : EditSubAction::eCreateOrRemoveBlock);
  if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
    NS_WARNING("HTMLEditor::MaybeSplitElementsAtEveryBRElement() failed");
    return splitAtBRElementsResult.propagateErr();
  }

  // If there is no visible and editable nodes in the edit targets, make an
  // empty block.
  // XXX Isn't this odd if there are only non-editable visible nodes?
  if (HTMLEditUtils::IsEmptyOneHardLine(
          arrayOfContents, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
    if (NS_WARN_IF(aSelectionRanges.Ranges().IsEmpty())) {
      return Err(NS_ERROR_FAILURE);
    }

    auto pointToInsertBlock =
        aSelectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
    if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand &&
        (&aNewFormatTagName == nsGkAtoms::normal ||
         &aNewFormatTagName == nsGkAtoms::_empty)) {
      if (!pointToInsertBlock.IsInContentNode()) {
        NS_WARNING(
            "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find "
            "block parent because container of the point is not content");
        return Err(NS_ERROR_FAILURE);
      }
      // We are removing blocks (going to "body text")
      const RefPtr<Element> editableBlockElement =
          HTMLEditUtils::GetInclusiveAncestorElement(
              *pointToInsertBlock.ContainerAs<nsIContent>(),
              HTMLEditUtils::ClosestEditableBlockElement,
              BlockInlineCheck::UseComputedDisplayOutsideStyle);
      if (!editableBlockElement) {
        NS_WARNING(
            "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find "
            "block parent");
        return Err(NS_ERROR_FAILURE);
      }
      if (editableBlockElement->IsAnyOfHTMLElements(
              nsGkAtoms::dd, nsGkAtoms::dl, nsGkAtoms::dt) ||
          !HTMLEditUtils::IsFormatElementForParagraphStateCommand(
              *editableBlockElement)) {
        return RefPtr<Element>();
      }

      // 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<nsIContent> brContent = HTMLEditUtils::GetNextLeafContent(
              pointToInsertBlock, {LeafNodeOption::IgnoreNonEditableNode},
              BlockInlineCheck::UseComputedDisplayOutsideStyle,
              &aEditingHost)) {
        if (brContent && brContent->IsHTMLElement(nsGkAtoms::br)) {
          AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
          nsresult rv = DeleteNodeWithTransaction(*brContent);
          if (NS_FAILED(rv)) {
            NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
            return Err(rv);
          }
        }
      }
      // Do the splits!
      Result<SplitNodeResult, nsresult> 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 <br> element at the split point
      Result<CreateLineBreakResult, nsresult> insertBRElementResultOrError =
          InsertLineBreak(
              WithTransaction::Yes, LineBreakType::BRElement,
              unwrappedSplitNodeResult.AtSplitPoint<EditorDOMPoint>());
      if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) {
        NS_WARNING(
            "HTMLEditor::InsertLineBreak(WithTransaction::Yes "
            "LineBreakType::BRElement) failed");
        return insertBRElementResultOrError.propagateErr();
      }
      CreateLineBreakResult insertBRElementResult =
          insertBRElementResultOrError.unwrap();
      MOZ_ASSERT(insertBRElementResult.Handled());
      aSelectionRanges.ClearSavedRanges();
      nsresult rv =
          aSelectionRanges.Collapse(insertBRElementResult.UnwrapCaretPoint());
      if (NS_FAILED(rv)) {
        NS_WARNING("AutoClonedRangeArray::Collapse() failed");
        return Err(rv);
      }
      return RefPtr<Element>();
    }

    // We are making a block.  Consume a br, if needed.
    if (nsCOMPtr<nsIContent> maybeBRContent =
            HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
                pointToInsertBlock,
                {LeafNodeOption::IgnoreNonEditableNode,
                 LeafNodeOption::TreatChildBlockAsLeafNode},
                BlockInlineCheck::UseComputedDisplayOutsideStyle,
                &aEditingHost)) {
      if (maybeBRContent->IsHTMLElement(nsGkAtoms::br)) {
        AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
        nsresult rv = DeleteNodeWithTransaction(*maybeBRContent);
        if (NS_FAILED(rv)) {
          NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
          return Err(rv);
        }
        // We don't need to act on this node any more
        arrayOfContents.RemoveElement(maybeBRContent);
      }
    }
    // Make sure we can put a block here.
    Result<CreateElementResult, nsresult> createNewBlockElementResult =
        InsertElementWithSplittingAncestorsWithTransaction(
            aNewFormatTagName, pointToInsertBlock,
            BRElementNextToSplitPoint::Keep, aEditingHost);
    if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
      NS_WARNING(
          nsPrintfCString(
              "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
              "%s) failed",
              nsAtomCString(&aNewFormatTagName).get())
              .get());
      return createNewBlockElementResult.propagateErr();
    }
    CreateElementResult unwrappedCreateNewBlockElementResult =
        createNewBlockElementResult.unwrap();
    unwrappedCreateNewBlockElementResult.IgnoreCaretPointSuggestion();
    MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode());

    // Delete anything that was in the list of nodes
    while (!arrayOfContents.IsEmpty()) {
      OwningNonNull<nsIContent>& 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("AutoClonedRangeArray::Collapse() failed");
      return Err(rv);
    }
    return unwrappedCreateNewBlockElementResult.UnwrapNewNode();
  }

  if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand) {
    // Okay, now go through all the nodes and make the right kind of blocks, or
    // whatever is appropriate.
    // Note: blockquote is handled a little differently.
    if (&aNewFormatTagName == nsGkAtoms::blockquote) {
      Result<CreateElementResult, nsresult>
          wrapContentsInBlockquoteElementsResult =
              WrapContentsInBlockquoteElementsWithTransaction(arrayOfContents,
                                                              aEditingHost);
      if (MOZ_UNLIKELY(wrapContentsInBlockquoteElementsResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction() "
            "failed");
        return wrapContentsInBlockquoteElementsResult.propagateErr();
      }
      wrapContentsInBlockquoteElementsResult.inspect()
          .IgnoreCaretPointSuggestion();
      return wrapContentsInBlockquoteElementsResult.unwrap().UnwrapNewNode();
    }
    if (&aNewFormatTagName == nsGkAtoms::normal ||
        &aNewFormatTagName == nsGkAtoms::_empty) {
      Result<EditorDOMPoint, nsresult> removeBlockContainerElementsResult =
          RemoveBlockContainerElementsWithTransaction(
              arrayOfContents, FormatBlockMode::XULParagraphStateCommand,
              BlockInlineCheck::UseComputedDisplayOutsideStyle);
      if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed");
        return removeBlockContainerElementsResult.propagateErr();
      }
      return RefPtr<Element>();
    }
  }

  Result<CreateElementResult, nsresult> wrapContentsInBlockElementResult =
      CreateOrChangeFormatContainerElement(arrayOfContents, aNewFormatTagName,
                                           aFormatBlockMode, aEditingHost);
  if (MOZ_UNLIKELY(wrapContentsInBlockElementResult.isErr())) {
    NS_WARNING("HTMLEditor::CreateOrChangeFormatContainerElement() failed");
    return wrapContentsInBlockElementResult.propagateErr();
  }
  wrapContentsInBlockElementResult.inspect().IgnoreCaretPointSuggestion();
  return wrapContentsInBlockElementResult.unwrap().UnwrapNewNode();
}

Result<EditActionResult, nsresult> 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<EditActionResult, nsresult> 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<EditActionResult, nsresult> 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);
  }

  if (!SelectionRef().IsCollapsed()) {
    return result;
  }

  // TODO: Investigate when we need to put a `<br>` element after indenting
  //       ranges.  Then, we could stop calling this here, or maybe we need to
  //       do it while moving content nodes.
  const auto caretPosition =
      EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
  Result<CreateLineBreakResult, nsresult> insertPaddingBRElementResultOrError =
      InsertPaddingBRElementIfInEmptyBlock(caretPosition, eNoStrip);
  if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
    NS_WARNING(
        "HTMLEditor::InsertPaddingBRElementIfInEmptyBlock(eNoStrip) failed");
    return insertPaddingBRElementResultOrError.propagateErr();
  }
  nsresult rv =
      insertPaddingBRElementResultOrError.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 result;
}

Result<EditorDOMPoint, nsresult> HTMLEditor::IndentListChildWithTransaction(
    RefPtr<Element>* aSubListElement, const EditorDOMPoint& aPointInListElement,
    nsIContent& aContentMovingToSubList, const Element& aEditingHost) {
  MOZ_ASSERT(aPointInListElement.IsInContentNode());
  MOZ_ASSERT(HTMLEditUtils::IsListElement(
      *aPointInListElement.ContainerAs<nsIContent>()));
  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* const nextEditableSibling = HTMLEditUtils::GetNextSibling(
          aContentMovingToSubList,
          {LeafNodeOption::IgnoreInvisibleText,
           LeafNodeOption::IgnoreNonEditableNode},
          BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
    if (HTMLEditUtils::IsListElement(*nextEditableSibling) &&
        aPointInListElement.GetContainer()->NodeInfo()->NameAtom() ==
            nextEditableSibling->NodeInfo()->NameAtom() &&
        aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() ==
            nextEditableSibling->NodeInfo()->NamespaceID()) {
      Result<MoveNodeResult, nsresult> 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 (const nsCOMPtr<nsIContent> previousEditableSibling =
          HTMLEditUtils::GetPreviousSibling(
              aContentMovingToSubList,
              {LeafNodeOption::IgnoreInvisibleText,
               LeafNodeOption::IgnoreNonEditableNode},
              BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
    if (HTMLEditUtils::IsListElement(*previousEditableSibling) &&
        aPointInListElement.GetContainer()->NodeInfo()->NameAtom() ==
            previousEditableSibling->NodeInfo()->NameAtom() &&
        aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() ==
            previousEditableSibling->NodeInfo()->NamespaceID()) {
      Result<MoveNodeResult, nsresult> 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,
                             {LeafNodeOption::IgnoreInvisibleText,
                              LeafNodeOption::IgnoreNonEditableNode},
                             BlockInlineCheck::UseComputedDisplayOutsideStyle)
                       : nullptr;
  if (!*aSubListElement || (previousEditableSibling &&
                            previousEditableSibling != *aSubListElement)) {
    nsAtom* containerName =
        aPointInListElement.GetContainer()->NodeInfo()->NameAtom();
    // Create a new nested list of correct type.
    Result<CreateElementResult, nsresult> 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<Element> subListElement = *aSubListElement;
  Result<MoveNodeResult, nsresult> 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<EditActionResult, nsresult> 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(aEditingHost);
    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");
    }
  }

  AutoClonedSelectionRangeArray 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(
        "AutoClonedSelectionRangeArray::ApplyTo() caused destroying the "
        "editor");
    return Err(NS_ERROR_EDITOR_DESTROYED);
  }
  if (NS_FAILED(rv)) {
    NS_WARNING("AutoClonedSelectionRangeArray::ApplyTo() failed");
    return Err(rv);
  }
  return EditActionResult::HandledResult();
}

nsresult HTMLEditor::HandleCSSIndentAroundRanges(
    AutoClonedSelectionRangeArray& 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<EditorRawDOMRange, nsresult> 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("AutoClonedRangeArray::SetBaseAndExtent() failed");
      return rv;
    }
  }

  if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) {
    return NS_ERROR_FAILURE;
  }

  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;

  // short circuit: detect case of collapsed selection inside an <li>.
  // just sublist that <li>.  This prevents bug 97797.

  if (aRanges.IsCollapsed()) {
    const auto atCaret = aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
    if (NS_WARN_IF(!atCaret.IsSet())) {
      return NS_ERROR_FAILURE;
    }
    MOZ_ASSERT(atCaret.IsInContentNode());
    Element* const editableBlockElement =
        HTMLEditUtils::GetInclusiveAncestorElement(
            *atCaret.ContainerAs<nsIContent>(),
            HTMLEditUtils::ClosestEditableBlockElement,
            BlockInlineCheck::UseHTMLDefaultStyle);
    if (editableBlockElement &&
        HTMLEditUtils::IsListItemElement(*editableBlockElement)) {
      arrayOfContents.AppendElement(*editableBlockElement);
    }
  }

  EditorDOMPoint pointToPutCaret;
  if (arrayOfContents.IsEmpty()) {
    {
      AutoClonedSelectionRangeArray extendedRanges(aRanges);
      extendedRanges.ExtendRangesToWrapLines(
          EditSubAction::eIndent, BlockInlineCheck::UseHTMLDefaultStyle,
          aEditingHost);
      Result<EditorDOMPoint, nsresult> splitResult =
          extendedRanges
              .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
                  *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
      if (MOZ_UNLIKELY(splitResult.isErr())) {
        NS_WARNING(
            "AutoClonedRangeArray::"
            "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
            "failed");
        return splitResult.unwrapErr();
      }
      if (splitResult.inspect().IsSet()) {
        pointToPutCaret = splitResult.unwrap();
      }
      nsresult rv = extendedRanges.CollectEditTargetNodes(
          *this, arrayOfContents, EditSubAction::eIndent,
          AutoClonedRangeArray::CollectNonEditableNodes::Yes);
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "AutoClonedRangeArray::CollectEditTargetNodes(EditSubAction::"
            "eIndent, CollectNonEditableNodes::Yes) failed");
        return rv;
      }
    }
    Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
        MaybeSplitElementsAtEveryBRElement(arrayOfContents,
                                           EditSubAction::eIndent);
    if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
      NS_WARNING(
          "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
          "eIndent) failed");
      return splitAtBRElementsResult.inspectErr();
    }
    if (splitAtBRElementsResult.inspect().IsSet()) {
      pointToPutCaret = splitAtBRElementsResult.unwrap();
    }
  }

  // If there is no visible and editable nodes in the edit targets, make an
  // empty block.
  // XXX Isn't this odd if there are only non-editable visible nodes?
  if (HTMLEditUtils::IsEmptyOneHardLine(
          arrayOfContents, BlockInlineCheck::UseHTMLDefaultStyle)) {
    const EditorDOMPoint pointToInsertDivElement =
        pointToPutCaret.IsSet()
            ? std::move(pointToPutCaret)
            : aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
    if (NS_WARN_IF(!pointToInsertDivElement.IsSet())) {
      return NS_ERROR_FAILURE;
    }

    // make sure we can put a block here
    Result<CreateElementResult, nsresult> 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<Element> newDivElement =
        unwrappedCreateNewDivElementResult.UnwrapNewNode();
    MOZ_ASSERT(newDivElement);
    const Result<EditorDOMPoint, nsresult> 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<nsIContent>& 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),
                         "AutoClonedRangeArray::Collapse() failed");
    return rv;
  }

  RefPtr<Element> 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<EditorRawDOMPoint>();
    if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) {
      return NS_OK;
    }
    Result<EditorRawDOMPoint, nsresult> 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 <div> elements which have start margin.
  RefPtr<Element> subListElement, divElement;
  for (size_t i = 0; i < arrayOfContents.Length(); i++) {
    const OwningNonNull<nsIContent>& content = arrayOfContents[i];

    // Here's where we actually figure out what to do.
    EditorDOMPoint atContent(content);
    if (NS_WARN_IF(!atContent.IsInContentNode())) {
      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::IsListElement(*atContent.ContainerAs<nsIContent>())) {
      const RefPtr<Element> oldSubListElement = subListElement;
      // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
      // keep it alive.
      Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
          IndentListChildWithTransaction(&subListElement, atContent,
                                         MOZ_KnownLive(content), aEditingHost);
      if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
        NS_WARNING("HTMLEditor::IndentListChildWithTransaction() failed");
        return pointToPutCaretOrError.unwrapErr();
      }
      if (subListElement != oldSubListElement) {
        // New list element is created, so we should put caret into the new list
        // element.
        latestNewBlockElement = subListElement;
      }
      if (pointToPutCaretOrError.inspect().IsSet()) {
        pointToPutCaret = pointToPutCaretOrError.unwrap();
      }
      continue;
    }

    // Not a list item.

    if (HTMLEditUtils::IsBlockElement(content,
                                      BlockInlineCheck::UseHTMLDefaultStyle)) {
      Result<EditorDOMPoint, nsresult> 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<CreateElementResult, nsresult> 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<EditorDOMPoint, nsresult> 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;
    }

    const auto IsMovableContentSibling = [&](const nsIContent& aContent) {
      return HTMLEditUtils::IsSimplyEditableNode(aContent) &&
             !HTMLEditUtils::IsBlockElement(
                 aContent, BlockInlineCheck::UseHTMLDefaultStyle);
    };
    MOZ_ASSERT(IsMovableContentSibling(content));
    const OwningNonNull<nsIContent> lastContent = [&]() {
      nsIContent* lastContent = content;
      for (; i + 1 < arrayOfContents.Length(); i++) {
        nsIContent* const nextContent = arrayOfContents[i + 1];
        if (lastContent->GetNextSibling() != nextContent ||
            !IsMovableContentSibling(*nextContent)) {
          break;
        }
        lastContent = nextContent;
      }
      return OwningNonNull<nsIContent>(*lastContent);
    }();
    // Move the content into the <div> which has start margin.
    // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
    // keep it alive.
    Result<MoveNodeResult, nsresult> moveNodeResult =
        MoveSiblingsToEndWithTransaction(MOZ_KnownLive(content), lastContent,
                                         *divElement);
    if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
      NS_WARNING("HTMLEditor::MoveSiblingsToEndWithTransaction() 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(
    AutoClonedSelectionRangeArray& 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<EditorRawDOMRange, nsresult> 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("AutoClonedRangeArray::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<OwningNonNull<nsIContent>, 64> arrayOfContents;
  {
    AutoClonedSelectionRangeArray extendedRanges(aRanges);
    extendedRanges.ExtendRangesToWrapLines(
        EditSubAction::eIndent, BlockInlineCheck::UseHTMLDefaultStyle,
        aEditingHost);
    Result<EditorDOMPoint, nsresult> splitResult =
        extendedRanges
            .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
                *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
    if (MOZ_UNLIKELY(splitResult.isErr())) {
      NS_WARNING(
          "AutoClonedRangeArray::"
          "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
          "failed");
      return splitResult.unwrapErr();
    }
    if (splitResult.inspect().IsSet()) {
      pointToPutCaret = splitResult.unwrap();
    }
    nsresult rv = extendedRanges.CollectEditTargetNodes(
        *this, arrayOfContents, EditSubAction::eIndent,
        AutoClonedRangeArray::CollectNonEditableNodes::Yes);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "AutoClonedRangeArray::CollectEditTargetNodes(EditSubAction::eIndent,"
          " CollectNonEditableNodes::Yes) failed");
      return rv;
    }
  }

  // FIXME: Split ancestors when we consider to indent the range.
  Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
      MaybeSplitElementsAtEveryBRElement(arrayOfContents,
                                         EditSubAction::eIndent);
  if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
    NS_WARNING(
        "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::eIndent)"
        " failed");
    return splitAtBRElementsResult.inspectErr();
  }
  if (splitAtBRElementsResult.inspect().IsSet()) {
    pointToPutCaret = splitAtBRElementsResult.unwrap();
  }

  // If there is no visible and editable nodes in the edit targets, make an
  // empty block.
  // XXX Isn't this odd if there are only non-editable visible nodes?
  if (HTMLEditUtils::IsEmptyOneHardLine(
          arrayOfContents, BlockInlineCheck::UseHTMLDefaultStyle)) {
    const EditorDOMPoint pointToInsertBlockquoteElement =
        pointToPutCaret.IsSet()
            ? std::move(pointToPutCaret)
            : EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
    if (NS_WARN_IF(!pointToInsertBlockquoteElement.IsSet())) {
      return NS_ERROR_FAILURE;
    }

    // If there is no element which can have <blockquote>, abort.
    if (NS_WARN_IF(!HTMLEditUtils::GetInsertionPointInInclusiveAncestor(
                        *nsGkAtoms::blockquote, pointToInsertBlockquoteElement,
                        &aEditingHost)
                        .IsSet())) {
      return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
    }

    // Make sure we can put a block here.
    // XXX Unfortunately, this calls
    // MaybeSplitAncestorsForInsertWithTransaction() then,
    // HTMLEditUtils::GetInsertionPointInInclusiveAncestor() is called again.
    Result<CreateElementResult, nsresult> 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<Element> 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<nsIContent>& 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<Element> 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<EditorRawDOMPoint>();
    if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) {
      return NS_OK;
    }
    Result<EditorRawDOMPoint, nsresult> 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<Element> subListElement, blockquoteElement, indentedListItemElement;
  for (size_t i = 0; i < arrayOfContents.Length(); i++) {
    const OwningNonNull<nsIContent>& content = arrayOfContents[i];

    // Here's where we actually figure out what to do.
    EditorDOMPoint atContent(content);
    if (NS_WARN_IF(!atContent.IsInContentNode())) {
      continue;
    }

    const auto IsNotHandlableContent = [](const nsIContent& aContent) {
      // Ignore all non-editable nodes.  Leave them be.
      // XXX We ignore non-editable nodes here, but not so in the above
      // block.
      return !EditorUtils::IsEditableContent(aContent, EditorType::HTML) ||
             !HTMLEditUtils::IsRemovableNode(aContent);
    };

    const auto IsMovableContentSibling = [&](const nsIContent& aContent) {
      return !IsNotHandlableContent(aContent) &&
             !HTMLEditUtils::IsListItemElement(aContent);
    };

    if (IsNotHandlableContent(content)) {
      continue;
    }

    // If the content has been moved to different place, ignore it.
    if (!content->IsInclusiveDescendantOf(&aEditingHost)) {
      continue;
    }

    if (HTMLEditUtils::IsListElement(*atContent.ContainerAs<nsIContent>())) {
      const RefPtr<Element> oldSubListElement = subListElement;
      // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
      // keep it alive.
      Result<EditorDOMPoint, nsresult> 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<Element> listItem =
            HTMLEditUtils::GetClosestInclusiveAncestorListItemElement(
                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* const previousEditableSibling =
          subListElement
              ? HTMLEditUtils::GetPreviousSibling(
                    *listItem, {LeafNodeOption::IgnoreNonEditableNode},
                    BlockInlineCheck::UseComputedDisplayOutsideStyle)
              : 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<CreateElementResult, nsresult> 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<MoveNodeResult, nsresult> 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 <blockquote> 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<CreateElementResult, nsresult> 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;
    }

    MOZ_ASSERT(IsMovableContentSibling(content));
    const OwningNonNull<nsIContent> lastContent = [&]() {
      nsIContent* lastContent = content;
      for (; i + 1 < arrayOfContents.Length(); i++) {
        const OwningNonNull<nsIContent>& nextContent = arrayOfContents[i + 1];
        if (lastContent->GetNextSibling() != nextContent ||
            !IsMovableContentSibling(nextContent)) {
          break;
        }
        lastContent = nextContent;
      }
      return OwningNonNull<nsIContent>(*lastContent);
    }();
    // tuck the node into the end of the active blockquote
    // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
    // keep it alive.
    Result<MoveNodeResult, nsresult> moveNodeResult =
        MoveSiblingsToEndWithTransaction(MOZ_KnownLive(content), lastContent,
                                         *blockquoteElement);
    if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
      NS_WARNING("HTMLEditor::MoveSiblingsToEndWithTransaction() 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<EditActionResult, nsresult> 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<EditActionResult, nsresult> 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<EditActionResult, nsresult> 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);
  }

  if (!SelectionRef().IsCollapsed()) {
    return result;
  }

  const auto caretPosition =
      EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
  Result<CreateLineBreakResult, nsresult> insertPaddingBRElementResultOrError =
      InsertPaddingBRElementIfInEmptyBlock(caretPosition, eNoStrip);
  if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
    NS_WARNING(
        "HTMLEditor::InsertPaddingBRElementIfInEmptyBlock(eNoStrip) failed");
    return insertPaddingBRElementResultOrError.propagateErr();
  }
  nsresult rv =
      insertPaddingBRElementResultOrError.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 result;
}

Result<EditActionResult, nsresult> 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<EditorRawDOMRange, nsresult> 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<SplitRangeOffFromNodeResult, nsresult> 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.GetContainer() ==
            unwrappedOutdentResult.GetLeftContent() ||
        EditorUtils::IsDescendantOf(*atStartOfSelection.GetContainer(),
                                    *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.GetContainer() ==
            unwrappedOutdentResult.GetRightContent() ||
        EditorUtils::IsDescendantOf(
            *atStartOfSelection.GetContainer(),
            *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<SplitRangeOffFromNodeResult, nsresult>
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<OwningNonNull<nsIContent>, 64> arrayOfContents;
  {
    AutoClonedSelectionRangeArray extendedSelectionRanges(SelectionRef());
    extendedSelectionRanges.ExtendRangesToWrapLines(
        EditSubAction::eOutdent, BlockInlineCheck::UseHTMLDefaultStyle,
        aEditingHost);
    nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
        *this, arrayOfContents, EditSubAction::eOutdent,
        AutoClonedRangeArray::CollectNonEditableNodes::Yes);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "AutoClonedRangeArray::CollectEditTargetNodes(EditSubAction::"
          "eOutdent, CollectNonEditableNodes::Yes) failed");
      return Err(rv);
    }
    Result<EditorDOMPoint, nsresult> 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<nsIContent> leftContentOfLastOutdented;
  nsCOMPtr<nsIContent> middleContentOfLastOutdented;
  nsCOMPtr<nsIContent> rightContentOfLastOutdented;
  RefPtr<Element> indentedParentElement;
  nsCOMPtr<nsIContent> firstContentToBeOutdented, lastContentToBeOutdented;
  BlockIndentedWith indentedParentIndentedWith = BlockIndentedWith::HTML;
  for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
    // Here's where we actually figure out what to do
    EditorDOMPoint atContent(content);
    if (NS_WARN_IF(!atContent.IsInContentNode())) {
      continue;
    }

    // If it's a `<blockquote>`, 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 <blockquote>");
        Result<SplitRangeOffFromNodeResult, nsresult> 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<EditorDOMPoint, nsresult> unwrapBlockquoteElementResult =
          RemoveBlockContainerWithTransaction(
              MOZ_KnownLive(*content->AsElement()));
      if (MOZ_UNLIKELY(unwrapBlockquoteElementResult.isErr())) {
        NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
        return unwrapBlockquoteElementResult.propagateErr();
      }
      const EditorDOMPoint& pointToPutCaret =
          unwrapBlockquoteElementResult.inspect();
      if (AllowsTransactionsToChangeSelection() && pointToPutCaret.IsSet()) {
        nsresult rv = CollapseSelectionTo(pointToPutCaret);
        if (NS_FAILED(rv)) {
          NS_WARNING("EditorBase::CollapseSelectionTo() failed");
          return Err(rv);
        }
      }
      continue;
    }

    // If we're using CSS and the node is a block element, check its start
    // margin whether it's indented with CSS.
    if (useCSS && HTMLEditUtils::IsBlockElement(
                      content, BlockInlineCheck::UseHTMLDefaultStyle)) {
      nsStaticAtom& marginProperty =
          MarginPropertyAtomForIndent(MOZ_KnownLive(content));
      if (NS_WARN_IF(Destroyed())) {
        return Err(NS_ERROR_EDITOR_DESTROYED);
      }
      nsAutoString value;
      DebugOnly<nsresult> 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<nsAtom> unit;
      CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit));
      // If indented with CSS, we should decrease the start margin.
      if (startMargin > 0) {
        const Result<EditorDOMPoint, nsresult> 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::IsListItemElement(*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<SplitRangeOffFromNodeResult, nsresult> 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<SplitRangeOffFromNodeResult, nsresult> 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<nsIContent> parentContent = content->GetParent();
         parentContent && !parentContent->IsHTMLElement(nsGkAtoms::body) &&
         parentContent != &aEditingHost &&
         (parentContent->IsHTMLElement(nsGkAtoms::table) ||
          !HTMLEditUtils::IsAnyTableElementExceptColumnElement(*parentContent));
         parentContent = parentContent->GetParent()) {
      if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(*parentContent))) {
        continue;
      }
      // If we reach a `<blockquote>` 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<nsINode> 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<nsresult> 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<nsAtom> 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::IsListElement(
                                   *atContent.ContainerAs<nsIContent>()) &&
                               HTMLEditUtils::IsListElement(*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., `<ul>` may have `<dd>` as its
    //     list item element.
    if (HTMLEditUtils::IsListElement(*atContent.ContainerAs<nsIContent>())) {
      if (!HTMLEditUtils::IsListElement(*content)) {
        continue;
      }
      // Just unwrap this sublist
      Result<EditorDOMPoint, nsresult> unwrapSubListElementResult =
          RemoveBlockContainerWithTransaction(
              MOZ_KnownLive(*content->AsElement()));
      if (MOZ_UNLIKELY(unwrapSubListElementResult.isErr())) {
        NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
        return unwrapSubListElementResult.propagateErr();
      }
      const EditorDOMPoint& pointToPutCaret =
          unwrapSubListElementResult.inspect();
      if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
        continue;
      }
      nsresult rv = CollapseSelectionTo(pointToPutCaret);
      if (NS_FAILED(rv)) {
        NS_WARNING("EditorBase::CollapseSelectionTo() failed");
        return Err(rv);
      }
      continue;
    }

    // If current content is a list element but its parent is not a list
    // element, move children to where it is and remove it from the tree.
    if (HTMLEditUtils::IsListElement(*content)) {
      // XXX If mutation event listener appends new children forever, this
      //     becomes an infinite loop so that we should set limitation from
      //     first child count.
      for (nsCOMPtr<nsIContent> lastChildContent = content->GetLastChild();
           lastChildContent; lastChildContent = content->GetLastChild()) {
        if (HTMLEditUtils::IsListItemElement(*lastChildContent)) {
          nsresult rv = LiftUpListItemElement(
              MOZ_KnownLive(*lastChildContent->AsElement()),
              LiftUpFromAllParentListElements::No);
          if (NS_FAILED(rv)) {
            NS_WARNING(
                "HTMLEditor::LiftUpListItemElement("
                "LiftUpFromAllParentListElements::No) failed");
            return Err(rv);
          }
          continue;
        }

        if (HTMLEditUtils::IsListElement(*lastChildContent)) {
          // We have an embedded list, so move it out from under the parent
          // list. Be sure to put it after the parent list because this
          // loop iterates backwards through the parent's list of children.
          EditorDOMPoint afterCurrentList(EditorDOMPoint::After(atContent));
          NS_WARNING_ASSERTION(
              afterCurrentList.IsSet(),
              "Failed to set it to after current list element");
          Result<MoveNodeResult, nsresult> moveListElementResult =
              MoveNodeWithTransaction(*lastChildContent, afterCurrentList);
          if (MOZ_UNLIKELY(moveListElementResult.isErr())) {
            NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
            return moveListElementResult.propagateErr();
          }
          nsresult rv = moveListElementResult.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");
          continue;
        }

        // Delete any non-list items for now
        // XXX Chrome moves it from the list element.  We should follow it.
        nsresult rv = DeleteNodeWithTransaction(*lastChildContent);
        if (NS_FAILED(rv)) {
          NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
          return Err(rv);
        }
      }
      // Delete the now-empty list
      Result<EditorDOMPoint, nsresult> unwrapListElementResult =
          RemoveBlockContainerWithTransaction(
              MOZ_KnownLive(*content->AsElement()));
      if (MOZ_UNLIKELY(unwrapListElementResult.isErr())) {
        NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
        return unwrapListElementResult.propagateErr();
      }
      const EditorDOMPoint& pointToPutCaret = unwrapListElementResult.inspect();
      if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
        continue;
      }
      nsresult rv = CollapseSelectionTo(pointToPutCaret);
      if (NS_FAILED(rv)) {
        NS_WARNING("EditorBase::CollapseSelectionTo() failed");
        return Err(rv);
      }
      continue;
    }

    if (useCSS) {
      if (RefPtr<Element> element = content->GetAsElementOrParentElement()) {
        const Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
            ChangeMarginStart(*element, 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 (!indentedParentElement) {
    return SplitRangeOffFromNodeResult(leftContentOfLastOutdented,
                                       middleContentOfLastOutdented,
                                       rightContentOfLastOutdented);
  }

  // We have a <blockquote> we haven't finished handling.
  Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
      OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented,
                         *lastContentToBeOutdented, indentedParentIndentedWith,
                         aEditingHost);
  if (MOZ_UNLIKELY(outdentResult.isErr())) {
    NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed");
    return outdentResult;
  }
  // We will restore selection soon.  Therefore, callers do not need to restore
  // the selection.
  SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap();
  unwrappedOutdentResult.ForgetCaretPointSuggestion();
  return unwrappedOutdentResult;
}

Result<SplitRangeOffFromNodeResult, nsresult>
HTMLEditor::RemoveBlockContainerElementWithTransactionBetween(
    Element& aBlockContainerElement, nsIContent& aStartOfRange,
    nsIContent& aEndOfRange, BlockInlineCheck aBlockInlineCheck) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  EditorDOMPoint pointToPutCaret;
  Result<SplitRangeOffFromNodeResult, nsresult> splitResult =
      SplitRangeOffFromElement(aBlockContainerElement, aStartOfRange,
                               aEndOfRange);
  if (MOZ_UNLIKELY(splitResult.isErr())) {
    if (splitResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
      NS_WARNING("HTMLEditor::SplitRangeOffFromElement() failed");
      return splitResult;
    }
    NS_WARNING(
        "HTMLEditor::SplitRangeOffFromElement() failed, but might be ignored");
    return SplitRangeOffFromNodeResult(nullptr, nullptr, nullptr);
  }
  SplitRangeOffFromNodeResult unwrappedSplitResult = splitResult.unwrap();
  unwrappedSplitResult.MoveCaretPointTo(pointToPutCaret,
                                        {SuggestCaret::OnlyIfHasSuggestion});

  // Even if either split aBlockContainerElement or did not split it, we should
  // unwrap the right most element which is split from aBlockContainerElement
  // (or aBlockContainerElement itself if it was not split without errors).
  Element* rightmostElement =
      unwrappedSplitResult.GetRightmostContentAs<Element>();
  MOZ_ASSERT(rightmostElement);
  if (NS_WARN_IF(!rightmostElement)) {
    return Err(NS_ERROR_FAILURE);
  }

  {
    // MOZ_KnownLive(rightmostElement) because it's grabbed by
    // unwrappedSplitResult.
    Result<EditorDOMPoint, nsresult> unwrapBlockElementResult =
        RemoveBlockContainerWithTransaction(MOZ_KnownLive(*rightmostElement));
    if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
      NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
      return unwrapBlockElementResult.propagateErr();
    }
    if (unwrapBlockElementResult.inspect().IsSet()) {
      pointToPutCaret = unwrapBlockElementResult.unwrap();
    }
  }

  return SplitRangeOffFromNodeResult(
      unwrappedSplitResult.GetLeftContent(), nullptr,
      unwrappedSplitResult.GetRightContent(), std::move(pointToPutCaret));
}

Result<SplitRangeOffFromNodeResult, nsresult>
HTMLEditor::SplitRangeOffFromElement(Element& aElementToSplit,
                                     nsIContent& aStartOfMiddleElement,
                                     nsIContent& aEndOfMiddleElement) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  // aStartOfMiddleElement and aEndOfMiddleElement must be exclusive
  // descendants of aElementToSplit.
  MOZ_ASSERT(
      EditorUtils::IsDescendantOf(aStartOfMiddleElement, aElementToSplit));
  MOZ_ASSERT(EditorUtils::IsDescendantOf(aEndOfMiddleElement, aElementToSplit));

  EditorDOMPoint pointToPutCaret;
  // Split at the start.
  Result<SplitNodeResult, nsresult> splitAtStartResult =
      SplitNodeDeepWithTransaction(aElementToSplit,
                                   EditorDOMPoint(&aStartOfMiddleElement),
                                   SplitAtEdges::eDoNotCreateEmptyContainer);
  if (MOZ_UNLIKELY(splitAtStartResult.isErr())) {
    if (splitAtStartResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
      NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed (at left)");
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING(
        "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
        "eDoNotCreateEmptyContainer) at start of middle element failed");
  } else {
    splitAtStartResult.inspect().CopyCaretPointTo(
        pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
  }

  // Split at after the end
  auto atAfterEnd = EditorDOMPoint::After(aEndOfMiddleElement);
  Element* rightElement =
      splitAtStartResult.isOk() && splitAtStartResult.inspect().DidSplit()
          ? splitAtStartResult.inspect().GetNextContentAs<Element>()
          : &aElementToSplit;
  // MOZ_KnownLive(rightElement) because it's grabbed by splitAtStartResult or
  // aElementToSplit whose lifetime is guaranteed by the caller.
  Result<SplitNodeResult, nsresult> splitAtEndResult =
      SplitNodeDeepWithTransaction(MOZ_KnownLive(*rightElement), atAfterEnd,
                                   SplitAtEdges::eDoNotCreateEmptyContainer);
  if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
    if (splitAtEndResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
      NS_WARNING(
          "HTMLEditor::SplitNodeDeepWithTransaction() failed (at right)");
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING(
        "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
        "eDoNotCreateEmptyContainer) after end of middle element failed");
  } else {
    splitAtEndResult.inspect().CopyCaretPointTo(
        pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
  }

  if (splitAtStartResult.isOk() && splitAtStartResult.inspect().DidSplit() &&
      splitAtEndResult.isOk() && splitAtEndResult.inspect().DidSplit()) {
    // Note that the middle node can be computed only with the latter split
    // result.
    return SplitRangeOffFromNodeResult(
        splitAtStartResult.inspect().GetPreviousContent(),
        splitAtEndResult.inspect().GetPreviousContent(),
        splitAtEndResult.inspect().GetNextContent(),
        std::move(pointToPutCaret));
  }
  if (splitAtStartResult.isOk() && splitAtStartResult.inspect().DidSplit()) {
    return SplitRangeOffFromNodeResult(
        splitAtStartResult.inspect().GetPreviousContent(),
        splitAtStartResult.inspect().GetNextContent(), nullptr,
        std::move(pointToPutCaret));
  }
  if (splitAtEndResult.isOk() && splitAtEndResult.inspect().DidSplit()) {
    return SplitRangeOffFromNodeResult(
        nullptr, splitAtEndResult.inspect().GetPreviousContent(),
        splitAtEndResult.inspect().GetNextContent(),
        std::move(pointToPutCaret));
  }
  return SplitRangeOffFromNodeResult(nullptr, &aElementToSplit, nullptr,
                                     std::move(pointToPutCaret));
}

Result<SplitRangeOffFromNodeResult, nsresult> HTMLEditor::OutdentPartOfBlock(
    Element& aBlockElement, nsIContent& aStartOfOutdent,
    nsIContent& aEndOfOutdent, BlockIndentedWith aBlockIndentedWith,
    const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  Result<SplitRangeOffFromNodeResult, nsresult> splitResult =
      SplitRangeOffFromElement(aBlockElement, aStartOfOutdent, aEndOfOutdent);
  if (MOZ_UNLIKELY(splitResult.isErr())) {
    NS_WARNING("HTMLEditor::SplitRangeOffFromElement() failed");
    return splitResult;
  }

  SplitRangeOffFromNodeResult unwrappedSplitResult = splitResult.unwrap();
  Element* middleElement = unwrappedSplitResult.GetMiddleContentAs<Element>();
  if (MOZ_UNLIKELY(!middleElement)) {
    NS_WARNING(
        "HTMLEditor::SplitRangeOffFromElement() didn't return middle content");
    unwrappedSplitResult.IgnoreCaretPointSuggestion();
    return Err(NS_ERROR_FAILURE);
  }
  if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*middleElement))) {
    unwrappedSplitResult.IgnoreCaretPointSuggestion();
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }

  nsresult rv = unwrappedSplitResult.SuggestCaretPointTo(
      *this, {SuggestCaret::OnlyIfHasSuggestion,
              SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
              SuggestCaret::AndIgnoreTrivialError});
  if (NS_FAILED(rv)) {
    NS_WARNING("SplitRangeOffFromNodeResult::SuggestCaretPointTo() failed");
    return Err(rv);
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "SplitRangeOffFromNodeResult::SuggestCaretPointTo() "
                       "failed, but ignored");

  if (aBlockIndentedWith == BlockIndentedWith::HTML) {
    // MOZ_KnownLive(middleElement) because of grabbed by unwrappedSplitResult.
    Result<EditorDOMPoint, nsresult> unwrapBlockElementResult =
        RemoveBlockContainerWithTransaction(MOZ_KnownLive(*middleElement));
    if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
      NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
      return unwrapBlockElementResult.propagateErr();
    }
    const EditorDOMPoint& pointToPutCaret = unwrapBlockElementResult.inspect();
    if (AllowsTransactionsToChangeSelection() && pointToPutCaret.IsSet()) {
      nsresult rv = CollapseSelectionTo(pointToPutCaret);
      if (NS_FAILED(rv)) {
        NS_WARNING("EditorBase::CollapseSelectionTo() failed");
        return Err(rv);
      }
    }
    return SplitRangeOffFromNodeResult(unwrappedSplitResult.GetLeftContent(),
                                       nullptr,
                                       unwrappedSplitResult.GetRightContent());
  }

  // MOZ_KnownLive(middleElement) because of grabbed by unwrappedSplitResult.
  Result<EditorDOMPoint, nsresult> pointToPutCaretOrError = ChangeMarginStart(
      MOZ_KnownLive(*middleElement), ChangeMargin::Decrease, aEditingHost);
  if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
    NS_WARNING("HTMLEditor::ChangeMarginStart(ChangeMargin::Decrease) failed");
    return pointToPutCaretOrError.propagateErr();
  }
  if (AllowsTransactionsToChangeSelection() &&
      pointToPutCaretOrError.inspect().IsSet()) {
    nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
    if (NS_FAILED(rv)) {
      NS_WARNING("EditorBase::CollapseSelectionTo() failed");
      return Err(rv);
    }
  }
  return unwrappedSplitResult;
}

Result<CreateElementResult, nsresult> HTMLEditor::ChangeListElementType(
    Element& aListElement, nsAtom& aNewListTag, nsAtom& aNewListItemTag) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  EditorDOMPoint pointToPutCaret;

  AutoTArray<OwningNonNull<nsIContent>, 32> listElementChildren;
  HTMLEditUtils::CollectAllChildren(aListElement, listElementChildren);

  for (const OwningNonNull<nsIContent>& childContent : listElementChildren) {
    if (!childContent->IsElement()) {
      continue;
    }
    Element& childElement = *childContent->AsElement();
    if (HTMLEditUtils::IsListItemElement(childElement) &&
        !childContent->IsHTMLElement(&aNewListItemTag)) {
      // MOZ_KnownLive(childElement) because its lifetime is guaranteed by
      // listElementChildren.
      Result<CreateElementResult, nsresult>
          replaceWithNewListItemElementResult =
              ReplaceContainerAndCloneAttributesWithTransaction(
                  MOZ_KnownLive(childElement), aNewListItemTag);
      if (MOZ_UNLIKELY(replaceWithNewListItemElementResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction() "
            "failed");
        return replaceWithNewListItemElementResult;
      }
      CreateElementResult unwrappedReplaceWithNewListItemElementResult =
          replaceWithNewListItemElementResult.unwrap();
      unwrappedReplaceWithNewListItemElementResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      continue;
    }
    if (HTMLEditUtils::IsListElement(childElement) &&
        !childElement.IsHTMLElement(&aNewListTag)) {
      // XXX List elements shouldn't have other list elements as their
      //     child.  Why do we handle such invalid tree?
      //     -> Maybe, for bug 525888.
      // MOZ_KnownLive(childElement) because its lifetime is guaranteed by
      // listElementChildren.
      Result<CreateElementResult, nsresult> convertListTypeResult =
          ChangeListElementType(MOZ_KnownLive(childElement), aNewListTag,
                                aNewListItemTag);
      if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
        NS_WARNING("HTMLEditor::ChangeListElementType() failed");
        return convertListTypeResult;
      }
      CreateElementResult unwrappedConvertListTypeResult =
          convertListTypeResult.unwrap();
      unwrappedConvertListTypeResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      continue;
    }
  }

  if (aListElement.IsHTMLElement(&aNewListTag)) {
    return CreateElementResult(aListElement, std::move(pointToPutCaret));
  }

  // XXX If we replace the list element, shouldn't we create it first and then,
  //     move children into it before inserting the new list element into the
  //     DOM tree? Then, we could reduce the cost of dispatching DOM mutation
  //     events.
  Result<CreateElementResult, nsresult> replaceWithNewListElementResult =
      ReplaceContainerWithTransaction(aListElement, aNewListTag);
  if (MOZ_UNLIKELY(replaceWithNewListElementResult.isErr())) {
    NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed");
    return replaceWithNewListElementResult;
  }
  CreateElementResult unwrappedReplaceWithNewListElementResult =
      replaceWithNewListElementResult.unwrap();
  unwrappedReplaceWithNewListElementResult.MoveCaretPointTo(
      pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
  return CreateElementResult(
      unwrappedReplaceWithNewListElementResult.UnwrapNewNode(),
      std::move(pointToPutCaret));
}

Result<EditorDOMPoint, nsresult> HTMLEditor::CreateStyleForInsertText(
    const EditorDOMPoint& aPointToInsertText, const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aPointToInsertText.IsSetAndValid());
  MOZ_ASSERT(mPendingStylesToApplyToNewContent);

  const RefPtr<Element> documentRootElement = GetDocument()->GetRootElement();
  if (NS_WARN_IF(!documentRootElement)) {
    return Err(NS_ERROR_FAILURE);
  }

  // process clearing any styles first
  UniquePtr<PendingStyle> pendingStyle =
      mPendingStylesToApplyToNewContent->TakeClearingStyle();

  EditorDOMPoint pointToPutCaret(aPointToInsertText);
  {
    // Transactions may set selection, but we will set selection if necessary.
    AutoTransactionsConserveSelection dontChangeMySelection(*this);

    while (pendingStyle &&
           pointToPutCaret.GetContainer() != documentRootElement) {
      // MOZ_KnownLive because we own pendingStyle which guarantees the lifetime
      // of its members.
      Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
          ClearStyleAt(pointToPutCaret, pendingStyle->ToInlineStyle(),
                       pendingStyle->GetSpecifiedStyle(), aEditingHost);
      if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
        NS_WARNING("HTMLEditor::ClearStyleAt() failed");
        return pointToPutCaretOrError;
      }
      pointToPutCaret = pointToPutCaretOrError.unwrap();
      if (NS_WARN_IF(!pointToPutCaret.IsSetAndValid())) {
        return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
      }
      pendingStyle = mPendingStylesToApplyToNewContent->TakeClearingStyle();
    }
  }

  // then process setting any styles
  const int32_t relFontSize =
      mPendingStylesToApplyToNewContent->TakeRelativeFontSize();
  AutoTArray<EditorInlineStyleAndValue, 32> stylesToSet;
  mPendingStylesToApplyToNewContent->TakeAllPreservedStyles(stylesToSet);
  if (stylesToSet.IsEmpty() && !relFontSize) {
    return pointToPutCaret;
  }

  // We're in chrome, e.g., the email composer of Thunderbird, and there is
  // relative font size changes, we need to keep using legacy path until we port
  // IncrementOrDecrementFontSizeAsSubAction() to work with
  // AutoInlineStyleSetter.
  if (relFontSize) {
    // we have at least one style to add; make a new text node to insert style
    // nodes above.
    EditorDOMPoint pointToInsertTextNode(pointToPutCaret);
    if (pointToInsertTextNode.IsInTextNode()) {
      // if we are in a text node, split it
      Result<SplitNodeResult, nsresult> splitTextNodeResult =
          SplitNodeDeepWithTransaction(
              MOZ_KnownLive(*pointToInsertTextNode.ContainerAs<Text>()),
              pointToInsertTextNode,
              SplitAtEdges::eAllowToCreateEmptyContainer);
      if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
            "eAllowToCreateEmptyContainer) failed");
        return splitTextNodeResult.propagateErr();
      }
      SplitNodeResult unwrappedSplitTextNodeResult =
          splitTextNodeResult.unwrap();
      unwrappedSplitTextNodeResult.MoveCaretPointTo(
          pointToPutCaret, *this,
          {SuggestCaret::OnlyIfHasSuggestion,
           SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
      pointToInsertTextNode =
          unwrappedSplitTextNodeResult.AtSplitPoint<EditorDOMPoint>();
    }
    if (!pointToInsertTextNode.IsInContentNode() ||
        !HTMLEditUtils::IsContainerNode(
            *pointToInsertTextNode.ContainerAs<nsIContent>())) {
      return pointToPutCaret;
    }
    RefPtr<Text> newEmptyTextNode = CreateTextNode(u""_ns);
    if (!newEmptyTextNode) {
      NS_WARNING("EditorBase::CreateTextNode() failed");
      return Err(NS_ERROR_FAILURE);
    }
    Result<CreateTextResult, nsresult> insertNewTextNodeResult =
        InsertNodeWithTransaction<Text>(*newEmptyTextNode,
                                        pointToInsertTextNode);
    if (MOZ_UNLIKELY(insertNewTextNodeResult.isErr())) {
      NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
      return insertNewTextNodeResult.propagateErr();
    }
    insertNewTextNodeResult.inspect().IgnoreCaretPointSuggestion();
    pointToPutCaret.Set(newEmptyTextNode, 0u);

    // FIXME: If the stylesToSet have background-color style, it may
    // be applied shorter because outer <span> element height is not
    // computed with inner element's height.
    HTMLEditor::FontSize incrementOrDecrement =
        relFontSize > 0 ? HTMLEditor::FontSize::incr
                        : HTMLEditor::FontSize::decr;
    for ([[maybe_unused]] uint32_t j : IntegerRange(Abs(relFontSize))) {
      Result<CreateElementResult, nsresult> wrapTextInBigOrSmallElementResult =
          SetFontSizeOnTextNode(*newEmptyTextNode, 0, UINT32_MAX,
                                incrementOrDecrement);
      if (MOZ_UNLIKELY(wrapTextInBigOrSmallElementResult.isErr())) {
        NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed");
        return wrapTextInBigOrSmallElementResult.propagateErr();
      }
      // We don't need to update here because we'll suggest caret position
      // which is computed above.
      MOZ_ASSERT(pointToPutCaret.IsSet());
      wrapTextInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion();
    }

    for (const EditorInlineStyleAndValue& styleToSet : stylesToSet) {
      AutoInlineStyleSetter inlineStyleSetter(styleToSet);
      // MOZ_KnownLive(...ContainerAs<nsIContent>()) because pointToPutCaret
      // grabs the result.
      Result<CaretPoint, nsresult> setStyleResult =
          inlineStyleSetter.ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
              *this, MOZ_KnownLive(*pointToPutCaret.ContainerAs<nsIContent>()));
      if (MOZ_UNLIKELY(setStyleResult.isErr())) {
        NS_WARNING("HTMLEditor::SetInlinePropertyOnNode() failed");
        return setStyleResult.propagateErr();
      }
      // We don't need to update here because we'll suggest caret position which
      // is computed above.
      MOZ_ASSERT(pointToPutCaret.IsSet());
      setStyleResult.unwrap().IgnoreCaretPointSuggestion();
    }
    return pointToPutCaret;
  }

  // If we have preserved commands except relative font style changes, we can
  // use inline style setting code which reuse ancestors better.
  AutoClonedRangeArray ranges(pointToPutCaret);
  if (MOZ_UNLIKELY(ranges.Ranges().IsEmpty())) {
    NS_WARNING("AutoClonedRangeArray::AutoClonedRangeArray() failed");
    return Err(NS_ERROR_FAILURE);
  }
  nsresult rv =
      SetInlinePropertiesAroundRanges(ranges, stylesToSet, aEditingHost);
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::SetInlinePropertiesAroundRanges() failed");
    return Err(rv);
  }
  if (NS_WARN_IF(ranges.Ranges().IsEmpty())) {
    return Err(NS_ERROR_FAILURE);
  }
  // Now `ranges` selects new styled contents and the range may not be
  // collapsed.  We should use the deepest editable start point of the range
  // to insert text.
  nsINode* container = ranges.FirstRangeRef()->GetStartContainer();
  if (MOZ_UNLIKELY(!container->IsContent())) {
    container = ranges.FirstRangeRef()->GetChildAtStartOffset();
    if (MOZ_UNLIKELY(!container)) {
      NS_WARNING("How did we get lost insertion point?");
      return Err(NS_ERROR_FAILURE);
    }
  }
  pointToPutCaret =
      HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
          *container->AsContent(), {});
  if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
    return Err(NS_ERROR_FAILURE);
  }
  return pointToPutCaret;
}

Result<EditActionResult, nsresult> HTMLEditor::AlignAsSubAction(
    const nsAString& aAlignType, const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  AutoPlaceholderBatch treatAsOneTransaction(
      *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
  IgnoredErrorResult ignoredError;
  AutoEditSubActionNotifier startToHandleEditSubAction(
      *this, EditSubAction::eSetOrClearAlignment, 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<EditActionResult, nsresult> 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();
  }

  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 (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
    NS_WARNING("Mutation event listener might have changed the selection");
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }

  if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
    nsresult rv = EnsureCaretNotAfterInvisibleBRElement(aEditingHost);
    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");
    }
  }

  AutoClonedSelectionRangeArray selectionRanges(SelectionRef());

  // XXX Why do we do this only when there is only one selection range?
  if (!selectionRanges.IsCollapsed() &&
      selectionRanges.Ranges().Length() == 1u) {
    Result<EditorRawDOMRange, nsresult> extendedRange =
        GetRangeExtendedToHardLineEdgesForBlockEditAction(
            selectionRanges.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 etStartAndEnd() here.
    nsresult rv = selectionRanges.SetBaseAndExtent(
        extendedRange.inspect().StartRef(), extendedRange.inspect().EndRef());
    if (NS_FAILED(rv)) {
      NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
      return Err(rv);
    }
  }

  rv = AlignContentsAtRanges(selectionRanges, aAlignType, aEditingHost);
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::AlignContentsAtSelection() failed");
    return Err(rv);
  }

  if (selectionRanges.IsCollapsed()) {
    // FIXME: If we get rid of the legacy mutation events, we should be able to
    // just insert a line break without empty check.
    Result<CreateLineBreakResult, nsresult>
        insertPaddingBRElementResultOrError =
            InsertPaddingBRElementIfInEmptyBlock(
                selectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>(),
                eNoStrip);
    if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
      NS_WARNING(
          "HTMLEditor::InsertPaddingBRElementIfInEmptyBlock(eNoStrip) failed");
      return insertPaddingBRElementResultOrError.propagateErr();
    }
    EditorDOMPoint pointToPutCaret;
    insertPaddingBRElementResultOrError.unwrap().MoveCaretPointTo(
        pointToPutCaret, *this,
        {SuggestCaret::OnlyIfHasSuggestion,
         SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
    if (pointToPutCaret.IsSet()) {
      nsresult rv = selectionRanges.Collapse(pointToPutCaret);
      if (NS_FAILED(rv)) {
        NS_WARNING("AutoClonedRangeArray::Collapse() failed");
        return Err(rv);
      }
    }
  }

  rv = selectionRanges.ApplyTo(SelectionRef());
  if (NS_FAILED(rv)) {
    NS_WARNING("AutoClonedRangeArray::ApplyTo() failed");
    return Err(rv);
  }

  if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
    NS_WARNING("Mutation event listener might have changed the selection");
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }

  return EditActionResult::HandledResult();
}

nsresult HTMLEditor::AlignContentsAtRanges(
    AutoClonedSelectionRangeArray& aRanges, const nsAString& aAlignType,
    const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
  MOZ_ASSERT(aRanges.IsInContent());

  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
  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  {
    AutoClonedSelectionRangeArray extendedRanges(aRanges);
    extendedRanges.ExtendRangesToWrapLines(
        EditSubAction::eSetOrClearAlignment,
        BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
    Result<EditorDOMPoint, nsresult> splitResult =
        extendedRanges
            .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
                *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
    if (MOZ_UNLIKELY(splitResult.isErr())) {
      NS_WARNING(
          "AutoClonedRangeArray::"
          "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
          "failed");
      return splitResult.unwrapErr();
    }
    if (splitResult.inspect().IsSet()) {
      pointToPutCaret = splitResult.unwrap();
    }
    nsresult rv = extendedRanges.CollectEditTargetNodes(
        *this, arrayOfContents, EditSubAction::eSetOrClearAlignment,
        AutoClonedRangeArray::CollectNonEditableNodes::Yes);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "AutoClonedRangeArray::CollectEditTargetNodes(EditSubAction::"
          "eSetOrClearAlignment, CollectNonEditableNodes::Yes) failed");
      return rv;
    }
  }

  Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
      MaybeSplitElementsAtEveryBRElement(arrayOfContents,
                                         EditSubAction::eSetOrClearAlignment);
  if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
    NS_WARNING(
        "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
        "eSetOrClearAlignment) failed");
    return splitAtBRElementsResult.inspectErr();
  }
  if (splitAtBRElementsResult.inspect().IsSet()) {
    pointToPutCaret = splitAtBRElementsResult.unwrap();
  }

  // If we don't have any nodes, or we have only a single br, then we are
  // creating an empty alignment div.  We have to do some different things for
  // these.
  bool createEmptyDivElement = arrayOfContents.IsEmpty();
  if (arrayOfContents.Length() == 1) {
    const OwningNonNull<nsIContent>& content = arrayOfContents[0];

    if (HTMLEditUtils::IsAlignAttrSupported(content) &&
        HTMLEditUtils::IsBlockElement(content,
                                      BlockInlineCheck::UseHTMLDefaultStyle)) {
      // The node is a table element, an hr, a paragraph, a div or a section
      // header; in HTML 4, it can directly carry the ALIGN attribute and we
      // don't need to make a div! If we are in CSS mode, all the work is done
      // in SetBlockElementAlign().
      Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
          SetBlockElementAlign(MOZ_KnownLive(*content->AsElement()), aAlignType,
                               EditTarget::OnlyDescendantsExceptTable);
      if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
        NS_WARNING("HTMLEditor::SetBlockElementAlign() failed");
        return pointToPutCaretOrError.unwrapErr();
      }
      if (pointToPutCaretOrError.inspect().IsSet()) {
        pointToPutCaret = pointToPutCaretOrError.unwrap();
      }
    }

    if (content->IsHTMLElement(nsGkAtoms::br)) {
      // The special case createEmptyDivElement code (below) that consumes
      // `<br>` elements can cause tables to split if the start node of the
      // selection is not in a table cell or caption, for example parent is a
      // `<tr>`.  Avoid this unnecessary splitting if possible by leaving
      // createEmptyDivElement false so that we fall through to the normal case
      // alignment code.
      //
      // XXX: It seems a little error prone for the createEmptyDivElement
      //      special case code to assume that the start node of the selection
      //      is the parent of the single node in the arrayOfContents, as the
      //      paragraph above points out. Do we rely on the selection start
      //      node because of the fact that arrayOfContents can be empty?  We
      //      should probably revisit this issue. - kin

      const EditorDOMPoint firstRangeStartPoint =
          pointToPutCaret.IsSet()
              ? pointToPutCaret
              : aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
      if (NS_WARN_IF(!firstRangeStartPoint.IsInContentNode())) {
        return NS_ERROR_FAILURE;
      }
      nsIContent& parent = *firstRangeStartPoint.ContainerAs<nsIContent>();
      createEmptyDivElement =
          !HTMLEditUtils::IsAnyTableElementExceptColumnElement(parent) ||
          HTMLEditUtils::IsTableCellOrCaptionElement(parent);
    }
  }

  if (createEmptyDivElement) {
    if (MOZ_UNLIKELY(!pointToPutCaret.IsSet() && !aRanges.IsInContent())) {
      NS_WARNING("Mutation event listener might have changed the selection");
      return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
    }
    const EditorDOMPoint pointToInsertDivElement =
        pointToPutCaret.IsSet()
            ? pointToPutCaret
            : aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
    Result<CreateElementResult, nsresult> insertNewDivElementResult =
        InsertDivElementToAlignContents(pointToInsertDivElement, aAlignType,
                                        aEditingHost);
    if (insertNewDivElementResult.isErr()) {
      NS_WARNING("HTMLEditor::InsertDivElementToAlignContents() failed");
      return insertNewDivElementResult.unwrapErr();
    }
    CreateElementResult unwrappedInsertNewDivElementResult =
        insertNewDivElementResult.unwrap();
    aRanges.ClearSavedRanges();
    EditorDOMPoint pointToPutCaret =
        unwrappedInsertNewDivElementResult.UnwrapCaretPoint();
    nsresult rv = aRanges.Collapse(pointToPutCaret);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "AutoClonedRangeArray::Collapse() failed");
    return rv;
  }

  Result<CreateElementResult, nsresult> maybeCreateDivElementResult =
      AlignNodesAndDescendants(arrayOfContents, aAlignType, aEditingHost);
  if (MOZ_UNLIKELY(maybeCreateDivElementResult.isErr())) {
    NS_WARNING("HTMLEditor::AlignNodesAndDescendants() failed");
    return maybeCreateDivElementResult.unwrapErr();
  }
  maybeCreateDivElementResult.inspect().IgnoreCaretPointSuggestion();

  MOZ_ASSERT(aRanges.HasSavedRanges());
  aRanges.RestoreFromSavedRanges();
  // If restored range is collapsed outside the latest cased <div> element,
  // we should move caret into the <div>.
  if (maybeCreateDivElementResult.inspect().GetNewNode() &&
      aRanges.IsCollapsed() && !aRanges.Ranges().IsEmpty()) {
    const auto firstRangeStartRawPoint =
        aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
    if (MOZ_LIKELY(firstRangeStartRawPoint.IsSet())) {
      Result<EditorRawDOMPoint, nsresult> pointInNewDivOrError =
          HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
              EditorRawDOMPoint>(
              *maybeCreateDivElementResult.inspect().GetNewNode(),
              firstRangeStartRawPoint);
      if (MOZ_UNLIKELY(pointInNewDivOrError.isErr())) {
        NS_WARNING(
            "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, "
            "but ignored");
      } else if (pointInNewDivOrError.inspect().IsSet()) {
        nsresult rv = aRanges.Collapse(pointInNewDivOrError.unwrap());
        if (NS_FAILED(rv)) {
          NS_WARNING("AutoClonedRangeArray::Collapse() failed");
          return rv;
        }
      }
    }
  }
  return NS_OK;
}

Result<CreateElementResult, nsresult>
HTMLEditor::InsertDivElementToAlignContents(
    const EditorDOMPoint& aPointToInsert, const nsAString& aAlignType,
    const Element& aEditingHost) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
  MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
  MOZ_ASSERT(aPointToInsert.IsSetAndValid());

  if (NS_WARN_IF(!aPointToInsert.IsSet())) {
    return Err(NS_ERROR_FAILURE);
  }

  Result<CreateElementResult, nsresult> createNewDivElementResult =
      InsertElementWithSplittingAncestorsWithTransaction(
          *nsGkAtoms::div, aPointToInsert, BRElementNextToSplitPoint::Delete,
          aEditingHost);
  if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
    NS_WARNING(
        "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
        "nsGkAtoms::div, BRElementNextToSplitPoint::Delete) failed");
    return createNewDivElementResult;
  }
  CreateElementResult unwrappedCreateNewDivElementResult =
      createNewDivElementResult.unwrap();
  // We'll suggest start of the new <div>, so we don't need the suggested
  // position.
  unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion();

  MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode());
  RefPtr<Element> newDivElement =
      unwrappedCreateNewDivElementResult.UnwrapNewNode();
  // Set up the alignment on the div, using HTML or CSS
  Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
      SetBlockElementAlign(*newDivElement, aAlignType,
                           EditTarget::OnlyDescendantsExceptTable);
  if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
    NS_WARNING(
        "HTMLEditor::SetBlockElementAlign(EditTarget::"
        "OnlyDescendantsExceptTable) failed");
    return pointToPutCaretOrError.propagateErr();
  }
  // We don't need the new suggested position too.

  // Put in a padding <br> element for empty last line so that it won't get
  // deleted.
  {
    Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
        InsertPaddingBRElementForEmptyLastLineWithTransaction(
            EditorDOMPoint(newDivElement, 0u));
    if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
      NS_WARNING(
          "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
          "failed");
      return insertPaddingBRElementResult;
    }
    insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
  }

  return CreateElementResult(std::move(newDivElement),
                             EditorDOMPoint(newDivElement, 0u));
}

Result<CreateElementResult, nsresult> HTMLEditor::AlignNodesAndDescendants(
    nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
    const nsAString& aAlignType, const Element& aEditingHost) {
  // Detect all the transitions in the array, where a transition means that
  // adjacent nodes in the array don't have the same parent.
  AutoTArray<bool, 64> transitionList;
  HTMLEditor::MakeTransitionList(aArrayOfContents, transitionList);

  RefPtr<Element> latestCreatedDivElement;
  EditorDOMPoint pointToPutCaret;

  // Okay, now go through all the nodes and give them an align attrib or put
  // them in a div, or whatever is appropriate.  Woohoo!

  RefPtr<Element> createdDivElement;
  const bool useCSS = IsCSSEnabled();
  for (size_t i = 0; i < aArrayOfContents.Length(); i++) {
    const OwningNonNull<nsIContent>& content = aArrayOfContents[i];

    // Ignore all non-editable nodes.  Leave them be.
    if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
      continue;
    }

    // The node is a table element, an hr, a paragraph, a div or a section
    // header; in HTML 4, it can directly carry the ALIGN attribute and we
    // don't need to nest it, just set the alignment.  In CSS, assign the
    // corresponding CSS styles in SetBlockElementAlign().
    if (HTMLEditUtils::IsAlignAttrSupported(content)) {
      Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
          SetBlockElementAlign(MOZ_KnownLive(*content->AsElement()), aAlignType,
                               EditTarget::NodeAndDescendantsExceptTable);
      if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
        NS_WARNING(
            "HTMLEditor::SetBlockElementAlign(EditTarget::"
            "NodeAndDescendantsExceptTable) failed");
        return pointToPutCaretOrError.propagateErr();
      }
      if (pointToPutCaretOrError.inspect().IsSet()) {
        pointToPutCaret = pointToPutCaretOrError.unwrap();
      }
      // Clear out createdDivElement so that we don't put nodes after this one
      // into it
      createdDivElement = nullptr;
      continue;
    }

    EditorDOMPoint atContent(content);
    if (NS_WARN_IF(!atContent.IsInContentNode())) {
      continue;
    }

    // Skip insignificant formatting text nodes to prevent unnecessary
    // structure splitting!
    if (content->IsText() &&
        ((HTMLEditUtils::IsAnyTableElementExceptColumnElement(
              *atContent.ContainerAs<nsIContent>()) &&
          !HTMLEditUtils::IsTableCellOrCaptionElement(
              *atContent.ContainerAs<nsIContent>())) ||
         HTMLEditUtils::IsListElement(*atContent.ContainerAs<nsIContent>()) ||
         HTMLEditUtils::IsEmptyNode(
             *content,
             {EmptyCheckOption::TreatSingleBRElementAsVisible,
              EmptyCheckOption::TreatNonEditableContentAsInvisible}))) {
      continue;
    }

    // If it's a list item, or a list inside a list, forget any "current" div,
    // and instead put divs inside the appropriate block (td, li, etc.)
    if (HTMLEditUtils::IsListItemElement(*content) ||
        HTMLEditUtils::IsListElement(*content)) {
      Element* listOrListItemElement = content->AsElement();
      {
        AutoEditorDOMPointOffsetInvalidator lockChild(atContent);
        // MOZ_KnownLive(*listOrListItemElement): An element of aArrayOfContents
        // which is array of OwningNonNull.
        Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
            RemoveAlignFromDescendants(MOZ_KnownLive(*listOrListItemElement),
                                       aAlignType,
                                       EditTarget::OnlyDescendantsExceptTable);
        if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
          NS_WARNING(
              "HTMLEditor::RemoveAlignFromDescendants(EditTarget::"
              "OnlyDescendantsExceptTable) failed");
          return pointToPutCaretOrError.propagateErr();
        }
        if (pointToPutCaretOrError.inspect().IsSet()) {
          pointToPutCaret = pointToPutCaretOrError.unwrap();
        }
      }
      if (NS_WARN_IF(!atContent.IsSetAndValid())) {
        return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
      }

      if (useCSS) {
        nsStyledElement* styledListOrListItemElement =
            nsStyledElement::FromNode(listOrListItemElement);
        if (styledListOrListItemElement &&
            EditorElementStyle::Align().IsCSSSettable(
                *styledListOrListItemElement)) {
          // MOZ_KnownLive(*styledListOrListItemElement): An element of
          // aArrayOfContents which is array of OwningNonNull.
          Result<size_t, nsresult> result =
              CSSEditUtils::SetCSSEquivalentToStyle(
                  WithTransaction::Yes, *this,
                  MOZ_KnownLive(*styledListOrListItemElement),
                  EditorElementStyle::Align(), &aAlignType);
          if (MOZ_UNLIKELY(result.isErr())) {
            if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
              return result.propagateErr();
            }
            NS_WARNING(
                "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::"
                "Align()) failed, but ignored");
          }
        }
        createdDivElement = nullptr;
        continue;
      }

      if (HTMLEditUtils::IsListElement(*atContent.ContainerAs<nsIContent>())) {
        // If we don't use CSS, add a content to list element: they have to
        // be inside another list, i.e., >= second level of nesting.
        // XXX AlignContentsInAllTableCellsAndListItems() handles only list
        //     item elements and table cells.  Is it intentional?  Why don't
        //     we need to align contents in other type blocks?
        // MOZ_KnownLive(*listOrListItemElement): An element of aArrayOfContents
        // which is array of OwningNonNull.
        Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
            AlignContentsInAllTableCellsAndListItems(
                MOZ_KnownLive(*listOrListItemElement), aAlignType);
        if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
          NS_WARNING(
              "HTMLEditor::AlignContentsInAllTableCellsAndListItems() failed");
          return pointToPutCaretOrError.propagateErr();
        }
        if (pointToPutCaretOrError.inspect().IsSet()) {
          pointToPutCaret = pointToPutCaretOrError.unwrap();
        }
        createdDivElement = nullptr;
        continue;
      }

      // Clear out createdDivElement so that we don't put nodes after this one
      // into it
    }

    // Need to make a div to put things in if we haven't already, or if this
    // node doesn't go in div we used earlier.
    if (!createdDivElement || transitionList[i]) {
      // First, check that our element can contain a div.
      if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(),
                                         *nsGkAtoms::div)) {
        // XXX Why do we return "OK" here rather than returning error or
        //     doing continue?
        return latestCreatedDivElement
                   ? CreateElementResult(std::move(latestCreatedDivElement),
                                         std::move(pointToPutCaret))
                   : CreateElementResult::NotHandled(
                         std::move(pointToPutCaret));
      }

      Result<CreateElementResult, nsresult> createNewDivElementResult =
          InsertElementWithSplittingAncestorsWithTransaction(
              *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
              aEditingHost);
      if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
            "nsGkAtoms::div) failed");
        return createNewDivElementResult;
      }
      CreateElementResult unwrappedCreateNewDivElementResult =
          createNewDivElementResult.unwrap();
      if (unwrappedCreateNewDivElementResult.HasCaretPointSuggestion()) {
        pointToPutCaret = unwrappedCreateNewDivElementResult.UnwrapCaretPoint();
      }

      MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode());
      createdDivElement = unwrappedCreateNewDivElementResult.UnwrapNewNode();
      // Set up the alignment on the div
      Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
          SetBlockElementAlign(*createdDivElement, aAlignType,
                               EditTarget::OnlyDescendantsExceptTable);
      if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
        if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() ==
                       NS_ERROR_EDITOR_DESTROYED)) {
          return pointToPutCaretOrError.propagateErr();
        }
        NS_WARNING(
            "HTMLEditor::SetBlockElementAlign(EditTarget::"
            "OnlyDescendantsExceptTable) failed, but ignored");
      } else if (pointToPutCaretOrError.inspect().IsSet()) {
        pointToPutCaret = pointToPutCaretOrError.unwrap();
      }
      latestCreatedDivElement = createdDivElement;
    }

    const OwningNonNull<nsIContent> lastContent = [&]() {
      nsIContent* lastContent = content;
      for (; i + 1 < aArrayOfContents.Length(); i++) {
        const OwningNonNull<nsIContent>& nextContent = aArrayOfContents[i + 1];
        if (lastContent->GetNextSibling() != nextContent ||
            !EditorUtils::IsEditableContent(content, EditorType::HTML) ||
            !HTMLEditUtils::IsAlignAttrSupported(nextContent) ||
            // If we meets an invisible `Text` in table or list, we don't move
            // it to avoid to handle ancestors for them.  However, ignoring the
            // empty `Text` nodes is more expensive than moving them here.
            // Therefore, here does not check whether the following sibling of
            // `content` is an empty `Text`.

            // In some cases, we reach here even if `content` is a list or a
            // list item.  However, anyway we need to run a preparation for such
            // element.  Therefore, we cannot move such type of elements with
            // `content` here.
            HTMLEditUtils::IsListItemElement(*nextContent) ||
            HTMLEditUtils::IsListElement(*nextContent) ||
            // Similarly, if the sibling is in the transitionList, we need to
            // handle it separately.
            transitionList[i + 1]) {
          break;
        }
        lastContent = nextContent;
      }
      return OwningNonNull<nsIContent>(*lastContent);
    }();

    // Tuck the node into the end of the active div
    //
    // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it alive.
    Result<MoveNodeResult, nsresult> moveNodeResult =
        MoveSiblingsToEndWithTransaction(MOZ_KnownLive(content), lastContent,
                                         *createdDivElement);
    if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
      NS_WARNING("HTMLEditor::MoveSiblingsToEndWithTransaction() failed");
      return moveNodeResult.propagateErr();
    }
    MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
    if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
      pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
    }
  }

  return latestCreatedDivElement
             ? CreateElementResult(std::move(latestCreatedDivElement),
                                   std::move(pointToPutCaret))
             : CreateElementResult::NotHandled(std::move(pointToPutCaret));
}

Result<EditorDOMPoint, nsresult>
HTMLEditor::AlignContentsInAllTableCellsAndListItems(
    Element& aElement, const nsAString& aAlignType) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  // Gather list of table cells or list items
  AutoTArray<OwningNonNull<Element>, 64> arrayOfTableCellsAndListItems;
  DOMIterator iter(aElement);
  iter.AppendNodesToArray(
      +[](nsINode& aNode, void*) -> bool {
        MOZ_ASSERT(Element::FromNode(&aNode));
        return HTMLEditUtils::IsTableCellElement(*aNode.AsElement()) ||
               HTMLEditUtils::IsListItemElement(*aNode.AsElement());
      },
      arrayOfTableCellsAndListItems);

  // Now that we have the list, align their contents as requested
  EditorDOMPoint pointToPutCaret;
  for (auto& tableCellOrListItemElement : arrayOfTableCellsAndListItems) {
    // MOZ_KnownLive because 'arrayOfTableCellsAndListItems' is guaranteed to
    // keep it alive.
    Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
        AlignBlockContentsWithDivElement(
            MOZ_KnownLive(tableCellOrListItemElement), aAlignType);
    if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
      NS_WARNING("HTMLEditor::AlignBlockContentsWithDivElement() failed");
      return pointToPutCaretOrError;
    }
    if (pointToPutCaretOrError.inspect().IsSet()) {
      pointToPutCaret = pointToPutCaretOrError.unwrap();
    }
  }

  return pointToPutCaret;
}

Result<EditorDOMPoint, nsresult> HTMLEditor::AlignBlockContentsWithDivElement(
    Element& aBlockElement, const nsAString& aAlignType) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  // XXX Chrome wraps text into a <div> only when the container has different
  // blocks.  At that time, Chrome seems treating non-editable nodes as a line
  // break.  So, their behavior is also odd so that it does not make sense to
  // follow their behavior when there is non-editable content.

  // XXX I don't understand why we should NOT align non-editable children
  //     with modifying EDITABLE `<div>` element.
  const nsCOMPtr<nsIContent> firstEditableContent =
      HTMLEditUtils::GetFirstChild(
          aBlockElement, {LeafNodeOption::IgnoreNonEditableNode},
          BlockInlineCheck::UseComputedDisplayOutsideStyle);
  if (!firstEditableContent) {
    // This block has no editable content, nothing to align.
    return EditorDOMPoint();
  }

  // If there is only one editable content and it's a `<div>` element,
  // just set `align` attribute of it.
  const nsCOMPtr<nsIContent> lastEditableContent = HTMLEditUtils::GetLastChild(
      aBlockElement, {LeafNodeOption::IgnoreNonEditableNode},
      BlockInlineCheck::UseComputedDisplayOutsideStyle);
  if (firstEditableContent == lastEditableContent &&
      firstEditableContent->IsHTMLElement(nsGkAtoms::div)) {
    // XXX Chrome uses `style="text-align: foo"` instead of the legacy `align`
    // attribute.  That does not allow to align the child blocks center and they
    // put the style to every blocks in the selection range. So, it requires a
    // complicated change to follow their behavior.
    nsresult rv = SetAttributeOrEquivalent(
        MOZ_KnownLive(firstEditableContent->AsElement()), nsGkAtoms::align,
        aAlignType, false);
    if (NS_WARN_IF(Destroyed())) {
      NS_WARNING(
          "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align) caused "
          "destroying the editor");
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align) failed");
      return Err(rv);
    }
    return EditorDOMPoint();
  }

  // Otherwise, we need to insert a `<div>` element to set `align` attribute.
  Result<CreateElementResult, nsresult> createNewDivElementResultOrError =
      CreateAndInsertElement(
          WithTransaction::Yes, *nsGkAtoms::div,
          EditorDOMPoint(&aBlockElement, 0u),
          // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
          [&](HTMLEditor& aHTMLEditor, Element& aDivElement,
              const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
            MOZ_ASSERT(!aDivElement.IsInComposedDoc());
            // If aDivElement has not been connected yet, we do not need
            // transaction of setting align attribute here.
            nsresult rv = aHTMLEditor.SetAttributeOrEquivalent(
                &aDivElement, nsGkAtoms::align, aAlignType, false);
            if (NS_FAILED(rv)) {
              NS_WARNING(
                  "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align, "
                  "\"...\", false) failed");
              return rv;
            }
            if (!aBlockElement.HasChildren()) {
              return NS_OK;
            }
            // FIXME: This will move non-editable children between
            // firstEditableContent and lastEditableContent.  So, the design of
            // this method is odd.
            Result<MoveNodeResult, nsresult> moveChildrenResultOrError =
                aHTMLEditor.MoveSiblingsWithTransaction(
                    *firstEditableContent, *lastEditableContent,
                    EditorDOMPoint(&aDivElement, 0));
            if (MOZ_UNLIKELY(moveChildrenResultOrError.isErr())) {
              NS_WARNING_ASSERTION(
                  moveChildrenResultOrError.isOk(),
                  "HTMLEditor::MoveSiblingsWithTransaction() failed");
              return moveChildrenResultOrError.unwrapErr();
            }
            moveChildrenResultOrError.unwrap().IgnoreCaretPointSuggestion();
            return NS_OK;
          });
  if (MOZ_UNLIKELY(createNewDivElementResultOrError.isErr())) {
    NS_WARNING(
        "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes, "
        "nsGkAtoms::div) failed");
    return createNewDivElementResultOrError.propagateErr();
  }
  return createNewDivElementResultOrError.unwrap().UnwrapCaretPoint();
}

Result<EditorRawDOMRange, nsresult>
HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction(
    const nsRange* aRange, const Element& aEditingHost) const {
  MOZ_ASSERT(IsEditActionDataAvailable());

  // This tweaks selections to be more "natural".
  // Idea here is to adjust edges of selection ranges so that they do not cross
  // breaks or block boundaries unless something editable beyond that boundary
  // is also selected.  This adjustment makes it much easier for the various
  // block operations to determine what nodes to act on.
  if (NS_WARN_IF(!aRange) || NS_WARN_IF(!aRange->IsPositioned())) {
    return Err(NS_ERROR_FAILURE);
  }

  const EditorRawDOMPoint startPoint(aRange->StartRef());
  if (NS_WARN_IF(!startPoint.IsSet())) {
    return Err(NS_ERROR_FAILURE);
  }
  const EditorRawDOMPoint endPoint(aRange->EndRef());
  if (NS_WARN_IF(!endPoint.IsSet())) {
    return Err(NS_ERROR_FAILURE);
  }

  // adjusted values default to original values
  EditorRawDOMRange newRange(startPoint, endPoint);

  {
    // Is there any intervening visible white-space?  If so we can't push
    // selection past that, it would visibly change meaning of users selection.
    const WSScanResult prevVisibleThingOfEndPoint =
        WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
            {
                // We should refer only the default style of HTML because we
                // need to wrap any elements with a specific HTML element.  So
                // we should not refer actual style.  For example, we want to
                // reformat parent HTML block element even if selected in a
                // blocked phrase element or non-HTMLelement.
                WSRunScanner::Option::ReferHTMLDefaultStyle,
            },
            endPoint, &aEditingHost);
    if (MOZ_UNLIKELY(prevVisibleThingOfEndPoint.Failed())) {
      NS_WARNING(
          "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() failed");
      return Err(NS_ERROR_FAILURE);
    }
    if (prevVisibleThingOfEndPoint.ReachedSomethingNonTextContent()) {
      // eThisBlock and eOtherBlock conveniently distinguish cases
      // of going "down" into a block and "up" out of a block.
      if (prevVisibleThingOfEndPoint.ReachedOtherBlockElement()) {
        // endpoint is just after the close of a block.
        if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
                *prevVisibleThingOfEndPoint.ElementPtr(),
                {LeafNodeOption::TreatChildBlockAsLeafNode},
                BlockInlineCheck::UseHTMLDefaultStyle)) {
          newRange.SetEnd(EditorRawDOMPoint::After(*child));
        }
        // else block is empty - we can leave selection alone here, i think.
      } else if (prevVisibleThingOfEndPoint.ReachedCurrentBlockBoundary() ||
                 prevVisibleThingOfEndPoint
                     .ReachedInlineEditingHostBoundary()) {
        // endpoint is just after start of this block
        if (nsIContent* const child = HTMLEditUtils::GetPreviousLeafContent(
                endPoint, {LeafNodeOption::IgnoreNonEditableNode},
                BlockInlineCheck::UseHTMLDefaultStyle, &aEditingHost)) {
          newRange.SetEnd(EditorRawDOMPoint::After(*child));
        }
        // else block is empty - we can leave selection alone here, i think.
      } else if (prevVisibleThingOfEndPoint.ReachedBRElement()) {
        // endpoint is just after break.  lets adjust it to before it.
        newRange.SetEnd(prevVisibleThingOfEndPoint
                            .PointAtReachedContent<EditorRawDOMPoint>());
      }
    }
  }
  {
    // Is there any intervening visible white-space?  If so we can't push
    // selection past that, it would visibly change meaning of users selection.
    const WSScanResult nextVisibleThingOfStartPoint =
        WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
            {WSRunScanner::Option::ReferHTMLDefaultStyle}, startPoint,
            &aEditingHost);
    if (MOZ_UNLIKELY(nextVisibleThingOfStartPoint.Failed())) {
      NS_WARNING(
          "WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary() failed");
      return Err(NS_ERROR_FAILURE);
    }
    if (nextVisibleThingOfStartPoint.ReachedSomethingNonTextContent()) {
      // eThisBlock and eOtherBlock conveniently distinguish cases
      // of going "down" into a block and "up" out of a block.
      if (nextVisibleThingOfStartPoint.ReachedOtherBlockElement()) {
        // startpoint is just before the start of a block.
        if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
                *nextVisibleThingOfStartPoint.ElementPtr(),
                {LeafNodeOption::TreatChildBlockAsLeafNode},
                BlockInlineCheck::UseHTMLDefaultStyle)) {
          newRange.SetStart(EditorRawDOMPoint(child));
        }
        // else block is empty - we can leave selection alone here, i think.
      } else if (nextVisibleThingOfStartPoint.ReachedCurrentBlockBoundary() ||
                 nextVisibleThingOfStartPoint
                     .ReachedInlineEditingHostBoundary()) {
        // startpoint is just before end of this block
        if (nsIContent* const child = HTMLEditUtils::GetNextLeafContent(
                startPoint, {LeafNodeOption::IgnoreNonEditableNode},
                BlockInlineCheck::UseHTMLDefaultStyle, &aEditingHost)) {
          newRange.SetStart(EditorRawDOMPoint(child));
        }
        // else block is empty - we can leave selection alone here, i think.
      } else if (nextVisibleThingOfStartPoint.ReachedBRElement()) {
        // startpoint is just before a break.  lets adjust it to after it.
        // XXX If it's an invisible <br>, does this work? Will the following
        // checks solve that?
        newRange.SetStart(nextVisibleThingOfStartPoint
                              .PointAfterReachedContent<EditorRawDOMPoint>());
      }
    }
  }

  // There is a demented possibility we have to check for.  We might have a very
  // strange selection that is not collapsed and yet does not contain any
  // editable content, and satisfies some of the above conditions that cause
  // tweaking.  In this case we don't want to tweak the selection into a block
  // it was never in, etc.  There are a variety of strategies one might use to
  // try to detect these cases, but I think the most straightforward is to see
  // if the adjusted locations "cross" the old values: i.e., new end before old
  // start, or new start after old end.  If so then just leave things alone.

  Maybe<int32_t> comp = nsContentUtils::ComparePoints(
      startPoint.ToRawRangeBoundary(), newRange.EndRef().ToRawRangeBoundary());

  if (NS_WARN_IF(!comp)) {
    return Err(NS_ERROR_FAILURE);
  }

  if (*comp == 1) {
    return EditorRawDOMRange();  // New end before old start.
  }

  comp = nsContentUtils::ComparePoints(newRange.StartRef().ToRawRangeBoundary(),
                                       endPoint.ToRawRangeBoundary());

  if (NS_WARN_IF(!comp)) {
    return Err(NS_ERROR_FAILURE);
  }

  if (*comp == 1) {
    return EditorRawDOMRange();  // New start after old end.
  }

  return newRange;
}

template <typename EditorDOMRangeType>
already_AddRefed<nsRange> HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const EditorDOMRangeType& aRange) {
  MOZ_DIAGNOSTIC_ASSERT(aRange.IsPositioned());
  return CreateRangeIncludingAdjuscentWhiteSpaces(aRange.StartRef(),
                                                  aRange.EndRef());
}

template <typename EditorDOMPointType1, typename EditorDOMPointType2>
already_AddRefed<nsRange> HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const EditorDOMPointType1& aStartPoint,
    const EditorDOMPointType2& aEndPoint) {
  MOZ_DIAGNOSTIC_ASSERT(!aStartPoint.IsInNativeAnonymousSubtree());
  MOZ_DIAGNOSTIC_ASSERT(!aEndPoint.IsInNativeAnonymousSubtree());

  if (!aStartPoint.IsInContentNode() || !aEndPoint.IsInContentNode()) {
    NS_WARNING_ASSERTION(aStartPoint.IsSet(), "aStartPoint was not set");
    NS_WARNING_ASSERTION(aEndPoint.IsSet(), "aEndPoint was not set");
    return nullptr;
  }

  const Element* const editingHost = ComputeEditingHost();
  if (NS_WARN_IF(!editingHost)) {
    return nullptr;
  }

  EditorDOMPoint startPoint = aStartPoint.template To<EditorDOMPoint>();
  EditorDOMPoint endPoint = aEndPoint.template To<EditorDOMPoint>();
  AutoClonedRangeArray::
      UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
          startPoint, endPoint, *editingHost);

  if (NS_WARN_IF(!startPoint.IsInContentNode()) ||
      NS_WARN_IF(!endPoint.IsInContentNode())) {
    NS_WARNING(
        "AutoClonedRangeArray::"
        "UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement() "
        "failed");
    return nullptr;
  }

  // For text actions, we want to look backwards (or forwards, as
  // appropriate) for additional white-space or nbsp's.  We may have to act
  // on these later even though they are outside of the initial selection.
  // Even if they are in another node!
  // XXX Those scanners do not treat siblings of the text nodes.  Perhaps,
  //     we should use `WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo()`
  //     and `WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces()` instead.
  if (startPoint.IsInTextNode()) {
    while (!startPoint.IsStartOfContainer()) {
      if (!startPoint.IsPreviousCharASCIISpaceOrNBSP()) {
        break;
      }
      MOZ_ALWAYS_TRUE(startPoint.RewindOffset());
    }
  }
  if (!startPoint.GetChildOrContainerIfDataNode() ||
      !startPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
          editingHost)) {
    return nullptr;
  }
  if (endPoint.IsInTextNode()) {
    while (!endPoint.IsEndOfContainer()) {
      if (!endPoint.IsCharASCIISpaceOrNBSP()) {
        break;
      }
      MOZ_ALWAYS_TRUE(endPoint.AdvanceOffset());
    }
  }
  EditorDOMPoint lastRawPoint(endPoint);
  if (!lastRawPoint.IsStartOfContainer()) {
    lastRawPoint.RewindOffset();
  }
  if (!lastRawPoint.GetChildOrContainerIfDataNode() ||
      !lastRawPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
          editingHost)) {
    return nullptr;
  }

  RefPtr<nsRange> range =
      nsRange::Create(startPoint.ToRawRangeBoundary(),
                      endPoint.ToRawRangeBoundary(), IgnoreErrors());
  NS_WARNING_ASSERTION(range, "nsRange::Create() failed");
  return range.forget();
}

Result<EditorDOMPoint, nsresult> HTMLEditor::MaybeSplitElementsAtEveryBRElement(
    nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
    EditSubAction aEditSubAction) {
  // Post-process the list to break up inline containers that contain br's, but
  // only for operations that might care, like making lists or paragraphs
  switch (aEditSubAction) {
    case EditSubAction::eCreateOrRemoveBlock:
    case EditSubAction::eFormatBlockForHTMLCommand:
    case EditSubAction::eMergeBlockContents:
    case EditSubAction::eCreateOrChangeList:
    case EditSubAction::eSetOrClearAlignment:
    case EditSubAction::eSetPositionToAbsolute:
    case EditSubAction::eIndent:
    case EditSubAction::eOutdent: {
      EditorDOMPoint pointToPutCaret;
      for (size_t index : Reversed(IntegerRange(aArrayOfContents.Length()))) {
        OwningNonNull<nsIContent>& content = aArrayOfContents[index];
        if (HTMLEditUtils::IsInlineContent(
                content, BlockInlineCheck::UseHTMLDefaultStyle) &&
            HTMLEditUtils::IsContainerNode(content) && !content->IsText()) {
          AutoTArray<OwningNonNull<nsIContent>, 24> arrayOfInlineContents;
          // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
          // alive.
          Result<EditorDOMPoint, nsresult> splitResult =
              SplitElementsAtEveryBRElement(MOZ_KnownLive(content),
                                            arrayOfInlineContents);
          if (splitResult.isErr()) {
            NS_WARNING("HTMLEditor::SplitElementsAtEveryBRElement() failed");
            return splitResult;
          }
          if (splitResult.inspect().IsSet()) {
            pointToPutCaret = splitResult.unwrap();
          }
          // Put these nodes in aArrayOfContents, replacing the current node
          aArrayOfContents.RemoveElementAt(index);
          aArrayOfContents.InsertElementsAt(index, arrayOfInlineContents);
        }
      }
      return pointToPutCaret;
    }
    default:
      return EditorDOMPoint();
  }
}

Result<EditorDOMPoint, nsresult>
HTMLEditor::SplitInlineAncestorsAtRangeBoundaries(
    RangeItem& aRangeItem, BlockInlineCheck aBlockInlineCheck,
    const Element& aEditingHost,
    const nsIContent* aAncestorLimiter /* = nullptr */) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  EditorDOMPoint pointToPutCaret;
  if (!aRangeItem.Collapsed() && aRangeItem.mEndContainer &&
      aRangeItem.mEndContainer->IsContent()) {
    nsCOMPtr<nsIContent> mostAncestorInlineContentAtEnd =
        HTMLEditUtils::GetMostDistantAncestorInlineElement(
            *aRangeItem.mEndContainer->AsContent(), aBlockInlineCheck,
            &aEditingHost, aAncestorLimiter);

    if (mostAncestorInlineContentAtEnd) {
      Result<SplitNodeResult, nsresult> splitEndInlineResult =
          SplitNodeDeepWithTransaction(
              *mostAncestorInlineContentAtEnd, aRangeItem.EndPoint(),
              SplitAtEdges::eDoNotCreateEmptyContainer);
      if (MOZ_UNLIKELY(splitEndInlineResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
            "eDoNotCreateEmptyContainer) failed");
        return splitEndInlineResult.propagateErr();
      }
      SplitNodeResult unwrappedSplitEndInlineResult =
          splitEndInlineResult.unwrap();
      unwrappedSplitEndInlineResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      if (pointToPutCaret.IsInContentNode() &&
          MOZ_UNLIKELY(
              &aEditingHost !=
              ComputeEditingHost(*pointToPutCaret.ContainerAs<nsIContent>()))) {
        NS_WARNING(
            "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
            "eDoNotCreateEmptyContainer) caused changing editing host");
        return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
      }
      const auto splitPointAtEnd =
          unwrappedSplitEndInlineResult.AtSplitPoint<EditorRawDOMPoint>();
      if (MOZ_UNLIKELY(!splitPointAtEnd.IsSet())) {
        NS_WARNING(
            "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
            "eDoNotCreateEmptyContainer) didn't return split point");
        return Err(NS_ERROR_FAILURE);
      }
      aRangeItem.mEndContainer = splitPointAtEnd.GetContainer();
      aRangeItem.mEndOffset = splitPointAtEnd.Offset();
    }
  }

  if (!aRangeItem.mStartContainer || !aRangeItem.mStartContainer->IsContent()) {
    return pointToPutCaret;
  }

  nsCOMPtr<nsIContent> mostAncestorInlineContentAtStart =
      HTMLEditUtils::GetMostDistantAncestorInlineElement(
          *aRangeItem.mStartContainer->AsContent(), aBlockInlineCheck,
          &aEditingHost, aAncestorLimiter);

  if (mostAncestorInlineContentAtStart) {
    Result<SplitNodeResult, nsresult> splitStartInlineResult =
        SplitNodeDeepWithTransaction(*mostAncestorInlineContentAtStart,
                                     aRangeItem.StartPoint(),
                                     SplitAtEdges::eDoNotCreateEmptyContainer);
    if (MOZ_UNLIKELY(splitStartInlineResult.isErr())) {
      NS_WARNING(
          "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
          "eDoNotCreateEmptyContainer) failed");
      return splitStartInlineResult.propagateErr();
    }
    SplitNodeResult unwrappedSplitStartInlineResult =
        splitStartInlineResult.unwrap();
    // XXX Why don't we check editing host like above??
    unwrappedSplitStartInlineResult.MoveCaretPointTo(
        pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
    // XXX If we split only here because of collapsed range, we're modifying
    //     only start point of aRangeItem.  Shouldn't we modify end point here
    //     if it's collapsed?
    const auto splitPointAtStart =
        unwrappedSplitStartInlineResult.AtSplitPoint<EditorRawDOMPoint>();
    if (MOZ_UNLIKELY(!splitPointAtStart.IsSet())) {
      NS_WARNING(
          "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
          "eDoNotCreateEmptyContainer) didn't return split point");
      return Err(NS_ERROR_FAILURE);
    }
    aRangeItem.mStartContainer = splitPointAtStart.GetContainer();
    aRangeItem.mStartOffset = splitPointAtStart.Offset();
  }

  return pointToPutCaret;
}

Result<EditorDOMPoint, nsresult> HTMLEditor::SplitElementsAtEveryBRElement(
    nsIContent& aMostAncestorToBeSplit,
    nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  // First build up a list of all the break nodes inside the inline container.
  AutoTArray<OwningNonNull<HTMLBRElement>, 24> arrayOfBRElements;
  DOMIterator iter(aMostAncestorToBeSplit);
  iter.AppendAllNodesToArray(arrayOfBRElements);

  // If there aren't any breaks, just put inNode itself in the array
  if (arrayOfBRElements.IsEmpty()) {
    aOutArrayOfContents.AppendElement(aMostAncestorToBeSplit);
    return EditorDOMPoint();
  }

  // Else we need to bust up aMostAncestorToBeSplit along all the breaks
  nsCOMPtr<nsIContent> nextContent = &aMostAncestorToBeSplit;
  EditorDOMPoint pointToPutCaret;
  for (OwningNonNull<HTMLBRElement>& brElement : arrayOfBRElements) {
    EditorDOMPoint atBRNode(brElement);
    if (NS_WARN_IF(!atBRNode.IsSet())) {
      return Err(NS_ERROR_FAILURE);
    }
    Result<SplitNodeResult, nsresult> splitNodeResult =
        SplitNodeDeepWithTransaction(
            *nextContent, atBRNode, SplitAtEdges::eAllowToCreateEmptyContainer);
    if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
      NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
      return splitNodeResult.propagateErr();
    }
    SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
    unwrappedSplitNodeResult.MoveCaretPointTo(
        pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
    // Put previous node at the split point.
    if (nsIContent* previousContent =
            unwrappedSplitNodeResult.GetPreviousContent()) {
      // Might not be a left node.  A break might have been at the very
      // beginning of inline container, in which case
      // SplitNodeDeepWithTransaction() would not actually split anything.
      aOutArrayOfContents.AppendElement(*previousContent);
    }

    // Move break outside of container and also put in node list
    // MOZ_KnownLive because 'arrayOfBRElements' is guaranteed to keep it alive.
    Result<MoveNodeResult, nsresult> moveBRElementResult =
        MoveNodeWithTransaction(
            MOZ_KnownLive(brElement),
            unwrappedSplitNodeResult.AtNextContent<EditorDOMPoint>());
    if (MOZ_UNLIKELY(moveBRElementResult.isErr())) {
      NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
      return moveBRElementResult.propagateErr();
    }
    MoveNodeResult unwrappedMoveBRElementResult = moveBRElementResult.unwrap();
    unwrappedMoveBRElementResult.MoveCaretPointTo(
        pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
    aOutArrayOfContents.AppendElement(brElement);

    nextContent = unwrappedSplitNodeResult.GetNextContent();
  }

  // Now tack on remaining next node.
  aOutArrayOfContents.AppendElement(*nextContent);

  return pointToPutCaret;
}

// static
void HTMLEditor::MakeTransitionList(
    const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
    nsTArray<bool>& aTransitionArray) {
  nsINode* prevParent = nullptr;
  aTransitionArray.EnsureLengthAtLeast(aArrayOfContents.Length());
  for (uint32_t i = 0; i < aArrayOfContents.Length(); i++) {
    aTransitionArray[i] = aArrayOfContents[i]->GetParentNode() != prevParent;
    prevParent = aArrayOfContents[i]->GetParentNode();
  }
}

Result<CreateElementResult, nsresult>
HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction(
    const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
    const Element& aEditingHost) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());

  // The idea here is to put the nodes into a minimal number of blockquotes.
  // When the user blockquotes something, they expect one blockquote.  That
  // may not be possible (for instance, if they have two table cells selected,
  // you need two blockquotes inside the cells).
  RefPtr<Element> curBlock, blockElementToPutCaret;
  nsCOMPtr<nsINode> prevParent;

  EditorDOMPoint pointToPutCaret;
  for (size_t i = 0; i < aArrayOfContents.Length(); i++) {
    const OwningNonNull<nsIContent>& content = aArrayOfContents[i];

    const auto IsNewBlockRequired = [](const nsIContent& aContent) {
      return HTMLEditUtils::IsAnyTableElementExceptTableElementAndColumElement(
                 aContent) ||
             HTMLEditUtils::IsListItemElement(aContent);
    };

    if (IsNewBlockRequired(content)) {
      // Forget any previous block
      curBlock = nullptr;
      // Recursion time
      AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
      HTMLEditUtils::CollectAllChildren(*content, childContents);
      Result<CreateElementResult, nsresult>
          wrapChildrenInAnotherBlockquoteResult =
              WrapContentsInBlockquoteElementsWithTransaction(childContents,
                                                              aEditingHost);
      if (MOZ_UNLIKELY(wrapChildrenInAnotherBlockquoteResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction() "
            "failed");
        return wrapChildrenInAnotherBlockquoteResult;
      }
      CreateElementResult unwrappedWrapChildrenInAnotherBlockquoteResult =
          wrapChildrenInAnotherBlockquoteResult.unwrap();
      unwrappedWrapChildrenInAnotherBlockquoteResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      if (unwrappedWrapChildrenInAnotherBlockquoteResult.GetNewNode()) {
        blockElementToPutCaret =
            unwrappedWrapChildrenInAnotherBlockquoteResult.UnwrapNewNode();
      }
    }

    // If the node has different parent than previous node, further nodes in a
    // new parent
    if (prevParent) {
      if (prevParent != content->GetParentNode()) {
        // Forget any previous blockquote node we were using
        curBlock = nullptr;
        prevParent = content->GetParentNode();
      }
    } else {
      prevParent = content->GetParentNode();
    }

    // If no curBlock, make one
    if (!curBlock) {
      Result<CreateElementResult, nsresult> createNewBlockquoteElementResult =
          InsertElementWithSplittingAncestorsWithTransaction(
              *nsGkAtoms::blockquote, EditorDOMPoint(content),
              BRElementNextToSplitPoint::Keep, aEditingHost);
      if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
            "nsGkAtoms::blockquote) failed");
        return createNewBlockquoteElementResult;
      }
      CreateElementResult unwrappedCreateNewBlockquoteElementResult =
          createNewBlockquoteElementResult.unwrap();
      unwrappedCreateNewBlockquoteElementResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      MOZ_ASSERT(unwrappedCreateNewBlockquoteElementResult.GetNewNode());
      blockElementToPutCaret =
          unwrappedCreateNewBlockquoteElementResult.GetNewNode();
      curBlock = unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode();
    }

    const OwningNonNull<nsIContent> lastContent = [&]() {
      nsIContent* lastContent = content;
      for (; i + 1 < aArrayOfContents.Length(); i++) {
        const OwningNonNull<nsIContent>& nextContent = aArrayOfContents[i + 1];
        if (lastContent->GetNextSibling() == nextContent ||
            !IsNewBlockRequired(nextContent)) {
          break;
        }
        lastContent = nextContent;
      }
      return OwningNonNull<nsIContent>(*lastContent);
    }();

    // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to/ keep it alive.
    Result<MoveNodeResult, nsresult> moveNodeResult =
        MoveSiblingsToEndWithTransaction(MOZ_KnownLive(content), lastContent,
                                         *curBlock);
    if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
      NS_WARNING("HTMLEditor::MoveSiblingsToEndWithTransaction() failed");
      return moveNodeResult.propagateErr();
    }
    MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
    unwrappedMoveNodeResult.MoveCaretPointTo(
        pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
  }
  return blockElementToPutCaret
             ? CreateElementResult(std::move(blockElementToPutCaret),
                                   std::move(pointToPutCaret))
             : CreateElementResult::NotHandled(std::move(pointToPutCaret));
}

Result<EditorDOMPoint, nsresult>
HTMLEditor::RemoveBlockContainerElementsWithTransaction(
    const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
    FormatBlockMode aFormatBlockMode, BlockInlineCheck aBlockInlineCheck) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand);

  // Intent of this routine is to be used for converting to/from headers,
  // paragraphs, pre, and address.  Those blocks that pretty much just contain
  // inline things...
  RefPtr<Element> blockElement;
  nsCOMPtr<nsIContent> firstContent, lastContent;
  EditorDOMPoint pointToPutCaret;
  for (const auto& content : aArrayOfContents) {
    // If the current node is a format element, remove it.
    if (HTMLEditUtils::IsFormatElementForParagraphStateCommand(content)) {
      // Process any partial progress saved
      if (blockElement) {
        Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
            RemoveBlockContainerElementWithTransactionBetween(
                *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
        if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
          NS_WARNING(
              "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
              "failed");
          return unwrapBlockElementResult.propagateErr();
        }
        unwrapBlockElementResult.unwrap().MoveCaretPointTo(
            pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
        firstContent = lastContent = blockElement = nullptr;
      }
      if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
        continue;
      }
      // Remove current block
      Result<EditorDOMPoint, nsresult> unwrapFormatBlockResult =
          RemoveBlockContainerWithTransaction(
              MOZ_KnownLive(*content->AsElement()));
      if (MOZ_UNLIKELY(unwrapFormatBlockResult.isErr())) {
        NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
        return unwrapFormatBlockResult;
      }
      if (unwrapFormatBlockResult.inspect().IsSet()) {
        pointToPutCaret = unwrapFormatBlockResult.unwrap();
      }
      continue;
    }

    // XXX How about, <th>, <thead>, <tfoot>, <dt>, <dl>?
    if (content->IsAnyOfHTMLElements(
            nsGkAtoms::table, nsGkAtoms::tr, nsGkAtoms::tbody, nsGkAtoms::td,
            nsGkAtoms::li, nsGkAtoms::blockquote, nsGkAtoms::div) ||
        HTMLEditUtils::IsListElement(*content)) {
      // Process any partial progress saved
      if (blockElement) {
        Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
            RemoveBlockContainerElementWithTransactionBetween(
                *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
        if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
          NS_WARNING(
              "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
              "failed");
          return unwrapBlockElementResult.propagateErr();
        }
        unwrapBlockElementResult.unwrap().MoveCaretPointTo(
            pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
        firstContent = lastContent = blockElement = nullptr;
      }
      if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
        continue;
      }
      // Recursion time
      AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
      HTMLEditUtils::CollectAllChildren(*content, childContents);
      Result<EditorDOMPoint, nsresult> removeBlockContainerElementsResult =
          RemoveBlockContainerElementsWithTransaction(
              childContents, aFormatBlockMode, aBlockInlineCheck);
      if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed");
        return removeBlockContainerElementsResult;
      }
      if (removeBlockContainerElementsResult.inspect().IsSet()) {
        pointToPutCaret = removeBlockContainerElementsResult.unwrap();
      }
      continue;
    }

    if (HTMLEditUtils::IsInlineContent(content, aBlockInlineCheck)) {
      if (blockElement) {
        // If so, is this node a descendant?
        if (EditorUtils::IsDescendantOf(*content, *blockElement)) {
          // Then we don't need to do anything different for this node
          lastContent = content;
          continue;
        }
        // Otherwise, we have progressed beyond end of blockElement, so let's
        // handle it now.  We need to remove the portion of blockElement that
        // contains [firstContent - lastContent].
        Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
            RemoveBlockContainerElementWithTransactionBetween(
                *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
        if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
          NS_WARNING(
              "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
              "failed");
          return unwrapBlockElementResult.propagateErr();
        }
        unwrapBlockElementResult.unwrap().MoveCaretPointTo(
            pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
        firstContent = lastContent = blockElement = nullptr;
        // Fall out and handle content
      }
      blockElement = HTMLEditUtils::GetAncestorElement(
          content, HTMLEditUtils::ClosestEditableBlockElement,
          aBlockInlineCheck);
      if (!blockElement ||
          !HTMLEditUtils::IsFormatElementForParagraphStateCommand(
              *blockElement) ||
          !HTMLEditUtils::IsRemovableNode(*blockElement)) {
        // Not a block kind that we care about.
        blockElement = nullptr;
      } else {
        firstContent = lastContent = content;
      }
      continue;
    }

    if (blockElement) {
      // Some node that is already sans block style.  Skip over it and process
      // any partial progress saved.
      Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
          RemoveBlockContainerElementWithTransactionBetween(
              *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
      if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
            "failed");
        return unwrapBlockElementResult.propagateErr();
      }
      unwrapBlockElementResult.unwrap().MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      firstContent = lastContent = blockElement = nullptr;
      continue;
    }
  }
  // Process any partial progress saved
  if (blockElement) {
    Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
        RemoveBlockContainerElementWithTransactionBetween(
            *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
    if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
      NS_WARNING(
          "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
          "failed");
      return unwrapBlockElementResult.propagateErr();
    }
    unwrapBlockElementResult.unwrap().MoveCaretPointTo(
        pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
    firstContent = lastContent = blockElement = nullptr;
  }
  return pointToPutCaret;
}

Result<CreateElementResult, nsresult>
HTMLEditor::CreateOrChangeFormatContainerElement(
    nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
    const nsStaticAtom& aNewFormatTagName, FormatBlockMode aFormatBlockMode,
    const Element& aEditingHost) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());

  // Intent of this routine is to be used for converting to/from headers,
  // paragraphs, pre, and address.  Those blocks that pretty much just contain
  // inline things...
  RefPtr<Element> newBlock, curBlock, blockElementToPutCaret;
  // If we found a <br> element which should be moved into curBlock, this keeps
  // storing the <br> element after removing it from the tree.
  RefPtr<Element> pendingBRElementToMoveCurBlock;
  EditorDOMPoint pointToPutCaret;
  for (size_t i = 0; i < aArrayOfContents.Length(); i++) {
    const OwningNonNull<nsIContent>& content = aArrayOfContents[i];

    EditorDOMPoint atContent(content);
    if (NS_WARN_IF(!atContent.IsInContentNode())) {
      // If given node has been removed from the document, let's ignore it
      // since the following code may need its parent replace it with new
      // block.
      curBlock = nullptr;
      newBlock = nullptr;
      pendingBRElementToMoveCurBlock = nullptr;
      continue;
    }

    const auto IsSameFormatBlockOrNonEditableBlock =
        [&aNewFormatTagName](const nsIContent& aContent) {
          return aContent.IsHTMLElement(&aNewFormatTagName) ||
                 (!EditorUtils::IsEditableContent(aContent, EditorType::HTML) &&
                  HTMLEditUtils::IsBlockElement(
                      aContent, BlockInlineCheck::UseHTMLDefaultStyle));
        };

    const auto IsMozDivOrFormatBlock =
        [&aFormatBlockMode](const nsIContent& aContent) {
          return HTMLEditUtils::IsMozDivElement(aContent) ||
                 HTMLEditor::IsFormatElement(aFormatBlockMode, aContent);
        };

    const auto IsNewFormatBlockRequired = [](const nsIContent& aContent) {
      return aContent.IsHTMLElement(nsGkAtoms::table) ||
             HTMLEditUtils::IsListElement(aContent) ||
             aContent.IsAnyOfHTMLElements(
                 nsGkAtoms::tbody, nsGkAtoms::tr, nsGkAtoms::td, nsGkAtoms::li,
                 nsGkAtoms::blockquote, nsGkAtoms::div);
    };

    const auto IsMovableInlineContent = [&aNewFormatTagName](
                                            const nsIContent& aContent) {
      return HTMLEditUtils::IsInlineContent(
                 aContent, BlockInlineCheck::UseHTMLDefaultStyle) &&
             // If content is a non editable, drop it if we are going to <pre>.
             !(&aNewFormatTagName == nsGkAtoms::pre &&
               !EditorUtils::IsEditableContent(aContent, EditorType::HTML));
    };

    const auto IsMovableInlineContentSibling = [&](const nsIContent& aContent) {
      return !IsSameFormatBlockOrNonEditableBlock(aContent) &&
             !IsMozDivOrFormatBlock(aContent) &&
             !IsNewFormatBlockRequired(aContent) &&
             !aContent.IsHTMLElement(nsGkAtoms::br) &&
             IsMovableInlineContent(aContent);
    };

    // Is it already the right kind of block, or an uneditable block?
    if (IsSameFormatBlockOrNonEditableBlock(content)) {
      // Forget any previous block used for previous inline nodes
      curBlock = nullptr;
      pendingBRElementToMoveCurBlock = nullptr;
      // Do nothing to this block
      continue;
    }

    // If content is a format element, replace it with a new block of correct
    // type.
    // XXX: pre can't hold everything the others can
    if (IsMozDivOrFormatBlock(content)) {
      // Forget any previous block used for previous inline nodes
      curBlock = nullptr;
      pendingBRElementToMoveCurBlock = nullptr;
      RefPtr<Element> expectedContainerOfNewBlock =
          atContent.IsContainerHTMLElement(nsGkAtoms::dl) &&
                  HTMLEditUtils::IsSplittableNode(
                      *atContent.ContainerAs<Element>())
              ? atContent.GetContainerParentAs<Element>()
              : atContent.GetContainerAs<Element>();
      Result<CreateElementResult, nsresult> replaceWithNewBlockElementResult =
          ReplaceContainerAndCloneAttributesWithTransaction(
              MOZ_KnownLive(*content->AsElement()), aNewFormatTagName);
      if (MOZ_UNLIKELY(replaceWithNewBlockElementResult.isErr())) {
        NS_WARNING(
            "EditorBase::ReplaceContainerAndCloneAttributesWithTransaction() "
            "failed");
        return replaceWithNewBlockElementResult;
      }
      CreateElementResult unwrappedReplaceWithNewBlockElementResult =
          replaceWithNewBlockElementResult.unwrap();
      // If the new block element was moved to different element or removed by
      // the web app via mutation event listener, we should stop handling this
      // action since we cannot handle each of a lot of edge cases.
      if (NS_WARN_IF(unwrappedReplaceWithNewBlockElementResult.GetNewNode()
                         ->GetParentNode() != expectedContainerOfNewBlock)) {
        unwrappedReplaceWithNewBlockElementResult.IgnoreCaretPointSuggestion();
        return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
      }
      unwrappedReplaceWithNewBlockElementResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      newBlock = unwrappedReplaceWithNewBlockElementResult.UnwrapNewNode();
      continue;
    }

    if (IsNewFormatBlockRequired(content)) {
      // Forget any previous block used for previous inline nodes
      curBlock = nullptr;
      pendingBRElementToMoveCurBlock = nullptr;
      // Recursion time
      AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
      HTMLEditUtils::CollectAllChildren(*content, childContents);
      if (!childContents.IsEmpty()) {
        Result<CreateElementResult, nsresult> wrapChildrenInBlockElementResult =
            CreateOrChangeFormatContainerElement(
                childContents, aNewFormatTagName, aFormatBlockMode,
                aEditingHost);
        if (MOZ_UNLIKELY(wrapChildrenInBlockElementResult.isErr())) {
          NS_WARNING(
              "HTMLEditor::CreateOrChangeFormatContainerElement() failed");
          return wrapChildrenInBlockElementResult;
        }
        CreateElementResult unwrappedWrapChildrenInBlockElementResult =
            wrapChildrenInBlockElementResult.unwrap();
        unwrappedWrapChildrenInBlockElementResult.MoveCaretPointTo(
            pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
        if (unwrappedWrapChildrenInBlockElementResult.GetNewNode()) {
          blockElementToPutCaret =
              unwrappedWrapChildrenInBlockElementResult.UnwrapNewNode();
        }
        continue;
      }

      // Make sure we can put a block here
      Result<CreateElementResult, nsresult> createNewBlockElementResult =
          InsertElementWithSplittingAncestorsWithTransaction(
              aNewFormatTagName, atContent, BRElementNextToSplitPoint::Keep,
              aEditingHost);
      if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
        NS_WARNING(
            nsPrintfCString(
                "HTMLEditor::"
                "InsertElementWithSplittingAncestorsWithTransaction(%s) failed",
                nsAtomCString(&aNewFormatTagName).get())
                .get());
        return createNewBlockElementResult;
      }
      CreateElementResult unwrappedCreateNewBlockElementResult =
          createNewBlockElementResult.unwrap();
      unwrappedCreateNewBlockElementResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode());
      blockElementToPutCaret =
          unwrappedCreateNewBlockElementResult.UnwrapNewNode();
      continue;
    }

    if (content->IsHTMLElement(nsGkAtoms::br)) {
      if (curBlock) {
        if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand) {
          // If the node is a break, we honor it by putting further nodes in a
          // new parent.

          // Forget any previous block used for previous inline nodes.
          curBlock = nullptr;
          pendingBRElementToMoveCurBlock = nullptr;
        } else {
          // If the node is a break, we need to move it into end of the curBlock
          // if we'll move following content into curBlock.
          pendingBRElementToMoveCurBlock = content->AsElement();
        }
        // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
        // alive.
        nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
        if (NS_FAILED(rv)) {
          NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
          return Err(rv);
        }
        continue;
      }

      // The break is the first (or even only) node we encountered.  Create a
      // block for it.
      Result<CreateElementResult, nsresult> createNewBlockElementResult =
          InsertElementWithSplittingAncestorsWithTransaction(
              aNewFormatTagName, atContent, BRElementNextToSplitPoint::Keep,
              aEditingHost);
      if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
        NS_WARNING(nsPrintfCString("HTMLEditor::"
                                   "InsertElementWithSplittingAncestorsWith"
                                   "Transaction(%s) failed",
                                   nsAtomCString(&aNewFormatTagName).get())
                       .get());
        return createNewBlockElementResult;
      }
      CreateElementResult unwrappedCreateNewBlockElementResult =
          createNewBlockElementResult.unwrap();
      unwrappedCreateNewBlockElementResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      RefPtr<Element> newBlockElement =
          unwrappedCreateNewBlockElementResult.UnwrapNewNode();
      MOZ_ASSERT(newBlockElement);
      blockElementToPutCaret = newBlockElement;
      // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
      // alive.
      Result<MoveNodeResult, nsresult> moveNodeResult =
          MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
                                       *newBlockElement);
      if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
        NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
        return moveNodeResult.propagateErr();
      }
      MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
      unwrappedMoveNodeResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      curBlock = std::move(newBlockElement);
      continue;
    }

    if (!IsMovableInlineContent(content)) {
      // Do nothing to this block
      continue;
    }
    MOZ_ASSERT(IsMovableInlineContentSibling(content));

    // If no curBlock, make one
    if (!curBlock) {
      Result<CreateElementResult, nsresult> createNewBlockElementResult =
          InsertElementWithSplittingAncestorsWithTransaction(
              aNewFormatTagName, atContent, BRElementNextToSplitPoint::Keep,
              aEditingHost);
      if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
        NS_WARNING(nsPrintfCString("HTMLEditor::"
                                   "InsertElementWithSplittingAncestorsWith"
                                   "Transaction(%s) failed",
                                   nsAtomCString(&aNewFormatTagName).get())
                       .get());
        return createNewBlockElementResult;
      }
      CreateElementResult unwrappedCreateNewBlockElementResult =
          createNewBlockElementResult.unwrap();
      unwrappedCreateNewBlockElementResult.MoveCaretPointTo(
          pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
      MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode());
      blockElementToPutCaret =
          unwrappedCreateNewBlockElementResult.GetNewNode();
      curBlock = unwrappedCreateNewBlockElementResult.UnwrapNewNode();

      // Update container of content.
      atContent.Set(content);
      if (NS_WARN_IF(!atContent.IsSet())) {
        // This is possible due to mutation events, let's not assert
        return Err(NS_ERROR_UNEXPECTED);
      }
    } else if (pendingBRElementToMoveCurBlock) {
      Result<CreateElementResult, nsresult> insertBRElementResult =
          InsertNodeWithTransaction<Element>(
              *pendingBRElementToMoveCurBlock,
              EditorDOMPoint::AtEndOf(*curBlock));
      if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
        NS_WARNING("EditorBase::InsertNodeWithTransaction<Element>() failed");
        return insertBRElementResult.propagateErr();
      }
      insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
      pendingBRElementToMoveCurBlock = nullptr;
    }

    // This is a continuation of some inline nodes that belong together in
    // the same block item.  Use curBlock.
    const OwningNonNull<nsIContent> lastContent = [&]() {
      nsIContent* lastContent = content;
      for (; i + 1 < aArrayOfContents.Length(); i++) {
        const OwningNonNull<nsIContent>& nextContent = aArrayOfContents[i + 1];
        if (lastContent->GetNextSibling() != nextContent ||
            !IsMovableInlineContentSibling(nextContent)) {
          break;
        }
        lastContent = nextContent;
      }
      return OwningNonNull<nsIContent>(*lastContent);
    }();
    // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
    // alive.  We could try to make that a rvalue ref and create a const array
    // on the stack here, but callers are passing in auto arrays, and we don't
    // want to introduce copies..
    Result<MoveNodeResult, nsresult> moveNodeResult =
        MoveSiblingsToEndWithTransaction(MOZ_KnownLive(content), lastContent,
                                         *curBlock);
    if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
      NS_WARNING("HTMLEditor::MoveSiblingsToEndWithTransaction() failed");
      return moveNodeResult.propagateErr();
    }
    MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
    unwrappedMoveNodeResult.MoveCaretPointTo(
        pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
  }
  return blockElementToPutCaret
             ? CreateElementResult(std::move(blockElementToPutCaret),
                                   std::move(pointToPutCaret))
             : CreateElementResult::NotHandled(std::move(pointToPutCaret));
}

Result<SplitNodeResult, nsresult>
HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction(
    const nsAtom& aTag, const EditorDOMPoint& aStartOfDeepestRightNode,
    const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  if (NS_WARN_IF(!aEditingHost.IsInComposedDoc())) {
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }

  if (NS_WARN_IF(!aStartOfDeepestRightNode.IsSet())) {
    return Err(NS_ERROR_INVALID_ARG);
  }
  MOZ_ASSERT(aStartOfDeepestRightNode.IsSetAndValid());

  // The point must be descendant of editing host.
  // XXX Isn't it a valid case if it points a direct child of aEditingHost?
  if (NS_WARN_IF(
          !aStartOfDeepestRightNode.GetContainer()->IsInclusiveDescendantOf(
              &aEditingHost))) {
    return Err(NS_ERROR_INVALID_ARG);
  }

  // Look for a node that can legally contain the tag.
  const EditorDOMPoint pointToInsert =
      HTMLEditUtils::GetInsertionPointInInclusiveAncestor(
          aTag, aStartOfDeepestRightNode, &aEditingHost);
  if (MOZ_UNLIKELY(!pointToInsert.IsSet())) {
    NS_WARNING(
        "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() reached "
        "editing host");
    return Err(NS_ERROR_FAILURE);
  }
  // If the point itself can contain the tag, we don't need to split any
  // ancestor nodes.  In this case, we should return the given split point
  // as is.
  if (pointToInsert.GetContainer() == aStartOfDeepestRightNode.GetContainer()) {
    return SplitNodeResult::NotHandled(aStartOfDeepestRightNode);
  }

  Result<SplitNodeResult, nsresult> splitNodeResult =
      SplitNodeDeepWithTransaction(MOZ_KnownLive(*pointToInsert.GetChild()),
                                   aStartOfDeepestRightNode,
                                   SplitAtEdges::eAllowToCreateEmptyContainer);
  NS_WARNING_ASSERTION(splitNodeResult.isOk(),
                       "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
                       "eAllowToCreateEmptyContainer) failed");
  return splitNodeResult;
}

Result<CreateElementResult, nsresult>
HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(
    const nsAtom& aTagName, const EditorDOMPoint& aPointToInsert,
    BRElementNextToSplitPoint aBRElementNextToSplitPoint,
    const Element& aEditingHost,
    const InitializeInsertingElement& aInitializer) {
  MOZ_ASSERT(aPointToInsert.IsSetAndValid());

  const nsCOMPtr<nsIContent> childAtPointToInsert = aPointToInsert.GetChild();
  Result<SplitNodeResult, nsresult> splitNodeResult =
      MaybeSplitAncestorsForInsertWithTransaction(aTagName, aPointToInsert,
                                                  aEditingHost);
  if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
    NS_WARNING(
        "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed");
    return splitNodeResult.propagateErr();
  }
  SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
  DebugOnly<bool> wasCaretPositionSuggestedAtSplit =
      unwrappedSplitNodeResult.HasCaretPointSuggestion();
  // We'll update selection below, and nobody touches selection until then.
  // Therefore, we don't need to touch selection here.
  unwrappedSplitNodeResult.IgnoreCaretPointSuggestion();

  // If current handling node has been moved from the container by a
  // mutation event listener when we need to do something more for it,
  // we should stop handling this action since we cannot handle each
  // edge case.
  if (childAtPointToInsert &&
      NS_WARN_IF(!childAtPointToInsert->IsInclusiveDescendantOf(
          unwrappedSplitNodeResult.DidSplit()
              ? unwrappedSplitNodeResult.GetNextContent()
              : aPointToInsert.GetContainer()))) {
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }

  auto splitPoint = unwrappedSplitNodeResult.AtSplitPoint<EditorDOMPoint>();
  if (aBRElementNextToSplitPoint == BRElementNextToSplitPoint::Delete) {
    // Consume a trailing br, if any.  This is to keep an alignment from
    // creating extra lines, if possible.
    if (nsCOMPtr<nsIContent> maybeBRContent =
            HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
                splitPoint,
                {LeafNodeOption::IgnoreNonEditableNode,
                 LeafNodeOption::TreatChildBlockAsLeafNode},
                BlockInlineCheck::UseComputedDisplayOutsideStyle,
                &aEditingHost)) {
      if (maybeBRContent->IsHTMLElement(nsGkAtoms::br) &&
          splitPoint.GetChild()) {
        // Making use of html structure... if next node after where we are
        // putting our div is not a block, then the br we found is in same
        // block we are, so it's safe to consume it.
        if (nsIContent* const nextEditableSibling =
                HTMLEditUtils::GetNextSibling(
                    *splitPoint.GetChild(),
                    {LeafNodeOption::IgnoreNonEditableNode},
                    BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
          if (!HTMLEditUtils::IsBlockElement(
                  *nextEditableSibling,
                  BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
            AutoEditorDOMPointChildInvalidator lockOffset(splitPoint);
            nsresult rv = DeleteNodeWithTransaction(*maybeBRContent);
            if (NS_FAILED(rv)) {
              NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
              return Err(rv);
            }
          }
        }
      }
    }
  }

  Result<CreateElementResult, nsresult> createNewElementResult =
      CreateAndInsertElement(WithTransaction::Yes, aTagName, splitPoint,
                             aInitializer);
  if (MOZ_UNLIKELY(createNewElementResult.isErr())) {
    NS_WARNING(
        "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
    return createNewElementResult;
  }
  MOZ_ASSERT_IF(wasCaretPositionSuggestedAtSplit,
                createNewElementResult.inspect().HasCaretPointSuggestion());
  MOZ_ASSERT(createNewElementResult.inspect().GetNewNode());

  // If the new block element was moved to different element or removed by
  // the web app via mutation event listener, we should stop handling this
  // action since we cannot handle each of a lot of edge cases.
  if (NS_WARN_IF(
          createNewElementResult.inspect().GetNewNode()->GetParentNode() !=
          splitPoint.GetContainer())) {
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }

  return createNewElementResult;
}

nsresult HTMLEditor::JoinNearestEditableNodesWithTransaction(
    nsIContent& aNodeLeft, nsIContent& aNodeRight,
    EditorDOMPoint* aNewFirstChildOfRightNode) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aNewFirstChildOfRightNode);

  // Caller responsible for left and right node being the same type
  if (NS_WARN_IF(!aNodeLeft.GetParentNode())) {
    return NS_ERROR_FAILURE;
  }
  // If they don't have the same parent, first move the right node to after
  // the left one
  if (aNodeLeft.GetParentNode() != aNodeRight.GetParentNode()) {
    Result<MoveNodeResult, nsresult> moveNodeResult =
        MoveNodeWithTransaction(aNodeRight, EditorDOMPoint(&aNodeLeft));
    if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
      NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
      return moveNodeResult.unwrapErr();
    }
    nsresult rv = moveNodeResult.inspect().SuggestCaretPointTo(
        *this, {SuggestCaret::OnlyIfHasSuggestion,
                SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
                SuggestCaret::AndIgnoreTrivialError});
    if (NS_FAILED(rv)) {
      NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
      return rv;
    }
    NS_WARNING_ASSERTION(
        rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
        "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
  }

  // Separate join rules for differing blocks
  if (HTMLEditUtils::IsListElement(aNodeLeft) || aNodeLeft.IsText()) {
    // For lists, merge shallow (wouldn't want to combine list items)
    Result<JoinNodesResult, nsresult> joinNodesResult =
        JoinNodesWithTransaction(aNodeLeft, aNodeRight);
    if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
      NS_WARNING("HTMLEditor::JoinNodesWithTransaction failed");
      return joinNodesResult.unwrapErr();
    }
    *aNewFirstChildOfRightNode =
        joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>();
    return NS_OK;
  }

  // Remember the last left child, and first right child
  const nsCOMPtr<nsIContent> lastEditableChildOfLeftContent =
      HTMLEditUtils::GetLastChild(
          aNodeLeft, {LeafNodeOption::IgnoreNonEditableNode},
          BlockInlineCheck::UseComputedDisplayOutsideStyle);
  if (MOZ_UNLIKELY(NS_WARN_IF(!lastEditableChildOfLeftContent))) {
    return NS_ERROR_FAILURE;
  }

  const nsCOMPtr<nsIContent> firstEditableChildOfRightContent =
      HTMLEditUtils::GetFirstChild(
          aNodeRight, {LeafNodeOption::IgnoreNonEditableNode},
          BlockInlineCheck::UseComputedDisplayOutsideStyle);
  if (NS_WARN_IF(!firstEditableChildOfRightContent)) {
    return NS_ERROR_FAILURE;
  }

  // For list items, divs, etc., merge smart
  Result<JoinNodesResult, nsresult> joinNodesResult =
      JoinNodesWithTransaction(aNodeLeft, aNodeRight);
  if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
    NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
    return joinNodesResult.unwrapErr();
  }

  if ((lastEditableChildOfLeftContent->IsText() ||
       lastEditableChildOfLeftContent->IsElement()) &&
      HTMLEditUtils::CanContentsBeJoined(*lastEditableChildOfLeftContent,
                                         *firstEditableChildOfRightContent)) {
    nsresult rv = JoinNearestEditableNodesWithTransaction(
        *lastEditableChildOfLeftContent, *firstEditableChildOfRightContent,
        aNewFirstChildOfRightNode);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::JoinNearestEditableNodesWithTransaction() failed");
    return rv;
  }
  *aNewFirstChildOfRightNode =
      joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>();
  return NS_OK;
}

Element* HTMLEditor::GetMostDistantAncestorMailCiteElement(
    const nsINode& aNode) const {
  Element* mailCiteElement = nullptr;
  const bool isPlaintextEditor = IsPlaintextMailComposer();
  for (Element* element : aNode.InclusiveAncestorsOfType<Element>()) {
    if ((isPlaintextEditor && element->IsHTMLElement(nsGkAtoms::pre)) ||
        HTMLEditUtils::IsMailCiteElement(*element)) {
      mailCiteElement = element;
      continue;
    }
    if (element->IsHTMLElement(nsGkAtoms::body)) {
      break;
    }
  }
  return mailCiteElement;
}

nsresult HTMLEditor::CacheInlineStyles(Element& Element) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());

  nsresult rv = GetInlineStyles(
      Element, *TopLevelEditSubActionDataRef().mCachedPendingStyles);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::GetInlineStyles() failed");
  return rv;
}

nsresult HTMLEditor::GetInlineStyles(
    Element& aElement, AutoPendingStyleCacheArray& aPendingStyleCacheArray) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aPendingStyleCacheArray.IsEmpty());

  if (!IsCSSEnabled()) {
    // In the HTML styling mode, we should preserve the order of inline styles
    // specified with HTML elements, then, we can keep same order as original
    // one when we create new elements to apply the styles at new place.
    // XXX Currently, we don't preserve all inline parents, therefore, we cannot
    //     restore all inline elements as-is.  Perhaps, we should store all
    //     inline elements with more details (e.g., all attributes), and store
    //     same elements.  For example, web apps may give style as:
    //     em {
    //       font-style: italic;
    //     }
    //     em em {
    //       font-style: normal;
    //       font-weight: bold;
    //     }
    //     but we cannot restore the style as-is.
    nsString value;
    const bool givenElementIsEditable =
        HTMLEditUtils::IsSimplyEditableNode(aElement);
    auto NeedToAppend = [&](nsStaticAtom& aTagName, nsStaticAtom* aAttribute) {
      if (mPendingStylesToApplyToNewContent->GetStyleState(
              aTagName, aAttribute) != PendingStyleState::NotUpdated) {
        return false;  // The style has already been changed.
      }
      if (aPendingStyleCacheArray.Contains(aTagName, aAttribute)) {
        return false;  // Already preserved
      }
      return true;
    };
    for (Element* const inclusiveAncestor :
         aElement.InclusiveAncestorsOfType<Element>()) {
      if (HTMLEditUtils::IsBlockElement(
              *inclusiveAncestor,
              BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
          (givenElementIsEditable &&
           !HTMLEditUtils::IsSimplyEditableNode(*inclusiveAncestor))) {
        break;
      }
      if (inclusiveAncestor->IsAnyOfHTMLElements(
              nsGkAtoms::b, nsGkAtoms::i, nsGkAtoms::u, nsGkAtoms::s,
              nsGkAtoms::strike, nsGkAtoms::tt, nsGkAtoms::em,
              nsGkAtoms::strong, nsGkAtoms::dfn, nsGkAtoms::code,
              nsGkAtoms::samp, nsGkAtoms::var, nsGkAtoms::cite, nsGkAtoms::abbr,
              nsGkAtoms::acronym, nsGkAtoms::sub, nsGkAtoms::sup)) {
        nsStaticAtom& tagName = const_cast<nsStaticAtom&>(
            *inclusiveAncestor->NodeInfo()->NameAtom()->AsStatic());
        if (NeedToAppend(tagName, nullptr)) {
          aPendingStyleCacheArray.AppendElement(
              PendingStyleCache(tagName, nullptr, EmptyString()));
        }
        continue;
      }
      if (inclusiveAncestor->IsHTMLElement(nsGkAtoms::font)) {
        if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::face)) {
          inclusiveAncestor->GetAttr(nsGkAtoms::face, value);
          if (!value.IsEmpty()) {
            aPendingStyleCacheArray.AppendElement(
                PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::face, value));
            value.Truncate();
          }
        }
        if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::size)) {
          inclusiveAncestor->GetAttr(nsGkAtoms::size, value);
          if (!value.IsEmpty()) {
            aPendingStyleCacheArray.AppendElement(
                PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::size, value));
            value.Truncate();
          }
        }
        if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::color)) {
          inclusiveAncestor->GetAttr(nsGkAtoms::color, value);
          if (!value.IsEmpty()) {
            aPendingStyleCacheArray.AppendElement(
                PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::color, value));
            value.Truncate();
          }
        }
        continue;
      }
    }
    return NS_OK;
  }

  for (nsStaticAtom* property : {nsGkAtoms::b,
                                 nsGkAtoms::i,
                                 nsGkAtoms::u,
                                 nsGkAtoms::s,
                                 nsGkAtoms::strike,
                                 nsGkAtoms::face,
                                 nsGkAtoms::size,
                                 nsGkAtoms::color,
                                 nsGkAtoms::tt,
                                 nsGkAtoms::em,
                                 nsGkAtoms::strong,
                                 nsGkAtoms::dfn,
                                 nsGkAtoms::code,
                                 nsGkAtoms::samp,
                                 nsGkAtoms::var,
                                 nsGkAtoms::cite,
                                 nsGkAtoms::abbr,
                                 nsGkAtoms::acronym,
                                 nsGkAtoms::background_color,
                                 nsGkAtoms::sub,
                                 nsGkAtoms::sup}) {
    const EditorInlineStyle style =
        property == nsGkAtoms::face || property == nsGkAtoms::size ||
                property == nsGkAtoms::color
            ? EditorInlineStyle(*nsGkAtoms::font, property)
            : EditorInlineStyle(*property);
    // If type-in state is set, don't intervene
    const PendingStyleState styleState =
        mPendingStylesToApplyToNewContent->GetStyleState(*style.mHTMLProperty,
                                                         style.mAttribute);
    if (styleState != PendingStyleState::NotUpdated) {
      continue;
    }
    bool isSet = false;
    nsString value;  // Don't use nsAutoString here because it requires memcpy
                     // at creating new PendingStyleCache instance.
    // Don't use CSS for <font size>, we don't support it usefully (bug 780035)
    if (property == nsGkAtoms::size) {
      isSet = HTMLEditUtils::IsInlineStyleSetByElement(aElement, style, nullptr,
                                                       &value);
    } else if (style.IsCSSSettable(aElement)) {
      Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
          CSSEditUtils::IsComputedCSSEquivalentTo(*this, aElement, style,
                                                  value);
      if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
        NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
        return isComputedCSSEquivalentToStyleOrError.unwrapErr();
      }
      isSet = isComputedCSSEquivalentToStyleOrError.unwrap();
    }
    if (isSet) {
      aPendingStyleCacheArray.AppendElement(
          style.ToPendingStyleCache(std::move(value)));
    }
  }
  return NS_OK;
}

nsresult HTMLEditor::ReapplyCachedStyles() {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());

  // The idea here is to examine our cached list of styles and see if any have
  // been removed.  If so, add typeinstate for them, so that they will be
  // reinserted when new content is added.

  if (TopLevelEditSubActionDataRef().mCachedPendingStyles->IsEmpty() ||
      !SelectionRef().RangeCount()) {
    return NS_OK;
  }

  // remember if we are in css mode
  const bool useCSS = IsCSSEnabled();

  const RangeBoundary& atStartOfSelection =
      SelectionRef().GetRangeAt(0)->StartRef();
  const RefPtr<Element> startContainerElement =
      atStartOfSelection.GetContainer() &&
              atStartOfSelection.GetContainer()->IsContent()
          ? atStartOfSelection.GetContainer()->GetAsElementOrParentElement()
          : nullptr;
  if (NS_WARN_IF(!startContainerElement)) {
    return NS_OK;
  }

  AutoPendingStyleCacheArray styleCacheArrayAtInsertionPoint;
  nsresult rv =
      GetInlineStyles(*startContainerElement, styleCacheArrayAtInsertionPoint);
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::GetInlineStyles() failed, but ignored");
    return NS_OK;
  }

  for (PendingStyleCache& styleCacheBeforeEdit :
       Reversed(*TopLevelEditSubActionDataRef().mCachedPendingStyles)) {
    bool isFirst = false, isAny = false, isAll = false;
    nsAutoString currentValue;
    const EditorInlineStyle inlineStyle = styleCacheBeforeEdit.ToInlineStyle();
    if (useCSS && inlineStyle.IsCSSSettable(*startContainerElement)) {
      // check computed style first in css case
      // MOZ_KnownLive(styleCacheBeforeEdit.*) because they are nsStaticAtom
      // and its instances are alive until shutting down.
      Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
          CSSEditUtils::IsComputedCSSEquivalentTo(*this, *startContainerElement,
                                                  inlineStyle, currentValue);
      if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
        NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
        return isComputedCSSEquivalentToStyleOrError.unwrapErr();
      }
      isAny = isComputedCSSEquivalentToStyleOrError.unwrap();
    }
    if (!isAny) {
      // then check typeinstate and html style
      nsresult rv = GetInlinePropertyBase(
          inlineStyle, &styleCacheBeforeEdit.AttributeValueOrCSSValueRef(),
          &isFirst, &isAny, &isAll, ¤tValue);
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::GetInlinePropertyBase() failed");
        return rv;
      }
    }
    // This style has disappeared through deletion.  Let's add the styles to
    // mPendingStylesToApplyToNewContent when same style isn't applied to the
    // node already.
    if (isAny &&
        !IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) {
      continue;
    }
    AutoPendingStyleCacheArray::index_type index =
        styleCacheArrayAtInsertionPoint.IndexOf(
            styleCacheBeforeEdit.TagRef(), styleCacheBeforeEdit.GetAttribute());
    if (index == AutoPendingStyleCacheArray::NoIndex ||
        styleCacheBeforeEdit.AttributeValueOrCSSValueRef() !=
            styleCacheArrayAtInsertionPoint.ElementAt(index)
                .AttributeValueOrCSSValueRef()) {
      mPendingStylesToApplyToNewContent->PreserveStyle(styleCacheBeforeEdit);
    }
  }
  return NS_OK;
}

nsresult HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange(
    const RawRangeBoundary& aStartRef, const RawRangeBoundary& aEndRef) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  AutoTArray<OwningNonNull<Element>, 64> arrayOfEmptyElements;
  DOMIterator iter;
  if (NS_FAILED(iter.Init(aStartRef, aEndRef))) {
    NS_WARNING("DOMIterator::Init() failed");
    return NS_ERROR_FAILURE;
  }
  iter.AppendNodesToArray(
      +[](nsINode& aNode, void* aSelf) {
        MOZ_ASSERT(Element::FromNode(&aNode));
        MOZ_ASSERT(aSelf);
        Element& element = *aNode.AsElement();
        if (!EditorUtils::IsEditableContent(element, EditorType::HTML) ||
            (!HTMLEditUtils::IsListItemElement(element) &&
             !HTMLEditUtils::IsTableCellOrCaptionElement(element))) {
          return false;
        }
        return HTMLEditUtils::IsEmptyNode(
            element, {EmptyCheckOption::TreatSingleBRElementAsVisible,
                      EmptyCheckOption::TreatNonEditableContentAsInvisible});
      },
      arrayOfEmptyElements, this);

  // Put padding <br> elements for empty <li> and <td>.
  EditorDOMPoint pointToPutCaret;
  for (auto& emptyElement : arrayOfEmptyElements) {
    // Need to put br at END of node.  It may have empty containers in it and
    // still pass the "IsEmptyNode" test, and we want the br's to be after
    // them.  Also, we want the br to be after the selection if the selection
    // is in this node.
    EditorDOMPoint endOfNode(EditorDOMPoint::AtEndOf(emptyElement));
    Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
        InsertPaddingBRElementForEmptyLastLineWithTransaction(endOfNode);
    if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
      NS_WARNING(
          "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
          "failed");
      return insertPaddingBRElementResult.unwrapErr();
    }
    CreateElementResult unwrappedInsertPaddingBRElementResult =
        insertPaddingBRElementResult.unwrap();
    unwrappedInsertPaddingBRElementResult.MoveCaretPointTo(
        pointToPutCaret, *this,
        {SuggestCaret::OnlyIfHasSuggestion,
         SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
  }
  if (pointToPutCaret.IsSet()) {
    nsresult rv = CollapseSelectionTo(pointToPutCaret);
    if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
      NS_WARNING(
          "EditorBase::CollapseSelectionTo() caused destroying the editor");
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "EditorBase::CollapseSelectionTo() failed, but ignored");
  }
  return NS_OK;
}

void HTMLEditor::SetSelectionInterlinePosition() {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(SelectionRef().IsCollapsed());

  // Get the (collapsed) selection location
  const nsRange* firstRange = SelectionRef().GetRangeAt(0);
  if (NS_WARN_IF(!firstRange)) {
    return;
  }

  EditorDOMPoint atCaret(firstRange->StartRef());
  if (NS_WARN_IF(!atCaret.IsSet())) {
    return;
  }
  MOZ_ASSERT(atCaret.IsSetAndValid());

  // First, let's check to see if we are after a `<br>`.  We take care of this
  // special-case first so that we don't accidentally fall through into one of
  // the other conditionals.
  // XXX Although I don't understand "interline position", if caret is
  //     immediately after non-editable contents, but previous editable
  //     content is `<br>`, does this do right thing?
  if (Element* editingHost = ComputeEditingHost()) {
    if (nsIContent* previousEditableContentInBlock =
            HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
                atCaret,
                {LeafNodeOption::IgnoreNonEditableNode,
                 LeafNodeOption::TreatChildBlockAsLeafNode},
                BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
      if (previousEditableContentInBlock->IsHTMLElement(nsGkAtoms::br)) {
        DebugOnly<nsresult> rvIgnored = SelectionRef().SetInterlinePosition(
            InterlinePosition::StartOfNextLine);
        NS_WARNING_ASSERTION(
            NS_SUCCEEDED(rvIgnored),
            "Selection::SetInterlinePosition(InterlinePosition::"
            "StartOfNextLine) failed, but ignored");
        return;
      }
    }
  }

  if (!atCaret.GetChild()) {
    return;
  }

  // If caret is immediately after a block, set interline position to "right".
  // XXX Although I don't understand "interline position", if caret is
  //     immediately after non-editable contents, but previous editable
  //     content is a block, does this do right thing?
  if (nsIContent* const previousEditableContentInBlockAtCaret =
          HTMLEditUtils::GetPreviousSibling(
              *atCaret.GetChild(), {LeafNodeOption::IgnoreNonEditableNode},
              BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
    if (HTMLEditUtils::IsBlockElement(
            *previousEditableContentInBlockAtCaret,
            BlockInlineCheck::UseComputedDisplayStyle)) {
      DebugOnly<nsresult> rvIgnored = SelectionRef().SetInterlinePosition(
          InterlinePosition::StartOfNextLine);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                           "Selection::SetInterlinePosition(InterlinePosition::"
                           "StartOfNextLine) failed, but ignored");
      return;
    }
  }

  // If caret is immediately before a block, set interline position to "left".
  // XXX Although I don't understand "interline position", if caret is
  //     immediately before non-editable contents, but next editable
  //     content is a block, does this do right thing?
  if (nsIContent* const nextEditableContentInBlockAtCaret =
          HTMLEditUtils::GetNextSibling(
              *atCaret.GetChild(), {LeafNodeOption::IgnoreNonEditableNode},
              BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
    if (HTMLEditUtils::IsBlockElement(
            *nextEditableContentInBlockAtCaret,
            BlockInlineCheck::UseComputedDisplayStyle)) {
      DebugOnly<nsresult> rvIgnored =
          SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                           "Selection::SetInterlinePosition(InterlinePosition::"
                           "EndOfLine) failed, but ignored");
    }
  }
}

nsresult HTMLEditor::AdjustCaretPositionAndEnsurePaddingBRElement(
    nsIEditor::EDirection aDirectionAndAmount) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(SelectionRef().IsCollapsed());

  auto point = GetFirstSelectionStartPoint<EditorDOMPoint>();
  if (NS_WARN_IF(!point.IsInContentNode())) {
    return NS_ERROR_FAILURE;
  }

  // If selection start is not editable, climb up the tree until editable one.
  while (!EditorUtils::IsEditableContent(*point.ContainerAs<nsIContent>(),
                                         EditorType::HTML)) {
    point.Set(point.GetContainer());
    if (NS_WARN_IF(!point.IsInContentNode())) {
      return NS_ERROR_FAILURE;
    }
  }

  // If caret is in empty block element, we need to insert a `<br>` element
  // because the block should have one-line height.
  // XXX Even if only a part of the block is editable, shouldn't we put
  //     caret if the block element is now empty?
  if (Element* const editableBlockElement =
          HTMLEditUtils::GetInclusiveAncestorElement(
              *point.ContainerAs<nsIContent>(),
              HTMLEditUtils::ClosestEditableBlockElement,
              BlockInlineCheck::UseComputedDisplayStyle)) {
    if (editableBlockElement &&
        HTMLEditUtils::IsEmptyNode(
            *editableBlockElement,
            {EmptyCheckOption::TreatSingleBRElementAsVisible}) &&
        HTMLEditUtils::CanNodeContain(*point.GetContainer(), *nsGkAtoms::br)) {
      Element* bodyOrDocumentElement = GetRoot();
      if (NS_WARN_IF(!bodyOrDocumentElement)) {
        return NS_ERROR_FAILURE;
      }
      if (point.GetContainer() == bodyOrDocumentElement) {
        // Our root node is completely empty. Don't add a <br> here.
        // AfterEditInner() will add one for us when it calls
        // EditorBase::MaybeCreatePaddingBRElementForEmptyEditor().
        // XXX This kind of dependency between methods makes us spaghetti.
        //     Let's handle it here later.
        // XXX This looks odd check.  If active editing host is not a
        //     `<body>`, what are we doing?
        return NS_OK;
      }
      Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
          InsertPaddingBRElementForEmptyLastLineWithTransaction(point);
      if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction("
            ") failed");
        return insertPaddingBRElementResult.unwrapErr();
      }
      nsresult rv = insertPaddingBRElementResult.inspect().SuggestCaretPointTo(
          *this, {SuggestCaret::OnlyIfHasSuggestion,
                  SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
                  SuggestCaret::AndIgnoreTrivialError});
      if (NS_FAILED(rv)) {
        NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
        return rv;
      }
      NS_WARNING_ASSERTION(
          rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
          "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
      return NS_OK;
    }
  }

  // XXX Perhaps, we should do something if we're in a data node but not
  //     a text node.
  if (point.IsInTextNode()) {
    return NS_OK;
  }

  // Do we need to insert a padding <br> element for empty last line?  We do
  // if we are:
  // 1) prior node is in same block where selection is AND
  // 2) prior node is a br AND
  // 3) that br is not visible
  RefPtr<Element> editingHost = ComputeEditingHost();
  if (!editingHost) {
    return NS_OK;
  }

  if (nsCOMPtr<nsIContent> previousEditableContent =
          HTMLEditUtils::GetPreviousLeafContent(
              point, {LeafNodeOption::IgnoreNonEditableNode},
              BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
    // If caret and previous editable content are in same block element
    // (even if it's a non-editable element), we should put a padding <br>
    // element at end of the block.
    const Element* const blockElementContainingCaret =
        HTMLEditUtils::GetInclusiveAncestorElement(
            *point.ContainerAs<nsIContent>(),
            HTMLEditUtils::ClosestBlockElement,
            BlockInlineCheck::UseComputedDisplayStyle);
    const Element* const blockElementContainingPreviousEditableContent =
        HTMLEditUtils::GetAncestorElement(
            *previousEditableContent, HTMLEditUtils::ClosestBlockElement,
            BlockInlineCheck::UseComputedDisplayStyle);
    // If previous editable content of caret is in same block and a `<br>`
    // element, we need to adjust interline position.
    if (blockElementContainingCaret &&
        blockElementContainingCaret ==
            blockElementContainingPreviousEditableContent &&
        point.ContainerAs<nsIContent>()->GetEditingHost() ==
            previousEditableContent->GetEditingHost() &&
        previousEditableContent &&
        previousEditableContent->IsHTMLElement(nsGkAtoms::br)) {
      // If it's an invisible `<br>` element, we need to insert a padding
      // `<br>` element for making empty line have one-line height.
      if (HTMLEditUtils::IsBRElementFollowedByBlockBoundary(
              *previousEditableContent) &&
          !EditorUtils::IsPaddingBRElementForEmptyLastLine(
              *previousEditableContent)) {
        AutoEditorDOMPointChildInvalidator lockOffset(point);
        Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
            InsertPaddingBRElementForEmptyLastLineWithTransaction(point);
        if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
          NS_WARNING(
              "HTMLEditor::"
              "InsertPaddingBRElementForEmptyLastLineWithTransaction() failed");
          return insertPaddingBRElementResult.unwrapErr();
        }
        insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
        nsresult rv = CollapseSelectionTo(EditorRawDOMPoint(
            insertPaddingBRElementResult.inspect().GetNewNode(),
            InterlinePosition::StartOfNextLine));
        if (NS_FAILED(rv)) {
          NS_WARNING("EditorBase::CollapseSelectionTo() failed");
          return rv;
        }
      }
      // If it's a visible `<br>` element and next editable content is a
      // padding `<br>` element, we need to set interline position.
      else if (nsIContent* nextEditableContentInBlock =
                   HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
                       *previousEditableContent,
                       {LeafNodeOption::IgnoreNonEditableNode,
                        LeafNodeOption::TreatChildBlockAsLeafNode},
                       BlockInlineCheck::UseComputedDisplayStyle,
                       editingHost)) {
        if (EditorUtils::IsPaddingBRElementForEmptyLastLine(
                *nextEditableContentInBlock)) {
          // Make it stick to the padding `<br>` element so that it will be
          // on blank line.
          DebugOnly<nsresult> rvIgnored = SelectionRef().SetInterlinePosition(
              InterlinePosition::StartOfNextLine);
          NS_WARNING_ASSERTION(
              NS_SUCCEEDED(rvIgnored),
              "Selection::SetInterlinePosition(InterlinePosition::"
              "StartOfNextLine) failed, but ignored");
        }
      }
    }
  }

  // If previous editable content in same block is `<br>`, text node, `<img>`
  //  or `<hr>`, current caret position is fine.
  if (nsIContent* const previousEditableContentInBlock =
          HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
              point,
              {LeafNodeOption::IgnoreNonEditableNode,
               LeafNodeOption::TreatChildBlockAsLeafNode},
              BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
    if (previousEditableContentInBlock->IsHTMLElement(nsGkAtoms::br) ||
        previousEditableContentInBlock->IsText() ||
        HTMLEditUtils::IsImageElement(*previousEditableContentInBlock) ||
        previousEditableContentInBlock->IsHTMLElement(nsGkAtoms::hr)) {
      return NS_OK;
    }
  }

  // If next editable content in same block is `<br>`, text node, `<img>` or
  // `<hr>`, current caret position is fine.
  if (nsIContent* nextEditableContentInBlock =
          HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
              point,
              {LeafNodeOption::IgnoreNonEditableNode,
               LeafNodeOption::TreatChildBlockAsLeafNode},
              BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
    if (nextEditableContentInBlock->IsText() ||
        nextEditableContentInBlock->IsAnyOfHTMLElements(
            nsGkAtoms::br, nsGkAtoms::img, nsGkAtoms::hr)) {
      return NS_OK;
    }
  }

  // Otherwise, look for a near editable content towards edit action direction.

  // If there is no editable content, keep current caret position.
  // XXX Why do we treat `nsIEditor::ePreviousWord` etc as forward direction?
  nsIContent* nearEditableContent = HTMLEditUtils::GetAdjacentContentToPutCaret(
      point,
      aDirectionAndAmount == nsIEditor::ePrevious ? WalkTreeDirection::Backward
                                                  : WalkTreeDirection::Forward,
      *editingHost);
  if (!nearEditableContent) {
    return NS_OK;
  }

  EditorRawDOMPoint pointToPutCaret =
      HTMLEditUtils::GetGoodCaretPointFor<EditorRawDOMPoint>(
          *nearEditableContent, aDirectionAndAmount);
  if (!pointToPutCaret.IsSet()) {
    NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
    return NS_ERROR_FAILURE;
  }
  nsresult rv = CollapseSelectionTo(pointToPutCaret);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::CollapseSelectionTo() failed");
  return rv;
}

nsresult HTMLEditor::RemoveEmptyNodesIn(const EditorDOMRange& aRange) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aRange.IsPositioned());

  // Some general notes on the algorithm used here: the goal is to examine all
  // the nodes in aRange, and remove the empty ones.  We do this by
  // using a content iterator to traverse all the nodes in the range, and
  // placing the empty nodes into an array.  After finishing the iteration,
  // we delete the empty nodes in the array.  (They cannot be deleted as we
  // find them because that would invalidate the iterator.)
  //
  // Since checking to see if a node is empty can be costly for nodes with
  // many descendants, there are some optimizations made.  I rely on the fact
  // that the iterator is post-order: it will visit children of a node before
  // visiting the parent node.  So if I find that a child node is not empty, I
  // know that its parent is not empty without even checking.  So I put the
  // parent on a "skipList" which is just a voidArray of nodes I can skip the
  // empty check on.  If I encounter a node on the skiplist, i skip the
  // processing for that node and replace its slot in the skiplist with that
  // node's parent.
  //
  // An interesting idea is to go ahead and regard parent nodes that are NOT
  // on the skiplist as being empty (without even doing the IsEmptyNode check)
  // on the theory that if they weren't empty, we would have encountered a
  // non-empty child earlier and thus put this parent node on the skiplist.
  //
  // Unfortunately I can't use that strategy here, because the range may
  // include some children of a node while excluding others.  Thus I could
  // find all the _examined_ children empty, but still not have an empty
  // parent.

  const RawRangeBoundary endOfRange = [&]() {
    // If the range is not collapsed and end of the range is start of a
    // container, it means that the inclusive ancestor empty element may be
    // created by splitting the left nodes.
    if (aRange.Collapsed() || !aRange.IsInContentNodes() ||
        !aRange.EndRef().IsStartOfContainer()) {
      return aRange.EndRef().ToRawRangeBoundary();
    }
    nsINode* const commonAncestor =
        nsContentUtils::GetClosestCommonInclusiveAncestor(
            aRange.StartRef().ContainerAs<nsIContent>(),
            aRange.EndRef().ContainerAs<nsIContent>());
    if (!commonAncestor) {
      return aRange.EndRef().ToRawRangeBoundary();
    }
    nsIContent* maybeRightContent = nullptr;
    for (nsIContent* content : aRange.EndRef()
                                   .ContainerAs<nsIContent>()
                                   ->InclusiveAncestorsOfType<nsIContent>()) {
      if (!HTMLEditUtils::IsSimplyEditableNode(*content) ||
          content == commonAncestor) {
        break;
      }
      if (aRange.StartRef().ContainerAs<nsIContent>() == content) {
        break;
      }
      EmptyCheckOptions options = {
          EmptyCheckOption::TreatListItemAsVisible,
          EmptyCheckOption::TreatTableCellAsVisible,
          EmptyCheckOption::TreatNonEditableContentAsInvisible};
      if (!HTMLEditUtils::IsBlockElement(
              *content, BlockInlineCheck::UseComputedDisplayStyle)) {
        options += EmptyCheckOption::TreatSingleBRElementAsVisible;
      }
      if (!HTMLEditUtils::IsEmptyNode(*content, options)) {
        break;
      }
      maybeRightContent = content;
    }
    if (!maybeRightContent) {
      return aRange.EndRef().ToRawRangeBoundary();
    }
    return EditorRawDOMPoint::After(*maybeRightContent).ToRawRangeBoundary();
  }();

  PostContentIterator postOrderIter;
  nsresult rv =
      postOrderIter.Init(aRange.StartRef().ToRawRangeBoundary(), endOfRange);
  if (NS_FAILED(rv)) {
    NS_WARNING("PostContentIterator::Init() failed");
    return rv;
  }

  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfEmptyContents,
      arrayOfEmptyCites;

  // Collect empty nodes first.
  {
    const bool isMailEditor = IsMailEditor();
    AutoTArray<OwningNonNull<nsIContent>, 64> knownNonEmptyContents;
    Maybe<AutoClonedSelectionRangeArray> maybeSelectionRanges;
    for (; !postOrderIter.IsDone(); postOrderIter.Next()) {
      MOZ_ASSERT(postOrderIter.GetCurrentNode()->IsContent());

      nsIContent* content = postOrderIter.GetCurrentNode()->AsContent();
      nsIContent* parentContent = content->GetParent();

      size_t idx = knownNonEmptyContents.IndexOf(content);
      if (idx != decltype(knownNonEmptyContents)::NoIndex) {
        // This node is on our skip list.  Skip processing for this node, and
        // replace its value in the skip list with the value of its parent
        if (parentContent) {
          knownNonEmptyContents[idx] = parentContent;
        }
        continue;
      }

      const bool isEmptyNode = [&]() {
        if (!content->IsElement()) {
          return false;
        }
        Element& element = *content->AsElement();
        const bool isMailCite =
            isMailEditor && HTMLEditUtils::IsMailCiteElement(element);
        const bool isCandidate = [&]() {
          if (element.IsHTMLElement(nsGkAtoms::body)) {
            // Don't delete the body
            return false;
          }
          if (isMailCite || element.IsHTMLElement(nsGkAtoms::a) ||
              HTMLEditUtils::IsInlineStyleElement(element) ||
              HTMLEditUtils::IsListElement(element) ||
              element.IsHTMLElement(nsGkAtoms::div)) {
            // Only consider certain nodes to be empty for purposes of removal
            return true;
          }
          if (HTMLEditUtils::IsFormatElementForFormatBlockCommand(element) ||
              HTMLEditUtils::IsListItemElement(element) ||
              element.IsHTMLElement(nsGkAtoms::blockquote)) {
            // These node types are candidates if selection is not in them.  If
            // it is one of these, don't delete if selection inside.  This is so
            // we can create empty headings, etc., for the user to type into.
            if (maybeSelectionRanges.isNothing()) {
              maybeSelectionRanges.emplace(SelectionRef());
            }
            return !maybeSelectionRanges
                        ->IsAtLeastOneContainerOfRangeBoundariesInclusiveDescendantOf(
                            element);
          }
          return false;
        }();

        if (!isCandidate) {
          return false;
        }

        // We delete mailcites even if they have a solo br in them.  Other
        // nodes we require to be empty.
        HTMLEditUtils::EmptyCheckOptions options{
            EmptyCheckOption::TreatListItemAsVisible,
            EmptyCheckOption::TreatTableCellAsVisible};
        if (!isMailCite) {
          options += EmptyCheckOption::TreatSingleBRElementAsVisible;
        } else {
          // XXX Maybe unnecessary to specify this.
          options += EmptyCheckOption::TreatNonEditableContentAsInvisible;
        }
        if (!HTMLEditUtils::IsEmptyNode(*content, options)) {
          return false;
        }

        if (isMailCite) {
          // mailcites go on a separate list from other empty nodes
          arrayOfEmptyCites.AppendElement(*content);
        }
        // Don't delete non-editable nodes in this method because this is a
        // clean up method to remove unnecessary nodes of the result of
        // editing.  So, we shouldn't delete non-editable nodes which were
        // there before editing.  Additionally, if the element is some special
        // elements such as <body>, we shouldn't delete it.
        else if (HTMLEditUtils::IsSimplyEditableNode(*content) &&
                 HTMLEditUtils::IsRemovableNode(*content)) {
          arrayOfEmptyContents.AppendElement(*content);
        }
        return true;
      }();
      if (!isEmptyNode && parentContent) {
        knownNonEmptyContents.AppendElement(*parentContent);
      }
    }  // end of the for-loop iterating with postOrderIter
  }

  // now delete the empty nodes
  for (OwningNonNull<nsIContent>& emptyContent : arrayOfEmptyContents) {
    // MOZ_KnownLive due to bug 1622253
    nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(emptyContent));
    if (NS_FAILED(rv)) {
      NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
      return rv;
    }
  }

  // Now delete the empty mailcites.  This is a separate step because we want
  // to pull out any br's and preserve them.
  EditorDOMPoint pointToPutCaret;
  for (OwningNonNull<nsIContent>& emptyCite : arrayOfEmptyCites) {
    if (!HTMLEditUtils::IsEmptyNode(
            emptyCite,
            {EmptyCheckOption::TreatSingleBRElementAsVisible,
             EmptyCheckOption::TreatListItemAsVisible,
             EmptyCheckOption::TreatTableCellAsVisible,
             EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
      // We are deleting a cite that has just a `<br>`.  We want to delete cite,
      // but preserve `<br>`.
      Result<CreateLineBreakResult, nsresult> insertBRElementResultOrError =
          InsertLineBreak(WithTransaction::Yes, LineBreakType::BRElement,
                          EditorDOMPoint(emptyCite));
      if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) {
        NS_WARNING(
            "HTMLEditor::InsertLineBreak(WithTransaction::Yes, "
            "LineBreakType::BRElement) failed");
        return insertBRElementResultOrError.unwrapErr();
      }
      CreateLineBreakResult insertBRElementResult =
          insertBRElementResultOrError.unwrap();
      MOZ_ASSERT(insertBRElementResult.Handled());
      // XXX Is this intentional selection change?
      insertBRElementResult.MoveCaretPointTo(
          pointToPutCaret, *this,
          {SuggestCaret::OnlyIfHasSuggestion,
           SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
    }
    // MOZ_KnownLive because 'arrayOfEmptyCites' is guaranteed to keep it alive.
    nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(emptyCite));
    if (NS_FAILED(rv)) {
      NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
      return rv;
    }
  }
  // XXX Is this intentional selection change?
  if (pointToPutCaret.IsSet()) {
    nsresult rv = CollapseSelectionTo(pointToPutCaret);
    if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
      NS_WARNING(
          "EditorBase::CollapseSelectionTo() caused destroying the editor");
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "EditorBase::CollapseSelectionTo() failed, but ignored");
  }

  return NS_OK;
}

nsresult HTMLEditor::LiftUpListItemElement(
    Element& aListItemElement,
    LiftUpFromAllParentListElements aLiftUpFromAllParentListElements) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  if (!HTMLEditUtils::IsListItemElement(aListItemElement)) {
    return NS_ERROR_INVALID_ARG;
  }

  if (NS_WARN_IF(!aListItemElement.GetParentElement()) ||
      NS_WARN_IF(!aListItemElement.GetParentElement()->GetParentNode())) {
    return NS_ERROR_FAILURE;
  }

  // if it's first or last list item, don't need to split the list
  // otherwise we do.
  const bool isFirstListItem = HTMLEditUtils::IsFirstChild(
      aListItemElement, {LeafNodeOption::IgnoreNonEditableNode},
      BlockInlineCheck::UseComputedDisplayOutsideStyle);
  const bool isLastListItem = HTMLEditUtils::IsLastChild(
      aListItemElement, {LeafNodeOption::IgnoreNonEditableNode},
      BlockInlineCheck::UseComputedDisplayOutsideStyle);

  Element* leftListElement = aListItemElement.GetParentElement();
  if (NS_WARN_IF(!leftListElement)) {
    return NS_ERROR_FAILURE;
  }

  // If it's at middle of parent list element, split the parent list element.
  // Then, aListItem becomes the first list item of the right list element.
  if (!isFirstListItem && !isLastListItem) {
    EditorDOMPoint atListItemElement(&aListItemElement);
    if (NS_WARN_IF(!atListItemElement.IsSet())) {
      return NS_ERROR_FAILURE;
    }
    MOZ_ASSERT(atListItemElement.IsSetAndValid());
    Result<SplitNodeResult, nsresult> splitListItemParentResult =
        SplitNodeWithTransaction(atListItemElement);
    if (MOZ_UNLIKELY(splitListItemParentResult.isErr())) {
      NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
      return splitListItemParentResult.unwrapErr();
    }
    nsresult rv = splitListItemParentResult.inspect().SuggestCaretPointTo(
        *this, {SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
    if (NS_FAILED(rv)) {
      NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
      return rv;
    }

    leftListElement =
        splitListItemParentResult.inspect().GetPreviousContentAs<Element>();
    if (MOZ_UNLIKELY(!leftListElement)) {
      NS_WARNING(
          "HTMLEditor::SplitNodeWithTransaction() didn't return left list "
          "element");
      return NS_ERROR_FAILURE;
    }
  }

  // In most cases, insert the list item into the new left list node..
  EditorDOMPoint pointToInsertListItem(leftListElement);
  if (NS_WARN_IF(!pointToInsertListItem.IsInContentNode())) {
    return NS_ERROR_FAILURE;
  }

  // But when the list item was the first child of the right list, it should
  // be inserted between the both list elements.  This allows user to hit
  // Enter twice at a list item breaks the parent list node.
  if (!isFirstListItem) {
    DebugOnly<bool> advanced = pointToInsertListItem.AdvanceOffset();
    NS_WARNING_ASSERTION(advanced,
                         "Failed to advance offset to right list node");
  }

  EditorDOMPoint pointToPutCaret;
  {
    Result<MoveNodeResult, nsresult> moveListItemElementResult =
        MoveNodeWithTransaction(aListItemElement, pointToInsertListItem);
    if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
      NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
      return moveListItemElementResult.unwrapErr();
    }
    MoveNodeResult unwrappedMoveListItemElementResult =
        moveListItemElementResult.unwrap();
    unwrappedMoveListItemElementResult.MoveCaretPointTo(
        pointToPutCaret, *this,
        {SuggestCaret::OnlyIfHasSuggestion,
         SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
  }

  // Unwrap list item contents if they are no longer in a list
  // XXX If the parent list element is a child of another list element
  //     (although invalid tree), the list item element won't be unwrapped.
  //     That makes the parent ancestor element tree valid, but might be
  //     unexpected result.
  // XXX If aListItemElement is <dl> or <dd> and current parent is <ul> or <ol>,
  //     the list items won't be unwrapped.  If aListItemElement is <li> and its
  //     current parent is <dl>, there is same issue.
  if (!HTMLEditUtils::IsListElement(
          *pointToInsertListItem.ContainerAs<nsIContent>()) &&
      HTMLEditUtils::IsListItemElement(aListItemElement)) {
    Result<EditorDOMPoint, nsresult> unwrapOrphanListItemElementResult =
        RemoveBlockContainerWithTransaction(aListItemElement);
    if (MOZ_UNLIKELY(unwrapOrphanListItemElementResult.isErr())) {
      NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
      return unwrapOrphanListItemElementResult.unwrapErr();
    }
    if (AllowsTransactionsToChangeSelection() &&
        unwrapOrphanListItemElementResult.inspect().IsSet()) {
      pointToPutCaret = unwrapOrphanListItemElementResult.unwrap();
    }
    if (!pointToPutCaret.IsSet()) {
      return NS_OK;
    }
    nsresult rv = CollapseSelectionTo(pointToPutCaret);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "EditorBase::CollapseSelectionTo() failed");
    return rv;
  }

  if (pointToPutCaret.IsSet()) {
    nsresult rv = CollapseSelectionTo(pointToPutCaret);
    if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
      NS_WARNING("EditorBase::CollapseSelectionTo() failed");
      return rv;
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "EditorBase::CollapseSelectionTo() failed, but ignored");
  }

  if (aLiftUpFromAllParentListElements == LiftUpFromAllParentListElements::No) {
    return NS_OK;
  }
  // XXX If aListItemElement is moved to unexpected element by mutation event
  //     listener, shouldn't we stop calling this?
  nsresult rv = LiftUpListItemElement(aListItemElement,
                                      LiftUpFromAllParentListElements::Yes);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::LiftUpListItemElement("
                       "LiftUpFromAllParentListElements::Yes) failed");
  return rv;
}

nsresult HTMLEditor::DestroyListStructureRecursively(Element& aListElement) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(HTMLEditUtils::IsListElement(aListElement));

  // XXX If mutation event listener inserts new child into `aListElement`,
  //     this becomes infinite loop so that we should set limit of the
  //     loop count from original child count.
  while (aListElement.GetFirstChild()) {
    const OwningNonNull<nsIContent> child = *aListElement.GetFirstChild();

    if (HTMLEditUtils::IsListItemElement(*child)) {
      // XXX Using LiftUpListItemElement() is too expensive for this purpose.
      //     Looks like the reason why this method uses it is, only this loop
      //     wants to work with first child of aListElement.  However, what it
      //     actually does is removing <li> as container.  Perhaps, we should
      //     decide destination first, and then, move contents in `child`.
      // XXX If aListElement is is a child of another list element (although
      //     it's invalid tree), this moves the list item to outside of
      //     aListElement's parent.  Is that really intentional behavior?
      nsresult rv = LiftUpListItemElement(
          MOZ_KnownLive(*child->AsElement()),
          HTMLEditor::LiftUpFromAllParentListElements::Yes);
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:"
            ":Yes) failed");
        return rv;
      }
      continue;
    }

    if (HTMLEditUtils::IsListElement(*child)) {
      nsresult rv =
          DestroyListStructureRecursively(MOZ_KnownLive(*child->AsElement()));
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DestroyListStructureRecursively() failed");
        return rv;
      }
      continue;
    }

    // Delete any non-list items for now
    // XXX This is not HTML5 aware.  HTML5 allows all list elements to have
    //     <script> and <template> and <dl> element to have <div> to group
    //     some <dt> and <dd> elements.  So, this may break valid children.
    nsresult rv = DeleteNodeWithTransaction(*child);
    if (NS_FAILED(rv)) {
      NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
      return rv;
    }
  }

  // Delete the now-empty list
  const Result<EditorDOMPoint, nsresult> unwrapListElementResult =
      RemoveBlockContainerWithTransaction(aListElement);
  if (MOZ_UNLIKELY(unwrapListElementResult.isErr())) {
    NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
    return unwrapListElementResult.inspectErr();
  }
  const EditorDOMPoint& pointToPutCaret = unwrapListElementResult.inspect();
  if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
    return NS_OK;
  }
  nsresult rv = CollapseSelectionTo(pointToPutCaret);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::CollapseSelectionTo() failed");
  return rv;
}

nsresult HTMLEditor::EnsureSelectionInBodyOrDocumentElement() {
  MOZ_ASSERT(IsEditActionDataAvailable());

  RefPtr<Element> bodyOrDocumentElement = GetRoot();
  if (NS_WARN_IF(!bodyOrDocumentElement)) {
    return NS_ERROR_FAILURE;
  }

  const auto atCaret = GetFirstSelectionStartPoint<EditorRawDOMPoint>();
  if (NS_WARN_IF(!atCaret.IsSet())) {
    return NS_ERROR_FAILURE;
  }

  // XXX This does wrong things.  Web apps can put any elements as sibling
  //     of `<body>` element.  Therefore, this collapses `Selection` into
  //     the `<body>` element which `HTMLDocument.body` is set to.  So,
  //     this makes users impossible to modify content outside of the
  //     `<body>` element even if caret is in an editing host.

  // Check that selection start container is inside the <body> element.
  // XXXsmaug this code is insane.
  nsINode* temp = atCaret.GetContainer();
  while (temp && !temp->IsHTMLElement(nsGkAtoms::body)) {
    temp = temp->GetParentOrShadowHostNode();
  }

  // If we aren't in the <body> element, force the issue.
  if (!temp) {
    nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement);
    if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
      NS_WARNING(
          "EditorBase::CollapseSelectionToStartOf() caused destroying the "
          "editor");
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
    return NS_OK;
  }

  const auto selectionEndPoint = GetFirstSelectionEndPoint<EditorRawDOMPoint>();
  if (NS_WARN_IF(!selectionEndPoint.IsSet())) {
    return NS_ERROR_FAILURE;
  }

  // check that selNode is inside body
  // XXXsmaug this code is insane.
  temp = selectionEndPoint.GetContainer();
  while (temp && !temp->IsHTMLElement(nsGkAtoms::body)) {
    temp = temp->GetParentOrShadowHostNode();
  }

  // If we aren't in the <body> element, force the issue.
  if (!temp) {
    nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement);
    if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
      NS_WARNING(
          "EditorBase::CollapseSelectionToStartOf() caused destroying the "
          "editor");
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
  }

  return NS_OK;
}

Result<CreateLineBreakResult, nsresult>
HTMLEditor::InsertPaddingBRElementIfInEmptyBlock(
    const EditorDOMPoint& aPoint,
    nsIEditor::EStripWrappers aDeleteEmptyInlines) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  if (MOZ_UNLIKELY(!aPoint.IsInContentNode())) {
    return CreateLineBreakResult::NotHandled();
  }

  const RefPtr<Element> editableBlockElement =
      HTMLEditUtils::GetInclusiveAncestorElement(
          *aPoint.ContainerAs<nsIContent>(),
          HTMLEditUtils::ClosestEditableBlockElement,
          BlockInlineCheck::UseComputedDisplayStyle);

  if (!editableBlockElement ||
      !HTMLEditUtils::IsEmptyNode(
          *editableBlockElement,
          {EmptyCheckOption::TreatSingleBRElementAsVisible,
           EmptyCheckOption::TreatBlockAsVisible})) {
    return CreateLineBreakResult::NotHandled();
  }

  EditorDOMPoint pointToInsertLineBreak;
  if (aDeleteEmptyInlines == nsIEditor::eStrip &&
      aPoint.ContainerAs<nsIContent>() != editableBlockElement) {
    nsCOMPtr<nsIContent> emptyInlineAncestor =
        HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
            *aPoint.ContainerAs<nsIContent>(),
            BlockInlineCheck::UseComputedDisplayStyle);
    if (!emptyInlineAncestor) {
      emptyInlineAncestor = aPoint.ContainerAs<nsIContent>();
    }
    nsresult rv = DeleteNodeWithTransaction(*emptyInlineAncestor);
    if (NS_FAILED(rv)) {
      NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
      return Err(rv);
    }
    pointToInsertLineBreak = EditorDOMPoint(editableBlockElement, 0u);
  } else {
    pointToInsertLineBreak = aPoint;
  }

  // TODO: Use InsertLineBreak instead even if we're inserting a <br>.
  Result<CreateElementResult, nsresult> insertPaddingLineBreakResultOrError =
      InsertPaddingBRElementForEmptyLastLineWithTransaction(
          pointToInsertLineBreak);
  if (MOZ_UNLIKELY(insertPaddingLineBreakResultOrError.isErr())) {
    NS_WARNING(
        "EditorBase::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
        "failed");
    return insertPaddingLineBreakResultOrError.propagateErr();
  }
  CreateElementResult insertPaddingLineBreakResult =
      insertPaddingLineBreakResultOrError.unwrap();
  RefPtr<HTMLBRElement> paddingBRElement =
      HTMLBRElement::FromNodeOrNull(insertPaddingLineBreakResult.GetNewNode());
  if (NS_WARN_IF(!paddingBRElement)) {
    return Err(NS_ERROR_FAILURE);
  }
  if (NS_WARN_IF(!paddingBRElement->IsInComposedDoc())) {
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }
  insertPaddingLineBreakResult.IgnoreCaretPointSuggestion();

  EditorDOMPoint editorDOMPoint{paddingBRElement};
  EditorLineBreak editorLineBreak{std::move(paddingBRElement)};
  return CreateLineBreakResult{std::move(editorLineBreak),
                               std::move(editorDOMPoint)};
}

Result<CreateLineBreakResult, nsresult>
HTMLEditor::InsertPaddingBRElementIfNeeded(
    const EditorDOMPoint& aPoint, nsIEditor::EStripWrappers aDeleteEmptyInlines,
    const Element& aEditingHost) {
  MOZ_ASSERT(aPoint.IsInContentNode());
  MOZ_ASSERT(HTMLEditUtils::NodeIsEditableOrNotInComposedDoc(
      *aPoint.ContainerAs<nsIContent>()));

  auto pointToInsertPaddingBR = [&]() MOZ_NEVER_INLINE_DEBUG -> EditorDOMPoint {
    // If the point is immediately before a block boundary which is for a
    // mailcite in plaintext mail composer (it is a <span> styled as block), we
    // should not treat it as a block because it's required by the serializer to
    // give the mailcite contents are not appear with outer content in the same
    // lines.
    if (IsPlaintextMailComposer()) {
      const WSScanResult nextVisibleThing =
          WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
              {WSRunScanner::Option::OnlyEditableNodes}, aPoint);
      if (nextVisibleThing.ReachedBlockBoundary() &&
          HTMLEditUtils::IsMailCiteElement(*nextVisibleThing.ElementPtr()) &&
          HTMLEditUtils::IsInlineContent(
              *nextVisibleThing.ElementPtr(),
              BlockInlineCheck::UseHTMLDefaultStyle)) {
        return nextVisibleThing.ReachedCurrentBlockBoundary()
                   ? EditorDOMPoint::AtEndOf(*nextVisibleThing.ElementPtr())
                   : EditorDOMPoint(nextVisibleThing.ElementPtr());
      }
    }
    return HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible(aPoint,
                                                                  aEditingHost);
  }();
  if (!pointToInsertPaddingBR.IsSet()) {
    return CreateLineBreakResult::NotHandled();
  }
  if (aDeleteEmptyInlines == nsIEditor::eStrip &&
      pointToInsertPaddingBR.IsContainerElement() &&
      HTMLEditUtils::IsEmptyInlineContainer(
          *pointToInsertPaddingBR.ContainerAs<Element>(),
          {EmptyCheckOption::TreatSingleBRElementAsVisible,
           EmptyCheckOption::TreatBlockAsVisible,
           EmptyCheckOption::TreatListItemAsVisible,
           EmptyCheckOption::TreatTableCellAsVisible},
          BlockInlineCheck::UseComputedDisplayStyle)) {
    RefPtr<Element> emptyInlineAncestor =
        HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(
            *pointToInsertPaddingBR.ContainerAs<nsIContent>(),
            BlockInlineCheck::UseComputedDisplayStyle);
    if (!emptyInlineAncestor) {
      emptyInlineAncestor = pointToInsertPaddingBR.ContainerAs<Element>();
    }
    AutoTrackDOMPoint trackPointToInsertPaddingBR(RangeUpdaterRef(),
                                                  &pointToInsertPaddingBR);
    nsresult rv = DeleteNodeWithTransaction(*emptyInlineAncestor);
    if (NS_FAILED(rv)) {
      NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
      return Err(rv);
    }
  }

  // Padding <br> elements may appear and disappear a lot even during IME has a
  // composition.  Therefore, IME may be confused with the mutation if we use
  // normal <br> element since it does not match with expectation of IME.  For
  // hiding the mutations from IME, we need to set the new <br> element flag to
  // NS_PADDING_FOR_EMPTY_LAST_LINE.
  Result<CreateElementResult, nsresult> insertPaddingBRResultOrError =
      InsertBRElement(WithTransaction::Yes,
                      BRElementType::PaddingForEmptyLastLine,
                      pointToInsertPaddingBR);
  if (MOZ_UNLIKELY(insertPaddingBRResultOrError.isErr())) {
    NS_WARNING(
        "EditorBase::InsertBRElement(WithTransaction::Yes, "
        "BRElementType::PaddingForEmptyLastLine) failed");
    return insertPaddingBRResultOrError.propagateErr();
  }
  return CreateLineBreakResult(insertPaddingBRResultOrError.unwrap());
}

Result<EditorDOMPoint, nsresult> HTMLEditor::RemoveAlignFromDescendants(
    Element& aElement, const nsAString& aAlignType, EditTarget aEditTarget) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(!aElement.IsHTMLElement(nsGkAtoms::table));

  const bool useCSS = IsCSSEnabled();

  EditorDOMPoint pointToPutCaret;

  // Let's remove all alignment hints in the children of aNode; it can
  // be an ALIGN attribute (in case we just remove it) or a CENTER
  // element (here we have to remove the container and keep its
  // children). We break on tables and don't look at their children.
  nsCOMPtr<nsIContent> nextSibling;
  for (nsIContent* content =
           aEditTarget == EditTarget::NodeAndDescendantsExceptTable
               ? &aElement
               : aElement.GetFirstChild();
       content; content = nextSibling) {
    // Get the next sibling before removing content from the DOM tree.
    // XXX If next sibling is removed from the parent and/or inserted to
    //     different parent, we will behave unexpectedly.  I think that
    //     we should create child list and handle it with checking whether
    //     it's still a child of expected parent.
    nextSibling = aEditTarget == EditTarget::NodeAndDescendantsExceptTable
                      ? nullptr
                      : content->GetNextSibling();

    if (content->IsHTMLElement(nsGkAtoms::center)) {
      OwningNonNull<Element> centerElement = *content->AsElement();
      {
        Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
            RemoveAlignFromDescendants(centerElement, aAlignType,
                                       EditTarget::OnlyDescendantsExceptTable);
        if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
          NS_WARNING(
              "HTMLEditor::RemoveAlignFromDescendants(EditTarget::"
              "OnlyDescendantsExceptTable) failed");
          return pointToPutCaretOrError;
        }
        if (pointToPutCaretOrError.inspect().IsSet()) {
          pointToPutCaret = pointToPutCaretOrError.unwrap();
        }
      }

      // We may have to insert a `<br>` element before first child of the
      // `<center>` element because it should be first element of a hard line
      // even after removing the `<center>` element.
      {
        Result<CreateElementResult, nsresult>
            maybeInsertBRElementBeforeFirstChildResult =
                EnsureHardLineBeginsWithFirstChildOf(centerElement);
        if (MOZ_UNLIKELY(maybeInsertBRElementBeforeFirstChildResult.isErr())) {
          NS_WARNING(
              "HTMLEditor::EnsureHardLineBeginsWithFirstChildOf() failed");
          return maybeInsertBRElementBeforeFirstChildResult.propagateErr();
        }
        CreateElementResult unwrappedResult =
            maybeInsertBRElementBeforeFirstChildResult.unwrap();
        if (unwrappedResult.HasCaretPointSuggestion()) {
          pointToPutCaret = unwrappedResult.UnwrapCaretPoint();
        }
      }

      // We may have to insert a `<br>` element after last child of the
      // `<center>` element because it should be last element of a hard line
      // even after removing the `<center>` element.
      {
        Result<CreateElementResult, nsresult>
            maybeInsertBRElementAfterLastChildResult =
                EnsureHardLineEndsWithLastChildOf(centerElement);
        if (MOZ_UNLIKELY(maybeInsertBRElementAfterLastChildResult.isErr())) {
          NS_WARNING("HTMLEditor::EnsureHardLineEndsWithLastChildOf() failed");
          return maybeInsertBRElementAfterLastChildResult.propagateErr();
        }
        CreateElementResult unwrappedResult =
            maybeInsertBRElementAfterLastChildResult.unwrap();
        if (unwrappedResult.HasCaretPointSuggestion()) {
          pointToPutCaret = unwrappedResult.UnwrapCaretPoint();
        }
      }

      {
        Result<EditorDOMPoint, nsresult> unwrapCenterElementResult =
            RemoveContainerWithTransaction(centerElement);
        if (MOZ_UNLIKELY(unwrapCenterElementResult.isErr())) {
          NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
          return unwrapCenterElementResult;
        }
        if (unwrapCenterElementResult.inspect().IsSet()) {
          pointToPutCaret = unwrapCenterElementResult.unwrap();
        }
      }
      continue;
    }

    if (!HTMLEditUtils::IsBlockElement(*content,
                                       BlockInlineCheck::UseHTMLDefaultStyle) &&
        !content->IsHTMLElement(nsGkAtoms::hr)) {
      continue;
    }

    const OwningNonNull<Element> blockOrHRElement = *content->AsElement();
    if (HTMLEditUtils::IsAlignAttrSupported(blockOrHRElement)) {
      nsresult rv =
          RemoveAttributeWithTransaction(blockOrHRElement, *nsGkAtoms::align);
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::align) "
            "failed");
        return Err(rv);
      }
    }
    if (useCSS) {
      if (blockOrHRElement->IsAnyOfHTMLElements(nsGkAtoms::table,
                                                nsGkAtoms::hr)) {
        nsresult rv = SetAttributeOrEquivalent(
            blockOrHRElement, nsGkAtoms::align, aAlignType, false);
        if (NS_WARN_IF(Destroyed())) {
          return Err(NS_ERROR_EDITOR_DESTROYED);
        }
        if (NS_FAILED(rv)) {
          NS_WARNING(
              "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align) failed");
          return Err(rv);
        }
      } else {
        nsStyledElement* styledBlockOrHRElement =
            nsStyledElement::FromNode(blockOrHRElement);
        if (NS_WARN_IF(!styledBlockOrHRElement)) {
          return Err(NS_ERROR_FAILURE);
        }
        // MOZ_KnownLive(*styledBlockOrHRElement): It's `blockOrHRElement
        // which is OwningNonNull.
        nsAutoString dummyCssValue;
        Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
            CSSEditUtils::RemoveCSSInlineStyleWithTransaction(
                *this, MOZ_KnownLive(*styledBlockOrHRElement),
                nsGkAtoms::textAlign, dummyCssValue);
        if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
          NS_WARNING(
              "CSSEditUtils::RemoveCSSInlineStyleWithTransaction(nsGkAtoms::"
              "textAlign) failed");
          return pointToPutCaretOrError;
        }
        if (pointToPutCaretOrError.inspect().IsSet()) {
          pointToPutCaret = pointToPutCaretOrError.unwrap();
        }
      }
    }
    if (!blockOrHRElement->IsHTMLElement(nsGkAtoms::table)) {
      // unless this is a table, look at children
      Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
          RemoveAlignFromDescendants(blockOrHRElement, aAlignType,
                                     EditTarget::OnlyDescendantsExceptTable);
      if (pointToPutCaretOrError.isErr()) {
        NS_WARNING(
            "HTMLEditor::RemoveAlignFromDescendants(EditTarget::"
            "OnlyDescendantsExceptTable) failed");
        return pointToPutCaretOrError;
      }
      if (pointToPutCaretOrError.inspect().IsSet()) {
        pointToPutCaret = pointToPutCaretOrError.unwrap();
      }
    }
  }
  return pointToPutCaret;
}

Result<CreateElementResult, nsresult>
HTMLEditor::EnsureHardLineBeginsWithFirstChildOf(
    Element& aRemovingContainerElement) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  nsIContent* const firstEditableChild = HTMLEditUtils::GetFirstChild(
      aRemovingContainerElement, {LeafNodeOption::IgnoreNonEditableNode},
      BlockInlineCheck::UseComputedDisplayOutsideStyle);
  if (!firstEditableChild) {
    return CreateElementResult::NotHandled();
  }

  if (HTMLEditUtils::IsBlockElement(
          *firstEditableChild, BlockInlineCheck::UseComputedDisplayStyle) ||
      firstEditableChild->IsHTMLElement(nsGkAtoms::br)) {
    return CreateElementResult::NotHandled();
  }

  nsIContent* const previousEditableContent = HTMLEditUtils::GetPreviousSibling(
      aRemovingContainerElement, {LeafNodeOption::IgnoreNonEditableNode},
      BlockInlineCheck::UseComputedDisplayOutsideStyle);
  if (!previousEditableContent) {
    return CreateElementResult::NotHandled();
  }

  if (HTMLEditUtils::IsBlockElement(
          *previousEditableContent,
          BlockInlineCheck::UseComputedDisplayStyle) ||
      previousEditableContent->IsHTMLElement(nsGkAtoms::br)) {
    return CreateElementResult::NotHandled();
  }

  Result<CreateLineBreakResult, nsresult> insertBRElementResultOrError =
      InsertLineBreak(WithTransaction::Yes, LineBreakType::BRElement,
                      EditorDOMPoint(&aRemovingContainerElement, 0u));
  if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) {
    NS_WARNING(
        "HTMLEditor::InsertLineBreak(WithTransaction::Yes, "
        "LineBreakType::BRElement) failed");
    return insertBRElementResultOrError.propagateErr();
  }
  CreateLineBreakResult insertBRElementResult =
      insertBRElementResultOrError.unwrap();
  return CreateElementResult(insertBRElementResult->BRElementRef(),
                             insertBRElementResult.UnwrapCaretPoint());
}

Result<CreateElementResult, nsresult>
HTMLEditor::EnsureHardLineEndsWithLastChildOf(
    Element& aRemovingContainerElement) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  nsIContent* const firstEditableContent = HTMLEditUtils::GetLastChild(
      aRemovingContainerElement, {LeafNodeOption::IgnoreNonEditableNode},
      BlockInlineCheck::UseComputedDisplayOutsideStyle);
  if (!firstEditableContent) {
    return CreateElementResult::NotHandled();
  }

  if (HTMLEditUtils::IsBlockElement(
          *firstEditableContent, BlockInlineCheck::UseComputedDisplayStyle) ||
      firstEditableContent->IsHTMLElement(nsGkAtoms::br)) {
    return CreateElementResult::NotHandled();
  }

  nsIContent* const nextEditableContent = HTMLEditUtils::GetPreviousSibling(
      aRemovingContainerElement, {LeafNodeOption::IgnoreNonEditableNode},
      BlockInlineCheck::UseComputedDisplayOutsideStyle);
  if (!nextEditableContent) {
    return CreateElementResult::NotHandled();
  }

  if (HTMLEditUtils::IsBlockElement(
          *nextEditableContent, BlockInlineCheck::UseComputedDisplayStyle) ||
      nextEditableContent->IsHTMLElement(nsGkAtoms::br)) {
    return CreateElementResult::NotHandled();
  }

  Result<CreateLineBreakResult, nsresult> insertBRElementResultOrError =
      InsertLineBreak(WithTransaction::Yes, LineBreakType::BRElement,
                      EditorDOMPoint::AtEndOf(aRemovingContainerElement));
  if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) {
    NS_WARNING(
        "HTMLEditor::InsertLineBreak(WithTransaction::Yes, "
        "LineBreakType::BRElement) failed");
    return insertBRElementResultOrError.propagateErr();
  }
  CreateLineBreakResult insertBRElementResult =
      insertBRElementResultOrError.unwrap();
  return CreateElementResult(insertBRElementResult->BRElementRef(),
                             insertBRElementResult.UnwrapCaretPoint());
}

Result<EditorDOMPoint, nsresult> HTMLEditor::SetBlockElementAlign(
    Element& aBlockOrHRElement, const nsAString& aAlignType,
    EditTarget aEditTarget) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(HTMLEditUtils::IsBlockElement(
                 aBlockOrHRElement, BlockInlineCheck::UseHTMLDefaultStyle) ||
             aBlockOrHRElement.IsHTMLElement(nsGkAtoms::hr));
  MOZ_ASSERT(IsCSSEnabled() ||
             HTMLEditUtils::IsAlignAttrSupported(aBlockOrHRElement));

  EditorDOMPoint pointToPutCaret;
  if (!aBlockOrHRElement.IsHTMLElement(nsGkAtoms::table)) {
    Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
        RemoveAlignFromDescendants(aBlockOrHRElement, aAlignType, aEditTarget);
    if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
      NS_WARNING("HTMLEditor::RemoveAlignFromDescendants() failed");
      return pointToPutCaretOrError;
    }
    if (pointToPutCaretOrError.inspect().IsSet()) {
      pointToPutCaret = pointToPutCaretOrError.unwrap();
    }
  }
  nsresult rv = SetAttributeOrEquivalent(&aBlockOrHRElement, nsGkAtoms::align,
                                         aAlignType, false);
  if (NS_WARN_IF(Destroyed())) {
    return Err(NS_ERROR_EDITOR_DESTROYED);
  }
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::SetAttributeOrEquivalent(nsGkAtoms::align) failed");
    return Err(rv);
  }
  return pointToPutCaret;
}

Result<EditorDOMPoint, nsresult> HTMLEditor::ChangeMarginStart(
    Element& aElement, ChangeMargin aChangeMargin,
    const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(aElement);
  if (NS_WARN_IF(Destroyed())) {
    return Err(NS_ERROR_EDITOR_DESTROYED);
  }
  nsAutoString value;
  DebugOnly<nsresult> rvIgnored =
      CSSEditUtils::GetSpecifiedProperty(aElement, 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 f;
  RefPtr<nsAtom> unit;
  CSSEditUtils::ParseLength(value, &f, getter_AddRefs(unit));
  if (!f) {
    unit = nsGkAtoms::px;
  }
  int8_t multiplier = aChangeMargin == ChangeMargin::Increase ? 1 : -1;
  if (nsGkAtoms::in == unit) {
    f += NS_EDITOR_INDENT_INCREMENT_IN * multiplier;
  } else if (nsGkAtoms::cm == unit) {
    f += NS_EDITOR_INDENT_INCREMENT_CM * multiplier;
  } else if (nsGkAtoms::mm == unit) {
    f += NS_EDITOR_INDENT_INCREMENT_MM * multiplier;
  } else if (nsGkAtoms::pt == unit) {
    f += NS_EDITOR_INDENT_INCREMENT_PT * multiplier;
  } else if (nsGkAtoms::pc == unit) {
    f += NS_EDITOR_INDENT_INCREMENT_PC * multiplier;
  } else if (nsGkAtoms::em == unit) {
    f += NS_EDITOR_INDENT_INCREMENT_EM * multiplier;
  } else if (nsGkAtoms::ex == unit) {
    f += NS_EDITOR_INDENT_INCREMENT_EX * multiplier;
  } else if (nsGkAtoms::px == unit) {
    f += NS_EDITOR_INDENT_INCREMENT_PX * multiplier;
  } else if (nsGkAtoms::percentage == unit) {
    f += NS_EDITOR_INDENT_INCREMENT_PERCENT * multiplier;
  }

  if (0 < f) {
    if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) {
      nsAutoString newValue;
      newValue.AppendFloat(f);
      newValue.Append(nsDependentAtomString(unit));
      // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must
      // be guaranteed by caller because of MOZ_CAN_RUN_SCRIPT method.
      // MOZ_KnownLive(merginProperty): It's nsStaticAtom.
      nsresult rv = CSSEditUtils::SetCSSPropertyWithTransaction(
          *this, MOZ_KnownLive(*styledElement), MOZ_KnownLive(marginProperty),
          newValue);
      if (rv == NS_ERROR_EDITOR_DESTROYED) {
        NS_WARNING(
            "CSSEditUtils::SetCSSPropertyWithTransaction() destroyed the "
            "editor");
        return Err(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "CSSEditUtils::SetCSSPropertyWithTransaction() failed, but ignored");
    }
    return EditorDOMPoint();
  }

  if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) {
    // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must
    // be guaranteed by caller because of MOZ_CAN_RUN_SCRIPT method.
    // MOZ_KnownLive(merginProperty): It's nsStaticAtom.
    nsresult rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
        *this, MOZ_KnownLive(*styledElement), MOZ_KnownLive(marginProperty),
        value);
    if (rv == NS_ERROR_EDITOR_DESTROYED) {
      NS_WARNING(
          "CSSEditUtils::RemoveCSSPropertyWithTransaction() destroyed the "
          "editor");
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "CSSEditUtils::RemoveCSSPropertyWithTransaction() failed, but ignored");
  }

  // Remove unnecessary divs
  if (!aElement.IsHTMLElement(nsGkAtoms::div) ||
      HTMLEditUtils::ElementHasAttribute(aElement)) {
    return EditorDOMPoint();
  }
  // Don't touch editing host nor node which is outside of it.
  if (&aElement == &aEditingHost ||
      !aElement.IsInclusiveDescendantOf(&aEditingHost)) {
    return EditorDOMPoint();
  }

  Result<EditorDOMPoint, nsresult> unwrapDivElementResult =
      RemoveContainerWithTransaction(aElement);
  NS_WARNING_ASSERTION(unwrapDivElementResult.isOk(),
                       "HTMLEditor::RemoveContainerWithTransaction() failed");
  return unwrapDivElementResult;
}

Result<EditActionResult, nsresult>
HTMLEditor::SetSelectionToAbsoluteAsSubAction(const Element& aEditingHost) {
  AutoPlaceholderBatch treatAsOneTransaction(
      *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
  IgnoredErrorResult ignoredError;
  AutoEditSubActionNotifier startToHandleEditSubAction(
      *this, EditSubAction::eSetPositionToAbsolute, 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<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
    if (MOZ_UNLIKELY(result.isErr())) {
      NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
      return result;
    }
    if (result.inspect().Canceled()) {
      return result;
    }
  }

  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(aEditingHost);
    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");
    }
  }

  auto EnsureCaretInElementIfCollapsedOutside =
      [&](Element& aElement) MOZ_CAN_RUN_SCRIPT {
        if (!SelectionRef().IsCollapsed() || !SelectionRef().RangeCount()) {
          return NS_OK;
        }
        const auto firstRangeStartPoint =
            GetFirstSelectionStartPoint<EditorRawDOMPoint>();
        if (MOZ_UNLIKELY(!firstRangeStartPoint.IsSet())) {
          return NS_OK;
        }
        const Result<EditorRawDOMPoint, nsresult> pointToPutCaretOrError =
            HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
                EditorRawDOMPoint>(aElement, firstRangeStartPoint);
        if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
          NS_WARNING(
              "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() "
              "failed, but ignored");
          return NS_OK;
        }
        if (!pointToPutCaretOrError.inspect().IsSet()) {
          return NS_OK;
        }
        nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
        if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
          NS_WARNING("EditorBase::CollapseSelectionTo() failed");
          return NS_ERROR_EDITOR_DESTROYED;
        }
        NS_WARNING_ASSERTION(
            NS_SUCCEEDED(rv),
            "EditorBase::CollapseSelectionTo() failed, but ignored");
        return NS_OK;
      };

  const RefPtr<Element> focusElement = GetSelectionContainerElement();
  if (focusElement && HTMLEditUtils::IsImageElement(*focusElement)) {
    nsresult rv = EnsureCaretInElementIfCollapsedOutside(*focusElement);
    if (NS_FAILED(rv)) {
      NS_WARNING("EnsureCaretInElementIfCollapsedOutside() failed");
      return Err(rv);
    }
    return EditActionResult::HandledResult();
  }

  // XXX Why do we do this only when there is only one selection range?
  if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) {
    Result<EditorRawDOMRange, nsresult> 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());
    }
  }

  RefPtr<Element> divElement;
  rv = MoveSelectedContentsToDivElementToMakeItAbsolutePosition(
      address_of(divElement), aEditingHost);
  // MoveSelectedContentsToDivElementToMakeItAbsolutePosition() may restore
  // selection with AutoSelectionRestorer.  Therefore, the editor might have
  // already been destroyed now.
  if (NS_WARN_IF(Destroyed())) {
    return Err(NS_ERROR_EDITOR_DESTROYED);
  }
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "HTMLEditor::MoveSelectedContentsToDivElementToMakeItAbsolutePosition()"
        " failed");
    return Err(rv);
  }

  if (IsSelectionRangeContainerNotContent()) {
    NS_WARNING("Mutation event listener might have changed the selection");
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }

  if (SelectionRef().IsCollapsed()) {
    const auto caretPosition =
        EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
    Result<CreateLineBreakResult, nsresult>
        insertPaddingBRElementResultOrError =
            InsertPaddingBRElementIfInEmptyBlock(caretPosition, eNoStrip);
    if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
      NS_WARNING(
          "HTMLEditor::InsertPaddingBRElementIfInEmptyBlock(eNoStrip) failed");
      return insertPaddingBRElementResultOrError.propagateErr();
    }
    nsresult rv =
        insertPaddingBRElementResultOrError.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 (!divElement) {
    return EditActionResult::HandledResult();
  }

  rv = SetPositionToAbsoluteOrStatic(*divElement, true);
  if (NS_WARN_IF(Destroyed())) {
    return Err(NS_ERROR_EDITOR_DESTROYED);
  }
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::SetPositionToAbsoluteOrStatic() failed");
    return Err(rv);
  }

  rv = EnsureCaretInElementIfCollapsedOutside(*divElement);
  if (NS_FAILED(rv)) {
    NS_WARNING("EnsureCaretInElementIfCollapsedOutside() failed");
    return Err(rv);
  }
  return EditActionResult::HandledResult();
}

nsresult HTMLEditor::MoveSelectedContentsToDivElementToMakeItAbsolutePosition(
    RefPtr<Element>* aTargetElement, const Element& aEditingHost) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aTargetElement);

  AutoSelectionRestorer restoreSelectionLater(this);

  EditorDOMPoint pointToPutCaret;

  // Use these ranges to construct a list of nodes to act on.
  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  {
    AutoClonedSelectionRangeArray extendedSelectionRanges(SelectionRef());
    extendedSelectionRanges.ExtendRangesToWrapLines(
        EditSubAction::eSetPositionToAbsolute,
        BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
    Result<EditorDOMPoint, nsresult> splitResult =
        extendedSelectionRanges
            .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
                *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
    if (MOZ_UNLIKELY(splitResult.isErr())) {
      NS_WARNING(
          "AutoClonedRangeArray::"
          "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
          "failed");
      return splitResult.unwrapErr();
    }
    if (splitResult.inspect().IsSet()) {
      pointToPutCaret = splitResult.unwrap();
    }
    nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
        *this, arrayOfContents, EditSubAction::eSetPositionToAbsolute,
        AutoClonedRangeArray::CollectNonEditableNodes::Yes);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "AutoClonedRangeArray::CollectEditTargetNodes(EditSubAction::"
          "eSetPositionToAbsolute, CollectNonEditableNodes::Yes) failed");
      return rv;
    }
  }

  Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
      MaybeSplitElementsAtEveryBRElement(arrayOfContents,
                                         EditSubAction::eSetPositionToAbsolute);
  if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
    NS_WARNING(
        "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
        "eSetPositionToAbsolute) failed");
    return splitAtBRElementsResult.inspectErr();
  }
  if (splitAtBRElementsResult.inspect().IsSet()) {
    pointToPutCaret = splitAtBRElementsResult.unwrap();
  }

  if (AllowsTransactionsToChangeSelection() &&
      pointToPutCaret.IsSetAndValid()) {
    nsresult rv = CollapseSelectionTo(pointToPutCaret);
    if (NS_FAILED(rv)) {
      NS_WARNING("EditorBase::CollapseSelectionTo() failed");
      return rv;
    }
  }

  // If there is no visible and editable nodes in the edit targets, make an
  // empty block.
  // XXX Isn't this odd if there are only non-editable visible nodes?
  if (HTMLEditUtils::IsEmptyOneHardLine(
          arrayOfContents, BlockInlineCheck::UseHTMLDefaultStyle)) {
    const auto atCaret =
        EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
    if (NS_WARN_IF(!atCaret.IsSet())) {
      return NS_ERROR_FAILURE;
    }

    // Make sure we can put a block here.
    Result<CreateElementResult, nsresult> createNewDivElementResult =
        InsertElementWithSplittingAncestorsWithTransaction(
            *nsGkAtoms::div, atCaret, BRElementNextToSplitPoint::Keep,
            aEditingHost);
    if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
      NS_WARNING(
          "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
          "nsGkAtoms::div) failed");
      return createNewDivElementResult.unwrapErr();
    }
    CreateElementResult unwrappedCreateNewDivElementResult =
        createNewDivElementResult.unwrap();
    // We'll update selection after deleting the content nodes and nobody
    // refers selection until then.  Therefore, we don't need to update
    // selection here.
    unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion();
    RefPtr<Element> newDivElement =
        unwrappedCreateNewDivElementResult.UnwrapNewNode();
    MOZ_ASSERT(newDivElement);
    // Delete anything that was in the list of nodes
    // XXX We don't need to remove items from the array.
    for (OwningNonNull<nsIContent>& curNode : arrayOfContents) {
      // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it alive.
      nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*curNode));
      if (NS_FAILED(rv)) {
        NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
        return rv;
      }
    }
    // Don't restore the selection
    restoreSelectionLater.Abort();
    nsresult rv = CollapseSelectionToStartOf(*newDivElement);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "EditorBase::CollapseSelectionToStartOf() failed");
    *aTargetElement = std::move(newDivElement);
    return rv;
  }

  // `<div>` element to be positioned absolutely.  This may have already
  // existed or newly created by this method.
  RefPtr<Element> targetDivElement;
  // Newly created list element for moving selected list item elements into
  // targetDivElement.  I.e., this is created in the `<div>` element.
  RefPtr<Element> createdListElement;
  // If we handle a parent list item element, this is set to it.  In such case,
  // we should handle its children again.
  RefPtr<Element> handledListItemElement;
  for (size_t i = 0; i < arrayOfContents.Length(); i++) {
    const OwningNonNull<nsIContent>& content = arrayOfContents[i];

    // Here's where we actually figure out what to do.
    EditorDOMPoint atContent(content);
    if (NS_WARN_IF(!atContent.IsInContentNode())) {
      return NS_ERROR_FAILURE;  // XXX not continue??
    }

    // Ignore all non-editable nodes.  Leave them be.
    if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
      continue;
    }

    // If current node is a child of a list element, we need another list
    // element in absolute-positioned `<div>` element to avoid non-selected
    // list items are moved into the `<div>` element.
    if (HTMLEditUtils::IsListElement(*atContent.ContainerAs<nsIContent>())) {
      // If we cannot move current node to created list element, we need a
      // list element in the target `<div>` element for the destination.
      // Therefore, duplicate same list element into the target `<div>`
      // element.
      nsIContent* const previousEditableContent =
          createdListElement
              ? HTMLEditUtils::GetPreviousSibling(
                    content, {LeafNodeOption::IgnoreNonEditableNode},
                    BlockInlineCheck::UseComputedDisplayOutsideStyle)
              : nullptr;
      if (!createdListElement ||
          (previousEditableContent &&
           previousEditableContent != createdListElement)) {
        nsAtom* ULOrOLOrDLTagName =
            atContent.GetContainer()->NodeInfo()->NameAtom();
        if (targetDivElement) {
          // XXX Do we need to split the container? Since we'll append new
          //     element at end of the <div> element.
          Result<SplitNodeResult, nsresult> splitNodeResult =
              MaybeSplitAncestorsForInsertWithTransaction(
                  MOZ_KnownLive(*ULOrOLOrDLTagName), atContent, aEditingHost);
          if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
            NS_WARNING(
                "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() "
                "failed");
            return splitNodeResult.unwrapErr();
          }
          // We'll update selection after creating a list element below.
          // Therefore, we don't need to touch selection here.
          splitNodeResult.inspect().IgnoreCaretPointSuggestion();
        } else {
          // If we've not had a target <div> element yet, let's insert a <div>
          // element with splitting the ancestors.
          Result<CreateElementResult, nsresult> createNewDivElementResult =
              InsertElementWithSplittingAncestorsWithTransaction(
                  *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
                  aEditingHost);
          if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
            NS_WARNING(
                "HTMLEditor::"
                "InsertElementWithSplittingAncestorsWithTransaction(nsGkAtoms::"
                "div) failed");
            return createNewDivElementResult.unwrapErr();
          }
          // We'll update selection after creating a list element below.
          // Therefor, we don't need to touch selection here.
          createNewDivElementResult.inspect().IgnoreCaretPointSuggestion();
          MOZ_ASSERT(createNewDivElementResult.inspect().GetNewNode());
          targetDivElement = createNewDivElementResult.unwrap().UnwrapNewNode();
        }
        Result<CreateElementResult, nsresult> createNewListElementResult =
            CreateAndInsertElement(WithTransaction::Yes,
                                   MOZ_KnownLive(*ULOrOLOrDLTagName),
                                   EditorDOMPoint::AtEndOf(targetDivElement));
        if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
          NS_WARNING(
              "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) "
              "failed");
          return createNewListElementResult.unwrapErr();
        }
        nsresult rv = createNewListElementResult.inspect().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");
        createdListElement =
            createNewListElementResult.unwrap().UnwrapNewNode();
        MOZ_ASSERT(createdListElement);
      }
      // Move current node (maybe, assumed as a list item element) into the
      // new list element in the target `<div>` element to be positioned
      // absolutely.
      // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it alive.
      Result<MoveNodeResult, nsresult> moveNodeResult =
          MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
                                       *createdListElement);
      if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
        NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
        return moveNodeResult.propagateErr();
      }
      nsresult rv = moveNodeResult.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");
      continue;
    }

    // If contents in a list item element is selected, we should move current
    // node into the target `<div>` element with the list item element itself
    // because we want to keep indent level of the contents.
    if (RefPtr<Element> listItemElement =
            HTMLEditUtils::GetClosestInclusiveAncestorListItemElement(
                content, &aEditingHost)) {
      if (handledListItemElement == listItemElement) {
        // Current node has already been moved into the `<div>` element.
        continue;
      }
      // If we cannot move the list item element into created list element,
      // we need another list element in the target `<div>` element.
      nsIContent* const previousEditableContent =
          createdListElement
              ? HTMLEditUtils::GetPreviousSibling(
                    *listItemElement, {LeafNodeOption::IgnoreNonEditableNode},
                    BlockInlineCheck::UseComputedDisplayOutsideStyle)
              : nullptr;
      if (!createdListElement ||
          (previousEditableContent &&
           previousEditableContent != createdListElement)) {
        EditorDOMPoint atListItem(listItemElement);
        if (NS_WARN_IF(!atListItem.IsSet())) {
          return NS_ERROR_FAILURE;
        }
        // XXX If content is the listItemElement and not in a list element,
        //     we duplicate wrong element into the target `<div>` element.
        nsAtom* containerName =
            atListItem.GetContainer()->NodeInfo()->NameAtom();
        if (targetDivElement) {
          // XXX Do we need to split the container? Since we'll append new
          //     element at end of the <div> element.
          Result<SplitNodeResult, nsresult> splitNodeResult =
              MaybeSplitAncestorsForInsertWithTransaction(
                  MOZ_KnownLive(*containerName), atListItem, aEditingHost);
          if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
            NS_WARNING(
                "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() "
                "failed");
            return splitNodeResult.unwrapErr();
          }
          // We'll update selection after creating a list element below.
          // Therefore, we don't need to touch selection here.
          splitNodeResult.inspect().IgnoreCaretPointSuggestion();
        } else {
          // If we've not had a target <div> element yet, let's insert a <div>
          // element with splitting the ancestors.
          Result<CreateElementResult, nsresult> createNewDivElementResult =
              InsertElementWithSplittingAncestorsWithTransaction(
                  *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
                  aEditingHost);
          if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
            NS_WARNING(
                "HTMLEditor::"
                "InsertElementWithSplittingAncestorsWithTransaction("
                "nsGkAtoms::div) failed");
            return createNewDivElementResult.unwrapErr();
          }
          // We'll update selection after creating a list element below.
          // Therefore, we don't need to touch selection here.
          createNewDivElementResult.inspect().IgnoreCaretPointSuggestion();
          MOZ_ASSERT(createNewDivElementResult.inspect().GetNewNode());
          targetDivElement = createNewDivElementResult.unwrap().UnwrapNewNode();
        }
        // XXX So, createdListElement may be set to a non-list element.
        Result<CreateElementResult, nsresult> createNewListElementResult =
            CreateAndInsertElement(WithTransaction::Yes,
                                   MOZ_KnownLive(*containerName),
                                   EditorDOMPoint::AtEndOf(targetDivElement));
        if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
          NS_WARNING(
              "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) "
              "failed");
          return createNewListElementResult.unwrapErr();
        }
        nsresult rv = createNewListElementResult.inspect().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");
        createdListElement =
            createNewListElementResult.unwrap().UnwrapNewNode();
        MOZ_ASSERT(createdListElement);
      }
      // Move current list item element into the createdListElement (could be
      // non-list element due to the above bug) in a candidate `<div>` element
      // to be positioned absolutely.
      Result<MoveNodeResult, nsresult> moveListItemElementResult =
          MoveNodeToEndWithTransaction(*listItemElement, *createdListElement);
      if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
        NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
        return moveListItemElementResult.unwrapErr();
      }
      nsresult rv = moveListItemElementResult.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");
      handledListItemElement = std::move(listItemElement);
      continue;
    }

    if (!targetDivElement) {
      // If we meet a `<div>` element, use it as the absolute-position
      // container.
      // XXX This looks odd.  If there are 2 or more `<div>` elements are
      //     selected, first found `<div>` element will have all other
      //     selected nodes.
      if (content->IsHTMLElement(nsGkAtoms::div)) {
        targetDivElement = content->AsElement();
        MOZ_ASSERT(!createdListElement);
        MOZ_ASSERT(!handledListItemElement);
        continue;
      }
      // Otherwise, create new `<div>` element to be positioned absolutely
      // and to contain all selected nodes.
      Result<CreateElementResult, nsresult> createNewDivElementResult =
          InsertElementWithSplittingAncestorsWithTransaction(
              *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
              aEditingHost);
      if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
        NS_WARNING(
            "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
            "nsGkAtoms::div) failed");
        return createNewDivElementResult.unwrapErr();
      }
      nsresult rv = createNewDivElementResult.inspect().SuggestCaretPointTo(
          *this, {SuggestCaret::OnlyIfHasSuggestion,
                  SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
      if (NS_FAILED(rv)) {
        NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
        return rv;
      }
      MOZ_ASSERT(createNewDivElementResult.inspect().GetNewNode());
      targetDivElement = createNewDivElementResult.unwrap().UnwrapNewNode();
    }

    const OwningNonNull<nsIContent> lastContent = [&]() {
      nsIContent* lastContent = content;
      for (; i + 1 < arrayOfContents.Length(); i++) {
        const OwningNonNull<nsIContent>& nextContent = arrayOfContents[i + 1];
        if (lastContent->GetNextSibling() == nextContent ||
            HTMLEditUtils::IsListElement(*nextContent) ||
            HTMLEditUtils::IsListItemElement(*nextContent) ||
            !EditorUtils::IsEditableContent(content, EditorType::HTML)) {
          break;
        }
        lastContent = nextContent;
      }
      return OwningNonNull<nsIContent>(*lastContent);
    }();

    // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it alive.
    Result<MoveNodeResult, nsresult> moveNodeResult =
        MoveSiblingsToEndWithTransaction(MOZ_KnownLive(content), lastContent,
                                         *targetDivElement);
    if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
      NS_WARNING("HTMLEditor::MoveSiblingsToEndWithTransaction() failed");
      return moveNodeResult.unwrapErr();
    }
    nsresult rv = moveNodeResult.inspect().SuggestCaretPointTo(
        *this, {SuggestCaret::OnlyIfHasSuggestion,
                SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
                SuggestCaret::AndIgnoreTrivialError});
    if (NS_FAILED(rv)) {
      NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
      return rv;
    }
    NS_WARNING_ASSERTION(
        rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
        "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
    // Forget createdListElement, if any
    createdListElement = nullptr;
  }
  *aTargetElement = std::move(targetDivElement);
  return NS_OK;
}

Result<EditActionResult, nsresult>
HTMLEditor::SetSelectionToStaticAsSubAction() {
  MOZ_ASSERT(IsEditActionDataAvailable());

  AutoPlaceholderBatch treatAsOneTransaction(
      *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
  IgnoredErrorResult ignoredError;
  AutoEditSubActionNotifier startToHandleEditSubAction(
      *this, EditSubAction::eSetPositionToStatic, 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<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
    if (MOZ_UNLIKELY(result.isErr())) {
      NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
      return result;
    }
    if (result.inspect().Canceled()) {
      return result;
    }
  }

  const RefPtr<Element> editingHost =
      ComputeEditingHost(LimitInBodyElement::No);
  if (NS_WARN_IF(!editingHost)) {
    return Err(NS_ERROR_FAILURE);
  }

  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(*editingHost);
    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<Element> element = GetAbsolutelyPositionedSelectionContainer();
  if (!element) {
    if (NS_WARN_IF(Destroyed())) {
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING(
        "HTMLEditor::GetAbsolutelyPositionedSelectionContainer() returned "
        "nullptr");
    return Err(NS_ERROR_FAILURE);
  }

  {
    AutoSelectionRestorer restoreSelectionLater(this);

    nsresult rv = SetPositionToAbsoluteOrStatic(*element, false);
    if (NS_WARN_IF(Destroyed())) {
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::SetPositionToAbsoluteOrStatic() failed");
      return Err(rv);
    }
  }

  // Restoring Selection might cause destroying the HTML editor.
  if (MOZ_UNLIKELY(Destroyed())) {
    NS_WARNING("Destroying AutoSelectionRestorer caused destroying the editor");
    return Err(NS_ERROR_EDITOR_DESTROYED);
  }
  return EditActionResult::HandledResult();
}

Result<EditActionResult, nsresult> HTMLEditor::AddZIndexAsSubAction(
    int32_t aChange) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  AutoPlaceholderBatch treatAsOneTransaction(
      *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
  IgnoredErrorResult ignoredError;
  AutoEditSubActionNotifier startToHandleEditSubAction(
      *this,
      aChange < 0 ? EditSubAction::eDecreaseZIndex
                  : EditSubAction::eIncreaseZIndex,
      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<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
    if (MOZ_UNLIKELY(result.isErr())) {
      NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
      return result;
    }
    if (result.inspect().Canceled()) {
      return result;
    }
  }

  const RefPtr<Element> editingHost =
      ComputeEditingHost(LimitInBodyElement::No);
  if (NS_WARN_IF(!editingHost)) {
    return Err(NS_ERROR_FAILURE);
  }

  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(*editingHost);
    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<Element> absolutelyPositionedElement =
      GetAbsolutelyPositionedSelectionContainer();
  if (!absolutelyPositionedElement) {
    if (NS_WARN_IF(Destroyed())) {
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING(
        "HTMLEditor::GetAbsolutelyPositionedSelectionContainer() returned "
        "nullptr");
    return Err(NS_ERROR_FAILURE);
  }

  nsStyledElement* absolutelyPositionedStyledElement =
      nsStyledElement::FromNode(absolutelyPositionedElement);
  if (NS_WARN_IF(!absolutelyPositionedStyledElement)) {
    return Err(NS_ERROR_FAILURE);
  }

  {
    AutoSelectionRestorer restoreSelectionLater(this);

    // MOZ_KnownLive(*absolutelyPositionedStyledElement): It's
    // absolutelyPositionedElement whose type is RefPtr.
    Result<int32_t, nsresult> result = AddZIndexWithTransaction(
        MOZ_KnownLive(*absolutelyPositionedStyledElement), aChange);
    if (MOZ_UNLIKELY(result.isErr())) {
      NS_WARNING("HTMLEditor::AddZIndexWithTransaction() failed");
      return result.propagateErr();
    }
  }

  // Restoring Selection might cause destroying the HTML editor.
  if (MOZ_UNLIKELY(Destroyed())) {
    NS_WARNING("Destroying AutoSelectionRestorer caused destroying the editor");
    return Err(NS_ERROR_EDITOR_DESTROYED);
  }

  return EditActionResult::HandledResult();
}

}  // namespace mozilla

Messung V0.5 in Prozent
C=95 H=97 G=95

¤ Dauer der Verarbeitung: 0.577 Sekunden  (vorverarbeitet am  2026-08-25) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=141584
#Domains=752002