editor/libeditor/HTMLEditRules.cpp
author Ehsan Akhgari <ehsan@mozilla.com>
Mon, 31 Jul 2017 11:20:08 -0400
changeset 420787 c579ac37ac11a44b9af0c75720f519a40c439b19
parent 420768 2b347fb55a9965acec727f6e40671ba859636603
child 420983 395cb2c77448f9d3f305a189c191c7c6c360844b
permissions -rw-r--r--
Bug 1385514 - Part 2: Replace 'spaz' with 'change' in the editor code

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

#include "HTMLEditRules.h"

#include <stdlib.h>

#include "HTMLEditUtils.h"
#include "TextEditUtils.h"
#include "WSRunObject.h"
#include "mozilla/Assertions.h"
#include "mozilla/CSSEditUtils.h"
#include "mozilla/EditorUtils.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Move.h"
#include "mozilla/Preferences.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/Element.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/mozalloc.h"
#include "nsAString.h"
#include "nsAlgorithm.h"
#include "nsCRT.h"
#include "nsCRTGlue.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsGkAtoms.h"
#include "nsIAtom.h"
#include "nsIContent.h"
#include "nsIContentIterator.h"
#include "nsID.h"
#include "nsIDOMCharacterData.h"
#include "nsIDOMDocument.h"
#include "nsIDOMElement.h"
#include "nsIDOMNode.h"
#include "nsIFrame.h"
#include "nsIHTMLAbsPosEditor.h"
#include "nsIHTMLDocument.h"
#include "nsINode.h"
#include "nsLiteralString.h"
#include "nsRange.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTextNode.h"
#include "nsThreadUtils.h"
#include "nsUnicharUtils.h"
#include <algorithm>

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

class nsISupports;

namespace mozilla {

class RulesInfo;

using namespace dom;

//const static char* kMOZEditorBogusNodeAttr="MOZ_EDITOR_BOGUS_NODE";
//const static char* kMOZEditorBogusNodeValue="TRUE";

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

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

static bool IsBlockNode(const nsINode& node)
{
  return HTMLEditor::NodeIsBlockStatic(&node);
}

static bool IsInlineNode(const nsINode& node)
{
  return !IsBlockNode(node);
}

static bool
IsStyleCachePreservingAction(EditAction action)
{
  return action == EditAction::deleteSelection ||
         action == EditAction::insertBreak ||
         action == EditAction::makeList ||
         action == EditAction::indent ||
         action == EditAction::outdent ||
         action == EditAction::align ||
         action == EditAction::makeBasicBlock ||
         action == EditAction::removeList ||
         action == EditAction::makeDefListItem ||
         action == EditAction::insertElement ||
         action == EditAction::insertQuotation;
}

static nsIAtom&
ParagraphSeparatorElement(ParagraphSeparator separator)
{
  switch (separator) {
    default:
      MOZ_FALLTHROUGH_ASSERT("Unexpected paragraph separator!");

    case ParagraphSeparator::div:
      return *nsGkAtoms::div;

    case ParagraphSeparator::p:
      return *nsGkAtoms::p;

    case ParagraphSeparator::br:
      return *nsGkAtoms::br;
  }
}

class TableCellAndListItemFunctor final : public BoolDomIterFunctor
{
public:
  // Used to build list of all li's, td's & th's iterator covers
  virtual bool operator()(nsINode* aNode) const
  {
    return HTMLEditUtils::IsTableCell(aNode) ||
           HTMLEditUtils::IsListItem(aNode);
  }
};

class BRNodeFunctor final : public BoolDomIterFunctor
{
public:
  virtual bool operator()(nsINode* aNode) const
  {
    return aNode->IsHTMLElement(nsGkAtoms::br);
  }
};

class EmptyEditableFunctor final : public BoolDomIterFunctor
{
public:
  explicit EmptyEditableFunctor(HTMLEditor* aHTMLEditor)
    : mHTMLEditor(aHTMLEditor)
  {}

  virtual bool operator()(nsINode* aNode) const
  {
    if (mHTMLEditor->IsEditable(aNode) &&
        (HTMLEditUtils::IsListItem(aNode) ||
         HTMLEditUtils::IsTableCellOrCaption(*aNode))) {
      bool bIsEmptyNode;
      nsresult rv =
        mHTMLEditor->IsEmptyNode(aNode, &bIsEmptyNode, false, false);
      NS_ENSURE_SUCCESS(rv, false);
      if (bIsEmptyNode) {
        return true;
      }
    }
    return false;
  }

protected:
  HTMLEditor* mHTMLEditor;
};

/********************************************************
 * mozilla::HTMLEditRules
 ********************************************************/

HTMLEditRules::HTMLEditRules()
  : mHTMLEditor(nullptr)
  , mListenerEnabled(false)
  , mReturnInEmptyLIKillsList(false)
  , mDidDeleteSelection(false)
  , mDidRangedDelete(false)
  , mRestoreContentEditableCount(false)
  , mJoinOffset(0)
{
  InitFields();
}

void
HTMLEditRules::InitFields()
{
  mHTMLEditor = nullptr;
  mDocChangeRange = nullptr;
  mListenerEnabled = true;
  mReturnInEmptyLIKillsList = true;
  mDidDeleteSelection = false;
  mDidRangedDelete = false;
  mRestoreContentEditableCount = false;
  mUtilRange = nullptr;
  mJoinOffset = 0;
  mNewBlock = nullptr;
  mRangeItem = new RangeItem();

  InitStyleCacheArray(mCachedStyles);
}

void
HTMLEditRules::InitStyleCacheArray(StyleCache aStyleCache[SIZE_STYLE_TABLE])
{
  aStyleCache[0] = StyleCache(nsGkAtoms::b, EmptyString());
  aStyleCache[1] = StyleCache(nsGkAtoms::i, EmptyString());
  aStyleCache[2] = StyleCache(nsGkAtoms::u, EmptyString());
  aStyleCache[3] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("face"));
  aStyleCache[4] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("size"));
  aStyleCache[5] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("color"));
  aStyleCache[6] = StyleCache(nsGkAtoms::tt, EmptyString());
  aStyleCache[7] = StyleCache(nsGkAtoms::em, EmptyString());
  aStyleCache[8] = StyleCache(nsGkAtoms::strong, EmptyString());
  aStyleCache[9] = StyleCache(nsGkAtoms::dfn, EmptyString());
  aStyleCache[10] = StyleCache(nsGkAtoms::code, EmptyString());
  aStyleCache[11] = StyleCache(nsGkAtoms::samp, EmptyString());
  aStyleCache[12] = StyleCache(nsGkAtoms::var, EmptyString());
  aStyleCache[13] = StyleCache(nsGkAtoms::cite, EmptyString());
  aStyleCache[14] = StyleCache(nsGkAtoms::abbr, EmptyString());
  aStyleCache[15] = StyleCache(nsGkAtoms::acronym, EmptyString());
  aStyleCache[16] = StyleCache(nsGkAtoms::backgroundColor, EmptyString());
  aStyleCache[17] = StyleCache(nsGkAtoms::sub, EmptyString());
  aStyleCache[18] = StyleCache(nsGkAtoms::sup, EmptyString());
}

HTMLEditRules::~HTMLEditRules()
{
  // remove ourselves as a listener to edit actions
  // In some cases, we have already been removed by
  // ~HTMLEditor, in which case we will get a null pointer here
  // which we ignore.  But this allows us to add the ability to
  // switch rule sets on the fly if we want.
  if (mHTMLEditor) {
    mHTMLEditor->RemoveEditActionListener(this);
  }
}

NS_IMPL_ADDREF_INHERITED(HTMLEditRules, TextEditRules)
NS_IMPL_RELEASE_INHERITED(HTMLEditRules, TextEditRules)
NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLEditRules)
  NS_INTERFACE_TABLE_INHERITED(HTMLEditRules, nsIEditActionListener)
NS_INTERFACE_TABLE_TAIL_INHERITING(TextEditRules)

NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLEditRules, TextEditRules,
                                   mDocChangeRange, mUtilRange, mNewBlock,
                                   mRangeItem)

NS_IMETHODIMP
HTMLEditRules::Init(TextEditor* aTextEditor)
{
  if (NS_WARN_IF(!aTextEditor)) {
    return NS_ERROR_INVALID_ARG;
  }

  InitFields();

  mHTMLEditor = aTextEditor->AsHTMLEditor();
  if (NS_WARN_IF(!mHTMLEditor)) {
    return NS_ERROR_INVALID_ARG;
  }

  // call through to base class Init
  nsresult rv = TextEditRules::Init(aTextEditor);
  NS_ENSURE_SUCCESS(rv, rv);

  // cache any prefs we care about
  static const char kPrefName[] =
    "editor.html.typing.returnInEmptyListItemClosesList";
  nsAutoCString returnInEmptyLIKillsList;
  Preferences::GetCString(kPrefName, returnInEmptyLIKillsList);

  // only when "false", becomes FALSE.  Otherwise (including empty), TRUE.
  // XXX Why was this pref designed as a string and not bool?
  mReturnInEmptyLIKillsList = !returnInEmptyLIKillsList.EqualsLiteral("false");

  // make a utility range for use by the listenter
  nsCOMPtr<nsINode> node = mHTMLEditor->GetRoot();
  if (!node) {
    node = mHTMLEditor->GetDocument();
  }

  NS_ENSURE_STATE(node);

  mUtilRange = new nsRange(node);

  // set up mDocChangeRange to be whole doc
  // temporarily turn off rules sniffing
  AutoLockRulesSniffing lockIt(this);
  if (!mDocChangeRange) {
    mDocChangeRange = new nsRange(node);
  }

  if (node->IsElement()) {
    ErrorResult rv;
    mDocChangeRange->SelectNode(*node, rv);
    NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
    AdjustSpecialBreaks();
  }

  // add ourselves as a listener to edit actions
  return mHTMLEditor->AddEditActionListener(this);
}

NS_IMETHODIMP
HTMLEditRules::DetachEditor()
{
  if (mHTMLEditor) {
    mHTMLEditor->RemoveEditActionListener(this);
  }
  mHTMLEditor = nullptr;
  return TextEditRules::DetachEditor();
}

NS_IMETHODIMP
HTMLEditRules::BeforeEdit(EditAction action,
                          nsIEditor::EDirection aDirection)
{
  if (mLockRulesSniffing) {
    return NS_OK;
  }

  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  AutoLockRulesSniffing lockIt(this);
  mDidExplicitlySetInterline = false;

  if (!mActionNesting) {
    mActionNesting++;

    // Clear our flag about if just deleted a range
    mDidRangedDelete = false;

    // Remember where our selection was before edit action took place:

    // Get selection
    RefPtr<Selection> selection = htmlEditor->GetSelection();

    // Get the selection location
    if (NS_WARN_IF(!selection) || !selection->RangeCount()) {
      return NS_ERROR_UNEXPECTED;
    }
    mRangeItem->mStartContainer = selection->GetRangeAt(0)->GetStartContainer();
    mRangeItem->mStartOffset = selection->GetRangeAt(0)->StartOffset();
    mRangeItem->mEndContainer = selection->GetRangeAt(0)->GetEndContainer();
    mRangeItem->mEndOffset = selection->GetRangeAt(0)->EndOffset();
    nsCOMPtr<nsINode> selStartNode = mRangeItem->mStartContainer;
    nsCOMPtr<nsINode> selEndNode = mRangeItem->mEndContainer;

    // Register with range updater to track this as we perturb the doc
    htmlEditor->mRangeUpdater.RegisterRangeItem(mRangeItem);

    // Clear deletion state bool
    mDidDeleteSelection = false;

    // Clear out mDocChangeRange and mUtilRange
    if (mDocChangeRange) {
      // Clear out our accounting of what changed
      mDocChangeRange->Reset();
    }
    if (mUtilRange) {
      // Ditto for mUtilRange.
      mUtilRange->Reset();
    }

    // Remember current inline styles for deletion and normal insertion ops
    if (action == EditAction::insertText ||
        action == EditAction::insertIMEText ||
        action == EditAction::deleteSelection ||
        IsStyleCachePreservingAction(action)) {
      nsCOMPtr<nsINode> selNode =
        aDirection == nsIEditor::eNext ? selEndNode : selStartNode;
      nsresult rv = CacheInlineStyles(GetAsDOMNode(selNode));
      NS_ENSURE_SUCCESS(rv, rv);
    }

    // Stabilize the document against contenteditable count changes
    nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument();
    NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
    nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc);
    NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE);
    if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) {
      htmlDoc->ChangeContentEditableCount(nullptr, +1);
      mRestoreContentEditableCount = true;
    }

    // Check that selection is in subtree defined by body node
    ConfirmSelectionInBody();
    // Let rules remember the top level action
    mTheAction = action;
  }
  return NS_OK;
}


NS_IMETHODIMP
HTMLEditRules::AfterEdit(EditAction action,
                         nsIEditor::EDirection aDirection)
{
  if (mLockRulesSniffing) {
    return NS_OK;
  }

  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  AutoLockRulesSniffing lockIt(this);

  MOZ_ASSERT(mActionNesting > 0);
  nsresult rv = NS_OK;
  mActionNesting--;
  if (!mActionNesting) {
    // Do all the tricky stuff
    rv = AfterEditInner(action, aDirection);

    // Free up selectionState range item
    htmlEditor->mRangeUpdater.DropRangeItem(mRangeItem);

    // Reset the contenteditable count to its previous value
    if (mRestoreContentEditableCount) {
      nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument();
      NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
      nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc);
      NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE);
      if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) {
        htmlDoc->ChangeContentEditableCount(nullptr, -1);
      }
      mRestoreContentEditableCount = false;
    }
  }

  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult
HTMLEditRules::AfterEditInner(EditAction action,
                              nsIEditor::EDirection aDirection)
{
  ConfirmSelectionInBody();
  if (action == EditAction::ignore) {
    return NS_OK;
  }

  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<Selection> selection = mHTMLEditor->GetSelection();
  NS_ENSURE_STATE(selection);

  nsCOMPtr<nsIDOMNode> rangeStartContainer, rangeEndContainer;
  uint32_t rangeStartOffset = 0, rangeEndOffset = 0;
  // do we have a real range to act on?
  bool bDamagedRange = false;
  if (mDocChangeRange) {
    mDocChangeRange->GetStartContainer(getter_AddRefs(rangeStartContainer));
    mDocChangeRange->GetEndContainer(getter_AddRefs(rangeEndContainer));
    mDocChangeRange->GetStartOffset(&rangeStartOffset);
    mDocChangeRange->GetEndOffset(&rangeEndOffset);
    if (rangeStartContainer && rangeEndContainer) {
      bDamagedRange = true;
    }
  }

  if (bDamagedRange && !((action == EditAction::undo) ||
                         (action == EditAction::redo))) {
    // don't let any txns in here move the selection around behind our back.
    // Note that this won't prevent explicit selection setting from working.
    NS_ENSURE_STATE(mHTMLEditor);
    AutoTransactionsConserveSelection dontChangeMySelection(mHTMLEditor);

    // expand the "changed doc range" as needed
    PromoteRange(*mDocChangeRange, action);

    // if we did a ranged deletion or handling backspace key, make sure we have
    // a place to put caret.
    // Note we only want to do this if the overall operation was deletion,
    // not if deletion was done along the way for EditAction::loadHTML, EditAction::insertText, etc.
    // That's why this is here rather than DidDeleteSelection().
    if (action == EditAction::deleteSelection && mDidRangedDelete) {
      nsresult rv = InsertBRIfNeeded(selection);
      NS_ENSURE_SUCCESS(rv, rv);
    }

    // add in any needed <br>s, and remove any unneeded ones.
    AdjustSpecialBreaks();

    // merge any adjacent text nodes
    if (action != EditAction::insertText &&
        action != EditAction::insertIMEText) {
      NS_ENSURE_STATE(mHTMLEditor);
      nsresult rv = mHTMLEditor->CollapseAdjacentTextNodes(mDocChangeRange);
      NS_ENSURE_SUCCESS(rv, rv);
    }

    // clean up any empty nodes in the selection
    nsresult rv = RemoveEmptyNodes();
    NS_ENSURE_SUCCESS(rv, rv);

    // attempt to transform any unneeded nbsp's into spaces after doing various operations
    if (action == EditAction::insertText ||
        action == EditAction::insertIMEText ||
        action == EditAction::deleteSelection ||
        action == EditAction::insertBreak ||
        action == EditAction::htmlPaste ||
        action == EditAction::loadHTML) {
      rv = AdjustWhitespace(selection);
      NS_ENSURE_SUCCESS(rv, rv);

      // also do this for original selection endpoints.
      NS_ENSURE_STATE(mHTMLEditor);
      NS_ENSURE_STATE(mRangeItem->mStartContainer);
      NS_ENSURE_STATE(mRangeItem->mEndContainer);
      WSRunObject(mHTMLEditor, mRangeItem->mStartContainer,
                  mRangeItem->mStartOffset).AdjustWhitespace();
      // we only need to handle old selection endpoint if it was different from start
      if (mRangeItem->mStartContainer != mRangeItem->mEndContainer ||
          mRangeItem->mStartOffset != mRangeItem->mEndOffset) {
        NS_ENSURE_STATE(mHTMLEditor);
        WSRunObject(mHTMLEditor, mRangeItem->mEndContainer,
                    mRangeItem->mEndOffset).AdjustWhitespace();
      }
    }

    // if we created a new block, make sure selection lands in it
    if (mNewBlock) {
      rv = PinSelectionToNewBlock(selection);
      mNewBlock = nullptr;
    }

    // adjust selection for insert text, html paste, and delete actions
    if (action == EditAction::insertText ||
        action == EditAction::insertIMEText ||
        action == EditAction::deleteSelection ||
        action == EditAction::insertBreak ||
        action == EditAction::htmlPaste ||
        action == EditAction::loadHTML) {
      rv = AdjustSelection(selection, aDirection);
      NS_ENSURE_SUCCESS(rv, rv);
    }

    // check for any styles which were removed inappropriately
    if (action == EditAction::insertText ||
        action == EditAction::insertIMEText ||
        action == EditAction::deleteSelection ||
        IsStyleCachePreservingAction(action)) {
      NS_ENSURE_STATE(mHTMLEditor);
      mHTMLEditor->mTypeInState->UpdateSelState(selection);
      rv = ReapplyCachedStyles();
      NS_ENSURE_SUCCESS(rv, rv);
      ClearCachedStyles();
    }
  }

  NS_ENSURE_STATE(mHTMLEditor);

  nsresult rv =
    mHTMLEditor->HandleInlineSpellCheck(
                   action, selection,
                   GetAsDOMNode(mRangeItem->mStartContainer),
                   mRangeItem->mStartOffset,
                   rangeStartContainer, static_cast<int32_t>(rangeStartOffset),
                   rangeEndContainer, static_cast<int32_t>(rangeEndOffset));
  NS_ENSURE_SUCCESS(rv, rv);

  // detect empty doc
  rv = CreateBogusNodeIfNeeded(selection);
  NS_ENSURE_SUCCESS(rv, rv);

  // adjust selection HINT if needed
  if (!mDidExplicitlySetInterline) {
    CheckInterlinePosition(*selection);
  }

  return NS_OK;
}

NS_IMETHODIMP
HTMLEditRules::WillDoAction(Selection* aSelection,
                            RulesInfo* aInfo,
                            bool* aCancel,
                            bool* aHandled)
{
  MOZ_ASSERT(aInfo && aCancel && aHandled);

  *aCancel = false;
  *aHandled = false;

  // my kingdom for dynamic cast
  TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo);

  // Deal with actions for which we don't need to check whether the selection is
  // editable.
  if (info->action == EditAction::outputText ||
      info->action == EditAction::undo ||
      info->action == EditAction::redo) {
    return TextEditRules::WillDoAction(aSelection, aInfo, aCancel, aHandled);
  }

  // Nothing to do if there's no selection to act on
  if (!aSelection) {
    return NS_OK;
  }
  NS_ENSURE_TRUE(aSelection->RangeCount(), NS_OK);

  RefPtr<nsRange> range = aSelection->GetRangeAt(0);
  nsCOMPtr<nsINode> selStartNode = range->GetStartContainer();

  NS_ENSURE_STATE(mHTMLEditor);
  if (!mHTMLEditor->IsModifiableNode(selStartNode)) {
    *aCancel = true;
    return NS_OK;
  }

  nsCOMPtr<nsINode> selEndNode = range->GetEndContainer();

  if (selStartNode != selEndNode) {
    NS_ENSURE_STATE(mHTMLEditor);
    if (!mHTMLEditor->IsModifiableNode(selEndNode)) {
      *aCancel = true;
      return NS_OK;
    }

    NS_ENSURE_STATE(mHTMLEditor);
    if (!mHTMLEditor->IsModifiableNode(range->GetCommonAncestor())) {
      *aCancel = true;
      return NS_OK;
    }
  }

  switch (info->action) {
    case EditAction::insertText:
    case EditAction::insertIMEText:
      UndefineCaretBidiLevel(aSelection);
      return WillInsertText(info->action, aSelection, aCancel, aHandled,
                            info->inString, info->outString, info->maxLength);
    case EditAction::loadHTML:
      return WillLoadHTML(aSelection, aCancel);
    case EditAction::insertBreak:
      UndefineCaretBidiLevel(aSelection);
      return WillInsertBreak(*aSelection, aCancel, aHandled);
    case EditAction::deleteSelection:
      return WillDeleteSelection(aSelection, info->collapsedAction,
                                 info->stripWrappers, aCancel, aHandled);
    case EditAction::makeList:
      return WillMakeList(aSelection, info->blockType, info->entireList,
                          info->bulletType, aCancel, aHandled);
    case EditAction::indent:
      return WillIndent(aSelection, aCancel, aHandled);
    case EditAction::outdent:
      return WillOutdent(*aSelection, aCancel, aHandled);
    case EditAction::setAbsolutePosition:
      return WillAbsolutePosition(*aSelection, aCancel, aHandled);
    case EditAction::removeAbsolutePosition:
      return WillRemoveAbsolutePosition(aSelection, aCancel, aHandled);
    case EditAction::align:
      return WillAlign(*aSelection, *info->alignType, aCancel, aHandled);
    case EditAction::makeBasicBlock:
      return WillMakeBasicBlock(*aSelection, *info->blockType, aCancel,
                                aHandled);
    case EditAction::removeList:
      return WillRemoveList(aSelection, info->bOrdered, aCancel, aHandled);
    case EditAction::makeDefListItem:
      return WillMakeDefListItem(aSelection, info->blockType, info->entireList,
                                 aCancel, aHandled);
    case EditAction::insertElement:
      WillInsert(*aSelection, aCancel);
      return NS_OK;
    case EditAction::decreaseZIndex:
      return WillRelativeChangeZIndex(aSelection, -1, aCancel, aHandled);
    case EditAction::increaseZIndex:
      return WillRelativeChangeZIndex(aSelection, 1, aCancel, aHandled);
    default:
      return TextEditRules::WillDoAction(aSelection, aInfo,
                                         aCancel, aHandled);
  }
}

NS_IMETHODIMP
HTMLEditRules::DidDoAction(Selection* aSelection,
                           RulesInfo* aInfo,
                           nsresult aResult)
{
  TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo);
  switch (info->action) {
    case EditAction::insertBreak:
      return DidInsertBreak(aSelection, aResult);
    case EditAction::deleteSelection:
      return DidDeleteSelection(aSelection, info->collapsedAction, aResult);
    case EditAction::makeBasicBlock:
    case EditAction::indent:
    case EditAction::outdent:
    case EditAction::align:
      return DidMakeBasicBlock(aSelection, aInfo, aResult);
    case EditAction::setAbsolutePosition: {
      nsresult rv = DidMakeBasicBlock(aSelection, aInfo, aResult);
      NS_ENSURE_SUCCESS(rv, rv);
      return DidAbsolutePosition();
    }
    default:
      // pass through to TextEditRules
      return TextEditRules::DidDoAction(aSelection, aInfo, aResult);
  }
}

NS_IMETHODIMP_(bool)
HTMLEditRules::DocumentIsEmpty()
{
  return !!mBogusNode;
}

nsresult
HTMLEditRules::GetListState(bool* aMixed,
                            bool* aOL,
                            bool* aUL,
                            bool* aDL)
{
  NS_ENSURE_TRUE(aMixed && aOL && aUL && aDL, NS_ERROR_NULL_POINTER);
  *aMixed = false;
  *aOL = false;
  *aUL = false;
  *aDL = false;
  bool bNonList = false;

  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
  nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no,
                                   TouchContent::no);
  NS_ENSURE_SUCCESS(rv, rv);

  // Examine list type for nodes in selection.
  for (const auto& curNode : arrayOfNodes) {
    if (!curNode->IsElement()) {
      bNonList = true;
    } else if (curNode->IsHTMLElement(nsGkAtoms::ul)) {
      *aUL = true;
    } else if (curNode->IsHTMLElement(nsGkAtoms::ol)) {
      *aOL = true;
    } else if (curNode->IsHTMLElement(nsGkAtoms::li)) {
      if (dom::Element* parent = curNode->GetParentElement()) {
        if (parent->IsHTMLElement(nsGkAtoms::ul)) {
          *aUL = true;
        } else if (parent->IsHTMLElement(nsGkAtoms::ol)) {
          *aOL = true;
        }
      }
    } else if (curNode->IsAnyOfHTMLElements(nsGkAtoms::dl,
                                            nsGkAtoms::dt,
                                            nsGkAtoms::dd)) {
      *aDL = true;
    } else {
      bNonList = true;
    }
  }

  // hokey arithmetic with booleans
  if ((*aUL + *aOL + *aDL + bNonList) > 1) {
    *aMixed = true;
  }

  return NS_OK;
}

nsresult
HTMLEditRules::GetListItemState(bool* aMixed,
                                bool* aLI,
                                bool* aDT,
                                bool* aDD)
{
  NS_ENSURE_TRUE(aMixed && aLI && aDT && aDD, NS_ERROR_NULL_POINTER);
  *aMixed = false;
  *aLI = false;
  *aDT = false;
  *aDD = false;
  bool bNonList = false;

  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
  nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no,
                                   TouchContent::no);
  NS_ENSURE_SUCCESS(rv, rv);

  // examine list type for nodes in selection
  for (const auto& node : arrayOfNodes) {
    if (!node->IsElement()) {
      bNonList = true;
    } else if (node->IsAnyOfHTMLElements(nsGkAtoms::ul,
                                         nsGkAtoms::ol,
                                         nsGkAtoms::li)) {
      *aLI = true;
    } else if (node->IsHTMLElement(nsGkAtoms::dt)) {
      *aDT = true;
    } else if (node->IsHTMLElement(nsGkAtoms::dd)) {
      *aDD = true;
    } else if (node->IsHTMLElement(nsGkAtoms::dl)) {
      // need to look inside dl and see which types of items it has
      bool bDT, bDD;
      GetDefinitionListItemTypes(node->AsElement(), &bDT, &bDD);
      *aDT |= bDT;
      *aDD |= bDD;
    } else {
      bNonList = true;
    }
  }

  // hokey arithmetic with booleans
  if (*aDT + *aDD + bNonList > 1) {
    *aMixed = true;
  }

  return NS_OK;
}

nsresult
HTMLEditRules::GetAlignment(bool* aMixed,
                            nsIHTMLEditor::EAlignment* aAlign)
{
  MOZ_ASSERT(aMixed && aAlign);

  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  // For now, just return first alignment.  We'll lie about if it's mixed.
  // This is for efficiency given that our current ui doesn't care if it's
  // mixed.
  // cmanske: NOT TRUE! We would like to pay attention to mixed state in Format
  // | Align submenu!

  // This routine assumes that alignment is done ONLY via divs

  // Default alignment is left
  *aMixed = false;
  *aAlign = nsIHTMLEditor::eLeft;

  // Get selection
  NS_ENSURE_STATE(htmlEditor->GetSelection());
  OwningNonNull<Selection> selection = *htmlEditor->GetSelection();

  // Get selection location
  NS_ENSURE_TRUE(htmlEditor->GetRoot(), NS_ERROR_FAILURE);
  OwningNonNull<Element> root = *htmlEditor->GetRoot();

  int32_t rootOffset = root->GetParentNode() ?
                       root->GetParentNode()->IndexOf(root) : -1;

  NS_ENSURE_STATE(selection->GetRangeAt(0) &&
                  selection->GetRangeAt(0)->GetStartContainer());
  OwningNonNull<nsINode> parent =
    *selection->GetRangeAt(0)->GetStartContainer();
  int32_t offset = selection->GetRangeAt(0)->StartOffset();

  // Is the selection collapsed?
  nsCOMPtr<nsINode> nodeToExamine;
  if (selection->Collapsed() || parent->GetAsText()) {
    // If selection is collapsed, we want to look at 'parent' and its ancestors
    // for divs with alignment on them.  If we are in a text node, then that is
    // the node of interest.
    nodeToExamine = parent;
  } else if (parent->IsHTMLElement(nsGkAtoms::html) && offset == rootOffset) {
    // If we have selected the body, let's look at the first editable node
    nodeToExamine = htmlEditor->GetNextNode(parent, offset, true);
  } else {
    nsTArray<RefPtr<nsRange>> arrayOfRanges;
    GetPromotedRanges(selection, arrayOfRanges, EditAction::align);

    // Use these ranges to construct a list of nodes to act on.
    nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
    nsresult rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes,
                                       EditAction::align, TouchContent::no);
    NS_ENSURE_SUCCESS(rv, rv);
    nodeToExamine = arrayOfNodes.SafeElementAt(0);
  }

  NS_ENSURE_TRUE(nodeToExamine, NS_ERROR_NULL_POINTER);

  nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(*nodeToExamine);

  NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE);

  if (htmlEditor->IsCSSEnabled() &&
      htmlEditor->mCSSEditUtils->IsCSSEditableProperty(blockParent, nullptr,
                                                       nsGkAtoms::align)) {
    // We are in CSS mode and we know how to align this element with CSS
    nsAutoString value;
    // Let's get the value(s) of text-align or margin-left/margin-right
    htmlEditor->mCSSEditUtils->GetCSSEquivalentToHTMLInlineStyleSet(
        blockParent, nullptr, nsGkAtoms::align, value, CSSEditUtils::eComputed);
    if (value.EqualsLiteral("center") ||
        value.EqualsLiteral("-moz-center") ||
        value.EqualsLiteral("auto auto")) {
      *aAlign = nsIHTMLEditor::eCenter;
      return NS_OK;
    }
    if (value.EqualsLiteral("right") ||
        value.EqualsLiteral("-moz-right") ||
        value.EqualsLiteral("auto 0px")) {
      *aAlign = nsIHTMLEditor::eRight;
      return NS_OK;
    }
    if (value.EqualsLiteral("justify")) {
      *aAlign = nsIHTMLEditor::eJustify;
      return NS_OK;
    }
    *aAlign = nsIHTMLEditor::eLeft;
    return NS_OK;
  }

  // Check up the ladder for divs with alignment
  bool isFirstNodeToExamine = true;
  for (; nodeToExamine; nodeToExamine = nodeToExamine->GetParentNode()) {
    if (!isFirstNodeToExamine &&
        nodeToExamine->IsHTMLElement(nsGkAtoms::table)) {
      // The node to examine is a table and this is not the first node we
      // examine; let's break here to materialize the 'inline-block' behaviour
      // of html tables regarding to text alignment
      return NS_OK;
    }
    if (HTMLEditUtils::SupportsAlignAttr(*nodeToExamine)) {
      // Check for alignment
      nsAutoString typeAttrVal;
      nodeToExamine->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::align,
                                          typeAttrVal);
      ToLowerCase(typeAttrVal);
      if (!typeAttrVal.IsEmpty()) {
        if (typeAttrVal.EqualsLiteral("center")) {
          *aAlign = nsIHTMLEditor::eCenter;
        } else if (typeAttrVal.EqualsLiteral("right")) {
          *aAlign = nsIHTMLEditor::eRight;
        } else if (typeAttrVal.EqualsLiteral("justify")) {
          *aAlign = nsIHTMLEditor::eJustify;
        } else {
          *aAlign = nsIHTMLEditor::eLeft;
        }
        return NS_OK;
      }
    }
    isFirstNodeToExamine = false;
  }
  return NS_OK;
}

static nsIAtom& MarginPropertyAtomForIndent(CSSEditUtils& aHTMLCSSUtils,
                                            nsINode& aNode)
{
  nsAutoString direction;
  aHTMLCSSUtils.GetComputedProperty(aNode, *nsGkAtoms::direction, direction);
  return direction.EqualsLiteral("rtl") ?
    *nsGkAtoms::marginRight : *nsGkAtoms::marginLeft;
}

nsresult
HTMLEditRules::GetIndentState(bool* aCanIndent,
                              bool* aCanOutdent)
{
  // XXX Looks like that this is implementation of
  //     nsIHTMLEditor::getIndentState() however nobody calls this method
  //     even with the interface method.
  NS_ENSURE_TRUE(aCanIndent && aCanOutdent, NS_ERROR_FAILURE);
  *aCanIndent = true;
  *aCanOutdent = false;

  // get selection
  NS_ENSURE_STATE(mHTMLEditor && mHTMLEditor->GetSelection());
  OwningNonNull<Selection> selection = *mHTMLEditor->GetSelection();

  // contruct a list of nodes to act on.
  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
  nsresult rv = GetNodesFromSelection(*selection, EditAction::indent,
                                      arrayOfNodes, TouchContent::no);
  NS_ENSURE_SUCCESS(rv, rv);

  // examine nodes in selection for blockquotes or list elements;
  // these we can outdent.  Note that we return true for canOutdent
  // if *any* of the selection is outdentable, rather than all of it.
  NS_ENSURE_STATE(mHTMLEditor);
  bool useCSS = mHTMLEditor->IsCSSEnabled();
  for (auto& curNode : Reversed(arrayOfNodes)) {
    if (HTMLEditUtils::IsNodeThatCanOutdent(GetAsDOMNode(curNode))) {
      *aCanOutdent = true;
      break;
    } else if (useCSS) {
      // we are in CSS mode, indentation is done using the margin-left (or margin-right) property
      NS_ENSURE_STATE(mHTMLEditor);
      nsIAtom& marginProperty =
        MarginPropertyAtomForIndent(*mHTMLEditor->mCSSEditUtils, curNode);
      nsAutoString value;
      // retrieve its specified value
      NS_ENSURE_STATE(mHTMLEditor);
      mHTMLEditor->mCSSEditUtils->GetSpecifiedProperty(*curNode,
                                                       marginProperty, value);
      float f;
      nsCOMPtr<nsIAtom> unit;
      // get its number part and its unit
      NS_ENSURE_STATE(mHTMLEditor);
      mHTMLEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit));
      // if the number part is strictly positive, outdent is possible
      if (0 < f) {
        *aCanOutdent = true;
        break;
      }
    }
  }

  if (!*aCanOutdent) {
    // if we haven't found something to outdent yet, also check the parents
    // of selection endpoints.  We might have a blockquote or list item
    // in the parent hierarchy.

    // gather up info we need for test
    NS_ENSURE_STATE(mHTMLEditor);
    nsCOMPtr<nsIDOMNode> parent, tmp, root = do_QueryInterface(mHTMLEditor->GetRoot());
    NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER);
    int32_t selOffset;
    NS_ENSURE_STATE(mHTMLEditor);
    RefPtr<Selection> selection = mHTMLEditor->GetSelection();
    NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);

    // test start parent hierarchy
    rv = EditorBase::GetStartNodeAndOffset(selection, getter_AddRefs(parent),
                                           &selOffset);
    NS_ENSURE_SUCCESS(rv, rv);
    while (parent && parent != root) {
      if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) {
        *aCanOutdent = true;
        break;
      }
      tmp = parent;
      tmp->GetParentNode(getter_AddRefs(parent));
    }

    // test end parent hierarchy
    rv = EditorBase::GetEndNodeAndOffset(selection, getter_AddRefs(parent),
                                         &selOffset);
    NS_ENSURE_SUCCESS(rv, rv);
    while (parent && parent != root) {
      if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) {
        *aCanOutdent = true;
        break;
      }
      tmp = parent;
      tmp->GetParentNode(getter_AddRefs(parent));
    }
  }
  return NS_OK;
}


nsresult
HTMLEditRules::GetParagraphState(bool* aMixed,
                                 nsAString& outFormat)
{
  // This routine is *heavily* tied to our ui choices in the paragraph
  // style popup.  I can't see a way around that.
  NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER);
  *aMixed = true;
  outFormat.Truncate(0);

  bool bMixed = false;
  // using "x" as an uninitialized value, since "" is meaningful
  nsAutoString formatStr(NS_LITERAL_STRING("x"));

  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
  nsresult rv = GetParagraphFormatNodes(arrayOfNodes, TouchContent::no);
  NS_ENSURE_SUCCESS(rv, rv);

  // post process list.  We need to replace any block nodes that are not format
  // nodes with their content.  This is so we only have to look "up" the hierarchy
  // to find format nodes, instead of both up and down.
  for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
    auto& curNode = arrayOfNodes[i];
    nsAutoString format;
    // if it is a known format node we have it easy
    if (IsBlockNode(curNode) && !HTMLEditUtils::IsFormatNode(curNode)) {
      // arrayOfNodes.RemoveObject(curNode);
      rv = AppendInnerFormatNodes(arrayOfNodes, curNode);
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  // we might have an empty node list.  if so, find selection parent
  // and put that on the list
  if (arrayOfNodes.IsEmpty()) {
    nsCOMPtr<nsINode> selNode;
    int32_t selOffset;
    NS_ENSURE_STATE(mHTMLEditor);
    RefPtr<Selection> selection = mHTMLEditor->GetSelection();
    NS_ENSURE_STATE(selection);
    rv = EditorBase::GetStartNodeAndOffset(selection, getter_AddRefs(selNode),
                                           &selOffset);
    NS_ENSURE_SUCCESS(rv, rv);
    NS_ENSURE_TRUE(selNode, NS_ERROR_NULL_POINTER);
    arrayOfNodes.AppendElement(*selNode);
  }

  // remember root node
  NS_ENSURE_STATE(mHTMLEditor);
  nsCOMPtr<nsIDOMElement> rootElem = do_QueryInterface(mHTMLEditor->GetRoot());
  NS_ENSURE_TRUE(rootElem, NS_ERROR_NULL_POINTER);

  // loop through the nodes in selection and examine their paragraph format
  for (auto& curNode : Reversed(arrayOfNodes)) {
    nsAutoString format;
    // if it is a known format node we have it easy
    if (HTMLEditUtils::IsFormatNode(curNode)) {
      GetFormatString(GetAsDOMNode(curNode), format);
    } else if (IsBlockNode(curNode)) {
      // this is a div or some other non-format block.
      // we should ignore it.  Its children were appended to this list
      // by AppendInnerFormatNodes() call above.  We will get needed
      // info when we examine them instead.
      continue;
    } else {
      nsCOMPtr<nsIDOMNode> node, tmp = GetAsDOMNode(curNode);
      tmp->GetParentNode(getter_AddRefs(node));
      while (node) {
        if (node == rootElem) {
          format.Truncate(0);
          break;
        } else if (HTMLEditUtils::IsFormatNode(node)) {
          GetFormatString(node, format);
          break;
        }
        // else keep looking up
        tmp = node;
        tmp->GetParentNode(getter_AddRefs(node));
      }
    }

    // if this is the first node, we've found, remember it as the format
    if (formatStr.EqualsLiteral("x")) {
      formatStr = format;
    }
    // else make sure it matches previously found format
    else if (format != formatStr) {
      bMixed = true;
      break;
    }
  }

  *aMixed = bMixed;
  outFormat = formatStr;
  return NS_OK;
}

nsresult
HTMLEditRules::AppendInnerFormatNodes(nsTArray<OwningNonNull<nsINode>>& aArray,
                                      nsINode* aNode)
{
  MOZ_ASSERT(aNode);

  // we only need to place any one inline inside this node onto
  // the list.  They are all the same for purposes of determining
  // paragraph style.  We use foundInline to track this as we are
  // going through the children in the loop below.
  bool foundInline = false;
  for (nsIContent* child = aNode->GetFirstChild();
       child;
       child = child->GetNextSibling()) {
    bool isBlock = IsBlockNode(*child);
    bool isFormat = HTMLEditUtils::IsFormatNode(child);
    if (isBlock && !isFormat) {
      // if it's a div, etc., recurse
      AppendInnerFormatNodes(aArray, child);
    } else if (isFormat) {
      aArray.AppendElement(*child);
    } else if (!foundInline) {
      // if this is the first inline we've found, use it
      foundInline = true;
      aArray.AppendElement(*child);
    }
  }
  return NS_OK;
}

nsresult
HTMLEditRules::GetFormatString(nsIDOMNode* aNode,
                               nsAString& outFormat)
{
  NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);

  if (HTMLEditUtils::IsFormatNode(aNode)) {
    nsCOMPtr<nsIAtom> atom = EditorBase::GetTag(aNode);
    atom->ToString(outFormat);
  } else {
    outFormat.Truncate();
  }
  return NS_OK;
}

void
HTMLEditRules::WillInsert(Selection& aSelection,
                          bool* aCancel)
{
  MOZ_ASSERT(aCancel);

  TextEditRules::WillInsert(aSelection, aCancel);

  NS_ENSURE_TRUE_VOID(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  // Adjust selection to prevent insertion after a moz-BR.  This next only
  // works for collapsed selections right now, because selection is a pain to
  // work with when not collapsed.  (no good way to extend start or end of
  // selection), so we ignore those types of selections.
  if (!aSelection.Collapsed()) {
    return;
  }

  // If we are after a mozBR in the same block, then move selection to be
  // before it
  NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) &&
                      aSelection.GetRangeAt(0)->GetStartContainer());
  OwningNonNull<nsINode> selNode =
    *aSelection.GetRangeAt(0)->GetStartContainer();
  int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset();

  // Get prior node
  nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorHTMLNode(selNode,
                                                                selOffset);
  if (priorNode && TextEditUtils::IsMozBR(priorNode)) {
    nsCOMPtr<Element> block1 = htmlEditor->GetBlock(selNode);
    nsCOMPtr<Element> block2 = htmlEditor->GetBlockNodeParent(priorNode);

    if (block1 && block1 == block2) {
      // If we are here then the selection is right after a mozBR that is in
      // the same block as the selection.  We need to move the selection start
      // to be before the mozBR.
      selNode = priorNode->GetParentNode();
      selOffset = selNode->IndexOf(priorNode);
      nsresult rv = aSelection.Collapse(selNode, selOffset);
      NS_ENSURE_SUCCESS_VOID(rv);
    }
  }

  if (mDidDeleteSelection &&
      (mTheAction == EditAction::insertText ||
       mTheAction == EditAction::insertIMEText ||
       mTheAction == EditAction::deleteSelection)) {
    nsresult rv = ReapplyCachedStyles();
    NS_ENSURE_SUCCESS_VOID(rv);
  }
  // For most actions we want to clear the cached styles, but there are
  // exceptions
  if (!IsStyleCachePreservingAction(mTheAction)) {
    ClearCachedStyles();
  }
}

nsresult
HTMLEditRules::WillInsertText(EditAction aAction,
                              Selection* aSelection,
                              bool* aCancel,
                              bool* aHandled,
                              const nsAString* inString,
                              nsAString* outString,
                              int32_t aMaxLength)
{
  if (!aSelection || !aCancel || !aHandled) {
    return NS_ERROR_NULL_POINTER;
  }

  // initialize out param
  *aCancel = false;
  *aHandled = true;
  // If the selection isn't collapsed, delete it.  Don't delete existing inline
  // tags, because we're hopefully going to insert text (bug 787432).
  if (!aSelection->Collapsed()) {
    NS_ENSURE_STATE(mHTMLEditor);
    nsresult rv =
      mHTMLEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eNoStrip);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  WillInsert(*aSelection, aCancel);
  // initialize out param
  // we want to ignore result of WillInsert()
  *aCancel = false;

  // we need to get the doc
  NS_ENSURE_STATE(mHTMLEditor);
  nsCOMPtr<nsIDocument> doc = mHTMLEditor->GetDocument();
  NS_ENSURE_STATE(doc);

  // for every property that is set, insert a new inline style node
  nsresult rv = CreateStyleForInsertText(*aSelection, *doc);
  NS_ENSURE_SUCCESS(rv, rv);

  // get the (collapsed) selection location
  NS_ENSURE_STATE(mHTMLEditor);
  NS_ENSURE_STATE(aSelection->GetRangeAt(0));
  nsCOMPtr<nsINode> selNode = aSelection->GetRangeAt(0)->GetStartContainer();
  int32_t selOffset = aSelection->GetRangeAt(0)->StartOffset();
  NS_ENSURE_STATE(selNode);

  // dont put text in places that can't have it
  NS_ENSURE_STATE(mHTMLEditor);
  if (!EditorBase::IsTextNode(selNode) &&
      (!mHTMLEditor || !mHTMLEditor->CanContainTag(*selNode,
                                                   *nsGkAtoms::textTagName))) {
    return NS_ERROR_FAILURE;
  }

  if (aAction == EditAction::insertIMEText) {
    // Right now the WSRunObject code bails on empty strings, but IME needs
    // the InsertTextImpl() call to still happen since empty strings are meaningful there.
    NS_ENSURE_STATE(mHTMLEditor);
    // If there is one or more IME selections, its minimum offset should be
    // the insertion point.
    int32_t IMESelectionOffset =
      mHTMLEditor->GetIMESelectionStartOffsetIn(selNode);
    if (IMESelectionOffset >= 0) {
      selOffset = IMESelectionOffset;
    }
    if (inString->IsEmpty()) {
      rv = mHTMLEditor->InsertTextImpl(*inString, address_of(selNode),
                                       &selOffset, doc);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    } else {
      WSRunObject wsObj(mHTMLEditor, selNode, selOffset);
      rv = wsObj.InsertText(*inString, address_of(selNode), &selOffset, doc);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    }
  }
  // aAction == kInsertText
  else {
    // find where we are
    nsCOMPtr<nsINode> curNode = selNode;
    int32_t curOffset = selOffset;

    // is our text going to be PREformatted?
    // We remember this so that we know how to handle tabs.
    bool isPRE;
    NS_ENSURE_STATE(mHTMLEditor);
    rv = mHTMLEditor->IsPreformatted(GetAsDOMNode(selNode), &isPRE);
    NS_ENSURE_SUCCESS(rv, rv);

    // turn off the edit listener: we know how to
    // build the "doc changed range" ourselves, and it's
    // must faster to do it once here than to track all
    // the changes one at a time.
    AutoLockListener lockit(&mListenerEnabled);

    // don't change my selection in subtransactions
    NS_ENSURE_STATE(mHTMLEditor);
    AutoTransactionsConserveSelection dontChangeMySelection(mHTMLEditor);
    nsAutoString tString(*inString);
    const char16_t *unicodeBuf = tString.get();
    int32_t pos = 0;
    NS_NAMED_LITERAL_STRING(newlineStr, LFSTR);

    {
      NS_ENSURE_STATE(mHTMLEditor);
      AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater,
                                address_of(selNode), &selOffset);

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

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

          nsDependentSubstring subStr(tString, oldPos, subStrLen);

          // is it a return?
          if (subStr.Equals(newlineStr)) {
            NS_ENSURE_STATE(mHTMLEditor);
            nsCOMPtr<Element> br =
              mHTMLEditor->CreateBRImpl(address_of(curNode), &curOffset,
                                        nsIEditor::eNone);
            NS_ENSURE_STATE(br);
            pos++;
          } else {
            NS_ENSURE_STATE(mHTMLEditor);
            rv = mHTMLEditor->InsertTextImpl(subStr, address_of(curNode),
                                             &curOffset, doc);
            NS_ENSURE_SUCCESS(rv, rv);
          }
        }
      } else {
        NS_NAMED_LITERAL_STRING(tabStr, "\t");
        NS_NAMED_LITERAL_STRING(spacesStr, "    ");
        char specialChars[] = {TAB, nsCRT::LF, 0};
        while (unicodeBuf && pos != -1 &&
               pos < static_cast<int32_t>(inString->Length())) {
          int32_t oldPos = pos;
          int32_t subStrLen;
          pos = tString.FindCharInSet(specialChars, oldPos);

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

          nsDependentSubstring subStr(tString, oldPos, subStrLen);
          NS_ENSURE_STATE(mHTMLEditor);
          WSRunObject wsObj(mHTMLEditor, curNode, curOffset);

          // is it a tab?
          if (subStr.Equals(tabStr)) {
            rv =
              wsObj.InsertText(spacesStr, address_of(curNode), &curOffset, doc);
            NS_ENSURE_SUCCESS(rv, rv);
            pos++;
          }
          // is it a return?
          else if (subStr.Equals(newlineStr)) {
            nsCOMPtr<Element> br = wsObj.InsertBreak(address_of(curNode),
                                                     &curOffset,
                                                     nsIEditor::eNone);
            NS_ENSURE_TRUE(br, NS_ERROR_FAILURE);
            pos++;
          } else {
            rv = wsObj.InsertText(subStr, address_of(curNode), &curOffset, doc);
            NS_ENSURE_SUCCESS(rv, rv);
          }
        }
      }
    }
    aSelection->SetInterlinePosition(false);
    if (curNode) aSelection->Collapse(curNode, curOffset);
    // manually update the doc changed range so that AfterEdit will clean up
    // the correct portion of the document.
    if (!mDocChangeRange) {
      mDocChangeRange = new nsRange(selNode);
    }

    if (curNode) {
      rv = mDocChangeRange->SetStartAndEnd(selNode, selOffset,
                                           curNode, curOffset);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    } else {
      rv = mDocChangeRange->CollapseTo(selNode, selOffset);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    }
  }
  return NS_OK;
}

nsresult
HTMLEditRules::WillLoadHTML(Selection* aSelection,
                            bool* aCancel)
{
  NS_ENSURE_TRUE(aSelection && aCancel, NS_ERROR_NULL_POINTER);

  *aCancel = false;

  // Delete mBogusNode if it exists. If we really need one,
  // it will be added during post-processing in AfterEditInner().

  if (mBogusNode) {
    if (NS_WARN_IF(!mHTMLEditor)) {
      return NS_ERROR_UNEXPECTED;
    }
    mHTMLEditor->DeleteNode(mBogusNode);
    mBogusNode = nullptr;
  }

  return NS_OK;
}

nsresult
HTMLEditRules::WillInsertBreak(Selection& aSelection,
                               bool* aCancel,
                               bool* aHandled)
{
  MOZ_ASSERT(aCancel && aHandled);
  *aCancel = false;
  *aHandled = false;

  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  // If the selection isn't collapsed, delete it.
  if (!aSelection.Collapsed()) {
    nsresult rv =
      htmlEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  WillInsert(aSelection, aCancel);

  // Initialize out param.  We want to ignore result of WillInsert().
  *aCancel = false;

  // Split any mailcites in the way.  Should we abort this if we encounter
  // table cell boundaries?
  if (IsMailEditor()) {
    nsresult rv = SplitMailCites(&aSelection, aHandled);
    NS_ENSURE_SUCCESS(rv, rv);
    if (*aHandled) {
      return NS_OK;
    }
  }

  // Smart splitting rules
  NS_ENSURE_TRUE(aSelection.GetRangeAt(0) &&
                 aSelection.GetRangeAt(0)->GetStartContainer(),
                 NS_ERROR_FAILURE);
  OwningNonNull<nsINode> node = *aSelection.GetRangeAt(0)->GetStartContainer();
  int32_t offset = aSelection.GetRangeAt(0)->StartOffset();

  // Do nothing if the node is read-only
  if (!htmlEditor->IsModifiableNode(node)) {
    *aCancel = true;
    return NS_OK;
  }

  // Identify the block
  nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(node);
  NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE);

  // If the active editing host is an inline element, or if the active editing
  // host is the block parent itself and we're configured to use <br> as a
  // paragraph separator, just append a <br>.
  nsCOMPtr<Element> host = htmlEditor->GetActiveEditingHost();
  if (NS_WARN_IF(!host)) {
    return NS_ERROR_FAILURE;
  }
  ParagraphSeparator separator = mHTMLEditor->GetDefaultParagraphSeparator();
  if (!IsBlockNode(*host) ||
      // The nodes that can contain p and div are the same.  If the editing
      // host is a <p> or similar, we have to just insert a newline.
      (!mHTMLEditor->CanContainTag(*host, *nsGkAtoms::p) &&
       // These can't contain <p> as a child, but can as a descendant, so we
       // don't have to fall back to inserting a newline.
       !host->IsAnyOfHTMLElements(nsGkAtoms::ol, nsGkAtoms::ul, nsGkAtoms::dl,
                                  nsGkAtoms::table, nsGkAtoms::thead,
                                  nsGkAtoms::tbody, nsGkAtoms::tfoot,
                                  nsGkAtoms::tr)) ||
      (host == blockParent && separator == ParagraphSeparator::br)) {
    nsresult rv = StandardBreakImpl(node, offset, aSelection);
    NS_ENSURE_SUCCESS(rv, rv);
    *aHandled = true;
    return NS_OK;
  }
  if (host == blockParent && separator != ParagraphSeparator::br) {
    // Insert a new block first
    MOZ_ASSERT(separator == ParagraphSeparator::div ||
               separator == ParagraphSeparator::p);
    nsresult rv = MakeBasicBlock(aSelection,
                                 ParagraphSeparatorElement(separator));
    // We warn on failure, but don't handle it, because it might be harmless.
    // Instead we just check that a new block was actually created.
    Unused << NS_WARN_IF(NS_FAILED(rv));

    // Reinitialize node/offset in case they're not inside the new block
    if (NS_WARN_IF(!aSelection.GetRangeAt(0) ||
                   !aSelection.GetRangeAt(0)->GetStartContainer())) {
      return NS_ERROR_FAILURE;
    }
    node = *aSelection.GetRangeAt(0)->GetStartContainer();
    offset = aSelection.GetRangeAt(0)->StartOffset();

    blockParent = mHTMLEditor->GetBlock(node);
    if (NS_WARN_IF(!blockParent)) {
      return NS_ERROR_UNEXPECTED;
    }
    if (NS_WARN_IF(blockParent == host)) {
      // Didn't create a new block for some reason, fall back to <br>
      rv = StandardBreakImpl(node, offset, aSelection);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      *aHandled = true;
      return NS_OK;
    }
  }

  // If block is empty, populate with br.  (For example, imagine a div that
  // contains the word "text".  The user selects "text" and types return.
  // "Text" is deleted leaving an empty block.  We want to put in one br to
  // make block have a line.  Then code further below will put in a second br.)
  bool isEmpty;
  IsEmptyBlock(*blockParent, &isEmpty);
  if (isEmpty) {
    nsCOMPtr<Element> br = htmlEditor->CreateBR(blockParent,
                                                blockParent->Length());
    NS_ENSURE_STATE(br);
  }

  nsCOMPtr<Element> listItem = IsInListItem(blockParent);
  if (listItem && listItem != host) {
    ReturnInListItem(aSelection, *listItem, node, offset);
    *aHandled = true;
    return NS_OK;
  } else if (HTMLEditUtils::IsHeader(*blockParent)) {
    // Headers: close (or split) header
    ReturnInHeader(aSelection, *blockParent, node, offset);
    *aHandled = true;
    return NS_OK;
  } else if (blockParent->IsAnyOfHTMLElements(nsGkAtoms::p, nsGkAtoms::div)) {
    // Paragraphs: special rules to look for <br>s
    nsresult rv =
      ReturnInParagraph(&aSelection, GetAsDOMNode(blockParent),
                        GetAsDOMNode(node), offset, aCancel, aHandled);
    NS_ENSURE_SUCCESS(rv, rv);
    // Fall through, we may not have handled it in ReturnInParagraph()
  }

  // If not already handled then do the standard thing
  if (!(*aHandled)) {
    *aHandled = true;
    return StandardBreakImpl(node, offset, aSelection);
  }
  return NS_OK;
}

nsresult
HTMLEditRules::StandardBreakImpl(nsINode& aNode,
                                 int32_t aOffset,
                                 Selection& aSelection)
{
  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  nsCOMPtr<Element> brNode;
  bool bAfterBlock = false;
  bool bBeforeBlock = false;
  nsCOMPtr<nsINode> node = &aNode;

  if (IsPlaintextEditor()) {
    brNode = htmlEditor->CreateBR(node, aOffset);
    NS_ENSURE_STATE(brNode);
  } else {
    WSRunObject wsObj(htmlEditor, node, aOffset);
    int32_t visOffset = 0;
    WSType wsType;
    nsCOMPtr<nsINode> visNode;
    wsObj.PriorVisibleNode(node, aOffset, address_of(visNode),
                           &visOffset, &wsType);
    if (wsType & WSType::block) {
      bAfterBlock = true;
    }
    wsObj.NextVisibleNode(node, aOffset, address_of(visNode),
                          &visOffset, &wsType);
    if (wsType & WSType::block) {
      bBeforeBlock = true;
    }
    nsCOMPtr<nsIDOMNode> linkDOMNode;
    if (htmlEditor->IsInLink(GetAsDOMNode(node), address_of(linkDOMNode))) {
      // Split the link
      nsCOMPtr<Element> linkNode = do_QueryInterface(linkDOMNode);
      NS_ENSURE_STATE(linkNode || !linkDOMNode);
      nsCOMPtr<nsINode> linkParent = linkNode->GetParentNode();
      aOffset = htmlEditor->SplitNodeDeep(*linkNode, *node->AsContent(),
                                          aOffset,
                                          HTMLEditor::EmptyContainers::no);
      NS_ENSURE_STATE(aOffset != -1);
      node = linkParent;
    }
    brNode = wsObj.InsertBreak(address_of(node), &aOffset, nsIEditor::eNone);
    NS_ENSURE_TRUE(brNode, NS_ERROR_FAILURE);
  }
  node = brNode->GetParentNode();
  NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
  int32_t offset = node->IndexOf(brNode);
  if (bAfterBlock && bBeforeBlock) {
    // We just placed a br between block boundaries.  This is the one case
    // where we want the selection to be before the br we just placed, as the
    // br will be on a new line, rather than at end of prior line.
    aSelection.SetInterlinePosition(true);
    nsresult rv = aSelection.Collapse(node, offset);
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    WSRunObject wsObj(htmlEditor, node, offset + 1);
    nsCOMPtr<nsINode> secondBR;
    int32_t visOffset = 0;
    WSType wsType;
    wsObj.NextVisibleNode(node, offset + 1, address_of(secondBR),
                          &visOffset, &wsType);
    if (wsType == WSType::br) {
      // The next thing after the break we inserted is another break.  Move the
      // second break to be the first break's sibling.  This will prevent them
      // from being in different inline nodes, which would break
      // SetInterlinePosition().  It will also assure that if the user clicks
      // away and then clicks back on their new blank line, they will still get
      // the style from the line above.
      nsCOMPtr<nsINode> brParent = secondBR->GetParentNode();
      int32_t brOffset = brParent ? brParent->IndexOf(secondBR) : -1;
      if (brParent != node || brOffset != offset + 1) {
        nsresult rv =
          htmlEditor->MoveNode(secondBR->AsContent(), node, offset + 1);
        NS_ENSURE_SUCCESS(rv, rv);
      }
    }
    // SetInterlinePosition(true) means we want the caret to stick to the
    // content on the "right".  We want the caret to stick to whatever is past
    // the break.  This is because the break is on the same line we were on,
    // but the next content will be on the following line.

    // An exception to this is if the break has a next sibling that is a block
    // node.  Then we stick to the left to avoid an uber caret.
    nsCOMPtr<nsIContent> siblingNode = brNode->GetNextSibling();
    if (siblingNode && IsBlockNode(*siblingNode)) {
      aSelection.SetInterlinePosition(false);
    } else {
      aSelection.SetInterlinePosition(true);
    }
    nsresult rv = aSelection.Collapse(node, offset + 1);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  return NS_OK;
}

nsresult
HTMLEditRules::DidInsertBreak(Selection* aSelection,
                              nsresult aResult)
{
  return NS_OK;
}

nsresult
HTMLEditRules::SplitMailCites(Selection* aSelection,
                              bool* aHandled)
{
  NS_ENSURE_TRUE(aSelection && aHandled, NS_ERROR_NULL_POINTER);
  nsCOMPtr<nsIContent> leftCite, rightCite;
  nsCOMPtr<nsINode> selNode;
  nsCOMPtr<Element> citeNode;
  int32_t selOffset;
  nsresult rv =
    EditorBase::GetStartNodeAndOffset(aSelection, getter_AddRefs(selNode),
                                      &selOffset);
  NS_ENSURE_SUCCESS(rv, rv);
  citeNode = GetTopEnclosingMailCite(*selNode);
  if (citeNode) {
    // If our selection is just before a break, nudge it to be
    // just after it.  This does two things for us.  It saves us the trouble of having to add
    // a break here ourselves to preserve the "blockness" of the inline span mailquote
    // (in the inline case), and :
    // it means the break won't end up making an empty line that happens to be inside a
    // mailquote (in either inline or block case).
    // The latter can confuse a user if they click there and start typing,
    // because being in the mailquote may affect wrapping behavior, or font color, etc.
    NS_ENSURE_STATE(mHTMLEditor);
    WSRunObject wsObj(mHTMLEditor, selNode, selOffset);
    nsCOMPtr<nsINode> visNode;
    int32_t visOffset=0;
    WSType wsType;
    wsObj.NextVisibleNode(selNode, selOffset, address_of(visNode),
                          &visOffset, &wsType);
    if (wsType == WSType::br) {
      // ok, we are just before a break.  is it inside the mailquote?
      if (visNode != citeNode && citeNode->Contains(visNode)) {
        // it is.  so lets reset our selection to be just after it.
        NS_ENSURE_STATE(mHTMLEditor);
        selNode = mHTMLEditor->GetNodeLocation(visNode, &selOffset);
        ++selOffset;
      }
    }

    NS_ENSURE_STATE(mHTMLEditor);
    NS_ENSURE_STATE(selNode->IsContent());
    int32_t newOffset = mHTMLEditor->SplitNodeDeep(*citeNode,
        *selNode->AsContent(), selOffset, HTMLEditor::EmptyContainers::no,
        getter_AddRefs(leftCite), getter_AddRefs(rightCite));
    NS_ENSURE_STATE(newOffset != -1);

    // Add an invisible <br> to the end of the left part if it was a <span> of
    // style="display: block". This is important, since when serialising the
    // cite to plain text, the span which caused the visual break is discarded.
    // So the added <br> will guarantee that the serialiser will insert a
    // break where the user saw one.
    if (leftCite &&
        leftCite->IsHTMLElement(nsGkAtoms::span) &&
        leftCite->GetPrimaryFrame()->IsFrameOfType(nsIFrame::eBlockFrame)) {
       nsCOMPtr<nsINode> lastChild = leftCite->GetLastChild();
       if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) {
         // We ignore the result here.
         nsCOMPtr<Element> invisBR =
           mHTMLEditor->CreateBR(leftCite, leftCite->Length());
      }
    }

    selNode = citeNode->GetParentNode();
    NS_ENSURE_STATE(mHTMLEditor);
    nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(selNode, newOffset);
    NS_ENSURE_STATE(brNode);

    // want selection before the break, and on same line
    aSelection->SetInterlinePosition(true);
    rv = aSelection->Collapse(selNode, newOffset);
    NS_ENSURE_SUCCESS(rv, rv);

    // if citeNode wasn't a block, we might also want another break before it.
    // We need to examine the content both before the br we just added and also
    // just after it.  If we don't have another br or block boundary adjacent,
    // then we will need a 2nd br added to achieve blank line that user expects.
    if (IsInlineNode(*citeNode)) {
      NS_ENSURE_STATE(mHTMLEditor);
      WSRunObject wsObj(mHTMLEditor, selNode, newOffset);
      nsCOMPtr<nsINode> visNode;
      int32_t visOffset=0;
      WSType wsType;
      wsObj.PriorVisibleNode(selNode, newOffset, address_of(visNode),
                             &visOffset, &wsType);
      if (wsType == WSType::normalWS || wsType == WSType::text ||
          wsType == WSType::special) {
        NS_ENSURE_STATE(mHTMLEditor);
        WSRunObject wsObjAfterBR(mHTMLEditor, selNode, newOffset+1);
        wsObjAfterBR.NextVisibleNode(selNode, newOffset + 1,
                                     address_of(visNode), &visOffset, &wsType);
        if (wsType == WSType::normalWS || wsType == WSType::text ||
            wsType == WSType::special ||
            // In case we're at the very end.
            wsType == WSType::thisBlock) {
          NS_ENSURE_STATE(mHTMLEditor);
          brNode = mHTMLEditor->CreateBR(selNode, newOffset);
          NS_ENSURE_STATE(brNode);
        }
      }
    }

    // delete any empty cites
    bool bEmptyCite = false;
    if (leftCite) {
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->IsEmptyNode(leftCite, &bEmptyCite, true, false);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      if (bEmptyCite) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->DeleteNode(leftCite);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }
      }
    }

    if (rightCite) {
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->IsEmptyNode(rightCite, &bEmptyCite, true, false);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      if (bEmptyCite) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->DeleteNode(rightCite);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }
      }
    }
    *aHandled = true;
  }
  return NS_OK;
}


nsresult
HTMLEditRules::WillDeleteSelection(Selection* aSelection,
                                   nsIEditor::EDirection aAction,
                                   nsIEditor::EStripWrappers aStripWrappers,
                                   bool* aCancel,
                                   bool* aHandled)
{
  MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
             aStripWrappers == nsIEditor::eNoStrip);

  if (!aSelection || !aCancel || !aHandled) {
    return NS_ERROR_NULL_POINTER;
  }
  // Initialize out params
  *aCancel = false;
  *aHandled = false;

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

  // If there is only bogus content, cancel the operation
  if (mBogusNode) {
    *aCancel = true;
    return NS_OK;
  }

  // First check for table selection mode.  If so, hand off to table editor.
  nsCOMPtr<nsIDOMElement> cell;
  NS_ENSURE_STATE(mHTMLEditor);
  nsresult rv =
    mHTMLEditor->GetFirstSelectedCell(nullptr, getter_AddRefs(cell));
  if (NS_SUCCEEDED(rv) && cell) {
    NS_ENSURE_STATE(mHTMLEditor);
    rv = mHTMLEditor->DeleteTableCellContents();
    *aHandled = true;
    return rv;
  }
  cell = nullptr;

  // origCollapsed is used later to determine whether we should join blocks. We
  // don't really care about bCollapsed because it will be modified by
  // ExtendSelectionForDelete later. TryToJoinBlocks() should happen if the
  // original selection is collapsed and the cursor is at the end of a block
  // element, in which case ExtendSelectionForDelete would always make the
  // selection not collapsed.
  bool bCollapsed = aSelection->Collapsed();
  bool join = false;
  bool origCollapsed = bCollapsed;

  nsCOMPtr<nsINode> selNode;
  int32_t selOffset;

  NS_ENSURE_STATE(aSelection->GetRangeAt(0));
  nsCOMPtr<nsINode> startNode = aSelection->GetRangeAt(0)->GetStartContainer();
  int32_t startOffset = aSelection->GetRangeAt(0)->StartOffset();
  NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);

  if (bCollapsed) {
    // If we are inside an empty block, delete it.
    NS_ENSURE_STATE(mHTMLEditor);
    nsCOMPtr<Element> host = mHTMLEditor->GetActiveEditingHost();
    NS_ENSURE_TRUE(host, NS_ERROR_FAILURE);
    rv = CheckForEmptyBlock(startNode, host, aSelection, aAction, aHandled);
    NS_ENSURE_SUCCESS(rv, rv);
    if (*aHandled) {
      return NS_OK;
    }

    // Test for distance between caret and text that will be deleted
    rv = CheckBidiLevelForDeletion(aSelection, GetAsDOMNode(startNode),
                                   startOffset, aAction, aCancel);
    NS_ENSURE_SUCCESS(rv, rv);
    if (*aCancel) {
      return NS_OK;
    }

    NS_ENSURE_STATE(mHTMLEditor);
    rv = mHTMLEditor->ExtendSelectionForDelete(aSelection, &aAction);
    NS_ENSURE_SUCCESS(rv, rv);

    // We should delete nothing.
    if (aAction == nsIEditor::eNone) {
      return NS_OK;
    }

    // ExtendSelectionForDelete() may have changed the selection, update it
    NS_ENSURE_STATE(aSelection->GetRangeAt(0));
    startNode = aSelection->GetRangeAt(0)->GetStartContainer();
    startOffset = aSelection->GetRangeAt(0)->StartOffset();
    NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);

    bCollapsed = aSelection->Collapsed();
  }

  if (bCollapsed) {
    // What's in the direction we are deleting?
    NS_ENSURE_STATE(mHTMLEditor);
    WSRunObject wsObj(mHTMLEditor, startNode, startOffset);
    nsCOMPtr<nsINode> visNode;
    int32_t visOffset;
    WSType wsType;

    // Find next visible node
    if (aAction == nsIEditor::eNext) {
      wsObj.NextVisibleNode(startNode, startOffset, address_of(visNode),
                            &visOffset, &wsType);
    } else {
      wsObj.PriorVisibleNode(startNode, startOffset, address_of(visNode),
                             &visOffset, &wsType);
    }

    if (!visNode) {
      // Can't find anything to delete!
      *aCancel = true;
      // XXX This is the result of mHTMLEditor->GetFirstSelectedCell().
      //     The value could be both an error and NS_OK.
      return rv;
    }

    if (wsType == WSType::normalWS) {
      // We found some visible ws to delete.  Let ws code handle it.
      *aHandled = true;
      if (aAction == nsIEditor::eNext) {
        rv = wsObj.DeleteWSForward();
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }
      } else {
        rv = wsObj.DeleteWSBackward();
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }
      }
      return InsertBRIfNeeded(aSelection);
    }

    if (wsType == WSType::text) {
      // Found normal text to delete.
      OwningNonNull<Text> nodeAsText = *visNode->GetAsText();
      int32_t so = visOffset;
      int32_t eo = visOffset + 1;
      if (aAction == nsIEditor::ePrevious) {
        if (!so) {
          return NS_ERROR_UNEXPECTED;
        }
        so--;
        eo--;
        // Bug 1068979: delete both codepoints if surrogate pair
        if (so > 0) {
          const nsTextFragment *text = nodeAsText->GetText();
          if (NS_IS_LOW_SURROGATE(text->CharAt(so)) &&
              NS_IS_HIGH_SURROGATE(text->CharAt(so - 1))) {
            so--;
          }
        }
      } else {
        RefPtr<nsRange> range = aSelection->GetRangeAt(0);
        NS_ENSURE_STATE(range);

        NS_ASSERTION(range->GetStartContainer() == visNode,
                     "selection start not in visNode");
        NS_ASSERTION(range->GetEndContainer() == visNode,
                     "selection end not in visNode");

        so = range->StartOffset();
        eo = range->EndOffset();
      }
      NS_ENSURE_STATE(mHTMLEditor);
      rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(visNode),
                                             &so, address_of(visNode), &eo);
      NS_ENSURE_SUCCESS(rv, rv);
      NS_ENSURE_STATE(mHTMLEditor);
      *aHandled = true;
      rv = mHTMLEditor->DeleteText(nodeAsText, std::min(so, eo),
                                   DeprecatedAbs(eo - so));
      NS_ENSURE_SUCCESS(rv, rv);

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

      DeleteNodeIfCollapsedText(nodeAsText);

      rv = InsertBRIfNeeded(aSelection);
      NS_ENSURE_SUCCESS(rv, rv);

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

      return NS_OK;
    }

    if (wsType == WSType::special || wsType == WSType::br ||
        visNode->IsHTMLElement(nsGkAtoms::hr)) {
      // Short circuit for invisible breaks.  delete them and recurse.
      if (visNode->IsHTMLElement(nsGkAtoms::br) &&
          (!mHTMLEditor || !mHTMLEditor->IsVisibleBRElement(visNode))) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->DeleteNode(visNode);
        NS_ENSURE_SUCCESS(rv, rv);
        return WillDeleteSelection(aSelection, aAction, aStripWrappers,
                                   aCancel, aHandled);
      }

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

        selNode = visNode->GetParentNode();
        selOffset = selNode ? selNode->IndexOf(visNode) : -1;

        bool interLineIsRight;
        rv = aSelection->GetInterlinePosition(&interLineIsRight);
        NS_ENSURE_SUCCESS(rv, rv);

        if (startNode == selNode && startOffset - 1 == selOffset &&
            !interLineIsRight) {
          moveOnly = false;
        }

        if (moveOnly) {
          // Go to the position after the <hr>, but to the end of the <hr> line
          // by setting the interline position to left.
          ++selOffset;
          aSelection->Collapse(selNode, selOffset);
          aSelection->SetInterlinePosition(false);
          mDidExplicitlySetInterline = true;
          *aHandled = true;

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

          WSType otherWSType;
          nsCOMPtr<nsINode> otherNode;
          int32_t otherOffset;

          wsObj.NextVisibleNode(startNode, startOffset, address_of(otherNode),
                                &otherOffset, &otherWSType);

          if (otherWSType == WSType::br) {
            // Delete the <br>

            NS_ENSURE_STATE(mHTMLEditor);
            nsCOMPtr<nsIContent> otherContent(do_QueryInterface(otherNode));
            rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, otherContent);
            NS_ENSURE_SUCCESS(rv, rv);
            NS_ENSURE_STATE(mHTMLEditor);
            rv = mHTMLEditor->DeleteNode(otherNode);
            NS_ENSURE_SUCCESS(rv, rv);
          }

          return NS_OK;
        }
        // Else continue with normal delete code
      }

      // Found break or image, or hr.
      NS_ENSURE_STATE(mHTMLEditor);
      NS_ENSURE_STATE(visNode->IsContent());
      rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, visNode->AsContent());
      NS_ENSURE_SUCCESS(rv, rv);
      // Remember sibling to visnode, if any
      NS_ENSURE_STATE(mHTMLEditor);
      nsCOMPtr<nsIContent> sibling = mHTMLEditor->GetPriorHTMLSibling(visNode);
      // Delete the node, and join like nodes if appropriate
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->DeleteNode(visNode);
      NS_ENSURE_SUCCESS(rv, rv);
      // We did something, so let's say so.
      *aHandled = true;
      // Is there a prior node and are they siblings?
      nsCOMPtr<nsINode> stepbrother;
      if (sibling) {
        NS_ENSURE_STATE(mHTMLEditor);
        stepbrother = mHTMLEditor->GetNextHTMLSibling(sibling);
      }
      // Are they both text nodes?  If so, join them!
      if (startNode == stepbrother && startNode->GetAsText() &&
          sibling->GetAsText()) {
        EditorDOMPoint pt = JoinNodesSmart(*sibling, *startNode->AsContent());
        NS_ENSURE_STATE(pt.node);
        // Fix up selection
        rv = aSelection->Collapse(pt.node, pt.offset);
        NS_ENSURE_SUCCESS(rv, rv);
      }
      rv = InsertBRIfNeeded(aSelection);
      NS_ENSURE_SUCCESS(rv, rv);
      return NS_OK;
    }

    if (wsType == WSType::otherBlock) {
      // Make sure it's not a table element.  If so, cancel the operation
      // (translation: users cannot backspace or delete across table cells)
      if (HTMLEditUtils::IsTableElement(visNode)) {
        *aCancel = true;
        return NS_OK;
      }

      // Next to a block.  See if we are between a block and a br.  If so, we
      // really want to delete the br.  Else join content at selection to the
      // block.
      bool bDeletedBR = false;
      WSType otherWSType;
      nsCOMPtr<nsINode> otherNode;
      int32_t otherOffset;

      // Find node in other direction
      if (aAction == nsIEditor::eNext) {
        wsObj.PriorVisibleNode(startNode, startOffset, address_of(otherNode),
                               &otherOffset, &otherWSType);
      } else {
        wsObj.NextVisibleNode(startNode, startOffset, address_of(otherNode),
                              &otherOffset, &otherWSType);
      }

      // First find the adjacent node in the block
      nsCOMPtr<nsIContent> leafNode;
      nsCOMPtr<nsINode> leftNode, rightNode;
      if (aAction == nsIEditor::ePrevious) {
        NS_ENSURE_STATE(mHTMLEditor);
        leafNode = mHTMLEditor->GetLastEditableLeaf(*visNode);
        leftNode = leafNode;
        rightNode = startNode;
      } else {
        NS_ENSURE_STATE(mHTMLEditor);
        leafNode = mHTMLEditor->GetFirstEditableLeaf(*visNode);
        leftNode = startNode;
        rightNode = leafNode;
      }

      if (otherNode->IsHTMLElement(nsGkAtoms::br)) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->DeleteNode(otherNode);
        NS_ENSURE_SUCCESS(rv, rv);
        // XXX Only in this case, setting "handled" to true only when it
        //     succeeds?
        *aHandled = true;
        bDeletedBR = true;
      }

      // Don't cross table boundaries
      if (leftNode && rightNode &&
          InDifferentTableElements(leftNode, rightNode)) {
        return NS_OK;
      }

      if (bDeletedBR) {
        // Put selection at edge of block and we are done.
        NS_ENSURE_STATE(leafNode);
        EditorDOMPoint newSel = GetGoodSelPointForNode(*leafNode, aAction);
        NS_ENSURE_STATE(newSel.node);
        aSelection->Collapse(newSel.node, newSel.offset);
        return NS_OK;
      }

      // Else we are joining content to block

      nsCOMPtr<nsINode> selPointNode = startNode;
      int32_t selPointOffset = startOffset;
      {
        NS_ENSURE_STATE(mHTMLEditor);
        AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater,
                                  address_of(selPointNode), &selPointOffset);
        NS_ENSURE_STATE(leftNode && leftNode->IsContent() &&
                        rightNode && rightNode->IsContent());
        EditActionResult ret =
          TryToJoinBlocks(*leftNode->AsContent(), *rightNode->AsContent());
        *aHandled |= ret.Handled();
        *aCancel |= ret.Canceled();
        if (NS_WARN_IF(ret.Failed())) {
          return ret.Rv();
        }
      }

      // If TryToJoinBlocks() didn't handle it  and it's not canceled,
      // user may want to modify the start leaf node or the last leaf node
      // of the block.
      if (!*aHandled && !*aCancel && leafNode != startNode) {
        int32_t offset =
          aAction == nsIEditor::ePrevious ?
            static_cast<int32_t>(leafNode->Length()) : 0;
        aSelection->Collapse(leafNode, offset);
        return WillDeleteSelection(aSelection, aAction, aStripWrappers,
                                   aCancel, aHandled);
      }

      // Otherwise, we must have deleted the selection as user expected.
      aSelection->Collapse(selPointNode, selPointOffset);
      return NS_OK;
    }

    if (wsType == WSType::thisBlock) {
      // At edge of our block.  Look beside it and see if we can join to an
      // adjacent block

      // Make sure it's not a table element.  If so, cancel the operation
      // (translation: users cannot backspace or delete across table cells)
      if (HTMLEditUtils::IsTableElement(visNode)) {
        *aCancel = true;
        return NS_OK;
      }

      // First find the relevant nodes
      nsCOMPtr<nsINode> leftNode, rightNode;
      if (aAction == nsIEditor::ePrevious) {
        NS_ENSURE_STATE(mHTMLEditor);
        leftNode = mHTMLEditor->GetPriorHTMLNode(visNode);
        rightNode = startNode;
      } else {
        NS_ENSURE_STATE(mHTMLEditor);
        rightNode = mHTMLEditor->GetNextHTMLNode(visNode);
        leftNode = startNode;
      }

      // Nothing to join
      if (!leftNode || !rightNode) {
        *aCancel = true;
        return NS_OK;
      }

      // Don't cross table boundaries -- cancel it
      if (InDifferentTableElements(leftNode, rightNode)) {
        *aCancel = true;
        return NS_OK;
      }

      nsCOMPtr<nsINode> selPointNode = startNode;
      int32_t selPointOffset = startOffset;
      {
        NS_ENSURE_STATE(mHTMLEditor);
        AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater,
                                  address_of(selPointNode), &selPointOffset);
        NS_ENSURE_STATE(leftNode->IsContent() && rightNode->IsContent());
        EditActionResult ret =
          TryToJoinBlocks(*leftNode->AsContent(), *rightNode->AsContent());
        // This should claim that trying to join the block means that
        // this handles the action because the caller shouldn't do anything
        // anymore in this case.
        *aHandled = true;
        *aCancel |= ret.Canceled();
        if (NS_WARN_IF(ret.Failed())) {
          return ret.Rv();
        }
      }
      aSelection->Collapse(selPointNode, selPointOffset);
      return NS_OK;
    }
  }


  // Else we have a non-collapsed selection.  First adjust the selection.
  rv = ExpandSelectionForDeletion(*aSelection);
  NS_ENSURE_SUCCESS(rv, rv);

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

  // Refresh start and end points
  NS_ENSURE_STATE(aSelection->GetRangeAt(0));
  startNode = aSelection->GetRangeAt(0)->GetStartContainer();
  startOffset = aSelection->GetRangeAt(0)->StartOffset();
  NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
  nsCOMPtr<nsINode> endNode = aSelection->GetRangeAt(0)->GetEndContainer();
  int32_t endOffset = aSelection->GetRangeAt(0)->EndOffset();
  NS_ENSURE_TRUE(endNode, NS_ERROR_FAILURE);

  // Figure out if the endpoints are in nodes that can be merged.  Adjust
  // surrounding whitespace in preparation to delete selection.
  if (!IsPlaintextEditor()) {
    NS_ENSURE_STATE(mHTMLEditor);
    AutoTransactionsConserveSelection dontChangeMySelection(mHTMLEditor);
    rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor,
                                           address_of(startNode), &startOffset,
                                           address_of(endNode), &endOffset);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  {
    // Track location of where we are deleting
    NS_ENSURE_STATE(mHTMLEditor);
    AutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater,
                                   address_of(startNode), &startOffset);
    AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater,
                                 address_of(endNode), &endOffset);
    // We are handling all ranged deletions directly now.
    *aHandled = true;

    if (endNode == startNode) {
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers);
      NS_ENSURE_SUCCESS(rv, rv);
    } else {
      // Figure out mailcite ancestors
      nsCOMPtr<Element> startCiteNode = GetTopEnclosingMailCite(*startNode);
      nsCOMPtr<Element> endCiteNode = GetTopEnclosingMailCite(*endNode);

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

      // Figure out block parents
      NS_ENSURE_STATE(mHTMLEditor);
      nsCOMPtr<Element> leftParent = mHTMLEditor->GetBlock(*startNode);
      nsCOMPtr<Element> rightParent = mHTMLEditor->GetBlock(*endNode);

      // Are endpoint block parents the same?  Use default deletion
      if (leftParent && leftParent == rightParent) {
        NS_ENSURE_STATE(mHTMLEditor);
        mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers);
      } else {
        // Deleting across blocks.  Are the blocks of same type?
        NS_ENSURE_STATE(leftParent && rightParent);

        // Are the blocks siblings?
        nsCOMPtr<nsINode> leftBlockParent = leftParent->GetParentNode();
        nsCOMPtr<nsINode> rightBlockParent = rightParent->GetParentNode();

        // MOOSE: this could conceivably screw up a table.. fix me.
        NS_ENSURE_STATE(mHTMLEditor);
        if (leftBlockParent == rightBlockParent &&
            mHTMLEditor->NodesSameType(GetAsDOMNode(leftParent),
                                       GetAsDOMNode(rightParent)) &&
            // XXX What's special about these three types of block?
            (leftParent->IsHTMLElement(nsGkAtoms::p) ||
             HTMLEditUtils::IsListItem(leftParent) ||
             HTMLEditUtils::IsHeader(*leftParent))) {
          // First delete the selection
          NS_ENSURE_STATE(mHTMLEditor);
          rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers);
          NS_ENSURE_SUCCESS(rv, rv);
          // Join blocks
          NS_ENSURE_STATE(mHTMLEditor);
          EditorDOMPoint pt =
            mHTMLEditor->JoinNodeDeep(*leftParent, *rightParent);
          NS_ENSURE_STATE(pt.node);
          // Fix up selection
          rv = aSelection->Collapse(pt.node, pt.offset);
          NS_ENSURE_SUCCESS(rv, rv);
          return NS_OK;
        }

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

        AutoRangeArray arrayOfRanges(aSelection);
        for (auto& range : arrayOfRanges.mRanges) {
          // Build a list of nodes in the range
          nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
          TrivialFunctor functor;
          DOMSubtreeIterator iter;
          nsresult rv = iter.Init(*range);
          NS_ENSURE_SUCCESS(rv, rv);
          iter.AppendList(functor, arrayOfNodes);

          // Now that we have the list, delete non-table elements
          int32_t listCount = arrayOfNodes.Length();
          for (int32_t j = 0; j < listCount; j++) {
            nsCOMPtr<nsINode> somenode = do_QueryInterface(arrayOfNodes[0]);
            NS_ENSURE_STATE(somenode);
            DeleteNonTableElements(somenode);
            arrayOfNodes.RemoveElementAt(0);
            // If something visible is deleted, no need to join.  Visible means
            // all nodes except non-visible textnodes and breaks.
            if (join && origCollapsed) {
              if (!somenode->IsContent()) {
                join = false;
                continue;
              }
              nsCOMPtr<nsIContent> content = somenode->AsContent();
              if (content->NodeType() == nsIDOMNode::TEXT_NODE) {
                NS_ENSURE_STATE(mHTMLEditor);
                mHTMLEditor->IsVisTextNode(content, &join, true);
              } else {
                NS_ENSURE_STATE(mHTMLEditor);
                join = content->IsHTMLElement(nsGkAtoms::br) &&
                       !mHTMLEditor->IsVisibleBRElement(somenode);
              }
            }
          }
        }

        // Check endpoints for possible text deletion.  We can assume that if
        // text node is found, we can delete to end or to begining as
        // appropriate, since the case where both sel endpoints in same text
        // node was already handled (we wouldn't be here)
        if (startNode->GetAsText() &&
            startNode->Length() > static_cast<uint32_t>(startOffset)) {
          // Delete to last character
          OwningNonNull<nsGenericDOMDataNode> dataNode =
            *static_cast<nsGenericDOMDataNode*>(startNode.get());
          NS_ENSURE_STATE(mHTMLEditor);
          rv = mHTMLEditor->DeleteText(dataNode, startOffset,
                                       startNode->Length() - startOffset);
          NS_ENSURE_SUCCESS(rv, rv);
        }
        if (endNode->GetAsText() && endOffset) {
          // Delete to first character
          NS_ENSURE_STATE(mHTMLEditor);
          OwningNonNull<nsGenericDOMDataNode> dataNode =
            *static_cast<nsGenericDOMDataNode*>(endNode.get());
          rv = mHTMLEditor->DeleteText(dataNode, 0, endOffset);
          NS_ENSURE_SUCCESS(rv, rv);
        }

        if (join) {
          EditActionResult ret = TryToJoinBlocks(*leftParent, *rightParent);
          MOZ_ASSERT(*aHandled);
          *aCancel |= ret.Canceled();
          if (NS_WARN_IF(ret.Failed())) {
            return ret.Rv();
          }
        }
      }
    }
  }

  // We might have left only collapsed whitespace in the start/end nodes
  {
    AutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater,
                                   address_of(startNode), &startOffset);
    AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater,
                                 address_of(endNode), &endOffset);

    DeleteNodeIfCollapsedText(*startNode);
    DeleteNodeIfCollapsedText(*endNode);
  }

  // If we're joining blocks: if deleting forward the selection should be
  // collapsed to the end of the selection, if deleting backward the selection
  // should be collapsed to the beginning of the selection. But if we're not
  // joining then the selection should collapse to the beginning of the
  // selection if we'redeleting forward, because the end of the selection will
  // still be in the next block. And same thing for deleting backwards
  // (selection should collapse to the end, because the beginning will still be
  // in the first block). See Bug 507936
  if (aAction == (join ? nsIEditor::eNext : nsIEditor::ePrevious)) {
    rv = aSelection->Collapse(endNode, endOffset);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  } else {
    rv = aSelection->Collapse(startNode, startOffset);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }
  return NS_OK;
}

/**
 * If aNode is a text node that contains only collapsed whitespace, delete it.
 * It doesn't serve any useful purpose, and we don't want it to confuse code
 * that doesn't correctly skip over it.
 *
 * If deleting the node fails (like if it's not editable), the caller should
 * proceed as usual, so don't return any errors.
 */
void
HTMLEditRules::DeleteNodeIfCollapsedText(nsINode& aNode)
{
  if (!aNode.GetAsText()) {
    return;
  }
  bool empty;
  nsresult rv = mHTMLEditor->IsVisTextNode(aNode.AsContent(), &empty, false);
  NS_ENSURE_SUCCESS_VOID(rv);
  if (empty) {
    mHTMLEditor->DeleteNode(&aNode);
  }
}


/**
 * InsertBRIfNeeded() determines if a br is needed for current selection to not
 * be spastic.  If so, it inserts one.  Callers responsibility to only call
 * with collapsed selection.
 *
 * @param aSelection        The collapsed selection.
 */
nsresult
HTMLEditRules::InsertBRIfNeeded(Selection* aSelection)
{
  NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);

  // get selection
  nsCOMPtr<nsINode> node;
  int32_t offset;
  nsresult rv =
    EditorBase::GetStartNodeAndOffset(aSelection,
                                      getter_AddRefs(node), &offset);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);

  // inline elements don't need any br
  if (!IsBlockNode(*node)) {
    return NS_OK;
  }

  // examine selection
  NS_ENSURE_STATE(mHTMLEditor);
  WSRunObject wsObj(mHTMLEditor, node, offset);
  if (((wsObj.mStartReason & WSType::block) ||
       (wsObj.mStartReason & WSType::br)) &&
      (wsObj.mEndReason & WSType::block)) {
    // if we are tucked between block boundaries then insert a br
    // first check that we are allowed to
    NS_ENSURE_STATE(mHTMLEditor);
    if (mHTMLEditor->CanContainTag(*node, *nsGkAtoms::br)) {
      NS_ENSURE_STATE(mHTMLEditor);
      nsCOMPtr<Element> br =
        mHTMLEditor->CreateBR(node, offset, nsIEditor::ePrevious);
      return br ? NS_OK : NS_ERROR_FAILURE;
    }
  }
  return NS_OK;
}

/**
 * GetGoodSelPointForNode() finds where at a node you would want to set the
 * selection if you were trying to have a caret next to it.  Always returns a
 * valid value (unless mHTMLEditor has gone away).
 *
 * @param aNode         The node
 * @param aAction       Which edge to find:
 *                        eNext/eNextWord/eToEndOfLine indicates beginning,
 *                        ePrevious/PreviousWord/eToBeginningOfLine ending.
 */
EditorDOMPoint
HTMLEditRules::GetGoodSelPointForNode(nsINode& aNode,
                                      nsIEditor::EDirection aAction)
{
  MOZ_ASSERT(aAction == nsIEditor::eNext ||
             aAction == nsIEditor::eNextWord ||
             aAction == nsIEditor::ePrevious ||
             aAction == nsIEditor::ePreviousWord ||
             aAction == nsIEditor::eToBeginningOfLine ||
             aAction == nsIEditor::eToEndOfLine);

  bool isPreviousAction = (aAction == nsIEditor::ePrevious ||
                           aAction == nsIEditor::ePreviousWord ||
                           aAction == nsIEditor::eToBeginningOfLine);

  NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
  if (aNode.GetAsText() || mHTMLEditor->IsContainer(&aNode) ||
      NS_WARN_IF(!aNode.GetParentNode())) {
    return EditorDOMPoint(&aNode, isPreviousAction ? aNode.Length() : 0);
  }

  EditorDOMPoint ret;
  ret.node = aNode.GetParentNode();
  ret.offset = ret.node ? ret.node->IndexOf(&aNode) : -1;
  NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
  if ((!aNode.IsHTMLElement(nsGkAtoms::br) ||
       mHTMLEditor->IsVisibleBRElement(&aNode)) && isPreviousAction) {
    ret.offset++;
  }
  return ret;
}

EditActionResult
HTMLEditRules::TryToJoinBlocks(nsIContent& aLeftNode,
                               nsIContent& aRightNode)
{
  if (NS_WARN_IF(!mHTMLEditor)) {
    return EditActionIgnored(NS_ERROR_UNEXPECTED);
  }

  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  nsCOMPtr<Element> leftBlock = htmlEditor->GetBlock(aLeftNode);
  nsCOMPtr<Element> rightBlock = htmlEditor->GetBlock(aRightNode);

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

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

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

  // Bail if both blocks the same
  if (leftBlock == rightBlock) {
    return EditActionIgnored();
  }

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

  // Special rule here: if we are trying to join list items, and they are in
  // different lists, join the lists instead.
  bool mergeLists = false;
  nsIAtom* existingList = nsGkAtoms::_empty;
  int32_t offset;
  nsCOMPtr<Element> leftList, rightList;
  if (HTMLEditUtils::IsListItem(leftBlock) &&
      HTMLEditUtils::IsListItem(rightBlock)) {
    leftList = leftBlock->GetParentElement();
    rightList = rightBlock->GetParentElement();
    if (leftList && rightList && leftList != rightList &&
        !EditorUtils::IsDescendantOf(leftList, rightBlock, &offset) &&
        !EditorUtils::IsDescendantOf(rightList, leftBlock, &offset)) {
      // There are some special complications if the lists are descendants of
      // the other lists' items.  Note that it is okay for them to be
      // descendants of the other lists themselves, which is the usual case for
      // sublists in our implementation.
      leftBlock = leftList;
      rightBlock = rightList;
      mergeLists = true;
      existingList = leftList->NodeInfo()->NameAtom();
    }
  }

  AutoTransactionsConserveSelection dontChangeMySelection(htmlEditor);

  int32_t rightOffset = 0;
  int32_t leftOffset = -1;

  // offset below is where you find yourself in rightBlock when you traverse
  // upwards from leftBlock
  if (EditorUtils::IsDescendantOf(leftBlock, rightBlock, &rightOffset)) {
    // Tricky case.  Left block is inside right block.  Do ws adjustment.  This
    // just destroys non-visible ws at boundaries we will be joining.
    rightOffset++;
    nsresult rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
                                                  WSRunObject::kBlockEnd,
                                                  leftBlock);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return EditActionIgnored(rv);
    }

    {
      // We can't just track rightBlock because it's an Element.
      nsCOMPtr<nsINode> trackingRightBlock(rightBlock);
      AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater,
                                address_of(trackingRightBlock), &rightOffset);
      rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
                                           WSRunObject::kAfterBlock,
                                           rightBlock, rightOffset);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return EditActionIgnored(rv);
      }

      if (trackingRightBlock->IsElement()) {
        rightBlock = trackingRightBlock->AsElement();
      } else {
        if (NS_WARN_IF(!trackingRightBlock->GetParentElement())) {
          return EditActionIgnored(NS_ERROR_UNEXPECTED);
        }
        rightBlock = trackingRightBlock->GetParentElement();
      }
    }
    // Do br adjustment.
    nsCOMPtr<Element> brNode =
      CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd);
    EditActionResult ret(NS_OK);
    if (mergeLists) {
      // The idea here is to take all children in rightList that are past
      // offset, and pull them into leftlist.
      for (nsCOMPtr<nsIContent> child = rightList->GetChildAt(offset);
           child; child = rightList->GetChildAt(rightOffset)) {
        rv = htmlEditor->MoveNode(child, leftList, -1);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return EditActionIgnored(rv);
        }
      }
      // XXX Should this set to true only when above for loop moves the node?
      ret.MarkAsHandled();
    } else {
      // XXX Why do we ignore the result of MoveBlock()?
      EditActionResult retMoveBlock =
        MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset);
      if (retMoveBlock.Handled()) {
        ret.MarkAsHandled();
      }
    }
    if (brNode && NS_SUCCEEDED(htmlEditor->DeleteNode(brNode))) {
      ret.MarkAsHandled();
    }
    return ret;
  }

  // Offset below is where you find yourself in leftBlock when you traverse
  // upwards from rightBlock
  if (EditorUtils::IsDescendantOf(rightBlock, leftBlock, &leftOffset)) {
    // Tricky case.  Right block is inside left block.  Do ws adjustment.  This
    // just destroys non-visible ws at boundaries we will be joining.
    nsresult rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
                                                  WSRunObject::kBlockStart,
                                                  rightBlock);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return EditActionIgnored(rv);
    }

    {
      // We can't just track leftBlock because it's an Element, so track
      // something else.
      nsCOMPtr<nsINode> trackingLeftBlock(leftBlock);
      AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater,
                                address_of(trackingLeftBlock), &leftOffset);
      rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
                                           WSRunObject::kBeforeBlock,
                                           leftBlock, leftOffset);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return EditActionIgnored(rv);
      }

      if (trackingLeftBlock->IsElement()) {
        leftBlock = trackingLeftBlock->AsElement();
      } else {
        if (NS_WARN_IF(!trackingLeftBlock->GetParentElement())) {
          return EditActionIgnored(NS_ERROR_UNEXPECTED);
        }
        leftBlock = trackingLeftBlock->GetParentElement();
      }
    }
    // Do br adjustment.
    nsCOMPtr<Element> brNode =
      CheckForInvisibleBR(*leftBlock, BRLocation::beforeBlock, leftOffset);
    EditActionResult ret(NS_OK);
    if (mergeLists) {
      // XXX Why do we ignore the result of MoveContents()?
      EditActionResult retMoveContents =
        MoveContents(*rightList, *leftList, &leftOffset);
      if (retMoveContents.Handled()) {
        ret.MarkAsHandled();
      }
    } else {
      // Left block is a parent of right block, and the parent of the previous
      // visible content.  Right block is a child and contains the contents we
      // want to move.

      int32_t previousContentOffset;
      nsCOMPtr<nsINode> previousContentParent;

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

        previousContentParent = aLeftNode.GetParentNode();
        previousContentOffset = previousContentParent ?
          previousContentParent->IndexOf(&aLeftNode) : -1;

        // We want to move our content just after the previous visible node.
        previousContentOffset++;
      }

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

      nsCOMPtr<Element> editorRoot = htmlEditor->GetEditorRoot();
      if (!editorRoot || &aLeftNode != editorRoot) {
        nsCOMPtr<nsIContent> splittedPreviousContent;
        rv = htmlEditor->SplitStyleAbovePoint(
                           address_of(previousContentParent),
                           &previousContentOffset,
                           nullptr, nullptr, nullptr,
                           getter_AddRefs(splittedPreviousContent));
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return EditActionIgnored(rv);
        }

        if (splittedPreviousContent) {
          previousContentParent = splittedPreviousContent->GetParentNode();
          previousContentOffset = previousContentParent ?
            previousContentParent->IndexOf(splittedPreviousContent) : -1;
        }
      }

      if (NS_WARN_IF(!previousContentParent)) {
        return EditActionIgnored(NS_ERROR_NULL_POINTER);
      }

      ret |= MoveBlock(*previousContentParent->AsElement(), *rightBlock,
                       previousContentOffset, rightOffset);
      if (NS_WARN_IF(ret.Failed())) {
        return ret;
      }
    }
    if (brNode && NS_SUCCEEDED(htmlEditor->DeleteNode(brNode))) {
      ret.MarkAsHandled();
    }
    return ret;
  }

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

  // Adjust whitespace at block boundaries
  nsresult rv =
    WSRunObject::PrepareToJoinBlocks(htmlEditor, leftBlock, rightBlock);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return EditActionIgnored(rv);
  }
  // Do br adjustment.
  nsCOMPtr<Element> brNode =
    CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd);
  EditActionResult ret(NS_OK);
  if (mergeLists || leftBlock->NodeInfo()->NameAtom() ==
                    rightBlock->NodeInfo()->NameAtom()) {
    // Nodes are same type.  merge them.
    EditorDOMPoint pt = JoinNodesSmart(*leftBlock, *rightBlock);
    if (pt.node && mergeLists) {
      nsCOMPtr<Element> newBlock;
      ConvertListType(rightBlock, getter_AddRefs(newBlock),
                      existingList, nsGkAtoms::li);
    }
    ret.MarkAsHandled();
  } else {
    // Nodes are dissimilar types.
    ret |= MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset);
    if (NS_WARN_IF(ret.Failed())) {
      return ret;
    }
  }
  if (brNode) {
    rv = htmlEditor->DeleteNode(brNode);
    // XXX In other top level if blocks, the result of DeleteNode()
    //     is ignored.  Why does only this result is respected?
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return ret.SetResult(rv);
    }
    ret.MarkAsHandled();
  }
  return ret;
}

EditActionResult
HTMLEditRules::MoveBlock(Element& aLeftBlock,
                         Element& aRightBlock,
                         int32_t aLeftOffset,
                         int32_t aRightOffset)
{
  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
  // GetNodesFromPoint is the workhorse that figures out what we wnat to move.
  nsresult rv = GetNodesFromPoint(EditorDOMPoint(&aRightBlock, aRightOffset),
                                  EditAction::makeList, arrayOfNodes,
                                  TouchContent::yes);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return EditActionIgnored(rv);
  }

  EditActionResult ret(NS_OK);
  for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) {
    // get the node to act on
    if (IsBlockNode(arrayOfNodes[i])) {
      // For block nodes, move their contents only, then delete block.
      ret |=
        MoveContents(*arrayOfNodes[i]->AsElement(), aLeftBlock, &aLeftOffset);
      if (NS_WARN_IF(ret.Failed())) {
        return ret;
      }
      if (NS_WARN_IF(!mHTMLEditor)) {
        return ret.SetResult(NS_ERROR_UNEXPECTED);
      }
      rv = mHTMLEditor->DeleteNode(arrayOfNodes[i]);
      ret.MarkAsHandled();
    } else {
      // Otherwise move the content as is, checking against the DTD.
      ret |=
        MoveNodeSmart(*arrayOfNodes[i]->AsContent(), aLeftBlock, &aLeftOffset);
    }
  }

  // XXX We're only checking return value of the last iteration
  if (NS_WARN_IF(ret.Failed())) {
    return ret;
  }

  return ret;
}

EditActionResult
HTMLEditRules::MoveNodeSmart(nsIContent& aNode,
                             Element& aDestElement,
                             int32_t* aInOutDestOffset)
{
  MOZ_ASSERT(aInOutDestOffset);

  if (NS_WARN_IF(!mHTMLEditor)) {
    return EditActionIgnored(NS_ERROR_UNEXPECTED);
  }

  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  // Check if this node can go into the destination node
  if (htmlEditor->CanContain(aDestElement, aNode)) {
    // If it can, move it there
    nsresult rv =
      htmlEditor->MoveNode(&aNode, &aDestElement, *aInOutDestOffset);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return EditActionIgnored(rv);
    }
    if (*aInOutDestOffset != -1) {
      (*aInOutDestOffset)++;
    }
    // XXX Should we check if the node is actually moved in this case?
    return EditActionHandled();
  }

  // If it can't, move its children (if any), and then delete it.
  EditActionResult ret(NS_OK);
  if (aNode.IsElement()) {
    ret = MoveContents(*aNode.AsElement(), aDestElement, aInOutDestOffset);
    if (NS_WARN_IF(ret.Failed())) {
      return ret;
    }
  }

  nsresult rv = htmlEditor->DeleteNode(&aNode);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return ret.SetResult(rv);
  }
  return ret.MarkAsHandled();
}

EditActionResult
HTMLEditRules::MoveContents(Element& aElement,
                            Element& aDestElement,
                            int32_t* aInOutDestOffset)
{
  MOZ_ASSERT(aInOutDestOffset);

  if (NS_WARN_IF(&aElement == &aDestElement)) {
    return EditActionIgnored(NS_ERROR_ILLEGAL_VALUE);
  }

  EditActionResult ret(NS_OK);
  while (aElement.GetFirstChild()) {
    ret |=
      MoveNodeSmart(*aElement.GetFirstChild(), aDestElement, aInOutDestOffset);
    if (NS_WARN_IF(ret.Failed())) {
      return ret;
    }
  }
  return ret;
}


nsresult
HTMLEditRules::DeleteNonTableElements(nsINode* aNode)
{
  MOZ_ASSERT(aNode);
  if (!HTMLEditUtils::IsTableElementButNotTable(aNode)) {
    NS_ENSURE_STATE(mHTMLEditor);
    return mHTMLEditor->DeleteNode(aNode->AsDOMNode());
  }

  for (int32_t i = aNode->GetChildCount() - 1; i >= 0; --i) {
    nsresult rv = DeleteNonTableElements(aNode->GetChildAt(i));
    NS_ENSURE_SUCCESS(rv, rv);
  }
  return NS_OK;
}

nsresult
HTMLEditRules::DidDeleteSelection(Selection* aSelection,
                                  nsIEditor::EDirection aDir,
                                  nsresult aResult)
{
  if (!aSelection) {
    return NS_ERROR_NULL_POINTER;
  }

  // find where we are
  nsCOMPtr<nsINode> startNode;
  int32_t startOffset;
  nsresult rv = EditorBase::GetStartNodeAndOffset(aSelection,
                                                  getter_AddRefs(startNode),
                                                  &startOffset);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);

  // find any enclosing mailcite
  nsCOMPtr<Element> citeNode = GetTopEnclosingMailCite(*startNode);
  if (citeNode) {
    bool isEmpty = true, seenBR = false;
    NS_ENSURE_STATE(mHTMLEditor);
    mHTMLEditor->IsEmptyNodeImpl(citeNode, &isEmpty, true, true, false,
                                 &seenBR);
    if (isEmpty) {
      int32_t offset;
      nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(citeNode, &offset);
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->DeleteNode(citeNode);
      NS_ENSURE_SUCCESS(rv, rv);
      if (parent && seenBR) {
        NS_ENSURE_STATE(mHTMLEditor);
        nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(parent, offset);
        NS_ENSURE_STATE(brNode);
        aSelection->Collapse(parent, offset);
      }
    }
  }

  // call through to base class
  return TextEditRules::DidDeleteSelection(aSelection, aDir, aResult);
}

nsresult
HTMLEditRules::WillMakeList(Selection* aSelection,
                            const nsAString* aListType,
                            bool aEntireList,
                            const nsAString* aBulletType,
                            bool* aCancel,
                            bool* aHandled,
                            const nsAString* aItemType)
{
  if (!aSelection || !aListType || !aCancel || !aHandled) {
    return NS_ERROR_NULL_POINTER;
  }
  OwningNonNull<nsIAtom> listType = NS_Atomize(*aListType);

  WillInsert(*aSelection, aCancel);

  // initialize out param
  // we want to ignore result of WillInsert()
  *aCancel = false;
  *aHandled = false;

  // deduce what tag to use for list items
  nsCOMPtr<nsIAtom> itemType;
  if (aItemType) {
    itemType = NS_Atomize(*aItemType);
    NS_ENSURE_TRUE(itemType, NS_ERROR_OUT_OF_MEMORY);
  } else if (listType == nsGkAtoms::dl) {
    itemType = nsGkAtoms::dd;
  } else {
    itemType = nsGkAtoms::li;
  }

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

  *aHandled = true;

  nsresult rv = NormalizeSelection(aSelection);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_STATE(mHTMLEditor);
  AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);

  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
  rv = GetListActionNodes(arrayOfNodes,
                          aEntireList ? EntireList::yes : EntireList::no);
  NS_ENSURE_SUCCESS(rv, rv);

  // check if all our nodes are <br>s, or empty inlines
  bool bOnlyBreaks = true;
  for (auto& curNode : arrayOfNodes) {
    // if curNode is not a Break or empty inline, we're done
    if (!TextEditUtils::IsBreak(curNode) &&
        !IsEmptyInline(curNode)) {
      bOnlyBreaks = false;
      break;
    }
  }

  // if no nodes, we make empty list.  Ditto if the user tried to make a list
  // of some # of breaks.
  if (arrayOfNodes.IsEmpty() || bOnlyBreaks) {
    // if only breaks, delete them
    if (bOnlyBreaks) {
      for (auto& node : arrayOfNodes) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->DeleteNode(node);
        NS_ENSURE_SUCCESS(rv, rv);
      }
    }

    // get selection location
    NS_ENSURE_STATE(aSelection->RangeCount());
    nsCOMPtr<nsINode> container =
      aSelection->GetRangeAt(0)->GetStartContainer();
    int32_t offset = aSelection->GetRangeAt(0)->StartOffset();
    NS_ENSURE_STATE(container);

    // make sure we can put a list here
    NS_ENSURE_STATE(mHTMLEditor);
    if (!mHTMLEditor->CanContainTag(*container, listType)) {
      *aCancel = true;
      return NS_OK;
    }
    rv = SplitAsNeeded(listType, container, offset);
    NS_ENSURE_SUCCESS(rv, rv);
    NS_ENSURE_STATE(mHTMLEditor);
    nsCOMPtr<Element> theList =
      mHTMLEditor->CreateNode(listType, container, offset);
    NS_ENSURE_STATE(theList);

    NS_ENSURE_STATE(mHTMLEditor);
    nsCOMPtr<Element> theListItem =
      mHTMLEditor->CreateNode(itemType, theList, 0);
    NS_ENSURE_STATE(theListItem);

    // remember our new block for postprocessing
    mNewBlock = theListItem;
    // put selection in new list item
    *aHandled = true;
    rv = aSelection->Collapse(theListItem, 0);
    // Don't restore the selection
    selectionRestorer.Abort();
    return rv;
  }

  // if there is only one node in the array, and it is a list, div, or
  // blockquote, then look inside of it until we find inner list or content.

  LookInsideDivBQandList(arrayOfNodes);

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

  uint32_t listCount = arrayOfNodes.Length();
  nsCOMPtr<nsINode> curParent;
  nsCOMPtr<Element> curList, prevListItem;

  for (uint32_t i = 0; i < listCount; i++) {
    // here's where we actually figure out what to do
    nsCOMPtr<Element> newBlock;
    NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
    OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent();
    int32_t offset;
    curParent = EditorBase::GetNodeLocation(curNode, &offset);

    // make sure we don't assemble content that is in different table cells
    // into the same list.  respect table cell boundaries when listifying.
    if (curList && InDifferentTableElements(curList, curNode)) {
      curList = nullptr;
    }

    // If curNode is a break, delete it, and quit remembering prev list item.
    // If an empty inline container, delete it, but still remember the previous
    // item.
    NS_ENSURE_STATE(mHTMLEditor);
    if (mHTMLEditor->IsEditable(curNode) && (TextEditUtils::IsBreak(curNode) ||
                                             IsEmptyInline(curNode))) {
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->DeleteNode(curNode);
      NS_ENSURE_SUCCESS(rv, rv);
      if (TextEditUtils::IsBreak(curNode)) {
        prevListItem = nullptr;
      }
      continue;
    }

    if (HTMLEditUtils::IsList(curNode)) {
      // do we have a curList already?
      if (curList && !EditorUtils::IsDescendantOf(curNode, curList)) {
        // move all of our children into curList.  cheezy way to do it: move
        // whole list and then RemoveContainer() on the list.  ConvertListType
        // first: that routine handles converting the list item types, if
        // needed
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(curNode, curList, -1);
        NS_ENSURE_SUCCESS(rv, rv);
        rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock),
                             listType, itemType);
        NS_ENSURE_SUCCESS(rv, rv);
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->RemoveBlockContainer(*newBlock);
        NS_ENSURE_SUCCESS(rv, rv);
      } else {
        // replace list with new list type
        rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock),
                             listType, itemType);
        NS_ENSURE_SUCCESS(rv, rv);
        curList = newBlock;
      }
      prevListItem = nullptr;
      continue;
    }

    if (HTMLEditUtils::IsListItem(curNode)) {
      NS_ENSURE_STATE(mHTMLEditor);
      if (!curParent->IsHTMLElement(listType)) {
        // list item is in wrong type of list. if we don't have a curList,
        // split the old list and make a new list of correct type.
        if (!curList || EditorUtils::IsDescendantOf(curNode, curList)) {
          NS_ENSURE_STATE(mHTMLEditor);
          NS_ENSURE_STATE(curParent->IsContent());
          ErrorResult rv;
          nsCOMPtr<nsIContent> splitNode =
            mHTMLEditor->SplitNode(*curParent->AsContent(), offset, rv);
          NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
          newBlock = splitNode ? splitNode->AsElement() : nullptr;
          int32_t offset;
          nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(curParent,
                                                                 &offset);
          NS_ENSURE_STATE(mHTMLEditor);
          curList = mHTMLEditor->CreateNode(listType, parent, offset);
          NS_ENSURE_STATE(curList);
        }
        // move list item to new list
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(curNode, curList, -1);
        NS_ENSURE_SUCCESS(rv, rv);
        // convert list item type if needed
        NS_ENSURE_STATE(mHTMLEditor);
        if (!curNode->IsHTMLElement(itemType)) {
          NS_ENSURE_STATE(mHTMLEditor);
          newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(),
                                                   itemType);
          NS_ENSURE_STATE(newBlock);
        }
      } else {
        // item is in right type of list.  But we might still have to move it.
        // and we might need to convert list item types.
        if (!curList) {
          curList = curParent->AsElement();
        } else if (curParent != curList) {
          // move list item to new list
          NS_ENSURE_STATE(mHTMLEditor);
          rv = mHTMLEditor->MoveNode(curNode, curList, -1);
          NS_ENSURE_SUCCESS(rv, rv);
        }
        NS_ENSURE_STATE(mHTMLEditor);
        if (!curNode->IsHTMLElement(itemType)) {
          NS_ENSURE_STATE(mHTMLEditor);
          newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(),
                                                   itemType);
          NS_ENSURE_STATE(newBlock);
        }
      }
      NS_ENSURE_STATE(mHTMLEditor);
      nsCOMPtr<Element> curElement = do_QueryInterface(curNode);
      if (aBulletType && !aBulletType->IsEmpty()) {
        rv = mHTMLEditor->SetAttribute(curElement, nsGkAtoms::type,
                                       *aBulletType);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }
      } else {
        rv = mHTMLEditor->RemoveAttribute(curElement, nsGkAtoms::type);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }
      }
      continue;
    }

    // if we hit a div clear our prevListItem, insert divs contents
    // into our node array, and remove the div
    if (curNode->IsHTMLElement(nsGkAtoms::div)) {
      prevListItem = nullptr;
      int32_t j = i + 1;
      GetInnerContent(*curNode, arrayOfNodes, &j);
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->RemoveContainer(curNode);
      NS_ENSURE_SUCCESS(rv, rv);
      listCount = arrayOfNodes.Length();
      continue;
    }

    // need to make a list to put things in if we haven't already,
    if (!curList) {
      rv = SplitAsNeeded(listType, curParent, offset);
      NS_ENSURE_SUCCESS(rv, rv);
      NS_ENSURE_STATE(mHTMLEditor);
      curList = mHTMLEditor->CreateNode(listType, curParent, offset);
      // remember our new block for postprocessing
      mNewBlock = curList;
      // curList is now the correct thing to put curNode in
      prevListItem = nullptr;
    }

    // if curNode isn't a list item, we must wrap it in one
    nsCOMPtr<Element> listItem;
    if (!HTMLEditUtils::IsListItem(curNode)) {
      if (IsInlineNode(curNode) && prevListItem) {
        // this is a continuation of some inline nodes that belong together in
        // the same list item.  use prevListItem
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(curNode, prevListItem, -1);
        NS_ENSURE_SUCCESS(rv, rv);
      } else {
        // don't wrap li around a paragraph.  instead replace paragraph with li
        if (curNode->IsHTMLElement(nsGkAtoms::p)) {
          NS_ENSURE_STATE(mHTMLEditor);
          listItem = mHTMLEditor->ReplaceContainer(curNode->AsElement(),
                                                   itemType);
          NS_ENSURE_STATE(listItem);
        } else {
          NS_ENSURE_STATE(mHTMLEditor);
          listItem = mHTMLEditor->InsertContainerAbove(curNode, itemType);
          NS_ENSURE_STATE(listItem);
        }
        if (IsInlineNode(curNode)) {
          prevListItem = listItem;
        } else {
          prevListItem = nullptr;
        }
      }
    } else {
      listItem = curNode->AsElement();
    }

    if (listItem) {
      // if we made a new list item, deal with it: tuck the listItem into the
      // end of the active list
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->MoveNode(listItem, curList, -1);
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  return NS_OK;
}


nsresult
HTMLEditRules::WillRemoveList(Selection* aSelection,
                              bool aOrdered,
                              bool* aCancel,
                              bool* aHandled)
{
  if (!aSelection || !aCancel || !aHandled) {
    return NS_ERROR_NULL_POINTER;
  }
  // initialize out param
  *aCancel = false;
  *aHandled = true;

  nsresult rv = NormalizeSelection(aSelection);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_STATE(mHTMLEditor);
  AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);

  nsTArray<RefPtr<nsRange>> arrayOfRanges;
  GetPromotedRanges(*aSelection, arrayOfRanges, EditAction::makeList);

  // use these ranges to contruct a list of nodes to act on.
  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
  rv = GetListActionNodes(arrayOfNodes, EntireList::no);
  NS_ENSURE_SUCCESS(rv, rv);

  // Remove all non-editable nodes.  Leave them be.
  for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
    OwningNonNull<nsINode> testNode = arrayOfNodes[i];
    NS_ENSURE_STATE(mHTMLEditor);
    if (!mHTMLEditor->IsEditable(testNode)) {
      arrayOfNodes.RemoveElementAt(i);
    }
  }

  // Only act on lists or list items in the array
  for (auto& curNode : arrayOfNodes) {
    // here's where we actually figure out what to do
    if (HTMLEditUtils::IsListItem(curNode)) {
      // unlist this listitem
      bool bOutOfList;
      do {
        rv = PopListItem(*curNode->AsContent(), &bOutOfList);
        NS_ENSURE_SUCCESS(rv, rv);
      } while (!bOutOfList); // keep popping it out until it's not in a list anymore
    } else if (HTMLEditUtils::IsList(curNode)) {
      // node is a list, move list items out
      rv = RemoveListStructure(*curNode->AsElement());
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }
  return NS_OK;
}

nsresult
HTMLEditRules::WillMakeDefListItem(Selection* aSelection,
                                   const nsAString *aItemType,
                                   bool aEntireList,
                                   bool* aCancel,
                                   bool* aHandled)
{
  // for now we let WillMakeList handle this
  NS_NAMED_LITERAL_STRING(listType, "dl");
  return WillMakeList(aSelection, &listType.AsString(), aEntireList, nullptr,
                      aCancel, aHandled, aItemType);
}

nsresult
HTMLEditRules::WillMakeBasicBlock(Selection& aSelection,
                                  const nsAString& aBlockType,
                                  bool* aCancel,
                                  bool* aHandled)
{
  MOZ_ASSERT(aCancel && aHandled);

  OwningNonNull<nsIAtom> blockType = NS_Atomize(aBlockType);

  WillInsert(aSelection, aCancel);
  // We want to ignore result of WillInsert()
  *aCancel = false;
  *aHandled = true;

  nsresult rv = MakeBasicBlock(aSelection, blockType);
  Unused << NS_WARN_IF(NS_FAILED(rv));
  return rv;
}

nsresult
HTMLEditRules::MakeBasicBlock(Selection& aSelection, nsIAtom& blockType)
{
  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  nsresult rv = NormalizeSelection(&aSelection);
  NS_ENSURE_SUCCESS(rv, rv);
  AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);
  AutoTransactionsConserveSelection dontChangeMySelection(htmlEditor);

  // Contruct a list of nodes to act on.
  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
  rv = GetNodesFromSelection(aSelection, EditAction::makeBasicBlock,
                             arrayOfNodes);
  NS_ENSURE_SUCCESS(rv, rv);

  // If nothing visible in list, make an empty block
  if (ListIsEmptyLine(arrayOfNodes)) {
    // Get selection location
    NS_ENSURE_STATE(aSelection.GetRangeAt(0) &&
                    aSelection.GetRangeAt(0)->GetStartContainer());
    OwningNonNull<nsINode> container =
      *aSelection.GetRangeAt(0)->GetStartContainer();
    int32_t offset = aSelection.GetRangeAt(0)->StartOffset();

    if (&blockType == nsGkAtoms::normal ||
        &blockType == nsGkAtoms::_empty) {
      // We are removing blocks (going to "body text")
      NS_ENSURE_TRUE(htmlEditor->GetBlock(container), NS_ERROR_NULL_POINTER);
      OwningNonNull<Element> curBlock = *htmlEditor->GetBlock(container);
      if (HTMLEditUtils::IsFormatNode(curBlock)) {
        // If the first editable node after selection is a br, consume it.
        // Otherwise it gets pushed into a following block after the split,
        // which is visually bad.
        nsCOMPtr<nsIContent> brNode =
          htmlEditor->GetNextHTMLNode(container, offset);
        if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) {
          rv = htmlEditor->DeleteNode(brNode);
          NS_ENSURE_SUCCESS(rv, rv);
        }
        // Do the splits!
        offset = htmlEditor->SplitNodeDeep(curBlock, *container->AsContent(),
                                            offset,
                                            HTMLEditor::EmptyContainers::no);
        NS_ENSURE_STATE(offset != -1);
        // Put a br at the split point
        brNode = htmlEditor->CreateBR(curBlock->GetParentNode(), offset);
        NS_ENSURE_STATE(brNode);
        // Put selection at the split point
        rv = aSelection.Collapse(curBlock->GetParentNode(), offset);
        // Don't restore the selection
        selectionRestorer.Abort();
        NS_ENSURE_SUCCESS(rv, rv);
      }
      // Else nothing to do!
    } else {
      // We are making a block.  Consume a br, if needed.
      nsCOMPtr<nsIContent> brNode =
        htmlEditor->GetNextHTMLNode(container, offset, true);
      if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) {
        rv = htmlEditor->DeleteNode(brNode);
        NS_ENSURE_SUCCESS(rv, rv);
        // We don't need to act on this node any more
        arrayOfNodes.RemoveElement(brNode);
      }
      // Make sure we can put a block here
      rv = SplitAsNeeded(blockType, container, offset);
      NS_ENSURE_SUCCESS(rv, rv);
      nsCOMPtr<Element> block =
        htmlEditor->CreateNode(&blockType, container, offset);
      NS_ENSURE_STATE(block);
      // Remember our new block for postprocessing
      mNewBlock = block;
      // Delete anything that was in the list of nodes
      while (!arrayOfNodes.IsEmpty()) {
        OwningNonNull<nsINode> curNode = arrayOfNodes[0];
        rv = htmlEditor->DeleteNode(curNode);
        NS_ENSURE_SUCCESS(rv, rv);
        arrayOfNodes.RemoveElementAt(0);
      }
      // Put selection in new block
      rv = aSelection.Collapse(block, 0);
      // Don't restore the selection
      selectionRestorer.Abort();
      NS_ENSURE_SUCCESS(rv, rv);
    }
    return NS_OK;
  }
  // Okay, now go through all the nodes and make the right kind of blocks, or
  // whatever is approriate.  Woohoo!  Note: blockquote is handled a little
  // differently.
  if (&blockType == nsGkAtoms::blockquote) {
    rv = MakeBlockquote(arrayOfNodes);
    NS_ENSURE_SUCCESS(rv, rv);
  } else if (&blockType == nsGkAtoms::normal ||
             &blockType == nsGkAtoms::_empty) {
    rv = RemoveBlockStyle(arrayOfNodes);
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    rv = ApplyBlockStyle(arrayOfNodes, blockType);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  return NS_OK;
}

nsresult
HTMLEditRules::DidMakeBasicBlock(Selection* aSelection,
                                 RulesInfo* aInfo,
                                 nsresult aResult)
{
  NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
  // check for empty block.  if so, put a moz br in it.
  if (!aSelection->Collapsed()) {
    return NS_OK;
  }

  NS_ENSURE_STATE(aSelection->GetRangeAt(0) &&
                  aSelection->GetRangeAt(0)->GetStartContainer());
  nsresult rv =
    InsertMozBRIfNeeded(*aSelection->GetRangeAt(0)->GetStartContainer());
  NS_ENSURE_SUCCESS(rv, rv);
  return NS_OK;
}

nsresult
HTMLEditRules::WillIndent(Selection* aSelection,
                          bool* aCancel,
                          bool* aHandled)
{
  NS_ENSURE_STATE(mHTMLEditor);
  if (mHTMLEditor->IsCSSEnabled()) {
    nsresult rv = WillCSSIndent(aSelection, aCancel, aHandled);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  } else {
    nsresult rv = WillHTMLIndent(aSelection, aCancel, aHandled);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }
  return NS_OK;
}

nsresult
HTMLEditRules::WillCSSIndent(Selection* aSelection,
                             bool* aCancel,
                             bool* aHandled)
{
  if (!aSelection || !aCancel || !aHandled) {
    return NS_ERROR_NULL_POINTER;
  }

  WillInsert(*aSelection, aCancel);

  // initialize out param
  // we want to ignore result of WillInsert()
  *aCancel = false;
  *aHandled = true;

  nsresult rv = NormalizeSelection(aSelection);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_STATE(mHTMLEditor);
  AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
  nsTArray<OwningNonNull<nsRange>> arrayOfRanges;
  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;

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

  nsCOMPtr<Element> liNode;
  if (aSelection->Collapsed()) {
    nsCOMPtr<nsINode> node;
    int32_t offset;
    nsresult rv =
      EditorBase::GetStartNodeAndOffset(aSelection,
                                        getter_AddRefs(node), &offset);
    NS_ENSURE_SUCCESS(rv, rv);
    NS_ENSURE_STATE(mHTMLEditor);
    nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*node);
    if (block && HTMLEditUtils::IsListItem(block)) {
      liNode = block;
    }
  }

  if (liNode) {
    arrayOfNodes.AppendElement(*liNode);
  } else {
    // convert the selection ranges into "promoted" selection ranges:
    // this basically just expands the range to include the immediate
    // block parent, and then further expands to include any ancestors
    // whose children are all in the range
    rv = GetNodesFromSelection(*aSelection, EditAction::indent, arrayOfNodes);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // if nothing visible in list, make an empty block
  if (ListIsEmptyLine(arrayOfNodes)) {
    // get selection location
    NS_ENSURE_STATE(aSelection->RangeCount());
    nsCOMPtr<nsINode> container = aSelection->GetRangeAt(0)->GetStartContainer();
    int32_t offset = aSelection->GetRangeAt(0)->StartOffset();
    NS_ENSURE_STATE(container);

    // make sure we can put a block here
    rv = SplitAsNeeded(*nsGkAtoms::div, container, offset);
    NS_ENSURE_SUCCESS(rv, rv);
    NS_ENSURE_STATE(mHTMLEditor);
    nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::div,
                                                         container, offset);
    NS_ENSURE_STATE(theBlock);
    // remember our new block for postprocessing
    mNewBlock = theBlock;
    ChangeIndentation(*theBlock, Change::plus);
    // delete anything that was in the list of nodes
    while (!arrayOfNodes.IsEmpty()) {
      OwningNonNull<nsINode> curNode = arrayOfNodes[0];
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->DeleteNode(curNode);
      NS_ENSURE_SUCCESS(rv, rv);
      arrayOfNodes.RemoveElementAt(0);
    }
    // put selection in new block
    *aHandled = true;
    rv = aSelection->Collapse(theBlock, 0);
    // Don't restore the selection
    selectionRestorer.Abort();
    return rv;
  }

  // Ok, now go through all the nodes and put them in a blockquote,
  // or whatever is appropriate.  Wohoo!
  nsCOMPtr<nsINode> curParent;
  nsCOMPtr<Element> curList, curQuote;
  nsCOMPtr<nsIContent> sibling;
  int32_t listCount = arrayOfNodes.Length();
  for (int32_t i = 0; i < listCount; i++) {
    // here's where we actually figure out what to do
    NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
    nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent();

    // Ignore all non-editable nodes.  Leave them be.
    NS_ENSURE_STATE(mHTMLEditor);
    if (!mHTMLEditor->IsEditable(curNode)) {
      continue;
    }

    curParent = curNode->GetParentNode();
    int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;

    // some logic for putting list items into nested lists...
    if (HTMLEditUtils::IsList(curParent)) {
      sibling = nullptr;

      // Check for whether we should join a list that follows curNode.
      // We do this if the next element is a list, and the list is of the
      // same type (li/ol) as curNode was a part it.
      NS_ENSURE_STATE(mHTMLEditor);
      sibling = mHTMLEditor->GetNextHTMLSibling(curNode);
      if (sibling && HTMLEditUtils::IsList(sibling) &&
          curParent->NodeInfo()->NameAtom() ==
            sibling->NodeInfo()->NameAtom() &&
          curParent->NodeInfo()->NamespaceID() ==
            sibling->NodeInfo()->NamespaceID()) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(curNode, sibling, 0);
        NS_ENSURE_SUCCESS(rv, rv);
        continue;
      }
      // Check for whether we should join a list that preceeds curNode.
      // We do this if the previous element is a list, and the list is of
      // the same type (li/ol) as curNode was a part of.
      NS_ENSURE_STATE(mHTMLEditor);
      sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
      if (sibling && HTMLEditUtils::IsList(sibling) &&
          curParent->NodeInfo()->NameAtom() ==
            sibling->NodeInfo()->NameAtom() &&
          curParent->NodeInfo()->NamespaceID() ==
            sibling->NodeInfo()->NamespaceID()) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(curNode, sibling, -1);
        NS_ENSURE_SUCCESS(rv, rv);
        continue;
      }
      sibling = nullptr;

      // check to see if curList is still appropriate.  Which it is if
      // curNode is still right after it in the same list.
      if (curList) {
        NS_ENSURE_STATE(mHTMLEditor);
        sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
      }

      if (!curList || (sibling && sibling != curList)) {
        // create a new nested list of correct type
        rv =
          SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset);
        NS_ENSURE_SUCCESS(rv, rv);
        NS_ENSURE_STATE(mHTMLEditor);
        curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
                                          curParent, offset);
        NS_ENSURE_STATE(curList);
        // curList is now the correct thing to put curNode in
        // remember our new block for postprocessing
        mNewBlock = curList;
      }
      // tuck the node into the end of the active list
      uint32_t listLen = curList->Length();
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->MoveNode(curNode, curList, listLen);
      NS_ENSURE_SUCCESS(rv, rv);
    }
    // Not a list item.
    else {
      if (curNode && IsBlockNode(*curNode)) {
        ChangeIndentation(*curNode->AsElement(), Change::plus);
        curQuote = nullptr;
      } else {
        if (!curQuote) {
          // First, check that our element can contain a div.
          if (NS_WARN_IF(!mHTMLEditor)) {
            return NS_ERROR_UNEXPECTED;
          }
          if (!mHTMLEditor->CanContainTag(*curParent, *nsGkAtoms::div)) {
            return NS_OK; // cancelled
          }

          rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset);
          NS_ENSURE_SUCCESS(rv, rv);
          NS_ENSURE_STATE(mHTMLEditor);
          curQuote = mHTMLEditor->CreateNode(nsGkAtoms::div, curParent,
                                             offset);
          NS_ENSURE_STATE(curQuote);
          ChangeIndentation(*curQuote, Change::plus);
          // remember our new block for postprocessing
          mNewBlock = curQuote;
          // curQuote is now the correct thing to put curNode in
        }

        // tuck the node into the end of the active blockquote
        uint32_t quoteLen = curQuote->Length();
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(curNode, curQuote, quoteLen);
        NS_ENSURE_SUCCESS(rv, rv);
      }
    }
  }
  return NS_OK;
}

nsresult
HTMLEditRules::WillHTMLIndent(Selection* aSelection,
                              bool* aCancel,
                              bool* aHandled)
{
  if (!aSelection || !aCancel || !aHandled) {
    return NS_ERROR_NULL_POINTER;
  }
  WillInsert(*aSelection, aCancel);

  // initialize out param
  // we want to ignore result of WillInsert()
  *aCancel = false;
  *aHandled = true;

  nsresult rv = NormalizeSelection(aSelection);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_STATE(mHTMLEditor);
  AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);

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

  nsTArray<RefPtr<nsRange>> arrayOfRanges;
  GetPromotedRanges(*aSelection, arrayOfRanges, EditAction::indent);

  // use these ranges to contruct a list of nodes to act on.
  nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
  rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes, EditAction::indent);
  NS_ENSURE_SUCCESS(rv, rv);

  // if nothing visible in list, make an empty block
  if (ListIsEmptyLine(arrayOfNodes)) {
    // get selection location
    NS_ENSURE_STATE(aSelection->RangeCount());
    nsCOMPtr<nsINode> container =
      aSelection->GetRangeAt(0)->GetStartContainer();
    int32_t offset = aSelection->GetRangeAt(0)->StartOffset();
    NS_ENSURE_STATE(container);

    // make sure we can put a block here
    rv = SplitAsNeeded(*nsGkAtoms::blockquote, container, offset);
    NS_ENSURE_SUCCESS(rv, rv);
    NS_ENSURE_STATE(mHTMLEditor);
    nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::blockquote,
                                                         container, offset);
    NS_ENSURE_STATE(theBlock);
    // remember our new block for postprocessing
    mNewBlock = theBlock;
    // delete anything that was in the list of nodes
    while (!arrayOfNodes.IsEmpty()) {
      OwningNonNull<nsINode> curNode = arrayOfNodes[0];
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->DeleteNode(curNode);
      NS_ENSURE_SUCCESS(rv, rv);
      arrayOfNodes.RemoveElementAt(0);
    }
    // put selection in new block
    *aHandled = true;
    rv = aSelection->Collapse(theBlock, 0);
    // Don't restore the selection
    selectionRestorer.Abort();
    return rv;
  }

  // Ok, now go through all the nodes and put them in a blockquote,
  // or whatever is appropriate.  Wohoo!
  nsCOMPtr<nsINode> curParent;
  nsCOMPtr<nsIContent> sibling;
  nsCOMPtr<Element> curList, curQuote, indentedLI;
  int32_t listCount = arrayOfNodes.Length();
  for (int32_t i = 0; i < listCount; i++) {
    // here's where we actually figure out what to do
    NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
    nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent();

    // Ignore all non-editable nodes.  Leave them be.
    NS_ENSURE_STATE(mHTMLEditor);
    if (!mHTMLEditor->IsEditable(curNode)) {
      continue;
    }

    curParent = curNode->GetParentNode();
    int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;

    // some logic for putting list items into nested lists...
    if (HTMLEditUtils::IsList(curParent)) {
      sibling = nullptr;

      // Check for whether we should join a list that follows curNode.
      // We do this if the next element is a list, and the list is of the
      // same type (li/ol) as curNode was a part it.
      NS_ENSURE_STATE(mHTMLEditor);
      sibling = mHTMLEditor->GetNextHTMLSibling(curNode);
      if (sibling && HTMLEditUtils::IsList(sibling) &&
          curParent->NodeInfo()->NameAtom() ==
            sibling->NodeInfo()->NameAtom() &&
          curParent->NodeInfo()->NamespaceID() ==
            sibling->NodeInfo()->NamespaceID()) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(curNode, sibling, 0);
        NS_ENSURE_SUCCESS(rv, rv);
        continue;
      }

      // Check for whether we should join a list that preceeds curNode.
      // We do this if the previous element is a list, and the list is of
      // the same type (li/ol) as curNode was a part of.
      NS_ENSURE_STATE(mHTMLEditor);
      sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
      if (sibling && HTMLEditUtils::IsList(sibling) &&
          curParent->NodeInfo()->NameAtom() ==
            sibling->NodeInfo()->NameAtom() &&
          curParent->NodeInfo()->NamespaceID() ==
            sibling->NodeInfo()->NamespaceID()) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(curNode, sibling, -1);
        NS_ENSURE_SUCCESS(rv, rv);
        continue;
      }

      sibling = nullptr;

      // check to see if curList is still appropriate.  Which it is if
      // curNode is still right after it in the same list.
      if (curList) {
        NS_ENSURE_STATE(mHTMLEditor);
        sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
      }

      if (!curList || (sibling && sibling != curList)) {
        // create a new nested list of correct type
        rv =
          SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset);
        NS_ENSURE_SUCCESS(rv, rv);
        NS_ENSURE_STATE(mHTMLEditor);
        curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
                                          curParent, offset);
        NS_ENSURE_STATE(curList);
        // curList is now the correct thing to put curNode in
        // remember our new block for postprocessing
        mNewBlock = curList;
      }
      // tuck the node into the end of the active list
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->MoveNode(curNode, curList, -1);
      NS_ENSURE_SUCCESS(rv, rv);
      // forget curQuote, if any
      curQuote = nullptr;
    }
    // Not a list item, use blockquote?
    else {
      // if we are inside a list item, we don't want to blockquote, we want
      // to sublist the list item.  We may have several nodes listed in the
      // array of nodes to act on, that are in the same list item.  Since
      // we only want to indent that li once, we must keep track of the most
      // recent indented list item, and not indent it if we find another node
      // to act on that is still inside the same li.
      nsCOMPtr<Element> listItem = IsInListItem(curNode);
      if (listItem) {
        if (indentedLI == listItem) {
          // already indented this list item
          continue;
        }
        curParent = listItem->GetParentNode();
        offset = curParent ? curParent->IndexOf(listItem) : -1;
        // check to see if curList is still appropriate.  Which it is if
        // curNode is still right after it in the same list.
        if (curList) {
          sibling = nullptr;
          NS_ENSURE_STATE(mHTMLEditor);
          sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
        }

        if (!curList || (sibling && sibling != curList)) {
          // create a new nested list of correct type
          rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent,
                             offset);
          NS_ENSURE_SUCCESS(rv, rv);
          NS_ENSURE_STATE(mHTMLEditor);
          curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
                                            curParent, offset);
          NS_ENSURE_STATE(curList);
        }
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(listItem, curList, -1);
        NS_ENSURE_SUCCESS(rv, rv);
        // remember we indented this li
        indentedLI = listItem;
      } else {
        // need to make a blockquote to put things in if we haven't already,
        // or if this node doesn't go in blockquote we used earlier.
        // One reason it might not go in prio blockquote is if we are now
        // in a different table cell.
        if (curQuote && InDifferentTableElements(curQuote, curNode)) {
          curQuote = nullptr;
        }

        if (!curQuote) {
          // First, check that our element can contain a blockquote.
          if (NS_WARN_IF(!mHTMLEditor)) {
            return NS_ERROR_UNEXPECTED;
          }
          if (!mHTMLEditor->CanContainTag(*curParent, *nsGkAtoms::blockquote)) {
            return NS_OK; // cancelled
          }

          rv = SplitAsNeeded(*nsGkAtoms::blockquote, curParent, offset);
          NS_ENSURE_SUCCESS(rv, rv);
          NS_ENSURE_STATE(mHTMLEditor);
          curQuote = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, curParent,
                                             offset);
          NS_ENSURE_STATE(curQuote);
          // remember our new block for postprocessing
          mNewBlock = curQuote;
          // curQuote is now the correct thing to put curNode in
        }

        // tuck the node into the end of the active blockquote
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->MoveNode(curNode, curQuote, -1);
        NS_ENSURE_SUCCESS(rv, rv);
        // forget curList, if any
        curList = nullptr;
      }
    }
  }
  return NS_OK;
}


nsresult
HTMLEditRules::WillOutdent(Selection& aSelection,
                           bool* aCancel,
                           bool* aHandled)
{
  MOZ_ASSERT(aCancel && aHandled);
  *aCancel = false;
  *aHandled = true;
  nsCOMPtr<nsIContent> rememberedLeftBQ, rememberedRightBQ;
  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
  bool useCSS = htmlEditor->IsCSSEnabled();

  nsresult rv = NormalizeSelection(&aSelection);
  NS_ENSURE_SUCCESS(rv, rv);

  // Some scoping for selection resetting - we may need to tweak it
  {
    AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);

    // Convert the selection ranges into "promoted" selection ranges: this
    // basically just expands the range to include the immediate block parent,
    // and then further expands to include any ancestors whose children are all
    // in the range
    nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
    rv = GetNodesFromSelection(aSelection, EditAction::outdent, arrayOfNodes);
    NS_ENSURE_SUCCESS(rv, rv);

    // Okay, now go through all the nodes and remove a level of blockquoting,
    // or whatever is appropriate.  Wohoo!

    nsCOMPtr<Element> curBlockQuote;
    nsCOMPtr<nsIContent> firstBQChild, lastBQChild;
    bool curBlockQuoteIsIndentedWithCSS = false;
    for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) {
      if (!arrayOfNodes[i]->IsContent()) {
        continue;
      }
      OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent();

      // Here's where we actually figure out what to do
      nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
      int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;

      // Is it a blockquote?
      if (curNode->IsHTMLElement(nsGkAtoms::blockquote)) {
        // If it is a blockquote, remove it.  So we need to finish up dealng
        // with any curBlockQuote first.
        if (curBlockQuote) {
          rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
                                  curBlockQuoteIsIndentedWithCSS,
                                  getter_AddRefs(rememberedLeftBQ),
                                  getter_AddRefs(rememberedRightBQ));
          NS_ENSURE_SUCCESS(rv, rv);
          curBlockQuote = nullptr;
          firstBQChild = nullptr;
          lastBQChild = nullptr;
          curBlockQuoteIsIndentedWithCSS = false;
        }
        rv = htmlEditor->RemoveBlockContainer(curNode);
        NS_ENSURE_SUCCESS(rv, rv);
        continue;
      }
      // Is it a block with a 'margin' property?
      if (useCSS && IsBlockNode(curNode)) {
        nsIAtom& marginProperty =
          MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode);
        nsAutoString value;
        htmlEditor->mCSSEditUtils->GetSpecifiedProperty(curNode,
                                                         marginProperty,
                                                         value);
        float f;
        nsCOMPtr<nsIAtom> unit;
        NS_ENSURE_STATE(htmlEditor);
        htmlEditor->mCSSEditUtils->ParseLength(value, &f,
                                               getter_AddRefs(unit));
        if (f > 0) {
          ChangeIndentation(*curNode->AsElement(), Change::minus);
          continue;
        }
      }
      // Is it a list item?
      if (HTMLEditUtils::IsListItem(curNode)) {
        // If it is a list item, that means we are not outdenting whole list.
        // So we need to finish up dealing with any curBlockQuote, and then pop
        // this list item.
        if (curBlockQuote) {
          rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
                                  curBlockQuoteIsIndentedWithCSS,
                                  getter_AddRefs(rememberedLeftBQ),
                                  getter_AddRefs(rememberedRightBQ));
          NS_ENSURE_SUCCESS(rv, rv);
          curBlockQuote = nullptr;
          firstBQChild = nullptr;
          lastBQChild = nullptr;
          curBlockQuoteIsIndentedWithCSS = false;
        }
        rv = PopListItem(*curNode->AsContent());
        NS_ENSURE_SUCCESS(rv, rv);
        continue;
      }
      // Do we have a blockquote that we are already committed to removing?
      if (curBlockQuote) {
        // If so, is this node a descendant?
        if (EditorUtils::IsDescendantOf(curNode, curBlockQuote)) {
          lastBQChild = curNode;
          // Then we don't need to do anything different for this node
          continue;
        }
        // Otherwise, we have progressed beyond end of curBlockQuote, so
        // let's handle it now.  We need to remove the portion of
        // curBlockQuote that contains [firstBQChild - lastBQChild].
        rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
                                curBlockQuoteIsIndentedWithCSS,
                                getter_AddRefs(rememberedLeftBQ),
                                getter_AddRefs(rememberedRightBQ));
        NS_ENSURE_SUCCESS(rv, rv);
        curBlockQuote = nullptr;
        firstBQChild = nullptr;
        lastBQChild = nullptr;
        curBlockQuoteIsIndentedWithCSS = false;
        // Fall out and handle curNode
      }

      // Are we inside a blockquote?
      OwningNonNull<nsINode> n = curNode;
      curBlockQuoteIsIndentedWithCSS = false;
      // Keep looking up the hierarchy as long as we don't hit the body or the
      // active editing host or a table element (other than an entire table)
      while (!n->IsHTMLElement(nsGkAtoms::body) && 
             htmlEditor->IsDescendantOfEditorRoot(n) &&
             (n->IsHTMLElement(nsGkAtoms::table) ||
              !HTMLEditUtils::IsTableElement(n))) {
        if (!n->GetParentNode()) {
          break;
        }
        n = *n->GetParentNode();
        if (n->IsHTMLElement(nsGkAtoms::blockquote)) {
          // If so, remember it and the first node we are taking out of it.
          curBlockQuote = n->AsElement();
          firstBQChild = curNode;
          lastBQChild = curNode;
          break;
        } else if (useCSS) {
          nsIAtom& marginProperty =
            MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode);
          nsAutoString value;
          htmlEditor->mCSSEditUtils->GetSpecifiedProperty(*n, marginProperty,
                                                           value);
          float f;
          nsCOMPtr<nsIAtom> unit;
          htmlEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit));
          if (f > 0 && !(HTMLEditUtils::IsList(curParent) &&
                         HTMLEditUtils::IsList(curNode))) {
            curBlockQuote = n->AsElement();
            firstBQChild = curNode;
            lastBQChild = curNode;
            curBlockQuoteIsIndentedWithCSS = true;
            break;
          }
        }
      }

      if (!curBlockQuote) {
        // Couldn't find enclosing blockquote.  Handle list cases.
        if (HTMLEditUtils::IsList(curParent)) {
          // Move node out of list
          if (HTMLEditUtils::IsList(curNode)) {
            // Just unwrap this sublist
            rv = htmlEditor->RemoveBlockContainer(curNode);
            NS_ENSURE_SUCCESS(rv, rv);
          }
          // handled list item case above
        } else if (HTMLEditUtils::IsList(curNode)) {
          // node is a list, but parent is non-list: move list items out
          nsCOMPtr<nsIContent> child = curNode->GetLastChild();
          while (child) {
            if (HTMLEditUtils::IsListItem(child)) {
              rv = PopListItem(*child);
              NS_ENSURE_SUCCESS(rv, rv);
            } else if (HTMLEditUtils::IsList(child)) {
              // We have an embedded list, so move it out from under the parent
              // list. Be sure to put it after the parent list because this
              // loop iterates backwards through the parent's list of children.

              rv = htmlEditor->MoveNode(child, curParent, offset + 1);
              NS_ENSURE_SUCCESS(rv, rv);
            } else {
              // Delete any non-list items for now
              rv = htmlEditor->DeleteNode(child);
              NS_ENSURE_SUCCESS(rv, rv);
            }
            child = curNode->GetLastChild();
          }
          // Delete the now-empty list
          rv = htmlEditor->RemoveBlockContainer(curNode);
          NS_ENSURE_SUCCESS(rv, rv);
        } else if (useCSS) {
          nsCOMPtr<Element> element;
          if (curNode->GetAsText()) {
            // We want to outdent the parent of text nodes
            element = curNode->GetParentElement();
          } else if (curNode->IsElement()) {
            element = curNode->AsElement();
          }
          if (element) {
            ChangeIndentation(*element, Change::minus);
          }
        }
      }
    }
    if (curBlockQuote) {
      // We have a blockquote we haven't finished handling
      rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
                              curBlockQuoteIsIndentedWithCSS,
                              getter_AddRefs(rememberedLeftBQ),
                              getter_AddRefs(rememberedRightBQ));
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }
  // Make sure selection didn't stick to last piece of content in old bq (only
  // a problem for collapsed selections)
  if (rememberedLeftBQ || rememberedRightBQ) {
    if (aSelection.Collapsed()) {
      // Push selection past end of rememberedLeftBQ
      NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_OK);
      nsCOMPtr<nsINode> startNode =
        aSelection.GetRangeAt(0)->GetStartContainer();
      int32_t startOffset = aSelection.GetRangeAt(0)->StartOffset();
      if (rememberedLeftBQ &&
          (startNode == rememberedLeftBQ ||
           EditorUtils::IsDescendantOf(startNode, rememberedLeftBQ))) {
        // Selection is inside rememberedLeftBQ - push it past it.
        startNode = rememberedLeftBQ->GetParentNode();
        startOffset = startNode ? 1 + startNode->IndexOf(rememberedLeftBQ) : 0;
        aSelection.Collapse(startNode, startOffset);
      }
      // And pull selection before beginning of rememberedRightBQ
      startNode = aSelection.GetRangeAt(0)->GetStartContainer();
      startOffset = aSelection.GetRangeAt(0)->StartOffset();
      if (rememberedRightBQ &&
          (startNode == rememberedRightBQ ||
           EditorUtils::IsDescendantOf(startNode, rememberedRightBQ))) {
        // Selection is inside rememberedRightBQ - push it before it.
        startNode = rememberedRightBQ->GetParentNode();
        startOffset = startNode ? startNode->IndexOf(rememberedRightBQ) : -1;
        aSelection.Collapse(startNode, startOffset);
      }
    }
    return NS_OK;
  }
  return NS_OK;
}


/**
 * RemovePartOfBlock() splits aBlock and move aStartChild to aEndChild out of
 * aBlock.
 */
nsresult
HTMLEditRules::RemovePartOfBlock(Element& aBlock,
                                 nsIContent& aStartChild,
                                 nsIContent& aEndChild)
{
  SplitBlock(aBlock, aStartChild, aEndChild);
  // Get rid of part of blockquote we are outdenting

  NS_ENSURE_STATE(mHTMLEditor);
  nsresult rv = mHTMLEditor->RemoveBlockContainer(aBlock);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

void
HTMLEditRules::SplitBlock(Element& aBlock,
                          nsIContent& aStartChild,
                          nsIContent& aEndChild,
                          nsIContent** aOutLeftNode,
                          nsIContent** aOutRightNode,
                          nsIContent** aOutMiddleNode)
{
  // aStartChild and aEndChild must be exclusive descendants of aBlock
  MOZ_ASSERT(EditorUtils::IsDescendantOf(&aStartChild, &aBlock) &&
             EditorUtils::IsDescendantOf(&aEndChild, &aBlock));
  NS_ENSURE_TRUE_VOID(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  // Get split point location
  OwningNonNull<nsIContent> startParent = *aStartChild.GetParent();
  int32_t startOffset = startParent->IndexOf(&aStartChild);

  // Do the splits!
  nsCOMPtr<nsIContent> newMiddleNode1;
  htmlEditor->SplitNodeDeep(aBlock, startParent, startOffset,
                            HTMLEditor::EmptyContainers::no,
                            aOutLeftNode, getter_AddRefs(newMiddleNode1));

  // Get split point location
  OwningNonNull<nsIContent> endParent = *aEndChild.GetParent();
  // +1 because we want to be after the child
  int32_t endOffset = 1 + endParent->IndexOf(&aEndChild);

  // Do the splits!
  nsCOMPtr<nsIContent> newMiddleNode2;
  htmlEditor->SplitNodeDeep(aBlock, endParent, endOffset,
                            HTMLEditor::EmptyContainers::no,
                            getter_AddRefs(newMiddleNode2), aOutRightNode);

  if (aOutMiddleNode) {
    if (newMiddleNode2) {
      newMiddleNode2.forget(aOutMiddleNode);
    } else {
      newMiddleNode1.forget(aOutMiddleNode);
    }
  }
}

nsresult
HTMLEditRules::OutdentPartOfBlock(Element& aBlock,
                                  nsIContent& aStartChild,
                                  nsIContent& aEndChild,
                                  bool aIsBlockIndentedWithCSS,
                                  nsIContent** aOutLeftNode,
                                  nsIContent** aOutRightNode)
{
  MOZ_ASSERT(aOutLeftNode && aOutRightNode);

  nsCOMPtr<nsIContent> middleNode;
  SplitBlock(aBlock, aStartChild, aEndChild, aOutLeftNode, aOutRightNode,
             getter_AddRefs(middleNode));

  NS_ENSURE_STATE(middleNode);

  if (!aIsBlockIndentedWithCSS) {
    NS_ENSURE_STATE(mHTMLEditor);
    nsresult rv = mHTMLEditor->RemoveBlockContainer(*middleNode);
    NS_ENSURE_SUCCESS(rv, rv);
  } else if (middleNode->IsElement()) {
    // We do nothing if middleNode isn't an element
    nsresult rv = ChangeIndentation(*middleNode->AsElement(), Change::minus);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

/**
 * ConvertListType() converts list type and list item type.
 */
nsresult
HTMLEditRules::ConvertListType(Element* aList,
                               Element** aOutList,
                               nsIAtom* aListType,
                               nsIAtom* aItemType)
{
  MOZ_ASSERT(aList);
  MOZ_ASSERT(aOutList);
  MOZ_ASSERT(aListType);
  MOZ_ASSERT(aItemType);

  nsCOMPtr<nsINode> child = aList->GetFirstChild();
  while (child) {
    if (child->IsElement()) {
      dom::Element* element = child->AsElement();
      if (HTMLEditUtils::IsListItem(element) &&
          !element->IsHTMLElement(aItemType)) {
        child = mHTMLEditor->ReplaceContainer(element, aItemType);
        NS_ENSURE_STATE(child);
      } else if (HTMLEditUtils::IsList(element) &&
                 !element->IsHTMLElement(aListType)) {
        nsCOMPtr<dom::Element> temp;
        nsresult rv = ConvertListType(child->AsElement(), getter_AddRefs(temp),
                                      aListType, aItemType);
        NS_ENSURE_SUCCESS(rv, rv);
        child = temp.forget();
      }
    }
    child = child->GetNextSibling();
  }

  if (aList->IsHTMLElement(aListType)) {
    nsCOMPtr<dom::Element> list = aList->AsElement();
    list.forget(aOutList);
    return NS_OK;
  }

  *aOutList = mHTMLEditor->ReplaceContainer(aList, aListType).take();
  NS_ENSURE_STATE(aOutList);

  return NS_OK;
}


/**
 * CreateStyleForInsertText() takes care of clearing and setting appropriate
 * style nodes for text insertion.
 */
nsresult
HTMLEditRules::CreateStyleForInsertText(Selection& aSelection,
                                        nsIDocument& aDoc)
{
  MOZ_ASSERT(mHTMLEditor->mTypeInState);

  bool weDidSomething = false;
  NS_ENSURE_STATE(aSelection.GetRangeAt(0));
  nsCOMPtr<nsINode> node = aSelection.GetRangeAt(0)->GetStartContainer();
  int32_t offset = aSelection.GetRangeAt(0)->StartOffset();

  // next examine our present style and make sure default styles are either
  // present or explicitly overridden.  If neither, add the default style to
  // the TypeInState
  int32_t length = mHTMLEditor->mDefaultStyles.Length();
  for (int32_t j = 0; j < length; j++) {
    PropItem* propItem = mHTMLEditor->mDefaultStyles[j];
    MOZ_ASSERT(propItem);
    bool bFirst, bAny, bAll;

    // GetInlineProperty also examine TypeInState.  The only gotcha here is
    // that a cleared property looks like an unset property.  For now I'm
    // assuming that's not a problem: that default styles will always be
    // multivalue styles (like font face or size) where clearing the style
    // means we want to go back to the default.  If we ever wanted a "toggle"
    // style like bold for a default, though, I'll have to add code to detect
    // the difference between unset and explicitly cleared, else user would
    // never be able to unbold, for instance.
    nsAutoString curValue;
    NS_ENSURE_STATE(mHTMLEditor);
    nsresult rv =
      mHTMLEditor->GetInlinePropertyBase(*propItem->tag, &propItem->attr,
                                         nullptr, &bFirst, &bAny, &bAll,
                                         &curValue, false);
    NS_ENSURE_SUCCESS(rv, rv);

    if (!bAny) {
      // no style set for this prop/attr
      mHTMLEditor->mTypeInState->SetProp(propItem->tag, propItem->attr,
                                         propItem->value);
    }
  }

  nsCOMPtr<Element> rootElement = aDoc.GetRootElement();
  NS_ENSURE_STATE(rootElement);

  // process clearing any styles first
  UniquePtr<PropItem> item =
    Move(mHTMLEditor->mTypeInState->TakeClearProperty());
  while (item && node != rootElement) {
    NS_ENSURE_STATE(mHTMLEditor);
    nsresult rv =
      mHTMLEditor->ClearStyle(address_of(node), &offset,
                              item->tag, &item->attr);
    NS_ENSURE_SUCCESS(rv, rv);
    item = Move(mHTMLEditor->mTypeInState->TakeClearProperty());
    weDidSomething = true;
  }

  // then process setting any styles
  int32_t relFontSize = mHTMLEditor->mTypeInState->TakeRelativeFontSize();
  item = Move(mHTMLEditor->mTypeInState->TakeSetProperty());

  if (item || relFontSize) {
    // we have at least one style to add; make a new text node to insert style
    // nodes above.
    if (RefPtr<Text> text = node->GetAsText()) {
      // if we are in a text node, split it
      NS_ENSURE_STATE(mHTMLEditor);
      offset = mHTMLEditor->SplitNodeDeep(*text, *text, offset);
      NS_ENSURE_STATE(offset != -1);
      node = node->GetParentNode();
    }
    if (!mHTMLEditor->IsContainer(node)) {
      return NS_OK;
    }
    OwningNonNull<Text> newNode = aDoc.CreateTextNode(EmptyString());
    NS_ENSURE_STATE(mHTMLEditor);
    nsresult rv = mHTMLEditor->InsertNode(*newNode, *node, offset);
    NS_ENSURE_SUCCESS(rv, rv);
    node = newNode;
    offset = 0;
    weDidSomething = true;

    if (relFontSize) {
      // dir indicated bigger versus smaller.  1 = bigger, -1 = smaller
      HTMLEditor::FontSize dir = relFontSize > 0 ?
        HTMLEditor::FontSize::incr : HTMLEditor::FontSize::decr;
      for (int32_t j = 0; j < DeprecatedAbs(relFontSize); j++) {
        NS_ENSURE_STATE(mHTMLEditor);
        rv = mHTMLEditor->RelativeFontChangeOnTextNode(dir, newNode, 0, -1);
        NS_ENSURE_SUCCESS(rv, rv);
      }
    }

    while (item) {
      NS_ENSURE_STATE(mHTMLEditor);
      rv = mHTMLEditor->SetInlinePropertyOnNode(*node->AsContent(),
                                                *item->tag, &item->attr,
                                                item->value);
      NS_ENSURE_SUCCESS(rv, rv);
      item = mHTMLEditor->mTypeInState->TakeSetProperty();
    }
  }
  if (weDidSomething) {
    return aSelection.Collapse(node, offset);
  }

  return NS_OK;
}


/**
 * Figure out if aNode is (or is inside) an empty block.  A block can have
 * children and still be considered empty, if the children are empty or
 * non-editable.
 */
nsresult
HTMLEditRules::IsEmptyBlock(Element& aNode,
                            bool* aOutIsEmptyBlock,
                            MozBRCounts aMozBRCounts)
{
  MOZ_ASSERT(aOutIsEmptyBlock);
  *aOutIsEmptyBlock = true;

  NS_ENSURE_TRUE(IsBlockNode(aNode), NS_ERROR_NULL_POINTER);

  return mHTMLEditor->IsEmptyNode(aNode.AsDOMNode(), aOutIsEmptyBlock,
                                  aMozBRCounts == MozBRCounts::yes ? false
                                                                   : true);
}


nsresult
HTMLEditRules::WillAlign(Selection& aSelection,
                         const nsAString& aAlignType,
                         bool* aCancel,
                         bool* aHandled)
{
  MOZ_ASSERT(aCancel && aHandled);

  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  WillInsert(aSelection, aCancel);

  // Initialize out param.  We want to ignore result of WillInsert().
  *aCancel = false;
  *aHandled = false;

  nsresult rv = NormalizeSelection(&aSelection);
  NS_ENSURE_SUCCESS(rv, rv);
  AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);

  // Convert the selection ranges into "promoted" selection ranges: This
  // basically just expands the range to include the immediate block parent,
  // and then further expands to include any ancestors whose children are all
  // in the range
  *aHandled = true;
  nsTArray<OwningNonNull<nsINode>> nodeArray;
  rv = GetNodesFromSelection(aSelection, EditAction::align, nodeArray);
  NS_ENSURE_SUCCESS(rv, rv);

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

    if (HTMLEditUtils::SupportsAlignAttr(*node)) {
      // The node is a table element, an hr, a paragraph, a div or a section
      // header; in HTML 4, it can directly carry the ALIGN attribute and we
      // don't need to make a div! If we are in CSS mode, all the work is done
      // in AlignBlock
      rv = AlignBlock(*node->AsElement(), aAlignType, ContentsOnly::yes);
      NS_ENSURE_SUCCESS(rv, rv);
      return NS_OK;
    }

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

      NS_ENSURE_STATE(aSelection.GetRangeAt(0) &&
                      aSelection.GetRangeAt(0)->GetStartContainer());
      OwningNonNull<nsINode> parent =
        *aSelection.GetRangeAt(0)->GetStartContainer();

      emptyDiv = !HTMLEditUtils::IsTableElement(parent) ||
                 HTMLEditUtils::IsTableCellOrCaption(parent);
    }
  }
  if (emptyDiv) {
    nsCOMPtr<nsINode> parent =
      aSelection.GetRangeAt(0) ? aSelection.GetRangeAt(0)->GetStartContainer()
                               : nullptr;
    NS_ENSURE_STATE(parent);
    int32_t offset = aSelection.GetRangeAt(0)->StartOffset();

    rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset);
    NS_ENSURE_SUCCESS(rv, rv);
    // Consume a trailing br, if any.  This is to keep an alignment from
    // creating extra lines, if possible.
    nsCOMPtr<nsIContent> brContent =
      htmlEditor->GetNextHTMLNode(parent, offset);
    if (brContent && TextEditUtils::IsBreak(brContent)) {
      // Making use of html structure... if next node after where we are
      // putting our div is not a block, then the br we found is in same block
      // we are, so it's safe to consume it.
      nsCOMPtr<nsIContent> sibling = htmlEditor->GetNextHTMLSibling(parent,
                                                                    offset);
      if (sibling && !IsBlockNode(*sibling)) {
        rv = htmlEditor->DeleteNode(brContent);
        NS_ENSURE_SUCCESS(rv, rv);
      }
    }
    nsCOMPtr<Element> div = htmlEditor->CreateNode(nsGkAtoms::div, parent,
                                                   offset);
    NS_ENSURE_STATE(div);
    // Remember our new block for postprocessing
    mNewBlock = div;
    // Set up the alignment on the div, using HTML or CSS
    rv = AlignBlock(*div, aAlignType, ContentsOnly::yes);
    NS_ENSURE_SUCCESS(rv, rv);
    *aHandled = true;
    // Put in a moz-br so that it won't get deleted
    rv = CreateMozBR(div->AsDOMNode(), 0);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = aSelection.Collapse(div, 0);
    // Don't restore the selection
    selectionRestorer.Abort();
    NS_ENSURE_SUCCESS(rv, rv);
    return NS_OK;
  }

  // Next we detect all the transitions in the array, where a transition
  // means that adjacent nodes in the array don't have the same parent.

  nsTArray<bool> transitionList;
  MakeTransitionList(nodeArray, transitionList);

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

  nsCOMPtr<Element> curDiv;
  bool useCSS = htmlEditor->IsCSSEnabled();
  for (size_t i = 0; i < nodeArray.Length(); i++) {
    auto& curNode = nodeArray[i];
    // Here's where we actually figure out what to do

    // Ignore all non-editable nodes.  Leave them be.
    if (!htmlEditor->IsEditable(curNode)) {
      continue;
    }

    // The node is a table element, an hr, a paragraph, a div or a section
    // header; in HTML 4, it can directly carry the ALIGN attribute and we
    // don't need to nest it, just set the alignment.  In CSS, assign the
    // corresponding CSS styles in AlignBlock
    if (HTMLEditUtils::SupportsAlignAttr(*curNode)) {
      rv = AlignBlock(*curNode->AsElement(), aAlignType, ContentsOnly::no);
      NS_ENSURE_SUCCESS(rv, rv);
      // Clear out curDiv so that we don't put nodes after this one into it
      curDiv = nullptr;
      continue;
    }

    nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
    int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;

    // Skip insignificant formatting text nodes to prevent unnecessary
    // structure splitting!
    bool isEmptyTextNode = false;
    if (curNode->GetAsText() &&
        ((HTMLEditUtils::IsTableElement(curParent) &&
          !HTMLEditUtils::IsTableCellOrCaption(*curParent)) ||
         HTMLEditUtils::IsList(curParent) ||
         (NS_SUCCEEDED(htmlEditor->IsEmptyNode(curNode, &isEmptyTextNode)) &&
          isEmptyTextNode))) {
      continue;
    }

    // If it's a list item, or a list inside a list, forget any "current" div,
    // and instead put divs inside the appropriate block (td, li, etc.)
    if (HTMLEditUtils::IsListItem(curNode) ||
        HTMLEditUtils::IsList(curNode)) {
      rv = RemoveAlignment(*curNode, aAlignType, true);
      NS_ENSURE_SUCCESS(rv, rv);
      if (useCSS) {
        htmlEditor->mCSSEditUtils->SetCSSEquivalentToHTMLStyle(
            curNode->AsElement(), nullptr, nsGkAtoms::align,
            &aAlignType, false);
        curDiv = nullptr;
        continue;
      }
      if (HTMLEditUtils::IsList(curParent)) {
        // If we don't use CSS, add a contraint to list element: they have to
        // be inside another list, i.e., >= second level of nesting
        rv = AlignInnerBlocks(*curNode, &aAlignType);
        NS_ENSURE_SUCCESS(rv, rv);
        curDiv = nullptr;
        continue;
      }
      // Clear out curDiv so that we don't put nodes after this one into it
    }

    // Need to make a div to put things in if we haven't already, or if this
    // node doesn't go in div we used earlier.
    if (!curDiv || transitionList[i]) {
      // First, check that our element can contain a div.
      if (!htmlEditor->CanContainTag(*curParent, *nsGkAtoms::div)) {
        // Cancelled
        return NS_OK;
      }

      rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset);
      NS_ENSURE_SUCCESS(rv, rv);
      curDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParent, offset);
      NS_ENSURE_STATE(curDiv);
      // Remember our new block for postprocessing
      mNewBlock = curDiv;
      // Set up the alignment on the div
      rv = AlignBlock(*curDiv, aAlignType, ContentsOnly::yes);
    }

    NS_ENSURE_STATE(curNode->IsContent());

    // Tuck the node into the end of the active div
    rv = htmlEditor->MoveNode(curNode->AsContent(), curDiv, -1);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}


/**
 * AlignInnerBlocks() aligns inside table cells or list items.
 */
nsresult
HTMLEditRules::AlignInnerBlocks(nsINode& aNode,
                                const nsAString* alignType)
{
  NS_ENSURE_TRUE(alignType, NS_ERROR_NULL_POINTER);

  // Gather list of table cells or list items
  nsTArray<OwningNonNull<nsINode>> nodeArray;
  TableCellAndListItemFunctor functor;
  DOMIterator iter(aNode);
  iter.AppendList(functor, nodeArray);

  // Now that we have the list, align their contents as requested
  for (auto& node : nodeArray) {
    nsresult rv = AlignBlockContents(GetAsDOMNode(node), alignType);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}


/**
 * AlignBlockContents() aligns contents of a block element.
 */
nsresult
HTMLEditRules::AlignBlockContents(nsIDOMNode* aNode,
                                  const nsAString* alignType)
{
  nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
  NS_ENSURE_TRUE(node && alignType, NS_ERROR_NULL_POINTER);
  nsCOMPtr<nsIContent> firstChild, lastChild;

  bool useCSS = mHTMLEditor->IsCSSEnabled();

  NS_ENSURE_STATE(mHTMLEditor);
  firstChild = mHTMLEditor->GetFirstEditableChild(*node);
  NS_ENSURE_STATE(mHTMLEditor);
  lastChild = mHTMLEditor->GetLastEditableChild(*node);
  if (!firstChild) {
    // this cell has no content, nothing to align
  } else if (firstChild == lastChild &&
             firstChild->IsHTMLElement(nsGkAtoms::div)) {
    // the cell already has a div containing all of its content: just
    // act on this div.
    RefPtr<Element> divElem = firstChild->AsElement();
    if (useCSS) {
      NS_ENSURE_STATE(mHTMLEditor);
      nsresult rv = mHTMLEditor->SetAttributeOrEquivalent(divElem,
                                                          nsGkAtoms::align,
                                                          *alignType, false);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    } else {
      NS_ENSURE_STATE(mHTMLEditor);
      nsresult rv = mHTMLEditor->SetAttribute(divElem, nsGkAtoms::align,
                                              *alignType);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    }
  } else {
    // else we need to put in a div, set the alignment, and toss in all the children
    NS_ENSURE_STATE(mHTMLEditor);
    RefPtr<Element> divElem = mHTMLEditor->CreateNode(nsGkAtoms::div, node, 0);
    NS_ENSURE_STATE(divElem);
    // set up the alignment on the div
    if (useCSS) {
      NS_ENSURE_STATE(mHTMLEditor);
      nsresult rv =
        mHTMLEditor->SetAttributeOrEquivalent(divElem, nsGkAtoms::align,
                                              *alignType, false);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    } else {
      NS_ENSURE_STATE(mHTMLEditor);
      nsresult rv =
        mHTMLEditor->SetAttribute(divElem, nsGkAtoms::align, *alignType);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    }
    // tuck the children into the end of the active div
    while (lastChild && (lastChild != divElem)) {
      NS_ENSURE_STATE(mHTMLEditor);
      nsresult rv = mHTMLEditor->MoveNode(lastChild, divElem, 0);
      NS_ENSURE_SUCCESS(rv, rv);
      NS_ENSURE_STATE(mHTMLEditor);
      lastChild = mHTMLEditor->GetLastEditableChild(*node);
    }
  }
  return NS_OK;
}

/**
 * CheckForEmptyBlock() is called by WillDeleteSelection() to detect and handle
 * case of deleting from inside an empty block.
 */
nsresult
HTMLEditRules::CheckForEmptyBlock(nsINode* aStartNode,
                                  Element* aBodyNode,
                                  Selection* aSelection,
                                  nsIEditor::EDirection aAction,
                                  bool* aHandled)
{
  // If the editing host is an inline element, bail out early.
  if (aBodyNode && IsInlineNode(*aBodyNode)) {
    return NS_OK;
  }
  NS_ENSURE_STATE(mHTMLEditor);
  RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);

  // If we are inside an empty block, delete it.  Note: do NOT delete table
  // elements this way.
  nsCOMPtr<Element> block = htmlEditor->GetBlock(*aStartNode);
  bool bIsEmptyNode;
  nsCOMPtr<Element> emptyBlock;
  if (block && block != aBodyNode) {
    // Efficiency hack, avoiding IsEmptyNode() call when in body
    nsresult rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false);
    NS_ENSURE_SUCCESS(rv, rv);
    while (block && bIsEmptyNode && !HTMLEditUtils::IsTableElement(block) &&
           block != aBodyNode) {
      emptyBlock = block;
      block = htmlEditor->GetBlockNodeParent(emptyBlock);
      if (block) {
        rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false);
        NS_ENSURE_SUCCESS(rv, rv);
      }
    }
  }

  if (emptyBlock && emptyBlock->IsEditable()) {
    nsCOMPtr<nsINode> blockParent = emptyBlock->GetParentNode();
    NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE);
    int32_t offset = blockParent->IndexOf(emptyBlock);

    if (HTMLEditUtils::IsListItem(emptyBlock)) {
      // Are we the first list item in the list?
      NS_ENSURE_STATE(htmlEditor);
      if (htmlEditor->IsFirstEditableChild(emptyBlock)) {
        nsCOMPtr<nsINode> listParent = blockParent->GetParentNode();
        NS_ENSURE_TRUE(listParent, NS_ERROR_FAILURE);
        int32_t listOffset = listParent->IndexOf(blockParent);
        // If we are a sublist, skip the br creation
        if (!HTMLEditUtils::IsList(listParent)) {
          // Create a br before list
          NS_ENSURE_STATE(htmlEditor);
          nsCOMPtr<Element> br =
            htmlEditor->CreateBR(listParent, listOffset);
          NS_ENSURE_STATE(br);
          // Adjust selection to be right before it
          nsresult rv = aSelection->Collapse(listParent, listOffset);
          NS_ENSURE_SUCCESS(rv, rv);
        }
        // Else just let selection percolate up.  We'll adjust it in
        // AfterEdit()
      }
    } else {
      if (aAction == nsIEditor::eNext || aAction == nsIEditor::eNextWord ||
          aAction == nsIEditor::eToEndOfLine) {
        // Move to the start of the next node, if any
        nsCOMPtr<nsIContent> nextNode = htmlEditor->GetNextNode(blockParent,
                                                                offset + 1, true);
        if (nextNode) {
          EditorDOMPoint pt = GetGoodSelPointForNode(*nextNode, aAction);
          nsresult rv = aSelection->Collapse(pt.node, pt.offset);
          NS_ENSURE_SUCCESS(rv, rv);
        } else {
          // Adjust selection to be right after it.
          nsresult rv = aSelection->Collapse(blockParent, offset + 1);
          NS_ENSURE_SUCCESS(rv, rv);
        }
      } else if (aAction == nsIEditor::ePrevious ||
                 aAction == nsIEditor::ePreviousWord ||
                 aAction == nsIEditor::eToBeginningOfLine) {
        // Move to the end of the previous node
        nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorNode(blockParent,
                                                                  offset,
                                                                  true);
        if (priorNode) {
          EditorDOMPoint pt = GetGoodSelPointForNode(*priorNode, aAction);
          nsresult rv = aSelection->Collapse(pt.node, pt.offset);
          NS_ENSURE_SUCCESS(rv, rv);
        } else {
          nsresult rv = aSelection->Collapse(blockParent, offset + 1);
          NS_ENSURE_SUCCESS(rv, rv);
        }
      } else if (aAction != nsIEditor::eNone) {
        NS_RUNTIMEABORT("CheckForEmptyBlock doesn't support this action yet");
      }
    }
    NS_ENSURE_STATE(htmlEditor);
    *aHandled = true;
    nsresult rv = htmlEditor->DeleteNode(emptyBlock);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  return NS_OK;
}

Element*
HTMLEditRules::CheckForInvisibleBR(Element& aBlock,
                                   BRLocation aWhere,
                                   int32_t aOffset)
{
  nsCOMPtr<nsINode> testNode;
  int32_t testOffset = 0;

  if (aWhere == BRLocation::blockEnd) {
    // No block crossing
    nsCOMPtr<nsIContent> rightmostNode =
      mHTMLEditor->GetRightmostChild(&aBlock, true);

    if (!rightmostNode) {
      return nullptr;
    }

    testNode = rightmostNode->GetParentNode();
    // Use offset + 1, so last node is included in our evaluation
    testOffset = testNode->IndexOf(rightmostNode) + 1;
  } else if (aOffset) {
    testNode = &aBlock;
    // We'll check everything to the left of the input position
    testOffset = aOffset;
  } else {
    return nullptr;
  }

  WSRunObject wsTester(mHTMLEditor, testNode, testOffset);
  if (WSType::br == wsTester.mStartReason) {
    return wsTester.mStartReasonNode->AsElement();
  }

  return nullptr;
}

/**
 * aLists and aTables allow the caller to specify what kind of content to
 * "look inside".  If aTables is Tables::yes, look inside any table content,
 * and insert the inner content into the supplied nsTArray at offset
 * aIndex.  Similarly with aLists and list content.  aIndex is updated to
 * point past inserted elements.
 */
void
HTMLEditRules::GetInnerContent(
                 nsINode& aNode,
                 nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
                 int32_t* aIndex,
                 Lists aLists,
                 Tables aTables)
{
  MOZ_ASSERT(aIndex);

  for (nsCOMPtr<nsIContent> node = mHTMLEditor->GetFirstEditableChild(aNode);
       node; node = node->GetNextSibling()) {
    if ((aLists == Lists::yes && (HTMLEditUtils::IsList(node) ||
                                  HTMLEditUtils::IsListItem(node))) ||
        (aTables == Tables::yes && HTMLEditUtils::IsTableElement(node))) {
      GetInnerContent(*node, aOutArrayOfNodes, aIndex, aLists, aTables);
    } else {
      aOutArrayOfNodes.InsertElementAt(*aIndex, *node);
      (*aIndex)++;
    }
  }
}

/**
 * Promotes selection to include blocks that have all their children selected.
 */
nsresult
HTMLEditRules::ExpandSelectionForDeletion(Selection& aSelection)
{
  // Don't need to touch collapsed selections
  if (aSelection.Collapsed()) {
    return NS_OK;
  }

  // We don't need to mess with cell selections, and we assume multirange
  // selections are those.
  if (aSelection.RangeCount() != 1) {
    return NS_OK;
  }

  // Find current sel start and end
  NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_ERROR_NULL_POINTER);
  OwningNonNull<nsRange> range = *aSelection.GetRangeAt(0);

  nsCOMPtr<nsINode> selStartNode = range->GetStartContainer();
  int32_t selStartOffset = range->StartOffset();
  nsCOMPtr<nsINode> selEndNode = range->GetEndContainer();
  int32_t selEndOffset = range->EndOffset();

  // Find current selection common block parent
  nsCOMPtr<Element> selCommon =
    HTMLEditor::GetBlock(*range->GetCommonAncestor());
  NS_ENSURE_STATE(selCommon);

  // Set up for loops and cache our root element
  nsCOMPtr<nsINode> firstBRParent;
  nsCOMPtr<nsINode> unused;
  int32_t visOffset = 0, firstBROffset = 0;
  WSType wsType;
  nsCOMPtr<Element> root = mHTMLEditor->GetActiveEditingHost();
  NS_ENSURE_TRUE(root, NS_ERROR_FAILURE);

  // Find previous visible thingy before start of selection
  if (selStartNode != selCommon && selStartNode != root) {
    while (true) {
      WSRunObject wsObj(mHTMLEditor, selStartNode, selStartOffset);
      wsObj.PriorVisibleNode(selStartNode, selStartOffset, address_of(unused),
                             &visOffset, &wsType);
      if (wsType != WSType::thisBlock) {
        break;
      }
      // We want to keep looking up.  But stop if we are crossing table
      // element boundaries, or if we hit the root.
      if (HTMLEditUtils::IsTableElement(wsObj.mStartReasonNode) ||
          selCommon == wsObj.mStartReasonNode ||
          root == wsObj.mStartReasonNode) {
        break;
      }
      selStartNode = wsObj.mStartReasonNode->GetParentNode();
      selStartOffset = selStartNode ?
        selStartNode->IndexOf(wsObj.mStartReasonNode) : -1;
    }
  }

  // Find next visible thingy after end of selection
  if (selEndNode != selCommon && selEndNode != root) {
    for (;;) {
      WSRunObject wsObj(mHTMLEditor, selEndNode, selEndOffset);
      wsObj.NextVisibleNode(selEndNode, selEndOffset, address_of(unused),
                            &visOffset, &wsType);
      if (wsType == WSType::br) {
        if (mHTMLEditor->IsVisibleBRElement(wsObj.mEndReasonNode)) {
          break;
        }
        if (!firstBRParent) {
          firstBRParent = selEndNode;
          firstBROffset = selEndOffset;
        }
        selEndNode = wsObj.mEndReasonNode->GetParentNode();
        selEndOffset = selEndNode
          ? selEndNode->IndexOf(wsObj.mEndReasonNode) + 1 : 0;
      } else if (wsType == WSType::thisBlock) {
        // We want to keep looking up.  But stop if we are crossing table
        // element boundaries, or if we hit the root.
        if (HTMLEditUtils::IsTableElement(wsObj.mEndReasonNode) ||
            selCommon == wsObj.mEndReasonNode ||
            root == wsObj.mEndReasonNode) {
          break;
        }
        selEndNode = wsObj.mEndReasonNode->GetParentNode();
        selEndOffset = 1 + selEndNode->IndexOf(wsObj.mEndReasonNode);
      } else {
        break;
      }
    }
  }
  // Now set the selection to the new range
  aSelection.Collapse(selStartNode, selStartOffset);

  // Expand selection endpoint only if we didn't pass a br, or if we really
  // needed to pass that br (i.e., its block is now totally selected)
  bool doEndExpansion = true;
  if (firstBRParent) {
    // Find block node containing br
    nsCOMPtr<Element> brBlock = HTMLEditor::GetBlock(*firstBRParent);
    bool nodeBefore = false, nodeAfter = false;

    // Create a range that represents expanded selection
    RefPtr<nsRange> range = new nsRange(selStartNode);
    nsresult rv = range->SetStartAndEnd(selStartNode, selStartOffset,
                                        selEndNode, selEndOffset);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    // Check if block is entirely inside range
    if (brBlock) {
      nsRange::CompareNodeToRange(brBlock, range, &nodeBefore, &nodeAfter);
    }

    // If block isn't contained, forgo grabbing the br in expanded selection
    if (nodeBefore || nodeAfter) {
      doEndExpansion = false;
    }
  }
  if (doEndExpansion) {
    nsresult rv = aSelection.Extend(selEndNode, selEndOffset);
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    // Only expand to just before br
    nsresult rv = aSelection.Extend(firstBRParent, firstBROffset);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

/**
 * NormalizeSelection() tweaks non-collapsed selections to be more "natural".
 * Idea here is to adjust selection endpoint so that they do not cross breaks
 * or block boundaries unless something editable beyond that boundary is also
 * selected.  This adjustment makes it much easier for the various block
 * operations to determine what nodes to act on.
 */
nsresult
HTMLEditRules::NormalizeSelection(Selection* inSelection)
{
  NS_ENSURE_TRUE(inSelection, NS_ERROR_NULL_POINTER);

  // don't need to touch collapsed selections
  if (inSelection->Collapsed()) {
    return NS_OK;
  }

  int32_t rangeCount;
  nsresult rv = inSelection->GetRangeCount(&rangeCount);
  NS_ENSURE_SUCCESS(rv, rv);

  // we don't need to mess with cell selections, and we assume multirange selections are those.
  if (rangeCount != 1) {
    return NS_OK;
  }

  RefPtr<nsRange> range = inSelection->GetRangeAt(0);
  NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER);
  nsCOMPtr<nsIDOMNode> startNode, endNode;
  uint32_t startOffset, endOffset;
  nsCOMPtr<nsIDOMNode> newStartNode, newEndNode;

  rv = range->GetStartContainer(getter_AddRefs(startNode));
  NS_ENSURE_SUCCESS(rv, rv);
  rv = range->GetStartOffset(&startOffset);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = range->GetEndContainer(getter_AddRefs(endNode));
  NS_ENSURE_SUCCESS(rv, rv);
  rv = range->GetEndOffset(&endOffset);
  NS_ENSURE_SUCCESS(rv, rv);

  // adjusted values default to original values
  newStartNode = startNode;
  uint32_t newStartOffset = startOffset;
  newEndNode = endNode;
  uint32_t newEndOffset = endOffset;

  // some locals we need for whitespace code
  nsCOMPtr<nsINode> unused;
  int32_t offset = -1;
  WSType wsType;

  // let the whitespace code do the heavy lifting
  WSRunObject wsEndObj(mHTMLEditor, endNode, static_cast<int32_t>(endOffset));
  // is there any intervening visible whitespace?  if so we can't push selection past that,
  // it would visibly change maening of users selection
  nsCOMPtr<nsINode> endNode_(do_QueryInterface(endNode));
  wsEndObj.PriorVisibleNode(endNode_, static_cast<int32_t>(endOffset),
                            address_of(unused), &offset, &wsType);
  if (wsType != WSType::text && wsType != WSType::normalWS) {
    // eThisBlock and eOtherBlock conveniently distinquish cases
    // of going "down" into a block and "up" out of a block.
    if (wsEndObj.mStartReason == WSType::otherBlock) {
      // endpoint is just after the close of a block.
      nsCOMPtr<nsIDOMNode> child =
        GetAsDOMNode(mHTMLEditor->GetRightmostChild(wsEndObj.mStartReasonNode,
                                                    true));
      if (child) {
        int32_t offset = -1;
        newEndNode = EditorBase::GetNodeLocation(child, &offset);
        // offset *after* child
        newEndOffset = static_cast<uint32_t>(offset + 1);
      }
      // else block is empty - we can leave selection alone here, i think.
    } else if (wsEndObj.mStartReason == WSType::thisBlock) {
      // endpoint is just after start of this block
      nsCOMPtr<nsIDOMNode> child;
      NS_ENSURE_STATE(mHTMLEditor);
      mHTMLEditor->GetPriorHTMLNode(endNode, static_cast<int32_t>(endOffset),
                                    address_of(child));
      if (child) {
        int32_t offset = -1;
        newEndNode = EditorBase::GetNodeLocation(child, &offset);
        // offset *after* child
        newEndOffset = static_cast<uint32_t>(offset + 1);
      }
      // else block is empty - we can leave selection alone here, i think.
    } else if (wsEndObj.mStartReason == WSType::br) {
      // endpoint is just after break.  lets adjust it to before it.
      int32_t offset = -1;
      newEndNode =
        EditorBase::GetNodeLocation(GetAsDOMNode(wsEndObj.mStartReasonNode),
                                    &offset);
      newEndOffset = static_cast<uint32_t>(offset);;
    }
  }


  // similar dealio for start of range
  WSRunObject wsStartObj(mHTMLEditor, startNode,
                         static_cast<int32_t>(startOffset));
  // is there any intervening visible whitespace?  if so we can't push selection past that,
  // it would visibly change maening of users selection
  nsCOMPtr<nsINode> startNode_(do_QueryInterface(startNode));
  wsStartObj.NextVisibleNode(startNode_, static_cast<int32_t>(startOffset),
                             address_of(unused), &offset, &wsType);
  if (wsType != WSType::text && wsType != WSType::normalWS) {
    // eThisBlock and eOtherBlock conveniently distinquish cases
    // of going "down" into a block and "up" out of a block.
    if (wsStartObj.mEndReason == WSType::otherBlock) {
      // startpoint is just before the start of a block.
      nsCOMPtr<nsIDOMNode> child =
        GetAsDOMNode(mHTMLEditor->GetLeftmostChild(wsStartObj.mEndReasonNode,
                                                   true));
      if (child) {
        int32_t offset = -1;
        newStartNode = EditorBase::GetNodeLocation(child, &offset);
        newStartOffset = static_cast<uint32_t>(offset);
      }
      // else block is empty - we can leave selection alone here, i think.
    } else if (wsStartObj.mEndReason == WSType::thisBlock) {
      // startpoint is just before end of this block
      nsCOMPtr<nsIDOMNode> child;
      NS_ENSURE_STATE(mHTMLEditor);
      mHTMLEditor->GetNextHTMLNode(startNode, static_cast<int32_t>(startOffset),
                                   address_of(child));
      if (child) {
        int32_t offset = -1;
        newStartNode = EditorBase::GetNodeLocation(child, &offset);
        newStartOffset = static_cast<uint32_t>(offset);
      }
      // else block is empty - we can leave selection alone here, i think.
    } else if (wsStartObj.mEndReason == WSType::br) {
      // startpoint is just before a break.  lets adjust it to after it.
      int32_t offset = -1;
      newStartNode =
        EditorBase::GetNodeLocation(GetAsDOMNode(wsStartObj.mEndReasonNode),
                                    &offset);
      // offset *after* break
      newStartOffset = static_cast<uint32_t>(offset + 1);
    }
  }

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

  int16_t comp;
  comp = nsContentUtils::ComparePoints(startNode, startOffset,
                                       newEndNode, newEndOffset);
  if (comp == 1) {
    return NS_OK;  // New end before old start.
  }
  comp = nsContentUtils::ComparePoints(newStartNode, newStartOffset,
                                       endNode, endOffset);
  if (comp == 1) {
    return NS_OK;  // New start after old end.
  }

  // otherwise set selection to new values.
  inSelection->Collapse(newStartNode, newStartOffset);
  inSelection->Extend(newEndNode, newEndOffset);
  return NS_OK;
}

/**
 * GetPromotedPoint() figures out where a start or end point for a block
 * operation really is.
 */
EditorDOMPoint
HTMLEditRules::GetPromotedPoint(RulesEndpoint aWhere,
                                nsINode& aNode,
                                int32_t aOffset,
                                EditAction actionID)
{
  if (NS_WARN_IF(!mHTMLEditor)) {
    return EditorDOMPoint(&aNode, aOffset);
  }
  RefPtr<HTMLEditor> htmlEditor = mHTMLEditor;

  // we do one thing for text actions, something else entirely for other
  // actions
  if (actionID == EditAction::insertText ||
      actionID == EditAction::insertIMEText ||
      actionID == EditAction::insertBreak ||
      actionID == EditAction::deleteText) {
    bool isSpace, isNBSP;
    nsCOMPtr<nsIContent> content =
      aNode.IsContent() ? aNode.AsContent() : nullptr;
    nsCOMPtr<nsIContent> temp;
    int32_t newOffset = aOffset;
    // for text actions, we want to look backwards (or forwards, as
    // appropriate) for additional whitespace or nbsp's.  We may have to act on
    // these later even though they are outside of the initial selection.  Even
    // if they are in another node!
    while (content) {
      int32_t offset;
      if (aWhere == kStart) {
        htmlEditor->IsPrevCharInNodeWhitespace(content, newOffset,
                                               &isSpace, &isNBSP,
                                               getter_AddRefs(temp), &offset);
      } else {
        htmlEditor->IsNextCharInNodeWhitespace(content, newOffset,
                                               &isSpace, &isNBSP,
                                               getter_AddRefs(temp), &offset);
      }
      if (isSpace || isNBSP) {
        content = temp;
        newOffset = offset;
      } else {
        break;
      }
    }

    return EditorDOMPoint(content, newOffset);
  }

  nsCOMPtr<nsINode> node = &aNode;
  int32_t offset = aOffset;

  // else not a text section.  In this case we want to see if we should grab
  // any adjacent inline nodes and/or parents and other ancestors
  if (aWhere == kStart) {
    // some special casing for text nodes
    if (node->IsNodeOfType(nsINode::eTEXT)) {
      if (!node->GetParentNode()) {
        // Okay, can't promote any further
        return EditorDOMPoint(node, offset);
      }
      offset = node->GetParentNode()->IndexOf(node);
      node = node->GetParentNode();
    }

    // look back through any further inline nodes that aren't across a <br>
    // from us, and that are enclosed in the same block.
    nsCOMPtr<nsINode> priorNode =
      htmlEditor->GetPriorHTMLNode(node, offset, true);

    while (priorNode && priorNode->GetParentNode() &&
           !htmlEditor->IsVisibleBRElement(priorNode) &&
           !IsBlockNode(*priorNode)) {
      offset = priorNode->GetParentNode()->IndexOf(priorNode);
      node = priorNode->GetParentNode();
      priorNode = htmlEditor->GetPriorHTMLNode(node, offset, true);
    }

    // finding the real start for this point.  look up the tree for as long as
    // we are the first node in the container, and as long as we haven't hit
    // the body node.
    nsCOMPtr<nsIContent> nearNode =
      htmlEditor->GetPriorHTMLNode(node, offset, true);
    while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) &&
           node->GetParentNode()) {
      // some cutoffs are here: we don't need to also include them in the
      // aWhere == kEnd case.  as long as they are in one or the other it will
      // work.  special case for outdent: don't keep looking up if we have
      // found a blockquote element to act on
      if (actionID == EditAction::outdent &&
          node->IsHTMLElement(nsGkAtoms::blockquote)) {
        break;
      }

      int32_t parentOffset = node->GetParentNode()->IndexOf(node);
      nsCOMPtr<nsINode> parent = node->GetParentNode();

      // Don't walk past the editable section. Note that we need to check
      // before walking up to a parent because we need to return the parent
      // object, so the parent itself might not be in the editable area, but
      // it's OK if we're not performing a block-level action.
      bool blockLevelAction = actionID == EditAction::indent ||
                              actionID == EditAction::outdent ||
                              actionID == EditAction::align ||
                              actionID == EditAction::makeBasicBlock;
      if (!htmlEditor->IsDescendantOfEditorRoot(parent) &&
          (blockLevelAction ||
           !htmlEditor->IsDescendantOfEditorRoot(node))) {
        break;
      }

      node = parent;
      offset = parentOffset;
      nearNode = htmlEditor->GetPriorHTMLNode(node, offset, true);
    }
    return EditorDOMPoint(node, offset);
  }

  // aWhere == kEnd
  // some special casing for text nodes
  if (node->IsNodeOfType(nsINode::eTEXT)) {
    if (!node->GetParentNode()) {
      // Okay, can't promote any further
      return EditorDOMPoint(node, offset);
    }
    // want to be after the text node
    offset = 1 + node->GetParentNode()->IndexOf(node);
    node = node->GetParentNode();
  }

  // look ahead through any further inline nodes that aren't across a <br> from
  // us, and that are enclosed in the same block.
  nsCOMPtr<nsIContent> nextNode =
    htmlEditor->GetNextHTMLNode(node, offset, true);

  while (nextNode && !IsBlockNode(*nextNode) && nextNode->GetParentNode()) {
    offset = 1 + nextNode->GetParentNode()->IndexOf(nextNode);
    node = nextNode->GetParentNode();
    if (htmlEditor->IsVisibleBRElement(nextNode)) {
      break;
    }

    // Check for newlines in pre-formatted text nodes.
    bool isPRE;
    htmlEditor->IsPreformatted(nextNode->AsDOMNode(), &isPRE);
    if (isPRE) {
      if (EditorBase::IsTextNode(nextNode)) {
        nsAutoString tempString;
        nextNode->GetAsText()->GetData(tempString);
        int32_t newlinePos = tempString.FindChar(nsCRT::LF);
        if (newlinePos >= 0) {
          if (static_cast<uint32_t>(newlinePos) + 1 == tempString.Length()) {
            // No need for special processing if the newline is at the end.
            break;
          }
          return EditorDOMPoint(nextNode, newlinePos + 1);
        }
      }
    }
    nextNode = htmlEditor->GetNextHTMLNode(node, offset, true);
  }

  // finding the real end for this point.  look up the tree for as long as we
  // are the last node in the container, and as long as we haven't hit the body
  // node.
  nsCOMPtr<nsIContent> nearNode =
    htmlEditor->GetNextHTMLNode(node, offset, true);
  while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) &&
         node->GetParentNode()) {
    int32_t parentOffset = node->GetParentNode()->IndexOf(node);
    nsCOMPtr<nsINode> parent = node->GetParentNode();

    // Don't walk past the editable section. Note that we need to check before
    // walking up to a parent because we need to return the parent object, so
    // the parent itself might not be in the editable area, but it's OK.
    if (!htmlEditor->IsDescendantOfEditorRoot(node) &&
        !htmlEditor->IsDescendantOfEditorRoot(parent)) {
      break;
    }

    node = parent;
    // we want to be AFTER nearNode
    offset = parentOffset + 1;
    nearNode = htmlEditor->GetNextHTMLNode(node, offset, true);
  }
  return EditorDOMPoint(node, offset);
}

/**
 * GetPromotedRanges() runs all the selection range endpoint through
 * GetPromotedPoint().
 */
void
HTMLEditRules::GetPromotedRanges(Selection& aSelection,
                                 nsTArray<RefPtr<nsRange>>& outArrayOfRanges,
                                 EditAction inOperationType)
{
  uint32_t rangeCount = aSelection<