/* 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/. */

#ifndef HTMLEditorNestedClasses_h
#define HTMLEditorNestedClasses_h

#include "EditorDOMPoint.h"
#include "EditorForwards.h"
#include "HTMLEditor.h"       // for HTMLEditor
#include "HTMLEditHelpers.h"  // for EditorInlineStyleAndValue
#include "HTMLEditUtils.h"    // for HTMLEditUtils::IsContainerNode

#include "mozilla/Attributes.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/Result.h"
#include "mozilla/dom/CharacterDataBuffer.h"
#include "mozilla/dom/Text.h"

namespace mozilla {

struct LimitersAndCaretData;  // Declared in nsFrameSelection.h
namespace dom {
class HTMLBRElement;
};

/*****************************************************************************
 * AutoInlineStyleSetter is a temporary class to set an inline style to
 * specific nodes.
 ****************************************************************************/

class MOZ_STACK_CLASS HTMLEditor::AutoInlineStyleSetter final
    : private EditorInlineStyleAndValue {
  using Element = dom::Element;
  using Text = dom::Text;

 public:
  explicit AutoInlineStyleSetter(
      const EditorInlineStyleAndValue& aStyleAndValue)
      : EditorInlineStyleAndValue(aStyleAndValue) {}

  void Reset() {
    mFirstHandledPoint.Clear();
    mLastHandledPoint.Clear();
  }

  const EditorDOMPoint& FirstHandledPointRef() const {
    return mFirstHandledPoint;
  }
  const EditorDOMPoint& LastHandledPointRef() const {
    return mLastHandledPoint;
  }

  /**
   * Split aText at aStartOffset and aEndOffset (except when they are start or
   * end of its data) and wrap the middle text node in an element to apply the
   * style.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
  SplitTextNodeAndApplyStyleToMiddleNode(HTMLEditor& aHTMLEditor, Text& aText,
                                         uint32_t aStartOffset,
                                         uint32_t aEndOffset);

  /**
   * Remove same style from children and apply the style entire (except
   * non-editable nodes) aContent.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
  ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(HTMLEditor& aHTMLEditor,
                                                     nsIContent& aContent);

  /**
   * Invert the style with creating new element or something.  This should
   * be called only when IsInvertibleWithCSS() returns true.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Element& aElement);
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitRangeOffFromNodeResult, nsresult>
  InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Text& aTextNode,
                       uint32_t aStartOffset, uint32_t aEndOffset);

  /**
   * Extend or shrink aRange for applying the style to the range.
   * See comments in the definition what this does.
   */
  Result<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToApplyTheStyle(
      const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
      const Element& aEditingHost) const;

  /**
   * Returns next/previous sibling of aContent or an ancestor of it if it's
   * editable and does not cross block boundary.
   */
  [[nodiscard]] static nsIContent* GetNextEditableInlineContent(
      const nsIContent& aContent, const nsINode* aLimiter = nullptr);
  [[nodiscard]] static nsIContent* GetPreviousEditableInlineContent(
      const nsIContent& aContent, const nsINode* aLimiter = nullptr);

  /**
   * GetEmptyTextNodeToApplyNewStyle creates new empty text node to insert
   * a new element which will contain newly inserted text or returns existing
   * empty text node if aCandidatePointToInsert is around it.
   *
   * NOTE: Unfortunately, editor does not want to insert text into empty inline
   * element in some places (e.g., automatically adjusting caret position to
   * nearest text node).  Therefore, we need to create new empty text node to
   * prepare new styles for inserting text.  This method is designed for the
   * preparation.
   *
   * @param aHTMLEditor                 The editor.
   * @param aCandidatePointToInsert     The point where the caller wants to
   *                                    insert new text.
   * @return            If this creates new empty text node returns it.
   *                    If this couldn't create new empty text node due to
   *                    the point or aEditingHost cannot have text node,
   *                    returns nullptr.
   *                    Otherwise, returns error.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result<RefPtr<Text>, nsresult>
  GetEmptyTextNodeToApplyNewStyle(
      HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCandidatePointToInsert);

 private:
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> ApplyStyle(
      HTMLEditor& aHTMLEditor, nsIContent& aContent);

  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
  ApplyCSSTextDecoration(HTMLEditor& aHTMLEditor, nsIContent& aContent);

  /**
   * Returns true if aStyledElement is a good element to set `style` attribute.
   */
  [[nodiscard]] bool ElementIsGoodContainerToSetStyle(
      nsStyledElement& aStyledElement) const;

  /**
   * ElementIsGoodContainerForTheStyle() returns true if aElement is a
   * good container for applying the style to a node.  I.e., if this returns
   * true, moving nodes into aElement is enough to apply the style to them.
   * Otherwise, you need to create new element for the style.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<bool, nsresult>
  ElementIsGoodContainerForTheStyle(HTMLEditor& aHTMLEditor,
                                    Element& aElement) const;

  /**
   * Return true if the node is an element node and it represents the style or
   * sets the style (including when setting different value) with `style`
   * attribute.
   */
  [[nodiscard]] bool ContentIsElementSettingTheStyle(
      const HTMLEditor& aHTMLEditor, nsIContent& aContent) const;

  /**
   * Helper methods to shrink range to apply the style.
   */
  [[nodiscard]] EditorRawDOMPoint GetShrunkenRangeStart(
      const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
      const nsINode& aCommonAncestorOfRange,
      const nsIContent* aFirstEntirelySelectedContentNodeInRange) const;
  [[nodiscard]] EditorRawDOMPoint GetShrunkenRangeEnd(
      const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
      const nsINode& aCommonAncestorOfRange,
      const nsIContent* aLastEntirelySelectedContentNodeInRange) const;

  /**
   * Helper methods to extend the range to apply the style.
   */
  [[nodiscard]] EditorRawDOMPoint
  GetExtendedRangeStartToWrapAncestorApplyingSameStyle(
      const HTMLEditor& aHTMLEditor,
      const EditorRawDOMPoint& aStartPoint) const;
  [[nodiscard]] EditorRawDOMPoint
  GetExtendedRangeEndToWrapAncestorApplyingSameStyle(
      const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aEndPoint) const;
  [[nodiscard]] EditorRawDOMRange
  GetExtendedRangeToMinimizeTheNumberOfNewElements(
      const HTMLEditor& aHTMLEditor, const nsINode& aCommonAncestor,
      EditorRawDOMPoint&& aStartPoint, EditorRawDOMPoint&& aEndPoint) const;

  /**
   * OnHandled() are called when this class creates new element to apply the
   * style, applies new style to existing element or ignores to apply the style
   * due to already set.
   */
  void OnHandled(const EditorDOMPoint& aStartPoint,
                 const EditorDOMPoint& aEndPoint) {
    if (!mFirstHandledPoint.IsSet()) {
      mFirstHandledPoint = aStartPoint;
    }
    mLastHandledPoint = aEndPoint;
  }
  void OnHandled(nsIContent& aContent) {
    if (aContent.IsElement() && !HTMLEditUtils::IsContainerNode(aContent)) {
      if (!mFirstHandledPoint.IsSet()) {
        mFirstHandledPoint.Set(&aContent);
      }
      mLastHandledPoint.SetAfter(&aContent);
      return;
    }
    if (!mFirstHandledPoint.IsSet()) {
      mFirstHandledPoint.Set(&aContent, 0u);
    }
    mLastHandledPoint = EditorDOMPoint::AtEndOf(aContent);
  }

  // mFirstHandledPoint and mLastHandledPoint store the first and last points
  // which are newly created or apply the new style, or just ignored at trying
  // to split a text node.
  EditorDOMPoint mFirstHandledPoint;
  EditorDOMPoint mLastHandledPoint;
};

/**
 * AutoMoveOneLineHandler moves the content in a line (between line breaks/block
 * boundaries) to specific point or end of a container element.
 */
class MOZ_STACK_CLASS HTMLEditor::AutoMoveOneLineHandler final {
 public:
  /**
   * Use this constructor when you want a line to move specific point.
   */
  explicit AutoMoveOneLineHandler(const EditorDOMPoint& aPointToInsert)
      : mPointToInsert(aPointToInsert),
        mMoveToEndOfContainer(MoveToEndOfContainer::No) {
    MOZ_ASSERT(mPointToInsert.IsSetAndValid());
    MOZ_ASSERT(mPointToInsert.IsInContentNode());
  }
  /**
   * Use this constructor when you want a line to move end of
   * aNewContainerElement.
   */
  explicit AutoMoveOneLineHandler(Element& aNewContainerElement)
      : mPointToInsert(&aNewContainerElement, 0),
        mMoveToEndOfContainer(MoveToEndOfContainer::Yes) {
    MOZ_ASSERT(mPointToInsert.IsSetAndValid());
  }

  /**
   * Must be called before calling Run().
   *
   * @param aHTMLEditor         The HTML editor.
   * @param aPointInHardLine    A point in a line which you want to move.
   * @param aEditingHost        The editing host.
   */
  [[nodiscard]] nsresult Prepare(HTMLEditor& aHTMLEditor,
                                 const EditorDOMPoint& aPointInHardLine,
                                 const Element& aEditingHost);
  /**
   * Must be called if Prepare() returned NS_OK.
   *
   * @param aHTMLEditor         The HTML editor.
   * @param aEditingHost        The editing host.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<MoveNodeResult, nsresult> Run(
      HTMLEditor& aHTMLEditor, const Element& aEditingHost);

  /**
   * Returns true if there are some content nodes which can be moved to another
   * place or deleted in the line containing aPointInHardLine.  Note that if
   * there is only a padding <br> element in an empty block element, this
   * returns false even though it may be deleted.
   */
  static Result<bool, nsresult> CanMoveOrDeleteSomethingInLine(
      const EditorDOMPoint& aPointInHardLine, const Element& aEditingHost);

  AutoMoveOneLineHandler(const AutoMoveOneLineHandler& aOther) = delete;
  AutoMoveOneLineHandler(AutoMoveOneLineHandler&& aOther) = delete;

 private:
  [[nodiscard]] bool ForceMoveToEndOfContainer() const {
    return mMoveToEndOfContainer == MoveToEndOfContainer::Yes;
  }
  [[nodiscard]] EditorDOMPoint& NextInsertionPointRef() {
    if (ForceMoveToEndOfContainer()) {
      mPointToInsert.SetToEndOf(mPointToInsert.GetContainer());
    }
    return mPointToInsert;
  }

  /**
   * Consider whether Run() should preserve or does not preserve white-space
   * style of moving content.
   *
   * @param aContentInLine      Specify a content node in the moving line.
   *                            Typically, container of aPointInHardLine of
   *                            Prepare().
   * @param aInclusiveAncestorBlockOfInsertionPoint
   *                            Inclusive ancestor block element of insertion
   *                            point.  Typically, computed
   *                            mDestInclusiveAncestorBlock.
   */
  [[nodiscard]] static PreserveWhiteSpaceStyle
  ConsiderWhetherPreserveWhiteSpaceStyle(
      const nsIContent* aContentInLine,
      const Element* aInclusiveAncestorBlockOfInsertionPoint);

  /**
   * Look for inclusive ancestor block element of aBlockElement and a descendant
   * of aAncestorElement.  If aBlockElement and aAncestorElement are same one,
   * this returns nullptr.
   *
   * @param aBlockElement       A block element which is a descendant of
   *                            aAncestorElement.
   * @param aAncestorElement    An inclusive ancestor block element of
   *                            aBlockElement.
   */
  [[nodiscard]] static Element*
  GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement(
      Element& aBlockElement, const Element& aAncestorElement);

  /**
   * Split ancestors at the line range boundaries and collect array of contents
   * in the line to aOutArrayOfContents.  Specify aNewContainer to the container
   * of insertion point to avoid splitting the destination.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
  SplitToMakeTheLineIsolated(
      HTMLEditor& aHTMLEditor, const nsIContent& aNewContainer,
      const Element& aEditingHost,
      nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) const;

  /**
   * Delete unnecessary trailing line break in aMovedContentRange if there is.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  DeleteUnnecessaryTrailingLineBreakInMovedLineEnd(
      HTMLEditor& aHTMLEditor, const EditorDOMRange& aMovedContentRange,
      const Element& aEditingHost) const;

  // Range of selected line.
  EditorDOMRange mLineRange;
  // Next insertion point.  If mMoveToEndOfContainer is `Yes`, this is
  // recomputed with its container in NextInsertionPointRef.  Therefore, this
  // should not be referred directly.
  EditorDOMPoint mPointToInsert;
  // An inclusive ancestor block element of the moving line.
  RefPtr<Element> mSrcInclusiveAncestorBlock;
  // An inclusive ancestor block element of the insertion point.
  RefPtr<Element> mDestInclusiveAncestorBlock;
  // nullptr if mMovingToParentBlock is false.
  // Must be non-nullptr if mMovingToParentBlock is true.  The topmost ancestor
  // block element which contains mSrcInclusiveAncestorBlock and a descendant of
  // mDestInclusiveAncestorBlock.  I.e., this may be same as
  // mSrcInclusiveAncestorBlock, but never same as mDestInclusiveAncestorBlock.
  RefPtr<Element> mTopmostSrcAncestorBlockInDestBlock;
  enum class MoveToEndOfContainer { No, Yes };
  MoveToEndOfContainer mMoveToEndOfContainer;
  PreserveWhiteSpaceStyle mPreserveWhiteSpaceStyle =
      PreserveWhiteSpaceStyle::No;
  // true if mDestInclusiveAncestorBlock is an ancestor of
  // mSrcInclusiveAncestorBlock.
  bool mMovingToParentBlock = false;
};

/**
 * Convert contents around aRanges of Run() to specified list element.  If there
 * are some different type of list elements, this method converts them to
 * specified list items too.  Basically, each line will be wrapped in a list
 * item element.  However, only when <p> element is selected, its child <br>
 * elements won't be treated as line separators.  Perhaps, this is a bug.
 */
class MOZ_STACK_CLASS HTMLEditor::AutoListElementCreator final {
 public:
  /**
   * @param aListElementTagName         The new list element tag name.
   * @param aListItemElementTagName     The new list item element tag name.
   * @param aBulletType                 If this is not empty string, it's set
   *                                    to `type` attribute of new list item
   *                                    elements.  Otherwise, existing `type`
   *                                    attributes will be removed.
   */
  AutoListElementCreator(const nsStaticAtom& aListElementTagName,
                         const nsStaticAtom& aListItemElementTagName,
                         const nsAString& aBulletType)
      // Needs const_cast hack here because the struct users may want
      // non-const nsStaticAtom pointer due to bug 1794954
      : mListTagName(const_cast<nsStaticAtom&>(aListElementTagName)),
        mListItemTagName(const_cast<nsStaticAtom&>(aListItemElementTagName)),
        mBulletType(aBulletType) {
    MOZ_ASSERT(&mListTagName == nsGkAtoms::ul ||
               &mListTagName == nsGkAtoms::ol ||
               &mListTagName == nsGkAtoms::dl);
    MOZ_ASSERT_IF(
        &mListTagName == nsGkAtoms::ul || &mListTagName == nsGkAtoms::ol,
        &mListItemTagName == nsGkAtoms::li);
    MOZ_ASSERT_IF(&mListTagName == nsGkAtoms::dl,
                  &mListItemTagName == nsGkAtoms::dt ||
                      &mListItemTagName == nsGkAtoms::dd);
  }

  /**
   * @param aHTMLEditor The HTML editor.
   * @param aRanges     [in/out] The ranges which will be converted to list.
   *                    The instance must not have saved ranges because it'll
   *                    be used in this method.
   *                    If succeeded, this will have selection ranges which
   *                    should be applied to `Selection`.
   *                    If failed, this keeps storing original selection
   *                    ranges.
   * @param aSelectAllOfCurrentList     Yes if this should treat all of
   *                                    ancestor list element at selection.
   * @param aEditingHost                The editing host.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
      HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& aRanges,
      HTMLEditor::SelectAllOfCurrentList aSelectAllOfCurrentList,
      const Element& aEditingHost) const;

 private:
  using ContentNodeArray = nsTArray<OwningNonNull<nsIContent>>;
  using AutoContentNodeArray = AutoTArray<OwningNonNull<nsIContent>, 64>;

  /**
   * If aSelectAllOfCurrentList is "Yes" and aRanges is in a list element,
   * returns the list element.
   * Otherwise, extend aRanges to select start and end lines selected by it and
   * correct all topmost content nodes in the extended ranges with splitting
   * ancestors at range edges.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList(
      HTMLEditor& aHTMLEditor, AutoClonedRangeArray& aRanges,
      SelectAllOfCurrentList aSelectAllOfCurrentList,
      const Element& aEditingHost, ContentNodeArray& aOutArrayOfContents) const;

  /**
   * Return true if aArrayOfContents has only <br> elements or empty inline
   * container elements.  I.e., it means that aArrayOfContents represents
   * only empty line(s) if this returns true.
   */
  [[nodiscard]] static bool
  IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements(
      const ContentNodeArray& aArrayOfContents);

  /**
   * Delete all content nodes ina ArrayOfContents, and if we can put new list
   * element at start of the first range of aRanges, insert new list element
   * there.
   *
   * @return            The empty list item element in new list element.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
  ReplaceContentNodesWithEmptyNewList(
      HTMLEditor& aHTMLEditor, const AutoClonedRangeArray& aRanges,
      const AutoContentNodeArray& aArrayOfContents,
      const Element& aEditingHost) const;

  /**
   * Creat new list elements or use existing list elements and move
   * aArrayOfContents into list item elements.
   *
   * @return            A list or list item element which should have caret.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult>
  WrapContentNodesIntoNewListElements(HTMLEditor& aHTMLEditor,
                                      AutoClonedRangeArray& aRanges,
                                      AutoContentNodeArray& aArrayOfContents,
                                      const Element& aEditingHost) const;

  struct MOZ_STACK_CLASS AutoHandlingState final {
    // Current list element which is a good container to create new list item
    // element.
    RefPtr<Element> mCurrentListElement;
    // Previously handled list item element.
    RefPtr<Element> mPreviousListItemElement;
    // List or list item element which should have caret after handling all
    // contents.
    RefPtr<Element> mListOrListItemElementToPutCaret;
    // Replacing block element.  This is typically already removed from the DOM
    // tree.
    RefPtr<Element> mReplacingBlockElement;
    // Once id attribute of mReplacingBlockElement copied, the id attribute
    // shouldn't be copied again.
    bool mMaybeCopiedReplacingBlockElementId = false;
  };

  /**
   * Helper methods of WrapContentNodesIntoNewListElements.  They are called for
   * handling one content node of aArrayOfContents.  It's set to aHandling*.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildContent(
      HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
      AutoHandlingState& aState, const Element& aEditingHost) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  HandleChildListElement(HTMLEditor& aHTMLEditor, Element& aHandlingListElement,
                         AutoHandlingState& aState) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListItemElement(
      HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
      AutoHandlingState& aState) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  HandleChildListItemInDifferentTypeList(HTMLEditor& aHTMLEditor,
                                         Element& aHandlingListItemElement,
                                         AutoHandlingState& aState) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListItemInSameTypeList(
      HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
      AutoHandlingState& aState) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildDivOrParagraphElement(
      HTMLEditor& aHTMLEditor, Element& aHandlingDivOrParagraphElement,
      AutoHandlingState& aState, const Element& aEditingHost) const;
  enum class EmptyListItem { NotCreate, Create };
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult CreateAndUpdateCurrentListElement(
      HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert,
      EmptyListItem aEmptyListItem, AutoHandlingState& aState,
      const Element& aEditingHost) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
  AppendListItemElement(HTMLEditor& aHTMLEditor, const Element& aListElement,
                        AutoHandlingState& aState) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult
  MaybeCloneAttributesToNewListItem(HTMLEditor& aHTMLEditor,
                                    Element& aListItemElement,
                                    AutoHandlingState& aState);
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildInlineContent(
      HTMLEditor& aHTMLEditor, nsIContent& aHandlingInlineContent,
      AutoHandlingState& aState) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult WrapContentIntoNewListItemElement(
      HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
      AutoHandlingState& aState) const;

  /**
   * If aRanges is collapsed outside aListItemOrListToPutCaret, this collapse
   * aRanges in aListItemOrListToPutCaret again.
   */
  nsresult EnsureCollapsedRangeIsInListItemOrListElement(
      Element& aListItemOrListToPutCaret, AutoClonedRangeArray& aRanges) const;

  MOZ_KNOWN_LIVE nsStaticAtom& mListTagName;
  MOZ_KNOWN_LIVE nsStaticAtom& mListItemTagName;
  const nsAutoString mBulletType;
};

/**
 * Handle "insertParagraph" command.
 */
class MOZ_STACK_CLASS HTMLEditor::AutoInsertParagraphHandler final {
 public:
  AutoInsertParagraphHandler() = delete;
  AutoInsertParagraphHandler(const AutoInsertParagraphHandler&) = delete;
  AutoInsertParagraphHandler(AutoInsertParagraphHandler&&) = delete;

  MOZ_CAN_RUN_SCRIPT explicit AutoInsertParagraphHandler(
      HTMLEditor& aHTMLEditor, const Element& aEditingHost)
      : mHTMLEditor(aHTMLEditor),
        mEditingHost(aEditingHost),
        mDefaultParagraphSeparatorTagName(
            aHTMLEditor.DefaultParagraphSeparatorTagName()),
        mDefaultParagraphSeparator(aHTMLEditor.GetDefaultParagraphSeparator()) {
  }

  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run();

 private:
  /**
   * Insert <br> element.
   *
   * @param aPointToInsert      The position where the new <br> should be
   *                            inserted.
   * @param aBlockElementWhichShouldHaveCaret
   *                            [optional] If set, this collapse selection into
   *                            the element with
   *                            CollapseSelectionToPointOrIntoBlockWhichShouldHaveCaret().
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  HandleInsertBRElement(
      const EditorDOMPoint& aPointToInsert,
      const Element* aBlockElementWhichShouldHaveCaret = nullptr);

  /**
   * Insert a linefeed.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  HandleInsertLinefeed(const EditorDOMPoint& aPointToInsert);

  /**
   * SplitParagraphWithTransaction() splits the parent block, aParentDivOrP, at
   * aPointToSplit.
   *
   * @param aBlockElementToSplit    The current paragraph which should be split.
   * @param aPointToSplit           The point to split aBlockElementToSplit.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
  SplitParagraphWithTransaction(Element& aBlockElementToSplit,
                                const EditorDOMPoint& aPointToSplit);

  /**
   * Delete preceding invisible line break before aPointToSplit if and only if
   * there is.
   *
   * @return New point to split aBlockElementToSplit
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
  EnsureNoInvisibleLineBreakBeforePointToSplit(
      const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit);

  /**
   * Maybe insert a <br> element if it's required to keep the inline container
   * visible after splitting aBlockElementToSplit at aPointToSplit.
   *
   * @return New point to split aBlockElementToSplit
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
  MaybeInsertFollowingBRElementToPreserveRightBlock(
      const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit);

  /**
   * Return true if the HTMLEditor is in the mode which `insertParagraph` should
   * always create a new paragraph or in the cases that we create a new
   * paragraph in the legacy mode.
   */
  [[nodiscard]] bool ShouldCreateNewParagraph(
      Element& aParentDivOrP, const EditorDOMPoint& aPointToSplit) const;

  /**
   * Return true if aBRElement is nullptr or an invisible <br> or a padding <br>
   * for making the last empty line visible.
   */
  [[nodiscard]] static bool
  IsNullOrInvisibleBRElementOrPaddingOneForEmptyLastLine(
      const dom::HTMLBRElement* aBRElement);

  /**
   * Handle insertParagraph command (i.e., handling Enter key press) in a
   * heading element.  This splits aHeadingElement element at aPointToSplit.
   * Then, if right heading element is empty, it'll be removed and new paragraph
   * is created (its type is decided with default paragraph separator).
   *
   * @param aHeadingElement     The heading element to be split.
   * @param aPointToSplit       The point to split aHeadingElement.
   * @return                    New paragraph element, meaning right heading
   *                            element if aHeadingElement is split, or newly
   *                            created or existing paragraph element.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertParagraphResult, nsresult>
  HandleInHeadingElement(Element& aHeadingElement,
                         const EditorDOMPoint& aPointToSplit);

  /**
   * Handle insertParagraph command at end of a heading element.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertParagraphResult, nsresult>
  HandleAtEndOfHeadingElement(Element& aHeadingElement);

  /**
   * Handle insertParagraph command (i.e., handling Enter key press) in a list
   * item element.
   *
   * @param aListItemElement    The list item which has the following point.
   * @param aPointToSplit       The point to split aListItemElement.
   * @return                    New paragraph element, meaning right list item
   *                            element if aListItemElement is split, or newly
   *                            created paragraph element.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<InsertParagraphResult, nsresult>
  HandleInListItemElement(Element& aListItemElement,
                          const EditorDOMPoint& aPointToSplit);

  /**
   * Split aMailCiteElement at aPointToSplit.
   *
   * @param aMailCiteElement    The mail-cite element which should be split.
   * @param aPointToSplit       The point to split.
   * @return                    Candidate caret position where is at inserted
   *                            <br> element into the split point.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
  HandleInMailCiteElement(Element& aMailCiteElement,
                          const EditorDOMPoint& aPointToSplit);

  /**
   * Insert a <br> element into aPointToBreak.
   * This may split container elements at the point and/or may move following
   * <br> element to immediately after the new <br> element if necessary.
   *
   * @param aPointToBreak       The point where new <br> element will be
   *                            inserted before.
   * @return                    If succeeded, returns new <br> element and
   *                            candidate caret point.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateElementResult, nsresult>
  InsertBRElement(const EditorDOMPoint& aPointToBreak);

  /**
   * Return true if we should insert a line break instead of a paragraph.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool ShouldInsertLineBreakInstead(
      const Element* aEditableBlockElement,
      const EditorDOMPoint& aCandidatePointToSplit);

  enum class InsertBRElementIntoEmptyBlock : bool { Start, End };

  /**
   * Make sure that aMaybeBlockElement is visible with putting a <br> element if
   * and only if it's an empty block element.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateLineBreakResult, nsresult>
  InsertBRElementIfEmptyBlockElement(
      Element& aMaybeBlockElement,
      InsertBRElementIntoEmptyBlock aInsertBRElementIntoEmptyBlock,
      BlockInlineCheck aBlockInlineCheck);

  /**
   * Split aMailCiteElement at aPointToSplit.  This deletes all inclusive
   * ancestors of aPointToSplit in aMailCiteElement too.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<SplitNodeResult, nsresult>
  SplitMailCiteElement(const EditorDOMPoint& aPointToSplit,
                       Element& aMailCiteElement);

  /**
   * aMailCiteElement may be a <span> element which is styled as block.  If it's
   * followed by a block boundary, it requires a padding <br> element when it's
   * serialized.  This method may insert a <br> element if it's required.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  MaybeInsertPaddingBRElementToInlineMailCiteElement(
      const EditorDOMPoint& aPointToInsertBRElement, Element& aMailCiteElement);

  /**
   * Return the deepest inline container element which is the first leaf or the
   * first leaf container of aBlockElement.
   */
  [[nodiscard]] static Element* GetDeepestFirstChildInlineContainerElement(
      Element& aBlockElement);

  /**
   * Collapse `Selection` to aCandidatePointToPutCaret or into
   * aBlockElementShouldHaveCaret.  If aBlockElementShouldHaveCaret is specified
   * and aCandidatePointToPutCaret is outside it, this ignores
   * aCandidatePointToPutCaret and collapse `Selection` into
   * aBlockElementShouldHaveCaret.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  CollapseSelectionToPointOrIntoBlockWhichShouldHaveCaret(
      const EditorDOMPoint& aCandidatePointToPutCaret,
      const Element* aBlockElementShouldHaveCaret,
      const SuggestCaretOptions& aOptions);

  /**
   * Return a better point to split the paragraph to avoid to keep a typing in a
   * link or a paragraph in list item in the new paragraph.
   */
  [[nodiscard]] EditorDOMPoint GetBetterPointToSplitParagraph(
      const Element& aBlockElementToSplit,
      const EditorDOMPoint& aCandidatePointToSplit,
      const Element& aEditingHost);

  enum class IgnoreBlockBoundaries : bool { No, Yes };

  /**
   * Return true if splitting aBlockElementToSplit at aPointToSplit will create
   * empty left element.
   *
   * @param aBlockElementToSplit    The paragraph element which we want to
   *                                split.
   * @param aPointToSplit           The split position in aBlockElementToSplit.
   * @param aIgnoreBlockBoundaries  If No, return true only when aPointToSplit
   *                                is immediately after a block boundary of
   *                                aBlockElementToSplit.  In other words,
   *                                may return true only when aPointToSplit
   *                                is not in a child block of
   *                                aBlockElementToSplit.
   *                                If Yes, return true even when aPointToSplit
   *                                is immediately after any current block
   *                                boundary which is followed by the block
   *                                boundary of aBlockElementToSplit.  In other
   *                                words, return true when aPointToSplit is in
   *                                a child block which is start of any ancestor
   *                                block elements.
   */
  [[nodiscard]] static bool SplitPointIsStartOfSplittingBlock(
      const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit,
      IgnoreBlockBoundaries aIgnoreBlockBoundaries);

  /**
   * Return true if splitting aBlockElementToSplit at aPointToSplit will create
   * empty right element.
   *
   * @param aBlockElementToSplit    The paragraph element which we want to
   *                                split.
   * @param aPointToSplit           The split position in aBlockElementToSplit.
   * @param aIgnoreBlockBoundaries  If No, return true only when aPointToSplit
   *                                is immediately before a block boundary of
   *                                aBlockElementToSplit.  In other words,
   *                                may return true only when aPointToSplit
   *                                is not in a child block of
   *                                aBlockElementToSplit.
   *                                If Yes, return true even when aPointToSplit
   *                                is immediately before any current block
   *                                boundary which is followed by the block
   *                                boundary of aBlockElementToSplit.  In other
   *                                words, return true when aPointToSplit is in
   *                                a child block which is end of any ancestor
   *                                block elements.
   */
  [[nodiscard]] static bool SplitPointIsEndOfSplittingBlock(
      const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit,
      IgnoreBlockBoundaries aIgnoreBlockBoundaries);

  MOZ_KNOWN_LIVE HTMLEditor& mHTMLEditor;
  MOZ_KNOWN_LIVE const Element& mEditingHost;
  MOZ_KNOWN_LIVE nsStaticAtom& mDefaultParagraphSeparatorTagName;
  const ParagraphSeparator mDefaultParagraphSeparator;
};

/**
 * Handle "insertLineBreak" command.
 */
class MOZ_STACK_CLASS HTMLEditor::AutoInsertLineBreakHandler final {
 public:
  AutoInsertLineBreakHandler() = delete;
  AutoInsertLineBreakHandler(const AutoInsertLineBreakHandler&) = delete;
  AutoInsertLineBreakHandler(AutoInsertLineBreakHandler&&) = delete;

  MOZ_CAN_RUN_SCRIPT explicit AutoInsertLineBreakHandler(
      HTMLEditor& aHTMLEditor, const Element& aEditingHost)
      : mHTMLEditor(aHTMLEditor), mEditingHost(aEditingHost) {}

  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult Run();

 private:
  /**
   * Insert a linefeed character into aPointToBreak.
   *
   * @param aPointToBreak       The point where new linefeed character will be
   *                            inserted before.
   * @param aEditingHost        Current active editing host.
   * @return                    A suggest point to put caret.
   */
  [[nodiscard]] static MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
  InsertLinefeed(HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToBreak,
                 const Element& aEditingHost);

  /**
   * Insert <br> element at `Selection`.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleInsertBRElement();

  /**
   * Insert a linefeed at `Selection`.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleInsertLinefeed();

  friend class AutoInsertParagraphHandler;

  MOZ_KNOWN_LIVE HTMLEditor& mHTMLEditor;
  MOZ_KNOWN_LIVE const Element& mEditingHost;
};

/**
 * Handle delete multiple ranges, typically they are the selection ranges.
 */
class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final {
 public:
  explicit AutoDeleteRangesHandler(
      const AutoDeleteRangesHandler* aParent = nullptr)
      : mParent(aParent),
        mOriginalDirectionAndAmount(nsIEditor::eNone),
        mOriginalStripWrappers(nsIEditor::eNoStrip) {}

  /**
   * ComputeRangesToDelete() computes actual deletion ranges.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ComputeRangesToDelete(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      AutoClonedSelectionRangeArray& aRangesToDelete,
      const Element& aEditingHost);

  /**
   * Deletes content in or around aRangesToDelete.
   * NOTE: This method creates SelectionBatcher.  Therefore, each caller
   *       needs to check if the editor is still available even if this returns
   *       NS_OK.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
      HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      nsIEditor::EStripWrappers aStripWrappers,
      AutoClonedSelectionRangeArray& aRangesToDelete,
      const Element& aEditingHost);

 private:
  enum class ComputeRangeFor : bool { GetTargetRanges, ToDeleteTheRange };

  [[nodiscard]] bool IsHandlingRecursively() const {
    return mParent != nullptr;
  }

  [[nodiscard]] bool CanFallbackToDeleteRangeWithTransaction(
      const nsRange& aRangeToDelete) const;

  [[nodiscard]] bool CanFallbackToDeleteRangesWithTransaction(
      const AutoClonedSelectionRangeArray& aRangesToDelete) const;

  /**
   * HandleDeleteAroundCollapsedRanges() handles deletion with collapsed
   * ranges.  Callers must guarantee that this is called only when
   * aRangesToDelete.IsCollapsed() returns true.
   *
   * @param aDirectionAndAmount Direction of the deletion.
   * @param aStripWrappers      Must be eStrip or eNoStrip.
   * @param aRangesToDelete     Ranges to delete.  This `IsCollapsed()` must
   *                            return true.
   * @param aWSRunScannerAtCaret        Scanner instance which scanned from
   *                                    caret point.
   * @param aScanFromCaretPointResult   Scan result of aWSRunScannerAtCaret
   *                                    toward aDirectionAndAmount.
   * @param aEditingHost        The editing host.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  HandleDeleteAroundCollapsedRanges(
      HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      nsIEditor::EStripWrappers aStripWrappers,
      AutoClonedSelectionRangeArray& aRangesToDelete,
      const WSRunScanner& aWSRunScannerAtCaret,
      const WSScanResult& aScanFromCaretPointResult,
      const Element& aEditingHost);
  nsresult ComputeRangesToDeleteAroundCollapsedRanges(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      AutoClonedSelectionRangeArray& aRangesToDelete,
      const WSRunScanner& aWSRunScannerAtCaret,
      const WSScanResult& aScanFromCaretPointResult,
      const Element& aEditingHost) const;

  /**
   * HandleDeleteNonCollapsedRanges() handles deletion with non-collapsed
   * ranges.  Callers must guarantee that this is called only when
   * aRangesToDelete.IsCollapsed() returns false.
   *
   * @param aDirectionAndAmount         Direction of the deletion.
   * @param aStripWrappers              Must be eStrip or eNoStrip.
   * @param aRangesToDelete             The ranges to delete.
   * @param aSelectionWasCollapsed      If the caller extended `Selection`
   *                                    from collapsed, set this to `Yes`.
   *                                    Otherwise, i.e., `Selection` is not
   *                                    collapsed from the beginning, set
   *                                    this to `No`.
   * @param aEditingHost                The editing host.
   */
  enum class SelectionWasCollapsed { Yes, No };
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  HandleDeleteNonCollapsedRanges(HTMLEditor& aHTMLEditor,
                                 nsIEditor::EDirection aDirectionAndAmount,
                                 nsIEditor::EStripWrappers aStripWrappers,
                                 AutoClonedSelectionRangeArray& aRangesToDelete,
                                 SelectionWasCollapsed aSelectionWasCollapsed,
                                 const Element& aEditingHost);
  nsresult ComputeRangesToDeleteNonCollapsedRanges(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      AutoClonedSelectionRangeArray& aRangesToDelete,
      SelectionWasCollapsed aSelectionWasCollapsed,
      const Element& aEditingHost) const;

  /**
   * Handle deletion of collapsed ranges in a text node.
   *
   * @param aDirectionAndAmount Must be eNext or ePrevious.
   * @param aCaretPosition      The position where caret is.  This container
   *                            must be a text node.
   * @param aEditingHost        The editing host.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
  HandleDeleteTextAroundCollapsedRanges(
      HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      AutoClonedSelectionRangeArray& aRangesToDelete,
      const Element& aEditingHost);
  nsresult ComputeRangesToDeleteTextAroundCollapsedRanges(
      nsIEditor::EDirection aDirectionAndAmount,
      AutoClonedSelectionRangeArray& aRangesToDelete) const;

  /**
   * Handle deletion of atomic elements like <br>, <hr>, <img>, <input>, etc and
   * data nodes except text node (e.g., comment node). Note that don't call this
   * directly with `<hr>` element.
   *
   * @param aAtomicContent      The atomic content to be deleted.
   * @param aCaretPoint         The caret point (i.e., selection start or
   *                            end).
   * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized
   *                             with the caret point.
   * @param aEditingHost        The editing host.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
  HandleDeleteAtomicContent(HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent,
                            const EditorDOMPoint& aCaretPoint,
                            const WSRunScanner& aWSRunScannerAtCaret,
                            const Element& aEditingHost);
  nsresult ComputeRangesToDeleteAtomicContent(
      const nsIContent& aAtomicContent,
      AutoClonedSelectionRangeArray& aRangesToDelete) const;

  /**
   * GetAtomicContnetToDelete() returns better content that is deletion of
   * atomic element.  If aScanFromCaretPointResult is special, since this
   * point may not be editable, we look for better point to remove atomic
   * content.
   *
   * @param aDirectionAndAmount       Direction of the deletion.
   * @param aWSRunScannerAtCaret      WSRunScanner instance which was
   *                                  initialized with the caret point.
   * @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret
   *                                  toward aDirectionAndAmount.
   */
  [[nodiscard]] static nsIContent* GetAtomicContentToDelete(
      nsIEditor::EDirection aDirectionAndAmount,
      const WSRunScanner& aWSRunScannerAtCaret,
      const WSScanResult& aScanFromCaretPointResult) MOZ_NONNULL_RETURN;

  /**
   * HandleDeleteAtOtherBlockBoundary() handles deletion at other block boundary
   * (i.e., immediately before or after a block). If this does not join blocks,
   * `Run()` may be called recursively with creating another instance.
   *
   * @param aDirectionAndAmount Direction of the deletion.
   * @param aStripWrappers      Must be eStrip or eNoStrip.
   * @param aOtherBlockElement  The block element which follows the caret or
   *                            is followed by caret.
   * @param aCaretPoint         The caret point (i.e., selection start or
   *                            end).
   * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized
   *                             with the caret point.
   * @param aRangesToDelete     Ranges to delete of the caller.  This should
   *                            be collapsed and the point should match with
   *                            aCaretPoint.
   * @param aEditingHost        The editing host.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  HandleDeleteAtOtherBlockBoundary(
      HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      nsIEditor::EStripWrappers aStripWrappers, Element& aOtherBlockElement,
      const EditorDOMPoint& aCaretPoint, WSRunScanner& aWSRunScannerAtCaret,
      AutoClonedSelectionRangeArray& aRangesToDelete,
      const Element& aEditingHost);

  /**
   * ExtendOrShrinkRangeToDelete() extends aRangeToDelete if there are
   * an invisible <br> element and/or some parent empty elements.
   *
   * @param aLimitersAndCaretData The frame selection data.
   * @param aRangeToDelete       The range to be extended for deletion.  This
   *                            must not be collapsed, must be positioned.
   */
  template <typename EditorDOMRangeType>
  [[nodiscard]] Result<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToDelete(
      const HTMLEditor& aHTMLEditor,
      const LimitersAndCaretData& aLimitersAndCaretData,
      const EditorDOMRangeType& aRangeToDelete,
      SelectionWasCollapsed aSelectionWasCollapsed,
      ComputeRangeFor aComputeRangeFor, const Element& aEditingHost) const;

  /**
   * Extend the start boundary of aRangeToDelete to contain ancestor inline
   * elements which will be empty once the content in aRangeToDelete is removed
   * from the tree.
   *
   * NOTE: This is designed for deleting inline elements which become empty if
   * aRangeToDelete which crosses a block boundary of right block child.
   * Therefore, you may need to improve this method if you want to use this in
   * the other cases.
   *
   * @param aRangeToDelete      [in/out] The range to delete.  This start
   *                            boundary may be modified.
   * @param aEditingHost        The editing host.
   * @return                    true if aRangeToDelete is modified.
   *                            false if aRangeToDelete is not modified.
   *                            error if aRangeToDelete gets unexpected
   *                            situation.
   */
  [[nodiscard]] static Result<bool, nsresult>
  ExtendRangeToContainAncestorInlineElementsAtStart(
      nsRange& aRangeToDelete, const Element& aEditingHost);

  /**
   * A helper method for ExtendOrShrinkRangeToDelete().  This returns shrunken
   * range if aRangeToDelete selects all over list elements which have some list
   * item elements to avoid to delete all list items from the list element.
   */
  [[nodiscard]] static EditorRawDOMRange
  GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements(
      const EditorRawDOMRange& aRangeToDelete,
      ComputeRangeFor aComputeRangeFor);

  /**
   * DeleteUnnecessaryNodes() removes unnecessary nodes around aRange.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  DeleteUnnecessaryNodes(HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
                         const Element& aEditingHost);

  /**
   * If aContent is a text node that contains only collapsed white-space or
   * empty and editable.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  DeleteNodeIfInvisibleAndEditableTextNode(HTMLEditor& aHTMLEditor,
                                           nsIContent& aContent);

  /**
   * DeleteParentBlocksIfEmpty() removes parent block elements if they
   * don't have visible contents.  Note that due performance issue of
   * WhiteSpaceVisibilityKeeper, this call may be expensive.  And also note that
   * this removes a empty block with a transaction.  So, please make sure that
   * you've already created `AutoPlaceholderBatch`.
   *
   * @param aPoint      The point whether this method climbing up the DOM
   *                    tree to remove empty parent blocks.
   * @return            NS_OK if one or more empty block parents are deleted.
   *                    NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND if the point is
   *                    not in empty block.
   *                    Or NS_ERROR_* if something unexpected occurs.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
  DeleteParentBlocksWithTransactionIfEmpty(HTMLEditor& aHTMLEditor,
                                           const EditorDOMPoint& aPoint,
                                           const Element& aEditingHost);

  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
  FallbackToDeleteRangeWithTransaction(HTMLEditor& aHTMLEditor,
                                       nsRange& aRangeToDelete) const {
    MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
    MOZ_ASSERT(CanFallbackToDeleteRangeWithTransaction(aRangeToDelete));
    Result<CaretPoint, nsresult> caretPointOrError =
        aHTMLEditor.DeleteRangeWithTransaction(mOriginalDirectionAndAmount,
                                               mOriginalStripWrappers,
                                               aRangeToDelete);
    NS_WARNING_ASSERTION(caretPointOrError.isOk(),
                         "EditorBase::DeleteRangeWithTransaction() failed");
    return caretPointOrError;
  }

  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult>
  FallbackToDeleteRangesWithTransaction(
      HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& aRangesToDelete,
      const Element& aEditingHost) const;

  /**
   * Compute target range(s) which will be called by
   * `EditorBase::DeleteRangeWithTransaction()` or
   * `HTMLEditor::DeleteRangesWithTransaction()`.
   * TODO: We should not use it for consistency with each deletion handler
   *       in this and nested classes.
   */
  nsresult ComputeRangeToDeleteRangeWithTransaction(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      nsRange& aRange, const Element& aEditingHost) const;
  nsresult ComputeRangesToDeleteRangesWithTransaction(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      AutoClonedSelectionRangeArray& aRangesToDelete,
      const Element& aEditingHost) const;

  nsresult FallbackToComputeRangeToDeleteRangeWithTransaction(
      const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete,
      const Element& aEditingHost) const {
    MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
    MOZ_ASSERT(CanFallbackToDeleteRangeWithTransaction(aRangeToDelete));
    nsresult rv = ComputeRangeToDeleteRangeWithTransaction(
        aHTMLEditor, mOriginalDirectionAndAmount, aRangeToDelete, aEditingHost);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "AutoDeleteRangesHandler::"
                         "ComputeRangeToDeleteRangeWithTransaction() failed");
    return rv;
  }
  nsresult FallbackToComputeRangesToDeleteRangesWithTransaction(
      const HTMLEditor& aHTMLEditor,
      AutoClonedSelectionRangeArray& aRangesToDelete,
      const Element& aEditingHost) const {
    MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
    MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete));
    nsresult rv = ComputeRangesToDeleteRangesWithTransaction(
        aHTMLEditor, mOriginalDirectionAndAmount, aRangesToDelete,
        aEditingHost);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "AutoDeleteRangesHandler::"
                         "ComputeRangesToDeleteRangesWithTransaction() failed");
    return rv;
  }

  class MOZ_STACK_CLASS AutoBlockElementsJoiner;
  class MOZ_STACK_CLASS AutoEmptyBlockAncestorDeleter;

  const AutoDeleteRangesHandler* const mParent;
  nsIEditor::EDirection mOriginalDirectionAndAmount;
  nsIEditor::EStripWrappers mOriginalStripWrappers;
};

/**
 * Handle join block elements.  Despite the name, this may just move first line
 * of a block into another block or just deleted the range with keeping table
 * structure.
 */
class MOZ_STACK_CLASS
HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner final {
 public:
  AutoBlockElementsJoiner() = delete;
  explicit AutoBlockElementsJoiner(
      AutoDeleteRangesHandler& aDeleteRangesHandler)
      : mDeleteRangesHandler(&aDeleteRangesHandler),
        mDeleteRangesHandlerConst(aDeleteRangesHandler) {}
  explicit AutoBlockElementsJoiner(
      const AutoDeleteRangesHandler& aDeleteRangesHandler)
      : mDeleteRangesHandler(nullptr),
        mDeleteRangesHandlerConst(aDeleteRangesHandler) {}

  /**
   * PrepareToDeleteAtCurrentBlockBoundary() considers left content and right
   * content which are joined for handling deletion at current block boundary
   * (i.e., at start or end of the current block).
   *
   * @param aHTMLEditor               The HTML editor.
   * @param aDirectionAndAmount       Direction of the deletion.
   * @param aCurrentBlockElement      The current block element.
   * @param aCaretPoint               The caret point (i.e., selection start
   *                                  or end).
   * @param aEditingHost              The editing host.
   * @return                          true if can continue to handle the
   *                                  deletion.
   */
  [[nodiscard]] bool PrepareToDeleteAtCurrentBlockBoundary(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint,
      const Element& aEditingHost);

  /**
   * PrepareToDeleteAtOtherBlockBoundary() considers left content and right
   * content which are joined for handling deletion at other block boundary
   * (i.e., immediately before or after a block).
   *
   * @param aHTMLEditor               The HTML editor.
   * @param aDirectionAndAmount       Direction of the deletion.
   * @param aOtherBlockElement        The block element which follows the
   *                                  caret or is followed by caret.
   * @param aCaretPoint               The caret point (i.e., selection start
   *                                  or end).
   * @param aWSRunScannerAtCaret      WSRunScanner instance which was
   *                                  initialized with the caret point.
   * @return                          true if can continue to handle the
   *                                  deletion.
   */
  [[nodiscard]] bool PrepareToDeleteAtOtherBlockBoundary(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      Element& aOtherBlockElement, const EditorDOMPoint& aCaretPoint,
      const WSRunScanner& aWSRunScannerAtCaret);

  /**
   * PrepareToDeleteNonCollapsedRange() considers left block element and
   * right block element which are inclusive ancestor block element of
   * start and end container of aRangeToDelete
   *
   * @param aHTMLEditor               The HTML editor.
   * @param aRangeToDelete            The range to delete.  Must not be
   *                                  collapsed.
   * @param aEditingHost              The editing host.
   * @return                          true if can continue to handle the
   *                                  deletion.
   */
  [[nodiscard]] bool PrepareToDeleteNonCollapsedRange(
      const HTMLEditor& aHTMLEditor, const nsRange& aRangeToDelete,
      const Element& aEditingHost);

  /**
   * Run() executes the joining.
   *
   * @param aHTMLEditor               The HTML editor.
   * @param aDirectionAndAmount       Direction of the deletion.
   * @param aStripWrappers            Must be eStrip or eNoStrip.
   * @param aCaretPoint               The caret point (i.e., selection start
   *                                  or end).
   * @param aRangeToDelete            The range to delete.  This should be
   *                                  collapsed and match with aCaretPoint.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
      HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      nsIEditor::EStripWrappers aStripWrappers,
      const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete,
      const Element& aEditingHost);

  nsresult ComputeRangeToDelete(const HTMLEditor& aHTMLEditor,
                                nsIEditor::EDirection aDirectionAndAmount,
                                const EditorDOMPoint& aCaretPoint,
                                nsRange& aRangeToDelete,
                                const Element& aEditingHost) const;

  /**
   * Run() executes the joining.
   *
   * @param aHTMLEditor               The HTML editor.
   * @param aLimitersAndCaretData     The data copied from nsFrameSelection.
   * @param aDirectionAndAmount       Direction of the deletion.
   * @param aStripWrappers            Whether delete or keep new empty
   *                                  ancestor elements.
   * @param aRangeToDelete            The range to delete.  Must not be
   *                                  collapsed.
   * @param aSelectionWasCollapsed    Whether selection was or was not
   *                                  collapsed when starting to handle
   *                                  deletion.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run(
      HTMLEditor& aHTMLEditor,
      const LimitersAndCaretData& aLimitersAndCaretData,
      nsIEditor::EDirection aDirectionAndAmount,
      nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
      AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
      const Element& aEditingHost);

  nsresult ComputeRangeToDelete(
      const HTMLEditor& aHTMLEditor,
      const AutoClonedSelectionRangeArray& aRangesToDelete,
      nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete,
      AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
      const Element& aEditingHost) const;

  [[nodiscard]] nsIContent* GetLeafContentInOtherBlockElement() const {
    MOZ_ASSERT(mMode == Mode::JoinOtherBlock);
    return mLeafContentInOtherBlock;
  }

 private:
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  HandleDeleteAtCurrentBlockBoundary(HTMLEditor& aHTMLEditor,
                                     nsIEditor::EDirection aDirectionAndAmount,
                                     const EditorDOMPoint& aCaretPoint,
                                     const Element& aEditingHost);
  nsresult ComputeRangeToDeleteAtCurrentBlockBoundary(
      const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
      nsRange& aRangeToDelete, const Element& aEditingHost) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  HandleDeleteAtOtherBlockBoundary(HTMLEditor& aHTMLEditor,
                                   nsIEditor::EDirection aDirectionAndAmount,
                                   nsIEditor::EStripWrappers aStripWrappers,
                                   const EditorDOMPoint& aCaretPoint,
                                   nsRange& aRangeToDelete,
                                   const Element& aEditingHost);
  // FYI: This method may modify selection, but it won't cause running
  //      script because of `AutoHideSelectionChanges` which blocks
  //      selection change listeners and the selection change event
  //      dispatcher.
  MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult ComputeRangeToDeleteAtOtherBlockBoundary(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete,
      const Element& aEditingHost) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  JoinBlockElementsInSameParent(
      HTMLEditor& aHTMLEditor,
      const LimitersAndCaretData& aLimitersAndCaretData,
      nsIEditor::EDirection aDirectionAndAmount,
      nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
      AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
      const Element& aEditingHost);
  nsresult ComputeRangeToJoinBlockElementsInSameParent(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      nsRange& aRangeToDelete, const Element& aEditingHost) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  HandleDeleteLineBreak(HTMLEditor& aHTMLEditor,
                        nsIEditor::EDirection aDirectionAndAmount,
                        const EditorDOMPoint& aCaretPoint,
                        const Element& aEditingHost);
  nsresult ComputeRangeToDeleteLineBreak(
      const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete,
      const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  DeleteContentInRange(HTMLEditor& aHTMLEditor,
                       const LimitersAndCaretData& aLimitersAndCaretData,
                       nsIEditor::EDirection aDirectionAndAmount,
                       nsIEditor::EStripWrappers aStripWrappers,
                       nsRange& aRangeToDelete, const Element& aEditingHost);
  nsresult ComputeRangeToDeleteContentInRange(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      nsRange& aRange, const Element& aEditingHost) const;
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult>
  HandleDeleteNonCollapsedRange(
      HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete,
      AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
      const Element& aEditingHost);
  nsresult ComputeRangeToDeleteNonCollapsedRange(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      nsRange& aRangeToDelete,
      AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed,
      const Element& aEditingHost) const;

  /**
   * JoinNodesDeepWithTransaction() joins aLeftNode and aRightNode "deeply".
   * First, they are joined simply, then, new right node is assumed as the
   * child at length of the left node before joined and new left node is
   * assumed as its previous sibling.  Then, they will be joined again.
   * And then, these steps are repeated.
   *
   * @param aLeftContent    The node which will be removed form the tree.
   * @param aRightContent   The node which will be inserted the contents of
   *                        aRightContent.
   * @return                The point of the first child of the last right
   * node. The result is always set if this succeeded.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult>
  JoinNodesDeepWithTransaction(HTMLEditor& aHTMLEditor,
                               nsIContent& aLeftContent,
                               nsIContent& aRightContent);

  enum class PutCaretTo : bool { StartOfRange, EndOfRange };

  /**
   * DeleteNodesEntirelyInRangeButKeepTableStructure() removes each node in
   * aArrayOfContent.  However, if some nodes are part of a table, removes all
   * children of them instead.  I.e., this does not make damage to table
   * structure at the range, but may remove table entirely if it's in the
   * range.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<DeleteRangeResult, nsresult>
  DeleteNodesEntirelyInRangeButKeepTableStructure(
      HTMLEditor& aHTMLEditor,
      const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContent,
      PutCaretTo aPutCaretTo);
  [[nodiscard]] bool NeedsToJoinNodesAfterDeleteNodesEntirelyInRange() const;
  Result<bool, nsresult>
  ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure(
      const HTMLEditor& aHTMLEditor, nsRange& aRange,
      AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed)
      const;

  /**
   * DeleteContentButKeepTableStructure() removes aContent if it's an element
   * which is part of a table structure.  If it's a part of table structure,
   * removes its all children recursively.  I.e., this may delete all of a
   * table, but won't break table structure partially.
   *
   * @param aContent            The content which or whose all children should
   *                            be removed.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<DeleteRangeResult, nsresult>
  DeleteContentButKeepTableStructure(HTMLEditor& aHTMLEditor,
                                     nsIContent& aContent);

  /**
   * DeleteTextAtStartAndEndOfRange() removes text if start and/or end of
   * aRange is in a text node.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<DeleteRangeResult, nsresult>
  DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange,
                                 PutCaretTo aPutCaretTo);

  /**
   * Return a block element which is an inclusive ancestor of the container of
   * aPoint if aPoint is start of ancestor blocks.  For example, if `<div
   * id=div1>abc<div id=div2><div id=div3>[]def</div></div></div>`, return
   * #div2.
   */
  template <typename EditorDOMPointType>
  [[nodiscard]] static Result<Element*, nsresult>
  GetMostDistantBlockAncestorIfPointIsStartAtBlock(
      const EditorDOMPointType& aPoint, const Element& aEditingHost,
      const Element* aAncestorLimiter = nullptr);

  /**
   * Extend aRangeToDelete to contain new empty inline ancestors and contain
   * an invisible <br> element before right child block which causes an empty
   * line but the range starts after it.
   */
  void ExtendRangeToDeleteNonCollapsedRange(
      const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete,
      const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const;

  /**
   * Compute mLeafContentInOtherBlock from the DOM.
   */
  [[nodiscard]] nsIContent* ComputeLeafContentInOtherBlockElement(
      nsIEditor::EDirection aDirectionAndAmount) const;

  class MOZ_STACK_CLASS AutoInclusiveAncestorBlockElementsJoiner;

  enum class Mode {
    NotInitialized,
    JoinCurrentBlock,
    JoinOtherBlock,
    JoinBlocksInSameParent,
    DeleteBRElement,
    // The instance will handle only the <br> element immediately before a
    // block.
    DeletePrecedingBRElementOfBlock,
    // The instance will handle only the preceding preformatted line break
    // before a block.
    DeletePrecedingPreformattedLineBreak,
    DeleteContentInRange,
    DeleteNonCollapsedRange,
    // The instance will handle preceding lines of the right block and content
    // in the range in the right block.
    DeletePrecedingLinesAndContentInRange,
  };
  AutoDeleteRangesHandler* mDeleteRangesHandler;
  const AutoDeleteRangesHandler& mDeleteRangesHandlerConst;
  nsCOMPtr<nsIContent> mLeftContent;
  nsCOMPtr<nsIContent> mRightContent;
  nsCOMPtr<nsIContent> mLeafContentInOtherBlock;
  RefPtr<Element> mOtherBlockElement;
  // mSkippedInvisibleContents stores all content nodes which are skipped at
  // scanning mLeftContent and mRightContent.  The content nodes should be
  // removed at deletion.
  AutoTArray<OwningNonNull<nsIContent>, 8> mSkippedInvisibleContents;
  RefPtr<dom::HTMLBRElement> mBRElement;
  EditorDOMPointInText mPreformattedLineBreak;
  Mode mMode = Mode::NotInitialized;
};

/**
 * Actually handle joining inclusive ancestor block elements.
 */
class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler::
    AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner final {
 public:
  AutoInclusiveAncestorBlockElementsJoiner() = delete;
  AutoInclusiveAncestorBlockElementsJoiner(
      nsIContent& aInclusiveDescendantOfLeftBlockElement,
      nsIContent& aInclusiveDescendantOfRightBlockElement)
      : mInclusiveDescendantOfLeftBlockElement(
            aInclusiveDescendantOfLeftBlockElement),
        mInclusiveDescendantOfRightBlockElement(
            aInclusiveDescendantOfRightBlockElement),
        mCanJoinBlocks(false),
        mFallbackToDeleteLeafContent(false) {}

  [[nodiscard]] bool IsSet() const {
    return mLeftBlockElement && mRightBlockElement;
  }
  [[nodiscard]] bool IsSameBlockElement() const {
    return mLeftBlockElement && mLeftBlockElement == mRightBlockElement;
  }

  /**
   * Prepare for joining inclusive ancestor block elements.  When this
   * returns false, the deletion should be canceled.
   */
  [[nodiscard]] Result<bool, nsresult> Prepare(const HTMLEditor& aHTMLEditor,
                                               const Element& aEditingHost);

  /**
   * When this returns true, this can join the blocks with `Run()`.
   */
  [[nodiscard]] bool CanJoinBlocks() const { return mCanJoinBlocks; }

  /**
   * When this returns true, `Run()` must return "ignored" so that
   * caller can skip calling `Run()`.  This is available only when
   * `CanJoinBlocks()` returns `true`.
   * TODO: This should be merged into `CanJoinBlocks()` in the future.
   */
  [[nodiscard]] bool ShouldDeleteLeafContentInstead() const {
    MOZ_ASSERT(CanJoinBlocks());
    return mFallbackToDeleteLeafContent;
  }

  /**
   * ComputeRangesToDelete() extends aRangeToDelete includes the element
   * boundaries between joining blocks.  If they won't be joined, this
   * collapses the range to aCaretPoint.
   */
  [[nodiscard]] nsresult ComputeRangeToDelete(
      const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint,
      nsRange& aRangeToDelete, const Element& aEditingHost) const;

  /**
   * Join inclusive ancestor block elements which are found by preceding
   * Prepare() call.
   * The right element is always joined to the left element.
   * If the elements are the same type and not nested within each other,
   * JoinEditableNodesWithTransaction() is called (example, joining two
   * list items together into one).
   * If the elements are not the same type, or one is a descendant of the
   * other, we instead destroy the right block placing its children into
   * left block.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<DeleteRangeResult, nsresult> Run(
      HTMLEditor& aHTMLEditor, const Element& aEditingHost);

 private:
  /**
   * This method returns true when
   * `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`,
   * `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` and
   * `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` handle it
   * with the `if` block of the main lambda of them.
   */
  [[nodiscard]] bool CanMergeLeftAndRightBlockElements() const {
    if (!IsSet()) {
      return false;
    }
    // `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`
    if (mPointContainingTheOtherBlockElement.GetContainer() ==
        mRightBlockElement) {
      return mNewListElementTagNameOfRightListElement.isSome();
    }
    // `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()`
    if (mPointContainingTheOtherBlockElement.GetContainer() ==
        mLeftBlockElement) {
      return mNewListElementTagNameOfRightListElement.isSome() &&
             mRightBlockElement->GetChildCount();
    }
    MOZ_ASSERT(!mPointContainingTheOtherBlockElement.IsSet());
    // `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()`
    return mNewListElementTagNameOfRightListElement.isSome() ||
           (mLeftBlockElement->NodeInfo()->NameAtom() ==
                mRightBlockElement->NodeInfo()->NameAtom() &&
            EditorUtils::GetComputedWhiteSpaceStyles(*mLeftBlockElement) ==
                EditorUtils::GetComputedWhiteSpaceStyles(*mRightBlockElement));
  }

  OwningNonNull<nsIContent> mInclusiveDescendantOfLeftBlockElement;
  OwningNonNull<nsIContent> mInclusiveDescendantOfRightBlockElement;
  RefPtr<Element> mLeftBlockElement;
  RefPtr<Element> mRightBlockElement;
  Maybe<nsAtom*> mNewListElementTagNameOfRightListElement;
  EditorDOMPoint mPointContainingTheOtherBlockElement;
  RefPtr<dom::HTMLBRElement> mPrecedingInvisibleBRElement;
  bool mCanJoinBlocks;
  bool mFallbackToDeleteLeafContent;
};

/**
 * Handle deleting empty block ancestors.
 */
class MOZ_STACK_CLASS
HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter final {
 public:
  /**
   * ScanEmptyBlockInclusiveAncestor() scans an inclusive ancestor element
   * which is empty and a block element.  Then, stores the result and
   * returns the found empty block element.
   *
   * @param aHTMLEditor         The HTMLEditor.
   * @param aStartContent       Start content to look for empty ancestors.
   */
  [[nodiscard]] Element* ScanEmptyBlockInclusiveAncestor(
      const HTMLEditor& aHTMLEditor, nsIContent& aStartContent);

  /**
   * ComputeTargetRanges() computes "target ranges" for deleting
   * `mEmptyInclusiveAncestorBlockElement`.
   */
  nsresult ComputeTargetRanges(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      const Element& aEditingHost,
      AutoClonedSelectionRangeArray& aRangesToDelete) const;

  /**
   * Deletes found empty block element by `ScanEmptyBlockInclusiveAncestor()`.
   * If found one is a list item element, calls
   * `MaybeInsertBRElementBeforeEmptyListItemElement()` before deleting
   * the list item element.
   * If found empty ancestor is not a list item element,
   * `GetNewCaretPosition()` will be called to determine new caret position.
   * Finally, removes the empty block ancestor.
   *
   * @param aHTMLEditor         The HTMLEditor.
   * @param aDirectionAndAmount If found empty ancestor block is a list item
   *                            element, this is ignored.  Otherwise:
   *                            - If eNext, eNextWord or eToEndOfLine,
   *                              collapse Selection to after found empty
   *                              ancestor.
   *                            - If ePrevious, ePreviousWord or
   *                              eToBeginningOfLine, collapse Selection to
   *                              end of previous editable node.
   *                            - Otherwise, eNone is allowed but does
   *                              nothing.
   * @param aEditingHost        The editing host.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<DeleteRangeResult, nsresult> Run(
      HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      const Element& aEditingHost);

 private:
  /**
   * MaybeReplaceSubListWithNewListItem() replaces
   * mEmptyInclusiveAncestorBlockElement with new list item element
   * (containing <br>) if:
   * - mEmptyInclusiveAncestorBlockElement is a list element
   * - The parent of mEmptyInclusiveAncestorBlockElement is a list element
   * - The parent becomes empty after deletion
   * If this does not perform the replacement, returns "ignored".
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<DeleteRangeResult, nsresult>
  MaybeReplaceSubListWithNewListItem(HTMLEditor& aHTMLEditor);

  /**
   * MaybeInsertBRElementBeforeEmptyListItemElement() inserts a `<br>` element
   * if `mEmptyInclusiveAncestorBlockElement` is a list item element which
   * is first editable element in its parent, and its grand parent is not a
   * list element, inserts a `<br>` element before the empty list item.
   */
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CreateLineBreakResult, nsresult>
  MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor);

  /**
   * GetNewCaretPosition() returns new caret position after deleting
   * `mEmptyInclusiveAncestorBlockElement`.
   */
  [[nodiscard]] Result<CaretPoint, nsresult> GetNewCaretPosition(
      const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount,
      const Element& aEditingHost) const;

  RefPtr<Element> mEmptyInclusiveAncestorBlockElement;
};

/******************************************************************************
 * NormalizedStringToInsertText stores normalized insertion string with
 * normalized surrounding white-spaces if the insertion point is surrounded by
 * collapsible white-spaces.  For deleting invisible (collapsed) white-spaces,
 * this also stores the replace range and new white-space length before and
 * after the inserting text.
 ******************************************************************************/

struct MOZ_STACK_CLASS HTMLEditor::NormalizedStringToInsertText final {
  NormalizedStringToInsertText(
      const nsAString& aStringToInsertWithoutSurroundingWhiteSpaces,
      const EditorDOMPoint& aPointToInsert)
      : mNormalizedString(aStringToInsertWithoutSurroundingWhiteSpaces),
        mReplaceStartOffset(
            aPointToInsert.IsInTextNode() ? aPointToInsert.Offset() : 0u),
        mReplaceEndOffset(mReplaceStartOffset) {
    MOZ_ASSERT(aStringToInsertWithoutSurroundingWhiteSpaces.Length() ==
               InsertingTextLength());
  }

  NormalizedStringToInsertText(
      const nsAString& aStringToInsertWithSurroundingWhiteSpaces,
      uint32_t aInsertOffset, uint32_t aReplaceStartOffset,
      uint32_t aReplaceLength,
      uint32_t aNewPrecedingWhiteSpaceLengthBeforeInsertionString,
      uint32_t aNewFollowingWhiteSpaceLengthAfterInsertionString)
      : mNormalizedString(aStringToInsertWithSurroundingWhiteSpaces),
        mReplaceStartOffset(aReplaceStartOffset),
        mReplaceEndOffset(mReplaceStartOffset + aReplaceLength),
        mReplaceLengthBefore(aInsertOffset - mReplaceStartOffset),
        mReplaceLengthAfter(aReplaceLength - mReplaceLengthBefore),
        mNewLengthBefore(aNewPrecedingWhiteSpaceLengthBeforeInsertionString),
        mNewLengthAfter(aNewFollowingWhiteSpaceLengthAfterInsertionString) {
    MOZ_ASSERT(aReplaceStartOffset <= aInsertOffset);
    MOZ_ASSERT(aReplaceStartOffset + aReplaceLength >= aInsertOffset);
    MOZ_ASSERT(aNewPrecedingWhiteSpaceLengthBeforeInsertionString +
                   aNewFollowingWhiteSpaceLengthAfterInsertionString <
               mNormalizedString.Length());
    MOZ_ASSERT(mReplaceLengthBefore + mReplaceLengthAfter == ReplaceLength());
    MOZ_ASSERT(mReplaceLengthBefore >= mNewLengthBefore);
    MOZ_ASSERT(mReplaceLengthAfter >= mNewLengthAfter);
  }

  NormalizedStringToInsertText GetMinimizedData(const Text& aText) const {
    if (mNormalizedString.IsEmpty() || !ReplaceLength()) {
      return *this;
    }
    const dom::CharacterDataBuffer& characterDataBuffer = aText.DataBuffer();
    const uint32_t minimizedReplaceStart = [&]() {
      const auto firstDiffCharOffset =
          mNewLengthBefore ? characterDataBuffer.FindFirstDifferentCharOffset(
                                 PrecedingWhiteSpaces(), mReplaceStartOffset)
                           : dom::CharacterDataBuffer::kNotFound;
      if (firstDiffCharOffset == dom::CharacterDataBuffer::kNotFound) {
        return
            // We don't need to insert new normalized white-spaces before the
            // inserting string,
            (mReplaceStartOffset + mReplaceLengthBefore)
            // but keep extending the replacing range for deleting invisible
            // white-spaces.
            - DeletingPrecedingInvisibleWhiteSpaces();
      }
      return firstDiffCharOffset;
    }();
    const uint32_t minimizedReplaceEnd = [&]() {
      const auto lastDiffCharOffset =
          mNewLengthAfter ? characterDataBuffer.RFindFirstDifferentCharOffset(
                                FollowingWhiteSpaces(), mReplaceEndOffset)
                          : dom::CharacterDataBuffer::kNotFound;
      if (lastDiffCharOffset == dom::CharacterDataBuffer::kNotFound) {
        return
            // We don't need to insert new normalized white-spaces after the
            // inserting string,
            (mReplaceEndOffset - mReplaceLengthAfter)
            // but keep extending the replacing range for deleting invisible
            // white-spaces.
            + DeletingFollowingInvisibleWhiteSpaces();
      }
      return lastDiffCharOffset + 1u;
    }();
    if (minimizedReplaceStart == mReplaceStartOffset &&
        minimizedReplaceEnd == mReplaceEndOffset) {
      return *this;
    }
    const uint32_t newPrecedingWhiteSpaceLength =
        mNewLengthBefore - (minimizedReplaceStart - mReplaceStartOffset);
    const uint32_t newFollowingWhiteSpaceLength =
        mNewLengthAfter - (mReplaceEndOffset - minimizedReplaceEnd);
    return NormalizedStringToInsertText(
        Substring(mNormalizedString,
                  mNewLengthBefore - newPrecedingWhiteSpaceLength,
                  mNormalizedString.Length() -
                      (mNewLengthBefore - newPrecedingWhiteSpaceLength) -
                      (mNewLengthAfter - newFollowingWhiteSpaceLength)),
        OffsetToInsertText(), minimizedReplaceStart,
        minimizedReplaceEnd - minimizedReplaceStart,
        newPrecedingWhiteSpaceLength, newFollowingWhiteSpaceLength);
  }

  /**
   * Return offset to insert the given text.
   */
  [[nodiscard]] uint32_t OffsetToInsertText() const {
    return mReplaceStartOffset + mReplaceLengthBefore;
  }

  /**
   * Return inserting text length not containing the surrounding white-spaces.
   */
  [[nodiscard]] uint32_t InsertingTextLength() const {
    return mNormalizedString.Length() - mNewLengthBefore - mNewLengthAfter;
  }

  /**
   * Return end offset of inserted string after replacing the text with
   * mNormalizedString.
   */
  [[nodiscard]] uint32_t EndOffsetOfInsertedText() const {
    return OffsetToInsertText() + InsertingTextLength();
  }

  /**
   * Return the length to replace with mNormalizedString.  The result means that
   * it's the length of surrounding white-spaces at the insertion point.
   */
  [[nodiscard]] uint32_t ReplaceLength() const {
    return mReplaceEndOffset - mReplaceStartOffset;
  }

  [[nodiscard]] uint32_t DeletingPrecedingInvisibleWhiteSpaces() const {
    return mReplaceLengthBefore - mNewLengthBefore;
  }
  [[nodiscard]] uint32_t DeletingFollowingInvisibleWhiteSpaces() const {
    return mReplaceLengthAfter - mNewLengthAfter;
  }

  [[nodiscard]] nsDependentSubstring PrecedingWhiteSpaces() const {
    return Substring(mNormalizedString, 0u, mNewLengthBefore);
  }
  [[nodiscard]] nsDependentSubstring FollowingWhiteSpaces() const {
    return Substring(mNormalizedString,
                     mNormalizedString.Length() - mNewLengthAfter);
  }

  // Normalizes string which should be inserted.
  nsAutoString mNormalizedString;
  // Start offset in the `Text` to replace.
  const uint32_t mReplaceStartOffset;
  // End offset in the `Text` to replace.
  const uint32_t mReplaceEndOffset;
  // If it needs to replace preceding and/or following white-spaces, these
  // members store the length of white-spaces which should be replaced
  // before/after the insertion point.
  const uint32_t mReplaceLengthBefore = 0u;
  const uint32_t mReplaceLengthAfter = 0u;
  // If it needs to replace preceding and/or following white-spaces, these
  // members store the new length of white-spaces before/after the insertion
  // string.
  const uint32_t mNewLengthBefore = 0u;
  const uint32_t mNewLengthAfter = 0u;
};

/******************************************************************************
 * ReplaceWhiteSpacesData stores normalized string to replace white-spaces in
 * a `Text`.  If ReplaceLength() returns 0, this user needs to do nothing.
 ******************************************************************************/

struct MOZ_STACK_CLASS HTMLEditor::ReplaceWhiteSpacesData final {
  ReplaceWhiteSpacesData() = default;

  /**
   * @param aWhiteSpaces        The new white-spaces which we will replace the
   *                            range with.
   * @param aStartOffset        Replace start offset in the text node.
   * @param aReplaceLength      Replace length in the text node.
   * @param aOffsetAfterReplacing
   *                            [optional] If the caller may want to put caret
   *                            middle of the white-spaces, the offset may be
   *                            changed by deleting some invisible white-spaces.
   *                            Therefore, this may be set for the purpose.
   */
  ReplaceWhiteSpacesData(const nsAString& aWhiteSpaces, uint32_t aStartOffset,
                         uint32_t aReplaceLength,
                         uint32_t aOffsetAfterReplacing = UINT32_MAX)
      : mNormalizedString(aWhiteSpaces),
        mReplaceStartOffset(aStartOffset),
        mReplaceEndOffset(aStartOffset + aReplaceLength),
        mNewOffsetAfterReplace(aOffsetAfterReplacing) {
    MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length());
    MOZ_ASSERT_IF(mNewOffsetAfterReplace != UINT32_MAX,
                  mNewOffsetAfterReplace <=
                      mReplaceStartOffset + mNormalizedString.Length());
  }

  /**
   * @param aWhiteSpaces        The new white-spaces which we will replace the
   *                            range with.
   * @param aStartOffset        Replace start offset in the text node.
   * @param aReplaceLength      Replace length in the text node.
   * @param aOffsetAfterReplacing
   *                            [optional] If the caller may want to put caret
   *                            middle of the white-spaces, the offset may be
   *                            changed by deleting some invisible white-spaces.
   *                            Therefore, this may be set for the purpose.
   */
  ReplaceWhiteSpacesData(nsAutoString&& aWhiteSpaces, uint32_t aStartOffset,
                         uint32_t aReplaceLength,
                         uint32_t aOffsetAfterReplacing = UINT32_MAX)
      : mNormalizedString(std::forward<nsAutoString>(aWhiteSpaces)),
        mReplaceStartOffset(aStartOffset),
        mReplaceEndOffset(aStartOffset + aReplaceLength),
        mNewOffsetAfterReplace(aOffsetAfterReplacing) {
    MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length());
    MOZ_ASSERT_IF(mNewOffsetAfterReplace != UINT32_MAX,
                  mNewOffsetAfterReplace <=
                      mReplaceStartOffset + mNormalizedString.Length());
  }

  ReplaceWhiteSpacesData GetMinimizedData(const Text& aText) const {
    if (!ReplaceLength()) {
      return *this;
    }
    const dom::CharacterDataBuffer& characterDataBuffer = aText.DataBuffer();
    const auto minimizedReplaceStart = [&]() -> uint32_t {
      if (mNormalizedString.IsEmpty()) {
        return mReplaceStartOffset;
      }
      const uint32_t firstDiffCharOffset =
          characterDataBuffer.FindFirstDifferentCharOffset(mNormalizedString,
                                                           mReplaceStartOffset);
      if (firstDiffCharOffset == dom::CharacterDataBuffer::kNotFound) {
        // We don't need to insert new white-spaces,
        return mReplaceStartOffset + mNormalizedString.Length();
      }
      return firstDiffCharOffset;
    }();
    const auto minimizedReplaceEnd = [&]() -> uint32_t {
      if (mNormalizedString.IsEmpty()) {
        return mReplaceEndOffset;
      }
      if (minimizedReplaceStart ==
          mReplaceStartOffset + mNormalizedString.Length()) {
        // Note that here may be invisible white-spaces before
        // mReplaceEndOffset.  Then, this value may be larger than
        // minimizedReplaceStart.
        MOZ_ASSERT(mReplaceEndOffset >= minimizedReplaceStart);
        return mReplaceEndOffset;
      }
      if (ReplaceLength() != mNormalizedString.Length()) {
        // If we're deleting some invisible white-spaces, don't shrink the end
        // of the replacing range because it may shrink mNormalizedString too
        // much.
        return mReplaceEndOffset;
      }
      const auto lastDiffCharOffset =
          characterDataBuffer.RFindFirstDifferentCharOffset(mNormalizedString,
                                                            mReplaceEndOffset);
      MOZ_ASSERT(lastDiffCharOffset != dom::CharacterDataBuffer::kNotFound);
      return lastDiffCharOffset == dom::CharacterDataBuffer::kNotFound
                 ? mReplaceEndOffset
                 : lastDiffCharOffset + 1u;
    }();
    if (minimizedReplaceStart == mReplaceStartOffset &&
        minimizedReplaceEnd == mReplaceEndOffset) {
      return *this;
    }
    const uint32_t precedingUnnecessaryLength =
        minimizedReplaceStart - mReplaceStartOffset;
    const uint32_t followingUnnecessaryLength =
        mReplaceEndOffset - minimizedReplaceEnd;
    return ReplaceWhiteSpacesData(
        Substring(mNormalizedString, precedingUnnecessaryLength,
                  mNormalizedString.Length() - (precedingUnnecessaryLength +
                                                followingUnnecessaryLength)),
        minimizedReplaceStart, minimizedReplaceEnd - minimizedReplaceStart,
        mNewOffsetAfterReplace);
  }

  /**
   * Return the normalized string before mNewOffsetAfterReplace.  So,
   * mNewOffsetAfterReplace must not be UINT32_MAX and in the replaced range
   * when this is called.
   *
   * @param aReplaceEndOffset   Specify the offset in the Text node of
   *                            mNewOffsetAfterReplace before replacing with the
   *                            data.
   * @return The substring before mNewOffsetAfterReplace which is typically set
   * for new caret position in the Text node or collapsed deleting range
   * surrounded by the white-spaces.
   */
  [[nodiscard]] ReplaceWhiteSpacesData PreviousDataOfNewOffset(
      uint32_t aReplaceEndOffset) const {
    MOZ_ASSERT(mNewOffsetAfterReplace != UINT32_MAX);
    MOZ_ASSERT(mReplaceStartOffset <= mNewOffsetAfterReplace);
    MOZ_ASSERT(mReplaceEndOffset >= mNewOffsetAfterReplace);
    MOZ_ASSERT(mReplaceStartOffset <= aReplaceEndOffset);
    MOZ_ASSERT(mReplaceEndOffset >= aReplaceEndOffset);
    if (!ReplaceLength() || aReplaceEndOffset == mReplaceStartOffset) {
      return ReplaceWhiteSpacesData();
    }
    return ReplaceWhiteSpacesData(
        Substring(mNormalizedString, 0u,
                  mNewOffsetAfterReplace - mReplaceStartOffset),
        mReplaceStartOffset, aReplaceEndOffset - mReplaceStartOffset);
  }

  /**
   * Return the normalized string after mNewOffsetAfterReplace.  So,
   * mNewOffsetAfterReplace must not be UINT32_MAX and in the replaced range
   * when this is called.
   *
   * @param aReplaceStartOffset Specify the replace start offset with the
   *                            normalized white-spaces.
   * @return The substring after mNewOffsetAfterReplace which is typically set
   * for new caret position in the Text node or collapsed deleting range
   * surrounded by the white-spaces.
   */
  [[nodiscard]] ReplaceWhiteSpacesData NextDataOfNewOffset(
      uint32_t aReplaceStartOffset) const {
    MOZ_ASSERT(mNewOffsetAfterReplace != UINT32_MAX);
    MOZ_ASSERT(mReplaceStartOffset <= mNewOffsetAfterReplace);
    MOZ_ASSERT(mReplaceEndOffset >= mNewOffsetAfterReplace);
    MOZ_ASSERT(mReplaceStartOffset <= aReplaceStartOffset);
    MOZ_ASSERT(mReplaceEndOffset >= aReplaceStartOffset);
    if (!ReplaceLength() || aReplaceStartOffset == mReplaceEndOffset) {
      return ReplaceWhiteSpacesData();
    }
    return ReplaceWhiteSpacesData(
        Substring(mNormalizedString,
                  mNewOffsetAfterReplace - mReplaceStartOffset),
        aReplaceStartOffset, mReplaceEndOffset - aReplaceStartOffset);
  }

  [[nodiscard]] uint32_t ReplaceLength() const {
    return mReplaceEndOffset - mReplaceStartOffset;
  }
  [[nodiscard]] uint32_t DeletingInvisibleWhiteSpaces() const {
    return ReplaceLength() - mNormalizedString.Length();
  }

  [[nodiscard]] ReplaceWhiteSpacesData operator+(
      const ReplaceWhiteSpacesData& aOther) const {
    if (!ReplaceLength()) {
      return aOther;
    }
    if (!aOther.ReplaceLength()) {
      return *this;
    }
    MOZ_ASSERT(mReplaceEndOffset == aOther.mReplaceStartOffset);
    MOZ_ASSERT_IF(
        aOther.mNewOffsetAfterReplace != UINT32_MAX,
        aOther.mNewOffsetAfterReplace >= DeletingInvisibleWhiteSpaces());
    return ReplaceWhiteSpacesData(
        nsAutoString(mNormalizedString + aOther.mNormalizedString),
        mReplaceStartOffset, aOther.mReplaceEndOffset,
        aOther.mNewOffsetAfterReplace != UINT32_MAX
            ? aOther.mNewOffsetAfterReplace - DeletingInvisibleWhiteSpaces()
            : mNewOffsetAfterReplace);
  }

  nsAutoString mNormalizedString;
  const uint32_t mReplaceStartOffset = 0u;
  const uint32_t mReplaceEndOffset = 0u;
  // If the caller specifies a point in a white-space sequence, some invisible
  // white-spaces will be deleted with replacing them with normalized string.
  // Then, they may want to keep the position for putting caret or something.
  // So, this may store a specific offset in the text node after replacing.
  const uint32_t mNewOffsetAfterReplace = UINT32_MAX;
};

/******************************************************************************
 * A runnable to run HTMLEditor::OnModifiedDocument when it's safe.
 ******************************************************************************/

class HTMLEditor::DocumentModifiedEvent final : public Runnable {
 public:
  explicit DocumentModifiedEvent(HTMLEditor& aHTMLEditor)
      : Runnable("DocumentModifiedEvent"), mHTMLEditor(aHTMLEditor) {}

  MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() {
    (void)MOZ_KnownLive(mHTMLEditor)->OnModifyDocument(*this);
    return NS_OK;
  }

  const nsTArray<EditorDOMPointInText>& NewInvisibleWhiteSpacesRef() const {
    return mNewInvisibleWhiteSpaces;
  }

 private:
  ~DocumentModifiedEvent() = default;

  const OwningNonNull<HTMLEditor> mHTMLEditor;
  nsTArray<EditorDOMPointInText> mNewInvisibleWhiteSpaces;
};

}  // namespace mozilla

#endif  // #ifndef HTMLEditorNestedClasses_h
