editor/libeditor/HTMLEditSubActionHandler.cpp
author Olli Pettay <Olli.Pettay@helsinki.fi>
Wed, 20 May 2020 14:55:15 +0000
changeset 531248 2c5eea62791725beabc11d5fcfe17f0c5f515860
parent 530967 dc4525f247b901b1f6f958e3fd8518dd85dfff17
child 535634 2a1cceae6e2b614b4c9d3206fcecc9f11b137a04
permissions -rw-r--r--
Bug 1637712 - Consider to move *Ancestor* API to nsINode r=emilio Differential Revision: https://phabricator.services.mozilla.com/D75169

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "HTMLEditor.h"

#include <algorithm>
#include <utility>

#include "HTMLEditUtils.h"
#include "WSRunObject.h"
#include "mozilla/Assertions.h"
#include "mozilla/CSSEditUtils.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/EditAction.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorUtils.h"
#include "mozilla/InternalMutationEvent.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/Preferences.h"
#include "mozilla/RangeUtils.h"
#include "mozilla/TextComposition.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/RangeBinding.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/StaticRange.h"
#include "mozilla/mozalloc.h"
#include "nsAString.h"
#include "nsAlgorithm.h"
#include "nsAtom.h"
#include "nsCRT.h"
#include "nsCRTGlue.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsGkAtoms.h"
#include "nsHTMLDocument.h"
#include "nsIContent.h"
#include "nsID.h"
#include "nsIFrame.h"
#include "nsINode.h"
#include "nsLiteralString.h"
#include "nsRange.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTextNode.h"
#include "nsThreadUtils.h"
#include "nsUnicharUtils.h"

// Workaround for windows headers
#ifdef SetProp
#  undef SetProp
#endif

class nsISupports;

namespace mozilla {

using namespace dom;
using StyleDifference = HTMLEditUtils::StyleDifference;
using ChildBlockBoundary = HTMLEditUtils::ChildBlockBoundary;

enum { kLonely = 0, kPrevSib = 1, kNextSib = 2, kBothSibs = 3 };

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

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

class MOZ_RAII AutoSetTemporaryAncestorLimiter final {
  MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER;

 public:
  explicit AutoSetTemporaryAncestorLimiter(
      HTMLEditor& aHTMLEditor, Selection& aSelection,
      nsINode& aStartPointNode MOZ_GUARD_OBJECT_NOTIFIER_PARAM) {
    MOZ_GUARD_OBJECT_NOTIFIER_INIT;

    MOZ_ASSERT(aSelection.GetType() == SelectionType::eNormal);

    if (aSelection.GetAncestorLimiter()) {
      return;
    }

    Element* root = aHTMLEditor.FindSelectionRoot(&aStartPointNode);
    if (root) {
      aHTMLEditor.InitializeSelectionAncestorLimit(*root);
      mSelection = &aSelection;
    }
  }

  ~AutoSetTemporaryAncestorLimiter() {
    if (mSelection) {
      mSelection->SetAncestorLimiter(nullptr);
    }
  }

 private:
  RefPtr<Selection> mSelection;
};

// Helper struct for DoJoinNodes() and DoSplitNode().
struct MOZ_STACK_CLASS SavedRange final {
  RefPtr<Selection> mSelection;
  nsCOMPtr<nsINode> mStartContainer;
  nsCOMPtr<nsINode> mEndContainer;
  int32_t mStartOffset = 0;
  int32_t mEndOffset = 0;
};

template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock(
    RangeBoundary& aStartRef, RangeBoundary& aEndRef);
template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock(
    RawRangeBoundary& aStartRef, RangeBoundary& aEndRef);
template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock(
    RangeBoundary& aStartRef, RawRangeBoundary& aEndRef);
template void HTMLEditor::SelectBRElementIfCollapsedInEmptyBlock(
    RawRangeBoundary& aStartRef, RawRangeBoundary& aEndRef);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const RangeBoundary& aStartRef, const RangeBoundary& aEndRef);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const RawRangeBoundary& aStartRef, const RangeBoundary& aEndRef);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const RangeBoundary& aStartRef, const RawRangeBoundary& aEndRef);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
    const RawRangeBoundary& aStartRef, const RawRangeBoundary& aEndRef);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd(
    const RangeBoundary& aStartRef, const RangeBoundary& aEndRef,
    EditSubAction aEditSubAction);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd(
    const RawRangeBoundary& aStartRef, const RangeBoundary& aEndRef,
    EditSubAction aEditSubAction);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd(
    const RangeBoundary& aStartRef, const RawRangeBoundary& aEndRef,
    EditSubAction aEditSubAction);
template already_AddRefed<nsRange>
HTMLEditor::CreateRangeExtendedToHardLineStartAndEnd(
    const RawRangeBoundary& aStartRef, const RawRangeBoundary& aEndRef,
    EditSubAction aEditSubAction);
template EditorDOMPoint HTMLEditor::GetWhiteSpaceEndPoint(
    const RangeBoundary& aPoint, ScanDirection aScanDirection);
template EditorDOMPoint HTMLEditor::GetWhiteSpaceEndPoint(
    const RawRangeBoundary& aPoint, ScanDirection aScanDirection);
template EditorDOMPoint HTMLEditor::GetCurrentHardLineStartPoint(
    const RangeBoundary& aPoint, EditSubAction aEditSubAction);
template EditorDOMPoint HTMLEditor::GetCurrentHardLineStartPoint(
    const RawRangeBoundary& aPoint, EditSubAction aEditSubAction);
template EditorDOMPoint HTMLEditor::GetCurrentHardLineEndPoint(
    const RangeBoundary& aPoint);
template EditorDOMPoint HTMLEditor::GetCurrentHardLineEndPoint(
    const RawRangeBoundary& aPoint);
template Element* HTMLEditor::GetInvisibleBRElementAt(
    const EditorDOMPoint& aPoint);
template Element* HTMLEditor::GetInvisibleBRElementAt(
    const EditorRawDOMPoint& aPoint);
template nsIContent* HTMLEditor::FindNearEditableContent(
    const EditorDOMPoint& aPoint, nsIEditor::EDirection aDirection);
template nsIContent* HTMLEditor::FindNearEditableContent(
    const EditorRawDOMPoint& aPoint, nsIEditor::EDirection aDirection);
template nsresult HTMLEditor::DeleteTextAndTextNodesWithTransaction(
    const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint);
template nsresult HTMLEditor::DeleteTextAndTextNodesWithTransaction(
    const EditorDOMPointInText& aStartPoint,
    const EditorDOMPointInText& aEndPoint);

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

  nsresult rv = TextEditor::InitEditorContentAndSelection();
  if (NS_FAILED(rv)) {
    NS_WARNING("TextEditor::InitEditorContentAndSelection() failed");
    return rv;
  }

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

  if (!bodyOrDocumentElement) {
    return NS_OK;
  }

  rv = InsertBRElementToEmptyListItemsAndTableCellsInRange(
      RawRangeBoundary(bodyOrDocumentElement, 0u),
      RawRangeBoundary(bodyOrDocumentElement,
                       bodyOrDocumentElement->GetChildCount()));
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange() "
      "failed, but ignored");
  return NS_OK;
}

void HTMLEditor::OnStartToHandleTopLevelEditSubAction(
    EditSubAction aTopLevelEditSubAction,
    nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(!aRv.Failed());

  EditorBase::OnStartToHandleTopLevelEditSubAction(
      aTopLevelEditSubAction, aDirectionOfTopLevelEditSubAction, aRv);

  MOZ_ASSERT(GetTopLevelEditSubAction() == aTopLevelEditSubAction);
  MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() ==
             aDirectionOfTopLevelEditSubAction);

  if (NS_WARN_IF(Destroyed())) {
    aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
    return;
  }

  if (!mInitSucceeded) {
    return;  // We should do nothing if we're being initialized.
  }

  NS_WARNING_ASSERTION(
      !aRv.Failed(),
      "EditorBase::OnStartToHandleTopLevelEditSubAction() failed");

  // Remember where our selection was before edit action took place:
  if (GetCompositionStartPoint().IsSet()) {
    // If there is composition string, let's remember current composition
    // range.
    TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(
        GetCompositionStartPoint(), GetCompositionEndPoint());
  } 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(!SelectionRefPtr()->RangeCount())) {
      aRv.Throw(NS_ERROR_UNEXPECTED);
      return;
    }
    if (const nsRange* range = SelectionRefPtr()->GetRangeAt(0)) {
      TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(*range);
    }
  }

  // Register with range updater to track this as we perturb the doc
  RangeUpdaterRef().RegisterRangeItem(
      *TopLevelEditSubActionDataRef().mSelectedRange);

  // Remember current inline styles for deletion and normal insertion ops
  bool cacheInlineStyles;
  switch (aTopLevelEditSubAction) {
    case EditSubAction::eInsertText:
    case EditSubAction::eInsertTextComingFromIME:
    case EditSubAction::eDeleteSelectedContent:
      cacheInlineStyles = true;
      break;
    default:
      cacheInlineStyles =
          IsStyleCachePreservingSubAction(aTopLevelEditSubAction);
      break;
  }
  if (cacheInlineStyles) {
    nsCOMPtr<nsIContent> containerContent = nsIContent::FromNodeOrNull(
        aDirectionOfTopLevelEditSubAction == nsIEditor::eNext
            ? TopLevelEditSubActionDataRef().mSelectedRange->mEndContainer
            : TopLevelEditSubActionDataRef().mSelectedRange->mStartContainer);
    if (NS_WARN_IF(!containerContent)) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
    nsresult rv = CacheInlineStyles(*containerContent);
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::CacheInlineStyles() failed");
      aRv.Throw(rv);
      return;
    }
  }

  // Stabilize the document against contenteditable count changes
  Document* document = GetDocument();
  if (NS_WARN_IF(!document)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }
  if (document->GetEditingState() == Document::EditingState::eContentEditable) {
    document->ChangeContentEditableCount(nullptr, +1);
    TopLevelEditSubActionDataRef().mRestoreContentEditableCount = true;
  }

  // Check that selection is in subtree defined by body node
  nsresult rv = EnsureSelectionInBodyOrDocumentElement();
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
    return;
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() "
                       "failed, but ignored");
}

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

  nsresult rv;
  while (true) {
    if (NS_WARN_IF(Destroyed())) {
      rv = NS_ERROR_EDITOR_DESTROYED;
      break;
    }

    if (!mInitSucceeded) {
      rv = NS_OK;  // We should do nothing if we're being initialized.
      break;
    }

    // Do all the tricky stuff
    rv = OnEndHandlingTopLevelEditSubActionInternal();
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() failied");
    // Perhaps, we need to do the following jobs even if the editor has been
    // destroyed since they adjust some states of HTML document but don't
    // modify the DOM tree nor Selection.

    // Free up selectionState range item
    if (TopLevelEditSubActionDataRef().mSelectedRange) {
      RangeUpdaterRef().DropRangeItem(
          *TopLevelEditSubActionDataRef().mSelectedRange);
    }

    // Reset the contenteditable count to its previous value
    if (TopLevelEditSubActionDataRef().mRestoreContentEditableCount) {
      Document* document = GetDocument();
      if (NS_WARN_IF(!document)) {
        rv = NS_ERROR_FAILURE;
        break;
      }
      if (document->GetEditingState() ==
          Document::EditingState::eContentEditable) {
        document->ChangeContentEditableCount(nullptr, -1);
      }
    }
    break;
  }
  DebugOnly<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());

  nsresult rv = EnsureSelectionInBodyOrDocumentElement();
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() "
                       "failed, but ignored");

  switch (GetTopLevelEditSubAction()) {
    case EditSubAction::eReplaceHeadWithHTMLSource:
    case EditSubAction::eCreatePaddingBRElementForEmptyEditor:
      return NS_OK;
    default:
      break;
  }

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

    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 whitespaces.
        RefPtr<nsRange> extendedChangedRange =
            CreateRangeIncludingAdjuscentWhiteSpaces(
                *TopLevelEditSubActionDataRef().mChangedRange);
        if (extendedChangedRange) {
          MOZ_ASSERT(extendedChangedRange->IsPositioned());
          // Use extended range temporarily.
          TopLevelEditSubActionDataRef().mChangedRange =
              std::move(extendedChangedRange);
        }
        break;
      }
      default: {
        RefPtr<nsRange> extendedChangedRange =
            CreateRangeExtendedToHardLineStartAndEnd(
                *TopLevelEditSubActionDataRef().mChangedRange,
                GetTopLevelEditSubAction());
        if (extendedChangedRange) {
          MOZ_ASSERT(extendedChangedRange->IsPositioned());
          // Use extended range temporarily.
          TopLevelEditSubActionDataRef().mChangedRange =
              std::move(extendedChangedRange);
        }
        break;
      }
    }

    // if we did a ranged deletion or handling backspace key, make sure we have
    // a place to put caret.
    // Note we only want to do this if the overall operation was deletion,
    // not if deletion was done along the way for
    // EditSubAction::eInsertHTMLSource, EditSubAction::eInsertText, etc.
    // That's why this is here rather than DeleteSelectionAsSubAction().
    // However, we shouldn't insert <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) {
      nsresult rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
          EditorBase::GetStartPoint(*SelectionRefPtr()));
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "HTMLEditor::"
            "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() "
            "failed");
        return rv;
      }
    }

    // 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 selection
    rv = RemoveEmptyNodesIn(
        MOZ_KnownLive(*TopLevelEditSubActionDataRef().mChangedRange));
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::RemoveEmptyNodesIn() failed");
      return rv;
    }

    // attempt to transform any unneeded nbsp's into spaces after doing various
    // operations
    switch (GetTopLevelEditSubAction()) {
      case EditSubAction::eInsertText:
      case EditSubAction::eInsertTextComingFromIME:
      case EditSubAction::eDeleteSelectedContent:
      case EditSubAction::eInsertLineBreak:
      case EditSubAction::eInsertParagraphSeparator:
      case EditSubAction::ePasteHTMLContent:
      case EditSubAction::eInsertHTMLSource: {
        // TODO: Temporarily, WSRunObject replaces ASCII whitespaces with NPSPs
        //       and then, we'll replace them with ASCII whitespaces here.  We
        //       should avoid this overwriting things as far as possible because
        //       replacing characters in text nodes causes running mutation
        //       event listeners which are really expensive.
        // Adjust end of composition string if there is composition string.
        EditorRawDOMPoint pointToAdjust(GetCompositionEndPoint());
        if (!pointToAdjust.IsSet()) {
          // Otherwise, adjust current selection start point.
          pointToAdjust = EditorBase::GetStartPoint(*SelectionRefPtr());
          if (NS_WARN_IF(!pointToAdjust.IsSet())) {
            return NS_ERROR_FAILURE;
          }
        }
        rv = WSRunObject(*this, pointToAdjust).AdjustWhitespace();
        if (NS_WARN_IF(Destroyed())) {
          return NS_ERROR_EDITOR_DESTROYED;
        }
        if (NS_FAILED(rv)) {
          NS_WARNING("WSRunObject::AdjustWhitespace() failed");
          return rv;
        }

        // also do this for original selection endpoints.
        // XXX Hmm, if `AdjustWhitespace()` runs mutation event listener
        //     and that causes changing `mSelectedRange`, what we should do?
        if (NS_WARN_IF(
                !TopLevelEditSubActionDataRef().mSelectedRange->IsSet())) {
          return NS_ERROR_FAILURE;
        }
        DebugOnly<nsresult> rvIgnored =
            WSRunObject(
                *this,
                TopLevelEditSubActionDataRef().mSelectedRange->StartRawPoint())
                .AdjustWhitespace();
        if (NS_WARN_IF(Destroyed())) {
          return NS_ERROR_EDITOR_DESTROYED;
        }
        NS_WARNING_ASSERTION(
            NS_SUCCEEDED(rvIgnored),
            "WSRunObject::AdjustWhitespace() failed, but ignored");
        // we only need to handle old selection endpoint if it was different
        // from start
        if (TopLevelEditSubActionDataRef().mSelectedRange->IsCollapsed()) {
          DebugOnly<nsresult> rvIgnored =
              WSRunObject(
                  *this,
                  TopLevelEditSubActionDataRef().mSelectedRange->EndRawPoint())
                  .AdjustWhitespace();
          if (NS_WARN_IF(Destroyed())) {
            return NS_ERROR_EDITOR_DESTROYED;
          }
          NS_WARNING_ASSERTION(
              NS_SUCCEEDED(rvIgnored),
              "WSRunObject::AdjustWhitespace() failed, but ignored");
        }
        break;
      }
      default:
        break;
    }

    // If we created a new block, make sure caret is in it.
    if (TopLevelEditSubActionDataRef().mNewBlockElement &&
        SelectionRefPtr()->IsCollapsed()) {
      nsresult rv = EnsureCaretInBlockElement(
          MOZ_KnownLive(*TopLevelEditSubActionDataRef().mNewBlockElement));
      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "HTMLEditor::EnsureSelectionInBlockElement() failed, but ignored");
    }

    // Adjust selection for insert text, html paste, and delete actions if
    // we haven't removed new empty blocks.  Note that if empty block parents
    // are removed, Selection should've been adjusted by the method which
    // did it.
    if (!TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks &&
        SelectionRefPtr()->IsCollapsed()) {
      switch (GetTopLevelEditSubAction()) {
        case EditSubAction::eInsertText:
        case EditSubAction::eInsertTextComingFromIME:
        case EditSubAction::eDeleteSelectedContent:
        case EditSubAction::eInsertLineBreak:
        case EditSubAction::eInsertParagraphSeparator:
        case EditSubAction::ePasteHTMLContent:
        case EditSubAction::eInsertHTMLSource:
          // XXX AdjustCaretPositionAndEnsurePaddingBRElement() intentionally
          //     does not create padding `<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 =
            IsStyleCachePreservingSubAction(GetTopLevelEditSubAction());
        break;
    }
    if (reapplyCachedStyle) {
      DebugOnly<nsresult> rvIgnored =
          mTypeInState->UpdateSelState(SelectionRefPtr());
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                           "TypeInState::UpdateSelState() failed, but ignored");
      rv = ReapplyCachedStyles();
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::ReapplyCachedStyles() failed");
        return rv;
      }
      TopLevelEditSubActionDataRef().mCachedInlineStyles->Clear();
    }
  }

  rv = HandleInlineSpellCheck(
      TopLevelEditSubActionDataRef().mSelectedRange->StartPoint(),
      TopLevelEditSubActionDataRef().mChangedRange);
  if (NS_FAILED(rv)) {
    NS_WARNING("EditorBase::HandleInlineSpellCheck() failed");
    return rv;
  }

  // detect empty doc
  // XXX Need to investigate when the padding <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 &&
      SelectionRefPtr()->IsCollapsed()) {
    SetSelectionInterlinePosition();
  }

  return NS_OK;
}

EditActionResult HTMLEditor::CanHandleHTMLEditSubAction() const {
  MOZ_ASSERT(IsEditActionDataAvailable());

  if (NS_WARN_IF(Destroyed())) {
    return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
  }

  // If there is not selection ranges, we should ignore the result.
  if (!SelectionRefPtr()->RangeCount()) {
    return EditActionCanceled();
  }

  const nsRange* range = SelectionRefPtr()->GetRangeAt(0);
  nsINode* selStartNode = range->GetStartContainer();
  if (NS_WARN_IF(!selStartNode)) {
    return EditActionResult(NS_ERROR_FAILURE);
  }

  if (!HTMLEditUtils::IsSimplyEditableNode(*selStartNode)) {
    return EditActionCanceled();
  }

  nsINode* selEndNode = range->GetEndContainer();
  if (NS_WARN_IF(!selEndNode)) {
    return EditActionResult(NS_ERROR_FAILURE);
  }

  if (selStartNode == selEndNode) {
    return EditActionIgnored();
  }

  if (!HTMLEditUtils::IsSimplyEditableNode(*selEndNode)) {
    return EditActionCanceled();
  }

  // XXX What does it mean the common ancestor is editable?  I have no idea.
  //     It should be in same (active) editing host, and even if it's editable,
  //     there may be non-editable contents in the range.
  nsINode* commonAncestor = range->GetClosestCommonInclusiveAncestor();
  if (!commonAncestor) {
    NS_WARNING(
        "AbstractRange::GetClosestCommonInclusiveAncestor() returned nullptr");
    return EditActionResult(NS_ERROR_FAILURE);
  }
  return HTMLEditUtils::IsSimplyEditableNode(*commonAncestor)
             ? EditActionIgnored()
             : EditActionCanceled();
}

ListElementSelectionState::ListElementSelectionState(HTMLEditor& aHTMLEditor,
                                                     ErrorResult& aRv) {
  MOZ_ASSERT(!aRv.Failed());

  if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
    aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
    return;
  }

  // XXX Should we create another constructor which won't create
  //     AutoEditActionDataSetter?  Or should we create another
  //     AutoEditActionDataSetter which won't nest edit action?
  EditorBase::AutoEditActionDataSetter editActionData(aHTMLEditor,
                                                      EditAction::eNotEditing);
  if (NS_WARN_IF(!editActionData.CanHandle())) {
    aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
    return;
  }

  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  nsresult rv = aHTMLEditor.CollectEditTargetNodesInExtendedSelectionRanges(
      arrayOfContents, EditSubAction::eCreateOrChangeList,
      HTMLEditor::CollectNonEditableNodes::No);
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges("
        "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
    aRv = EditorBase::ToGenericNSResult(rv);
    return;
  }

  // Examine list type for nodes in selection.
  for (const auto& content : arrayOfContents) {
    if (!content->IsElement()) {
      mIsOtherContentSelected = true;
    } else if (content->IsHTMLElement(nsGkAtoms::ul)) {
      mIsULElementSelected = true;
    } else if (content->IsHTMLElement(nsGkAtoms::ol)) {
      mIsOLElementSelected = true;
    } else if (content->IsHTMLElement(nsGkAtoms::li)) {
      if (Element* parent = content->GetParentElement()) {
        if (parent->IsHTMLElement(nsGkAtoms::ul)) {
          mIsULElementSelected = true;
        } else if (parent->IsHTMLElement(nsGkAtoms::ol)) {
          mIsOLElementSelected = true;
        }
      }
    } else if (content->IsAnyOfHTMLElements(nsGkAtoms::dl, nsGkAtoms::dt,
                                            nsGkAtoms::dd)) {
      mIsDLElementSelected = true;
    } else {
      mIsOtherContentSelected = true;
    }

    if (mIsULElementSelected && mIsOLElementSelected && mIsDLElementSelected &&
        mIsOtherContentSelected) {
      break;
    }
  }
}

ListItemElementSelectionState::ListItemElementSelectionState(
    HTMLEditor& aHTMLEditor, ErrorResult& aRv) {
  MOZ_ASSERT(!aRv.Failed());

  if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
    aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
    return;
  }

  // XXX Should we create another constructor which won't create
  //     AutoEditActionDataSetter?  Or should we create another
  //     AutoEditActionDataSetter which won't nest edit action?
  EditorBase::AutoEditActionDataSetter editActionData(aHTMLEditor,
                                                      EditAction::eNotEditing);
  if (NS_WARN_IF(!editActionData.CanHandle())) {
    aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
    return;
  }

  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  nsresult rv = aHTMLEditor.CollectEditTargetNodesInExtendedSelectionRanges(
      arrayOfContents, EditSubAction::eCreateOrChangeList,
      HTMLEditor::CollectNonEditableNodes::No);
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges("
        "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
    aRv = EditorBase::ToGenericNSResult(rv);
    return;
  }

  // examine list type for nodes in selection
  for (const auto& content : arrayOfContents) {
    if (!content->IsElement()) {
      mIsOtherElementSelected = true;
    } else if (content->IsAnyOfHTMLElements(nsGkAtoms::ul, nsGkAtoms::ol,
                                            nsGkAtoms::li)) {
      mIsLIElementSelected = true;
    } else if (content->IsHTMLElement(nsGkAtoms::dt)) {
      mIsDTElementSelected = true;
    } else if (content->IsHTMLElement(nsGkAtoms::dd)) {
      mIsDDElementSelected = true;
    } else if (content->IsHTMLElement(nsGkAtoms::dl)) {
      if (mIsDTElementSelected && mIsDDElementSelected) {
        continue;
      }
      // need to look inside dl and see which types of items it has
      DefinitionListItemScanner scanner(*content->AsElement());
      mIsDTElementSelected |= scanner.DTElementFound();
      mIsDDElementSelected |= scanner.DDElementFound();
    } else {
      mIsOtherElementSelected = true;
    }

    if (mIsLIElementSelected && mIsDTElementSelected && mIsDDElementSelected &&
        mIsOtherElementSelected) {
      break;
    }
  }
}

AlignStateAtSelection::AlignStateAtSelection(HTMLEditor& aHTMLEditor,
                                             ErrorResult& aRv) {
  MOZ_ASSERT(!aRv.Failed());

  if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
    aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
    return;
  }

  // XXX Should we create another constructor which won't create
  //     AutoEditActionDataSetter?  Or should we create another
  //     AutoEditActionDataSetter which won't nest edit action?
  EditorBase::AutoEditActionDataSetter editActionData(aHTMLEditor,
                                                      EditAction::eNotEditing);
  if (NS_WARN_IF(!editActionData.CanHandle())) {
    aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
    return;
  }

  if (aHTMLEditor.IsSelectionRangeContainerNotContent()) {
    NS_WARNING("Some selection containers are not content node, but ignored");
    return;
  }

  // For now, just return first alignment.  We don't check if it's mixed.
  // This is for efficiency given that our current UI doesn't care if it's
  // mixed.
  // cmanske: NOT TRUE! We would like to pay attention to mixed state in
  // [Format] -> [Align] submenu!

  // This routine assumes that alignment is done ONLY by `<div>` elements
  // if aHTMLEditor is not in CSS mode.

  if (NS_WARN_IF(!aHTMLEditor.GetRoot())) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  OwningNonNull<Element> bodyOrDocumentElement = *aHTMLEditor.GetRoot();
  EditorRawDOMPoint atBodyOrDocumentElement(bodyOrDocumentElement);

  const nsRange* firstRange = aHTMLEditor.SelectionRefPtr()->GetRangeAt(0);
  mFoundSelectionRanges = !!firstRange;
  if (!mFoundSelectionRanges) {
    NS_WARNING("There was no selection range");
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }
  EditorRawDOMPoint atStartOfSelection(firstRange->StartRef());
  if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }
  MOZ_ASSERT(atStartOfSelection.IsSetAndValid());

  nsIContent* editTargetContent = nullptr;
  // If selection is collapsed or in a text node, take the container.
  if (aHTMLEditor.SelectionRefPtr()->IsCollapsed() ||
      atStartOfSelection.IsInTextNode()) {
    editTargetContent = atStartOfSelection.GetContainerAsContent();
    if (NS_WARN_IF(!editTargetContent)) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
  }
  // If selection container is the `<body>` element which is set to
  // `HTMLDocument.body`, take first editable node in it.
  // XXX Why don't we just compare `atStartOfSelection.GetChild()` and
  //     `bodyOrDocumentElement`?  Then, we can avoid computing the
  //     offset.
  else if (atStartOfSelection.IsContainerHTMLElement(nsGkAtoms::html) &&
           atBodyOrDocumentElement.IsSet() &&
           atStartOfSelection.Offset() == atBodyOrDocumentElement.Offset()) {
    editTargetContent = aHTMLEditor.GetNextEditableNode(atStartOfSelection);
    if (NS_WARN_IF(!editTargetContent)) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
  }
  // Otherwise, use first selected node.
  // XXX Only for retreiving it, the following block treats all selected
  //     ranges.  `HTMLEditor` should have
  //     `GetFirstSelectionRangeExtendedToHardLineStartAndEnd()`.
  else {
    AutoTArray<RefPtr<nsRange>, 4> arrayOfRanges;
    aHTMLEditor.GetSelectionRangesExtendedToHardLineStartAndEnd(
        arrayOfRanges, EditSubAction::eSetOrClearAlignment);

    AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
    nsresult rv = aHTMLEditor.CollectEditTargetNodes(
        arrayOfRanges, arrayOfContents, EditSubAction::eSetOrClearAlignment,
        HTMLEditor::CollectNonEditableNodes::Yes);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::CollectEditTargetNodes(eSetOrClearAlignment, "
          "CollectNonEditableNodes::Yes) failed");
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
    if (arrayOfContents.IsEmpty()) {
      NS_WARNING(
          "HTMLEditor::CollectEditTargetNodes(eSetOrClearAlignment, "
          "CollectNonEditableNodes::Yes) returned no contents");
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
    editTargetContent = arrayOfContents[0];
  }

  RefPtr<Element> blockElementAtEditTarget =
      HTMLEditUtils::GetInclusiveAncestorBlockElement(*editTargetContent);
  if (NS_WARN_IF(!blockElementAtEditTarget)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  if (aHTMLEditor.IsCSSEnabled() &&
      CSSEditUtils::IsCSSEditableProperty(blockElementAtEditTarget, nullptr,
                                          nsGkAtoms::align)) {
    // We are in CSS mode and we know how to align this element with CSS
    nsAutoString value;
    // Let's get the value(s) of text-align or margin-left/margin-right
    DebugOnly<nsresult> rvIgnored =
        CSSEditUtils::GetComputedCSSEquivalentToHTMLInlineStyleSet(
            *blockElementAtEditTarget, nullptr, nsGkAtoms::align, value);
    if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
      aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
      return;
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rvIgnored),
        "CSSEditUtils::GetComputedCSSEquivalentToHTMLInlineStyleSet(nsGkAtoms::"
        "align, "
        "eComputed) failed, but ignored");
    if (value.EqualsLiteral("center") || value.EqualsLiteral("-moz-center") ||
        value.EqualsLiteral("auto auto")) {
      mFirstAlign = nsIHTMLEditor::eCenter;
      return;
    }
    if (value.EqualsLiteral("right") || value.EqualsLiteral("-moz-right") ||
        value.EqualsLiteral("auto 0px")) {
      mFirstAlign = nsIHTMLEditor::eRight;
      return;
    }
    if (value.EqualsLiteral("justify")) {
      mFirstAlign = nsIHTMLEditor::eJustify;
      return;
    }
    // XXX In RTL document, is this expected?
    mFirstAlign = nsIHTMLEditor::eLeft;
    return;
  }

  for (nsIContent* containerContent :
       editTargetContent->InclusiveAncestorsOfType<nsIContent>()) {
    // If the node is a parent `<table>` element of edit target, let's break
    // here to materialize the 'inline-block' behaviour of html tables
    // regarding to text alignment.
    if (containerContent != editTargetContent &&
        containerContent->IsHTMLElement(nsGkAtoms::table)) {
      return;
    }

    if (CSSEditUtils::IsCSSEditableProperty(containerContent, nullptr,
                                            nsGkAtoms::align)) {
      nsAutoString value;
      DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetSpecifiedProperty(
          *containerContent, *nsGkAtoms::textAlign, value);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                           "CSSEditUtils::GetSpecifiedProperty(nsGkAtoms::"
                           "textAlign) failed, but ignored");
      if (!value.IsEmpty()) {
        if (value.EqualsLiteral("center")) {
          mFirstAlign = nsIHTMLEditor::eCenter;
          return;
        }
        if (value.EqualsLiteral("right")) {
          mFirstAlign = nsIHTMLEditor::eRight;
          return;
        }
        if (value.EqualsLiteral("justify")) {
          mFirstAlign = nsIHTMLEditor::eJustify;
          return;
        }
        if (value.EqualsLiteral("left")) {
          mFirstAlign = nsIHTMLEditor::eLeft;
          return;
        }
        // XXX
        // text-align: start and end aren't supported yet
      }
    }

    if (!HTMLEditUtils::SupportsAlignAttr(*containerContent)) {
      continue;
    }

    nsAutoString alignAttributeValue;
    containerContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::align,
                                           alignAttributeValue);
    if (alignAttributeValue.IsEmpty()) {
      continue;
    }

    if (alignAttributeValue.LowerCaseEqualsASCII("center")) {
      mFirstAlign = nsIHTMLEditor::eCenter;
      return;
    }
    if (alignAttributeValue.LowerCaseEqualsASCII("right")) {
      mFirstAlign = nsIHTMLEditor::eRight;
      return;
    }
    // XXX This is odd case.  `<div align="justify">` is not in any standards.
    if (alignAttributeValue.LowerCaseEqualsASCII("justify")) {
      mFirstAlign = nsIHTMLEditor::eJustify;
      return;
    }
    // XXX In RTL document, is this expected?
    mFirstAlign = nsIHTMLEditor::eLeft;
    return;
  }
}

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;
}

ParagraphStateAtSelection::ParagraphStateAtSelection(HTMLEditor& aHTMLEditor,
                                                     ErrorResult& aRv) {
  if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
    aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
    return;
  }

  // XXX Should we create another constructor which won't create
  //     AutoEditActionDataSetter?  Or should we create another
  //     AutoEditActionDataSetter which won't nest edit action?
  EditorBase::AutoEditActionDataSetter editActionData(aHTMLEditor,
                                                      EditAction::eNotEditing);
  if (NS_WARN_IF(!editActionData.CanHandle())) {
    aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
    return;
  }

  if (aHTMLEditor.IsSelectionRangeContainerNotContent()) {
    NS_WARNING("Some selection containers are not content node, but ignored");
    return;
  }

  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  nsresult rv =
      CollectEditableFormatNodesInSelection(aHTMLEditor, arrayOfContents);
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "ParagraphStateAtSelection::CollectEditableFormatNodesInSelection() "
        "failed");
    aRv.Throw(rv);
    return;
  }

  // We need to append descendant format block if block nodes are not format
  // block.  This is so we only have to look "up" the hierarchy to find
  // format nodes, instead of both up and down.
  for (int32_t i = arrayOfContents.Length() - 1; i >= 0; i--) {
    auto& content = arrayOfContents[i];
    nsAutoString format;
    if (HTMLEditUtils::IsBlockElement(content) &&
        !HTMLEditUtils::IsFormatNode(content)) {
      // XXX This RemoveObject() call has already been commented out and
      //     the above comment explained we're trying to replace non-format
      //     block nodes in the array.  According to the following blocks and
      //     `AppendDescendantFormatNodesAndFirstInlineNode()`, replacing
      //     non-format block with descendants format blocks makes sense.
      // arrayOfContents.RemoveObject(node);
      ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
          arrayOfContents, *content->AsElement());
    }
  }

  // We might have an empty node list.  if so, find selection parent
  // and put that on the list
  if (arrayOfContents.IsEmpty()) {
    EditorRawDOMPoint atCaret(
        EditorBase::GetStartPoint(*aHTMLEditor.SelectionRefPtr()));
    if (NS_WARN_IF(!atCaret.IsSet())) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
    nsIContent* content = atCaret.GetContainerAsContent();
    if (NS_WARN_IF(!content)) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
    arrayOfContents.AppendElement(*content);
  }

  Element* bodyOrDocumentElement = aHTMLEditor.GetRoot();
  if (NS_WARN_IF(!bodyOrDocumentElement)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  for (auto& content : Reversed(arrayOfContents)) {
    nsAtom* paragraphStateOfNode = nsGkAtoms::_empty;
    if (HTMLEditUtils::IsFormatNode(content)) {
      MOZ_ASSERT(content->NodeInfo()->NameAtom());
      paragraphStateOfNode = content->NodeInfo()->NameAtom();
    }
    // Ignore non-format block node since its children have been appended
    // the list above so that we'll handle this descendants later.
    else if (HTMLEditUtils::IsBlockElement(content)) {
      continue;
    }
    // If we meet an inline node, let's get its parent format.
    else {
      for (nsINode* parentNode = content->GetParentNode(); parentNode;
           parentNode = parentNode->GetParentNode()) {
        // If we reach `HTMLDocument.body` or `Document.documentElement`,
        // there is no format.
        if (parentNode == bodyOrDocumentElement) {
          break;
        }
        if (HTMLEditUtils::IsFormatNode(parentNode)) {
          MOZ_ASSERT(parentNode->NodeInfo()->NameAtom());
          paragraphStateOfNode = parentNode->NodeInfo()->NameAtom();
          break;
        }
      }
    }

    // if this is the first node, we've found, remember it as the format
    if (!mFirstParagraphState) {
      mFirstParagraphState = paragraphStateOfNode;
      continue;
    }
    // else make sure it matches previously found format
    if (mFirstParagraphState != paragraphStateOfNode) {
      mIsMixed = true;
      break;
    }
  }
}

// static
void ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
    nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
    Element& aNonFormatBlockElement) {
  MOZ_ASSERT(HTMLEditUtils::IsBlockElement(aNonFormatBlockElement));
  MOZ_ASSERT(!HTMLEditUtils::IsFormatNode(&aNonFormatBlockElement));

  // We only need to place any one inline inside this node onto
  // the list.  They are all the same for purposes of determining
  // paragraph style.  We use foundInline to track this as we are
  // going through the children in the loop below.
  bool foundInline = false;
  for (nsIContent* childContent = aNonFormatBlockElement.GetFirstChild();
       childContent; childContent = childContent->GetNextSibling()) {
    bool isBlock = HTMLEditUtils::IsBlockElement(*childContent);
    bool isFormat = HTMLEditUtils::IsFormatNode(childContent);
    // If the child is a non-format block element, let's check its children
    // recursively.
    if (isBlock && !isFormat) {
      ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
          aArrayOfContents, *childContent->AsElement());
      continue;
    }

    // If it's a format block, append it.
    if (isFormat) {
      aArrayOfContents.AppendElement(*childContent);
      continue;
    }

    MOZ_ASSERT(!isBlock);

    // If we haven't found inline node, append only this first inline node.
    // XXX I think that this makes sense if caller of this removes
    //     aNonFormatBlockElement from aArrayOfContents because the last loop
    //     of the constructor can check parent format block with
    //     aNonFormatBlockElement.
    if (!foundInline) {
      foundInline = true;
      aArrayOfContents.AppendElement(*childContent);
      continue;
    }
  }
}

// static
nsresult ParagraphStateAtSelection::CollectEditableFormatNodesInSelection(
    HTMLEditor& aHTMLEditor,
    nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents) {
  nsresult rv = aHTMLEditor.CollectEditTargetNodesInExtendedSelectionRanges(
      aArrayOfContents, EditSubAction::eCreateOrRemoveBlock,
      HTMLEditor::CollectNonEditableNodes::Yes);
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges("
        "eCreateOrRemoveBlock, CollectNonEditableNodes::Yes) failed");
    return rv;
  }

  // Pre-process our list of nodes
  for (int32_t i = aArrayOfContents.Length() - 1; i >= 0; i--) {
    OwningNonNull<nsIContent> content = aArrayOfContents[i];

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

    // Scan for table elements.  If we find table elements other than table,
    // replace it with a list of any editable non-table content.  Ditto for
    // list elements.
    if (HTMLEditUtils::IsTableElement(content) ||
        HTMLEditUtils::IsList(content) || HTMLEditUtils::IsListItem(content)) {
      aArrayOfContents.RemoveElementAt(i);
      aHTMLEditor.CollectChildren(content, aArrayOfContents, i,
                                  HTMLEditor::CollectListChildren::Yes,
                                  HTMLEditor::CollectTableChildren::Yes,
                                  HTMLEditor::CollectNonEditableNodes::Yes);
    }
  }
  return NS_OK;
}

nsresult HTMLEditor::EnsureCaretNotAfterPaddingBRElement() {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(SelectionRefPtr()->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 = SelectionRefPtr()->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());

  nsCOMPtr<nsIContent> previousEditableContent =
      GetPreviousEditableHTMLNode(atSelectionStart);
  if (!previousEditableContent ||
      !EditorUtils::IsPaddingBRElementForEmptyLastLine(
          *previousEditableContent)) {
    return NS_OK;
  }

  if (!atSelectionStart.IsInContentNode()) {
    return NS_OK;
  }

  RefPtr<Element> blockElementAtSelectionStart =
      HTMLEditUtils::GetInclusiveAncestorBlockElement(
          *atSelectionStart.ContainerAsContent());
  RefPtr<Element> parentBlockElementOfPreviousEditableContent =
      HTMLEditUtils::GetAncestorBlockElement(*previousEditableContent);

  if (!blockElementAtSelectionStart ||
      blockElementAtSelectionStart !=
          parentBlockElementOfPreviousEditableContent) {
    return NS_OK;
  }

  // If we are here then the selection is right after a padding <br>
  // element for empty last line that is in the same block as the
  // selection.  We need to move the selection start to be before the
  // padding <br> element.
  EditorRawDOMPoint atPreviousEditableContent(previousEditableContent);
  nsresult rv = CollapseSelectionTo(atPreviousEditableContent);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::CollapseSelectionTo() failed");
  return rv;
}

nsresult HTMLEditor::PrepareInlineStylesForCaret() {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
  MOZ_ASSERT(SelectionRefPtr()->IsCollapsed());

  // XXX This method works with the top level edit sub-action, but this
  //     must be wrong if we are handling nested edit action.

  if (TopLevelEditSubActionDataRef().mDidDeleteSelection) {
    switch (GetTopLevelEditSubAction()) {
      case EditSubAction::eInsertText:
      case EditSubAction::eInsertTextComingFromIME:
      case EditSubAction::eDeleteSelectedContent: {
        nsresult rv = ReapplyCachedStyles();
        if (NS_FAILED(rv)) {
          NS_WARNING("HTMLEditor::ReapplyCachedStyles() failed");
          return rv;
        }
        break;
      }
      default:
        break;
    }
  }
  // For most actions we want to clear the cached styles, but there are
  // exceptions
  if (!IsStyleCachePreservingSubAction(GetTopLevelEditSubAction())) {
    TopLevelEditSubActionDataRef().mCachedInlineStyles->Clear();
  }
  return NS_OK;
}

EditActionResult HTMLEditor::HandleInsertText(
    EditSubAction aEditSubAction, const nsAString& aInsertionString) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
  MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText ||
             aEditSubAction == EditSubAction::eInsertTextComingFromIME);

  EditActionResult result = CanHandleHTMLEditSubAction();
  if (result.Failed() || result.Canceled()) {
    NS_WARNING_ASSERTION(result.Succeeded(),
                         "HTMLEditor::CanHandleHTMLEditSubAction() failed");
    return result;
  }

  UndefineCaretBidiLevel();

  // If the selection isn't collapsed, delete it.  Don't delete existing inline
  // tags, because we're hopefully going to insert text (bug 787432).
  if (!SelectionRefPtr()->IsCollapsed()) {
    nsresult rv =
        DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "EditorBase::DeleteSelectionAsSubAction(nsIEditor::eNone, "
          "nsIEditor::eNoStrip) failed");
      return EditActionHandled(rv);
    }
  }

  nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
                       "failed, but ignored");

  if (NS_SUCCEEDED(rv) && SelectionRefPtr()->IsCollapsed()) {
    nsresult rv = EnsureCaretNotAfterPaddingBRElement();
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::EnsureCaretNotAfterPaddingBRElement() "
                         "failed, but ignored");
    if (NS_SUCCEEDED(rv)) {
      nsresult rv = PrepareInlineStylesForCaret();
      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
        return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
    }
  }

  RefPtr<Document> document = GetDocument();
  if (NS_WARN_IF(!document)) {
    return EditActionHandled(NS_ERROR_FAILURE);
  }

  RefPtr<const nsRange> firstRange = SelectionRefPtr()->GetRangeAt(0);
  if (NS_WARN_IF(!firstRange)) {
    return EditActionHandled(NS_ERROR_FAILURE);
  }

  // for every property that is set, insert a new inline style node
  // XXX CreateStyleForInsertText() adjusts selection automatically, but
  //     it should just return the insertion point instead.
  rv = CreateStyleForInsertText(*firstRange);
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed");
    return EditActionHandled(rv);
  }

  firstRange = SelectionRefPtr()->GetRangeAt(0);
  if (NS_WARN_IF(!firstRange)) {
    return EditActionHandled(NS_ERROR_FAILURE);
  }

  EditorDOMPoint pointToInsert(firstRange->StartRef());
  if (NS_WARN_IF(!pointToInsert.IsSet()) ||
      NS_WARN_IF(!pointToInsert.IsInContentNode())) {
    return EditActionHandled(NS_ERROR_FAILURE);
  }
  MOZ_ASSERT(pointToInsert.IsSetAndValid());

  // dont put text in places that can't have it
  if (!pointToInsert.IsInTextNode() &&
      !HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(),
                                     *nsGkAtoms::textTagName)) {
    NS_WARNING("Selection start container couldn't have text nodes");
    return EditActionHandled(NS_ERROR_FAILURE);
  }

  if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
    EditorRawDOMPoint compositionStartPoint = GetCompositionStartPoint();
    if (!compositionStartPoint.IsSet()) {
      compositionStartPoint = pointToInsert;
    }

    if (aInsertionString.IsEmpty()) {
      // Right now the WSRunObject code bails on empty strings, but IME needs
      // the InsertTextWithTransaction() call to still happen since empty
      // strings are meaningful there.
      nsresult rv = InsertTextWithTransaction(*document, aInsertionString,
                                              compositionStartPoint);
      if (NS_WARN_IF(Destroyed())) {
        return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "HTMLEditor::InsertTextWithTransaction() failed");
      return EditActionHandled(rv);
    }

    EditorRawDOMPoint compositionEndPoint = GetCompositionEndPoint();
    if (!compositionEndPoint.IsSet()) {
      compositionEndPoint = compositionStartPoint;
    }
    WSRunObject wsObj(*this, compositionStartPoint, compositionEndPoint);
    nsresult rv = wsObj.InsertText(*document, aInsertionString);
    if (NS_WARN_IF(Destroyed())) {
      return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("WSRunObject::InsertText() failed");
      return EditActionHandled(rv);
    }

    compositionStartPoint = GetCompositionStartPoint();
    compositionEndPoint = GetCompositionEndPoint();
    if (NS_WARN_IF(!compositionStartPoint.IsSet()) ||
        NS_WARN_IF(!compositionEndPoint.IsSet())) {
      // Mutation event listener has changed the DOM tree...
      return EditActionHandled();
    }
    rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
        compositionStartPoint.ToRawRangeBoundary(),
        compositionEndPoint.ToRawRangeBoundary());
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed");
    return EditActionHandled(rv);
  }

  MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText);

  // find where we are
  EditorDOMPoint currentPoint(pointToInsert);

  // is our text going to be PREformatted?
  // We remember this so that we know how to handle tabs.
  bool isPRE =
      EditorUtils::IsContentPreformatted(*pointToInsert.ContainerAsContent());

  // turn off the edit listener: we know how to
  // build the "doc changed range" ourselves, and it's
  // must faster to do it once here than to track all
  // the changes one at a time.
  AutoRestore<bool> disableListener(
      EditSubActionDataRef().mAdjustChangedRangeFromListener);
  EditSubActionDataRef().mAdjustChangedRangeFromListener = false;

  // don't change my selection in subtransactions
  AutoTransactionsConserveSelection dontChangeMySelection(*this);
  int32_t pos = 0;
  NS_NAMED_LITERAL_STRING(newlineStr, LFSTR);

  {
    AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert);

    // for efficiency, break out the pre case separately.  This is because
    // its a lot cheaper to search the input string for only newlines than
    // it is to search for both tabs and newlines.
    if (isPRE || IsPlaintextEditor()) {
      while (pos != -1 &&
             pos < static_cast<int32_t>(aInsertionString.Length())) {
        int32_t oldPos = pos;
        int32_t subStrLen;
        pos = aInsertionString.FindChar(nsCRT::LF, oldPos);

        if (pos != -1) {
          subStrLen = pos - oldPos;
          // if first char is newline, then use just it
          if (!subStrLen) {
            subStrLen = 1;
          }
        } else {
          subStrLen = aInsertionString.Length() - oldPos;
          pos = aInsertionString.Length();
        }

        nsDependentSubstring subStr(aInsertionString, oldPos, subStrLen);

        // is it a return?
        if (subStr.Equals(newlineStr)) {
          RefPtr<Element> brElement =
              InsertBRElementWithTransaction(currentPoint, nsIEditor::eNone);
          if (NS_WARN_IF(Destroyed())) {
            return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
          }
          if (!brElement) {
            NS_WARNING(
                "HTMLEditor::InsertBRElementWithTransaction(eNone) failed");
            return EditActionHandled(NS_ERROR_FAILURE);
          }
          pos++;
          if (brElement->GetNextSibling()) {
            pointToInsert.Set(brElement->GetNextSibling());
          } else {
            pointToInsert.SetToEndOf(currentPoint.GetContainer());
          }
          // XXX In most cases, pointToInsert and currentPoint are same here.
          //     But if the <br> element has been moved to different point by
          //     mutation observer, those points become different.
          currentPoint.SetAfter(brElement);
          NS_WARNING_ASSERTION(currentPoint.IsSet(),
                               "Failed to set after the <br> element");
          NS_WARNING_ASSERTION(currentPoint == pointToInsert,
                               "Perhaps, <br> element position has been moved "
                               "to different point "
                               "by mutation observer");
        } else {
          EditorRawDOMPoint pointAfterInsertedString;
          nsresult rv = InsertTextWithTransaction(
              *document, subStr, EditorRawDOMPoint(currentPoint),
              &pointAfterInsertedString);
          if (NS_WARN_IF(Destroyed())) {
            return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
          }
          if (NS_FAILED(rv)) {
            NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
            return EditActionHandled(rv);
          }
          currentPoint = pointAfterInsertedString;
          pointToInsert = pointAfterInsertedString;
        }
      }
    } else {
      NS_NAMED_LITERAL_STRING(tabStr, "\t");
      NS_NAMED_LITERAL_STRING(spacesStr, "    ");
      char specialChars[] = {TAB, nsCRT::LF, 0};
      nsAutoString insertionString(aInsertionString);  // For FindCharInSet().
      while (pos != -1 &&
             pos < static_cast<int32_t>(insertionString.Length())) {
        int32_t oldPos = pos;
        int32_t subStrLen;
        pos = insertionString.FindCharInSet(specialChars, oldPos);

        if (pos != -1) {
          subStrLen = pos - oldPos;
          // if first char is newline, then use just it
          if (!subStrLen) {
            subStrLen = 1;
          }
        } else {
          subStrLen = insertionString.Length() - oldPos;
          pos = insertionString.Length();
        }

        nsDependentSubstring subStr(insertionString, oldPos, subStrLen);
        WSRunObject wsObj(*this, currentPoint);

        // is it a tab?
        if (subStr.Equals(tabStr)) {
          EditorRawDOMPoint pointAfterInsertedSpaces;
          nsresult rv =
              wsObj.InsertText(*document, spacesStr, &pointAfterInsertedSpaces);
          if (NS_WARN_IF(Destroyed())) {
            return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
          }
          if (NS_FAILED(rv)) {
            NS_WARNING("WSRunObject::InsertText() failed");
            return EditActionHandled(rv);
          }
          pos++;
          MOZ_ASSERT(pointAfterInsertedSpaces.IsSet());
          currentPoint = pointAfterInsertedSpaces;
          pointToInsert = pointAfterInsertedSpaces;
        }
        // is it a return?
        else if (subStr.Equals(newlineStr)) {
          RefPtr<Element> newBRElement =
              wsObj.InsertBreak(MOZ_KnownLive(*SelectionRefPtr()), currentPoint,
                                nsIEditor::eNone);
          if (NS_WARN_IF(Destroyed())) {
            return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
          }
          if (!newBRElement) {
            NS_WARNING("WSRunObject::InsertBreak(eNone) failed");
            return EditActionHandled(NS_ERROR_FAILURE);
          }
          pos++;
          if (newBRElement->GetNextSibling()) {
            pointToInsert.Set(newBRElement->GetNextSibling());
          } else {
            pointToInsert.SetToEndOf(currentPoint.GetContainer());
          }
          currentPoint.SetAfter(newBRElement);
          NS_WARNING_ASSERTION(currentPoint.IsSet(),
                               "Failed to set after the new <br> element");
          // XXX If the newBRElement has been moved or removed by mutation
          //     observer, we hit this assert.  We need to check if
          //     newBRElement is in expected point, though, we must have
          //     a lot of same bugs...
          NS_WARNING_ASSERTION(
              currentPoint == pointToInsert,
              "Perhaps, newBRElement has been moved or removed unexpectedly");
        } else {
          EditorRawDOMPoint pointAfterInsertedString;
          nsresult rv =
              wsObj.InsertText(*document, subStr, &pointAfterInsertedString);
          if (NS_WARN_IF(Destroyed())) {
            return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
          }
          if (NS_FAILED(rv)) {
            NS_WARNING("WSRunObject::InsertText() failed");
            return EditActionHandled(rv);
          }
          MOZ_ASSERT(pointAfterInsertedString.IsSet());
          currentPoint = pointAfterInsertedString;
          pointToInsert = pointAfterInsertedString;
        }
      }
    }

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

  IgnoredErrorResult ignoredError;
  SelectionRefPtr()->SetInterlinePosition(false, ignoredError);
  NS_WARNING_ASSERTION(!ignoredError.Failed(),
                       "Failed to unset interline position");

  if (currentPoint.IsSet()) {
    nsresult rv = CollapseSelectionTo(currentPoint);
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "Selection::Collapse() failed, but ignored");
  }

  // manually update the doc changed range so that AfterEdit will clean up
  // the correct portion of the document.
  if (currentPoint.IsSet()) {
    nsresult rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
        pointToInsert.ToRawRangeBoundary(), currentPoint.ToRawRangeBoundary());
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::SetStartAndEnd() failed");
    return EditActionHandled(rv);
  }

  rv = TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(pointToInsert);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsRange::CollapseTo() failed");
  return EditActionHandled(rv);
}

EditActionResult HTMLEditor::InsertParagraphSeparatorAsSubAction() {
  if (NS_WARN_IF(!mInitSucceeded)) {
    return EditActionIgnored(NS_ERROR_NOT_INITIALIZED);
  }

  EditActionResult result = CanHandleHTMLEditSubAction();
  if (result.Failed() || result.Canceled()) {
    NS_WARNING_ASSERTION(result.Succeeded(),
                         "HTMLEditor::CanHandleHTMLEditSubAction() failed");
    return result;
  }

  // XXX This may be called by execCommand() with "insertParagraph".
  //     In such case, naming the transaction "TypingTxnName" is odd.
  AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName);

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

  UndefineCaretBidiLevel();

  // If the selection isn't collapsed, delete it.
  if (!SelectionRefPtr()->IsCollapsed()) {
    nsresult rv =
        DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed");
      return EditActionIgnored(rv);
    }
  }

  nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
                       "failed, but ignored");

  if (NS_SUCCEEDED(rv) && SelectionRefPtr()->IsCollapsed()) {
    nsresult rv = EnsureCaretNotAfterPaddingBRElement();
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::EnsureCaretNotAfterPaddingBRElement() "
                         "failed, but ignored");
    if (NS_SUCCEEDED(rv)) {
      nsresult rv = PrepareInlineStylesForCaret();
      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
        return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
    }
  }

  // Split any mailcites in the way.  Should we abort this if we encounter
  // table cell boundaries?
  if (IsMailEditor()) {
    EditorDOMPoint pointToSplit(EditorBase::GetStartPoint(*SelectionRefPtr()));
    if (NS_WARN_IF(!pointToSplit.IsSet())) {
      return EditActionIgnored(NS_ERROR_FAILURE);
    }

    EditActionResult result = SplitMailCiteElements(pointToSplit);
    if (result.Failed()) {
      NS_WARNING("HTMLEditor::SplitMailCiteElements() failed");
      return result;
    }
    if (result.Handled()) {
      return result;
    }
  }

  // Smart splitting rules
  const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
  if (NS_WARN_IF(!firstRange)) {
    return EditActionIgnored(NS_ERROR_FAILURE);
  }

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

  // Do nothing if the node is read-only
  if (!HTMLEditUtils::IsSimplyEditableNode(
          *atStartOfSelection.GetContainer())) {
    return EditActionCanceled();
  }

  // If the active editing host is an inline element, or if the active editing
  // host is the block parent itself and we're configured to use <br> as a
  // paragraph separator, just append a <br>.
  RefPtr<Element> editingHost = GetActiveEditingHost();
  if (NS_WARN_IF(!editingHost)) {
    return EditActionIgnored(NS_ERROR_FAILURE);
  }

  // Look for the nearest parent block.  However, don't return error even if
  // there is no block parent here because in such case, i.e., editing host
  // is an inline element, we should insert <br> simply.
  RefPtr<Element> blockElement =
      atStartOfSelection.IsInContentNode()
          ? HTMLEditUtils::GetInclusiveAncestorBlockElement(
                *atStartOfSelection.ContainerAsContent(), editingHost)
          : nullptr;

  ParagraphSeparator separator = GetDefaultParagraphSeparator();
  bool insertBRElement;
  // If there is no block parent in the editing host, i.e., the editing host
  // itself is also a non-block element, we should insert a <br> element.
  if (!blockElement) {
    // XXX Chromium checks if the CSS box of the editing host is a block.
    insertBRElement = true;
  }
  // If only the editing host is block, and the default paragraph separator
  // is <br> or the editing host cannot contain a <p> element, we should
  // insert a <br> element.
  else if (editingHost == blockElement) {
    insertBRElement = separator == ParagraphSeparator::br ||
                      !HTMLEditUtils::CanElementContainParagraph(*editingHost);
  }
  // If the nearest block parent is a single-line container declared in
  // the execCommand spec and not the editing host, we should separate the
  // block even if the default paragraph separator is <br> element.
  else if (HTMLEditUtils::IsSingleLineContainer(*blockElement)) {
    insertBRElement = false;
  }
  // Otherwise, unless there is no block ancestor which can contain <p>
  // element, we shouldn't insert a <br> element here.
  else {
    insertBRElement = true;
    for (Element* blockAncestor = blockElement;
         blockAncestor && insertBRElement;
         blockAncestor = HTMLEditUtils::GetAncestorBlockElement(*blockAncestor,
                                                                editingHost)) {
      insertBRElement =
          !HTMLEditUtils::CanElementContainParagraph(*blockAncestor);
    }
  }

  // If we cannot insert a <p>/<div> element at the selection, we should insert
  // a <br> element instead.
  if (insertBRElement) {
    nsresult rv = InsertBRElement(atStartOfSelection);
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::InsertBRElement() failed");
      return EditActionIgnored(rv);
    }
    return EditActionHandled();
  }

  if (editingHost == blockElement && separator != ParagraphSeparator::br) {
    // Insert a new block first
    MOZ_ASSERT(separator == ParagraphSeparator::div ||
               separator == ParagraphSeparator::p);
    // FormatBlockContainerWithTransaction() creates AutoSelectionRestorer.
    // Therefore, even if it returns NS_OK, editor might have been destroyed
    // at restoring Selection.
    nsresult rv = FormatBlockContainerWithTransaction(
        MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator)));
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED) ||
        NS_WARN_IF(Destroyed())) {
      return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
    }
    // We warn on failure, but don't handle it, because it might be harmless.
    // Instead we just check that a new block was actually created.
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::FormatBlockContainerWithTransaction() "
                         "failed, but ignored");

    firstRange = SelectionRefPtr()->GetRangeAt(0);
    if (NS_WARN_IF(!firstRange)) {
      return EditActionIgnored(NS_ERROR_FAILURE);
    }

    atStartOfSelection = firstRange->StartRef();
    if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
      return EditActionIgnored(NS_ERROR_FAILURE);
    }
    MOZ_ASSERT(atStartOfSelection.IsSetAndValid());

    blockElement =
        atStartOfSelection.IsInContentNode()
            ? HTMLEditUtils::GetInclusiveAncestorBlockElement(
                  *atStartOfSelection.ContainerAsContent(), editingHost)
            : nullptr;
    if (NS_WARN_IF(!blockElement)) {
      return EditActionIgnored(NS_ERROR_UNEXPECTED);
    }
    if (NS_WARN_IF(blockElement == editingHost)) {
      // Didn't create a new block for some reason, fall back to <br>
      rv = InsertBRElement(atStartOfSelection);
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::InsertBRElement() failed");
        return EditActionIgnored(rv);
      }
      return EditActionHandled();
    }
    // Now, mNewBlockElement is last created block element for wrapping inline
    // elements around the caret position and AfterEditInner() will move
    // caret into it.  However, it may be different from block parent of
    // the caret position.  E.g., FormatBlockContainerWithTransaction() may
    // wrap following inline elements of a <br> element which is next sibling
    // of container of the caret.  So, we need to adjust mNewBlockElement here
    // for avoiding jumping caret to odd position.
    TopLevelEditSubActionDataRef().mNewBlockElement = blockElement;
  }

  // If block is empty, populate with br.  (For example, imagine a div that
  // contains the word "text".  The user selects "text" and types return.
  // "Text" is deleted leaving an empty block.  We want to put in one br to
  // make block have a line.  Then code further below will put in a second br.)
  if (IsEmptyBlockElement(*blockElement, IgnoreSingleBR::No)) {
    AutoEditorDOMPointChildInvalidator lockOffset(atStartOfSelection);
    EditorDOMPoint endOfBlockParent;
    endOfBlockParent.SetToEndOf(blockElement);
    RefPtr<Element> brElement =
        InsertBRElementWithTransaction(endOfBlockParent);
    if (NS_WARN_IF(Destroyed())) {
      return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
    }
    if (!brElement) {
      NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed");
      return EditActionIgnored(NS_ERROR_FAILURE);
    }
  }

  RefPtr<Element> listItem = GetNearestAncestorListItemElement(*blockElement);
  if (listItem && listItem != editingHost) {
    nsresult rv = HandleInsertParagraphInListItemElement(
        *listItem, MOZ_KnownLive(*atStartOfSelection.GetContainer()),
        atStartOfSelection.Offset());
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::HandleInsertParagraphInListItemElement() "
                         "failed, but ignored");
    return EditActionHandled();
  }

  if (HTMLEditUtils::IsHeader(*blockElement)) {
    // Headers: close (or split) header
    nsresult rv = HandleInsertParagraphInHeadingElement(
        *blockElement, MOZ_KnownLive(*atStartOfSelection.GetContainer()),
        atStartOfSelection.Offset());
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::HandleInsertParagraphInHeadingElement() "
                         "failed, but ignored");
    return EditActionHandled();
  }

  // XXX Ideally, we should take same behavior with both <p> container and
  //     <div> container.  However, we are still using <br> as default
  //     paragraph separator (non-standard) and we've split only <p> container
  //     long time.  Therefore, some web apps may depend on this behavior like
  //     Gmail.  So, let's use traditional odd behavior only when the default
  //     paragraph separator is <br>.  Otherwise, take consistent behavior
  //     between <p> container and <div> container.
  if ((separator == ParagraphSeparator::br &&
       blockElement->IsHTMLElement(nsGkAtoms::p)) ||
      (separator != ParagraphSeparator::br &&
       blockElement->IsAnyOfHTMLElements(nsGkAtoms::p, nsGkAtoms::div))) {
    AutoEditorDOMPointChildInvalidator lockOffset(atStartOfSelection);
    // Paragraphs: special rules to look for <br>s
    EditActionResult result = HandleInsertParagraphInParagraph(*blockElement);
    if (result.Failed()) {
      NS_WARNING("HTMLEditor::HandleInsertParagraphInParagraph() failed");
      return result;
    }
    if (result.Handled()) {
      // Now, atStartOfSelection may be invalid because the left paragraph
      // may have less children than its offset.  For avoiding warnings of
      // validation of EditorDOMPoint, we should not touch it anymore.
      lockOffset.Cancel();
      return result;
    }
    // Fall through, if HandleInsertParagraphInParagraph() didn't handle it.
    MOZ_ASSERT(!result.Canceled(),
               "HandleInsertParagraphInParagraph() canceled this edit action, "
               "InsertParagraphSeparatorAsSubAction() needs to handle this "
               "action instead");
  }

  // If nobody handles this edit action, let's insert new <br> at the selection.
  rv = InsertBRElement(atStartOfSelection);
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::InsertBRElement() failed");
    return EditActionIgnored(rv);
  }
  return EditActionHandled();
}

nsresult HTMLEditor::InsertBRElement(const EditorDOMPoint& aPointToBreak) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  if (NS_WARN_IF(!aPointToBreak.IsSet())) {
    return NS_ERROR_INVALID_ARG;
  }

  bool brElementIsAfterBlock = false, brElementIsBeforeBlock = false;

  // First, insert a <br> element.
  RefPtr<Element> brElement;
  if (IsPlaintextEditor()) {
    brElement = InsertBRElementWithTransaction(aPointToBreak);
    if (NS_WARN_IF(Destroyed())) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    if (!brElement) {
      NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed");
      return NS_ERROR_FAILURE;
    }
  } else {
    EditorDOMPoint pointToBreak(aPointToBreak);
    WSRunObject wsObj(*this, pointToBreak);
    brElementIsAfterBlock =
        wsObj.ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToBreak)
            .ReachedBlockBoundary();
    brElementIsBeforeBlock =
        wsObj.ScanNextVisibleNodeOrBlockBoundaryFrom(pointToBreak)
            .ReachedBlockBoundary();
    // If the container of the break is a link, we need to split it and
    // insert new <br> between the split links.
    RefPtr<Element> linkNode =
        HTMLEditor::GetLinkElement(pointToBreak.GetContainer());
    if (linkNode) {
      SplitNodeResult splitLinkNodeResult = SplitNodeDeepWithTransaction(
          *linkNode, pointToBreak, SplitAtEdges::eDoNotCreateEmptyContainer);
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (splitLinkNodeResult.Failed()) {
        NS_WARNING(
            "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
            "eDoNotCreateEmptyContainer) failed");
        return splitLinkNodeResult.Rv();
      }
      pointToBreak = splitLinkNodeResult.SplitPoint();
    }
    brElement = wsObj.InsertBreak(MOZ_KnownLive(*SelectionRefPtr()),
                                  pointToBreak, nsIEditor::eNone);
    if (NS_WARN_IF(Destroyed())) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    if (!brElement) {
      NS_WARNING("WSRunObject::InsertBreak(eNone) failed");
      return NS_ERROR_FAILURE;
    }
  }

  // If the <br> element has already been removed from the DOM tree by a
  // mutation event listener, don't continue handling this.
  if (NS_WARN_IF(!brElement->GetParentNode())) {
    return NS_ERROR_FAILURE;
  }

  if (brElementIsAfterBlock && brElementIsBeforeBlock) {
    // We just placed a <br> between block boundaries.  This is the one case
    // where we want the selection to be before the br we just placed, as the
    // br will be on a new line, rather than at end of prior line.
    // XXX brElementIsAfterBlock and brElementIsBeforeBlock were set before
    //     modifying the DOM tree.  So, now, the <br> element may not be
    //     between blocks.
    IgnoredErrorResult ignoredError;
    SelectionRefPtr()->SetInterlinePosition(true, ignoredError);
    NS_WARNING_ASSERTION(
        !ignoredError.Failed(),
        "Selection::SetInterlinePosition(true) failed, but ignored");
    nsresult rv = CollapseSelectionTo(EditorRawDOMPoint(brElement));
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::CollapseSelectionTo() failed");
    return rv;
  }

  EditorDOMPoint afterBRElement(brElement);
  DebugOnly<bool> advanced = afterBRElement.AdvanceOffset();
  NS_WARNING_ASSERTION(advanced,
                       "Failed to advance offset after the new <br> element");
  WSScanResult forwardScanFromAfterBRElementResult =
      WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(*this, afterBRElement);
  if (forwardScanFromAfterBRElementResult.ReachedBRElement()) {
    // The next thing after the break we inserted is another break.  Move the
    // second break to be the first break's sibling.  This will prevent them
    // from being in different inline nodes, which would break
    // SetInterlinePosition().  It will also assure that if the user clicks
    // away and then clicks back on their new blank line, they will still get
    // the style from the line above.
    if (brElement->GetNextSibling() !=
        forwardScanFromAfterBRElementResult.BRElementPtr()) {
      MOZ_ASSERT(forwardScanFromAfterBRElementResult.BRElementPtr());
      nsresult rv = MoveNodeWithTransaction(
          MOZ_KnownLive(*forwardScanFromAfterBRElementResult.BRElementPtr()),
          afterBRElement);
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
        return rv;
      }
    }
  }

  // SetInterlinePosition(true) means we want the caret to stick to the
  // content on the "right".  We want the caret to stick to whatever is past
  // the break.  This is because the break is on the same line we were on,
  // but the next content will be on the following line.

  // An exception to this is if the break has a next sibling that is a block
  // node.  Then we stick to the left to avoid an uber caret.
  nsIContent* nextSiblingOfBRElement = brElement->GetNextSibling();
  IgnoredErrorResult ignoredError;
  SelectionRefPtr()->SetInterlinePosition(
      !(nextSiblingOfBRElement &&
        HTMLEditUtils::IsBlockElement(*nextSiblingOfBRElement)),
      ignoredError);
  NS_WARNING_ASSERTION(!ignoredError.Failed(),
                       "Selection::SetInterlinePosition() failed, but ignored");
  nsresult rv = CollapseSelectionTo(afterBRElement);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::CollapseSelectionTo() failed");
  return rv;
}

EditActionResult HTMLEditor::SplitMailCiteElements(
    const EditorDOMPoint& aPointToSplit) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aPointToSplit.IsSet());

  RefPtr<Element> citeNode =
      GetMostAncestorMailCiteElement(*aPointToSplit.GetContainer());
  if (!citeNode) {
    return EditActionIgnored();
  }

  EditorDOMPoint pointToSplit(aPointToSplit);

  // If our selection is just before a break, nudge it to be just after it.
  // This does two things for us.  It saves us the trouble of having to add
  // a break here ourselves to preserve the "blockness" of the inline span
  // mailquote (in the inline case), and :
  // it means the break won't end up making an empty line that happens to be
  // inside a mailquote (in either inline or block case).
  // The latter can confuse a user if they click there and start typing,
  // because being in the mailquote may affect wrapping behavior, or font
  // color, etc.
  WSScanResult forwardScanFromPointToSplitResult =
      WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(*this, pointToSplit);
  // If selection start point is before a break and it's inside the mailquote,
  // let's split it after the visible node.
  if (forwardScanFromPointToSplitResult.ReachedBRElement() &&
      forwardScanFromPointToSplitResult.BRElementPtr() != citeNode &&
      citeNode->Contains(forwardScanFromPointToSplitResult.BRElementPtr())) {
    pointToSplit = forwardScanFromPointToSplitResult.PointAfterContent();
  }

  if (NS_WARN_IF(!pointToSplit.GetContainerAsContent())) {
    return EditActionIgnored(NS_ERROR_FAILURE);
  }

  SplitNodeResult splitCiteNodeResult = SplitNodeDeepWithTransaction(
      *citeNode, pointToSplit, SplitAtEdges::eDoNotCreateEmptyContainer);
  if (NS_WARN_IF(Destroyed())) {
    return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
  }
  if (splitCiteNodeResult.Failed()) {
    NS_WARNING(
        "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
        "eDoNotCreateEmptyContainer) failed");
    return EditActionIgnored(splitCiteNodeResult.Rv());
  }
  pointToSplit.Clear();

  // Add an invisible <br> to the end of current cite node (If new left cite
  // has not been created, we're at the end of it.  Otherwise, we're still at
  // the right node) if it was a <span> of style="display: block". This is
  // important, since when serializing the cite to plain text, the span which
  // caused the visual break is discarded.  So the added <br> will guarantee
  // that the serializer will insert a break where the user saw one.
  // FYI: splitCiteNodeResult grabs the previous node with nsCOMPtr.  So, it's
  //      safe to access previousNodeOfSplitPoint even after changing the DOM
  //      tree and/or selection even though it's raw pointer.
  nsIContent* previousNodeOfSplitPoint = splitCiteNodeResult.GetPreviousNode();
  if (previousNodeOfSplitPoint &&
      previousNodeOfSplitPoint->IsHTMLElement(nsGkAtoms::span) &&
      previousNodeOfSplitPoint->GetPrimaryFrame() &&
      previousNodeOfSplitPoint->GetPrimaryFrame()->IsBlockFrameOrSubclass()) {
    nsCOMPtr<nsINode> lastChild = previousNodeOfSplitPoint->GetLastChild();
    if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) {
      // We ignore the result here.
      EditorDOMPoint endOfPreviousNodeOfSplitPoint;
      endOfPreviousNodeOfSplitPoint.SetToEndOf(previousNodeOfSplitPoint);
      RefPtr<Element> invisibleBRElement =
          InsertBRElementWithTransaction(endOfPreviousNodeOfSplitPoint);
      if (NS_WARN_IF(Destroyed())) {
        return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          invisibleBRElement,
          "HTMLEditor::InsertBRElementWithTransaction() failed, but ignored");
    }
  }

  // In most cases, <br> should be inserted after current cite.  However, if
  // left cite hasn't been created because the split point was start of the
  // cite node, <br> should be inserted before the current cite.
  EditorDOMPoint pointToInsertBRNode(splitCiteNodeResult.SplitPoint());
  RefPtr<Element> brElement =
      InsertBRElementWithTransaction(pointToInsertBRNode);
  if (NS_WARN_IF(Destroyed())) {
    return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
  }
  if (!brElement) {
    NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed");
    return EditActionIgnored(NS_ERROR_FAILURE);
  }
  // Now, offset of pointToInsertBRNode is invalid.  Let's clear it.
  pointToInsertBRNode.Clear();

  // Want selection before the break, and on same line.
  EditorDOMPoint atBRElement(brElement);
  {
    AutoEditorDOMPointChildInvalidator lockOffset(atBRElement);
    IgnoredErrorResult ignoredError;
    SelectionRefPtr()->SetInterlinePosition(true, ignoredError);
    NS_WARNING_ASSERTION(
        !ignoredError.Failed(),
        "Selection::SetInterlinePosition(true) failed, but ignored");
    nsresult rv = CollapseSelectionTo(atBRElement);
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::CollapseSelectionTo() failed");
      return EditActionIgnored(rv);
    }
  }

  // if citeNode wasn't a block, we might also want another break before it.
  // We need to examine the content both before the br we just added and also
  // just after it.  If we don't have another br or block boundary adjacent,
  // then we will need a 2nd br added to achieve blank line that user expects.
  if (HTMLEditUtils::IsInlineElement(*citeNode)) {
    // Use DOM point which we tried to collapse to.
    EditorDOMPoint pointToCreateNewBRElement(atBRElement);

    WSScanResult backwardScanFromPointToCreateNewBRElementResult =
        WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
            *this, pointToCreateNewBRElement);
    if (backwardScanFromPointToCreateNewBRElementResult
            .InNormalWhiteSpacesOrText() ||
        backwardScanFromPointToCreateNewBRElementResult
            .ReachedSpecialContent()) {
      EditorRawDOMPoint pointAfterNewBRElement(
          EditorRawDOMPoint::After(pointToCreateNewBRElement));
      NS_WARNING_ASSERTION(pointAfterNewBRElement.IsSet(),
                           "Failed to set to after the <br> node");
      WSScanResult forwardScanFromPointAfterNewBRElementResult =
          WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
              *this, pointAfterNewBRElement);
      if (forwardScanFromPointAfterNewBRElementResult
              .InNormalWhiteSpacesOrText() ||
          forwardScanFromPointAfterNewBRElementResult.ReachedSpecialContent() ||
          // In case we're at the very end.
          forwardScanFromPointAfterNewBRElementResult
              .ReachedCurrentBlockBoundary()) {
        brElement = InsertBRElementWithTransaction(pointToCreateNewBRElement);
        if (NS_WARN_IF(Destroyed())) {
          return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
        }
        if (!brElement) {
          NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed");
          return EditActionIgnored(NS_ERROR_FAILURE);
        }
        // Now, those points may be invalid.
        pointToCreateNewBRElement.Clear();
        pointAfterNewBRElement.Clear();
      }
    }
  }

  // delete any empty cites
  if (previousNodeOfSplitPoint &&
      IsEmptyNode(*previousNodeOfSplitPoint, true, false)) {
    nsresult rv =
        DeleteNodeWithTransaction(MOZ_KnownLive(*previousNodeOfSplitPoint));
    if (NS_WARN_IF(Destroyed())) {
      return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
      return EditActionIgnored(rv);
    }
  }

  if (citeNode && IsEmptyNode(*citeNode, true, false)) {
    nsresult rv = DeleteNodeWithTransaction(*citeNode);
    if (NS_WARN_IF(Destroyed())) {
      return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
      return EditActionIgnored(rv);
    }
  }

  return EditActionHandled();
}

EditActionResult HTMLEditor::HandleDeleteSelection(
    nsIEditor::EDirection aDirectionAndAmount,
    nsIEditor::EStripWrappers aStripWrappers) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
             aStripWrappers == nsIEditor::eNoStrip);

  EditActionResult result =
      HandleDeleteSelectionInternal(aDirectionAndAmount, aStripWrappers);
  if (result.Failed() || result.Canceled()) {
    NS_WARNING_ASSERTION(result.Succeeded(),
                         "HTMLEditor::HandleDeleteSelectionInternal() failed");
    return result;
  }
  // If it's just ignored, we should fall this back to
  // `DeleteSelectionWithTransaction()` when selection is not collapsed or
  // content around collapsed range should be deleted.
  if (!result.Handled() && SelectionRefPtr()->RangeCount() > 0 &&
      (!SelectionRefPtr()->IsCollapsed() ||
       EditorBase::HowToHandleCollapsedRangeFor(aDirectionAndAmount) !=
           HowToHandleCollapsedRange::Ignore)) {
    nsresult rv =
        DeleteSelectionWithTransaction(aDirectionAndAmount, aStripWrappers);
    if (rv == NS_ERROR_EDITOR_DESTROYED) {
      NS_WARNING(
          "EditorBase::DeleteSelectionWithTransaction() caused destroying the "
          "editor");
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "EditorBase::DeleteSelectionWithTransaction() failed, but ignored");
  }

  // XXX At here, selection may have no range because of mutation event
  //     listeners can do anything so that we should just return NS_OK instead
  //     of returning error.
  EditorDOMPoint atNewStartOfSelection(
      EditorBase::GetStartPoint(*SelectionRefPtr()));
  if (NS_WARN_IF(!atNewStartOfSelection.IsSet())) {
    return EditActionHandled(NS_ERROR_FAILURE);
  }
  if (atNewStartOfSelection.GetContainerAsContent()) {
    nsresult rv = DeleteMostAncestorMailCiteElementIfEmpty(
        MOZ_KnownLive(*atNewStartOfSelection.GetContainerAsContent()));
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::DeleteMostAncestorMailCiteElementIfEmpty() failed");
      return EditActionHandled(rv);
    }
  }
  return EditActionHandled();
}

EditActionResult HTMLEditor::HandleDeleteSelectionInternal(
    nsIEditor::EDirection aDirectionAndAmount,
    nsIEditor::EStripWrappers aStripWrappers) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
             aStripWrappers == nsIEditor::eNoStrip);

  // Remember that we did a selection deletion.  Used by
  // CreateStyleForInsertText()
  TopLevelEditSubActionDataRef().mDidDeleteSelection = true;

  // If there is only padding `<br>` element for empty editor, cancel the
  // operation.
  if (mPaddingBRElementForEmptyEditor) {
    return EditActionCanceled();
  }

  // First check for table selection mode.  If so, hand off to table editor.
  ErrorResult error;
  if (RefPtr<Element> cellElement = GetFirstSelectedTableCellElement(error)) {
    error.SuppressException();
    nsresult rv = DeleteTableCellContentsWithTransaction();
    if (NS_WARN_IF(Destroyed())) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::DeleteTableCellContentsWithTransaction() failed");
    return EditActionHandled(rv);
  }

  // Only when there is no selection range, GetFirstSelectedTableCellElement()
  // returns error and in this case, anyway we cannot handle delete selection.
  // So, let's return error here even though we haven't handled this.
  if (error.Failed()) {
    NS_WARNING("HTMLEditor::GetFirstSelectedTableCellElement() failed");
    return EditActionResult(error.StealNSResult());
  }

  // selectionWasCollapsed is used later to determine whether we should join
  // blocks in HandleDeleteNonCollapasedSelection(). We don't really care
  // about collapsed because it will be modified by ExtendSelectionForDelete()
  // later. TryToJoinBlocksWithTransaction() should happen if the original
  // selection is collapsed and the cursor is at the end of a block element,
  // in which case ExtendSelectionForDelete() would always make the selection
  // not collapsed.
  SelectionWasCollapsed selectionWasCollapsed = SelectionRefPtr()->IsCollapsed()
                                                    ? SelectionWasCollapsed::Yes
                                                    : SelectionWasCollapsed::No;

  if (selectionWasCollapsed == SelectionWasCollapsed::Yes) {
    EditorDOMPoint startPoint(EditorBase::GetStartPoint(*SelectionRefPtr()));
    if (NS_WARN_IF(!startPoint.IsSet())) {
      return EditActionResult(NS_ERROR_FAILURE);
    }

    // If we are inside an empty block, delete it.
    RefPtr<Element> editingHost = GetActiveEditingHost();
    if (NS_WARN_IF(!editingHost)) {
      return EditActionResult(NS_ERROR_FAILURE);
    }

    if (startPoint.GetContainerAsContent()) {
      AutoEditorDOMPointChildInvalidator lockOffset(startPoint);

      EditActionResult result = MaybeDeleteTopMostEmptyAncestor(
          MOZ_KnownLive(*startPoint.GetContainerAsContent()), *editingHost,
          aDirectionAndAmount);
      if (result.Failed() || result.Handled()) {
        NS_WARNING_ASSERTION(
            result.Succeeded(),
            "HTMLEditor::MaybeDeleteTopMostEmptyAncestor() failed");
        return result;
      }
    }

    // Test for distance between caret and text that will be deleted
    EditActionResult result =
        SetCaretBidiLevelForDeletion(startPoint, aDirectionAndAmount);
    if (result.Failed() || result.Canceled()) {
      NS_WARNING("EditorBase::SetCaretBidiLevelForDeletion() failed");
      return result;
    }

    // ExtendSelectionForDelete will use selection controller to set
    // selection for delete.  But if focus event doesn't receive yet,
    // ancestor isn't set.  So we must set root eleement of editor to
    // ancestor.
    AutoSetTemporaryAncestorLimiter autoSetter(*this, *SelectionRefPtr(),
                                               *startPoint.GetContainer());

    nsresult rv = ExtendSelectionForDelete(&aDirectionAndAmount);
    if (NS_FAILED(rv)) {
      NS_WARNING("TextEditor::ExtendSelectionForDelete() failed");
      return EditActionResult(rv);
    }

    if (aDirectionAndAmount == nsIEditor::eNone) {
      // If we're not called recursively, we should call
      // `DeleteSelectionWithTransaction()` here, but we cannot detect it for
      // now.  Therefore, we should just tell the caller of that we does
      // nothing.
      return EditActionIgnored();
    }

    if (SelectionRefPtr()->IsCollapsed()) {
      EditActionResult result = HandleDeleteAroundCollapsedSelection(
          aDirectionAndAmount, aStripWrappers);
      NS_WARNING_ASSERTION(
          result.Succeeded(),
          "HTMLEditor::HandleDeleteAroundCollapsedSelection() failed");
      return result;
    }
  }

  EditActionResult result = HandleDeleteNonCollapsedSelection(
      aDirectionAndAmount, aStripWrappers, selectionWasCollapsed);
  NS_WARNING_ASSERTION(
      result.Succeeded(),
      "HTMLEditor::HandleDeleteNonCollapasedSelection() failed");
  return result;
}

EditActionResult HTMLEditor::HandleDeleteAroundCollapsedSelection(
    nsIEditor::EDirection aDirectionAndAmount,
    nsIEditor::EStripWrappers aStripWrappers) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
  MOZ_ASSERT(SelectionRefPtr()->IsCollapsed());
  MOZ_ASSERT(aDirectionAndAmount != nsIEditor::eNone);

  EditorDOMPoint startPoint(EditorBase::GetStartPoint(*SelectionRefPtr()));
  if (NS_WARN_IF(!startPoint.IsSet())) {
    return EditActionResult(NS_ERROR_FAILURE);
  }

  // What's in the direction we are deleting?
  WSRunObject wsObj(*this, startPoint);
  WSScanResult scanFromStartPointResult =
      aDirectionAndAmount == nsIEditor::eNext
          ? wsObj.ScanNextVisibleNodeOrBlockBoundaryFrom(startPoint)
          : wsObj.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startPoint);
  if (!scanFromStartPointResult.GetContent()) {
    return EditActionCanceled();
  }

  if (scanFromStartPointResult.InNormalWhiteSpaces()) {
    EditActionResult result =
        HandleDeleteCollapsedSelectionAtWhiteSpaces(aDirectionAndAmount, wsObj);
    NS_WARNING_ASSERTION(
        result.Succeeded(),
        "HTMLEditor::HandleDelectCollapsedSelectionAtWhiteSpaces() failed");
    return result;
  }

  if (scanFromStartPointResult.InNormalText()) {
    if (NS_WARN_IF(!scanFromStartPointResult.GetContent()->IsText())) {
      return EditActionResult(NS_ERROR_FAILURE);
    }
    EditActionResult result = HandleDeleteCollapsedSelectionAtTextNode(
        aDirectionAndAmount, scanFromStartPointResult.Point());
    NS_WARNING_ASSERTION(
        result.Succeeded(),
        "HTMLEditor::HandleDeleteCollapsedSelectionAtTextNode() failed");
    return result;
  }

  if (scanFromStartPointResult.ReachedSpecialContent() ||
      scanFromStartPointResult.ReachedBRElement() ||
      scanFromStartPointResult.ReachedHRElement()) {
    EditActionResult result = HandleDeleteCollapsedSelectionAtAtomicContent(
        aDirectionAndAmount, aStripWrappers,
        MOZ_KnownLive(*scanFromStartPointResult.GetContent()), startPoint,
        wsObj);
    NS_WARNING_ASSERTION(
        result.Succeeded(),
        "HTMLEditor::HandleDeleteCollapsedSelectionAtAtomicContent() failed");
    return result;
  }

  if (scanFromStartPointResult.ReachedOtherBlockElement()) {
    if (NS_WARN_IF(!scanFromStartPointResult.GetContent()->IsElement())) {
      return EditActionResult(NS_ERROR_FAILURE);
    }
    EditActionResult result =
        HandleDeleteCollapsedSelectionAtOtherBlockBoundary(
            aDirectionAndAmount, aStripWrappers,
            MOZ_KnownLive(*scanFromStartPointResult.ElementPtr()), startPoint,
            wsObj);
    NS_WARNING_ASSERTION(
        result.Succeeded(),
        "HTMLEditor::HandleDeleteCollapsedSelectionAtOtherBlockBoundary() "
        "failed");
    return result;
  }

  if (scanFromStartPointResult.ReachedCurrentBlockBoundary()) {
    if (NS_WARN_IF(!scanFromStartPointResult.GetContent()->IsElement())) {
      return EditActionResult(NS_ERROR_FAILURE);
    }
    EditActionResult result =
        HandleDeleteCollapsedSelectionAtCurrentBlockBoundary(
            aDirectionAndAmount,
            MOZ_KnownLive(*scanFromStartPointResult.ElementPtr()), startPoint);
    NS_WARNING_ASSERTION(
        result.Succeeded(),
        "HTMLEditor::HandleDeleteCollapsedSelectionAtCurrentBlockBoundary() "
        "failed");
    return result;
  }

  MOZ_ASSERT_UNREACHABLE("New type of reached content hasn't been handled yet");
  return EditActionIgnored();
}

EditActionResult HTMLEditor::HandleDeleteCollapsedSelectionAtWhiteSpaces(
    nsIEditor::EDirection aDirectionAndAmount,
    WSRunObject& aWSRunObjectAtCaret) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  if (aDirectionAndAmount == nsIEditor::eNext) {
    nsresult rv = aWSRunObjectAtCaret.DeleteWSForward();
    if (NS_WARN_IF(Destroyed())) {
      return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("WSRunObject::DeleteWSForward() failed");
      return EditActionHandled(rv);
    }
  } else {
    nsresult rv = aWSRunObjectAtCaret.DeleteWSBackward();
    if (NS_WARN_IF(Destroyed())) {
      return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("WSRunObject::DeleteWSBackward() failed");
      return EditActionHandled(rv);
    }
  }
  nsresult rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
      EditorBase::GetStartPoint(*SelectionRefPtr()));
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() "
      "failed");
  return EditActionHandled(rv);
}

EditActionResult HTMLEditor::HandleDeleteCollapsedSelectionAtTextNode(
    nsIEditor::EDirection aDirectionAndAmount,
    const EditorDOMPoint& aPointToDelete) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
  MOZ_ASSERT(aPointToDelete.IsSet());
  MOZ_ASSERT(aPointToDelete.IsInTextNode());

  OwningNonNull<Text> visibleTextNode = *aPointToDelete.GetContainerAsText();
  EditorDOMPoint startToDelete, endToDelete;
  if (aDirectionAndAmount == nsIEditor::ePrevious) {
    if (aPointToDelete.IsStartOfContainer()) {
      return EditActionResult(NS_ERROR_UNEXPECTED);
    }
    startToDelete = aPointToDelete.PreviousPoint();
    endToDelete = aPointToDelete;
    // Bug 1068979: delete both codepoints if surrogate pair
    if (!startToDelete.IsStartOfContainer()) {
      const nsTextFragment* text = &visibleTextNode->TextFragment();
      if (text->IsLowSurrogateFollowingHighSurrogateAt(
              startToDelete.Offset())) {
        startToDelete.RewindOffset();
      }
    }
  } else {
    RefPtr<const nsRange> range = SelectionRefPtr()->GetRangeAt(0);
    if (NS_WARN_IF(!range) ||
        NS_WARN_IF(range->GetStartContainer() !=
                   aPointToDelete.GetContainer()) ||
        NS_WARN_IF(range->GetEndContainer() != aPointToDelete.GetContainer())) {
      return EditActionResult(NS_ERROR_FAILURE);
    }
    startToDelete = range->StartRef();
    endToDelete = range->EndRef();
  }
  nsresult rv =
      WSRunObject::PrepareToDeleteRange(*this, &startToDelete, &endToDelete);
  if (NS_WARN_IF(Destroyed())) {
    return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
  }
  if (NS_FAILED(rv)) {
    NS_WARNING("WSRunObject::PrepareToDeleteRange() failed");
    return EditActionResult(rv);
  }
  if (MaybeHasMutationEventListeners(
          NS_EVENT_BITS_MUTATION_NODEREMOVED |
          NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
          NS_EVENT_BITS_MUTATION_ATTRMODIFIED |
          NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) &&
      (NS_WARN_IF(!startToDelete.IsSetAndValid()) ||
       NS_WARN_IF(!startToDelete.IsInTextNode()) ||
       NS_WARN_IF(!endToDelete.IsSetAndValid()) ||
       NS_WARN_IF(!endToDelete.IsInTextNode()) ||
       NS_WARN_IF(startToDelete.ContainerAsText() != visibleTextNode) ||
       NS_WARN_IF(endToDelete.ContainerAsText() != visibleTextNode) ||
       NS_WARN_IF(startToDelete.Offset() >= endToDelete.Offset()))) {
    NS_WARNING("Mutation event listener changed the DOM tree");
    return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }
  rv = DeleteTextWithTransaction(visibleTextNode, startToDelete.Offset(),
                                 endToDelete.Offset() - startToDelete.Offset());
  if (NS_WARN_IF(Destroyed())) {
    return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
  }
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
    return EditActionHandled(rv);
  }

  // XXX When Backspace key is pressed, Chromium removes following empty
  //     text nodes when removing the last character of the non-empty text
  //     node.  However, Edge never removes empty text nodes even if
  //     selection is in the following empty text node(s).  For now, we
  //     should keep our traditional behavior same as Edge for backward
  //     compatibility.
  // XXX When Delete key is pressed, Edge removes all preceding empty
  //     text nodes when removing the first character of the non-empty
  //     text node.  Chromium removes only selected empty text node and
  //     following empty text nodes and the first character of the
  //     non-empty text node.  For now, we should keep our traditional
  //     behavior same as Chromium for backward compatibility.

  rv = DeleteNodeIfInvisibleAndEditableTextNode(visibleTextNode);
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::DeleteNodeIfInvisibleAndEditableTextNode() "
                       "failed, but ignored");

  // XXX `Selection` may be modified by mutation event listeners so
  //     that we should use EditorDOMPoint::AtEndOf(visibleTextNode)
  //     instead.  (Perhaps, we don't and/or shouldn't need to do this
  //     if the text node is preformatted.)
  rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
      EditorBase::GetStartPoint(*SelectionRefPtr()));
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()"
        " failed");
    return EditActionHandled(rv);
  }

  // Remember that we did a ranged delete for the benefit of
  // AfterEditInner().
  TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange = true;

  return EditActionHandled();
}

EditActionResult HTMLEditor::HandleDeleteCollapsedSelectionAtAtomicContent(
    nsIEditor::EDirection aDirectionAndAmount,
    nsIEditor::EStripWrappers aStripWrappers, nsIContent& aAtomicContent,
    const EditorDOMPoint& aCaretPoint, WSRunScanner& aWSRunScannerAtCaret) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aCaretPoint.IsSet());

  // If the atomic element is editing host, we should do nothing.
  if (&aAtomicContent == aWSRunScannerAtCaret.GetEditingHost()) {
    return EditActionHandled();
  }

  // Short circuit for invisible breaks.  delete them and recurse.
  if (aAtomicContent.IsHTMLElement(nsGkAtoms::br) &&
      !IsVisibleBRElement(&aAtomicContent)) {
    nsresult rv = DeleteNodeWithTransaction(aAtomicContent);
    if (NS_WARN_IF(Destroyed())) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
      return EditActionResult(rv);
    }
    // XXX This nesting call is not safe.  If mutation event listener
    //     creates same situation, this causes stack-overflow.
    EditActionResult result =
        HandleDeleteSelectionInternal(aDirectionAndAmount, aStripWrappers);
    NS_WARNING_ASSERTION(
        result.Succeeded(),
        "HTMLEditor::Nested HandleDeleteSelectionInternal() failed");
    return result;
  }

  // Special handling for backspace when positioned after <hr>
  if (aDirectionAndAmount == nsIEditor::ePrevious &&
      aAtomicContent.IsHTMLElement(nsGkAtoms::hr)) {
    // Only if the caret is positioned at the end-of-hr-line position, we
    // want to delete the <hr>.
    //
    // In other words, we only want to delete, if our selection position
    // (indicated by aCaretPoint) is the position directly
    // after the <hr>, on the same line as the <hr>.
    //
    // To detect this case we check:
    // aCaretPoint's container == parent of `<hr>` element
    // and
    // aCaretPoint's offset -1 == `<hr>` element offset
    // and
    // interline position is false (left)
    //
    // In any other case we set the position to aCaretPoint's container -1
    // and interlineposition to false, only moving the caret to the
    // end-of-hr-line position.
    bool moveOnly = true;

    EditorRawDOMPoint atHRElement(&aAtomicContent);

    ErrorResult error;
    bool interLineIsRight = SelectionRefPtr()->GetInterlinePosition(error);
    if (error.Failed()) {
      NS_WARNING("Selection::GetInterlinePosition() failed");
      return EditActionResult(error.StealNSResult());
    }

    if (aCaretPoint.GetContainer() == atHRElement.GetContainer() &&
        aCaretPoint.Offset() - 1 == atHRElement.Offset() && !interLineIsRight) {
      moveOnly = false;
    }

    if (moveOnly) {
      // Go to the position after the <hr>, but to the end of the <hr> line
      // by setting the interline position to left.
      EditorDOMPoint atNextOfHRElement(EditorDOMPoint::After(aAtomicContent));
      NS_WARNING_ASSERTION(atNextOfHRElement.IsSet(),
                           "Failed to set after <hr> element");

      {
        AutoEditorDOMPointChildInvalidator lockOffset(atNextOfHRElement);

        nsresult rv = CollapseSelectionTo(atNextOfHRElement);
        if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
          return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
        }
        NS_WARNING_ASSERTION(
            NS_SUCCEEDED(rv),
            "HTMLEditor::CollapseSelectionTo() failed, but ignored");
      }

      IgnoredErrorResult ignoredError;
      SelectionRefPtr()->SetInterlinePosition(false, ignoredError);
      NS_WARNING_ASSERTION(
          !ignoredError.Failed(),
          "Selection::SetInterlinePosition(false) failed, but ignored");
      TopLevelEditSubActionDataRef().mDidExplicitlySetInterLine = true;

      // There is one exception to the move only case.  If the <hr> is
      // followed by a <br> we want to delete the <br>.

      WSScanResult forwardScanFromCaretResult =
          aWSRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
              aCaretPoint);
      if (!forwardScanFromCaretResult.ReachedBRElement()) {
        return EditActionHandled();
      }

      // Delete the <br>
      nsresult rv = WSRunObject::PrepareToDeleteNode(
          *this, MOZ_KnownLive(forwardScanFromCaretResult.BRElementPtr()));
      if (NS_WARN_IF(Destroyed())) {
        return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("WSRunObject::PrepareToDeleteNode() failed");
        return EditActionHandled(rv);
      }
      rv = DeleteNodeWithTransaction(
          MOZ_KnownLive(*forwardScanFromCaretResult.BRElementPtr()));
      if (NS_WARN_IF(Destroyed())) {
        return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "HTMLEditor::DeleteNodeWithTransaction() failed");
      return EditActionHandled(rv);
    }
    // Else continue with normal delete code
  }

  // Found break or image, or hr.
  // XXX Oddly, this requires `MOZ_KnownLive()` for `&aAtomicContent` here...
  nsresult rv =
      WSRunObject::PrepareToDeleteNode(*this, MOZ_KnownLive(&aAtomicContent));
  if (NS_WARN_IF(Destroyed())) {
    return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
  }
  if (NS_FAILED(rv)) {
    NS_WARNING("WSRunObject::PrepareToDeleteNode() failed");
    return EditActionResult(rv);
  }
  // Remember sibling to visnode, if any
  nsCOMPtr<nsIContent> previousEditableSibling =
      GetPriorHTMLSibling(&aAtomicContent);
  // Delete the node, and join like nodes if appropriate
  rv = DeleteNodeWithTransaction(aAtomicContent);
  if (NS_WARN_IF(Destroyed())) {
    return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
  }
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
    return EditActionResult(rv);
  }
  // Is there a prior node and are they siblings?
  nsCOMPtr<nsINode> nextEditableSibling;
  if (previousEditableSibling) {
    nextEditableSibling = GetNextHTMLSibling(previousEditableSibling);
  }
  // Are they both text nodes?  If so, join them!
  // XXX This may cause odd behavior if there is non-editable nodes
  //     around the atomic content.
  if (aCaretPoint.GetContainer() == nextEditableSibling &&
      aCaretPoint.GetContainerAsText() &&
      previousEditableSibling->GetAsText()) {
    EditorDOMPoint atFirstChildOfRightNode;
    nsresult rv = JoinNearestEditableNodesWithTransaction(
        *previousEditableSibling,
        MOZ_KnownLive(*aCaretPoint.GetContainerAsContent()),
        &atFirstChildOfRightNode);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::JoinNearestEditableNodesWithTransaction() failed");
      return EditActionHandled(rv);
    }
    if (!atFirstChildOfRightNode.IsSet()) {
      NS_WARNING(
          "HTMLEditor::JoinNearestEditableNodesWithTransaction() didn't return "
          "right node position");
      return EditActionHandled(NS_ERROR_FAILURE);
    }
    // Fix up selection
    rv = CollapseSelectionTo(atFirstChildOfRightNode);
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::CollapseSelectionTo() failed");
      return EditActionHandled(rv);
    }
  }
  rv = InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
      EditorBase::GetStartPoint(*SelectionRefPtr()));
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() "
      "failed");
  return EditActionHandled(rv);
}

EditActionResult HTMLEditor::HandleDeleteCollapsedSelectionAtOtherBlockBoundary(
    nsIEditor::EDirection aDirectionAndAmount,
    nsIEditor::EStripWrappers aStripWrappers, Element& aOtherBlockElement,
    const EditorDOMPoint& aCaretPoint, WSRunScanner& aWSRunScannerAtCaret) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aCaretPoint.IsSet());

  // Make sure it's not a table element.  If so, cancel the operation
  // (translation: users cannot backspace or delete across table cells)
  if (HTMLEditUtils::IsTableElement(&aOtherBlockElement)) {
    return EditActionCanceled();
  }

  // Next to a block.  See if we are between a block and a br.  If so, we
  // really want to delete the br.  Else join content at selection to the
  // block.
  WSScanResult scanFromCaretResult =
      aDirectionAndAmount == nsIEditor::eNext
          ? aWSRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
                aCaretPoint)
          : aWSRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom(
                aCaretPoint);

  // First find the adjacent node in the block
  nsCOMPtr<nsIContent> leafNode;
  nsCOMPtr<nsINode> leftNode, rightNode;
  if (aDirectionAndAmount == nsIEditor::ePrevious) {
    leafNode = GetLastEditableLeaf(aOtherBlockElement);
    leftNode = leafNode;
    rightNode = aCaretPoint.GetContainer();
  } else {
    leafNode = GetFirstEditableLeaf(aOtherBlockElement);
    leftNode = aCaretPoint.GetContainer();
    rightNode = leafNode;
  }

  bool didBRElementDeleted = false;
  if (scanFromCaretResult.ReachedBRElement()) {
    nsresult rv = DeleteNodeWithTransaction(
        MOZ_KnownLive(*scanFromCaretResult.BRElementPtr()));
    if (NS_WARN_IF(Destroyed())) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
      return EditActionResult(rv);
    }
    didBRElementDeleted = true;
  }

  // Don't cross table boundaries
  if (leftNode && rightNode &&
      HTMLEditor::NodesInDifferentTableElements(*leftNode, *rightNode)) {
    // If we have not deleted `<br>` element and are not called recursively,
    // we should call `DeleteSelectionWithTransaction()` here, but we cannot
    // detect it for now.  Therefore, we should just tell the caller of that
    // we does nothing.
    return didBRElementDeleted ? EditActionHandled() : EditActionIgnored();
  }

  if (didBRElementDeleted) {
    // Put selection at edge of block and we are done.
    if (NS_WARN_IF(!leafNode)) {
      return EditActionHandled(NS_ERROR_FAILURE);
    }
    EditorDOMPoint newSel =
        GetGoodCaretPointFor(*leafNode, aDirectionAndAmount);
    if (!newSel.IsSet()) {
      NS_WARNING("HTMLEditor::GetGoodCaretPointFor() failed");
      return EditActionHandled(NS_ERROR_FAILURE);
    }
    nsresult rv = CollapseSelectionTo(newSel);
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::CollapseSelectionTo() failed, but ignored");
    return EditActionHandled();
  }

  // Else we are joining content to block
  EditActionResult result(NS_OK);
  EditorDOMPoint pointToPutCaret(aCaretPoint);
  {
    AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToPutCaret);
    if (NS_WARN_IF(!leftNode) || NS_WARN_IF(!leftNode->IsContent()) ||
        NS_WARN_IF(!rightNode) || NS_WARN_IF(!rightNode->IsContent())) {
      return EditActionResult(NS_ERROR_FAILURE);
    }
    result |=
        TryToJoinBlocksWithTransaction(MOZ_KnownLive(*leftNode->AsContent()),
                                       MOZ_KnownLive(*rightNode->AsContent()));
    if (result.Failed()) {
      NS_WARNING("HTMLEditor::TryToJoinBlocksWithTransaction() failed");
      return result;
    }
  }

  // If TryToJoinBlocksWithTransaction() didn't handle it and it's not
  // canceled, user may want to modify the start leaf node or the last leaf
  // node of the block.
  if (!result.Handled() && !result.Canceled() &&
      leafNode != aCaretPoint.GetContainer()) {
    int32_t offset = aDirectionAndAmount == nsIEditor::ePrevious
                         ? static_cast<int32_t>(leafNode->Length())
                         : 0;
    nsresult rv = CollapseSelectionTo(EditorRawDOMPoint(leafNode, offset));
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::CollapseSelectionTo() failed, but ignored");
    EditActionResult result =
        HandleDeleteSelectionInternal(aDirectionAndAmount, aStripWrappers);
    NS_WARNING_ASSERTION(
        result.Succeeded(),
        "Recursive HTMLEditor::HandleDeleteSelectionInternal() failed");
    return result;
  }

  // Otherwise, we must have deleted the selection as user expected.
  nsresult rv = CollapseSelectionTo(pointToPutCaret);
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::CollapseSelectionTo() failed, but ignored");
  return result;
}

EditActionResult
HTMLEditor::HandleDeleteCollapsedSelectionAtCurrentBlockBoundary(
    nsIEditor::EDirection aDirectionAndAmount, Element& aCurrentBlockElement,
    const EditorDOMPoint& aCaretPoint) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  // At edge of our block.  Look beside it and see if we can join to an
  // adjacent block

  // Make sure it's not a table element.  If so, cancel the operation
  // (translation: users cannot backspace or delete across table cells)
  if (HTMLEditUtils::IsTableElement(&aCurrentBlockElement)) {
    return EditActionCanceled();
  }

  // First find the relevant nodes
  nsCOMPtr<nsINode> leftNode, rightNode;
  if (aDirectionAndAmount == nsIEditor::ePrevious) {
    leftNode = GetPreviousEditableHTMLNode(aCurrentBlockElement);
    rightNode = aCaretPoint.GetContainer();
  } else {
    rightNode = GetNextEditableHTMLNode(aCurrentBlockElement);
    leftNode = aCaretPoint.GetContainer();
  }

  // Nothing to join
  if (!leftNode || !rightNode) {
    return EditActionCanceled();
  }

  // Don't cross table boundaries -- cancel it
  if (HTMLEditor::NodesInDifferentTableElements(*leftNode, *rightNode)) {
    return EditActionCanceled();
  }

  EditActionResult result(NS_OK);
  EditorDOMPoint pointToPutCaret(aCaretPoint);
  {
    AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToPutCaret);
    if (NS_WARN_IF(!leftNode->IsContent()) ||
        NS_WARN_IF(!rightNode->IsContent())) {
      return EditActionResult(NS_ERROR_FAILURE);
    }
    result |=
        TryToJoinBlocksWithTransaction(MOZ_KnownLive(*leftNode->AsContent()),
                                       MOZ_KnownLive(*rightNode->AsContent()));
    // This should claim that trying to join the block means that
    // this handles the action because the caller shouldn't do anything
    // anymore in this case.
    result.MarkAsHandled();
    if (result.Failed()) {
      NS_WARNING("HTMLEditor::TryToJoinBlocksWithTransaction() failed");
      return result;
    }
  }
  nsresult rv = CollapseSelectionTo(pointToPutCaret);
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::CollapseSelectionTo() failed, but ignored");
  return result;
}

EditActionResult HTMLEditor::HandleDeleteNonCollapsedSelection(
    nsIEditor::EDirection aDirectionAndAmount,
    nsIEditor::EStripWrappers aStripWrappers,
    SelectionWasCollapsed aSelectionWasCollapsed) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
  MOZ_ASSERT(!SelectionRefPtr()->IsCollapsed());

  // Else we have a non-collapsed selection.  First adjust the selection.
  // XXX Why do we extend selection only when there is only one range?
  if (SelectionRefPtr()->RangeCount() == 1) {
    if (const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0)) {
      RefPtr<StaticRange> extendedRange =
          GetRangeExtendedToIncludeInvisibleNodes(*firstRange);
      if (!extendedRange) {
        NS_WARNING(
            "HTMLEditor::GetRangeExtendedToIncludeInvisibleNodes() failed");
        return EditActionResult(NS_ERROR_FAILURE);
      }
      ErrorResult error;
      MOZ_KnownLive(SelectionRefPtr())
          ->SetStartAndEndInLimiter(extendedRange->StartRef().AsRaw(),
                                    extendedRange->EndRef().AsRaw(), error);
      if (NS_WARN_IF(Destroyed())) {
        error = NS_ERROR_EDITOR_DESTROYED;
      }
      if (error.Failed()) {
        NS_WARNING_ASSERTION(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED),
                             "Selection::SetStartAndEndInLimiter() failed");
        return EditActionResult(error.StealNSResult());
      }
    }
  }

  // Remember that we did a ranged delete for the benefit of AfterEditInner().
  TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange = true;

  const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
  if (NS_WARN_IF(!firstRange)) {
    return EditActionResult(NS_ERROR_FAILURE);
  }
  EditorDOMPoint firstRangeStart(firstRange->StartRef());
  EditorDOMPoint firstRangeEnd(firstRange->EndRef());
  if (NS_WARN_IF(!firstRangeStart.IsSet()) ||
      NS_WARN_IF(!firstRangeEnd.IsSet())) {
    return EditActionResult(NS_ERROR_FAILURE);
  }

  // Figure out if the endpoints are in nodes that can be merged.  Adjust
  // surrounding whitespace in preparation to delete selection.
  if (!IsPlaintextEditor()) {
    AutoTransactionsConserveSelection dontChangeMySelection(*this);
    nsresult rv = WSRunObject::PrepareToDeleteRange(*this, &firstRangeStart,
                                                    &firstRangeEnd);
    if (NS_WARN_IF(Destroyed())) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("WSRunObject::PrepareToDeleteRange() failed");
      return EditActionResult(rv);
    }
    if (MaybeHasMutationEventListeners(
            NS_EVENT_BITS_MUTATION_NODEREMOVED |
            NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
            NS_EVENT_BITS_MUTATION_ATTRMODIFIED |
            NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) &&
        (NS_WARN_IF(!firstRangeStart.IsSetAndValid()) ||
         NS_WARN_IF(!firstRangeEnd.IsSetAndValid()))) {
      NS_WARNING("Mutation event listener changed the DOM tree");
      return EditActionHandled(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
    }
  }

  // XXX This is odd.  We do we simply use `DeleteSelectionWithTransaction()`
  //     only when **first** range is in same container?
  if (firstRangeStart.GetContainer() == firstRangeEnd.GetContainer()) {
    // Because of previous DOM tree changes, the range may be collapsed.
    // If we've already removed all contents in the range, we shouldn't
    // delete anything around the caret.
    if (firstRangeStart != firstRangeEnd) {
      AutoTrackDOMPoint startTracker(RangeUpdaterRef(), &firstRangeStart);
      AutoTrackDOMPoint endTracker(RangeUpdaterRef(), &firstRangeEnd);

      nsresult rv =
          DeleteSelectionWithTransaction(aDirectionAndAmount, aStripWrappers);
      if (NS_FAILED(rv)) {
        NS_WARNING("EditorBase::DeleteSelectionWithTransaction() failed");
        return EditActionHandled(rv);
      }
    }
    // However, even if the range is removed, we may need to clean up the
    // containers which become empty.
    nsresult rv = DeleteUnnecessaryNodesAndCollapseSelection(
        aDirectionAndAmount, firstRangeStart, firstRangeEnd);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::DeleteUnnecessaryNodesAndCollapseSelection() failed");
    return EditActionHandled(rv);
  }

  // Figure out mailcite ancestors
  RefPtr<Element> startCiteNode =
      GetMostAncestorMailCiteElement(*firstRangeStart.GetContainer());
  RefPtr<Element> endCiteNode =
      GetMostAncestorMailCiteElement(*firstRangeEnd.GetContainer());

  // If we only have a mailcite at one of the two endpoints, set the
  // directionality of the deletion so that the selection will end up
  // outside the mailcite.
  if (startCiteNode && !endCiteNode) {
    aDirectionAndAmount = nsIEditor::eNext;
  } else if (!startCiteNode && endCiteNode) {
    aDirectionAndAmount = nsIEditor::ePrevious;
  }

  // Figure out block parents
  RefPtr<Element> leftBlockElement =
      firstRangeStart.IsInContentNode()
          ? HTMLEditUtils::GetInclusiveAncestorBlockElement(
                *firstRangeStart.ContainerAsContent())
          : nullptr;
  RefPtr<Element> rightBlockElement =
      firstRangeEnd.IsInContentNode()
          ? HTMLEditUtils::GetInclusiveAncestorBlockElement(
                *firstRangeEnd.ContainerAsContent())
          : nullptr;
  if (NS_WARN_IF(!leftBlockElement) || NS_WARN_IF(!rightBlockElement)) {
    return EditActionHandled(NS_ERROR_FAILURE);  // XXX "handled"??
  }

  // XXX This is also odd.  We do we simply use
  //     `DeleteSelectionWithTransaction()` only when **first** range is in
  //     same block?
  if (leftBlockElement && leftBlockElement == rightBlockElement) {
    {
      AutoTrackDOMPoint startTracker(RangeUpdaterRef(), &firstRangeStart);
      AutoTrackDOMPoint endTracker(RangeUpdaterRef(), &firstRangeEnd);

      nsresult rv =
          DeleteSelectionWithTransaction(aDirectionAndAmount, aStripWrappers);
      if (rv == NS_ERROR_EDITOR_DESTROYED) {
        NS_WARNING(
            "EditorBase::DeleteSelectionWithTransaction() caused destroying "
            "the editor");
        return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "EditorBase::DeleteSelectionWithTransaction() failed, but ignored");
    }
    nsresult rv = DeleteUnnecessaryNodesAndCollapseSelection(
        aDirectionAndAmount, firstRangeStart, firstRangeEnd);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::DeleteUnnecessaryNodesAndCollapseSelection() failed");
    return EditActionHandled(rv);
  }

  // Deleting across blocks.

  // If left block and right block are adjuscent siblings and they are same
  // type of elements, we can merge them after deleting the selected contents.
  // MOOSE: this could conceivably screw up a table.. fix me.
  if (leftBlockElement->GetParentNode() == rightBlockElement->GetParentNode() &&
      HTMLEditUtils::CanContentsBeJoined(
          *leftBlockElement, *rightBlockElement,
          IsCSSEnabled() ? StyleDifference::CompareIfSpanElements
                         : StyleDifference::Ignore) &&
      // XXX What's special about these three types of block?
      (leftBlockElement->IsHTMLElement(nsGkAtoms::p) ||
       HTMLEditUtils::IsListItem(leftBlockElement) ||
       HTMLEditUtils::IsHeader(*leftBlockElement))) {
    // First delete the selection
    nsresult rv =
        DeleteSelectionWithTransaction(aDirectionAndAmount, aStripWrappers);
    if (NS_FAILED(rv)) {
      NS_WARNING("EditorBase::DeleteSelectionWithTransaction() failed");
      return EditActionHandled(rv);
    }
    // Join blocks
    Result<EditorDOMPoint, nsresult> atFirstChildOfTheLastRightNodeOrError =
        JoinNodesDeepWithTransaction(*leftBlockElement, *rightBlockElement);
    if (atFirstChildOfTheLastRightNodeOrError.isErr()) {
      NS_WARNING("HTMLEditor::JoinNodesDeepWithTransaction() failed");
      return EditActionHandled(
          atFirstChildOfTheLastRightNodeOrError.unwrapErr());
    }
    MOZ_ASSERT(atFirstChildOfTheLastRightNodeOrError.inspect().IsSet());
    // Fix up selection
    rv = CollapseSelectionTo(atFirstChildOfTheLastRightNodeOrError.inspect());
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::CollapseSelectionTo() failed");
    return EditActionHandled(rv);
  }

  // Otherwise, delete every nodes in all ranges, then, clean up something.
  EditActionResult result(NS_OK);
  result.MarkAsHandled();
  {
    AutoTrackDOMPoint startTracker(RangeUpdaterRef(), &firstRangeStart);
    AutoTrackDOMPoint endTracker(RangeUpdaterRef(), &firstRangeEnd);

    // Else blocks not same type, or not siblings.  Delete everything
    // except table elements.
    bool join = true;

    AutoRangeArray arrayOfRanges(SelectionRefPtr());
    for (auto& range : arrayOfRanges.mRanges) {
      // Build a list of direct child nodes in the range
      AutoTArray<OwningNonNull<nsIContent>, 10> arrayOfTopChildren;
      DOMSubtreeIterator iter;
      nsresult rv = iter.Init(*range);
      if (NS_FAILED(rv)) {
        NS_WARNING("DOMSubtreeIterator::Init() failed");
        return result.SetResult(rv);
      }
      iter.AppendAllNodesToArray(arrayOfTopChildren);

      // Now that we have the list, delete non-table elements
      for (auto& content : arrayOfTopChildren) {
        // XXX After here, the child contents in the array may have been moved
        //     to somewhere or removed.  We should handle it.
        //
        // MOZ_KnownLive because 'arrayOfTopChildren' is guaranteed to
        // keep it alive.
        //
        // Even with https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 fixed
        // this might need to stay, because 'arrayOfTopChildren' is not const,
        // so it's not obvious how to prove via static analysis that it won't
        // change and release us.
        nsresult rv =
            DeleteElementsExceptTableRelatedElements(MOZ_KnownLive(content));
        if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
          return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
        }
        NS_WARNING_ASSERTION(
            NS_SUCCEEDED(rv),
            "HTMLEditor::DeleteElementsExceptTableRelatedElements() failed, "
            "but ignored");
        // If something visible is deleted, no need to join.  Visible means
        // all nodes except non-visible textnodes and breaks.
        // XXX Odd.  Why do we check the visibility after removing the node
        //     from the DOM tree?
        if (join && aSelectionWasCollapsed == SelectionWasCollapsed::Yes) {
          if (Text* text = content->GetAsText()) {
            join = !IsInVisibleTextFrames(*text);
          } else if (content->IsElement()) {
            if (IsEmptyNode(*content->AsElement())) {
              join = true;
            } else {
              join = content->IsHTMLElement(nsGkAtoms::br) &&
                     !IsVisibleBRElement(content);
            }
          }
        }
      }
    }

    // Check endpoints for possible text deletion.  We can assume that if
    // text node is found, we can delete to end or to begining as
    // appropriate, since the case where both sel endpoints in same text
    // node was already handled (we wouldn't be here)
    if (firstRangeStart.IsInTextNode() && !firstRangeStart.IsEndOfContainer()) {
      // Delete to last character
      OwningNonNull<Text> textNode = *firstRangeStart.GetContainerAsText();
      nsresult rv = DeleteTextWithTransaction(
          textNode, firstRangeStart.Offset(),
          firstRangeStart.GetContainer()->Length() - firstRangeStart.Offset());
      if (NS_WARN_IF(Destroyed())) {
        return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
        return result.SetResult(rv);
      }
    }
    if (firstRangeEnd.IsInTextNode() && !firstRangeEnd.IsStartOfContainer()) {
      // Delete to first character
      OwningNonNull<Text> textNode = *firstRangeEnd.GetContainerAsText();
      nsresult rv =
          DeleteTextWithTransaction(textNode, 0, firstRangeEnd.Offset());
      if (NS_WARN_IF(Destroyed())) {
        return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
        return result.SetResult(rv);
      }
    }

    if (join) {
      result |=
          TryToJoinBlocksWithTransaction(*leftBlockElement, *rightBlockElement);
      if (result.Failed()) {
        NS_WARNING("HTMLEditor::TryToJoinBlocksWithTransaction() failed");
        return result;
      }

      // If we're joining blocks: if deleting forward the selection should
      // be collapsed to the end of the selection, if deleting backward the
      // selection should be collapsed to the beginning of the selection.
      // But if we're not joining then the selection should collapse to the
      // beginning of the selection if we'redeleting forward, because the
      // end of the selection will still be in the next block. And same
      // thing for deleting backwards (selection should collapse to the end,
      // because the beginning will still be in the first block). See Bug
      // 507936.
      if (aDirectionAndAmount == nsIEditor::eNext) {
        aDirectionAndAmount = nsIEditor::ePrevious;
      } else {
        aDirectionAndAmount = nsIEditor::eNext;
      }
    }
  }

  nsresult rv = DeleteUnnecessaryNodesAndCollapseSelection(
      aDirectionAndAmount, firstRangeStart, firstRangeEnd);
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::DeleteUnnecessaryNodesAndCollapseSelection() failed");
  return result.SetResult(rv);
}

nsresult HTMLEditor::DeleteUnnecessaryNodesAndCollapseSelection(
    nsIEditor::EDirection aDirectionAndAmount,
    const EditorDOMPoint& aSelectionStartPoint,
    const EditorDOMPoint& aSelectionEndPoint) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());

  EditorDOMPoint atCaret(aSelectionStartPoint);
  EditorDOMPoint selectionEndPoint(aSelectionEndPoint);

  // If we're handling D&D, this is called to delete dragging item from the
  // tree.  In this case, we should remove parent blocks if it becomes empty.
  if (GetEditAction() == EditAction::eDrop ||
      GetEditAction() == EditAction::eDeleteByDrag) {
    MOZ_ASSERT((atCaret.GetContainer() == selectionEndPoint.GetContainer() &&
                atCaret.Offset() == selectionEndPoint.Offset()) ||
               (atCaret.GetContainer()->GetNextSibling() ==
                    selectionEndPoint.GetContainer() &&
                atCaret.IsEndOfContainer() &&
                selectionEndPoint.IsStartOfContainer()));
    {
      AutoTrackDOMPoint startTracker(RangeUpdaterRef(), &atCaret);
      AutoTrackDOMPoint endTracker(RangeUpdaterRef(), &selectionEndPoint);

      nsresult rv = DeleteParentBlocksWithTransactionIfEmpty(atCaret);
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "HTMLEditor::DeleteParentBlocksWithTransactionIfEmpty() failed");
        return rv;
      }
      TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks = rv == NS_OK;
    }
    // If we removed parent blocks, Selection should be collapsed at where
    // the most ancestor empty block has been.
    if (TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks) {
      nsresult rv = CollapseSelectionTo(atCaret);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "HTMLEditor::CollapseSelectionTo() failed");
      return rv;
    }
  }

  if (NS_WARN_IF(!atCaret.IsInContentNode()) ||
      NS_WARN_IF(!selectionEndPoint.IsInContentNode())) {
    return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
  }

  // We might have left only collapsed whitespace in the start/end nodes
  {
    AutoTrackDOMPoint startTracker(RangeUpdaterRef(), &atCaret);
    AutoTrackDOMPoint endTracker(RangeUpdaterRef(), &selectionEndPoint);

    nsresult rv = DeleteNodeIfInvisibleAndEditableTextNode(
        MOZ_KnownLive(*atCaret.ContainerAsContent()));
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::DeleteNodeIfInvisibleAndEditableTextNode("
                         ") failed to remove start node, but ignored");
    rv = DeleteNodeIfInvisibleAndEditableTextNode(
        MOZ_KnownLive(*selectionEndPoint.ContainerAsContent()));
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::DeleteNodeIfInvisibleAndEditableTextNode("
                         ") failed to remove end node, but ignored");
  }

  nsresult rv = CollapseSelectionTo(aDirectionAndAmount == nsIEditor::ePrevious
                                        ? selectionEndPoint
                                        : atCaret);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::CollapseSelectionTo() failed");
  return rv;
}

nsresult HTMLEditor::DeleteNodeIfInvisibleAndEditableTextNode(
    nsIContent& aContent) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  Text* text = aContent.GetAsText();
  if (!text) {
    return NS_OK;
  }

  if (IsVisibleTextNode(*text) || !HTMLEditUtils::IsSimplyEditableNode(*text)) {
    return NS_OK;
  }

  nsresult rv = DeleteNodeWithTransaction(aContent);
  if (NS_WARN_IF(Destroyed())) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::DeleteNodeWithTransaction() failed");
  return rv;
}

template <typename EditorDOMPointType>
nsresult HTMLEditor::DeleteTextAndTextNodesWithTransaction(
    const EditorDOMPointType& aStartPoint,
    const EditorDOMPointType& aEndPoint) {
  if (NS_WARN_IF(!aStartPoint.IsSet()) || NS_WARN_IF(!aEndPoint.IsSet())) {
    return NS_ERROR_INVALID_ARG;
  }

  // MOOSE: this routine needs to be modified to preserve the integrity of the
  // wsFragment info.

  if (aStartPoint == aEndPoint) {
    // Nothing to delete
    return NS_OK;
  }

  if (aStartPoint.GetContainer() == aEndPoint.GetContainer() &&
      aStartPoint.IsInTextNode()) {
    RefPtr<Text> textNode = aStartPoint.ContainerAsText();
    nsresult rv =
        DeleteTextWithTransaction(*textNode, aStartPoint.Offset(),
                                  aEndPoint.Offset() - aStartPoint.Offset());
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::DeleteTextWithTransaction() failed");
    return rv;
  }

  RefPtr<nsRange> range =
      nsRange::Create(aStartPoint.ToRawRangeBoundary(),
                      aEndPoint.ToRawRangeBoundary(), IgnoreErrors());
  if (!range) {
    NS_WARNING("nsRange::Create() failed");
    return NS_ERROR_FAILURE;
  }

  // Collect editable text nodes in the given range.
  AutoTArray<OwningNonNull<Text>, 16> arrayOfTextNodes;
  DOMIterator iter;
  if (NS_FAILED(iter.Init(*range))) {
    return NS_OK;  // Nothing to delete in the range.
  }
  iter.AppendNodesToArray(
      +[](nsINode& aNode, void*) {
        MOZ_ASSERT(aNode.IsText());
        return HTMLEditUtils::IsSimplyEditableNode(aNode);
      },
      arrayOfTextNodes);
  for (OwningNonNull<Text>& textNode : arrayOfTextNodes) {
    if (textNode == aStartPoint.GetContainer()) {
      if (aStartPoint.IsEndOfContainer()) {
        continue;
      }
      nsresult rv = DeleteTextWithTransaction(
          MOZ_KnownLive(textNode), aStartPoint.Offset(),
          textNode->Length() - aStartPoint.Offset());
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
        return rv;
      }
      continue;
    }

    if (textNode == aEndPoint.GetContainer()) {
      if (aEndPoint.IsStartOfContainer()) {
        break;
      }
      nsresult rv = DeleteTextWithTransaction(MOZ_KnownLive(textNode), 0,
                                              aEndPoint.Offset());
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed");
        return rv;
      }
      return NS_OK;
    }

    nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(textNode));
    if (NS_WARN_IF(Destroyed())) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
      return rv;
    }
  }

  return NS_OK;
}

nsresult HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
    const EditorDOMPoint& aPointToInsert) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aPointToInsert.IsSet());

  if (!aPointToInsert.GetContainerAsContent()) {
    return NS_OK;
  }

  // If container of the point is not in a block, we don't need to put a
  // `<br>` element here.
  if (!HTMLEditUtils::IsBlockElement(*aPointToInsert.ContainerAsContent())) {
    return NS_OK;
  }

  WSRunObject wsObj(*this, aPointToInsert);
  // If the point is not start of a hard line, we don't need to put a `<br>`
  // element here.
  if (!wsObj.StartsFromHardLineBreak()) {
    return NS_OK;
  }
  // If the point is not end of a hard line or the hard line does not end with
  // block boundary, we don't need to put a `<br>` element here.
  if (!wsObj.EndsByBlockBoundary()) {
    return NS_OK;
  }

  // If we cannot insert a `<br>` element here, do nothing.
  if (!HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(),
                                     *nsGkAtoms::br)) {
    return NS_OK;
  }

  RefPtr<Element> brElement =
      InsertBRElementWithTransaction(aPointToInsert, nsIEditor::ePrevious);
  if (NS_WARN_IF(Destroyed())) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(brElement,
                       "HTMLEditor::InsertBRElementWithTransaction() failed");
  return brElement ? NS_OK : NS_ERROR_FAILURE;
}

EditorDOMPoint HTMLEditor::GetGoodCaretPointFor(
    nsIContent& aContent, nsIEditor::EDirection aDirectionAndAmount) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext ||
             aDirectionAndAmount == nsIEditor::eNextWord ||
             aDirectionAndAmount == nsIEditor::ePrevious ||
             aDirectionAndAmount == nsIEditor::ePreviousWord ||
             aDirectionAndAmount == nsIEditor::eToBeginningOfLine ||
             aDirectionAndAmount == nsIEditor::eToEndOfLine);

  bool goingForward = (aDirectionAndAmount == nsIEditor::eNext ||
                       aDirectionAndAmount == nsIEditor::eNextWord ||
                       aDirectionAndAmount == nsIEditor::eToEndOfLine);

  // XXX Why don't we check whether the candidate position is enable or not?
  //     When the result is not editable point, caret will be enclosed in
  //     the non-editable content.

  // If we can put caret in aContent, return start or end in it.
  if (aContent.IsText() || HTMLEditUtils::IsContainerNode(aContent) ||
      NS_WARN_IF(!aContent.GetParentNode())) {
    return EditorDOMPoint(&aContent, goingForward ? 0 : aContent.Length());
  }

  // If we are going forward, put caret at aContent itself.
  if (goingForward) {
    return EditorDOMPoint(&aContent);
  }

  // If we are going backward, put caret to next node unless aContent is an
  // invisible `<br>` element.
  // XXX Shouldn't we put caret to first leaf of the next node?
  if (!aContent.IsHTMLElement(nsGkAtoms::br) || IsVisibleBRElement(&aContent)) {
    EditorDOMPoint ret(EditorDOMPoint::After(aContent));
    NS_WARNING_ASSERTION(ret.IsSet(), "Failed to set after aContent");
    return ret;
  }

  // Otherwise, we should put caret at the invisible `<br>` element.
  return EditorDOMPoint(&aContent);
}

Result<EditorDOMPoint, nsresult> HTMLEditor::JoinNodesDeepWithTransaction(
    nsIContent& aLeftContent, nsIContent& aRightContent) {
  // While the rightmost children and their descendants of the left node match
  // the leftmost children and their descendants of the right node, join them
  // up.

  nsCOMPtr<nsIContent> leftContentToJoin = &aLeftContent;
  nsCOMPtr<nsIContent> rightContentToJoin = &aRightContent;
  nsCOMPtr<nsINode> parentNode = aRightContent.GetParentNode();

  EditorDOMPoint ret;
  const HTMLEditUtils::StyleDifference kCompareStyle =
      IsCSSEnabled() ? StyleDifference::CompareIfSpanElements
                     : StyleDifference::Ignore;
  while (leftContentToJoin && rightContentToJoin && parentNode &&
         HTMLEditUtils::CanContentsBeJoined(
             *leftContentToJoin, *rightContentToJoin, kCompareStyle)) {
    uint32_t length = leftContentToJoin->Length();

    // Do the join
    nsresult rv =
        JoinNodesWithTransaction(*leftContentToJoin, *rightContentToJoin);
    if (NS_WARN_IF(Destroyed())) {
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
      return Err(rv);
    }

    // XXX rightContentToJoin may have fewer children or shorter length text.
    //     So, we need some adjustment here.
    ret.Set(rightContentToJoin, length);
    if (NS_WARN_IF(!ret.IsSet())) {
      return Err(NS_ERROR_FAILURE);
    }

    if (parentNode->IsText()) {
      // We've joined all the way down to text nodes, we're done!
      return ret;
    }

    // Get new left and right nodes, and begin anew
    parentNode = rightContentToJoin;
    rightContentToJoin = parentNode->GetChildAt_Deprecated(length);
    if (rightContentToJoin) {
      leftContentToJoin = rightContentToJoin->GetPreviousSibling();
    } else {
      leftContentToJoin = nullptr;
    }

    // Skip over non-editable nodes
    while (leftContentToJoin && !EditorUtils::IsEditableContent(
                                    *leftContentToJoin, EditorType::HTML)) {
      leftContentToJoin = leftContentToJoin->GetPreviousSibling();
    }
    if (!leftContentToJoin) {
      return ret;
    }

    while (rightContentToJoin && !EditorUtils::IsEditableContent(
                                     *rightContentToJoin, EditorType::HTML)) {
      rightContentToJoin = rightContentToJoin->GetNextSibling();
    }
    if (!rightContentToJoin) {
      return ret;
    }
  }

  if (!ret.IsSet()) {
    NS_WARNING("HTMLEditor::JoinNodesDeepWithTransaction() joined no contents");
    return Err(NS_ERROR_FAILURE);
  }
  return ret;
}

EditActionResult HTMLEditor::TryToJoinBlocksWithTransaction(
    nsIContent& aLeftContentInBlock, nsIContent& aRightContentInBlock) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  RefPtr<Element> leftBlockElement =
      HTMLEditUtils::GetInclusiveAncestorBlockElement(aLeftContentInBlock);
  RefPtr<Element> rightBlockElement =
      HTMLEditUtils::GetInclusiveAncestorBlockElement(aRightContentInBlock);

  // Sanity checks
  if (NS_WARN_IF(!leftBlockElement) || NS_WARN_IF(!rightBlockElement)) {
    return EditActionIgnored(NS_ERROR_NULL_POINTER);
  }
  if (NS_WARN_IF(leftBlockElement == rightBlockElement)) {
    return EditActionIgnored(NS_ERROR_UNEXPECTED);
  }

  if (HTMLEditUtils::IsTableElement(leftBlockElement) ||
      HTMLEditUtils::IsTableElement(rightBlockElement)) {
    // Do not try to merge table elements
    return EditActionCanceled();
  }

  // Make sure we don't try to move things into HR's, which look like blocks
  // but aren't containers
  if (leftBlockElement->IsHTMLElement(nsGkAtoms::hr)) {
    leftBlockElement =
        HTMLEditUtils::GetAncestorBlockElement(*leftBlockElement);
    if (NS_WARN_IF(!leftBlockElement)) {
      return EditActionIgnored(NS_ERROR_UNEXPECTED);
    }
  }
  if (rightBlockElement->IsHTMLElement(nsGkAtoms::hr)) {
    rightBlockElement =
        HTMLEditUtils::GetAncestorBlockElement(*rightBlockElement);
    if (NS_WARN_IF(!rightBlockElement)) {
      return EditActionIgnored(NS_ERROR_UNEXPECTED);
    }
  }

  // Bail if both blocks the same
  if (leftBlockElement == rightBlockElement) {
    return EditActionIgnored();
  }

  // Joining a list item to its parent is a NOP.
  if (HTMLEditUtils::IsList(leftBlockElement) &&
      HTMLEditUtils::IsListItem(rightBlockElement) &&
      rightBlockElement->GetParentNode() == leftBlockElement) {
    return EditActionHandled();
  }

  // Special rule here: if we are trying to join list items, and they are in
  // different lists, join the lists instead.
  bool mergeListElements = false;
  nsAtom* leftListElementTagName = nsGkAtoms::_empty;
  RefPtr<Element> leftListElement, rightListElement;
  if (HTMLEditUtils::IsListItem(leftBlockElement) &&
      HTMLEditUtils::IsListItem(rightBlockElement)) {
    // XXX leftListElement and/or rightListElement may be not list elements.
    leftListElement = leftBlockElement->GetParentElement();
    rightListElement = rightBlockElement->GetParentElement();
    EditorDOMPoint atChildInBlock;
    if (leftListElement && rightListElement &&
        leftListElement != rightListElement &&
        !EditorUtils::IsDescendantOf(*leftListElement, *rightBlockElement,
                                     &atChildInBlock) &&
        !EditorUtils::IsDescendantOf(*rightListElement, *leftBlockElement,
                                     &atChildInBlock)) {
      // There are some special complications if the lists are descendants of
      // the other lists' items.  Note that it is okay for them to be
      // descendants of the other lists themselves, which is the usual case for
      // sublists in our implementation.
      MOZ_DIAGNOSTIC_ASSERT(!atChildInBlock.IsSet());
      leftBlockElement = leftListElement;
      rightBlockElement = rightListElement;
      mergeListElements = true;
      leftListElementTagName = leftListElement->NodeInfo()->NameAtom();
    }
  }

  AutoTransactionsConserveSelection dontChangeMySelection(*this);

  // If the left block element is in the right block element, move the hard
  // line including the right block element to end of the left block.
  // However, if we are merging list elements, we don't join them.
  EditorDOMPoint atRightBlockChild;
  if (EditorUtils::IsDescendantOf(*leftBlockElement, *rightBlockElement,
                                  &atRightBlockChild)) {
    MOZ_ASSERT(atRightBlockChild.GetContainer() == rightBlockElement);
    DebugOnly<bool> advanced = atRightBlockChild.AdvanceOffset();
    NS_WARNING_ASSERTION(
        advanced,
        "Failed to advance offset to after child of rightBlockElement, "
        "leftBlockElement is a descendant of the child");
    nsresult rv =
        WSRunObject::Scrub(*this, EditorDOMPoint::AtEndOf(leftBlockElement));
    if (NS_FAILED(rv)) {
      NS_WARNING("WSRunObject::Scrub() failed at left block");
      return EditActionIgnored(rv);
    }

    {
      // We can't just track rightBlockElement because it's an Element.
      AutoTrackDOMPoint tracker(RangeUpdaterRef(), &atRightBlockChild);
      nsresult rv = WSRunObject::Scrub(*this, atRightBlockChild);
      if (NS_FAILED(rv)) {
        NS_WARNING("WSRunObject::Scrub() failed at right block child");
        return EditActionIgnored(rv);
      }

      // XXX AutoTrackDOMPoint instance, tracker, hasn't been destroyed here.
      //     Do we really need to do update rightBlockElement here??
      if (atRightBlockChild.GetContainerAsElement()) {
        rightBlockElement = atRightBlockChild.GetContainerAsElement();
      } else if (NS_WARN_IF(!atRightBlockChild.GetContainerParentAsElement())) {
        return EditActionIgnored(NS_ERROR_UNEXPECTED);
      } else {
        rightBlockElement = atRightBlockChild.GetContainerParentAsElement();
      }
    }

    // Do br adjustment.
    RefPtr<Element> invisibleBRElement =
        GetInvisibleBRElementAt(EditorDOMPoint::AtEndOf(leftBlockElement));
    EditActionResult ret(NS_OK);
    if (NS_WARN_IF(mergeListElements)) {
      // Since 2002, here was the following comment:
      // > The idea here is to take all children in rightListElement that are
      // > past offset, and pull them into leftlistElement.
      // However, this has never been performed because we are here only when
      // neither left list nor right list is a descendant of the other but
      // in such case, getting a list item in the right list node almost
      // always failed since a variable for offset of
      // rightListElement->GetChildAt() was not initialized.  So, it might be
      // a bug, but we should keep this traditional behavior for now.  If you
      // find when we get here, please remove this comment if we don't need to
      // do it.  Otherwise, please move children of the right list node to the
      // end of the left list node.

      // XXX Although, we don't do nothing here, but for keeping traditional
      //     behavior, we should mark as handled.
      ret.MarkAsHandled();
    } else {
      // XXX Why do we ignore the result of MoveOneHardLineContents()?
      NS_WARNING_ASSERTION(
          rightBlockElement == atRightBlockChild.GetContainer(),
          "The relation is not guaranteed but assumed");
      MoveNodeResult moveNodeResult = MoveOneHardLineContents(
          EditorDOMPoint(rightBlockElement, atRightBlockChild.Offset()),
          EditorDOMPoint(leftBlockElement, 0), MoveToEndOfContainer::Yes);
      if (NS_WARN_IF(moveNodeResult.EditorDestroyed())) {
        return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(moveNodeResult.Succeeded(),
                           "HTMLEditor::MoveOneHardLineContents("
                           "MoveToEndOfContainer::Yes) failed, but ignored");
      if (moveNodeResult.Succeeded()) {
        ret |= moveNodeResult;
      }
      // Now, all children of rightBlockElement were moved to leftBlockElement.
      // So, atRightBlockChild is now invalid.
      atRightBlockChild.Clear();
    }

    if (!invisibleBRElement) {
      return ret;
    }

    rv = DeleteNodeWithTransaction(*invisibleBRElement);
    if (NS_WARN_IF(Destroyed())) {
      return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::DeleteNodeWithTransaction() failed, but ignored");
    if (NS_SUCCEEDED(rv)) {
      ret.MarkAsHandled();
    }
    return ret;
  }

  MOZ_DIAGNOSTIC_ASSERT(!atRightBlockChild.IsSet());

  // If the right block element is in the left block element:
  // - move list item elements in the right block element to where the left
  //   list element is
  // - or first hard line in the right block element to where:
  //   - the left block element is.
  //   - or the given left content in the left block is.
  EditorDOMPoint atLeftBlockChild;
  if (EditorUtils::IsDescendantOf(*rightBlockElement, *leftBlockElement,
                                  &atLeftBlockChild)) {
    MOZ_ASSERT(leftBlockElement == atLeftBlockChild.GetContainer());

    nsresult rv =
        WSRunObject::Scrub(*this, EditorDOMPoint(rightBlockElement, 0));
    if (NS_FAILED(rv)) {
      NS_WARNING("WSRunObject::Scrub() failed at right block");
      return EditActionIgnored(rv);
    }

    {
      // We can't just track leftBlockElement because it's an Element, so track
      // something else.
      AutoTrackDOMPoint tracker(RangeUpdaterRef(), &atLeftBlockChild);
      rv = WSRunObject::Scrub(
          *this, EditorDOMPoint(leftBlockElement, atLeftBlockChild.Offset()));
      if (NS_FAILED(rv)) {
        NS_WARNING("WSRunObject::Scrub() failed at left block child");
        return EditActionIgnored(rv);
      }
      // XXX AutoTrackDOMPoint instance, tracker, hasn't been destroyed here.
      //     Do we really need to do update rightBlockElement here??
      if (atLeftBlockChild.GetContainerAsElement()) {
        leftBlockElement = atLeftBlockChild.GetContainerAsElement();
      } else if (NS_WARN_IF(!atLeftBlockChild.GetContainerParentAsElement())) {
        return EditActionIgnored(NS_ERROR_UNEXPECTED);
      } else {
        leftBlockElement = atLeftBlockChild.GetContainerParentAsElement();
      }
    }

    // Do br adjustment.
    RefPtr<Element> invisibleBRElement =
        GetInvisibleBRElementAt(atLeftBlockChild);
    EditActionResult ret(NS_OK);
    if (mergeListElements) {
      // XXX Why do we ignore the error from MoveChildrenWithTransaction()?
      // XXX Why is it guaranteed that `atLeftBlockChild.GetContainer()` is
      //     `leftListElement` here?  Looks like that above code may run
      //     mutation event listeners.
      NS_WARNING_ASSERTION(leftListElement == atLeftBlockChild.GetContainer(),
                           "This is not guaranteed, but assumed");
      MoveNodeResult moveNodeResult = MoveChildrenWithTransaction(
          *rightListElement,
          EditorDOMPoint(leftListElement, atLeftBlockChild.Offset()));
      if (NS_WARN_IF(moveNodeResult.EditorDestroyed())) {
        return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          moveNodeResult.Succeeded(),
          "HTMLEditor::MoveChildrenWithTransaction() failed, but ignored");
      if (moveNodeResult.Succeeded()) {
        ret |= moveNodeResult;
      }
      // atLeftBlockChild was moved to rightListElement.  So, it's invalid now.
      atLeftBlockChild.Clear();
    } else {
      // Left block is a parent of right block, and the parent of the previous
      // visible content.  Right block is a child and contains the contents we
      // want to move.

      EditorDOMPoint atPreviousContent;
      if (&aLeftContentInBlock == leftBlockElement) {
        // We are working with valid HTML, aLeftContentInBlock is a block node,
        // and is therefore allowed to contain rightBlockElement.  This is the
        // simple case, we will simply move the content in rightBlockElement
        // out of its block.
        atPreviousContent = atLeftBlockChild;
      } else {
        // We try to work as well as possible with HTML that's already invalid.
        // Although "right block" is a block, and a block must not be contained
        // in inline elements, reality is that broken documents do exist.  The
        // DIRECT parent of "left NODE" might be an inline element.  Previous
        // versions of this code skipped inline parents until the first block
        // parent was found (and used "left block" as the destination).
        // However, in some situations this strategy moves the content to an
        // unexpected position.  (see bug 200416) The new idea is to make the
        // moving content a sibling, next to the previous visible content.
        atPreviousContent.Set(&aLeftContentInBlock);

        // We want to move our content just after the previous visible node.
        atPreviousContent.AdvanceOffset();
      }

      MOZ_ASSERT(atPreviousContent.IsSet());

      // Because we don't want the moving content to receive the style of the
      // previous content, we split the previous content's style.

      RefPtr<Element> editingHost = GetActiveEditingHost();
      // XXX It's odd to continue handling this edit action if there is no
      //     editing host.
      if (!editingHost || &aLeftContentInBlock != editingHost) {
        SplitNodeResult splitResult = SplitAncestorStyledInlineElementsAt(
            atPreviousContent, nullptr, nullptr);
        if (splitResult.Failed()) {
          NS_WARNING(
              "HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
          return EditActionIgnored(splitResult.Rv());
        }

        if (splitResult.Handled()) {
          if (splitResult.GetNextNode()) {
            atPreviousContent.Set(splitResult.GetNextNode());
            if (!atPreviousContent.IsSet()) {
              NS_WARNING("Next node of split point was orphaned");
              return EditActionIgnored(NS_ERROR_NULL_POINTER);
            }
          } else {
            atPreviousContent = splitResult.SplitPoint();
            if (!atPreviousContent.IsSet()) {
              NS_WARNING("Split node was orphaned");
              return EditActionIgnored(NS_ERROR_NULL_POINTER);
            }
          }
        }
      }

      ret |= MoveOneHardLineContents(EditorDOMPoint(rightBlockElement, 0),
                                     atPreviousContent);
      if (ret.Failed()) {
        NS_WARNING("HTMLEditor::MoveOneHardLineContents() failed");
        return ret;
      }
    }

    if (!invisibleBRElement) {
      return ret;
    }

    rv = DeleteNodeWithTransaction(*invisibleBRElement);
    if (NS_WARN_IF(Destroyed())) {
      return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::DeleteNodeWithTransaction() failed, but ignored");
    if (NS_SUCCEEDED(rv)) {
      ret.MarkAsHandled();
    }
    return ret;
  }

  MOZ_DIAGNOSTIC_ASSERT(!atRightBlockChild.IsSet());
  MOZ_DIAGNOSTIC_ASSERT(!atLeftBlockChild.IsSet());

  // Normal case.  Blocks are siblings, or at least close enough.  An example
  // of the latter is <p>paragraph</p><ul><li>one<li>two<li>three</ul>.  The
  // first li and the p are not true siblings, but we still want to join them
  // if you backspace from li into p.

  // Adjust whitespace at block boundaries
  nsresult rv = WSRunObject::PrepareToJoinBlocks(*this, *leftBlockElement,
                                                 *rightBlockElement);
  if (NS_FAILED(rv)) {
    NS_WARNING("WSRunObject::PrepareToJoinBlocks() failed");
    return EditActionIgnored(rv);
  }
  // Do br adjustment.
  RefPtr<Element> invisibleBRElement =
      GetInvisibleBRElementAt(EditorDOMPoint::AtEndOf(leftBlockElement));
  EditActionResult ret(NS_OK);
  if (mergeListElements || leftBlockElement->NodeInfo()->NameAtom() ==
                               rightBlockElement->NodeInfo()->NameAtom()) {
    // Nodes are same type.  merge them.
    EditorDOMPoint atFirstChildOfRightNode;
    nsresult rv = JoinNearestEditableNodesWithTransaction(
        *leftBlockElement, *rightBlockElement, &atFirstChildOfRightNode);
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::JoinNearestEditableNodesWithTransaction()"
                         " failed, but ignored");
    if (mergeListElements && atFirstChildOfRightNode.IsSet()) {
      CreateElementResult convertListTypeResult = ChangeListElementType(
          *rightBlockElement, MOZ_KnownLive(*leftListElementTagName),
          *nsGkAtoms::li);
      if (NS_WARN_IF(convertListTypeResult.EditorDestroyed())) {
        return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          convertListTypeResult.Succeeded(),
          "HTMLEditor::ChangeListElementType() failed, but ignored");
    }
    ret.MarkAsHandled();
  } else {
    // Nodes are dissimilar types.
    ret |= MoveOneHardLineContents(EditorDOMPoint(rightBlockElement, 0),
                                   EditorDOMPoint(leftBlockElement, 0),
                                   MoveToEndOfContainer::Yes);
    if (ret.Failed()) {
      NS_WARNING(
          "HTMLEditor::MoveOneHardLineContents(MoveToEndOfContainer::Yes) "
          "failed");
      return ret;
    }
  }

  if (!invisibleBRElement) {
    return ret.MarkAsHandled();
  }

  rv = DeleteNodeWithTransaction(*invisibleBRElement);
  if (NS_WARN_IF(Destroyed())) {
    return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
  }
  // XXX In other top level if blocks, the result of
  //     DeleteNodeWithTransaction() is ignored.  Why does only this result
  //     is respected?
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
    return ret.SetResult(rv);
  }
  return ret.MarkAsHandled();
}

MoveNodeResult HTMLEditor::MoveOneHardLineContents(
    const EditorDOMPoint& aPointInHardLine,
    const EditorDOMPoint& aPointToInsert,
    MoveToEndOfContainer
        aMoveToEndOfContainer /* = MoveToEndOfContainer::No */) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  nsresult rv = SplitInlinesAndCollectEditTargetNodesInOneHardLine(
      aPointInHardLine, arrayOfContents, EditSubAction::eMergeBlockContents,
      HTMLEditor::CollectNonEditableNodes::Yes);
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "HTMLEditor::SplitInlinesAndCollectEditTargetNodesInOneHardLine("
        "eMergeBlockContents, CollectNonEditableNodes::Yes) failed");
    return MoveNodeResult(rv);
  }
  if (arrayOfContents.IsEmpty()) {
    return MoveNodeIgnored(aPointToInsert);
  }

  uint32_t offset = aPointToInsert.Offset();
  MoveNodeResult result;
  for (auto& content : arrayOfContents) {
    if (aMoveToEndOfContainer == MoveToEndOfContainer::Yes) {
      // For backward compatibility, we should move contents to end of the
      // container if this is called with MoveToEndOfContainer::Yes.
      offset = aPointToInsert.GetContainer()->Length();
    }
    // get the node to act on
    if (HTMLEditUtils::IsBlockElement(content)) {
      // For block nodes, move their contents only, then delete block.
      result |= MoveChildrenWithTransaction(
          MOZ_KnownLive(*content->AsElement()),
          EditorDOMPoint(aPointToInsert.GetContainer(), offset));
      if (result.Failed()) {
        NS_WARNING("HTMLEditor::MoveChildrenWithTransaction() failed");
        return result;
      }
      offset = result.NextInsertionPointRef().Offset();
      // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
      // keep it alive.
      DebugOnly<nsresult> rvIgnored =
          DeleteNodeWithTransaction(MOZ_KnownLive(*content));
      if (NS_WARN_IF(Destroyed())) {
        return MoveNodeResult(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rvIgnored),
          "HTMLEditor::DeleteNodeWithTransaction() failed, but ignored");
      result.MarkAsHandled();
      if (MaybeHasMutationEventListeners()) {
        // Mutation event listener may make `offset` value invalid with
        // removing some previous children while we call
        // `DeleteNodeWithTransaction()` so that we should adjust it here.
        offset = std::min(offset, aPointToInsert.GetContainer()->Length());
      }
      continue;
    }
    // XXX Different from the above block, we ignore error of moving nodes.
    // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
    // keep it alive.
    MoveNodeResult moveNodeResult = MoveNodeOrChildrenWithTransaction(
        MOZ_KnownLive(content),
        EditorDOMPoint(aPointToInsert.GetContainer(), offset));
    if (NS_WARN_IF(moveNodeResult.EditorDestroyed())) {
      return MoveNodeResult(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(
        moveNodeResult.Succeeded(),
        "HTMLEditor::MoveNodeOrChildrenWithTransaction() failed, but ignored");
    if (moveNodeResult.Succeeded()) {
      offset = moveNodeResult.NextInsertionPointRef().Offset();
      result |= moveNodeResult;
    }
  }

  NS_WARNING_ASSERTION(
      result.Succeeded(),
      "Last HTMLEditor::MoveNodeOrChildrenWithTransaction() failed");
  return result;
}

MoveNodeResult HTMLEditor::MoveNodeOrChildrenWithTransaction(
    nsIContent& aContent, const EditorDOMPoint& aPointToInsert) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(aPointToInsert.IsSet());

  // Check if this node can go into the destination node
  if (HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(), aContent)) {
    // If it can, move it there.
    uint32_t offsetAtInserting = aPointToInsert.Offset();
    nsresult rv = MoveNodeWithTransaction(aContent, aPointToInsert);
    if (NS_WARN_IF(Destroyed())) {
      return MoveNodeResult(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
      return MoveNodeResult(rv);
    }
    // Advance DOM point with offset for keeping backward compatibility.
    // XXX Where should we insert next content if a mutation event listener
    //     break the relation of offset and moved node?
    return MoveNodeHandled(aPointToInsert.GetContainer(), ++offsetAtInserting);
  }

  // If it can't, move its children (if any), and then delete it.
  MoveNodeResult result;
  if (aContent.IsElement()) {
    result = MoveChildrenWithTransaction(MOZ_KnownLive(*aContent.AsElement()),
                                         aPointToInsert);
    if (result.Failed()) {
      NS_WARNING("HTMLEditor::MoveChildrenWithTransaction() failed");
      return result;
    }
  } else {
    result = MoveNodeHandled(aPointToInsert);
  }

  nsresult rv = DeleteNodeWithTransaction(aContent);
  if (NS_WARN_IF(Destroyed())) {
    return MoveNodeResult(NS_ERROR_EDITOR_DESTROYED);
  }
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
    return MoveNodeResult(rv);
  }
  if (MaybeHasMutationEventListeners()) {
    // Mutation event listener may make `offset` value invalid with
    // removing some previous children while we call
    // `DeleteNodeWithTransaction()` so that we should adjust it here.
    if (!result.NextInsertionPointRef().IsSetAndValid()) {
      result = MoveNodeHandled(
          EditorDOMPoint::AtEndOf(*aPointToInsert.GetContainer()));
    }
  }
  return result;
}

MoveNodeResult HTMLEditor::MoveChildrenWithTransaction(
    Element& aElement, const EditorDOMPoint& aPointToInsert) {
  MOZ_ASSERT(aPointToInsert.IsSet());

  if (NS_WARN_IF(&aElement == aPointToInsert.GetContainer())) {
    return MoveNodeResult(NS_ERROR_INVALID_ARG);
  }

  MoveNodeResult result = MoveNodeIgnored(aPointToInsert);
  while (aElement.GetFirstChild()) {
    result |= MoveNodeOrChildrenWithTransaction(
        MOZ_KnownLive(*aElement.GetFirstChild()), result.NextInsertionPoint());
    if (result.Failed()) {
      NS_WARNING("HTMLEditor::MoveNodeOrChildrenWithTransaction() failed");
      return result;
    }
  }
  return result;
}

void HTMLEditor::MoveAllChildren(nsINode& aContainer,
                                 const EditorRawDOMPoint& aPointToInsert,
                                 ErrorResult& aError) {
  MOZ_ASSERT(!aError.Failed());

  if (!aContainer.HasChildren()) {
    return;
  }
  nsIContent* firstChild = aContainer.GetFirstChild();
  if (NS_WARN_IF(!firstChild)) {
    aError.Throw(NS_ERROR_FAILURE);
    return;
  }
  nsIContent* lastChild = aContainer.GetLastChild();
  if (NS_WARN_IF(!lastChild)) {
    aError.Throw(NS_ERROR_FAILURE);
    return;
  }
  MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert, aError);
  NS_WARNING_ASSERTION(!aError.Failed(),
                       "HTMLEditor::MoveChildrenBetween() failed");
}

void HTMLEditor::MoveChildrenBetween(nsIContent& aFirstChild,
                                     nsIContent& aLastChild,
                                     const EditorRawDOMPoint& aPointToInsert,
                                     ErrorResult& aError) {
  nsCOMPtr<nsINode> oldContainer = aFirstChild.GetParentNode();
  if (NS_WARN_IF(oldContainer != aLastChild.GetParentNode()) ||
      NS_WARN_IF(!aPointToInsert.IsSet()) ||
      NS_WARN_IF(!aPointToInsert.CanContainerHaveChildren())) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  // First, store all children which should be moved to the new container.
  AutoTArray<nsCOMPtr<nsIContent>, 10> children;
  for (nsIContent* child = &aFirstChild; child;
       child = child->GetNextSibling()) {
    children.AppendElement(child);
    if (child == &aLastChild) {
      break;
    }
  }

  if (NS_WARN_IF(children.LastElement() != &aLastChild)) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  nsCOMPtr<nsINode> newContainer = aPointToInsert.GetContainer();
  nsCOMPtr<nsIContent> nextNode = aPointToInsert.GetChild();
  for (size_t i = children.Length(); i > 0; --i) {
    nsCOMPtr<nsIContent>& child = children[i - 1];
    if (child->GetParentNode() != oldContainer) {
      // If the child has been moved to different container, we shouldn't
      // touch it.
      continue;
    }
    oldContainer->RemoveChild(*child, aError);
    if (aError.Failed()) {
      NS_WARNING("nsINode::RemoveChild() failed");
      return;
    }
    if (nextNode) {
      // If we're not appending the children to the new container, we should
      // check if referring next node of insertion point is still in the new
      // container.
      EditorRawDOMPoint pointToInsert(nextNode);
      if (NS_WARN_IF(!pointToInsert.IsSet()) ||
          NS_WARN_IF(pointToInsert.GetContainer() != newContainer)) {
        // The next node of insertion point has been moved by mutation observer.
        // Let's stop moving the remaining nodes.
        // XXX Or should we move remaining children after the last moved child?
        aError.Throw(NS_ERROR_FAILURE);
        return;
      }
    }
    newContainer->InsertBefore(*child, nextNode, aError);
    if (aError.Failed()) {
      NS_WARNING("nsINode::InsertBefore() failed");
      return;
    }
    // If the child was inserted or appended properly, the following children
    // should be inserted before it.  Otherwise, keep using current position.
    if (child->GetParentNode() == newContainer) {
      nextNode = child;
    }
  }
}

void HTMLEditor::MovePreviousSiblings(nsIContent& aChild,
                                      const EditorRawDOMPoint& aPointToInsert,
                                      ErrorResult& aError) {
  MOZ_ASSERT(!aError.Failed());

  if (NS_WARN_IF(!aChild.GetParentNode())) {
    aError.Throw(NS_ERROR_INVALID_ARG);
    return;
  }
  nsIContent* firstChild = aChild.GetParentNode()->GetFirstChild();
  if (NS_WARN_IF(!firstChild)) {
    aError.Throw(NS_ERROR_FAILURE);
    return;
  }
  nsIContent* lastChild =
      &aChild == firstChild ? firstChild : aChild.GetPreviousSibling();
  if (NS_WARN_IF(!lastChild)) {
    aError.Throw(NS_ERROR_FAILURE);
    return;
  }
  MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert, aError);
  NS_WARNING_ASSERTION(!aError.Failed(),
                       "HTMLEditor::MoveChildrenBetween() failed");
}

nsresult HTMLEditor::DeleteElementsExceptTableRelatedElements(nsINode& aNode) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  if (!HTMLEditUtils::IsTableElementButNotTable(&aNode)) {
    nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*aNode.AsContent()));
    if (NS_WARN_IF(Destroyed())) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::DeleteNodeWithTransaction() failed");
    return rv;
  }

  // XXX For performance, this should just call
  //     DeleteElementsExceptTableRelatedElements() while there are children
  //     in aNode.  If we need to avoid infinite loop because mutation event
  //     listeners can add unexpected nodes into aNode, we should just loop
  //     only original count of the children.
  AutoTArray<OwningNonNull<nsIContent>, 10> childList;
  for (nsIContent* child = aNode.GetFirstChild(); child;
       child = child->GetNextSibling()) {
    childList.AppendElement(*child);
  }

  for (const auto& child : childList) {
    // MOZ_KnownLive because 'childList' is guaranteed to
    // keep it alive.
    nsresult rv =
        DeleteElementsExceptTableRelatedElements(MOZ_KnownLive(child));
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::DeleteElementsExceptTableRelatedElements() failed");
      return rv;
    }
  }
  return NS_OK;
}

nsresult HTMLEditor::DeleteMostAncestorMailCiteElementIfEmpty(
    nsIContent& aContent) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  // The element must be `<blockquote type="cite">` or
  // `<span _moz_quote="true">`.
  RefPtr<Element> mailCiteElement = GetMostAncestorMailCiteElement(aContent);
  if (!mailCiteElement) {
    return NS_OK;
  }
  bool seenBR = false;
  if (!IsEmptyNodeImpl(*mailCiteElement, true, true, false, &seenBR)) {
    return NS_OK;
  }
  EditorDOMPoint atEmptyMailCiteElement(mailCiteElement);
  {
    AutoEditorDOMPointChildInvalidator lockOffset(atEmptyMailCiteElement);
    nsresult rv = DeleteNodeWithTransaction(*mailCiteElement);
    if (NS_WARN_IF(Destroyed())) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
      return rv;
    }
  }

  if (!atEmptyMailCiteElement.IsSet() || !seenBR) {
    NS_WARNING_ASSERTION(
        atEmptyMailCiteElement.IsSet(),
        "Mutation event listener might changed the DOM tree during "
        "HTMLEditor::DeleteNodeWithTransaction(), but ignored");
    return NS_OK;
  }

  RefPtr<Element> brElement =
      InsertBRElementWithTransaction(atEmptyMailCiteElement);
  if (NS_WARN_IF(Destroyed())) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  if (!brElement) {
    NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed");
    return NS_ERROR_FAILURE;
  }
  nsresult rv = CollapseSelectionTo(EditorRawDOMPoint(brElement));
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::::CollapseSelectionTo() failed, but ignored");
  return NS_OK;
}

EditActionResult HTMLEditor::MakeOrChangeListAndListItemAsSubAction(
    nsAtom& aListElementOrListItemElementTagName, const nsAString& aBulletType,
    SelectAllOfCurrentList aSelectAllOfCurrentList) {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(&aListElementOrListItemElementTagName == nsGkAtoms::ul ||
             &aListElementOrListItemElementTagName == nsGkAtoms::ol ||
             &aListElementOrListItemElementTagName == nsGkAtoms::dl ||
             &aListElementOrListItemElementTagName == nsGkAtoms::dd ||
             &aListElementOrListItemElementTagName == nsGkAtoms::dt);

  if (NS_WARN_IF(!mInitSucceeded)) {
    return EditActionIgnored(NS_ERROR_NOT_INITIALIZED);
  }

  EditActionResult result = CanHandleHTMLEditSubAction();
  if (result.Failed() || result.Canceled()) {
    NS_WARNING_ASSERTION(result.Succeeded(),
                         "HTMLEditor::CanHandleHTMLEditSubAction() failed");
    return result;
  }

  if (IsSelectionRangeContainerNotContent()) {
    NS_WARNING("Some selection containers are not content node, but ignored");
    return EditActionIgnored();
  }

  AutoPlaceholderBatch treatAsOneTransaction(*this);

  // 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 ignoredError;
  AutoEditSubActionNotifier startToHandleEditSubAction(
      *this,
      &aListElementOrListItemElementTagName == nsGkAtoms::dd ||
              &aListElementOrListItemElementTagName == nsGkAtoms::dt
          ? EditSubAction::eCreateOrChangeDefinitionListItem
          : EditSubAction::eCreateOrChangeList,
      nsIEditor::eNext, ignoredError);
  if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
    return EditActionResult(ignoredError.StealNSResult());
  }
  NS_WARNING_ASSERTION(
      !ignoredError.Failed(),
      "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");

  nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
                       "failed, but ignored");

  if (NS_SUCCEEDED(rv) && SelectionRefPtr()->IsCollapsed()) {
    nsresult rv = EnsureCaretNotAfterPaddingBRElement();
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::EnsureCaretNotAfterPaddingBRElement() "
                         "failed, but ignored");
    if (NS_SUCCEEDED(rv)) {
      nsresult rv = PrepareInlineStylesForCaret();
      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
    }
  }

  nsAtom* listTagName = nullptr;
  nsAtom* listItemTagName = nullptr;
  if (&aListElementOrListItemElementTagName == nsGkAtoms::ul ||
      &aListElementOrListItemElementTagName == nsGkAtoms::ol) {
    listTagName = &aListElementOrListItemElementTagName;
    listItemTagName = nsGkAtoms::li;
  } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dl) {
    listTagName = &aListElementOrListItemElementTagName;
    listItemTagName = nsGkAtoms::dd;
  } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dd ||
             &aListElementOrListItemElementTagName == nsGkAtoms::dt) {
    listTagName = nsGkAtoms::dl;
    listItemTagName = &aListElementOrListItemElementTagName;
  } else {
    NS_WARNING(
        "aListElementOrListItemElementTagName was neither list element name "
        "nor "
        "definition listitem element name");
    return EditActionResult(NS_ERROR_INVALID_ARG);
  }

  // Expands selection range to include the immediate block parent, and then
  // further expands to include any ancestors whose children are all in the
  // range.
  if (!SelectionRefPtr()->IsCollapsed()) {
    nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction();
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() "
          "failed");
      return EditActionResult(rv);
    }
  }

  // ChangeSelectedHardLinesToList() creates AutoSelectionRestorer.
  // Therefore, even if it returns NS_OK, editor might have been destroyed
  // at restoring Selection.
  result = ChangeSelectedHardLinesToList(MOZ_KnownLive(*listTagName),
                                         MOZ_KnownLive(*listItemTagName),
                                         aBulletType, aSelectAllOfCurrentList);
  if (NS_WARN_IF(Destroyed())) {
    return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
  }
  NS_WARNING_ASSERTION(result.Succeeded(),
                       "HTMLEditor::ChangeSelectedHardLinesToList() failed");
  return result;
}

EditActionResult HTMLEditor::ChangeSelectedHardLinesToList(
    nsAtom& aListElementTagName, nsAtom& aListItemElementTagName,
    const nsAString& aBulletType,
    SelectAllOfCurrentList aSelectAllOfCurrentList) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
  MOZ_ASSERT(!IsSelectionRangeContainerNotContent());

  AutoSelectionRestorer restoreSelectionLater(*this);

  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  Element* parentListElement =
      aSelectAllOfCurrentList == SelectAllOfCurrentList::Yes
          ? GetParentListElementAtSelection()
          : nullptr;
  if (parentListElement) {
    arrayOfContents.AppendElement(
        OwningNonNull<nsIContent>(*parentListElement));
  } else {
    AutoTransactionsConserveSelection dontChangeMySelection(*this);
    nsresult rv =
        SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(
            arrayOfContents, EditSubAction::eCreateOrChangeList,
            CollectNonEditableNodes::No);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::"
          "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges("
          "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
      return EditActionResult(rv);
    }
  }

  // check if all our nodes are <br>s, or empty inlines
  bool bOnlyBreaks = true;
  for (auto& content : arrayOfContents) {
    // if content is not a Break or empty inline, we're done
    if (!content->IsHTMLElement(nsGkAtoms::br) && !IsEmptyInlineNode(content)) {
      bOnlyBreaks = false;
      break;
    }
  }

  // if no nodes, we make empty list.  Ditto if the user tried to make a list
  // of some # of breaks.
  if (arrayOfContents.IsEmpty() || bOnlyBreaks) {
    // if only breaks, delete them
    if (bOnlyBreaks) {
      for (auto& content : arrayOfContents) {
        // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
        // keep it alive.
        nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
        if (NS_WARN_IF(Destroyed())) {
          return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
        }
        if (NS_FAILED(rv)) {
          NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
          return EditActionResult(rv);
        }
      }
    }

    const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
    if (NS_WARN_IF(!firstRange)) {
      return EditActionResult(NS_ERROR_FAILURE);
    }

    EditorDOMPoint atStartOfSelection(firstRange->StartRef());
    if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
      return EditActionResult(NS_ERROR_FAILURE);
    }

    // Make sure we can put a list here.
    if (!HTMLEditUtils::CanNodeContain(*atStartOfSelection.GetContainer(),
                                       aListElementTagName)) {
      return EditActionCanceled();
    }

    SplitNodeResult splitAtSelectionStartResult =
        MaybeSplitAncestorsForInsertWithTransaction(aListElementTagName,
                                                    atStartOfSelection);
    if (splitAtSelectionStartResult.Failed()) {
      NS_WARNING(
          "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed");
      return EditActionResult(splitAtSelectionStartResult.Rv());
    }
    RefPtr<Element> theList = CreateNodeWithTransaction(
        aListElementTagName, splitAtSelectionStartResult.SplitPoint());
    if (NS_WARN_IF(Destroyed())) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    if (!theList) {
      NS_WARNING("EditorBase::CreateNodeWithTransaction() failed");
      return EditActionResult(NS_ERROR_FAILURE);
    }

    RefPtr<Element> theListItem = CreateNodeWithTransaction(
        aListItemElementTagName, EditorDOMPoint(theList, 0));
    if (NS_WARN_IF(Destroyed())) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    if (!theListItem) {
      NS_WARNING("EditorBase::CreateNodeWithTransaction() failed");
      return EditActionResult(NS_ERROR_FAILURE);
    }

    // remember our new block for postprocessing
    TopLevelEditSubActionDataRef().mNewBlockElement = theListItem;
    // Put selection in new list item and don't restore the Selection.
    restoreSelectionLater.Abort();
    nsresult rv = CollapseSelectionToStartOf(*theListItem);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::CollapseSelectionToStartOf() failed");
    return EditActionResult(rv);
  }

  // if there is only one node in the array, and it is a list, div, or
  // blockquote, then look inside of it until we find inner list or content.
  if (arrayOfContents.Length() == 1) {
    if (Element* deepestDivBlockquoteOrListElement =
            GetDeepestEditableOnlyChildDivBlockquoteOrListElement(
                arrayOfContents[0])) {
      if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements(
              nsGkAtoms::div, nsGkAtoms::blockquote)) {
        arrayOfContents.Clear();
        CollectChildren(*deepestDivBlockquoteOrListElement, arrayOfContents, 0,
                        CollectListChildren::No, CollectTableChildren::No,
                        CollectNonEditableNodes::Yes);
      } else {
        arrayOfContents.ReplaceElementAt(
            0, OwningNonNull<nsIContent>(*deepestDivBlockquoteOrListElement));
      }
    }
  }

  // Ok, now go through all the nodes and put then in the list,
  // or whatever is approriate.  Wohoo!

  uint32_t countOfCollectedContents = arrayOfContents.Length();
  RefPtr<Element> curList, prevListItem;

  for (uint32_t i = 0; i < countOfCollectedContents; i++) {
    // here's where we actually figure out what to do
    OwningNonNull<nsIContent> content = arrayOfContents[i];

    // make sure we don't assemble content that is in different table cells
    // into the same list.  respect table cell boundaries when listifying.
    if (curList &&
        HTMLEditor::NodesInDifferentTableElements(*curList, content)) {
      curList = 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(content, EditorType::HTML) &&
        (content->IsHTMLElement(nsGkAtoms::br) || IsEmptyInlineNode(content))) {
      nsresult rv = DeleteNodeWithTransaction(*content);
      if (NS_WARN_IF(Destroyed())) {
        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
        return EditActionResult(rv);
      }
      if (content->IsHTMLElement(nsGkAtoms::br)) {
        prevListItem = nullptr;
      }
      continue;
    }

    if (HTMLEditUtils::IsList(content)) {
      // If we met a list element and current list element is not a descendant
      // of the list, append current node to end of the current list element.
      // Then, wrap it with list item element and delete the old container.
      if (curList && !EditorUtils::IsDescendantOf(*content, *curList)) {
        nsresult rv = MoveNodeToEndWithTransaction(*content, *curList);
        if (NS_WARN_IF(Destroyed())) {
          return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
        }
        if (NS_FAILED(rv)) {
          NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
          return EditActionResult(rv);
        }
        CreateElementResult convertListTypeResult =
            ChangeListElementType(MOZ_KnownLive(*content->AsElement()),
                                  aListElementTagName, aListItemElementTagName);
        if (convertListTypeResult.Failed()) {
          NS_WARNING("HTMLEditor::ChangeListElementType() failed");
          return EditActionResult(convertListTypeResult.Rv());
        }
        rv = RemoveBlockContainerWithTransaction(
            MOZ_KnownLive(*convertListTypeResult.GetNewNode()));
        if (NS_WARN_IF(Destroyed())) {
          return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
        }
        if (NS_FAILED(rv)) {
          NS_WARNING(
              "HTMLEditor::RemoveBlockContainerWithTransaction() failed");
          return EditActionResult(rv);
        }
        prevListItem = nullptr;
        continue;
      }

      // If current list element is in found list element or we've not met a
      // list element, convert current list element to proper type.
      CreateElementResult convertListTypeResult =
          ChangeListElementType(MOZ_KnownLive(*content->AsElement()),
                                aListElementTagName, aListItemElementTagName);
      if (convertListTypeResult.Failed()) {
        NS_WARNING("HTMLEditor::ChangeListElementType() failed");
        return EditActionResult(convertListTypeResult.Rv());
      }
      curList = convertListTypeResult.forget();
      prevListItem = nullptr;
      continue;
    }

    EditorDOMPoint atContent(content);
    if (NS_WARN_IF(!atContent.IsSet())) {
      return EditActionResult(NS_ERROR_FAILURE);
    }
    MOZ_ASSERT(atContent.IsSetAndValid());
    if (HTMLEditUtils::IsListItem(content)) {
      // If current list item element is not in proper list element, we need
      // to conver the list element.
      if (!atContent.IsContainerHTMLElement(&aListElementTagName)) {
        // If we've not met a list element or current node is not in current
        // list element, insert a list element at current node and set
        // current list element to the new one.
        if (!curList || EditorUtils::IsDescendantOf(*content, *curList)) {
          if (NS_WARN_IF(!atContent.GetContainerAsContent())) {
            return EditActionResult(NS_ERROR_FAILURE);
          }
          ErrorResult error;
          nsCOMPtr<nsIContent> newLeftNode =
              SplitNodeWithTransaction(atContent, error);
          if (NS_WARN_IF(Destroyed())) {
            error.SuppressException();
            return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
          }
          if (error.Failed()) {
            NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
            return EditActionResult(error.StealNSResult());
          }
          curList = CreateNodeWithTransaction(
              aListElementTagName, EditorDOMPoint(atContent.GetContainer()));
          if (NS_WARN_IF(Destroyed())) {
            return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
          }
          if (!curList) {
            NS_WARNING("EditorBase::CreateNodeWithTransaction() failed");
            return EditActionResult(NS_ERROR_FAILURE);
          }
        }
        // Then, move current node into current list element.
        nsresult rv = MoveNodeToEndWithTransaction(*content, *curList);
        if (NS_WARN_IF(Destroyed())) {
          return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
        }
        if (NS_FAILED(rv)) {
          NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
          return EditActionResult(rv);
        }
        // Convert list item type if current node is different list item type.
        if (!content->IsHTMLElement(&aListItemElementTagName)) {
          RefPtr<Element> newListItemElement = ReplaceContainerWithTransaction(
              MOZ_KnownLive(*content->AsElement()), aListItemElementTagName);
          if (NS_WARN_IF(Destroyed())) {
            return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
          }
          if (!newListItemElement) {
            NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed");
            return EditActionResult(NS_ERROR_FAILURE);
          }
        }
      } else {
        // If we've not met a list element, set current list element to the
        // parent of current list item element.
        if (!curList) {
          curList = atContent.GetContainerAsElement();
          NS_WARNING_ASSERTION(
              HTMLEditUtils::IsList(curList),
              "Current list item parent is not a list element");
        }
        // If current list item element is not a child of current list element,
        // move it into current list item.
        else if (atContent.GetContainer() != curList) {
          nsresult rv = MoveNodeToEndWithTransaction(*content, *curList);
          if (NS_WARN_IF(Destroyed())) {
            return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
          }
          if (NS_FAILED(rv)) {
            NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
            return EditActionResult(rv);
          }
        }
        // Then, if current list item element is not proper type for current
        // list element, convert list item element to proper element.
        if (!content->IsHTMLElement(&aListItemElementTagName)) {
          RefPtr<Element> newListItemElement = ReplaceContainerWithTransaction(
              MOZ_KnownLive(*content->AsElement()), aListItemElementTagName);
          if (NS_WARN_IF(Destroyed())) {
            return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
          }
          if (!newListItemElement) {
            NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed");
            return EditActionResult(NS_ERROR_FAILURE);
          }
        }
      }
      Element* element = Element::FromNode(content);
      if (NS_WARN_IF(!element)) {
        return EditActionResult(NS_ERROR_FAILURE);
      }
      // If bullet type is specified, set list type attribute.
      // XXX Cannot we set type attribute before inserting the list item
      //     element into the DOM tree?
      if (!aBulletType.IsEmpty()) {
        nsresult rv = SetAttributeWithTransaction(
            MOZ_KnownLive(*element), *nsGkAtoms::type, aBulletType);
        if (NS_WARN_IF(Destroyed())) {
          return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
        }
        if (NS_FAILED(rv)) {
          NS_WARNING(
              "EditorBase::SetAttributeWithTransaction(nsGkAtoms::type) "
              "failed");
          return EditActionResult(rv);
        }
        continue;
      }

      // Otherwise, remove list type attribute if there is.
      if (!element->HasAttr(nsGkAtoms::type)) {
        continue;
      }
      nsresult rv = RemoveAttributeWithTransaction(MOZ_KnownLive(*element),
                                                   *nsGkAtoms::type);
      if (NS_WARN_IF(Destroyed())) {
        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
      }
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::type) "
            "failed");
        return EditActionResult(rv);
      }
      continue;
    }

    MOZ_ASSERT(!HTMLEditUtils::IsList(content) &&
               !HTMLEditUtils::IsListItem(content));

    // If current node is a `<div>` element, replace it in the array with
    // its children.
    // XXX I think that this should be done when we collect the nodes above.
    //     Then, we can change this `for` loop to ranged-for loop.
    if (content->IsHTMLElement(nsGkAtoms::div)) {
      prevListItem = nullptr;
      CollectChildren(*content, arrayOfContents, i + 1,
                      CollectListChildren::Yes, CollectTableChildren::Yes,
                      CollectNonEditableNodes::Yes);
      nsresult rv =
          RemoveContainerWithTransaction(MOZ_KnownLive(*content->AsElement()));
      if (NS_WARN_IF(Destroyed())) {
        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
        return EditActionResult(rv);
      }
      // Extend the loop length to handle all children collected here.
      countOfCollectedContents = arrayOfContents.Length();
      continue;
    }

    // If we've not met a list element, create a list element and make it
    // current list element.
    if (!curList) {
      SplitNodeResult splitCurNodeResult =
          MaybeSplitAncestorsForInsertWithTransaction(aListElementTagName,
                                                      atContent);
      if (splitCurNodeResult.Failed()) {
        NS_WARNING(
            "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed");
        return EditActionResult(splitCurNodeResult.Rv());
      }
      prevListItem = nullptr;
      curList = CreateNodeWithTransaction(aListElementTagName,
                                          splitCurNodeResult.SplitPoint());
      if (NS_WARN_IF(Destroyed())) {
        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
      }
      if (!curList) {
        NS_WARNING("EditorBase::CreateNodeWithTransaction() failed");
        return EditActionResult(NS_ERROR_FAILURE);
      }
      // Set new block element of top level edit sub-action to the new list
      // element for setting selection into it.
      // XXX This must be wrong.  If we're handling nested edit action,
      //     we shouldn't overwrite the new block element.
      TopLevelEditSubActionDataRef().mNewBlockElement = curList;

      // atContent is now referring the right node with mOffset but
      // referring the left node with mRef.  So, invalidate it now.
      atContent.Clear();
    }

    // If we're currently handling contents of a list item and current node
    // is not a block element, move current node into the list item.
    if (HTMLEditUtils::IsInlineElement(content) && prevListItem) {
      nsresult rv = MoveNodeToEndWithTransaction(*content, *prevListItem);
      if (NS_WARN_IF(Destroyed())) {
        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
        return EditActionResult(rv);
      }
      continue;
    }

    // If current node is a paragraph, that means that it does not contain
    // block children so that we can just replace it with new list item
    // element and move it into current list element.
    // XXX This is too rough handling.  If web apps modifies DOM tree directly,
    //     any elements can have block elements as children.
    if (content->IsHTMLElement(nsGkAtoms::p)) {
      RefPtr<Element> newListItemElement = ReplaceContainerWithTransaction(
          MOZ_KnownLive(*content->AsElement()), aListItemElementTagName);
      if (NS_WARN_IF(Destroyed())) {
        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
      }
      if (!newListItemElement) {
        NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed");
        return EditActionResult(NS_ERROR_FAILURE);
      }
      prevListItem = nullptr;
      nsresult rv = MoveNodeToEndWithTransaction(*newListItemElement, *curList);
      if (NS_WARN_IF(Destroyed())) {
        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
        return EditActionResult(rv);
      }
      // XXX Why don't we set `type` attribute here??
      continue;
    }

    // If current node is not a paragraph, wrap current node with new list
    // item element and move it into current list element.
    RefPtr<Element> newListItemElement =
        InsertContainerWithTransaction(*content, aListItemElementTagName);
    if (NS_WARN_IF(Destroyed())) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    if (!newListItemElement) {
      NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
      return EditActionResult(NS_ERROR_FAILURE);
    }
    // If current node is not a block element, new list item should have
    // following inline nodes too.
    if (HTMLEditUtils::IsInlineElement(content)) {
      prevListItem = newListItemElement;
    } else {
      prevListItem = nullptr;
    }
    nsresult rv = MoveNodeToEndWithTransaction(*newListItemElement, *curList);
    if (NS_WARN_IF(Destroyed())) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
      return EditActionResult(rv);
    }
    // XXX Why don't we set `type` attribute here??
  }

  return EditActionHandled();
}

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

  EditActionResult result = CanHandleHTMLEditSubAction();
  if (result.Failed() || result.Canceled()) {
    NS_WARNING_ASSERTION(result.Succeeded(),
                         "HTMLEditor::CanHandleHTMLEditSubAction() failed");
    return result.Rv();
  }

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

  if (!SelectionRefPtr()->IsCollapsed()) {
    nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction();
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() "
          "failed");
      return rv;
    }
  }

  AutoSelectionRestorer restoreSelectionLater(*this);

  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  {
    AutoTransactionsConserveSelection dontChangeMySelection(*this);
    nsresult rv =
        SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(
            arrayOfContents, EditSubAction::eCreateOrChangeList,
            CollectNonEditableNodes::No);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::"
          "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges("
          "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
      return rv;
    }
  }

  // Remove all non-editable nodes.  Leave them be.
  // XXX SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges()
  //     should return only editable contents when it's called with
  //     CollectNonEditableNodes::No.
  for (int32_t i = arrayOfContents.Length() - 1; i >= 0; i--) {
    OwningNonNull<nsIContent>& content = arrayOfContents[i];
    if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
      arrayOfContents.RemoveElementAt(i);
    }
  }

  // Only act on lists or list items in the array
  for (auto& content : arrayOfContents) {
    // here's where we actually figure out what to do
    if (HTMLEditUtils::IsListItem(content)) {
      // unlist this listitem
      nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()),
                                          LiftUpFromAllParentListElements::Yes);
      if (NS_FAILED(rv)) {
        NS_WARNING(
            "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:"
            ":Yes) failed");
        return rv;
      }
      continue;
    }
    if (HTMLEditUtils::IsList(content)) {
      // node is a list, move list items out
      nsresult rv =
          DestroyListStructureRecursively(MOZ_KnownLive(*content->AsElement()));
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DestroyListStructureRecursively() failed");
        return rv;
      }
      continue;
    }
  }
  return NS_OK;
}

nsresult HTMLEditor::FormatBlockContainerWithTransaction(nsAtom& blockType) {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());

  if (!SelectionRefPtr()->IsCollapsed()) {
    nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction();
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() "
          "failed");
      return rv;
    }
  }

  AutoSelectionRestorer restoreSelectionLater(*this);
  AutoTransactionsConserveSelection dontChangeMySelection(*this);

  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  nsresult rv = SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(
      arrayOfContents, EditSubAction::eCreateOrRemoveBlock,
      CollectNonEditableNodes::Yes);
  if (NS_FAILED(rv)) {
    NS_WARNING(
        "HTMLEditor::"
        "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges("
        "eCreateOrRemoveBlock, CollectNonEditableNodes::Yes) failed");
    return rv;
  }

  // If there is no visible and editable nodes in the edit targets, make an
  // empty block.
  // XXX Isn't this odd if there are only non-editable visible nodes?
  if (IsEmptyOneHardLine(arrayOfContents)) {
    const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
    if (NS_WARN_IF(!firstRange)) {
      return NS_ERROR_FAILURE;
    }

    EditorDOMPoint pointToInsertBlock(firstRange->StartRef());
    if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) {
      if (!pointToInsertBlock.IsInContentNode()) {
        NS_WARNING(
            "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find "
            "block parent because container of the point is not content");
        return NS_ERROR_FAILURE;
      }
      // We are removing blocks (going to "body text")
      RefPtr<Element> blockElement =
          HTMLEditUtils::GetInclusiveAncestorBlockElement(
              *pointToInsertBlock.ContainerAsContent());
      if (!blockElement) {
        NS_WARNING(
            "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find "
            "block parent");
        return NS_ERROR_FAILURE;
      }
      if (!HTMLEditUtils::IsFormatNode(blockElement)) {
        return NS_OK;
      }

      // If the first editable node after selection is a br, consume it.
      // Otherwise it gets pushed into a following block after the split,
      // which is visually bad.
      nsCOMPtr<nsIContent> brContent =
          GetNextEditableHTMLNode(pointToInsertBlock);
      if (brContent && brContent->IsHTMLElement(nsGkAtoms::br)) {
        AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
        rv = DeleteNodeWithTransaction(*brContent);
        if (NS_WARN_IF(Destroyed())) {
          return NS_ERROR_EDITOR_DESTROYED;
        }
        if (NS_FAILED(rv)) {
          NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
          return rv;
        }
      }
      // Do the splits!
      SplitNodeResult splitNodeResult = SplitNodeDeepWithTransaction(
          *blockElement, pointToInsertBlock,
          SplitAtEdges::eDoNotCreateEmptyContainer);
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (splitNodeResult.Failed()) {
        NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
        return splitNodeResult.Rv();
      }
      EditorDOMPoint pointToInsertBRNode(splitNodeResult.SplitPoint());
      // Put a <br> element at the split point
      brContent = InsertBRElementWithTransaction(pointToInsertBRNode);
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (!brContent) {
        NS_WARNING("HTMLEditor::InsertBRElementWithTransaction() failed");
        return NS_ERROR_FAILURE;
      }
      // Don't restore the selection
      restoreSelectionLater.Abort();
      // Put selection at the split point
      nsresult rv = CollapseSelectionTo(EditorRawDOMPoint(brContent));
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "HTMLEditor::CollapseSelectionTo() failed");
      return rv;
    }

    // We are making a block.  Consume a br, if needed.
    nsCOMPtr<nsIContent> brNode =
        GetNextEditableHTMLNodeInBlock(pointToInsertBlock);
    if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) {
      AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
      rv = DeleteNodeWithTransaction(*brNode);
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
        return rv;
      }
      // We don't need to act on this node any more
      arrayOfContents.RemoveElement(brNode);
    }
    // Make sure we can put a block here.
    SplitNodeResult splitNodeResult =
        MaybeSplitAncestorsForInsertWithTransaction(blockType,
                                                    pointToInsertBlock);
    if (splitNodeResult.Failed()) {
      NS_WARNING(
          "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed");
      return splitNodeResult.Rv();
    }
    RefPtr<Element> block =
        CreateNodeWithTransaction(blockType, splitNodeResult.SplitPoint());
    if (NS_WARN_IF(Destroyed())) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    if (!block) {
      NS_WARNING("CreateNodeWithTransaction() failed");
      return NS_ERROR_FAILURE;
    }
    // Remember our new block for postprocessing
    TopLevelEditSubActionDataRef().mNewBlockElement = block;
    // 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.
      rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
        return rv;
      }
      arrayOfContents.RemoveElementAt(0);
    }
    // Don't restore the selection
    restoreSelectionLater.Abort();
    // Put selection in new block
    rv = CollapseSelectionToStartOf(*block);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::CollapseSelectionToStartOf() failed");
    return rv;
  }
  // Okay, now go through all the nodes and make the right kind of blocks, or
  // whatever is approriate.  Woohoo!  Note: blockquote is handled a little
  // differently.
  if (&blockType == nsGkAtoms::blockquote) {
    nsresult rv = MoveNodesIntoNewBlockquoteElement(arrayOfContents);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::MoveNodesIntoNewBlockquoteElement() failed");
    return rv;
  }
  if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) {
    nsresult rv = RemoveBlockContainerElements(arrayOfContents);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::RemoveBlockContainerElements() failed");
    return rv;
  }
  rv = CreateOrChangeBlockContainerElement(arrayOfContents, blockType);
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::CreateOrChangeBlockContainerElement() failed");
  return rv;
}

nsresult HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(!IsSelectionRangeContainerNotContent());

  if (!SelectionRefPtr()->IsCollapsed()) {
    return NS_OK;
  }

  const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
  if (NS_WARN_IF(!firstRange)) {
    return NS_ERROR_FAILURE;
  }
  const RangeBoundary& atStartOfSelection = firstRange->StartRef();
  if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
    return NS_ERROR_FAILURE;
  }
  if (!atStartOfSelection.Container()->IsElement()) {
    return NS_OK;
  }
  OwningNonNull<Element> startContainerElement =
      *atStartOfSelection.Container()->AsElement();
  nsresult rv =
      InsertPaddingBRElementForEmptyLastLineIfNeeded(startContainerElement);
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::InsertPaddingBRElementForEmptyLastLineIfNeeded() failed");
  return rv;
}

EditActionResult HTMLEditor::IndentAsSubAction() {
  MOZ_ASSERT(IsEditActionDataAvailable());

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

  EditActionResult result = CanHandleHTMLEditSubAction();
  if (result.Failed() || result.Canceled()) {
    NS_WARNING_ASSERTION(result.Succeeded(),
                         "HTMLEditor::CanHandleHTMLEditSubAction() failed");
    return result;
  }

  if (IsSelectionRangeContainerNotContent()) {
    NS_WARNING("Some selection containers are not content node, but ignored");
    return EditActionIgnored();
  }

  result |= HandleIndentAtSelection();
  if (result.Failed() || result.Canceled()) {
    NS_WARNING_ASSERTION(result.Succeeded(),
                         "HTMLEditor::HandleIndentAtSelection() failed");
    return result;
  }

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

  nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() failed");
  return result.SetResult(rv);
}

// Helper for Handle[CSS|HTML]IndentAtSelectionInternal
nsresult HTMLEditor::IndentListChild(RefPtr<Element>* aCurList,
                                     const EditorDOMPoint& aCurPoint,
                                     nsIContent& aContent) {
  MOZ_ASSERT(HTMLEditUtils::IsList(aCurPoint.GetContainer()),
             "unexpected container");
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());

  // some logic for putting list items into nested lists...

  // Check for whether we should join a list that follows aContent.
  // We do this if the next element is a list, and the list is of the
  // same type (li/ol) as aContent was a part it.
  if (nsIContent* nextEditableSibling =
          GetNextHTMLSibling(&aContent, SkipWhitespace::Yes)) {
    if (HTMLEditUtils::IsList(nextEditableSibling) &&
        aCurPoint.GetContainer()->NodeInfo()->NameAtom() ==
            nextEditableSibling->NodeInfo()->NameAtom() &&
        aCurPoint.GetContainer()->NodeInfo()->NamespaceID() ==
            nextEditableSibling->NodeInfo()->NamespaceID()) {
      nsresult rv = MoveNodeWithTransaction(
          aContent, EditorDOMPoint(nextEditableSibling, 0));
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "EdigtorBase::MoveNodeWithTransaction() failed");
      return rv;
    }
  }

  // Check for whether we should join a list that preceeds aContent.
  // We do this if the previous element is a list, and the list is of
  // the same type (li/ol) as aContent was a part of.
  if (nsCOMPtr<nsIContent> previousEditableSibling =
          GetPriorHTMLSibling(&aContent, SkipWhitespace::Yes)) {
    if (HTMLEditUtils::IsList(previousEditableSibling) &&
        aCurPoint.GetContainer()->NodeInfo()->NameAtom() ==
            previousEditableSibling->NodeInfo()->NameAtom() &&
        aCurPoint.GetContainer()->NodeInfo()->NamespaceID() ==
            previousEditableSibling->NodeInfo()->NamespaceID()) {
      nsresult rv =
          MoveNodeToEndWithTransaction(aContent, *previousEditableSibling);
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "HTMLEditor::MoveNodeToEndWithTransaction() failed");
      return rv;
    }
  }

  // check to see if aCurList is still appropriate.  Which it is if
  // aContent is still right after it in the same list.
  nsIContent* previousEditableSibling =
      *aCurList ? GetPriorHTMLSibling(&aContent, SkipWhitespace::Yes) : nullptr;
  if (!*aCurList ||
      (previousEditableSibling && previousEditableSibling != *aCurList)) {
    nsAtom* containerName = aCurPoint.GetContainer()->NodeInfo()->NameAtom();
    // Create a new nested list of correct type.
    SplitNodeResult splitNodeResult =
        MaybeSplitAncestorsForInsertWithTransaction(
            MOZ_KnownLive(*containerName), aCurPoint);
    if (splitNodeResult.Failed()) {
      NS_WARNING(
          "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed");
      return splitNodeResult.Rv();
    }
    *aCurList = CreateNodeWithTransaction(MOZ_KnownLive(*containerName),
                                          splitNodeResult.SplitPoint());
    if (NS_WARN_IF(Destroyed())) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    if (!*aCurList) {
      NS_WARNING("EditorBase::CreateNodeWithTransaction() failed");
      return NS_ERROR_FAILURE;
    }
    // aCurList is now the correct thing to put aContent in
    // remember our new block for postprocessing
    TopLevelEditSubActionDataRef().mNewBlockElement = *aCurList;
  }
  // tuck the node into the end of the active list
  RefPtr<nsINode> container = *aCurList;
  nsresult rv = MoveNodeToEndWithTransaction(aContent, *container);
  if (NS_WARN_IF(Destroyed())) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::MoveNodeToEndWithTransaction() failed");
  return rv;
}

EditActionResult HTMLEditor::HandleIndentAtSelection() {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(!IsSelectionRangeContainerNotContent());

  nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
                       "failed, but ignored");

  if (NS_SUCCEEDED(rv) && SelectionRefPtr()->IsCollapsed()) {
    nsresult rv = EnsureCaretNotAfterPaddingBRElement();
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::EnsureCaretNotAfterPaddingBRElement() "
                         "failed, but ignored");
    if (NS_SUCCEEDED(rv)) {
      nsresult rv = PrepareInlineStylesForCaret();
      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
        return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
    }
  }

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

  if (IsCSSEnabled()) {
    nsresult rv = HandleCSSIndentAtSelection();
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::HandleCSSIndentAtSelection() failed");
    return EditActionHandled(rv);
  }
  rv = HandleHTMLIndentAtSelection();
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::HandleHTMLIndent() failed");
  return EditActionHandled(rv);
}

nsresult HTMLEditor::HandleCSSIndentAtSelection() {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(!IsSelectionRangeContainerNotContent());

  if (!SelectionRefPtr()->IsCollapsed()) {
    nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction();
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() "
          "failed");
      return rv;
    }
  }

  // HandleCSSIndentAtSelectionInternal() creates AutoSelectionRestorer.
  // Therefore, even if it returns NS_OK, editor might have been destroyed
  // at restoring Selection.
  nsresult rv = HandleCSSIndentAtSelectionInternal();
  if (NS_WARN_IF(Destroyed())) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::HandleCSSIndentAtSelectionInternal() failed");
  return rv;
}

nsresult HTMLEditor::HandleCSSIndentAtSelectionInternal() {
  MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
  MOZ_ASSERT(!IsSelectionRangeContainerNotContent());

  AutoSelectionRestorer restoreSelectionLater(*this);
  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;

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

  if (SelectionRefPtr()->IsCollapsed()) {
    EditorRawDOMPoint atCaret(EditorBase::GetStartPoint(*SelectionRefPtr()));
    if (NS_WARN_IF(!atCaret.IsSet())) {
      return NS_ERROR_FAILURE;
    }
    MOZ_ASSERT(atCaret.IsInContentNode());
    Element* blockElement = HTMLEditUtils::GetInclusiveAncestorBlockElement(
        *atCaret.ContainerAsContent());
    if (blockElement && HTMLEditUtils::IsListItem(blockElement)) {
      arrayOfContents.AppendElement(*blockElement);
    }
  }

  if (arrayOfContents.IsEmpty()) {
    nsresult rv =
        SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges(
            arrayOfContents, EditSubAction::eIndent,
            CollectNonEditableNodes::Yes);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::"
          "SplitInlinesAndCollectEditTargetNodesInExtendedSelectionRanges("
          "eIndent, CollectNonEditableNodes::Yes) failed");
      return rv;
    }
  }

  // If there is no visible and editable nodes in the edit targets, make an
  // empty block.
  // XXX Isn't this odd if there are only non-editable visible nodes?
  if (IsEmptyOneHardLine(arrayOfContents)) {
    // get selection location
    const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
    if (NS_WARN_IF(!firstRange)) {
      return NS_ERROR_FAILURE;
    }

    EditorDOMPoint atStartOfSelection(firstRange->StartRef());
    if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
      return NS_ERROR_FAILURE;
    }

    // make sure we can put a block here
    SplitNodeResult splitNodeResult =
        MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::div,
                                                    atStartOfSelection);
    if (splitNodeResult.Failed()) {
      NS_WARNING(
          "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction(nsGkAtoms::"
          "div) failed");
      return splitNodeResult.Rv();
    }
    RefPtr<Element> theBlock = CreateNodeWithTransaction(
        *nsGkAtoms::div, splitNodeResult.SplitPoint());
    if (NS_WARN_IF(Destroyed())) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    if (!theBlock) {
      NS_WARNING(
          "EditorBase::CreateNodeWithTransaction(nsGkAtoms::div) failed");
      return NS_ERROR_FAILURE;
    }
    // remember our new block for postprocessing
    TopLevelEditSubActionDataRef().mNewBlockElement = theBlock;
    nsresult rv = ChangeMarginStart(*theBlock, ChangeMargin::Increase);
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::ChangeMarginStart() failed, but ignored");
    // delete anything that was in the list of nodes
    // XXX We don't need to remove the nodes from the array for performance.
    while (!arrayOfContents.IsEmpty()) {
      OwningNonNull<nsIContent>& content = arrayOfContents[0];
      // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
      // keep it alive.
      rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
        return rv;
      }
      arrayOfContents.RemoveElementAt(0);
    }
    // Don't restore the selection
    restoreSelectionLater.Abort();
    // put selection in new block
    rv = CollapseSelectionToStartOf(*theBlock);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::CollapseSelectionToStartOf() failed");
    return rv;
  }

  // Ok, now go through all the nodes and put them in a blockquote,
  // or whatever is appropriate.
  RefPtr<Element> curList, curQuote;
  for (OwningNonNull<nsIContent>& content : arrayOfContents) {
    // Here's where we actually figure out what to do.
    EditorDOMPoint atContent(content);
    if (NS_WARN_IF(!atContent.IsSet())) {
      continue;
    }

    // Ignore all non-editable nodes.  Leave them be.
    // XXX We ignore non-editable nodes here, but not so in the above block.
    if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
      continue;
    }

    if (HTMLEditUtils::IsList(atContent.GetContainer())) {
      // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
      // keep it alive.
      nsresult rv =
          IndentListChild(&curList, atContent, MOZ_KnownLive(content));
      if (NS_FAILED(rv)) {
        NS_WARNING("HTMLEditor::IndentListChild() failed");
        return rv;
      }
      continue;
    }

    // Not a list item.

    if (HTMLEditUtils::IsBlockElement(content)) {
      nsresult rv = ChangeMarginStart(MOZ_KnownLive(*content->AsElement()),
                                      ChangeMargin::Increase);
      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "HTMLEditor::ChangeMarginStart() failed, but ignored");
      curQuote = nullptr;
      continue;
    }

    if (!curQuote) {
      // First, check that our element can contain a div.
      if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(),
                                         *nsGkAtoms::div)) {
        return NS_OK;  // cancelled
      }

      SplitNodeResult splitNodeResult =
          MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::div,
                                                      atContent);
      if (splitNodeResult.Failed()) {
        NS_WARNING(
            "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction(nsGkAtoms:"
            ":div) failed");
        return splitNodeResult.Rv();
      }
      curQuote = CreateNodeWithTransaction(*nsGkAtoms::div,
                                           splitNodeResult.SplitPoint());
      if (NS_WARN_IF(Destroyed())) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      if (!curQuote) {
        NS_WARNING(
            "EditorBase::CreateNodeWithTransaction(nsGkAtoms::div) failed");
        return NS_ERROR_FAILURE;
      }
      nsresult rv = ChangeMarginStart(*curQuote, ChangeMargin::Increase);
      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "HTMLEditor::ChangeMarginStart() failed, but ignored");
      // remember our new block for postprocessing
      TopLevelEditSubActionDataRef().mNewBlockElement = curQuote;
      // curQuote is now the correct thing to put content in
    }

    // tuck the node into the end of the active blockquote
    // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
    // keep it alive.
    nsresult rv =
        MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *curQuote);
    if (NS_WARN_IF(Destroyed())) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    if (NS_FAILED(rv)) {
      NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
      return rv;
    }
  }
  return NS_OK;
}

nsresult HTMLEditor::HandleHTMLIndentAtSelection() {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(!IsSelectionRangeContainerNotContent());

  if (!SelectionRefPtr()->IsCollapsed()) {
    nsresult rv = MaybeExtendSelectionToHardLineEdgesForBlockEditAction();
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::MaybeExtendSelectionToHardLineEdgesForBlockEditAction() "
          "failed");
      return rv;
    }
  }

  // HandleHTMLIndentAtSelectionInternal() creates AutoSelectionRestorer.
  // Therefore, even if it returns NS_OK, editor might have been destroyed
  // at restoring Selection.
  nsresult rv = HandleHTMLIndentAtSelectionInternal();
  if (NS_WARN_IF(Destroyed())) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::HandleHTMLIndentAtSelectionInternal() failed");
  return rv;
}

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

  AutoSelectionRestorer restoreSelectionLater(*this);

  // convert the selection ranges into "promoted" selection ranges:
  // this basically just expands the range to include the immediate
  // block parent, and then further expands to include any ancestors
  // whose children are all in the range

  AutoTArray<RefPtr<nsRange>, 4> arrayOfRanges;
  GetSelectionRangesExtendedToHardLineStartAndEnd(arrayOfRanges,
                                                  EditSubAction::eIndent);

  // use these ranges to construct a list of nodes to act on.
  AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
  nsresult rv = SplitInlinesAndCollectEditTargetNodes(
      arrayOfRanges, arrayOfContents, EditSubAction::eIndent,
      CollectNonEditableNodes::Yes);
  if (