summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/HTMLEditSubActionHandler.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'editor/libeditor/HTMLEditSubActionHandler.cpp')
-rw-r--r--editor/libeditor/HTMLEditSubActionHandler.cpp12090
1 files changed, 12090 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp
new file mode 100644
index 0000000000..28e0ad1595
--- /dev/null
+++ b/editor/libeditor/HTMLEditSubActionHandler.cpp
@@ -0,0 +1,12090 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLEditor.h"
+#include "HTMLEditorInlines.h"
+#include "HTMLEditorNestedClasses.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "AutoRangeArray.h"
+#include "CSSEditUtils.h"
+#include "EditAction.h"
+#include "EditorDOMPoint.h"
+#include "EditorUtils.h"
+#include "HTMLEditHelpers.h"
+#include "HTMLEditUtils.h"
+#include "PendingStyles.h" // for SpecifiedStyle
+#include "WSRunObject.h"
+
+#include "ErrorList.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/EditorForwards.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/InternalMutationEvent.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/RangeUtils.h"
+#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
+#include "mozilla/TextComposition.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/RangeBinding.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h"
+#include "mozilla/mozalloc.h"
+#include "nsAString.h"
+#include "nsAlgorithm.h"
+#include "nsAtom.h"
+#include "nsCRT.h"
+#include "nsCRTGlue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsFrameSelection.h"
+#include "nsGkAtoms.h"
+#include "nsHTMLDocument.h"
+#include "nsIContent.h"
+#include "nsID.h"
+#include "nsIFrame.h"
+#include "nsINode.h"
+#include "nsLiteralString.h"
+#include "nsPrintfCString.h"
+#include "nsRange.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsStyledElement.h"
+#include "nsTArray.h"
+#include "nsTextNode.h"
+#include "nsThreadUtils.h"
+#include "nsUnicharUtils.h"
+
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
+using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions;
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
+using WalkTextOption = HTMLEditUtils::WalkTextOption;
+using WalkTreeDirection = HTMLEditUtils::WalkTreeDirection;
+using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
+
+/********************************************************
+ * first some helpful functors we will use
+ ********************************************************/
+
+static bool IsPendingStyleCachePreservingSubAction(
+ EditSubAction aEditSubAction) {
+ switch (aEditSubAction) {
+ case EditSubAction::eDeleteSelectedContent:
+ case EditSubAction::eInsertLineBreak:
+ case EditSubAction::eInsertParagraphSeparator:
+ case EditSubAction::eCreateOrChangeList:
+ case EditSubAction::eIndent:
+ case EditSubAction::eOutdent:
+ case EditSubAction::eSetOrClearAlignment:
+ case EditSubAction::eCreateOrRemoveBlock:
+ case EditSubAction::eFormatBlockForHTMLCommand:
+ case EditSubAction::eMergeBlockContents:
+ case EditSubAction::eRemoveList:
+ case EditSubAction::eCreateOrChangeDefinitionListItem:
+ case EditSubAction::eInsertElement:
+ case EditSubAction::eInsertQuotation:
+ case EditSubAction::eInsertQuotedText:
+ return true;
+ default:
+ return false;
+ }
+}
+
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMRange& aRange);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorRawDOMRange& aRange);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorRawDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint);
+template already_AddRefed<nsRange>
+HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint);
+
+nsresult HTMLEditor::InitEditorContentAndSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // We should do nothing with the result of GetRoot() if only a part of the
+ // document is editable.
+ if (!EntireDocumentIsEditable()) {
+ return NS_OK;
+ }
+
+ nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() failed");
+ return rv;
+ }
+
+ // If the selection hasn't been set up yet, set it up collapsed to the end of
+ // our editable content.
+ // XXX I think that this shouldn't do it in `HTMLEditor` because it maybe
+ // removed by the web app and if they call `Selection::AddRange()` without
+ // checking the range count, it may cause multiple selection ranges.
+ if (!SelectionRef().RangeCount()) {
+ nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() "
+ "failed");
+ return rv;
+ }
+ }
+
+ if (IsPlaintextMailComposer()) {
+ // XXX Should we do this in HTMLEditor? It's odd to guarantee that last
+ // empty line is visible only when it's in the plain text mode.
+ nsresult rv = EnsurePaddingBRElementInMultilineEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::EnsurePaddingBRElementInMultilineEditor() failed");
+ return rv;
+ }
+ }
+
+ Element* bodyOrDocumentElement = GetRoot();
+ if (NS_WARN_IF(!bodyOrDocumentElement && !GetDocument())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!bodyOrDocumentElement) {
+ return NS_OK;
+ }
+
+ rv = InsertBRElementToEmptyListItemsAndTableCellsInRange(
+ RawRangeBoundary(bodyOrDocumentElement, 0u),
+ RawRangeBoundary(bodyOrDocumentElement,
+ bodyOrDocumentElement->GetChildCount()));
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange() "
+ "failed, but ignored");
+ return NS_OK;
+}
+
+void HTMLEditor::OnStartToHandleTopLevelEditSubAction(
+ EditSubAction aTopLevelEditSubAction,
+ nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aRv.Failed());
+
+ EditorBase::OnStartToHandleTopLevelEditSubAction(
+ aTopLevelEditSubAction, aDirectionOfTopLevelEditSubAction, aRv);
+
+ MOZ_ASSERT(GetTopLevelEditSubAction() == aTopLevelEditSubAction);
+ MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() ==
+ aDirectionOfTopLevelEditSubAction);
+
+ if (NS_WARN_IF(Destroyed())) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ if (!mInitSucceeded) {
+ return; // We should do nothing if we're being initialized.
+ }
+
+ NS_WARNING_ASSERTION(
+ !aRv.Failed(),
+ "EditorBase::OnStartToHandleTopLevelEditSubAction() failed");
+
+ // Let's work with the latest layout information after (maybe) dispatching
+ // `beforeinput` event.
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+ document->FlushPendingNotifications(FlushType::Frames);
+ if (NS_WARN_IF(Destroyed())) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ // Remember where our selection was before edit action took place:
+ const auto atCompositionStart =
+ GetFirstIMESelectionStartPoint<EditorRawDOMPoint>();
+ if (atCompositionStart.IsSet()) {
+ // If there is composition string, let's remember current composition
+ // range.
+ TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(
+ atCompositionStart, GetLastIMESelectionEndPoint<EditorRawDOMPoint>());
+ } else {
+ // Get the selection location
+ // XXX This may occur so that I think that we shouldn't throw exception
+ // in this case.
+ if (NS_WARN_IF(!SelectionRef().RangeCount())) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+ if (const nsRange* range = SelectionRef().GetRangeAt(0)) {
+ TopLevelEditSubActionDataRef().mSelectedRange->StoreRange(*range);
+ }
+ }
+
+ // Register with range updater to track this as we perturb the doc
+ RangeUpdaterRef().RegisterRangeItem(
+ *TopLevelEditSubActionDataRef().mSelectedRange);
+
+ // Remember current inline styles for deletion and normal insertion ops
+ const bool cacheInlineStyles = [&]() {
+ switch (aTopLevelEditSubAction) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eDeleteSelectedContent:
+ return true;
+ default:
+ return IsPendingStyleCachePreservingSubAction(aTopLevelEditSubAction);
+ }
+ }();
+ if (cacheInlineStyles) {
+ const RefPtr<Element> editingHost =
+ ComputeEditingHost(LimitInBodyElement::No);
+ if (NS_WARN_IF(!editingHost)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsIContent* const startContainer =
+ HTMLEditUtils::GetContentToPreserveInlineStyles(
+ TopLevelEditSubActionDataRef()
+ .mSelectedRange->StartPoint<EditorRawDOMPoint>(),
+ *editingHost);
+ if (NS_WARN_IF(!startContainer)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ if (const RefPtr<Element> startContainerElement =
+ startContainer->GetAsElementOrParentElement()) {
+ nsresult rv = CacheInlineStyles(*startContainerElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::CacheInlineStyles() failed");
+ aRv.Throw(rv);
+ return;
+ }
+ }
+ }
+
+ // Stabilize the document against contenteditable count changes
+ if (document->GetEditingState() == Document::EditingState::eContentEditable) {
+ document->ChangeContentEditableCount(nullptr, +1);
+ TopLevelEditSubActionDataRef().mRestoreContentEditableCount = true;
+ }
+
+ // Check that selection is in subtree defined by body node
+ nsresult rv = EnsureSelectionInBodyOrDocumentElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() "
+ "failed, but ignored");
+}
+
+nsresult HTMLEditor::OnEndHandlingTopLevelEditSubAction() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ nsresult rv;
+ while (true) {
+ if (NS_WARN_IF(Destroyed())) {
+ rv = NS_ERROR_EDITOR_DESTROYED;
+ break;
+ }
+
+ if (!mInitSucceeded) {
+ rv = NS_OK; // We should do nothing if we're being initialized.
+ break;
+ }
+
+ // Do all the tricky stuff
+ rv = OnEndHandlingTopLevelEditSubActionInternal();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() failied");
+ // Perhaps, we need to do the following jobs even if the editor has been
+ // destroyed since they adjust some states of HTML document but don't
+ // modify the DOM tree nor Selection.
+
+ // Free up selectionState range item
+ if (TopLevelEditSubActionDataRef().mSelectedRange) {
+ RangeUpdaterRef().DropRangeItem(
+ *TopLevelEditSubActionDataRef().mSelectedRange);
+ }
+
+ // Reset the contenteditable count to its previous value
+ if (TopLevelEditSubActionDataRef().mRestoreContentEditableCount) {
+ Document* document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ if (document->GetEditingState() ==
+ Document::EditingState::eContentEditable) {
+ document->ChangeContentEditableCount(nullptr, -1);
+ }
+ }
+ break;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ EditorBase::OnEndHandlingTopLevelEditSubAction();
+ NS_WARNING_ASSERTION(
+ NS_FAILED(rv) || NS_SUCCEEDED(rvIgnored),
+ "EditorBase::OnEndHandlingTopLevelEditSubAction() failed, but ignored");
+ MOZ_ASSERT(!GetTopLevelEditSubAction());
+ MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == eNone);
+ return rv;
+}
+
+nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ nsresult rv = EnsureSelectionInBodyOrDocumentElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureSelectionInBodyOrDocumentElement() "
+ "failed, but ignored");
+
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eReplaceHeadWithHTMLSource:
+ case EditSubAction::eCreatePaddingBRElementForEmptyEditor:
+ return NS_OK;
+ default:
+ break;
+ }
+
+ if (TopLevelEditSubActionDataRef().mChangedRange->IsPositioned() &&
+ GetTopLevelEditSubAction() != EditSubAction::eUndo &&
+ GetTopLevelEditSubAction() != EditSubAction::eRedo) {
+ // don't let any txns in here move the selection around behind our back.
+ // Note that this won't prevent explicit selection setting from working.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ {
+ EditorDOMRange changedRange(
+ *TopLevelEditSubActionDataRef().mChangedRange);
+ if (changedRange.IsPositioned() &&
+ changedRange.EnsureNotInNativeAnonymousSubtree()) {
+ bool isBlockLevelSubAction = false;
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eInsertLineBreak:
+ case EditSubAction::eInsertParagraphSeparator:
+ case EditSubAction::eDeleteText: {
+ // XXX We should investigate whether this is really needed because
+ // it seems that the following code does not handle the
+ // white-spaces.
+ RefPtr<nsRange> extendedChangedRange =
+ CreateRangeIncludingAdjuscentWhiteSpaces(changedRange);
+ if (extendedChangedRange) {
+ MOZ_ASSERT(extendedChangedRange->IsPositioned());
+ // Use extended range temporarily.
+ TopLevelEditSubActionDataRef().mChangedRange =
+ std::move(extendedChangedRange);
+ }
+ break;
+ }
+ case EditSubAction::eCreateOrChangeList:
+ case EditSubAction::eCreateOrChangeDefinitionListItem:
+ case EditSubAction::eRemoveList:
+ case EditSubAction::eFormatBlockForHTMLCommand:
+ case EditSubAction::eCreateOrRemoveBlock:
+ case EditSubAction::eIndent:
+ case EditSubAction::eOutdent:
+ case EditSubAction::eSetOrClearAlignment:
+ case EditSubAction::eSetPositionToAbsolute:
+ case EditSubAction::eSetPositionToStatic:
+ case EditSubAction::eDecreaseZIndex:
+ case EditSubAction::eIncreaseZIndex:
+ isBlockLevelSubAction = true;
+ [[fallthrough]];
+ default: {
+ Element* editingHost = ComputeEditingHost();
+ if (MOZ_UNLIKELY(!editingHost)) {
+ break;
+ }
+ RefPtr<nsRange> extendedChangedRange = AutoRangeArray::
+ CreateRangeWrappingStartAndEndLinesContainingBoundaries(
+ changedRange, GetTopLevelEditSubAction(),
+ isBlockLevelSubAction
+ ? BlockInlineCheck::UseHTMLDefaultStyle
+ : BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ *editingHost);
+ if (!extendedChangedRange) {
+ break;
+ }
+ MOZ_ASSERT(extendedChangedRange->IsPositioned());
+ // Use extended range temporarily.
+ TopLevelEditSubActionDataRef().mChangedRange =
+ std::move(extendedChangedRange);
+ break;
+ }
+ }
+ }
+ }
+
+ // if we did a ranged deletion or handling backspace key, make sure we have
+ // a place to put caret.
+ // Note we only want to do this if the overall operation was deletion,
+ // not if deletion was done along the way for
+ // EditSubAction::eInsertHTMLSource, EditSubAction::eInsertText, etc.
+ // That's why this is here rather than DeleteSelectionAsSubAction().
+ // However, we shouldn't insert <br> elements if we've already removed
+ // empty block parents because users may want to disappear the line by
+ // the deletion.
+ // XXX We should make HandleDeleteSelection() store expected container
+ // for handling this here since we cannot trust current selection is
+ // collapsed at deleted point.
+ if (GetTopLevelEditSubAction() == EditSubAction::eDeleteSelectedContent &&
+ TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange &&
+ !TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks) {
+ const auto newCaretPosition =
+ GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (!newCaretPosition.IsSet()) {
+ NS_WARNING("There was no selection range");
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ Result<CaretPoint, nsresult> caretPointOrError =
+ InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ newCaretPosition);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() "
+ "failed");
+ return caretPointOrError.unwrapErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ }
+
+ // add in any needed <br>s, and remove any unneeded ones.
+ nsresult rv = InsertBRElementToEmptyListItemsAndTableCellsInRange(
+ TopLevelEditSubActionDataRef().mChangedRange->StartRef().AsRaw(),
+ TopLevelEditSubActionDataRef().mChangedRange->EndRef().AsRaw());
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange()"
+ " failed, but ignored");
+
+ // merge any adjacent text nodes
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ break;
+ default: {
+ nsresult rv = CollapseAdjacentTextNodes(
+ MOZ_KnownLive(*TopLevelEditSubActionDataRef().mChangedRange));
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::CollapseAdjacentTextNodes() failed");
+ return rv;
+ }
+ break;
+ }
+ }
+
+ // Clean up any empty nodes in the changed range unless they are inserted
+ // intentionally.
+ if (TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements) {
+ nsresult rv = RemoveEmptyNodesIn(
+ EditorDOMRange(*TopLevelEditSubActionDataRef().mChangedRange));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::RemoveEmptyNodesIn() failed");
+ return rv;
+ }
+ }
+
+ // attempt to transform any unneeded nbsp's into spaces after doing various
+ // operations
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eDeleteSelectedContent:
+ if (TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces) {
+ break;
+ }
+ [[fallthrough]];
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eInsertLineBreak:
+ case EditSubAction::eInsertParagraphSeparator:
+ case EditSubAction::ePasteHTMLContent:
+ case EditSubAction::eInsertHTMLSource: {
+ // Due to the replacement of white-spaces in
+ // WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(),
+ // selection ranges may be changed since DOM ranges track the DOM
+ // mutation by themselves. However, we want to keep selection as-is.
+ // Therefore, we should restore `Selection` after replacing
+ // white-spaces.
+ AutoSelectionRestorer restoreSelection(*this);
+ // TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII
+ // white-spaces with NPSPs and then, we'll replace them with ASCII
+ // white-spaces here. We should avoid this overwriting things as
+ // far as possible because replacing characters in text nodes
+ // causes running mutation event listeners which are really
+ // expensive.
+ // Adjust end of composition string if there is composition string.
+ auto pointToAdjust = GetLastIMESelectionEndPoint<EditorDOMPoint>();
+ if (!pointToAdjust.IsInContentNode()) {
+ // Otherwise, adjust current selection start point.
+ pointToAdjust = GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToAdjust.IsInContentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ if (EditorUtils::IsEditableContent(
+ *pointToAdjust.ContainerAs<nsIContent>(), EditorType::HTML)) {
+ AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(),
+ &pointToAdjust);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ *this, pointToAdjust);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
+ "failed");
+ return rv;
+ }
+ }
+
+ // also do this for original selection endpoints.
+ // XXX Hmm, if `NormalizeVisibleWhiteSpacesAt()` runs mutation event
+ // listener and that causes changing `mSelectedRange`, what we
+ // should do?
+ if (NS_WARN_IF(!TopLevelEditSubActionDataRef()
+ .mSelectedRange->IsPositioned())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorDOMPoint atStart =
+ TopLevelEditSubActionDataRef().mSelectedRange->StartPoint();
+ if (atStart != pointToAdjust && atStart.IsInContentNode() &&
+ EditorUtils::IsEditableContent(*atStart.ContainerAs<nsIContent>(),
+ EditorType::HTML)) {
+ AutoTrackDOMPoint trackPointToAdjust(RangeUpdaterRef(),
+ &pointToAdjust);
+ AutoTrackDOMPoint trackStartPoint(RangeUpdaterRef(), &atStart);
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(
+ *this, atStart);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
+ "failed, but ignored");
+ }
+ // we only need to handle old selection endpoint if it was different
+ // from start
+ EditorDOMPoint atEnd =
+ TopLevelEditSubActionDataRef().mSelectedRange->EndPoint();
+ if (!TopLevelEditSubActionDataRef().mSelectedRange->Collapsed() &&
+ atEnd != pointToAdjust && atEnd != atStart &&
+ atEnd.IsInContentNode() &&
+ EditorUtils::IsEditableContent(*atEnd.ContainerAs<nsIContent>(),
+ EditorType::HTML)) {
+ nsresult rv =
+ WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt(*this,
+ atEnd);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "WhiteSpaceVisibilityKeeper::NormalizeVisibleWhiteSpacesAt() "
+ "failed, but ignored");
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ // Adjust selection for insert text, html paste, and delete actions if
+ // we haven't removed new empty blocks. Note that if empty block parents
+ // are removed, Selection should've been adjusted by the method which
+ // did it.
+ if (!TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks &&
+ SelectionRef().IsCollapsed()) {
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eDeleteSelectedContent:
+ case EditSubAction::eInsertLineBreak:
+ case EditSubAction::eInsertParagraphSeparator:
+ case EditSubAction::ePasteHTMLContent:
+ case EditSubAction::eInsertHTMLSource:
+ // XXX AdjustCaretPositionAndEnsurePaddingBRElement() intentionally
+ // does not create padding `<br>` element for empty editor.
+ // Investigate which is better that whether this should does it
+ // or wait MaybeCreatePaddingBRElementForEmptyEditor().
+ rv = AdjustCaretPositionAndEnsurePaddingBRElement(
+ GetDirectionOfTopLevelEditSubAction());
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::AdjustCaretPositionAndEnsurePaddingBRElement() "
+ "failed");
+ return rv;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ // check for any styles which were removed inappropriately
+ bool reapplyCachedStyle;
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eDeleteSelectedContent:
+ reapplyCachedStyle = true;
+ break;
+ default:
+ reapplyCachedStyle =
+ IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction());
+ break;
+ }
+
+ // If the selection is in empty inline HTML elements, we should delete
+ // them unless it's inserted intentionally.
+ if (mPlaceholderBatch &&
+ TopLevelEditSubActionDataRef().mNeedsToCleanUpEmptyElements &&
+ SelectionRef().IsCollapsed() && SelectionRef().GetFocusNode()) {
+ RefPtr<Element> mostDistantEmptyInlineAncestor = nullptr;
+ for (Element* ancestor :
+ SelectionRef().GetFocusNode()->InclusiveAncestorsOfType<Element>()) {
+ if (!ancestor->IsHTMLElement() ||
+ !HTMLEditUtils::IsRemovableFromParentNode(*ancestor) ||
+ !HTMLEditUtils::IsEmptyInlineContainer(
+ *ancestor, {EmptyCheckOption::TreatSingleBRElementAsVisible},
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ break;
+ }
+ mostDistantEmptyInlineAncestor = ancestor;
+ }
+ if (mostDistantEmptyInlineAncestor) {
+ nsresult rv =
+ DeleteNodeWithTransaction(*mostDistantEmptyInlineAncestor);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteNodeWithTransaction() failed at deleting "
+ "empty inline ancestors");
+ return rv;
+ }
+ }
+ }
+
+ // But the cached inline styles should be restored from type-in-state later.
+ if (reapplyCachedStyle) {
+ DebugOnly<nsresult> rvIgnored =
+ mPendingStylesToApplyToNewContent->UpdateSelState(*this);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "PendingStyles::UpdateSelState() failed, but ignored");
+ rvIgnored = ReapplyCachedStyles();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "HTMLEditor::ReapplyCachedStyles() failed, but ignored");
+ TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
+ }
+ }
+
+ rv = HandleInlineSpellCheck(
+ TopLevelEditSubActionDataRef().mSelectedRange->StartPoint(),
+ TopLevelEditSubActionDataRef().mChangedRange);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::HandleInlineSpellCheck() failed");
+ return rv;
+ }
+
+ // detect empty doc
+ // XXX Need to investigate when the padding <br> element is removed because
+ // I don't see the <br> element with testing manually. If it won't be
+ // used, we can get rid of this cost.
+ rv = MaybeCreatePaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed");
+ return rv;
+ }
+
+ // adjust selection HINT if needed
+ if (!TopLevelEditSubActionDataRef().mDidExplicitlySetInterLine &&
+ SelectionRef().IsCollapsed()) {
+ SetSelectionInterlinePosition();
+ }
+
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::CanHandleHTMLEditSubAction(
+ CheckSelectionInReplacedElement aCheckSelectionInReplacedElement
+ /* = CheckSelectionInReplacedElement::Yes */) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ // If there is not selection ranges, we should ignore the result.
+ if (!SelectionRef().RangeCount()) {
+ return EditActionResult::CanceledResult();
+ }
+
+ const nsRange* range = SelectionRef().GetRangeAt(0);
+ nsINode* selStartNode = range->GetStartContainer();
+ if (NS_WARN_IF(!selStartNode) || NS_WARN_IF(!selStartNode->IsContent())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (!HTMLEditUtils::IsSimplyEditableNode(*selStartNode)) {
+ return EditActionResult::CanceledResult();
+ }
+
+ nsINode* selEndNode = range->GetEndContainer();
+ if (NS_WARN_IF(!selEndNode) || NS_WARN_IF(!selEndNode->IsContent())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (selStartNode == selEndNode) {
+ if (aCheckSelectionInReplacedElement ==
+ CheckSelectionInReplacedElement::Yes &&
+ HTMLEditUtils::IsNonEditableReplacedContent(
+ *selStartNode->AsContent())) {
+ return EditActionResult::CanceledResult();
+ }
+ return EditActionResult::IgnoredResult();
+ }
+
+ if (HTMLEditUtils::IsNonEditableReplacedContent(*selStartNode->AsContent()) ||
+ HTMLEditUtils::IsNonEditableReplacedContent(*selEndNode->AsContent())) {
+ return EditActionResult::CanceledResult();
+ }
+
+ if (!HTMLEditUtils::IsSimplyEditableNode(*selEndNode)) {
+ return EditActionResult::CanceledResult();
+ }
+
+ // If anchor node is in an HTML element which has inert attribute, we should
+ // do nothing.
+ // XXX HTMLEditor typically uses first range instead of anchor/focus range.
+ // Therefore, referring first range here is more reasonable than
+ // anchor/focus range of Selection.
+ nsIContent* const selAnchorContent = SelectionRef().GetDirection() == eDirNext
+ ? nsIContent::FromNode(selStartNode)
+ : nsIContent::FromNode(selEndNode);
+ if (selAnchorContent &&
+ HTMLEditUtils::ContentIsInert(*selAnchorContent->AsContent())) {
+ return EditActionResult::CanceledResult();
+ }
+
+ // XXX What does it mean the common ancestor is editable? I have no idea.
+ // It should be in same (active) editing host, and even if it's editable,
+ // there may be non-editable contents in the range.
+ nsINode* commonAncestor = range->GetClosestCommonInclusiveAncestor();
+ if (MOZ_UNLIKELY(!commonAncestor)) {
+ NS_WARNING(
+ "AbstractRange::GetClosestCommonInclusiveAncestor() returned nullptr");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return HTMLEditUtils::IsSimplyEditableNode(*commonAncestor)
+ ? EditActionResult::IgnoredResult()
+ : EditActionResult::CanceledResult();
+}
+
+MOZ_CAN_RUN_SCRIPT static nsStaticAtom& MarginPropertyAtomForIndent(
+ nsIContent& aContent) {
+ nsAutoString direction;
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetComputedProperty(
+ aContent, *nsGkAtoms::direction, direction);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetComputedProperty(nsGkAtoms::direction)"
+ " failed, but ignored");
+ return direction.EqualsLiteral("rtl") ? *nsGkAtoms::marginRight
+ : *nsGkAtoms::marginLeft;
+}
+
+nsresult HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(SelectionRef().IsCollapsed());
+
+ // If we are after a padding `<br>` element for empty last line in the same
+ // block, then move selection to be before it
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorRawDOMPoint atSelectionStart(firstRange->StartRef());
+ if (NS_WARN_IF(!atSelectionStart.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atSelectionStart.IsSetAndValid());
+
+ if (!atSelectionStart.IsInContentNode()) {
+ return NS_OK;
+ }
+
+ Element* editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ NS_WARNING(
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() did nothing "
+ "because of no editing host");
+ return NS_OK;
+ }
+
+ nsIContent* previousBRElement = HTMLEditUtils::GetPreviousContent(
+ atSelectionStart, {}, BlockInlineCheck::UseComputedDisplayStyle,
+ editingHost);
+ if (!previousBRElement || !previousBRElement->IsHTMLElement(nsGkAtoms::br) ||
+ !previousBRElement->GetParent() ||
+ !EditorUtils::IsEditableContent(*previousBRElement->GetParent(),
+ EditorType::HTML) ||
+ !HTMLEditUtils::IsInvisibleBRElement(*previousBRElement)) {
+ return NS_OK;
+ }
+
+ const RefPtr<const Element> blockElementAtSelectionStart =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *atSelectionStart.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ const RefPtr<const Element> parentBlockElementOfBRElement =
+ HTMLEditUtils::GetAncestorElement(
+ *previousBRElement, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+
+ if (!blockElementAtSelectionStart ||
+ blockElementAtSelectionStart != parentBlockElementOfBRElement) {
+ return NS_OK;
+ }
+
+ // If we are here then the selection is right after a padding <br>
+ // element for empty last line that is in the same block as the
+ // selection. We need to move the selection start to be before the
+ // padding <br> element.
+ EditorRawDOMPoint atInvisibleBRElement(previousBRElement);
+ nsresult rv = CollapseSelectionTo(atInvisibleBRElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::MaybeCreatePaddingBRElementForEmptyEditor() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (mPaddingBRElementForEmptyEditor) {
+ return NS_OK;
+ }
+
+ // XXX I think that we should not insert a <br> element if we're for a web
+ // content. Probably, this is required only by chrome editors such as
+ // the mail composer of Thunderbird and the composer of SeaMonkey.
+
+ const RefPtr<Element> bodyOrDocumentElement = GetRoot();
+ if (!bodyOrDocumentElement) {
+ return NS_OK;
+ }
+
+ // Skip adding the padding <br> element for empty editor if body
+ // is read-only.
+ if (!HTMLEditUtils::IsSimplyEditableNode(*bodyOrDocumentElement)) {
+ return NS_OK;
+ }
+
+ // Now we've got the body element. Iterate over the body element's children,
+ // looking for editable content. If no editable content is found, insert the
+ // padding <br> element.
+ EditorType editorType = GetEditorType();
+ bool isRootEditable =
+ EditorUtils::IsEditableContent(*bodyOrDocumentElement, editorType);
+ for (nsIContent* child = bodyOrDocumentElement->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (EditorUtils::IsPaddingBRElementForEmptyEditor(*child) ||
+ !isRootEditable || EditorUtils::IsEditableContent(*child, editorType) ||
+ HTMLEditUtils::IsBlockElement(
+ *child, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return NS_OK;
+ }
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eCreatePaddingBRElementForEmptyEditor,
+ nsIEditor::eNone, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // Create a br.
+ RefPtr<Element> newBRElement = CreateHTMLContent(nsGkAtoms::br);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_WARN_IF(!newBRElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mPaddingBRElementForEmptyEditor =
+ static_cast<HTMLBRElement*>(newBRElement.get());
+
+ // Give it a special attribute.
+ newBRElement->SetFlags(NS_PADDING_FOR_EMPTY_EDITOR);
+
+ // Put the node in the document.
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertNodeWithTransaction<Element>(
+ *newBRElement, EditorDOMPoint(bodyOrDocumentElement, 0u));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertBRElementResult.unwrapErr();
+ }
+
+ // Set selection.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionToStartOf() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+ return NS_OK;
+}
+
+nsresult HTMLEditor::EnsureNoPaddingBRElementForEmptyEditor() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!mPaddingBRElementForEmptyEditor) {
+ return NS_OK;
+ }
+
+ // If we're an HTML editor, a mutation event listener may recreate padding
+ // <br> element for empty editor again during the call of
+ // DeleteNodeWithTransaction(). So, move it first.
+ RefPtr<HTMLBRElement> paddingBRElement(
+ std::move(mPaddingBRElementForEmptyEditor));
+ nsresult rv = DeleteNodeWithTransaction(*paddingBRElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::ReflectPaddingBRElementForEmptyEditor() {
+ if (NS_WARN_IF(!mRootElement)) {
+ NS_WARNING("Failed to handle padding BR element due to no root element");
+ return NS_ERROR_FAILURE;
+ }
+ // The idea here is to see if the magic empty node has suddenly reappeared. If
+ // it has, set our state so we remember it. There is a tradeoff between doing
+ // here and at redo, or doing it everywhere else that might care. Since undo
+ // and redo are relatively rare, it makes sense to take the (small)
+ // performance hit here.
+ nsIContent* firstLeafChild = HTMLEditUtils::GetFirstLeafContent(
+ *mRootElement, {LeafNodeType::OnlyLeafNode});
+ if (firstLeafChild &&
+ EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) {
+ mPaddingBRElementForEmptyEditor =
+ static_cast<HTMLBRElement*>(firstLeafChild);
+ } else {
+ mPaddingBRElementForEmptyEditor = nullptr;
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::PrepareInlineStylesForCaret() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(SelectionRef().IsCollapsed());
+
+ // XXX This method works with the top level edit sub-action, but this
+ // must be wrong if we are handling nested edit action.
+
+ if (TopLevelEditSubActionDataRef().mDidDeleteSelection) {
+ switch (GetTopLevelEditSubAction()) {
+ case EditSubAction::eInsertText:
+ case EditSubAction::eInsertTextComingFromIME:
+ case EditSubAction::eDeleteSelectedContent: {
+ nsresult rv = ReapplyCachedStyles();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::ReapplyCachedStyles() failed");
+ return rv;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ // For most actions we want to clear the cached styles, but there are
+ // exceptions
+ if (!IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) {
+ TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
+ }
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::HandleInsertText(
+ EditSubAction aEditSubAction, const nsAString& aInsertionString,
+ SelectionHandling aSelectionHandling) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText ||
+ aEditSubAction == EditSubAction::eInsertTextComingFromIME);
+ MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore,
+ aEditSubAction == EditSubAction::eInsertTextComingFromIME);
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ UndefineCaretBidiLevel();
+
+ // If the selection isn't collapsed, delete it. Don't delete existing inline
+ // tags, because we're hopefully going to insert text (bug 787432).
+ if (!SelectionRef().IsCollapsed() &&
+ aSelectionHandling == SelectionHandling::Delete) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(nsIEditor::eNone, "
+ "nsIEditor::eNoStrip) failed");
+ return Err(rv);
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost(
+ GetDocument()->IsXMLDocument() ? LimitInBodyElement::No
+ : LimitInBodyElement::Yes);
+ if (NS_WARN_IF(!editingHost)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ auto pointToInsert = GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (MOZ_UNLIKELY(!pointToInsert.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // for every property that is set, insert a new inline style node
+ Result<EditorDOMPoint, nsresult> setStyleResult =
+ CreateStyleForInsertText(pointToInsert, *editingHost);
+ if (MOZ_UNLIKELY(setStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed");
+ return setStyleResult.propagateErr();
+ }
+ if (setStyleResult.inspect().IsSet()) {
+ pointToInsert = setStyleResult.unwrap();
+ }
+
+ if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) ||
+ NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ MOZ_ASSERT(pointToInsert.IsSetAndValid());
+
+ // If the point is not in an element which can contain text nodes, climb up
+ // the DOM tree.
+ if (!pointToInsert.IsInTextNode()) {
+ while (!HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(),
+ *nsGkAtoms::textTagName)) {
+ if (NS_WARN_IF(pointToInsert.GetContainer() == editingHost) ||
+ NS_WARN_IF(!pointToInsert.GetContainerParentAs<nsIContent>())) {
+ NS_WARNING("Selection start point couldn't have text nodes");
+ return Err(NS_ERROR_FAILURE);
+ }
+ pointToInsert.Set(pointToInsert.ContainerAs<nsIContent>());
+ }
+ }
+
+ if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
+ auto compositionStartPoint =
+ GetFirstIMESelectionStartPoint<EditorDOMPoint>();
+ if (!compositionStartPoint.IsSet()) {
+ compositionStartPoint = pointToInsert;
+ }
+
+ if (aInsertionString.IsEmpty()) {
+ // Right now the WhiteSpaceVisibilityKeeper code bails on empty strings,
+ // but IME needs the InsertTextWithTransaction() call to still happen
+ // since empty strings are meaningful there.
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, aInsertionString,
+ compositionStartPoint);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ nsresult rv = insertTextResult.unwrap().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+
+ auto compositionEndPoint = GetLastIMESelectionEndPoint<EditorDOMPoint>();
+ if (!compositionEndPoint.IsSet()) {
+ compositionEndPoint = compositionStartPoint;
+ }
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ WhiteSpaceVisibilityKeeper::ReplaceText(
+ *this, aInsertionString,
+ EditorDOMRange(compositionStartPoint, compositionEndPoint),
+ *editingHost);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::ReplaceText() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // CompositionTransaction should've set selection so that we should ignore
+ // caret suggestion.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+
+ compositionStartPoint = GetFirstIMESelectionStartPoint<EditorDOMPoint>();
+ compositionEndPoint = GetLastIMESelectionEndPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!compositionStartPoint.IsSet()) ||
+ NS_WARN_IF(!compositionEndPoint.IsSet())) {
+ // Mutation event listener has changed the DOM tree...
+ return EditActionResult::HandledResult();
+ }
+ nsresult rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
+ compositionStartPoint.ToRawRangeBoundary(),
+ compositionEndPoint.ToRawRangeBoundary());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText);
+
+ // find where we are
+ EditorDOMPoint currentPoint(pointToInsert);
+
+ // is our text going to be PREformatted?
+ // We remember this so that we know how to handle tabs.
+ const bool isWhiteSpaceCollapsible = !EditorUtils::IsWhiteSpacePreformatted(
+ *pointToInsert.ContainerAs<nsIContent>());
+
+ // turn off the edit listener: we know how to
+ // build the "doc changed range" ourselves, and it's
+ // must faster to do it once here than to track all
+ // the changes one at a time.
+ AutoRestore<bool> disableListener(
+ EditSubActionDataRef().mAdjustChangedRangeFromListener);
+ EditSubActionDataRef().mAdjustChangedRangeFromListener = false;
+
+ // don't change my selection in subtransactions
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+ int32_t pos = 0;
+ constexpr auto newlineStr = NS_LITERAL_STRING_FROM_CSTRING(LFSTR);
+
+ {
+ AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToInsert);
+
+ // for efficiency, break out the pre case separately. This is because
+ // its a lot cheaper to search the input string for only newlines than
+ // it is to search for both tabs and newlines.
+ if (!isWhiteSpaceCollapsible || IsPlaintextMailComposer()) {
+ while (pos != -1 &&
+ pos < AssertedCast<int32_t>(aInsertionString.Length())) {
+ int32_t oldPos = pos;
+ int32_t subStrLen;
+ pos = aInsertionString.FindChar(nsCRT::LF, oldPos);
+
+ if (pos != -1) {
+ subStrLen = pos - oldPos;
+ // if first char is newline, then use just it
+ if (!subStrLen) {
+ subStrLen = 1;
+ }
+ } else {
+ subStrLen = aInsertionString.Length() - oldPos;
+ pos = aInsertionString.Length();
+ }
+
+ nsDependentSubstring subStr(aInsertionString, oldPos, subStrLen);
+
+ // is it a return?
+ if (subStr.Equals(newlineStr)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, currentPoint);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // We don't want to update selection here because we've blocked
+ // InsertNodeTransaction updating selection with
+ // dontChangeMySelection.
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(!AllowsTransactionsToChangeSelection());
+
+ pos++;
+ RefPtr<Element> brElement =
+ unwrappedInsertBRElementResult.UnwrapNewNode();
+ if (brElement->GetNextSibling()) {
+ pointToInsert.Set(brElement->GetNextSibling());
+ } else {
+ pointToInsert.SetToEndOf(currentPoint.GetContainer());
+ }
+ // XXX In most cases, pointToInsert and currentPoint are same here.
+ // But if the <br> element has been moved to different point by
+ // mutation observer, those points become different.
+ currentPoint.SetAfter(brElement);
+ NS_WARNING_ASSERTION(currentPoint.IsSet(),
+ "Failed to set after the <br> element");
+ NS_WARNING_ASSERTION(currentPoint == pointToInsert,
+ "Perhaps, <br> element position has been moved "
+ "to different point "
+ "by mutation observer");
+ } else {
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, subStr, currentPoint);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ // Ignore the caret suggestion because of `dontChangeMySelection`
+ // above.
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ if (insertTextResult.inspect().Handled()) {
+ pointToInsert = currentPoint = insertTextResult.unwrap()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>();
+ } else {
+ pointToInsert = currentPoint;
+ }
+ }
+ }
+ } else {
+ constexpr auto tabStr = u"\t"_ns;
+ constexpr auto spacesStr = u" "_ns;
+ nsAutoString insertionString(aInsertionString); // For FindCharInSet().
+ while (pos != -1 &&
+ pos < AssertedCast<int32_t>(insertionString.Length())) {
+ int32_t oldPos = pos;
+ int32_t subStrLen;
+ pos = insertionString.FindCharInSet(u"\t\n", oldPos);
+
+ if (pos != -1) {
+ subStrLen = pos - oldPos;
+ // if first char is newline, then use just it
+ if (!subStrLen) {
+ subStrLen = 1;
+ }
+ } else {
+ subStrLen = insertionString.Length() - oldPos;
+ pos = insertionString.Length();
+ }
+
+ nsDependentSubstring subStr(insertionString, oldPos, subStrLen);
+
+ // is it a tab?
+ if (subStr.Equals(tabStr)) {
+ Result<InsertTextResult, nsresult> insertTextResult =
+ WhiteSpaceVisibilityKeeper::InsertText(
+ *this, spacesStr, currentPoint, *editingHost);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed");
+ return insertTextResult.propagateErr();
+ }
+ // Ignore the caret suggestion because of `dontChangeMySelection`
+ // above.
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ pos++;
+ if (insertTextResult.inspect().Handled()) {
+ pointToInsert = currentPoint = insertTextResult.unwrap()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>();
+ MOZ_ASSERT(pointToInsert.IsSet());
+ } else {
+ pointToInsert = currentPoint;
+ MOZ_ASSERT(pointToInsert.IsSet());
+ }
+ }
+ // is it a return?
+ else if (subStr.Equals(newlineStr)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ WhiteSpaceVisibilityKeeper::InsertBRElement(*this, currentPoint,
+ *editingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // TODO: Some methods called for handling non-preformatted text use
+ // ComputeEditingHost(). Therefore, they depend on the latest
+ // selection. So we cannot skip updating selection here.
+ nsresult rv = unwrappedInsertBRElementResult.SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ pos++;
+ RefPtr<Element> newBRElement =
+ unwrappedInsertBRElementResult.UnwrapNewNode();
+ MOZ_DIAGNOSTIC_ASSERT(newBRElement);
+ if (newBRElement->GetNextSibling()) {
+ pointToInsert.Set(newBRElement->GetNextSibling());
+ } else {
+ pointToInsert.SetToEndOf(currentPoint.GetContainer());
+ }
+ currentPoint.SetAfter(newBRElement);
+ NS_WARNING_ASSERTION(currentPoint.IsSet(),
+ "Failed to set after the new <br> element");
+ // XXX If the newBRElement has been moved or removed by mutation
+ // observer, we hit this assert. We need to check if
+ // newBRElement is in expected point, though, we must have
+ // a lot of same bugs...
+ NS_WARNING_ASSERTION(
+ currentPoint == pointToInsert,
+ "Perhaps, newBRElement has been moved or removed unexpectedly");
+ } else {
+ Result<InsertTextResult, nsresult> insertTextResult =
+ WhiteSpaceVisibilityKeeper::InsertText(
+ *this, subStr, currentPoint, *editingHost);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertText() failed");
+ return insertTextResult.propagateErr();
+ }
+ // Ignore the caret suggestion because of `dontChangeMySelection`
+ // above.
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ if (insertTextResult.inspect().Handled()) {
+ pointToInsert = currentPoint = insertTextResult.unwrap()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>();
+ MOZ_ASSERT(pointToInsert.IsSet());
+ } else {
+ pointToInsert = currentPoint;
+ MOZ_ASSERT(pointToInsert.IsSet());
+ }
+ }
+ }
+ }
+
+ // After this block, pointToInsert is updated by AutoTrackDOMPoint.
+ }
+
+ if (currentPoint.IsSet()) {
+ currentPoint.SetInterlinePosition(InterlinePosition::EndOfLine);
+ nsresult rv = CollapseSelectionTo(currentPoint);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Selection::Collapse() failed, but ignored");
+
+ // manually update the doc changed range so that AfterEdit will clean up
+ // the correct portion of the document.
+ rv = TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
+ pointToInsert.ToRawRangeBoundary(), currentPoint.ToRawRangeBoundary());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ DebugOnly<nsresult> rvIgnored =
+ SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "EndOfLine) failed, but ignored");
+ rv = TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(pointToInsert);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsRange::CollapseTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::InsertLineBreakAsSubAction() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ // XXX This may be called by execCommand() with "insertLineBreak".
+ // In such case, naming the transaction "TypingTxnName" is odd.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+
+ // calling it text insertion to trigger moz br treatment by rules
+ // XXX Why do we use EditSubAction::eInsertText here? Looks like
+ // EditSubAction::eInsertLineBreak or EditSubAction::eInsertNode
+ // is better.
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ UndefineCaretBidiLevel();
+
+ // If the selection isn't collapsed, delete it.
+ if (!SelectionRef().IsCollapsed()) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed");
+ return rv;
+ }
+ }
+
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorDOMPoint atStartOfSelection(firstRange->StartRef());
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
+
+ RefPtr<Element> editingHost = ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // For backward compatibility, we should not insert a linefeed if
+ // paragraph separator is set to "br" which is Gecko-specific mode.
+ if (GetDefaultParagraphSeparator() == ParagraphSeparator::br ||
+ !HTMLEditUtils::ShouldInsertLinefeedCharacter(atStartOfSelection,
+ *editingHost)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, atStartOfSelection,
+ nsIEditor::eNext);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+
+ auto pointToPutCaret =
+ EditorDOMPoint::After(*unwrappedInsertBRElementResult.GetNewNode());
+ if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) {
+ NS_WARNING("Inserted <br> was unexpectedly removed");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ WSScanResult backwardScanFromBeforeBRElementResult =
+ WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
+ editingHost,
+ EditorDOMPoint(unwrappedInsertBRElementResult.GetNewNode()),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(backwardScanFromBeforeBRElementResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ WSScanResult forwardScanFromAfterBRElementResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ editingHost, pointToPutCaret,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool brElementIsAfterBlock =
+ backwardScanFromBeforeBRElementResult.ReachedBlockBoundary();
+ const bool brElementIsBeforeBlock =
+ forwardScanFromAfterBRElementResult.ReachedBlockBoundary();
+ const bool isEmptyEditingHost = HTMLEditUtils::IsEmptyNode(
+ *editingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible});
+ if (brElementIsBeforeBlock &&
+ (isEmptyEditingHost || !brElementIsAfterBlock)) {
+ // Empty last line is invisible if it's immediately before either parent
+ // or another block's boundary so that we need to put invisible <br>
+ // element here for making it visible.
+ Result<CreateElementResult, nsresult> invisibleAdditionalBRElementResult =
+ WhiteSpaceVisibilityKeeper::InsertBRElement(*this, pointToPutCaret,
+ *editingHost);
+ if (MOZ_UNLIKELY(invisibleAdditionalBRElementResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed");
+ return invisibleAdditionalBRElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInvisibleAdditionalBRElement =
+ invisibleAdditionalBRElementResult.unwrap();
+ pointToPutCaret.Set(unwrappedInvisibleAdditionalBRElement.GetNewNode());
+ unwrappedInvisibleAdditionalBRElement.IgnoreCaretPointSuggestion();
+ } else if (forwardScanFromAfterBRElementResult
+ .InVisibleOrCollapsibleCharacters()) {
+ pointToPutCaret =
+ forwardScanFromAfterBRElementResult.Point<EditorDOMPoint>();
+ } else if (forwardScanFromAfterBRElementResult.ReachedSpecialContent()) {
+ // Next inserting text should be inserted into styled inline elements if
+ // they have first visible thing in the new line.
+ pointToPutCaret =
+ forwardScanFromAfterBRElementResult.PointAtContent<EditorDOMPoint>();
+ }
+
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "CreateElementResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ atStartOfSelection = EditorDOMPoint(firstRange->StartRef());
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
+
+ // Do nothing if the node is read-only
+ if (!HTMLEditUtils::IsSimplyEditableNode(
+ *atStartOfSelection.GetContainer())) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ Result<EditorDOMPoint, nsresult> insertLineFeedResult =
+ HandleInsertLinefeed(atStartOfSelection, *editingHost);
+ if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertLinefeed() failed");
+ return insertLineFeedResult.unwrapErr();
+ }
+ rv = CollapseSelectionTo(insertLineFeedResult.inspect());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::InsertParagraphSeparatorAsSubAction(const Element& aEditingHost) {
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction(
+ CheckSelectionInReplacedElement::OnlyWhenNotInSameNode);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ // XXX This may be called by execCommand() with "insertParagraph".
+ // In such case, naming the transaction "TypingTxnName" is odd.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertParagraphSeparator, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ UndefineCaretBidiLevel();
+
+ // If the selection isn't collapsed, delete it.
+ if (!SelectionRef().IsCollapsed()) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed");
+ return Err(rv);
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ AutoRangeArray selectionRanges(SelectionRef());
+ {
+ // If the editing host is the body element, the selection may be outside
+ // aEditingHost. In the case, we should use the editing host outside the
+ // <body> only here for keeping our traditional behavior for now.
+ // This should be fixed in bug 1634351.
+ const Element* editingHostMaybeOutsideBody = &aEditingHost;
+ if (aEditingHost.IsHTMLElement(nsGkAtoms::body)) {
+ editingHostMaybeOutsideBody = ComputeEditingHost(LimitInBodyElement::No);
+ if (NS_WARN_IF(!editingHostMaybeOutsideBody)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ selectionRanges.EnsureOnlyEditableRanges(*editingHostMaybeOutsideBody);
+ if (NS_WARN_IF(selectionRanges.Ranges().IsEmpty())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ auto pointToInsert =
+ selectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ while (true) {
+ Element* element = pointToInsert.GetContainerOrContainerParentElement();
+ if (MOZ_UNLIKELY(!element)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // If the element can have a <br> element (it means that the element or its
+ // container must be able to have <div> or <p> too), we can handle
+ // insertParagraph at the point.
+ if (HTMLEditUtils::CanNodeContain(*element, *nsGkAtoms::br)) {
+ break;
+ }
+ // Otherwise, try to insert paragraph at the parent.
+ pointToInsert = pointToInsert.ParentPoint();
+ }
+
+ if (IsMailEditor()) {
+ if (RefPtr<Element> mailCiteElement = GetMostDistantAncestorMailCiteElement(
+ *pointToInsert.ContainerAs<nsIContent>())) {
+ // Split any mailcites in the way. Should we abort this if we encounter
+ // table cell boundaries?
+ Result<EditorDOMPoint, nsresult> atNewBRElementOrError =
+ HandleInsertParagraphInMailCiteElement(*mailCiteElement,
+ pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(atNewBRElementOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::HandleInsertParagraphInMailCiteElement() failed");
+ return atNewBRElementOrError.propagateErr();
+ }
+ EditorDOMPoint pointToPutCaret = atNewBRElementOrError.unwrap();
+ MOZ_ASSERT(pointToPutCaret.IsSet());
+ pointToPutCaret.SetInterlinePosition(InterlinePosition::StartOfNextLine);
+ MOZ_ASSERT(pointToPutCaret.GetChild());
+ MOZ_ASSERT(pointToPutCaret.GetChild()->IsHTMLElement(nsGkAtoms::br));
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+ }
+
+ // If the active editing host is an inline element, or if the active editing
+ // host is the block parent itself and we're configured to use <br> as a
+ // paragraph separator, just append a <br>.
+ // If the editing host parent element is editable, it means that the editing
+ // host must be a <body> element and the selection may be outside the body
+ // element. If the selection is outside the editing host, we should not
+ // insert new paragraph nor <br> element.
+ // XXX Currently, we don't support editing outside <body> element, but Blink
+ // does it.
+ if (aEditingHost.GetParentElement() &&
+ HTMLEditUtils::IsSimplyEditableNode(*aEditingHost.GetParentElement()) &&
+ !nsContentUtils::ContentIsFlattenedTreeDescendantOf(
+ pointToInsert.ContainerAs<nsIContent>(), &aEditingHost)) {
+ return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
+ }
+
+ auto InsertLineBreakInstead =
+ [](const Element* aEditableBlockElement,
+ const EditorDOMPoint& aCandidatePointToSplit,
+ ParagraphSeparator aDefaultParagraphSeparator,
+ const Element& aEditingHost) {
+ // If there is no block parent in the editing host, i.e., the editing
+ // host itself is also a non-block element, we should insert a line
+ // break.
+ if (!aEditableBlockElement) {
+ // XXX Chromium checks if the CSS box of the editing host is a block.
+ return true;
+ }
+
+ // If the editable block element is not splittable, e.g., it's an
+ // editing host, and the default paragraph separator is <br> or the
+ // element cannot contain a <p> element, we should insert a <br>
+ // element.
+ if (!HTMLEditUtils::IsSplittableNode(*aEditableBlockElement)) {
+ return aDefaultParagraphSeparator == ParagraphSeparator::br ||
+ !HTMLEditUtils::CanElementContainParagraph(
+ *aEditableBlockElement) ||
+ (HTMLEditUtils::ShouldInsertLinefeedCharacter(
+ aCandidatePointToSplit, aEditingHost) &&
+ HTMLEditUtils::IsDisplayOutsideInline(aEditingHost));
+ }
+
+ // If the nearest block parent is a single-line container declared in
+ // the execCommand spec and not the editing host, we should separate the
+ // block even if the default paragraph separator is <br> element.
+ if (HTMLEditUtils::IsSingleLineContainer(*aEditableBlockElement)) {
+ return false;
+ }
+
+ // Otherwise, unless there is no block ancestor which can contain <p>
+ // element, we shouldn't insert a line break here.
+ for (const Element* editableBlockAncestor = aEditableBlockElement;
+ editableBlockAncestor;
+ editableBlockAncestor = HTMLEditUtils::GetAncestorElement(
+ *editableBlockAncestor,
+ HTMLEditUtils::ClosestEditableBlockElementOrButtonElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ if (HTMLEditUtils::CanElementContainParagraph(
+ *editableBlockAncestor)) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ // Look for the nearest parent block. However, don't return error even if
+ // there is no block parent here because in such case, i.e., editing host
+ // is an inline element, we should insert <br> simply.
+ RefPtr<Element> editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *pointToInsert.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrButtonElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+
+ // If we cannot insert a <p>/<div> element at the selection, we should insert
+ // a <br> element or a linefeed instead.
+ const ParagraphSeparator separator = GetDefaultParagraphSeparator();
+ if (InsertLineBreakInstead(editableBlockElement, pointToInsert, separator,
+ aEditingHost)) {
+ // For backward compatibility, we should not insert a linefeed if
+ // paragraph separator is set to "br" which is Gecko-specific mode.
+ if (separator != ParagraphSeparator::br &&
+ HTMLEditUtils::ShouldInsertLinefeedCharacter(pointToInsert,
+ aEditingHost)) {
+ Result<EditorDOMPoint, nsresult> insertLineFeedResult =
+ HandleInsertLinefeed(pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertLinefeed() failed");
+ return insertLineFeedResult.propagateErr();
+ }
+ nsresult rv = CollapseSelectionTo(insertLineFeedResult.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ HandleInsertBRElement(pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertBRElement() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ nsresult rv =
+ insertBRElementResult.inspect().SuggestCaretPointTo(*this, {});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ // If somebody wants to restrict caret position in a block element below,
+ // we should guarantee it. Otherwise, we can put caret to the candidate
+ // point.
+ auto CollapseSelection =
+ [this](const EditorDOMPoint& aCandidatePointToPutCaret,
+ const Element* aBlockElementShouldHaveCaret,
+ const SuggestCaretOptions& aOptions)
+ MOZ_CAN_RUN_SCRIPT -> nsresult {
+ if (!aCandidatePointToPutCaret.IsSet()) {
+ if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion)) {
+ return NS_OK;
+ }
+ return aOptions.contains(SuggestCaret::AndIgnoreTrivialError)
+ ? NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR
+ : NS_ERROR_FAILURE;
+ }
+ EditorDOMPoint pointToPutCaret(aCandidatePointToPutCaret);
+ if (aBlockElementShouldHaveCaret) {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorDOMPoint>(*aBlockElementShouldHaveCaret,
+ aCandidatePointToPutCaret);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() "
+ "failed, but ignored");
+ } else if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv) && MOZ_LIKELY(rv != NS_ERROR_EDITOR_DESTROYED) &&
+ aOptions.contains(SuggestCaret::AndIgnoreTrivialError)) {
+ rv = NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR;
+ }
+ return rv;
+ };
+
+ RefPtr<Element> blockElementToPutCaret;
+ // If the default paragraph separator is not <br> and selection is not in
+ // a splittable block element, we should wrap selected contents in a new
+ // paragraph, then, split it.
+ if (!HTMLEditUtils::IsSplittableNode(*editableBlockElement) &&
+ separator != ParagraphSeparator::br) {
+ MOZ_ASSERT(separator == ParagraphSeparator::div ||
+ separator == ParagraphSeparator::p);
+ // FIXME: If there is no splittable block element, the other browsers wrap
+ // the right nodes into new paragraph, but keep the left node as-is.
+ // We should follow them to make here simpler and better compatibility.
+ Result<RefPtr<Element>, nsresult> suggestBlockElementToPutCaretOrError =
+ FormatBlockContainerWithTransaction(
+ selectionRanges,
+ MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator)),
+ // For keeping the traditional behavior at insertParagraph command,
+ // let's use the XUL paragraph state command targets even if we're
+ // handling HTML insertParagraph command.
+ FormatBlockMode::XULParagraphStateCommand, aEditingHost);
+ if (MOZ_UNLIKELY(suggestBlockElementToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::FormatBlockContainerWithTransaction() failed");
+ return suggestBlockElementToPutCaretOrError.propagateErr();
+ }
+ if (selectionRanges.HasSavedRanges()) {
+ selectionRanges.RestoreFromSavedRanges();
+ }
+ pointToInsert = selectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(pointToInsert.IsSetAndValid());
+ blockElementToPutCaret = suggestBlockElementToPutCaretOrError.unwrap();
+
+ editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement(
+ *pointToInsert.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrButtonElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (NS_WARN_IF(!editableBlockElement)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ if (NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(*editableBlockElement))) {
+ // Didn't create a new block for some reason, fall back to <br>
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ HandleInsertBRElement(pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertBRElement() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ EditorDOMPoint pointToPutCaret =
+ unwrappedInsertBRElementResult.UnwrapCaretPoint();
+ if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) {
+ NS_WARNING(
+ "HTMLEditor::HandleInsertBRElement() didn't suggest a point to put "
+ "caret");
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsresult rv =
+ CollapseSelection(pointToPutCaret, blockElementToPutCaret, {});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CollapseSelection() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+ // We want to collapse selection in the editable block element.
+ blockElementToPutCaret = editableBlockElement;
+ }
+
+ // If block is empty, populate with br. (For example, imagine a div that
+ // contains the word "text". The user selects "text" and types return.
+ // "Text" is deleted leaving an empty block. We want to put in one br to
+ // make block have a line. Then code further below will put in a second br.)
+ RefPtr<Element> insertedPaddingBRElement;
+ if (HTMLEditUtils::IsEmptyBlockElement(
+ *editableBlockElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes,
+ EditorDOMPoint::AtEndOf(*editableBlockElement));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ insertedPaddingBRElement = unwrappedInsertBRElementResult.UnwrapNewNode();
+
+ pointToInsert = selectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+
+ RefPtr<Element> maybeNonEditableListItem =
+ HTMLEditUtils::GetClosestAncestorListItemElement(*editableBlockElement,
+ &aEditingHost);
+ if (maybeNonEditableListItem &&
+ HTMLEditUtils::IsSplittableNode(*maybeNonEditableListItem)) {
+ Result<InsertParagraphResult, nsresult> insertParagraphInListItemResult =
+ HandleInsertParagraphInListItemElement(*maybeNonEditableListItem,
+ pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertParagraphInListItemResult.isErr())) {
+ if (NS_WARN_IF(insertParagraphInListItemResult.unwrapErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::HandleInsertParagraphInListItemElement() failed, but "
+ "ignored");
+ return EditActionResult::HandledResult();
+ }
+ InsertParagraphResult unwrappedInsertParagraphInListItemResult =
+ insertParagraphInListItemResult.unwrap();
+ MOZ_ASSERT(unwrappedInsertParagraphInListItemResult.Handled());
+ MOZ_ASSERT(unwrappedInsertParagraphInListItemResult.GetNewNode());
+ const RefPtr<Element> listItemOrParagraphElement =
+ unwrappedInsertParagraphInListItemResult.UnwrapNewNode();
+ const EditorDOMPoint pointToPutCaret =
+ unwrappedInsertParagraphInListItemResult.UnwrapCaretPoint();
+ nsresult rv = CollapseSelection(pointToPutCaret, listItemOrParagraphElement,
+ {SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CollapseSelection() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CollapseSelection() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+
+ if (HTMLEditUtils::IsHeader(*editableBlockElement)) {
+ Result<InsertParagraphResult, nsresult>
+ insertParagraphInHeadingElementResult =
+ HandleInsertParagraphInHeadingElement(*editableBlockElement,
+ pointToInsert);
+ if (MOZ_UNLIKELY(insertParagraphInHeadingElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::HandleInsertParagraphInHeadingElement() failed, but "
+ "ignored");
+ return EditActionResult::HandledResult();
+ }
+ InsertParagraphResult unwrappedInsertParagraphInHeadingElementResult =
+ insertParagraphInHeadingElementResult.unwrap();
+ if (unwrappedInsertParagraphInHeadingElementResult.Handled()) {
+ MOZ_ASSERT(unwrappedInsertParagraphInHeadingElementResult.GetNewNode());
+ blockElementToPutCaret =
+ unwrappedInsertParagraphInHeadingElementResult.UnwrapNewNode();
+ }
+ const EditorDOMPoint pointToPutCaret =
+ unwrappedInsertParagraphInHeadingElementResult.UnwrapCaretPoint();
+ nsresult rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CollapseSelection() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CollapseSelection() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+
+ // XXX Ideally, we should take same behavior with both <p> container and
+ // <div> container. However, we are still using <br> as default
+ // paragraph separator (non-standard) and we've split only <p> container
+ // long time. Therefore, some web apps may depend on this behavior like
+ // Gmail. So, let's use traditional odd behavior only when the default
+ // paragraph separator is <br>. Otherwise, take consistent behavior
+ // between <p> container and <div> container.
+ if ((separator == ParagraphSeparator::br &&
+ editableBlockElement->IsHTMLElement(nsGkAtoms::p)) ||
+ (separator != ParagraphSeparator::br &&
+ editableBlockElement->IsAnyOfHTMLElements(nsGkAtoms::p,
+ nsGkAtoms::div))) {
+ // Paragraphs: special rules to look for <br>s
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ HandleInsertParagraphInParagraph(
+ *editableBlockElement,
+ insertedPaddingBRElement ? EditorDOMPoint(insertedPaddingBRElement)
+ : pointToInsert,
+ aEditingHost);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertParagraphInParagraph() failed");
+ return splitNodeResult.propagateErr();
+ }
+ if (splitNodeResult.inspect().Handled()) {
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ const RefPtr<Element> rightParagraphElement =
+ unwrappedSplitNodeResult.DidSplit()
+ ? unwrappedSplitNodeResult.GetNextContentAs<Element>()
+ : blockElementToPutCaret.get();
+ const EditorDOMPoint pointToPutCaret =
+ unwrappedSplitNodeResult.UnwrapCaretPoint();
+ nsresult rv = CollapseSelection(pointToPutCaret, rightParagraphElement,
+ {SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CollapseSelection() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CollapseSelection() failed, but ignored");
+ return EditActionResult::HandledResult();
+ }
+ MOZ_ASSERT(!splitNodeResult.inspect().HasCaretPointSuggestion());
+
+ // Fall through, if HandleInsertParagraphInParagraph() didn't handle it.
+ MOZ_ASSERT(pointToInsert.IsSetAndValid(),
+ "HTMLEditor::HandleInsertParagraphInParagraph() shouldn't touch "
+ "the DOM tree if it returns not-handled state");
+ }
+
+ // If nobody handles this edit action, let's insert new <br> at the selection.
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ HandleInsertBRElement(pointToInsert, aEditingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleInsertBRElement() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ EditorDOMPoint pointToPutCaret =
+ unwrappedInsertBRElementResult.UnwrapCaretPoint();
+ rv = CollapseSelection(pointToPutCaret, blockElementToPutCaret, {});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+Result<CreateElementResult, nsresult> HTMLEditor::HandleInsertBRElement(
+ const EditorDOMPoint& aPointToBreak, const Element& aEditingHost) {
+ MOZ_ASSERT(aPointToBreak.IsSet());
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ const bool editingHostIsEmpty = HTMLEditUtils::IsEmptyNode(
+ aEditingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible});
+ WSRunScanner wsRunScanner(&aEditingHost, aPointToBreak,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ WSScanResult backwardScanResult =
+ wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(aPointToBreak);
+ if (MOZ_UNLIKELY(backwardScanResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary();
+ WSScanResult forwardScanResult =
+ wsRunScanner.ScanNextVisibleNodeOrBlockBoundaryFrom(aPointToBreak);
+ if (MOZ_UNLIKELY(forwardScanResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ const bool brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary();
+
+ // First, insert a <br> element.
+ RefPtr<Element> brElement;
+ if (IsPlaintextMailComposer()) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, aPointToBreak);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult;
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // We'll return with suggesting new caret position and nobody refers
+ // selection after here. So we don't need to update selection here.
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ brElement = unwrappedInsertBRElementResult.UnwrapNewNode();
+ } else {
+ EditorDOMPoint pointToBreak(aPointToBreak);
+ // If the container of the break is a link, we need to split it and
+ // insert new <br> between the split links.
+ RefPtr<Element> linkNode =
+ HTMLEditor::GetLinkElement(pointToBreak.GetContainer());
+ if (linkNode) {
+ Result<SplitNodeResult, nsresult> splitLinkNodeResult =
+ SplitNodeDeepWithTransaction(
+ *linkNode, pointToBreak,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitLinkNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) failed");
+ return splitLinkNodeResult.propagateErr();
+ }
+ // TODO: Some methods called by
+ // WhiteSpaceVisibilityKeeper::InsertBRElement() use
+ // ComputeEditingHost() which depends on selection. Therefore,
+ // we cannot skip updating selection here.
+ nsresult rv = splitLinkNodeResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ pointToBreak =
+ splitLinkNodeResult.inspect().AtSplitPoint<EditorDOMPoint>();
+ }
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ WhiteSpaceVisibilityKeeper::InsertBRElement(*this, pointToBreak,
+ aEditingHost);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed");
+ return insertBRElementResult;
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // We'll return with suggesting new caret position and nobody refers
+ // selection after here. So we don't need to update selection here.
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ brElement = unwrappedInsertBRElementResult.UnwrapNewNode();
+ MOZ_ASSERT(brElement);
+ }
+
+ if (MOZ_UNLIKELY(!brElement->GetParentNode())) {
+ NS_WARNING("Inserted <br> element was removed by the web app");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ auto afterBRElement = EditorDOMPoint::After(brElement);
+
+ auto InsertAdditionalInvisibleLineBreak =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<CreateElementResult, nsresult> {
+ // Empty last line is invisible if it's immediately before either parent or
+ // another block's boundary so that we need to put invisible <br> element
+ // here for making it visible.
+ Result<CreateElementResult, nsresult> invisibleAdditionalBRElementResult =
+ WhiteSpaceVisibilityKeeper::InsertBRElement(*this, afterBRElement,
+ aEditingHost);
+ if (MOZ_UNLIKELY(invisibleAdditionalBRElementResult.isErr())) {
+ NS_WARNING("WhiteSpaceVisibilityKeeper::InsertBRElement() failed");
+ return invisibleAdditionalBRElementResult;
+ }
+ // afterBRElement points after the first <br> with referring an old child.
+ // Therefore, we need to update it with new child which is the new invisible
+ // <br>.
+ afterBRElement.Set(
+ invisibleAdditionalBRElementResult.inspect().GetNewNode());
+ return invisibleAdditionalBRElementResult;
+ };
+
+ if (brElementIsAfterBlock && brElementIsBeforeBlock) {
+ // We just placed a <br> between block boundaries. This is the one case
+ // where we want the selection to be before the br we just placed, as the
+ // br will be on a new line, rather than at end of prior line.
+ // XXX brElementIsAfterBlock and brElementIsBeforeBlock were set before
+ // modifying the DOM tree. So, now, the <br> element may not be
+ // between blocks.
+ EditorDOMPoint pointToPutCaret;
+ if (editingHostIsEmpty) {
+ Result<CreateElementResult, nsresult> invisibleAdditionalBRElementResult =
+ InsertAdditionalInvisibleLineBreak();
+ if (invisibleAdditionalBRElementResult.isErr()) {
+ return invisibleAdditionalBRElementResult;
+ }
+ invisibleAdditionalBRElementResult.unwrap().IgnoreCaretPointSuggestion();
+ pointToPutCaret = std::move(afterBRElement);
+ } else {
+ pointToPutCaret =
+ EditorDOMPoint(brElement, InterlinePosition::StartOfNextLine);
+ }
+ return CreateElementResult(std::move(brElement),
+ std::move(pointToPutCaret));
+ }
+
+ WSScanResult forwardScanFromAfterBRElementResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost, afterBRElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (forwardScanFromAfterBRElementResult.ReachedBRElement()) {
+ // The next thing after the break we inserted is another break. Move the
+ // second break to be the first break's sibling. This will prevent them
+ // from being in different inline nodes, which would break
+ // SetInterlinePosition(). It will also assure that if the user clicks
+ // away and then clicks back on their new blank line, they will still get
+ // the style from the line above.
+ if (brElement->GetNextSibling() !=
+ forwardScanFromAfterBRElementResult.BRElementPtr()) {
+ MOZ_ASSERT(forwardScanFromAfterBRElementResult.BRElementPtr());
+ Result<MoveNodeResult, nsresult> moveBRElementResult =
+ MoveNodeWithTransaction(
+ MOZ_KnownLive(
+ *forwardScanFromAfterBRElementResult.BRElementPtr()),
+ afterBRElement);
+ if (MOZ_UNLIKELY(moveBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveBRElementResult.propagateErr();
+ }
+ nsresult rv = moveBRElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ }
+ } else if (forwardScanFromAfterBRElementResult.ReachedBlockBoundary() &&
+ !brElementIsAfterBlock) {
+ Result<CreateElementResult, nsresult> invisibleAdditionalBRElementResult =
+ InsertAdditionalInvisibleLineBreak();
+ if (invisibleAdditionalBRElementResult.isErr()) {
+ return invisibleAdditionalBRElementResult;
+ }
+ invisibleAdditionalBRElementResult.unwrap().IgnoreCaretPointSuggestion();
+ }
+
+ // We want the caret to stick to whatever is past the break. This is because
+ // the break is on the same line we were on, but the next content will be on
+ // the following line.
+
+ // An exception to this is if the break has a next sibling that is a block
+ // node. Then we stick to the left to avoid an uber caret.
+ nsIContent* nextSiblingOfBRElement = brElement->GetNextSibling();
+ afterBRElement.SetInterlinePosition(
+ nextSiblingOfBRElement && HTMLEditUtils::IsBlockElement(
+ *nextSiblingOfBRElement,
+ BlockInlineCheck::UseComputedDisplayStyle)
+ ? InterlinePosition::EndOfLine
+ : InterlinePosition::StartOfNextLine);
+ return CreateElementResult(std::move(brElement), afterBRElement);
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::HandleInsertLinefeed(
+ const EditorDOMPoint& aPointToBreak, const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aPointToBreak.IsSet())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ const RefPtr<Document> document = GetDocument();
+ MOZ_DIAGNOSTIC_ASSERT(document);
+ if (NS_WARN_IF(!document)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // TODO: The following code is duplicated from `HandleInsertText`. They
+ // should be merged when we fix bug 92921.
+
+ Result<EditorDOMPoint, nsresult> setStyleResult =
+ CreateStyleForInsertText(aPointToBreak, aEditingHost);
+ if (MOZ_UNLIKELY(setStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed");
+ return setStyleResult.propagateErr();
+ }
+
+ EditorDOMPoint pointToInsert = setStyleResult.inspect().IsSet()
+ ? setStyleResult.inspect()
+ : aPointToBreak;
+ if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) ||
+ NS_WARN_IF(!pointToInsert.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(pointToInsert.IsSetAndValid());
+
+ // The node may not be able to have a text node so that we need to check it
+ // here.
+ if (!pointToInsert.IsInTextNode() &&
+ !HTMLEditUtils::CanNodeContain(*pointToInsert.ContainerAs<nsIContent>(),
+ *nsGkAtoms::textTagName)) {
+ NS_WARNING(
+ "HTMLEditor::HandleInsertLinefeed() couldn't insert a linefeed because "
+ "the insertion position couldn't have text nodes");
+ return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
+ }
+
+ AutoRestore<bool> disableListener(
+ EditSubActionDataRef().mAdjustChangedRangeFromListener);
+ EditSubActionDataRef().mAdjustChangedRangeFromListener = false;
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ EditorDOMPoint pointToPutCaret;
+ {
+ AutoTrackDOMPoint trackingInsertingPosition(RangeUpdaterRef(),
+ &pointToInsert);
+ Result<InsertTextResult, nsresult> insertTextResult =
+ InsertTextWithTransaction(*document, u"\n"_ns, pointToInsert);
+ if (MOZ_UNLIKELY(insertTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertTextWithTransaction() failed");
+ return insertTextResult.propagateErr();
+ }
+ // Ignore the caret suggestion because of `dontChangeMySelection` above.
+ insertTextResult.inspect().IgnoreCaretPointSuggestion();
+ pointToPutCaret = insertTextResult.inspect().Handled()
+ ? insertTextResult.unwrap()
+ .EndOfInsertedTextRef()
+ .To<EditorDOMPoint>()
+ : pointToInsert;
+ }
+
+ // Insert a padding <br> element at the end of the block element if there is
+ // no content between the inserted linefeed and the following block boundary
+ // to make sure that the last line is visible.
+ // XXX Blink/WebKit inserts another linefeed character in this case. However,
+ // for doing it, we need more work, e.g., updating serializer, deleting
+ // unnecessary padding <br> element at modifying the last line.
+ if (pointToPutCaret.IsInContentNode() && pointToPutCaret.IsEndOfContainer()) {
+ WSRunScanner wsScannerAtCaret(&aEditingHost, pointToPutCaret,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (wsScannerAtCaret.StartsFromPreformattedLineBreak() &&
+ wsScannerAtCaret.EndsByBlockBoundary() &&
+ HTMLEditUtils::CanNodeContain(*wsScannerAtCaret.GetEndReasonContent(),
+ *nsGkAtoms::br)) {
+ AutoTrackDOMPoint trackingInsertedPosition(RangeUpdaterRef(),
+ &pointToInsert);
+ AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(),
+ &pointToPutCaret);
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToPutCaret);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We're tracking next caret position with newCaretPosition. Therefore,
+ // we don't need to update selection here.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ }
+ }
+
+ // manually update the doc changed range so that
+ // OnEndHandlingTopLevelEditSubActionInternal will clean up the correct
+ // portion of the document.
+ MOZ_ASSERT(pointToPutCaret.IsSet());
+ if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
+ // XXX Here is odd. We did mChangedRange->SetStartAndEnd(pointToInsert,
+ // pointToPutCaret), but it always fails because of the latter is unset.
+ // Therefore, always returning NS_ERROR_FAILURE from here is the
+ // traditional behavior...
+ // TODO: Stop updating the interline position of Selection with fixing here
+ // and returning expected point.
+ DebugOnly<nsresult> rvIgnored =
+ SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "EndOfLine) failed, but ignored");
+ if (NS_FAILED(TopLevelEditSubActionDataRef().mChangedRange->CollapseTo(
+ pointToInsert))) {
+ NS_WARNING("nsRange::CollapseTo() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ NS_WARNING(
+ "We always return NS_ERROR_FAILURE here because of a failure of "
+ "updating mChangedRange");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (NS_FAILED(TopLevelEditSubActionDataRef().mChangedRange->SetStartAndEnd(
+ pointToInsert.ToRawRangeBoundary(),
+ pointToPutCaret.ToRawRangeBoundary()))) {
+ NS_WARNING("nsRange::SetStartAndEnd() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ pointToPutCaret.SetInterlinePosition(InterlinePosition::EndOfLine);
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::HandleInsertParagraphInMailCiteElement(
+ Element& aMailCiteElement, const EditorDOMPoint& aPointToSplit,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToSplit.IsSet());
+ NS_ASSERTION(!HTMLEditUtils::IsEmptyNode(
+ aMailCiteElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible}),
+ "The mail-cite element will be deleted, does it expected result "
+ "for you?");
+
+ auto splitCiteElementResult =
+ [&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
+ EditorDOMPoint pointToSplit(aPointToSplit);
+
+ // If our selection is just before a break, nudge it to be just after
+ // it. This does two things for us. It saves us the trouble of having
+ // to add a break here ourselves to preserve the "blockness" of the
+ // inline span mailquote (in the inline case), and : it means the break
+ // won't end up making an empty line that happens to be inside a
+ // mailquote (in either inline or block case). The latter can confuse a
+ // user if they click there and start typing, because being in the
+ // mailquote may affect wrapping behavior, or font color, etc.
+ WSScanResult forwardScanFromPointToSplitResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost, pointToSplit, BlockInlineCheck::UseHTMLDefaultStyle);
+ if (forwardScanFromPointToSplitResult.Failed()) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // If selection start point is before a break and it's inside the
+ // mailquote, let's split it after the visible node.
+ if (forwardScanFromPointToSplitResult.ReachedBRElement() &&
+ forwardScanFromPointToSplitResult.BRElementPtr() != &aMailCiteElement &&
+ aMailCiteElement.Contains(
+ forwardScanFromPointToSplitResult.BRElementPtr())) {
+ pointToSplit =
+ forwardScanFromPointToSplitResult.PointAfterContent<EditorDOMPoint>();
+ }
+
+ if (NS_WARN_IF(!pointToSplit.IsInContentNode())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ Result<SplitNodeResult, nsresult> splitResult =
+ SplitNodeDeepWithTransaction(aMailCiteElement, pointToSplit,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(aMailCiteElement, "
+ "SplitAtEdges::eDoNotCreateEmptyContainer) failed");
+ return splitResult;
+ }
+ nsresult rv = splitResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ return splitResult;
+ }();
+ if (MOZ_UNLIKELY(splitCiteElementResult.isErr())) {
+ NS_WARNING("Failed to split a mail-cite element");
+ return splitCiteElementResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitCiteElementResult =
+ splitCiteElementResult.unwrap();
+ // When adding caret suggestion to SplitNodeResult, here didn't change
+ // selection so that just ignore it.
+ unwrappedSplitCiteElementResult.IgnoreCaretPointSuggestion();
+
+ // Add an invisible <br> to the end of left cite node if it was a <span> of
+ // style="display: block". This is important, since when serializing the cite
+ // to plain text, the span which caused the visual break is discarded. So the
+ // added <br> will guarantee that the serializer will insert a break where the
+ // user saw one.
+ // FYI: unwrappedSplitCiteElementResult grabs the previous node and the next
+ // node with nsCOMPtr or EditorDOMPoint. So, it's safe to access
+ // leftCiteElement and rightCiteElement even after changing the DOM tree
+ // and/or selection even though it's raw pointer.
+ auto* const leftCiteElement =
+ unwrappedSplitCiteElementResult.GetPreviousContentAs<Element>();
+ auto* const rightCiteElement =
+ unwrappedSplitCiteElementResult.GetNextContentAs<Element>();
+ if (leftCiteElement && leftCiteElement->IsHTMLElement(nsGkAtoms::span) &&
+ // XXX Oh, this depends on layout information of new element, and it's
+ // created by the hacky flush in DoSplitNode(). So we need to
+ // redesign around this for bug 1710784.
+ leftCiteElement->GetPrimaryFrame() &&
+ leftCiteElement->GetPrimaryFrame()->IsBlockFrameOrSubclass()) {
+ nsIContent* lastChild = leftCiteElement->GetLastChild();
+ if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) {
+ Result<CreateElementResult, nsresult> insertInvisibleBRElementResult =
+ InsertBRElement(WithTransaction::Yes,
+ EditorDOMPoint::AtEndOf(*leftCiteElement));
+ if (MOZ_UNLIKELY(insertInvisibleBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertInvisibleBRElementResult.propagateErr();
+ }
+ // We don't need to update selection here because we'll do another
+ // InsertBRElement call soon.
+ insertInvisibleBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(insertInvisibleBRElementResult.inspect().GetNewNode());
+ }
+ }
+
+ // In most cases, <br> should be inserted after current cite. However, if
+ // left cite hasn't been created because the split point was start of the
+ // cite node, <br> should be inserted before the current cite.
+ Result<CreateElementResult, nsresult> insertBRElementResult = InsertBRElement(
+ WithTransaction::Yes,
+ unwrappedSplitCiteElementResult.AtSplitPoint<EditorDOMPoint>());
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return Err(insertBRElementResult.unwrapErr());
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // We'll return with suggesting caret position. Therefore, we don't need
+ // to update selection here.
+ unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+
+ // if aMailCiteElement wasn't a block, we might also want another break before
+ // it. We need to examine the content both before the br we just added and
+ // also just after it. If we don't have another br or block boundary
+ // adjacent, then we will need a 2nd br added to achieve blank line that user
+ // expects.
+ if (HTMLEditUtils::IsInlineContent(
+ aMailCiteElement, BlockInlineCheck::UseComputedDisplayStyle)) {
+ nsresult rvOfInsertingBRElement = [&]() MOZ_CAN_RUN_SCRIPT {
+ EditorDOMPoint pointToCreateNewBRElement(
+ unwrappedInsertBRElementResult.GetNewNode());
+
+ // XXX Cannot we replace this complicated check with just a call of
+ // HTMLEditUtils::IsVisibleBRElement with
+ // resultOfInsertingBRElement.inspect()?
+ WSScanResult backwardScanFromPointToCreateNewBRElementResult =
+ WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
+ &aEditingHost, pointToCreateNewBRElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(
+ backwardScanFromPointToCreateNewBRElementResult.Failed())) {
+ NS_WARNING(
+ "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() "
+ "failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (!backwardScanFromPointToCreateNewBRElementResult
+ .InVisibleOrCollapsibleCharacters() &&
+ !backwardScanFromPointToCreateNewBRElementResult
+ .ReachedSpecialContent()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ WSScanResult forwardScanFromPointAfterNewBRElementResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost,
+ EditorRawDOMPoint::After(pointToCreateNewBRElement),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(forwardScanFromPointAfterNewBRElementResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
+ return NS_ERROR_FAILURE;
+ }
+ if (!forwardScanFromPointAfterNewBRElementResult
+ .InVisibleOrCollapsibleCharacters() &&
+ !forwardScanFromPointAfterNewBRElementResult
+ .ReachedSpecialContent() &&
+ // In case we're at the very end.
+ !forwardScanFromPointAfterNewBRElementResult
+ .ReachedCurrentBlockBoundary()) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToCreateNewBRElement);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ return NS_OK;
+ }();
+
+ if (NS_FAILED(rvOfInsertingBRElement)) {
+ NS_WARNING(
+ "Failed to insert additional <br> element before the inline right "
+ "mail-cite element");
+ return Err(rvOfInsertingBRElement);
+ }
+ }
+
+ if (leftCiteElement &&
+ HTMLEditUtils::IsEmptyNode(
+ *leftCiteElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ // MOZ_KnownLive(leftCiteElement) because it's grabbed by
+ // unwrappedSplitCiteElementResult.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*leftCiteElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ if (rightCiteElement &&
+ HTMLEditUtils::IsEmptyNode(
+ *rightCiteElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ // MOZ_KnownLive(rightCiteElement) because it's grabbed by
+ // unwrappedSplitCiteElementResult.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*rightCiteElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ if (MOZ_UNLIKELY(!unwrappedInsertBRElementResult.GetNewNode()->GetParent())) {
+ NS_WARNING("Inserted <br> shouldn't become an orphan node");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ return EditorDOMPoint(unwrappedInsertBRElementResult.GetNewNode());
+}
+
+HTMLEditor::CharPointData
+HTMLEditor::GetPreviousCharPointDataForNormalizingWhiteSpaces(
+ const EditorDOMPointInText& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (!aPoint.IsStartOfContainer()) {
+ return CharPointData::InSameTextNode(
+ HTMLEditor::GetPreviousCharPointType(aPoint));
+ }
+ const auto previousCharPoint =
+ WSRunScanner::GetPreviousEditableCharPoint<EditorRawDOMPointInText>(
+ ComputeEditingHost(), aPoint,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (!previousCharPoint.IsSet()) {
+ return CharPointData::InDifferentTextNode(CharPointType::TextEnd);
+ }
+ return CharPointData::InDifferentTextNode(
+ HTMLEditor::GetCharPointType(previousCharPoint));
+}
+
+HTMLEditor::CharPointData
+HTMLEditor::GetInclusiveNextCharPointDataForNormalizingWhiteSpaces(
+ const EditorDOMPointInText& aPoint) const {
+ MOZ_ASSERT(aPoint.IsSetAndValid());
+
+ if (!aPoint.IsEndOfContainer()) {
+ return CharPointData::InSameTextNode(HTMLEditor::GetCharPointType(aPoint));
+ }
+ const auto nextCharPoint =
+ WSRunScanner::GetInclusiveNextEditableCharPoint<EditorRawDOMPointInText>(
+ ComputeEditingHost(), aPoint,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (!nextCharPoint.IsSet()) {
+ return CharPointData::InDifferentTextNode(CharPointType::TextEnd);
+ }
+ return CharPointData::InDifferentTextNode(
+ HTMLEditor::GetCharPointType(nextCharPoint));
+}
+
+// static
+void HTMLEditor::GenerateWhiteSpaceSequence(
+ nsAString& aResult, uint32_t aLength,
+ const CharPointData& aPreviousCharPointData,
+ const CharPointData& aNextCharPointData) {
+ MOZ_ASSERT(aResult.IsEmpty());
+ MOZ_ASSERT(aLength);
+ // For now, this method does not assume that result will be append to
+ // white-space sequence in the text node.
+ MOZ_ASSERT(aPreviousCharPointData.AcrossTextNodeBoundary() ||
+ !aPreviousCharPointData.IsCollapsibleWhiteSpace());
+ // For now, this method does not assume that the result will be inserted
+ // into white-space sequence nor start of white-space sequence.
+ MOZ_ASSERT(aNextCharPointData.AcrossTextNodeBoundary() ||
+ !aNextCharPointData.IsCollapsibleWhiteSpace());
+
+ if (aLength == 1) {
+ // Even if previous/next char is in different text node, we should put
+ // an ASCII white-space between visible characters.
+ // XXX This means that this does not allow to put an NBSP in HTML editor
+ // without preformatted style. However, Chrome has same issue too.
+ if (aPreviousCharPointData.Type() == CharPointType::VisibleChar &&
+ aNextCharPointData.Type() == CharPointType::VisibleChar) {
+ aResult.Assign(HTMLEditUtils::kSpace);
+ return;
+ }
+ // If it's start or end of text, put an NBSP.
+ if (aPreviousCharPointData.Type() == CharPointType::TextEnd ||
+ aNextCharPointData.Type() == CharPointType::TextEnd) {
+ aResult.Assign(HTMLEditUtils::kNBSP);
+ return;
+ }
+ // If the character is next to a preformatted linefeed, we need to put
+ // an NBSP for avoiding collapsed into the linefeed.
+ if (aPreviousCharPointData.Type() == CharPointType::PreformattedLineBreak ||
+ aNextCharPointData.Type() == CharPointType::PreformattedLineBreak) {
+ aResult.Assign(HTMLEditUtils::kNBSP);
+ return;
+ }
+ // Now, the white-space will be inserted to a white-space sequence, but not
+ // end of text. We can put an ASCII white-space only when both sides are
+ // not ASCII white-spaces.
+ aResult.Assign(
+ aPreviousCharPointData.Type() == CharPointType::ASCIIWhiteSpace ||
+ aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace
+ ? HTMLEditUtils::kNBSP
+ : HTMLEditUtils::kSpace);
+ return;
+ }
+
+ // Generate pairs of NBSP and ASCII white-space.
+ aResult.SetLength(aLength);
+ bool appendNBSP = true; // Basically, starts with an NBSP.
+ char16_t* lastChar = aResult.EndWriting() - 1;
+ for (char16_t* iter = aResult.BeginWriting(); iter != lastChar; iter++) {
+ *iter = appendNBSP ? HTMLEditUtils::kNBSP : HTMLEditUtils::kSpace;
+ appendNBSP = !appendNBSP;
+ }
+
+ // If the final one is expected to an NBSP, we can put an NBSP simply.
+ if (appendNBSP) {
+ *lastChar = HTMLEditUtils::kNBSP;
+ return;
+ }
+
+ // If next char point is end of text node, an ASCII white-space or
+ // preformatted linefeed, we need to put an NBSP.
+ *lastChar =
+ aNextCharPointData.AcrossTextNodeBoundary() ||
+ aNextCharPointData.Type() == CharPointType::ASCIIWhiteSpace ||
+ aNextCharPointData.Type() == CharPointType::PreformattedLineBreak
+ ? HTMLEditUtils::kNBSP
+ : HTMLEditUtils::kSpace;
+}
+
+void HTMLEditor::ExtendRangeToDeleteWithNormalizingWhiteSpaces(
+ EditorDOMPointInText& aStartToDelete, EditorDOMPointInText& aEndToDelete,
+ nsAString& aNormalizedWhiteSpacesInStartNode,
+ nsAString& aNormalizedWhiteSpacesInEndNode) const {
+ MOZ_ASSERT(aStartToDelete.IsSetAndValid());
+ MOZ_ASSERT(aEndToDelete.IsSetAndValid());
+ MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete));
+ MOZ_ASSERT(aNormalizedWhiteSpacesInStartNode.IsEmpty());
+ MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.IsEmpty());
+
+ // First, check whether there is surrounding white-spaces or not, and if there
+ // are, check whether they are collapsible or not. Note that we shouldn't
+ // touch white-spaces in different text nodes for performance, but we need
+ // adjacent text node's first or last character information in some cases.
+ Element* editingHost = ComputeEditingHost();
+ const EditorDOMPointInText precedingCharPoint =
+ WSRunScanner::GetPreviousEditableCharPoint(
+ editingHost, aStartToDelete,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ const EditorDOMPointInText followingCharPoint =
+ WSRunScanner::GetInclusiveNextEditableCharPoint(
+ editingHost, aEndToDelete, BlockInlineCheck::UseComputedDisplayStyle);
+ // Blink-compat: Normalize white-spaces in first node only when not removing
+ // its last character or no text nodes follow the first node.
+ // If removing last character of first node and there are
+ // following text nodes, white-spaces in following text node are
+ // normalized instead.
+ const bool removingLastCharOfStartNode =
+ aStartToDelete.ContainerAs<Text>() != aEndToDelete.ContainerAs<Text>() ||
+ (aEndToDelete.IsEndOfContainer() && followingCharPoint.IsSet());
+ const bool maybeNormalizePrecedingWhiteSpaces =
+ !removingLastCharOfStartNode && precedingCharPoint.IsSet() &&
+ !precedingCharPoint.IsEndOfContainer() &&
+ precedingCharPoint.ContainerAs<Text>() ==
+ aStartToDelete.ContainerAs<Text>() &&
+ precedingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP();
+ const bool maybeNormalizeFollowingWhiteSpaces =
+ followingCharPoint.IsSet() && !followingCharPoint.IsEndOfContainer() &&
+ (followingCharPoint.ContainerAs<Text>() ==
+ aEndToDelete.ContainerAs<Text>() ||
+ removingLastCharOfStartNode) &&
+ followingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP();
+
+ if (!maybeNormalizePrecedingWhiteSpaces &&
+ !maybeNormalizeFollowingWhiteSpaces) {
+ return; // There are no white-spaces.
+ }
+
+ // Next, consider the range to normalize.
+ EditorDOMPointInText startToNormalize, endToNormalize;
+ if (maybeNormalizePrecedingWhiteSpaces) {
+ Maybe<uint32_t> previousCharOffsetOfWhiteSpaces =
+ HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
+ precedingCharPoint, {WalkTextOption::TreatNBSPsCollapsible});
+ startToNormalize.Set(precedingCharPoint.ContainerAs<Text>(),
+ previousCharOffsetOfWhiteSpaces.isSome()
+ ? previousCharOffsetOfWhiteSpaces.value() + 1
+ : 0);
+ MOZ_ASSERT(!startToNormalize.IsEndOfContainer());
+ }
+ if (maybeNormalizeFollowingWhiteSpaces) {
+ Maybe<uint32_t> nextCharOffsetOfWhiteSpaces =
+ HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
+ followingCharPoint, {WalkTextOption::TreatNBSPsCollapsible});
+ if (nextCharOffsetOfWhiteSpaces.isSome()) {
+ endToNormalize.Set(followingCharPoint.ContainerAs<Text>(),
+ nextCharOffsetOfWhiteSpaces.value());
+ } else {
+ endToNormalize.SetToEndOf(followingCharPoint.ContainerAs<Text>());
+ }
+ MOZ_ASSERT(!endToNormalize.IsStartOfContainer());
+ }
+
+ // Next, retrieve surrounding information of white-space sequence.
+ // If we're removing first text node's last character, we need to
+ // normalize white-spaces starts from another text node. In this case,
+ // we need to lie for avoiding assertion in GenerateWhiteSpaceSequence().
+ CharPointData previousCharPointData =
+ removingLastCharOfStartNode
+ ? CharPointData::InDifferentTextNode(CharPointType::TextEnd)
+ : GetPreviousCharPointDataForNormalizingWhiteSpaces(
+ startToNormalize.IsSet() ? startToNormalize : aStartToDelete);
+ CharPointData nextCharPointData =
+ GetInclusiveNextCharPointDataForNormalizingWhiteSpaces(
+ endToNormalize.IsSet() ? endToNormalize : aEndToDelete);
+
+ // Next, compute number of white-spaces in start/end node.
+ uint32_t lengthInStartNode = 0, lengthInEndNode = 0;
+ if (startToNormalize.IsSet()) {
+ MOZ_ASSERT(startToNormalize.ContainerAs<Text>() ==
+ aStartToDelete.ContainerAs<Text>());
+ lengthInStartNode = aStartToDelete.Offset() - startToNormalize.Offset();
+ MOZ_ASSERT(lengthInStartNode);
+ }
+ if (endToNormalize.IsSet()) {
+ lengthInEndNode =
+ endToNormalize.ContainerAs<Text>() == aEndToDelete.ContainerAs<Text>()
+ ? endToNormalize.Offset() - aEndToDelete.Offset()
+ : endToNormalize.Offset();
+ MOZ_ASSERT(lengthInEndNode);
+ // If we normalize white-spaces in a text node, we can replace all of them
+ // with one ReplaceTextTransaction.
+ if (endToNormalize.ContainerAs<Text>() ==
+ aStartToDelete.ContainerAs<Text>()) {
+ lengthInStartNode += lengthInEndNode;
+ lengthInEndNode = 0;
+ }
+ }
+
+ MOZ_ASSERT(lengthInStartNode + lengthInEndNode);
+
+ // Next, generate normalized white-spaces.
+ if (!lengthInEndNode) {
+ HTMLEditor::GenerateWhiteSpaceSequence(
+ aNormalizedWhiteSpacesInStartNode, lengthInStartNode,
+ previousCharPointData, nextCharPointData);
+ } else if (!lengthInStartNode) {
+ HTMLEditor::GenerateWhiteSpaceSequence(
+ aNormalizedWhiteSpacesInEndNode, lengthInEndNode, previousCharPointData,
+ nextCharPointData);
+ } else {
+ // For making `GenerateWhiteSpaceSequence()` simpler, we should create
+ // whole white-space sequence first, then, copy to the out params.
+ nsAutoString whiteSpaces;
+ HTMLEditor::GenerateWhiteSpaceSequence(
+ whiteSpaces, lengthInStartNode + lengthInEndNode, previousCharPointData,
+ nextCharPointData);
+ aNormalizedWhiteSpacesInStartNode =
+ Substring(whiteSpaces, 0, lengthInStartNode);
+ aNormalizedWhiteSpacesInEndNode = Substring(whiteSpaces, lengthInStartNode);
+ MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.Length() == lengthInEndNode);
+ }
+
+ // TODO: Shrink the replacing range and string as far as possible because
+ // this may run a lot, i.e., HTMLEditor creates ReplaceTextTransaction
+ // a lot for normalizing white-spaces. Then, each transaction shouldn't
+ // have all white-spaces every time because once it's normalized, we
+ // don't need to normalize all of the sequence again, but currently
+ // we do.
+
+ // Finally, extend the range.
+ if (startToNormalize.IsSet()) {
+ aStartToDelete = startToNormalize;
+ }
+ if (endToNormalize.IsSet()) {
+ aEndToDelete = endToNormalize;
+ }
+}
+
+Result<CaretPoint, nsresult>
+HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces(
+ const EditorDOMPointInText& aStartToDelete,
+ const EditorDOMPointInText& aEndToDelete,
+ TreatEmptyTextNodes aTreatEmptyTextNodes,
+ DeleteDirection aDeleteDirection) {
+ MOZ_ASSERT(aStartToDelete.IsSetAndValid());
+ MOZ_ASSERT(aEndToDelete.IsSetAndValid());
+ MOZ_ASSERT(aStartToDelete.EqualsOrIsBefore(aEndToDelete));
+
+ // Use nsString for these replacing string because we should avoid to copy
+ // the buffer from auto storange to ReplaceTextTransaction.
+ nsString normalizedWhiteSpacesInFirstNode, normalizedWhiteSpacesInLastNode;
+
+ // First, check whether we need to normalize white-spaces after deleting
+ // the given range.
+ EditorDOMPointInText startToDelete(aStartToDelete);
+ EditorDOMPointInText endToDelete(aEndToDelete);
+ ExtendRangeToDeleteWithNormalizingWhiteSpaces(
+ startToDelete, endToDelete, normalizedWhiteSpacesInFirstNode,
+ normalizedWhiteSpacesInLastNode);
+
+ // If extended range is still collapsed, i.e., the caller just wants to
+ // normalize white-space sequence, but there is no white-spaces which need to
+ // be replaced, we need to do nothing here.
+ if (startToDelete == endToDelete) {
+ return CaretPoint(aStartToDelete.To<EditorDOMPoint>());
+ }
+
+ // Note that the container text node of startToDelete may be removed from
+ // the tree if it becomes empty. Therefore, we need to track the point.
+ EditorDOMPoint newCaretPosition;
+ if (aStartToDelete.ContainerAs<Text>() == aEndToDelete.ContainerAs<Text>()) {
+ newCaretPosition = aEndToDelete.To<EditorDOMPoint>();
+ } else if (aDeleteDirection == DeleteDirection::Forward) {
+ newCaretPosition.SetToEndOf(aStartToDelete.ContainerAs<Text>());
+ } else {
+ newCaretPosition.Set(aEndToDelete.ContainerAs<Text>(), 0u);
+ }
+
+ // Then, modify the text nodes in the range.
+ while (true) {
+ AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(),
+ &newCaretPosition);
+ // Use ReplaceTextTransaction if we need to normalize white-spaces in
+ // the first text node.
+ if (!normalizedWhiteSpacesInFirstNode.IsEmpty()) {
+ EditorDOMPoint trackingEndToDelete(endToDelete.ContainerAs<Text>(),
+ endToDelete.Offset());
+ {
+ AutoTrackDOMPoint trackEndToDelete(RangeUpdaterRef(),
+ &trackingEndToDelete);
+ uint32_t lengthToReplaceInFirstTextNode =
+ startToDelete.ContainerAs<Text>() ==
+ trackingEndToDelete.ContainerAs<Text>()
+ ? trackingEndToDelete.Offset() - startToDelete.Offset()
+ : startToDelete.ContainerAs<Text>()->TextLength() -
+ startToDelete.Offset();
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ ReplaceTextWithTransaction(
+ MOZ_KnownLive(*startToDelete.ContainerAs<Text>()),
+ startToDelete.Offset(), lengthToReplaceInFirstTextNode,
+ normalizedWhiteSpacesInFirstNode);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // We'll return computed caret point, newCaretPosition, below.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ if (startToDelete.ContainerAs<Text>() ==
+ trackingEndToDelete.ContainerAs<Text>()) {
+ MOZ_ASSERT(normalizedWhiteSpacesInLastNode.IsEmpty());
+ break; // There is no more text which we need to delete.
+ }
+ }
+ if (MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) &&
+ (NS_WARN_IF(!trackingEndToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(!trackingEndToDelete.IsInTextNode()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ MOZ_ASSERT(trackingEndToDelete.IsInTextNode());
+ endToDelete.Set(trackingEndToDelete.ContainerAs<Text>(),
+ trackingEndToDelete.Offset());
+ // If the remaining range was modified by mutation event listener,
+ // we should stop handling the deletion.
+ startToDelete =
+ EditorDOMPointInText::AtEndOf(*startToDelete.ContainerAs<Text>());
+ if (MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) &&
+ NS_WARN_IF(!startToDelete.IsBefore(endToDelete))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+ // Delete ASCII whiteSpaces in the range simpley if there are some text
+ // nodes which we don't need to replace their text.
+ if (normalizedWhiteSpacesInLastNode.IsEmpty() ||
+ startToDelete.ContainerAs<Text>() != endToDelete.ContainerAs<Text>()) {
+ // If we need to replace text in the last text node, we should
+ // delete text before its previous text node.
+ EditorDOMPointInText endToDeleteExceptReplaceRange =
+ normalizedWhiteSpacesInLastNode.IsEmpty()
+ ? endToDelete
+ : EditorDOMPointInText(endToDelete.ContainerAs<Text>(), 0);
+ if (startToDelete != endToDeleteExceptReplaceRange) {
+ Result<CaretPoint, nsresult> caretPointOrError =
+ DeleteTextAndTextNodesWithTransaction(startToDelete,
+ endToDeleteExceptReplaceRange,
+ aTreatEmptyTextNodes);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::DeleteTextAndTextNodesWithTransaction() failed");
+ return caretPointOrError.propagateErr();
+ }
+ nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CaretPoint::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CaretPoint::SuggestCaretPointTo() failed, but ignored");
+ if (normalizedWhiteSpacesInLastNode.IsEmpty()) {
+ break; // There is no more text which we need to delete.
+ }
+ if (MayHaveMutationEventListeners(
+ NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVED |
+ NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT |
+ NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED) &&
+ (NS_WARN_IF(!endToDeleteExceptReplaceRange.IsSetAndValid()) ||
+ NS_WARN_IF(!endToDelete.IsSetAndValid()) ||
+ NS_WARN_IF(endToDelete.IsStartOfContainer()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // Then, replace the text in the last text node.
+ startToDelete = endToDeleteExceptReplaceRange;
+ }
+ }
+
+ // Replace ASCII whiteSpaces in the range and following character in the
+ // last text node.
+ MOZ_ASSERT(!normalizedWhiteSpacesInLastNode.IsEmpty());
+ MOZ_ASSERT(startToDelete.ContainerAs<Text>() ==
+ endToDelete.ContainerAs<Text>());
+ Result<InsertTextResult, nsresult> replaceTextResult =
+ ReplaceTextWithTransaction(
+ MOZ_KnownLive(*startToDelete.ContainerAs<Text>()),
+ startToDelete.Offset(),
+ endToDelete.Offset() - startToDelete.Offset(),
+ normalizedWhiteSpacesInLastNode);
+ if (MOZ_UNLIKELY(replaceTextResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceTextWithTransaction() failed");
+ return replaceTextResult.propagateErr();
+ }
+ // We'll return computed caret point, newCaretPosition, below.
+ replaceTextResult.unwrap().IgnoreCaretPointSuggestion();
+ break;
+ }
+
+ if (NS_WARN_IF(!newCaretPosition.IsSetAndValid()) ||
+ NS_WARN_IF(!newCaretPosition.GetContainer()->IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // Look for leaf node to put caret if we remove some empty inline ancestors
+ // at new caret position.
+ if (!newCaretPosition.IsInTextNode()) {
+ if (const Element* editableBlockElementOrInlineEditingHost =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *newCaretPosition.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElementOrInlineEditingHost,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ Element* editingHost = ComputeEditingHost();
+ // Try to put caret next to immediately after previous editable leaf.
+ nsIContent* previousContent =
+ HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
+ newCaretPosition, *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost);
+ if (previousContent &&
+ !HTMLEditUtils::IsBlockElement(
+ *previousContent, BlockInlineCheck::UseComputedDisplayStyle)) {
+ newCaretPosition =
+ previousContent->IsText() ||
+ HTMLEditUtils::IsContainerNode(*previousContent)
+ ? EditorDOMPoint::AtEndOf(*previousContent)
+ : EditorDOMPoint::After(*previousContent);
+ }
+ // But if the point is very first of a block element or immediately after
+ // a child block, look for next editable leaf instead.
+ else if (nsIContent* nextContent =
+ HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
+ newCaretPosition,
+ *editableBlockElementOrInlineEditingHost,
+ {LeafNodeType::LeafNodeOrNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle,
+ editingHost)) {
+ newCaretPosition = nextContent->IsText() ||
+ HTMLEditUtils::IsContainerNode(*nextContent)
+ ? EditorDOMPoint(nextContent, 0)
+ : EditorDOMPoint(nextContent);
+ }
+ }
+ }
+
+ // For compatibility with Blink, we should move caret to end of previous
+ // text node if it's direct previous sibling of the first text node in the
+ // range.
+ if (newCaretPosition.IsStartOfContainer() &&
+ newCaretPosition.IsInTextNode() &&
+ newCaretPosition.GetContainer()->GetPreviousSibling() &&
+ newCaretPosition.GetContainer()->GetPreviousSibling()->IsText()) {
+ newCaretPosition.SetToEndOf(
+ newCaretPosition.GetContainer()->GetPreviousSibling()->AsText());
+ }
+
+ {
+ AutoTrackDOMPoint trackingNewCaretPosition(RangeUpdaterRef(),
+ &newCaretPosition);
+ Result<CaretPoint, nsresult> caretPointOrError =
+ InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ newCaretPosition);
+ if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary() failed");
+ return caretPointOrError;
+ }
+ }
+ if (!newCaretPosition.IsSetAndValid()) {
+ NS_WARNING("Inserting <br> element caused unexpected DOM tree");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ return CaretPoint(std::move(newCaretPosition));
+}
+
+Result<CaretPoint, nsresult>
+HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary(
+ const EditorDOMPoint& aPointToInsert) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsert.IsSet());
+
+ if (!aPointToInsert.IsInContentNode()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ // If container of the point is not in a block, we don't need to put a
+ // `<br>` element here.
+ if (!HTMLEditUtils::IsBlockElement(
+ *aPointToInsert.ContainerAs<nsIContent>(),
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ if (NS_WARN_IF(!HTMLEditUtils::IsSimplyEditableNode(
+ *aPointToInsert.ContainerAs<nsIContent>()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ WSRunScanner wsRunScanner(ComputeEditingHost(), aPointToInsert,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ // If the point is not start of a hard line, we don't need to put a `<br>`
+ // element here.
+ if (!wsRunScanner.StartsFromHardLineBreak()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+ // If the point is not end of a hard line or the hard line does not end with
+ // block boundary, we don't need to put a `<br>` element here.
+ if (!wsRunScanner.EndsByBlockBoundary()) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ // If we cannot insert a `<br>` element here, do nothing.
+ if (!HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(),
+ *nsGkAtoms::br)) {
+ return CaretPoint(EditorDOMPoint());
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult = InsertBRElement(
+ WithTransaction::Yes, aPointToInsert, nsIEditor::ePrevious);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes, ePrevious) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ return CaretPoint(insertBRElementResult.unwrap().UnwrapCaretPoint());
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::MakeOrChangeListAndListItemAsSubAction(
+ const nsStaticAtom& aListElementOrListItemElementTagName,
+ const nsAString& aBulletType,
+ SelectAllOfCurrentList aSelectAllOfCurrentList) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(&aListElementOrListItemElementTagName == nsGkAtoms::ul ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::ol ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dl ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dd ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dt);
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return Err(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return EditActionResult::IgnoredResult();
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+
+ // XXX EditSubAction::eCreateOrChangeDefinitionListItem and
+ // EditSubAction::eCreateOrChangeList are treated differently in
+ // HTMLEditor::MaybeSplitElementsAtEveryBRElement(). Only when
+ // EditSubAction::eCreateOrChangeList, it splits inline nodes.
+ // Currently, it shouldn't be done when we called for formatting
+ // `<dd>` or `<dt>` by
+ // HTMLEditor::MakeDefinitionListItemWithTransaction(). But this
+ // difference may be a bug. We should investigate this later.
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this,
+ &aListElementOrListItemElementTagName == nsGkAtoms::dd ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dt
+ ? EditSubAction::eCreateOrChangeDefinitionListItem
+ : EditSubAction::eCreateOrChangeList,
+ nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(error.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ const nsStaticAtom* listTagName = nullptr;
+ const nsStaticAtom* listItemTagName = nullptr;
+ if (&aListElementOrListItemElementTagName == nsGkAtoms::ul ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::ol) {
+ listTagName = &aListElementOrListItemElementTagName;
+ listItemTagName = nsGkAtoms::li;
+ } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dl) {
+ listTagName = &aListElementOrListItemElementTagName;
+ listItemTagName = nsGkAtoms::dd;
+ } else if (&aListElementOrListItemElementTagName == nsGkAtoms::dd ||
+ &aListElementOrListItemElementTagName == nsGkAtoms::dt) {
+ listTagName = nsGkAtoms::dl;
+ listItemTagName = &aListElementOrListItemElementTagName;
+ } else {
+ NS_WARNING(
+ "aListElementOrListItemElementTagName was neither list element name "
+ "nor "
+ "definition listitem element name");
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ const RefPtr<Element> editingHost = ComputeEditingHost();
+ if (MOZ_UNLIKELY(!editingHost)) {
+ return EditActionResult::CanceledResult();
+ }
+
+ // Expands selection range to include the immediate block parent, and then
+ // further expands to include any ancestors whose children are all in the
+ // range.
+ // XXX Why do we do this only when there is only one selection range?
+ if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ SelectionRef().GetRangeAt(0u), *editingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use Selection::SetStartAndEndInLimit() here.
+ error.SuppressException();
+ SelectionRef().SetBaseAndExtentInLimiter(
+ extendedRange.inspect().StartRef().ToRawRangeBoundary(),
+ extendedRange.inspect().EndRef().ToRawRangeBoundary(), error);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return Err(error.StealNSResult());
+ }
+ }
+
+ AutoListElementCreator listCreator(*listTagName, *listItemTagName,
+ aBulletType);
+ AutoRangeArray selectionRanges(SelectionRef());
+ Result<EditActionResult, nsresult> result = listCreator.Run(
+ *this, selectionRanges, aSelectAllOfCurrentList, *editingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::ConvertContentAroundRangesToList() failed");
+ // XXX Should we try to restore selection ranges in this case?
+ return result;
+ }
+
+ rv = selectionRanges.ApplyTo(SelectionRef());
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::ApplyTo() failed");
+ return Err(rv);
+ }
+ return result.inspect().Ignored() ? EditActionResult::CanceledResult()
+ : EditActionResult::HandledResult();
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AutoListElementCreator::Run(
+ HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges,
+ SelectAllOfCurrentList aSelectAllOfCurrentList,
+ const Element& aEditingHost) const {
+ MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!aHTMLEditor.IsSelectionRangeContainerNotContent());
+
+ if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(aHTMLEditor))) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ AutoContentNodeArray arrayOfContents;
+ nsresult rv = SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList(
+ aHTMLEditor, aRanges, aSelectAllOfCurrentList, aEditingHost,
+ arrayOfContents);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoListElementCreator::"
+ "SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList() failed");
+ return Err(rv);
+ }
+
+ // check if all our nodes are <br>s, or empty inlines
+ // if no nodes, we make empty list. Ditto if the user tried to make a list
+ // of some # of breaks.
+ if (AutoListElementCreator::
+ IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements(
+ arrayOfContents)) {
+ Result<RefPtr<Element>, nsresult> newListItemElementOrError =
+ ReplaceContentNodesWithEmptyNewList(aHTMLEditor, aRanges,
+ arrayOfContents, aEditingHost);
+ if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) {
+ NS_WARNING(
+ "AutoListElementCreator::ReplaceContentNodesWithEmptyNewList() "
+ "failed");
+ return newListItemElementOrError.propagateErr();
+ }
+ if (MOZ_UNLIKELY(!newListItemElementOrError.inspect())) {
+ aRanges.RestoreFromSavedRanges();
+ return EditActionResult::CanceledResult();
+ }
+ aRanges.ClearSavedRanges();
+ nsresult rv = aRanges.Collapse(
+ EditorRawDOMPoint(newListItemElementOrError.inspect(), 0u));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return Err(rv);
+ }
+ return EditActionResult::IgnoredResult();
+ }
+
+ Result<RefPtr<Element>, nsresult> listItemOrListToPutCaretOrError =
+ WrapContentNodesIntoNewListElements(aHTMLEditor, aRanges, arrayOfContents,
+ aEditingHost);
+ if (MOZ_UNLIKELY(listItemOrListToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "AutoListElementCreator::WrapContentNodesIntoNewListElements() failed");
+ return listItemOrListToPutCaretOrError.propagateErr();
+ }
+
+ MOZ_ASSERT(aRanges.HasSavedRanges());
+ aRanges.RestoreFromSavedRanges();
+
+ // If selection will be collapsed but not in listItemOrListToPutCaret, we need
+ // to adjust the caret position into it.
+ if (listItemOrListToPutCaretOrError.inspect()) {
+ DebugOnly<nsresult> rvIgnored =
+ EnsureCollapsedRangeIsInListItemOrListElement(
+ *listItemOrListToPutCaretOrError.inspect(), aRanges);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "AutoListElementCreator::"
+ "EnsureCollapsedRangeIsInListItemOrListElement() failed, but ignored");
+ }
+
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::AutoListElementCreator::
+ SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList(
+ HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges,
+ SelectAllOfCurrentList aSelectAllOfCurrentList,
+ const Element& aEditingHost,
+ ContentNodeArray& aOutArrayOfContents) const {
+ MOZ_ASSERT(aOutArrayOfContents.IsEmpty());
+
+ if (aSelectAllOfCurrentList == SelectAllOfCurrentList::Yes) {
+ if (Element* parentListElementOfRanges =
+ aRanges.GetClosestAncestorAnyListElementOfRange()) {
+ aOutArrayOfContents.AppendElement(
+ OwningNonNull<nsIContent>(*parentListElementOfRanges));
+ return NS_OK;
+ }
+ }
+
+ AutoRangeArray extendedRanges(aRanges);
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the
+ // normal cases, but removing this may cause the behavior with the
+ // legacy mutation event listeners. We should try to delete this in
+ // a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
+
+ extendedRanges.ExtendRangesToWrapLines(EditSubAction::eCreateOrChangeList,
+ BlockInlineCheck::UseHTMLDefaultStyle,
+ aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges.SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ aHTMLEditor, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed");
+ return splitResult.unwrapErr();
+ }
+ nsresult rv = extendedRanges.CollectEditTargetNodes(
+ aHTMLEditor, aOutArrayOfContents, EditSubAction::eCreateOrChangeList,
+ AutoRangeArray::CollectNonEditableNodes::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
+ return rv;
+ }
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ aHTMLEditor.MaybeSplitElementsAtEveryBRElement(
+ aOutArrayOfContents, EditSubAction::eCreateOrChangeList);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eCreateOrChangeList) failed");
+ return splitAtBRElementsResult.unwrapErr();
+ }
+ return NS_OK;
+}
+
+// static
+bool HTMLEditor::AutoListElementCreator::
+ IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements(
+ const ContentNodeArray& aArrayOfContents) {
+ for (const OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ // if content is not a <br> or empty inline, we're done
+ // XXX Should we handle line breaks in preformatted text node?
+ if (!content->IsHTMLElement(nsGkAtoms::br) &&
+ !HTMLEditUtils::IsEmptyInlineContainer(
+ content,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+Result<RefPtr<Element>, nsresult>
+HTMLEditor::AutoListElementCreator::ReplaceContentNodesWithEmptyNewList(
+ HTMLEditor& aHTMLEditor, const AutoRangeArray& aRanges,
+ const AutoContentNodeArray& aArrayOfContents,
+ const Element& aEditingHost) const {
+ // if only breaks, delete them
+ for (const OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ // MOZ_KnownLive because of bug 1620312
+ nsresult rv =
+ aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(*content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ const auto firstRangeStartPoint =
+ aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!firstRangeStartPoint.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Make sure we can put a list here.
+ if (!HTMLEditUtils::CanNodeContain(*firstRangeStartPoint.GetContainer(),
+ mListTagName)) {
+ return RefPtr<Element>();
+ }
+
+ RefPtr<Element> newListItemElement;
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ aHTMLEditor.InsertElementWithSplittingAncestorsWithTransaction(
+ mListTagName, firstRangeStartPoint, BRElementNextToSplitPoint::Keep,
+ aEditingHost,
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [&](HTMLEditor& aHTMLEditor, Element& aListElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ AutoHandlingState dummyState;
+ Result<CreateElementResult, nsresult> createListItemElementResult =
+ AppendListItemElement(aHTMLEditor, aListElement, dummyState);
+ if (MOZ_UNLIKELY(createListItemElementResult.isErr())) {
+ NS_WARNING(
+ "AutoListElementCreator::AppendListItemElement() failed");
+ return createListItemElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedResult =
+ createListItemElementResult.unwrap();
+ // There is AutoSelectionRestorer in this method so that it'll
+ // be restored or updated with making it abort. Therefore,
+ // we don't need to update selection here.
+ // XXX I'd like to check aRanges.HasSavedRanges() here, but it
+ // requires ifdefs to avoid bustage of opt builds caused
+ // by unused warning...
+ unwrappedResult.IgnoreCaretPointSuggestion();
+ newListItemElement = unwrappedResult.UnwrapNewNode();
+ MOZ_ASSERT(newListItemElement);
+ return NS_OK;
+ });
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "%s) failed",
+ nsAtomCString(&mListTagName).get())
+ .get());
+ return createNewListElementResult.propagateErr();
+ }
+ MOZ_ASSERT(createNewListElementResult.inspect().GetNewNode());
+
+ // Put selection in new list item and don't restore the Selection.
+ createNewListElementResult.inspect().IgnoreCaretPointSuggestion();
+ return newListItemElement;
+}
+
+Result<RefPtr<Element>, nsresult>
+HTMLEditor::AutoListElementCreator::WrapContentNodesIntoNewListElements(
+ HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges,
+ AutoContentNodeArray& aArrayOfContents, const Element& aEditingHost) const {
+ // if there is only one node in the array, and it is a list, div, or
+ // blockquote, then look inside of it until we find inner list or content.
+ if (aArrayOfContents.Length() == 1) {
+ if (Element* deepestDivBlockquoteOrListElement =
+ HTMLEditUtils::GetInclusiveDeepestFirstChildWhichHasOneChild(
+ aArrayOfContents[0], {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseHTMLDefaultStyle, nsGkAtoms::div,
+ nsGkAtoms::blockquote, nsGkAtoms::ul, nsGkAtoms::ol,
+ nsGkAtoms::dl)) {
+ if (deepestDivBlockquoteOrListElement->IsAnyOfHTMLElements(
+ nsGkAtoms::div, nsGkAtoms::blockquote)) {
+ aArrayOfContents.Clear();
+ HTMLEditUtils::CollectChildren(*deepestDivBlockquoteOrListElement,
+ aArrayOfContents, 0, {});
+ } else {
+ aArrayOfContents.ReplaceElementAt(
+ 0, OwningNonNull<nsIContent>(*deepestDivBlockquoteOrListElement));
+ }
+ }
+ }
+
+ // Ok, now go through all the nodes and put then in the list,
+ // or whatever is appropriate. Wohoo!
+ AutoHandlingState handlingState;
+ for (const OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ // MOZ_KnownLive because of bug 1620312
+ nsresult rv = HandleChildContent(aHTMLEditor, MOZ_KnownLive(content),
+ handlingState, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoListElementCreator::HandleChildContent() failed");
+ return Err(rv);
+ }
+ }
+
+ return std::move(handlingState.mListOrListItemElementToPutCaret);
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildContent(
+ HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
+ AutoHandlingState& aState, const Element& aEditingHost) const {
+ // make sure we don't assemble content that is in different table cells
+ // into the same list. respect table cell boundaries when listifying.
+ if (aState.mCurrentListElement &&
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ *aState.mCurrentListElement) !=
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ aHandlingContent)) {
+ aState.mCurrentListElement = nullptr;
+ }
+
+ // If current node is a `<br>` element, delete it and forget previous
+ // list item element.
+ // If current node is an empty inline node, just delete it.
+ if (EditorUtils::IsEditableContent(aHandlingContent, EditorType::HTML) &&
+ (aHandlingContent.IsHTMLElement(nsGkAtoms::br) ||
+ HTMLEditUtils::IsEmptyInlineContainer(
+ aHandlingContent,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ BlockInlineCheck::UseHTMLDefaultStyle))) {
+ nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aHandlingContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ if (aHandlingContent.IsHTMLElement(nsGkAtoms::br)) {
+ aState.mPreviousListItemElement = nullptr;
+ }
+ return NS_OK;
+ }
+
+ // If we meet a list, we can reuse it or convert it to the expected type list.
+ if (HTMLEditUtils::IsAnyListElement(&aHandlingContent)) {
+ nsresult rv = HandleChildListElement(
+ aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::HandleChildListElement() failed");
+ return rv;
+ }
+
+ // We cannot handle nodes if not in element node.
+ if (NS_WARN_IF(!aHandlingContent.GetParentElement())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If we meet a list item, we can just move it to current list element or new
+ // list element.
+ if (HTMLEditUtils::IsListItem(&aHandlingContent)) {
+ nsresult rv = HandleChildListItemElement(
+ aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::HandleChildListItemElement() failed");
+ return rv;
+ }
+
+ // If we meet a <div> or a <p>, we want only its children to wrapping into
+ // list element. Therefore, this call will call this recursively.
+ if (aHandlingContent.IsAnyOfHTMLElements(nsGkAtoms::div, nsGkAtoms::p)) {
+ nsresult rv = HandleChildDivOrParagraphElement(
+ aHTMLEditor, MOZ_KnownLive(*aHandlingContent.AsElement()), aState,
+ aEditingHost);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::HandleChildDivOrParagraphElement() failed");
+ return rv;
+ }
+
+ // If we've not met a list element, create a list element and make it
+ // current list element.
+ if (!aState.mCurrentListElement) {
+ nsresult rv = CreateAndUpdateCurrentListElement(
+ aHTMLEditor, EditorDOMPoint(&aHandlingContent),
+ EmptyListItem::NotCreate, aState, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoListElementCreator::HandleChildInlineElement() failed");
+ return rv;
+ }
+ }
+
+ // If we meet an inline content, we want to move it to previously used list
+ // item element or new list item element.
+ if (HTMLEditUtils::IsInlineContent(aHandlingContent,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ nsresult rv =
+ HandleChildInlineContent(aHTMLEditor, aHandlingContent, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::HandleChildInlineElement() failed");
+ return rv;
+ }
+
+ // Otherwise, we should wrap it into new list item element.
+ nsresult rv =
+ WrapContentIntoNewListItemElement(aHTMLEditor, aHandlingContent, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::WrapContentIntoNewListItemElement() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildListElement(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListElement,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aHandlingListElement));
+
+ // If we met a list element and current list element is not a descendant
+ // of the list, append current node to end of the current list element.
+ // Then, wrap it with list item element and delete the old container.
+ if (aState.mCurrentListElement &&
+ !EditorUtils::IsDescendantOf(aHandlingListElement,
+ *aState.mCurrentListElement)) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ aHandlingListElement, MOZ_KnownLive(*aState.mCurrentListElement));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+
+ Result<CreateElementResult, nsresult> convertListTypeResult =
+ aHTMLEditor.ChangeListElementType(aHandlingListElement, mListTagName,
+ mListItemTagName);
+ if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
+ NS_WARNING("HTMLEditor::ChangeListElementType() failed");
+ return convertListTypeResult.propagateErr();
+ }
+ convertListTypeResult.inspect().IgnoreCaretPointSuggestion();
+
+ Result<EditorDOMPoint, nsresult> unwrapNewListElementResult =
+ aHTMLEditor.RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*convertListTypeResult.inspect().GetNewNode()));
+ if (MOZ_UNLIKELY(unwrapNewListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapNewListElementResult.propagateErr();
+ }
+ aState.mPreviousListItemElement = nullptr;
+ return NS_OK;
+ }
+
+ // If current list element is in found list element or we've not met a
+ // list element, convert current list element to proper type.
+ Result<CreateElementResult, nsresult> convertListTypeResult =
+ aHTMLEditor.ChangeListElementType(aHandlingListElement, mListTagName,
+ mListItemTagName);
+ if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
+ NS_WARNING("HTMLEditor::ChangeListElementType() failed");
+ return convertListTypeResult.propagateErr();
+ }
+ CreateElementResult unwrappedConvertListTypeResult =
+ convertListTypeResult.unwrap();
+ unwrappedConvertListTypeResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedConvertListTypeResult.GetNewNode());
+ aState.mCurrentListElement = unwrappedConvertListTypeResult.UnwrapNewNode();
+ aState.mPreviousListItemElement = nullptr;
+ return NS_OK;
+}
+
+nsresult
+HTMLEditor::AutoListElementCreator::HandleChildListItemInDifferentTypeList(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(&aHandlingListItemElement));
+ MOZ_ASSERT(
+ !aHandlingListItemElement.GetParent()->IsHTMLElement(&mListTagName));
+
+ // If we've not met a list element or current node is not in current list
+ // element, insert a list element at current node and set current list element
+ // to the new one.
+ if (!aState.mCurrentListElement ||
+ aHandlingListItemElement.IsInclusiveDescendantOf(
+ aState.mCurrentListElement)) {
+ EditorDOMPoint atListItem(&aHandlingListItemElement);
+ MOZ_ASSERT(atListItem.IsInContentNode());
+
+ Result<SplitNodeResult, nsresult> splitListItemParentResult =
+ aHTMLEditor.SplitNodeWithTransaction(atListItem);
+ if (MOZ_UNLIKELY(splitListItemParentResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitListItemParentResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitListItemParentResult =
+ splitListItemParentResult.unwrap();
+ MOZ_ASSERT(unwrappedSplitListItemParentResult.DidSplit());
+ unwrappedSplitListItemParentResult.IgnoreCaretPointSuggestion();
+
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ aHTMLEditor.CreateAndInsertElement(
+ WithTransaction::Yes, mListTagName,
+ unwrappedSplitListItemParentResult.AtNextContent<EditorDOMPoint>());
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) "
+ "failed");
+ return createNewListElementResult.propagateErr();
+ }
+ CreateElementResult unwrapCreateNewListElementResult =
+ createNewListElementResult.unwrap();
+ unwrapCreateNewListElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrapCreateNewListElementResult.GetNewNode());
+ aState.mCurrentListElement =
+ unwrapCreateNewListElementResult.UnwrapNewNode();
+ }
+
+ // Then, move current node into current list element.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ aHandlingListItemElement, MOZ_KnownLive(*aState.mCurrentListElement));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+
+ // Convert list item type if current node is different list item type.
+ if (aHandlingListItemElement.IsHTMLElement(&mListItemTagName)) {
+ return NS_OK;
+ }
+ Result<CreateElementResult, nsresult> newListItemElementOrError =
+ aHTMLEditor.ReplaceContainerAndCloneAttributesWithTransaction(
+ aHandlingListItemElement, mListItemTagName);
+ if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed");
+ return newListItemElementOrError.propagateErr();
+ }
+ newListItemElementOrError.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildListItemElement(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(aHandlingListItemElement.GetParentNode());
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(&aHandlingListItemElement));
+
+ // If current list item element is not in proper list element, we need
+ // to convert the list element.
+ // XXX This check is not enough,
+ if (!aHandlingListItemElement.GetParentNode()->IsHTMLElement(&mListTagName)) {
+ nsresult rv = HandleChildListItemInDifferentTypeList(
+ aHTMLEditor, aHandlingListItemElement, aState);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoListElementCreator::HandleChildListItemInDifferentTypeList() "
+ "failed");
+ return rv;
+ }
+ } else {
+ nsresult rv = HandleChildListItemInSameTypeList(
+ aHTMLEditor, aHandlingListItemElement, aState);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoListElementCreator::HandleChildListItemInSameTypeList() failed");
+ return rv;
+ }
+ }
+
+ // If bullet type is specified, set list type attribute.
+ // XXX Cannot we set type attribute before inserting the list item
+ // element into the DOM tree?
+ if (!mBulletType.IsEmpty()) {
+ nsresult rv = aHTMLEditor.SetAttributeWithTransaction(
+ aHandlingListItemElement, *nsGkAtoms::type, mBulletType);
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::type) failed");
+ return rv;
+ }
+
+ // Otherwise, remove list type attribute if there is.
+ if (!aHandlingListItemElement.HasAttr(nsGkAtoms::type)) {
+ return NS_OK;
+ }
+ nsresult rv = aHTMLEditor.RemoveAttributeWithTransaction(
+ aHandlingListItemElement, *nsGkAtoms::type);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::type) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildListItemInSameTypeList(
+ HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(&aHandlingListItemElement));
+ MOZ_ASSERT(
+ aHandlingListItemElement.GetParent()->IsHTMLElement(&mListTagName));
+
+ EditorDOMPoint atListItem(&aHandlingListItemElement);
+ MOZ_ASSERT(atListItem.IsInContentNode());
+
+ // If we've not met a list element, set current list element to the
+ // parent of current list item element.
+ if (!aState.mCurrentListElement) {
+ aState.mCurrentListElement = atListItem.GetContainerAs<Element>();
+ NS_WARNING_ASSERTION(
+ HTMLEditUtils::IsAnyListElement(aState.mCurrentListElement),
+ "Current list item parent is not a list element");
+ }
+ // If current list item element is not a child of current list element,
+ // move it into current list item.
+ else if (atListItem.GetContainer() != aState.mCurrentListElement) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ aHandlingListItemElement,
+ MOZ_KnownLive(*aState.mCurrentListElement));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ moveNodeResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ // Then, if current list item element is not proper type for current
+ // list element, convert list item element to proper element.
+ if (aHandlingListItemElement.IsHTMLElement(&mListItemTagName)) {
+ return NS_OK;
+ }
+ // FIXME: Manage attribute cloning
+ Result<CreateElementResult, nsresult> newListItemElementOrError =
+ aHTMLEditor.ReplaceContainerAndCloneAttributesWithTransaction(
+ aHandlingListItemElement, mListItemTagName);
+ if (MOZ_UNLIKELY(newListItemElementOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction() "
+ "failed");
+ return newListItemElementOrError.propagateErr();
+ }
+ newListItemElementOrError.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildDivOrParagraphElement(
+ HTMLEditor& aHTMLEditor, Element& aHandlingDivOrParagraphElement,
+ AutoHandlingState& aState, const Element& aEditingHost) const {
+ MOZ_ASSERT(aHandlingDivOrParagraphElement.IsAnyOfHTMLElements(nsGkAtoms::div,
+ nsGkAtoms::p));
+
+ AutoRestore<RefPtr<Element>> previouslyReplacingBlockElement(
+ aState.mReplacingBlockElement);
+ aState.mReplacingBlockElement = &aHandlingDivOrParagraphElement;
+ AutoRestore<bool> previouslyReplacingBlockElementIdCopied(
+ aState.mMaybeCopiedReplacingBlockElementId);
+ aState.mMaybeCopiedReplacingBlockElementId = false;
+
+ // If the <div> or <p> is empty, we should replace it with a list element
+ // and/or a list item element.
+ if (HTMLEditUtils::IsEmptyNode(aHandlingDivOrParagraphElement,
+ {EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible})) {
+ if (!aState.mCurrentListElement) {
+ nsresult rv = CreateAndUpdateCurrentListElement(
+ aHTMLEditor, EditorDOMPoint(&aHandlingDivOrParagraphElement),
+ EmptyListItem::Create, aState, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoListElementCreator::CreateAndUpdateCurrentListElement("
+ "EmptyListItem::Create) failed");
+ return rv;
+ }
+ } else {
+ Result<CreateElementResult, nsresult> createListItemElementResult =
+ AppendListItemElement(
+ aHTMLEditor, MOZ_KnownLive(*aState.mCurrentListElement), aState);
+ if (MOZ_UNLIKELY(createListItemElementResult.isErr())) {
+ NS_WARNING("AutoListElementCreator::AppendListItemElement() failed");
+ return createListItemElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedResult =
+ createListItemElementResult.unwrap();
+ unwrappedResult.IgnoreCaretPointSuggestion();
+ aState.mListOrListItemElementToPutCaret = unwrappedResult.UnwrapNewNode();
+ }
+ nsresult rv =
+ aHTMLEditor.DeleteNodeWithTransaction(aHandlingDivOrParagraphElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+
+ // We don't want new inline contents inserted into the new list item element
+ // because we want to keep the line break at end of
+ // aHandlingDivOrParagraphElement.
+ aState.mPreviousListItemElement = nullptr;
+
+ return NS_OK;
+ }
+
+ // If current node is a <div> element, replace it with its children and handle
+ // them as same as topmost children in the range.
+ AutoContentNodeArray arrayOfContentsInDiv;
+ HTMLEditUtils::CollectChildren(aHandlingDivOrParagraphElement,
+ arrayOfContentsInDiv, 0,
+ {CollectChildrenOption::CollectListChildren,
+ CollectChildrenOption::CollectTableChildren});
+
+ Result<EditorDOMPoint, nsresult> unwrapDivElementResult =
+ aHTMLEditor.RemoveContainerWithTransaction(
+ aHandlingDivOrParagraphElement);
+ if (MOZ_UNLIKELY(unwrapDivElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapDivElementResult.unwrapErr();
+ }
+
+ for (const OwningNonNull<nsIContent>& content : arrayOfContentsInDiv) {
+ // MOZ_KnownLive because of bug 1620312
+ nsresult rv = HandleChildContent(aHTMLEditor, MOZ_KnownLive(content),
+ aState, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoListElementCreator::HandleChildContent() failed");
+ return rv;
+ }
+ }
+
+ // We don't want new inline contents inserted into the new list item element
+ // because we want to keep the line break at end of
+ // aHandlingDivOrParagraphElement.
+ aState.mPreviousListItemElement = nullptr;
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::CreateAndUpdateCurrentListElement(
+ HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert,
+ EmptyListItem aEmptyListItem, AutoHandlingState& aState,
+ const Element& aEditingHost) const {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ aState.mPreviousListItemElement = nullptr;
+ RefPtr<Element> newListItemElement;
+ auto initializer =
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [&](HTMLEditor&, Element& aListElement, const EditorDOMPoint&)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ // If the replacing element has `dir` attribute, the new list
+ // element should take it to correct its list marker position.
+ if (aState.mReplacingBlockElement) {
+ nsString dirValue;
+ if (aState.mReplacingBlockElement->GetAttr(nsGkAtoms::dir,
+ dirValue) &&
+ !dirValue.IsEmpty()) {
+ // We don't need to use transaction to set `dir` attribute here
+ // because the element will be stored with the `dir` attribute
+ // in InsertNodeTransaction. Therefore, undo should work.
+ IgnoredErrorResult ignoredError;
+ aListElement.SetAttr(nsGkAtoms::dir, dirValue, ignoredError);
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "Element::SetAttr(nsGkAtoms::dir) failed, but ignored");
+ }
+ }
+ if (aEmptyListItem == EmptyListItem::Create) {
+ Result<CreateElementResult, nsresult> createNewListItemResult =
+ AppendListItemElement(aHTMLEditor, aListElement, aState);
+ if (MOZ_UNLIKELY(createNewListItemResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::AppendNewElementToInsertingElement()"
+ " failed");
+ return createNewListItemResult.unwrapErr();
+ }
+ CreateElementResult unwrappedResult =
+ createNewListItemResult.unwrap();
+ unwrappedResult.IgnoreCaretPointSuggestion();
+ newListItemElement = unwrappedResult.UnwrapNewNode();
+ }
+ return NS_OK;
+ };
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ aHTMLEditor.InsertElementWithSplittingAncestorsWithTransaction(
+ mListTagName, aPointToInsert, BRElementNextToSplitPoint::Keep,
+ aEditingHost, initializer);
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction(%s) failed",
+ nsAtomCString(&mListTagName).get())
+ .get());
+ return createNewListElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewListElementResult =
+ createNewListElementResult.unwrap();
+ unwrappedCreateNewListElementResult.IgnoreCaretPointSuggestion();
+
+ MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode());
+ aState.mListOrListItemElementToPutCaret =
+ newListItemElement ? newListItemElement.get()
+ : unwrappedCreateNewListElementResult.GetNewNode();
+ aState.mCurrentListElement =
+ unwrappedCreateNewListElementResult.UnwrapNewNode();
+ aState.mPreviousListItemElement = std::move(newListItemElement);
+ return NS_OK;
+}
+
+// static
+nsresult HTMLEditor::AutoListElementCreator::MaybeCloneAttributesToNewListItem(
+ HTMLEditor& aHTMLEditor, Element& aListItemElement,
+ AutoHandlingState& aState) {
+ if (!aState.mReplacingBlockElement) {
+ return NS_OK;
+ }
+ // If we're replacing a block element, the list items should have attributes
+ // of the replacing element. However, we don't want to copy `dir` attribute
+ // because it does not affect content in list item element and setting
+ // opposite direction from the parent list causes the marker invisible.
+ // Therefore, we don't want to take it. Finally, we don't need to use
+ // transaction to copy the attributes here because the element will be stored
+ // with the attributes in InsertNodeTransaction. Therefore, undo should work.
+ nsresult rv = aHTMLEditor.CopyAttributes(
+ WithTransaction::No, aListItemElement,
+ MOZ_KnownLive(*aState.mReplacingBlockElement),
+ aState.mMaybeCopiedReplacingBlockElementId
+ ? HTMLEditor::CopyAllAttributesExceptIdAndDir
+ : HTMLEditor::CopyAllAttributesExceptDir);
+ aState.mMaybeCopiedReplacingBlockElementId = true;
+ if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::CopyAttributes(WithTransaction::No) failed");
+ return rv;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::AutoListElementCreator::AppendListItemElement(
+ HTMLEditor& aHTMLEditor, const Element& aListElement,
+ AutoHandlingState& aState) const {
+ const WithTransaction withTransaction = aListElement.IsInComposedDoc()
+ ? WithTransaction::Yes
+ : WithTransaction::No;
+ Result<CreateElementResult, nsresult> createNewListItemResult =
+ aHTMLEditor.CreateAndInsertElement(
+ withTransaction, mListItemTagName,
+ EditorDOMPoint::AtEndOf(aListElement),
+ !aState.mReplacingBlockElement
+ ? HTMLEditor::DoNothingForNewElement
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ : [&aState](HTMLEditor& aHTMLEditor, Element& aListItemElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ nsresult rv =
+ AutoListElementCreator::MaybeCloneAttributesToNewListItem(
+ aHTMLEditor, aListItemElement, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::"
+ "MaybeCloneAttributesToNewListItem() failed");
+ return rv;
+ });
+ NS_WARNING_ASSERTION(createNewListItemResult.isOk(),
+ "HTMLEditor::CreateAndInsertElement() failed");
+ return createNewListItemResult;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::HandleChildInlineContent(
+ HTMLEditor& aHTMLEditor, nsIContent& aHandlingInlineContent,
+ AutoHandlingState& aState) const {
+ MOZ_ASSERT(HTMLEditUtils::IsInlineContent(
+ aHandlingInlineContent, BlockInlineCheck::UseHTMLDefaultStyle));
+
+ // If we're currently handling contents of a list item and current node
+ // is not a block element, move current node into the list item.
+ if (!aState.mPreviousListItemElement) {
+ nsresult rv = WrapContentIntoNewListItemElement(
+ aHTMLEditor, aHandlingInlineContent, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::WrapContentIntoNewListItemElement() failed");
+ return rv;
+ }
+
+ Result<MoveNodeResult, nsresult> moveInlineElementResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ aHandlingInlineContent,
+ MOZ_KnownLive(*aState.mPreviousListItemElement));
+ if (MOZ_UNLIKELY(moveInlineElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveInlineElementResult.propagateErr();
+ }
+ moveInlineElementResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::WrapContentIntoNewListItemElement(
+ HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent,
+ AutoHandlingState& aState) const {
+ // If current node is not a paragraph, wrap current node with new list
+ // item element and move it into current list element.
+ Result<CreateElementResult, nsresult> wrapContentInListItemElementResult =
+ aHTMLEditor.InsertContainerWithTransaction(
+ aHandlingContent, mListItemTagName,
+ !aState.mReplacingBlockElement
+ ? HTMLEditor::DoNothingForNewElement
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ : [&aState](HTMLEditor& aHTMLEditor, Element& aListItemElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ nsresult rv =
+ AutoListElementCreator::MaybeCloneAttributesToNewListItem(
+ aHTMLEditor, aListItemElement, aState);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "AutoListElementCreator::"
+ "MaybeCloneAttributesToNewListItem() failed");
+ return rv;
+ });
+ if (MOZ_UNLIKELY(wrapContentInListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
+ return wrapContentInListItemElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedWrapContentInListItemElementResult =
+ wrapContentInListItemElementResult.unwrap();
+ unwrappedWrapContentInListItemElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedWrapContentInListItemElementResult.GetNewNode());
+
+ // MOZ_KnownLive(unwrappedWrapContentInListItemElementResult.GetNewNode()):
+ // The result is grabbed by unwrappedWrapContentInListItemElementResult.
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ aHTMLEditor.MoveNodeToEndWithTransaction(
+ MOZ_KnownLive(
+ *unwrappedWrapContentInListItemElementResult.GetNewNode()),
+ MOZ_KnownLive(*aState.mCurrentListElement));
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveListItemElementResult.unwrapErr();
+ }
+ moveListItemElementResult.inspect().IgnoreCaretPointSuggestion();
+
+ // If current node is not a block element, new list item should have
+ // following inline nodes too.
+ if (HTMLEditUtils::IsInlineContent(aHandlingContent,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ aState.mPreviousListItemElement =
+ unwrappedWrapContentInListItemElementResult.UnwrapNewNode();
+ } else {
+ aState.mPreviousListItemElement = nullptr;
+ }
+
+ // XXX Why don't we set `type` attribute here??
+ return NS_OK;
+}
+
+nsresult HTMLEditor::AutoListElementCreator::
+ EnsureCollapsedRangeIsInListItemOrListElement(
+ Element& aListItemOrListToPutCaret, AutoRangeArray& aRanges) const {
+ if (!aRanges.IsCollapsed() || aRanges.Ranges().IsEmpty()) {
+ return NS_OK;
+ }
+
+ const auto firstRangeStartPoint =
+ aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!firstRangeStartPoint.IsSet())) {
+ return NS_OK;
+ }
+ Result<EditorRawDOMPoint, nsresult> pointToPutCaretOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(aListItemOrListToPutCaret, firstRangeStartPoint);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditUtils::ComputePointToPutCaretInElementIfOutside()");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ nsresult rv = aRanges.Collapse(pointToPutCaretOrError.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::RemoveListAtSelectionAsSubAction(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result.unwrapErr();
+ }
+ if (result.inspect().Canceled()) {
+ return NS_OK;
+ }
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult error;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eRemoveList, nsIEditor::eNext, error);
+ if (NS_WARN_IF(error.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return error.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ SelectionRef().GetRangeAt(0u), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.unwrapErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use Selection::SetStartAndEndInLimit() here.
+ error.SuppressException();
+ SelectionRef().SetBaseAndExtentInLimiter(
+ extendedRange.inspect().StartRef().ToRawRangeBoundary(),
+ extendedRange.inspect().EndRef().ToRawRangeBoundary(), error);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (error.Failed()) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return error.StealNSResult();
+ }
+ }
+
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ {
+ AutoRangeArray extendedSelectionRanges(SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ EditSubAction::eCreateOrChangeList,
+ BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedSelectionRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eCreateOrChangeList,
+ AutoRangeArray::CollectNonEditableNodes::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
+ return rv;
+ }
+ }
+
+ const Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eCreateOrChangeList);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eCreateOrChangeList) failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ }
+
+ // Remove all non-editable nodes. Leave them be.
+ // XXX CollectEditTargetNodes() should return only editable contents when it's
+ // called with CollectNonEditableNodes::No, but checking it here, looks
+ // like just wasting the runtime cost.
+ for (int32_t i = arrayOfContents.Length() - 1; i >= 0; i--) {
+ OwningNonNull<nsIContent>& content = arrayOfContents[i];
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ arrayOfContents.RemoveElementAt(i);
+ }
+ }
+
+ // Only act on lists or list items in the array
+ for (auto& content : arrayOfContents) {
+ // here's where we actually figure out what to do
+ if (HTMLEditUtils::IsListItem(content)) {
+ // unlist this listitem
+ nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()),
+ LiftUpFromAllParentListElements::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:"
+ ":Yes) failed");
+ return rv;
+ }
+ continue;
+ }
+ if (HTMLEditUtils::IsAnyListElement(content)) {
+ // node is a list, move list items out
+ nsresult rv =
+ DestroyListStructureRecursively(MOZ_KnownLive(*content->AsElement()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DestroyListStructureRecursively() failed");
+ return rv;
+ }
+ continue;
+ }
+ }
+ return NS_OK;
+}
+
+Result<RefPtr<Element>, nsresult>
+HTMLEditor::FormatBlockContainerWithTransaction(
+ AutoRangeArray& aSelectionRanges, const nsStaticAtom& aNewFormatTagName,
+ FormatBlockMode aFormatBlockMode, const Element& aEditingHost) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!aSelectionRanges.IsCollapsed() &&
+ aSelectionRanges.Ranges().Length() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ aSelectionRanges.FirstRangeRef(), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use AutoRangeArray::SetStartAndEnd() here.
+ if (NS_FAILED(aSelectionRanges.SetBaseAndExtent(
+ extendedRange.inspect().StartRef(),
+ extendedRange.inspect().EndRef()))) {
+ NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ MOZ_ALWAYS_TRUE(aSelectionRanges.SaveAndTrackRanges(*this));
+
+ // TODO: We don't need AutoTransactionsConserveSelection here in the normal
+ // cases, but removing this may cause the behavior with the legacy
+ // mutation event listeners. We should try to delete this in a bug.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ aSelectionRanges.ExtendRangesToWrapLines(
+ aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
+ ? EditSubAction::eFormatBlockForHTMLCommand
+ : EditSubAction::eCreateOrRemoveBlock,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ aSelectionRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed");
+ return splitResult.propagateErr();
+ }
+ nsresult rv = aSelectionRanges.CollectEditTargetNodes(
+ *this, arrayOfContents,
+ aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
+ ? EditSubAction::eFormatBlockForHTMLCommand
+ : EditSubAction::eCreateOrRemoveBlock,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(CollectNonEditableNodes::No) "
+ "failed");
+ return Err(rv);
+ }
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(
+ arrayOfContents,
+ aFormatBlockMode == FormatBlockMode::HTMLFormatBlockCommand
+ ? EditSubAction::eFormatBlockForHTMLCommand
+ : EditSubAction::eCreateOrRemoveBlock);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING("HTMLEditor::MaybeSplitElementsAtEveryBRElement() failed");
+ return splitAtBRElementsResult.propagateErr();
+ }
+
+ // If there is no visible and editable nodes in the edit targets, make an
+ // empty block.
+ // XXX Isn't this odd if there are only non-editable visible nodes?
+ if (HTMLEditUtils::IsEmptyOneHardLine(
+ arrayOfContents, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ if (NS_WARN_IF(aSelectionRanges.Ranges().IsEmpty())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ auto pointToInsertBlock =
+ aSelectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand &&
+ (&aNewFormatTagName == nsGkAtoms::normal ||
+ &aNewFormatTagName == nsGkAtoms::_empty)) {
+ if (!pointToInsertBlock.IsInContentNode()) {
+ NS_WARNING(
+ "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find "
+ "block parent because container of the point is not content");
+ return Err(NS_ERROR_FAILURE);
+ }
+ // We are removing blocks (going to "body text")
+ const RefPtr<Element> editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *pointToInsertBlock.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (!editableBlockElement) {
+ NS_WARNING(
+ "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find "
+ "block parent");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (editableBlockElement->IsAnyOfHTMLElements(
+ nsGkAtoms::dd, nsGkAtoms::dl, nsGkAtoms::dt) ||
+ !HTMLEditUtils::IsFormatElementForParagraphStateCommand(
+ *editableBlockElement)) {
+ return RefPtr<Element>();
+ }
+
+ // If the first editable node after selection is a br, consume it.
+ // Otherwise it gets pushed into a following block after the split,
+ // which is visually bad.
+ if (nsCOMPtr<nsIContent> brContent = HTMLEditUtils::GetNextContent(
+ pointToInsertBlock, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle,
+ &aEditingHost)) {
+ if (brContent && brContent->IsHTMLElement(nsGkAtoms::br)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
+ nsresult rv = DeleteNodeWithTransaction(*brContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ }
+ // Do the splits!
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitNodeDeepWithTransaction(
+ *editableBlockElement, pointToInsertBlock,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ unwrappedSplitNodeResult.IgnoreCaretPointSuggestion();
+ // Put a <br> element at the split point
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(
+ WithTransaction::Yes,
+ unwrappedSplitNodeResult.AtSplitPoint<EditorDOMPoint>());
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode());
+ aSelectionRanges.ClearSavedRanges();
+ nsresult rv = aSelectionRanges.Collapse(
+ EditorRawDOMPoint(insertBRElementResult.inspect().GetNewNode()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return Err(rv);
+ }
+ return RefPtr<Element>();
+ }
+
+ // We are making a block. Consume a br, if needed.
+ if (nsCOMPtr<nsIContent> maybeBRContent = HTMLEditUtils::GetNextContent(
+ pointToInsertBlock,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost)) {
+ if (maybeBRContent->IsHTMLElement(nsGkAtoms::br)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
+ nsresult rv = DeleteNodeWithTransaction(*maybeBRContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ // We don't need to act on this node any more
+ arrayOfContents.RemoveElement(maybeBRContent);
+ }
+ }
+ // Make sure we can put a block here.
+ Result<CreateElementResult, nsresult> createNewBlockElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ aNewFormatTagName, pointToInsertBlock,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "%s) failed",
+ nsAtomCString(&aNewFormatTagName).get())
+ .get());
+ return createNewBlockElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewBlockElementResult =
+ createNewBlockElementResult.unwrap();
+ unwrappedCreateNewBlockElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode());
+
+ // Delete anything that was in the list of nodes
+ while (!arrayOfContents.IsEmpty()) {
+ OwningNonNull<nsIContent>& content = arrayOfContents[0];
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ arrayOfContents.RemoveElementAt(0);
+ }
+ // Put selection in new block
+ aSelectionRanges.ClearSavedRanges();
+ nsresult rv = aSelectionRanges.Collapse(EditorRawDOMPoint(
+ unwrappedCreateNewBlockElementResult.GetNewNode(), 0u));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return Err(rv);
+ }
+ return unwrappedCreateNewBlockElementResult.UnwrapNewNode();
+ }
+
+ if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand) {
+ // Okay, now go through all the nodes and make the right kind of blocks, or
+ // whatever is appropriate.
+ // Note: blockquote is handled a little differently.
+ if (&aNewFormatTagName == nsGkAtoms::blockquote) {
+ Result<CreateElementResult, nsresult>
+ wrapContentsInBlockquoteElementsResult =
+ WrapContentsInBlockquoteElementsWithTransaction(arrayOfContents,
+ aEditingHost);
+ if (MOZ_UNLIKELY(wrapContentsInBlockquoteElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction() "
+ "failed");
+ return wrapContentsInBlockquoteElementsResult.propagateErr();
+ }
+ wrapContentsInBlockquoteElementsResult.inspect()
+ .IgnoreCaretPointSuggestion();
+ return wrapContentsInBlockquoteElementsResult.unwrap().UnwrapNewNode();
+ }
+ if (&aNewFormatTagName == nsGkAtoms::normal ||
+ &aNewFormatTagName == nsGkAtoms::_empty) {
+ Result<EditorDOMPoint, nsresult> removeBlockContainerElementsResult =
+ RemoveBlockContainerElementsWithTransaction(
+ arrayOfContents, FormatBlockMode::XULParagraphStateCommand,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle);
+ if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed");
+ return removeBlockContainerElementsResult.propagateErr();
+ }
+ return RefPtr<Element>();
+ }
+ }
+
+ Result<CreateElementResult, nsresult> wrapContentsInBlockElementResult =
+ CreateOrChangeFormatContainerElement(arrayOfContents, aNewFormatTagName,
+ aFormatBlockMode, aEditingHost);
+ if (MOZ_UNLIKELY(wrapContentsInBlockElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::CreateOrChangeFormatContainerElement() failed");
+ return wrapContentsInBlockElementResult.propagateErr();
+ }
+ wrapContentsInBlockElementResult.inspect().IgnoreCaretPointSuggestion();
+ return wrapContentsInBlockElementResult.unwrap().UnwrapNewNode();
+}
+
+nsresult HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+
+ if (!SelectionRef().IsCollapsed()) {
+ return NS_OK;
+ }
+
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return NS_ERROR_FAILURE;
+ }
+ const RangeBoundary& atStartOfSelection = firstRange->StartRef();
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!atStartOfSelection.Container()->IsElement()) {
+ return NS_OK;
+ }
+ OwningNonNull<Element> startContainerElement =
+ *atStartOfSelection.Container()->AsElement();
+ nsresult rv =
+ InsertPaddingBRElementForEmptyLastLineIfNeeded(startContainerElement);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineIfNeeded() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::IndentAsSubAction(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eIndent, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return EditActionResult::IgnoredResult();
+ }
+
+ Result<EditActionResult, nsresult> result =
+ HandleIndentAtSelection(aEditingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::HandleIndentAtSelection() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Mutation event listener might have changed selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // TODO: Investigate when we need to put a `<br>` element after indenting
+ // ranges. Then, we could stop calling this here, or maybe we need to
+ // do it while moving content nodes.
+ nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() failed");
+ return Err(rv);
+ }
+ return result;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::IndentListChildWithTransaction(
+ RefPtr<Element>* aSubListElement, const EditorDOMPoint& aPointInListElement,
+ nsIContent& aContentMovingToSubList, const Element& aEditingHost) {
+ MOZ_ASSERT(
+ HTMLEditUtils::IsAnyListElement(aPointInListElement.GetContainer()),
+ "unexpected container");
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // some logic for putting list items into nested lists...
+
+ // If aContentMovingToSubList is followed by a sub-list element whose tag is
+ // same as the parent list element's tag, we can move it to start of the
+ // sub-list.
+ if (nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling(
+ aContentMovingToSubList, {WalkTreeOption::IgnoreWhiteSpaceOnlyText,
+ WalkTreeOption::IgnoreNonEditableNode})) {
+ if (HTMLEditUtils::IsAnyListElement(nextEditableSibling) &&
+ aPointInListElement.GetContainer()->NodeInfo()->NameAtom() ==
+ nextEditableSibling->NodeInfo()->NameAtom() &&
+ aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() ==
+ nextEditableSibling->NodeInfo()->NamespaceID()) {
+ Result<MoveNodeResult, nsresult> moveListElementResult =
+ MoveNodeWithTransaction(aContentMovingToSubList,
+ EditorDOMPoint(nextEditableSibling, 0u));
+ if (MOZ_UNLIKELY(moveListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveListElementResult.propagateErr();
+ }
+ return moveListElementResult.unwrap().UnwrapCaretPoint();
+ }
+ }
+
+ // If aContentMovingToSubList follows a sub-list element whose tag is same
+ // as the parent list element's tag, we can move it to end of the sub-list.
+ if (nsCOMPtr<nsIContent> previousEditableSibling =
+ HTMLEditUtils::GetPreviousSibling(
+ aContentMovingToSubList,
+ {WalkTreeOption::IgnoreWhiteSpaceOnlyText,
+ WalkTreeOption::IgnoreNonEditableNode})) {
+ if (HTMLEditUtils::IsAnyListElement(previousEditableSibling) &&
+ aPointInListElement.GetContainer()->NodeInfo()->NameAtom() ==
+ previousEditableSibling->NodeInfo()->NameAtom() &&
+ aPointInListElement.GetContainer()->NodeInfo()->NamespaceID() ==
+ previousEditableSibling->NodeInfo()->NamespaceID()) {
+ Result<MoveNodeResult, nsresult> moveListElementResult =
+ MoveNodeToEndWithTransaction(aContentMovingToSubList,
+ *previousEditableSibling);
+ if (MOZ_UNLIKELY(moveListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveListElementResult.propagateErr();
+ }
+ return moveListElementResult.unwrap().UnwrapCaretPoint();
+ }
+ }
+
+ // If aContentMovingToSubList does not follow aSubListElement, we need
+ // to create new sub-list element.
+ EditorDOMPoint pointToPutCaret;
+ nsIContent* previousEditableSibling =
+ *aSubListElement ? HTMLEditUtils::GetPreviousSibling(
+ aContentMovingToSubList,
+ {WalkTreeOption::IgnoreWhiteSpaceOnlyText,
+ WalkTreeOption::IgnoreNonEditableNode})
+ : nullptr;
+ if (!*aSubListElement || (previousEditableSibling &&
+ previousEditableSibling != *aSubListElement)) {
+ nsAtom* containerName =
+ aPointInListElement.GetContainer()->NodeInfo()->NameAtom();
+ // Create a new nested list of correct type.
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ MOZ_KnownLive(*containerName), aPointInListElement,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "%s) failed",
+ nsAtomCString(containerName).get())
+ .get());
+ return createNewListElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewListElementResult =
+ createNewListElementResult.unwrap();
+ MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode());
+ pointToPutCaret = unwrappedCreateNewListElementResult.UnwrapCaretPoint();
+ *aSubListElement = unwrappedCreateNewListElementResult.UnwrapNewNode();
+ }
+
+ // Finally, we should move aContentMovingToSubList into aSubListElement.
+ const RefPtr<Element> subListElement = *aSubListElement;
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(aContentMovingToSubList, *subListElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ return pointToPutCaret;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::HandleIndentAtSelection(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ AutoRangeArray selectionRanges(SelectionRef());
+
+ if (MOZ_UNLIKELY(!selectionRanges.IsInContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (IsCSSEnabled()) {
+ nsresult rv = HandleCSSIndentAroundRanges(selectionRanges, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::HandleCSSIndentAroundRanges() failed");
+ return Err(rv);
+ }
+ } else {
+ nsresult rv = HandleHTMLIndentAroundRanges(selectionRanges, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::HandleHTMLIndentAroundRanges() failed");
+ return Err(rv);
+ }
+ }
+ rv = selectionRanges.ApplyTo(SelectionRef());
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING("AutoRangeArray::ApplyTo() caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::ApplyTo() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::HandleCSSIndentAroundRanges(AutoRangeArray& aRanges,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!aRanges.Ranges().IsEmpty());
+ MOZ_ASSERT(aRanges.IsInContent());
+
+ if (aRanges.Ranges().IsEmpty()) {
+ NS_WARNING("There is no selection range");
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!aRanges.IsCollapsed() && aRanges.Ranges().Length() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ aRanges.FirstRangeRef(), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.unwrapErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use SetStartAndEnd() here.
+ nsresult rv = aRanges.SetBaseAndExtent(extendedRange.inspect().StartRef(),
+ extendedRange.inspect().EndRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed");
+ return rv;
+ }
+ }
+
+ if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+
+ // short circuit: detect case of collapsed selection inside an <li>.
+ // just sublist that <li>. This prevents bug 97797.
+
+ if (aRanges.IsCollapsed()) {
+ const auto atCaret = aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!atCaret.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atCaret.IsInContentNode());
+ Element* const editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *atCaret.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseHTMLDefaultStyle);
+ if (editableBlockElement &&
+ HTMLEditUtils::IsListItem(editableBlockElement)) {
+ arrayOfContents.AppendElement(*editableBlockElement);
+ }
+ }
+
+ EditorDOMPoint pointToPutCaret;
+ if (arrayOfContents.IsEmpty()) {
+ {
+ AutoRangeArray extendedRanges(aRanges);
+ extendedRanges.ExtendRangesToWrapLines(
+ EditSubAction::eIndent, BlockInlineCheck::UseHTMLDefaultStyle,
+ aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ nsresult rv = extendedRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eIndent,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eIndent, "
+ "CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eIndent);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eIndent) failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+ }
+
+ // If there is no visible and editable nodes in the edit targets, make an
+ // empty block.
+ // XXX Isn't this odd if there are only non-editable visible nodes?
+ if (HTMLEditUtils::IsEmptyOneHardLine(
+ arrayOfContents, BlockInlineCheck::UseHTMLDefaultStyle)) {
+ const EditorDOMPoint pointToInsertDivElement =
+ pointToPutCaret.IsSet()
+ ? std::move(pointToPutCaret)
+ : aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsertDivElement.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // make sure we can put a block here
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, pointToInsertDivElement,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ // We'll collapse ranges below, so we don't need to touch the ranges here.
+ unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion();
+ const RefPtr<Element> newDivElement =
+ unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newDivElement);
+ const Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(*newDivElement, ChangeMargin::Increase, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but "
+ "ignored");
+ }
+ // delete anything that was in the list of nodes
+ // XXX We don't need to remove the nodes from the array for performance.
+ for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // MOZ_KnownLive(content) due to bug 1622253
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+ aRanges.ClearSavedRanges();
+ nsresult rv = aRanges.Collapse(EditorDOMPoint(newDivElement, 0u));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+
+ RefPtr<Element> latestNewBlockElement;
+ auto RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside =
+ [&]() -> nsresult {
+ MOZ_ASSERT(aRanges.HasSavedRanges());
+ aRanges.RestoreFromSavedRanges();
+
+ if (!latestNewBlockElement || !aRanges.IsCollapsed() ||
+ aRanges.Ranges().IsEmpty()) {
+ return NS_OK;
+ }
+
+ const auto firstRangeStartRawPoint =
+ aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) {
+ return NS_OK;
+ }
+ Result<EditorRawDOMPoint, nsresult> pointInNewBlockElementOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(*latestNewBlockElement, firstRangeStartRawPoint);
+ if (MOZ_UNLIKELY(pointInNewBlockElementOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, "
+ "but ignored");
+ return NS_OK;
+ }
+ if (!pointInNewBlockElementOrError.inspect().IsSet()) {
+ return NS_OK;
+ }
+ return aRanges.Collapse(pointInNewBlockElementOrError.unwrap());
+ };
+
+ // Ok, now go through all the nodes and put them into sub-list element
+ // elements and new <div> elements which have start margin.
+ RefPtr<Element> subListElement, divElement;
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // Here's where we actually figure out what to do.
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ continue;
+ }
+
+ // Ignore all non-editable nodes. Leave them be.
+ // XXX We ignore non-editable nodes here, but not so in the above block.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ const RefPtr<Element> oldSubListElement = subListElement;
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ IndentListChildWithTransaction(&subListElement, atContent,
+ MOZ_KnownLive(content), aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::IndentListChildWithTransaction() failed");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ if (subListElement != oldSubListElement) {
+ // New list element is created, so we should put caret into the new list
+ // element.
+ latestNewBlockElement = subListElement;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ continue;
+ }
+
+ // Not a list item.
+
+ if (HTMLEditUtils::IsBlockElement(content,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(MOZ_KnownLive(*content->AsElement()),
+ ChangeMargin::Increase, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but "
+ "ignored");
+ } else if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ divElement = nullptr;
+ continue;
+ }
+
+ if (!divElement) {
+ // First, check that our element can contain a div.
+ if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(),
+ *nsGkAtoms::div)) {
+ // XXX This is odd, why do we stop indenting remaining content nodes?
+ // Perhaps, `continue` is better.
+ nsresult rv =
+ RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "RestoreSavedRangesAndCollapseInLatestBlockElement"
+ "IfOutside() failed");
+ return rv;
+ }
+
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ pointToPutCaret = unwrappedCreateNewDivElementResult.UnwrapCaretPoint();
+
+ MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode());
+ divElement = unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(*divElement, ChangeMargin::Increase, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Increase) failed, but "
+ "ignored");
+ } else if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+
+ latestNewBlockElement = divElement;
+ }
+
+ // Move the content into the <div> which has start margin.
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *divElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.unwrapErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ }
+
+ nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::HandleHTMLIndentAroundRanges(AutoRangeArray& aRanges,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!aRanges.Ranges().IsEmpty());
+ MOZ_ASSERT(aRanges.IsInContent());
+
+ // XXX Why do we do this only when there is only one range?
+ if (!aRanges.IsCollapsed() && aRanges.Ranges().Length() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ aRanges.FirstRangeRef(), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.unwrapErr();
+ }
+ // Note that end point may be prior to start point. So, we cannot use
+ // SetStartAndEnd() here.
+ nsresult rv = aRanges.SetBaseAndExtent(extendedRange.inspect().StartRef(),
+ extendedRange.inspect().EndRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::SetBaseAndExtent() failed");
+ return rv;
+ }
+ }
+
+ if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorDOMPoint pointToPutCaret;
+
+ // convert the selection ranges into "promoted" selection ranges:
+ // this basically just expands the range to include the immediate
+ // block parent, and then further expands to include any ancestors
+ // whose children are all in the range
+
+ // use these ranges to construct a list of nodes to act on.
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedRanges(aRanges);
+ extendedRanges.ExtendRangesToWrapLines(
+ EditSubAction::eIndent, BlockInlineCheck::UseHTMLDefaultStyle,
+ aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ nsresult rv = extendedRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eIndent,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eIndent, "
+ "CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+
+ // FIXME: Split ancestors when we consider to indent the range.
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eIndent);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::eIndent)"
+ " failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+
+ // If there is no visible and editable nodes in the edit targets, make an
+ // empty block.
+ // XXX Isn't this odd if there are only non-editable visible nodes?
+ if (HTMLEditUtils::IsEmptyOneHardLine(
+ arrayOfContents, BlockInlineCheck::UseHTMLDefaultStyle)) {
+ const EditorDOMPoint pointToInsertBlockquoteElement =
+ pointToPutCaret.IsSet()
+ ? std::move(pointToPutCaret)
+ : EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!pointToInsertBlockquoteElement.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If there is no element which can have <blockquote>, abort.
+ if (NS_WARN_IF(!HTMLEditUtils::GetInsertionPointInInclusiveAncestor(
+ *nsGkAtoms::blockquote, pointToInsertBlockquoteElement,
+ &aEditingHost)
+ .IsSet())) {
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+
+ // Make sure we can put a block here.
+ // XXX Unfortunately, this calls
+ // MaybeSplitAncestorsForInsertWithTransaction() then,
+ // HTMLEditUtils::GetInsertionPointInInclusiveAncestor() is called again.
+ Result<CreateElementResult, nsresult> createNewBlockquoteElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::blockquote, pointToInsertBlockquoteElement,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::blockquote) failed");
+ return createNewBlockquoteElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewBlockquoteElementResult =
+ createNewBlockquoteElementResult.unwrap();
+ unwrappedCreateNewBlockquoteElementResult.IgnoreCaretPointSuggestion();
+ RefPtr<Element> newBlockquoteElement =
+ unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newBlockquoteElement);
+ // delete anything that was in the list of nodes
+ // XXX We don't need to remove the nodes from the array for performance.
+ for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+ aRanges.ClearSavedRanges();
+ nsresult rv = aRanges.Collapse(EditorRawDOMPoint(newBlockquoteElement, 0u));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ return rv;
+ }
+
+ RefPtr<Element> latestNewBlockElement;
+ auto RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside =
+ [&]() -> nsresult {
+ MOZ_ASSERT(aRanges.HasSavedRanges());
+ aRanges.RestoreFromSavedRanges();
+
+ if (!latestNewBlockElement || !aRanges.IsCollapsed() ||
+ aRanges.Ranges().IsEmpty()) {
+ return NS_OK;
+ }
+
+ const auto firstRangeStartRawPoint =
+ aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!firstRangeStartRawPoint.IsSet())) {
+ return NS_OK;
+ }
+ Result<EditorRawDOMPoint, nsresult> pointInNewBlockElementOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(*latestNewBlockElement, firstRangeStartRawPoint);
+ if (MOZ_UNLIKELY(pointInNewBlockElementOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, "
+ "but ignored");
+ return NS_OK;
+ }
+ if (!pointInNewBlockElementOrError.inspect().IsSet()) {
+ return NS_OK;
+ }
+ return aRanges.Collapse(pointInNewBlockElementOrError.unwrap());
+ };
+
+ // Ok, now go through all the nodes and put them in a blockquote,
+ // or whatever is appropriate. Wohoo!
+ RefPtr<Element> subListElement, blockquoteElement, indentedListItemElement;
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // Here's where we actually figure out what to do.
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ continue;
+ }
+
+ // Ignore all non-editable nodes. Leave them be.
+ // XXX We ignore non-editable nodes here, but not so in the above block.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML) ||
+ !HTMLEditUtils::IsRemovableNode(content)) {
+ continue;
+ }
+
+ // If the content has been moved to different place, ignore it.
+ if (MOZ_UNLIKELY(!content->IsInclusiveDescendantOf(&aEditingHost))) {
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ const RefPtr<Element> oldSubListElement = subListElement;
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ IndentListChildWithTransaction(&subListElement, atContent,
+ MOZ_KnownLive(content), aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::IndentListChildWithTransaction() failed");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ if (oldSubListElement != subListElement) {
+ // New list element is created, so we should put caret into the new list
+ // element.
+ latestNewBlockElement = subListElement;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ blockquoteElement = nullptr;
+ continue;
+ }
+
+ // Not a list item, use blockquote?
+
+ // if we are inside a list item, we don't want to blockquote, we want
+ // to sublist the list item. We may have several nodes listed in the
+ // array of nodes to act on, that are in the same list item. Since
+ // we only want to indent that li once, we must keep track of the most
+ // recent indented list item, and not indent it if we find another node
+ // to act on that is still inside the same li.
+ if (RefPtr<Element> listItem =
+ HTMLEditUtils::GetClosestAncestorListItemElement(content,
+ &aEditingHost)) {
+ if (indentedListItemElement == listItem) {
+ // already indented this list item
+ continue;
+ }
+ // check to see if subListElement is still appropriate. Which it is if
+ // content is still right after it in the same list.
+ nsIContent* previousEditableSibling =
+ subListElement
+ ? HTMLEditUtils::GetPreviousSibling(
+ *listItem, {WalkTreeOption::IgnoreNonEditableNode})
+ : nullptr;
+ if (!subListElement || (previousEditableSibling &&
+ previousEditableSibling != subListElement)) {
+ EditorDOMPoint atListItem(listItem);
+ if (NS_WARN_IF(!listItem)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAtom* containerName =
+ atListItem.GetContainer()->NodeInfo()->NameAtom();
+ // Create a new nested list of correct type.
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ MOZ_KnownLive(*containerName), atListItem,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(nsPrintfCString("HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTr"
+ "ansaction(%s) failed",
+ nsAtomCString(containerName).get())
+ .get());
+ return createNewListElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewListElementResult =
+ createNewListElementResult.unwrap();
+ if (unwrappedCreateNewListElementResult.HasCaretPointSuggestion()) {
+ pointToPutCaret =
+ unwrappedCreateNewListElementResult.UnwrapCaretPoint();
+ }
+ MOZ_ASSERT(unwrappedCreateNewListElementResult.GetNewNode());
+ subListElement = unwrappedCreateNewListElementResult.UnwrapNewNode();
+ }
+
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ MoveNodeToEndWithTransaction(*listItem, *subListElement);
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveListItemElementResult.unwrapErr();
+ }
+ MoveNodeResult unwrappedMoveListItemElementResult =
+ moveListItemElementResult.unwrap();
+ if (unwrappedMoveListItemElementResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveListItemElementResult.UnwrapCaretPoint();
+ }
+
+ // Remember the list item element which we indented now for ignoring its
+ // children to avoid using <blockquote> in it.
+ indentedListItemElement = std::move(listItem);
+
+ continue;
+ }
+
+ // need to make a blockquote to put things in if we haven't already,
+ // or if this node doesn't go in blockquote we used earlier.
+ // One reason it might not go in prio blockquote is if we are now
+ // in a different table cell.
+ if (blockquoteElement &&
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
+ *blockquoteElement) !=
+ HTMLEditUtils::GetInclusiveAncestorAnyTableElement(content)) {
+ blockquoteElement = nullptr;
+ }
+
+ if (!blockquoteElement) {
+ // First, check that our element can contain a blockquote.
+ if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(),
+ *nsGkAtoms::blockquote)) {
+ // XXX This is odd, why do we stop indenting remaining content nodes?
+ // Perhaps, `continue` is better.
+ nsresult rv =
+ RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "RestoreSavedRangesAndCollapseInLatestBlockElement"
+ "IfOutside() failed");
+ return rv;
+ }
+
+ Result<CreateElementResult, nsresult> createNewBlockquoteElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::blockquote, atContent,
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::blockquote) failed");
+ return createNewBlockquoteElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewBlockquoteElementResult =
+ createNewBlockquoteElementResult.unwrap();
+ if (unwrappedCreateNewBlockquoteElementResult.HasCaretPointSuggestion()) {
+ pointToPutCaret =
+ unwrappedCreateNewBlockquoteElementResult.UnwrapCaretPoint();
+ }
+
+ MOZ_ASSERT(unwrappedCreateNewBlockquoteElementResult.GetNewNode());
+ blockquoteElement =
+ unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode();
+ latestNewBlockElement = blockquoteElement;
+ }
+
+ // tuck the node into the end of the active blockquote
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to
+ // keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
+ *blockquoteElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.unwrapErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ subListElement = nullptr;
+ }
+
+ nsresult rv = RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "RestoreSavedRangesAndCollapseInLatestBlockElementIfOutside() failed");
+ return rv;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::OutdentAsSubAction(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eOutdent, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return EditActionResult::IgnoredResult();
+ }
+
+ Result<EditActionResult, nsresult> result =
+ HandleOutdentAtSelection(aEditingHost);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::HandleOutdentAtSelection() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ nsresult rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() "
+ "failed");
+ return Err(rv);
+ }
+ return result;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::HandleOutdentAtSelection(
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ SelectionRef().GetRangeAt(0u), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use Selection::SetStartAndEndInLimit() here.
+ IgnoredErrorResult error;
+ SelectionRef().SetBaseAndExtentInLimiter(
+ extendedRange.inspect().StartRef().ToRawRangeBoundary(),
+ extendedRange.inspect().EndRef().ToRawRangeBoundary(), error);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return Err(error.StealNSResult());
+ }
+ }
+
+ // HandleOutdentAtSelectionInternal() creates AutoSelectionRestorer.
+ // Therefore, even if it returns NS_OK, the editor might have been destroyed
+ // at restoring Selection.
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ HandleOutdentAtSelectionInternal(aEditingHost);
+ MOZ_ASSERT_IF(outdentResult.isOk(),
+ !outdentResult.inspect().HasCaretPointSuggestion());
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::HandleOutdentAtSelectionInternal() failed");
+ return outdentResult.propagateErr();
+ }
+ SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap();
+
+ // Make sure selection didn't stick to last piece of content in old bq (only
+ // a problem for collapsed selections)
+ if (!unwrappedOutdentResult.GetLeftContent() &&
+ !unwrappedOutdentResult.GetRightContent()) {
+ return EditActionResult::HandledResult();
+ }
+
+ if (!SelectionRef().IsCollapsed()) {
+ return EditActionResult::HandledResult();
+ }
+
+ // Push selection past end of left element of last split indented element.
+ if (unwrappedOutdentResult.GetLeftContent()) {
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return EditActionResult::HandledResult();
+ }
+ const RangeBoundary& atStartOfSelection = firstRange->StartRef();
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (atStartOfSelection.Container() ==
+ unwrappedOutdentResult.GetLeftContent() ||
+ EditorUtils::IsDescendantOf(*atStartOfSelection.Container(),
+ *unwrappedOutdentResult.GetLeftContent())) {
+ // Selection is inside the left node - push it past it.
+ EditorRawDOMPoint afterRememberedLeftBQ(
+ EditorRawDOMPoint::After(*unwrappedOutdentResult.GetLeftContent()));
+ NS_WARNING_ASSERTION(
+ afterRememberedLeftBQ.IsSet(),
+ "Failed to set after remembered left blockquote element");
+ nsresult rv = CollapseSelectionTo(afterRememberedLeftBQ);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+ // And pull selection before beginning of right element of last split
+ // indented element.
+ if (unwrappedOutdentResult.GetRightContent()) {
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return EditActionResult::HandledResult();
+ }
+ const RangeBoundary& atStartOfSelection = firstRange->StartRef();
+ if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (atStartOfSelection.Container() ==
+ unwrappedOutdentResult.GetRightContent() ||
+ EditorUtils::IsDescendantOf(
+ *atStartOfSelection.Container(),
+ *unwrappedOutdentResult.GetRightContent())) {
+ // Selection is inside the right element - push it before it.
+ EditorRawDOMPoint atRememberedRightBQ(
+ unwrappedOutdentResult.GetRightContent());
+ nsresult rv = CollapseSelectionTo(atRememberedRightBQ);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ }
+ return EditActionResult::HandledResult();
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult>
+HTMLEditor::HandleOutdentAtSelectionInternal(const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ bool useCSS = IsCSSEnabled();
+
+ // Convert the selection ranges into "promoted" selection ranges: this
+ // basically just expands the range to include the immediate block parent,
+ // and then further expands to include any ancestors whose children are all
+ // in the range
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedSelectionRanges(SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ EditSubAction::eOutdent, BlockInlineCheck::UseHTMLDefaultStyle,
+ aEditingHost);
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eOutdent,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::eOutdent, "
+ "CollectNonEditableNodes::Yes) failed");
+ return Err(rv);
+ }
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eOutdent);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eOutdent) failed");
+ return splitAtBRElementsResult.propagateErr();
+ }
+ if (AllowsTransactionsToChangeSelection() &&
+ splitAtBRElementsResult.inspect().IsSet()) {
+ nsresult rv = CollapseSelectionTo(splitAtBRElementsResult.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ }
+
+ nsCOMPtr<nsIContent> leftContentOfLastOutdented;
+ nsCOMPtr<nsIContent> middleContentOfLastOutdented;
+ nsCOMPtr<nsIContent> rightContentOfLastOutdented;
+ RefPtr<Element> indentedParentElement;
+ nsCOMPtr<nsIContent> firstContentToBeOutdented, lastContentToBeOutdented;
+ BlockIndentedWith indentedParentIndentedWith = BlockIndentedWith::HTML;
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // Here's where we actually figure out what to do
+ EditorDOMPoint atContent(content);
+ if (!atContent.IsSet()) {
+ continue;
+ }
+
+ // If it's a `<blockquote>`, remove it to outdent its children.
+ if (content->IsHTMLElement(nsGkAtoms::blockquote)) {
+ // If we've already found an ancestor block element indented, we need to
+ // split it and remove the block element first.
+ if (indentedParentElement) {
+ NS_WARNING_ASSERTION(indentedParentElement == content,
+ "Indented parent element is not the <blockquote>");
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ OutdentPartOfBlock(*indentedParentElement,
+ *firstContentToBeOutdented,
+ *lastContentToBeOutdented,
+ indentedParentIndentedWith, aEditingHost);
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed");
+ return outdentResult;
+ }
+ SplitRangeOffFromNodeResult unwrappedOutdentResult =
+ outdentResult.unwrap();
+ unwrappedOutdentResult.IgnoreCaretPointSuggestion();
+ leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent();
+ middleContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapMiddleContent();
+ rightContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapRightContent();
+ indentedParentElement = nullptr;
+ firstContentToBeOutdented = nullptr;
+ lastContentToBeOutdented = nullptr;
+ indentedParentIndentedWith = BlockIndentedWith::HTML;
+ }
+ Result<EditorDOMPoint, nsresult> unwrapBlockquoteElementResult =
+ RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*content->AsElement()));
+ if (MOZ_UNLIKELY(unwrapBlockquoteElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapBlockquoteElementResult.propagateErr();
+ }
+ const EditorDOMPoint& pointToPutCaret =
+ unwrapBlockquoteElementResult.inspect();
+ if (AllowsTransactionsToChangeSelection() && pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ continue;
+ }
+
+ // If we're using CSS and the node is a block element, check its start
+ // margin whether it's indented with CSS.
+ if (useCSS && HTMLEditUtils::IsBlockElement(
+ content, BlockInlineCheck::UseHTMLDefaultStyle)) {
+ nsStaticAtom& marginProperty =
+ MarginPropertyAtomForIndent(MOZ_KnownLive(content));
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ nsAutoString value;
+ DebugOnly<nsresult> rvIgnored =
+ CSSEditUtils::GetSpecifiedProperty(content, marginProperty, value);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetSpecifiedProperty() failed, but ignored");
+ float startMargin = 0;
+ RefPtr<nsAtom> unit;
+ CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit));
+ // If indented with CSS, we should decrease the start margin.
+ if (startMargin > 0) {
+ const Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(MOZ_KnownLive(*content->AsElement()),
+ ChangeMargin::Decrease, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Decrease) failed, "
+ "but ignored");
+ } else if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaretOrError.inspect().IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ continue;
+ }
+ }
+
+ // If it's a list item, we should treat as that it "indents" its children.
+ if (HTMLEditUtils::IsListItem(content)) {
+ // If it is a list item, that means we are not outdenting whole list.
+ // XXX I don't understand this sentence... We may meet parent list
+ // element, no?
+ if (indentedParentElement) {
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ OutdentPartOfBlock(*indentedParentElement,
+ *firstContentToBeOutdented,
+ *lastContentToBeOutdented,
+ indentedParentIndentedWith, aEditingHost);
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed");
+ return outdentResult;
+ }
+ SplitRangeOffFromNodeResult unwrappedOutdentResult =
+ outdentResult.unwrap();
+ unwrappedOutdentResult.IgnoreCaretPointSuggestion();
+ leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent();
+ middleContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapMiddleContent();
+ rightContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapRightContent();
+ indentedParentElement = nullptr;
+ firstContentToBeOutdented = nullptr;
+ lastContentToBeOutdented = nullptr;
+ indentedParentIndentedWith = BlockIndentedWith::HTML;
+ }
+ // XXX `content` could become different element since
+ // `OutdentPartOfBlock()` may run mutation event listeners.
+ nsresult rv = LiftUpListItemElement(MOZ_KnownLive(*content->AsElement()),
+ LiftUpFromAllParentListElements::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:"
+ ":No) failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ // If we've found an ancestor block element which indents its children
+ // and the current node is NOT a descendant of it, we should remove it to
+ // outdent its children. Otherwise, i.e., current node is a descendant of
+ // it, we meet new node which should be outdented when the indented parent
+ // is removed.
+ if (indentedParentElement) {
+ if (EditorUtils::IsDescendantOf(*content, *indentedParentElement)) {
+ // Extend the range to be outdented at removing the
+ // indentedParentElement.
+ lastContentToBeOutdented = content;
+ continue;
+ }
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented,
+ *lastContentToBeOutdented,
+ indentedParentIndentedWith, aEditingHost);
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed");
+ return outdentResult;
+ }
+ SplitRangeOffFromNodeResult unwrappedOutdentResult =
+ outdentResult.unwrap();
+ unwrappedOutdentResult.IgnoreCaretPointSuggestion();
+ leftContentOfLastOutdented = unwrappedOutdentResult.UnwrapLeftContent();
+ middleContentOfLastOutdented =
+ unwrappedOutdentResult.UnwrapMiddleContent();
+ rightContentOfLastOutdented = unwrappedOutdentResult.UnwrapRightContent();
+ indentedParentElement = nullptr;
+ firstContentToBeOutdented = nullptr;
+ lastContentToBeOutdented = nullptr;
+ // curBlockIndentedWith = HTMLEditor::BlockIndentedWith::HTML;
+
+ // Then, we need to look for next indentedParentElement.
+ }
+
+ indentedParentIndentedWith = BlockIndentedWith::HTML;
+ for (nsCOMPtr<nsIContent> parentContent = content->GetParent();
+ parentContent && !parentContent->IsHTMLElement(nsGkAtoms::body) &&
+ parentContent != &aEditingHost &&
+ (parentContent->IsHTMLElement(nsGkAtoms::table) ||
+ !HTMLEditUtils::IsAnyTableElement(parentContent));
+ parentContent = parentContent->GetParent()) {
+ if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(*parentContent))) {
+ continue;
+ }
+ // If we reach a `<blockquote>` ancestor, it should be split at next
+ // time at least for outdenting current node.
+ if (parentContent->IsHTMLElement(nsGkAtoms::blockquote)) {
+ indentedParentElement = parentContent->AsElement();
+ firstContentToBeOutdented = content;
+ lastContentToBeOutdented = content;
+ break;
+ }
+
+ if (!useCSS) {
+ continue;
+ }
+
+ nsCOMPtr<nsINode> grandParentNode = parentContent->GetParentNode();
+ nsStaticAtom& marginProperty =
+ MarginPropertyAtomForIndent(MOZ_KnownLive(content));
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_WARN_IF(grandParentNode != parentContent->GetParentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ nsAutoString value;
+ DebugOnly<nsresult> rvIgnored = CSSEditUtils::GetSpecifiedProperty(
+ *parentContent, marginProperty, value);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetSpecifiedProperty() failed, but ignored");
+ // XXX Now, editing host may become different element. If so, shouldn't
+ // we stop this handling?
+ float startMargin;
+ RefPtr<nsAtom> unit;
+ CSSEditUtils::ParseLength(value, &startMargin, getter_AddRefs(unit));
+ // If we reach a block element which indents its children with start
+ // margin, we should remove it at next time.
+ if (startMargin > 0 &&
+ !(HTMLEditUtils::IsAnyListElement(atContent.GetContainer()) &&
+ HTMLEditUtils::IsAnyListElement(content))) {
+ indentedParentElement = parentContent->AsElement();
+ firstContentToBeOutdented = content;
+ lastContentToBeOutdented = content;
+ indentedParentIndentedWith = BlockIndentedWith::CSS;
+ break;
+ }
+ }
+
+ if (indentedParentElement) {
+ continue;
+ }
+
+ // If we don't have any block elements which indents current node and
+ // both current node and its parent are list element, remove current
+ // node to move all its children to the parent list.
+ // XXX This is buggy. When both lists' item types are different,
+ // we create invalid tree. E.g., `<ul>` may have `<dd>` as its
+ // list item element.
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ if (!HTMLEditUtils::IsAnyListElement(content)) {
+ continue;
+ }
+ // Just unwrap this sublist
+ Result<EditorDOMPoint, nsresult> unwrapSubListElementResult =
+ RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*content->AsElement()));
+ if (MOZ_UNLIKELY(unwrapSubListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapSubListElementResult.propagateErr();
+ }
+ const EditorDOMPoint& pointToPutCaret =
+ unwrapSubListElementResult.inspect();
+ if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
+ continue;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ // If current content is a list element but its parent is not a list
+ // element, move children to where it is and remove it from the tree.
+ if (HTMLEditUtils::IsAnyListElement(content)) {
+ // XXX If mutation event listener appends new children forever, this
+ // becomes an infinite loop so that we should set limitation from
+ // first child count.
+ for (nsCOMPtr<nsIContent> lastChildContent = content->GetLastChild();
+ lastChildContent; lastChildContent = content->GetLastChild()) {
+ if (HTMLEditUtils::IsListItem(lastChildContent)) {
+ nsresult rv = LiftUpListItemElement(
+ MOZ_KnownLive(*lastChildContent->AsElement()),
+ LiftUpFromAllParentListElements::No);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::LiftUpListItemElement("
+ "LiftUpFromAllParentListElements::No) failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(lastChildContent)) {
+ // We have an embedded list, so move it out from under the parent
+ // list. Be sure to put it after the parent list because this
+ // loop iterates backwards through the parent's list of children.
+ EditorDOMPoint afterCurrentList(EditorDOMPoint::After(atContent));
+ NS_WARNING_ASSERTION(
+ afterCurrentList.IsSet(),
+ "Failed to set it to after current list element");
+ Result<MoveNodeResult, nsresult> moveListElementResult =
+ MoveNodeWithTransaction(*lastChildContent, afterCurrentList);
+ if (MOZ_UNLIKELY(moveListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveListElementResult.propagateErr();
+ }
+ nsresult rv = moveListElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ continue;
+ }
+
+ // Delete any non-list items for now
+ // XXX Chrome moves it from the list element. We should follow it.
+ nsresult rv = DeleteNodeWithTransaction(*lastChildContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ // Delete the now-empty list
+ Result<EditorDOMPoint, nsresult> unwrapListElementResult =
+ RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*content->AsElement()));
+ if (MOZ_UNLIKELY(unwrapListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapListElementResult.propagateErr();
+ }
+ const EditorDOMPoint& pointToPutCaret = unwrapListElementResult.inspect();
+ if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
+ continue;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ if (useCSS) {
+ if (RefPtr<Element> element = content->GetAsElementOrParentElement()) {
+ const Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ChangeMarginStart(*element, ChangeMargin::Decrease, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::ChangeMarginStart(ChangeMargin::Decrease) failed, "
+ "but ignored");
+ } else if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaretOrError.inspect().IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ }
+ continue;
+ }
+ }
+
+ if (!indentedParentElement) {
+ return SplitRangeOffFromNodeResult(leftContentOfLastOutdented,
+ middleContentOfLastOutdented,
+ rightContentOfLastOutdented);
+ }
+
+ // We have a <blockquote> we haven't finished handling.
+ Result<SplitRangeOffFromNodeResult, nsresult> outdentResult =
+ OutdentPartOfBlock(*indentedParentElement, *firstContentToBeOutdented,
+ *lastContentToBeOutdented, indentedParentIndentedWith,
+ aEditingHost);
+ if (MOZ_UNLIKELY(outdentResult.isErr())) {
+ NS_WARNING("HTMLEditor::OutdentPartOfBlock() failed");
+ return outdentResult;
+ }
+ // We will restore selection soon. Therefore, callers do not need to restore
+ // the selection.
+ SplitRangeOffFromNodeResult unwrappedOutdentResult = outdentResult.unwrap();
+ unwrappedOutdentResult.ForgetCaretPointSuggestion();
+ return unwrappedOutdentResult;
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult>
+HTMLEditor::RemoveBlockContainerElementWithTransactionBetween(
+ Element& aBlockContainerElement, nsIContent& aStartOfRange,
+ nsIContent& aEndOfRange, BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ EditorDOMPoint pointToPutCaret;
+ Result<SplitRangeOffFromNodeResult, nsresult> splitResult =
+ SplitRangeOffFromElement(aBlockContainerElement, aStartOfRange,
+ aEndOfRange);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ if (splitResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::SplitRangeOffFromElement() failed");
+ return splitResult;
+ }
+ NS_WARNING(
+ "HTMLEditor::SplitRangeOffFromElement() failed, but might be ignored");
+ return SplitRangeOffFromNodeResult(nullptr, nullptr, nullptr);
+ }
+ SplitRangeOffFromNodeResult unwrappedSplitResult = splitResult.unwrap();
+ unwrappedSplitResult.MoveCaretPointTo(pointToPutCaret,
+ {SuggestCaret::OnlyIfHasSuggestion});
+
+ // Even if either split aBlockContainerElement or did not split it, we should
+ // unwrap the right most element which is split from aBlockContainerElement
+ // (or aBlockContainerElement itself if it was not split without errors).
+ Element* rightmostElement =
+ unwrappedSplitResult.GetRightmostContentAs<Element>();
+ MOZ_ASSERT(rightmostElement);
+ if (NS_WARN_IF(!rightmostElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ {
+ // MOZ_KnownLive(rightmostElement) because it's grabbed by
+ // unwrappedSplitResult.
+ Result<EditorDOMPoint, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerWithTransaction(MOZ_KnownLive(*rightmostElement));
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ if (unwrapBlockElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapBlockElementResult.unwrap();
+ }
+ }
+
+ return SplitRangeOffFromNodeResult(
+ unwrappedSplitResult.GetLeftContent(), nullptr,
+ unwrappedSplitResult.GetRightContent(), std::move(pointToPutCaret));
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult>
+HTMLEditor::SplitRangeOffFromElement(Element& aElementToSplit,
+ nsIContent& aStartOfMiddleElement,
+ nsIContent& aEndOfMiddleElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // aStartOfMiddleElement and aEndOfMiddleElement must be exclusive
+ // descendants of aElementToSplit.
+ MOZ_ASSERT(
+ EditorUtils::IsDescendantOf(aStartOfMiddleElement, aElementToSplit));
+ MOZ_ASSERT(EditorUtils::IsDescendantOf(aEndOfMiddleElement, aElementToSplit));
+
+ EditorDOMPoint pointToPutCaret;
+ // Split at the start.
+ Result<SplitNodeResult, nsresult> splitAtStartResult =
+ SplitNodeDeepWithTransaction(aElementToSplit,
+ EditorDOMPoint(&aStartOfMiddleElement),
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitAtStartResult.isErr())) {
+ if (splitAtStartResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed (at left)");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) at start of middle element failed");
+ } else {
+ splitAtStartResult.inspect().CopyCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ // Split at after the end
+ auto atAfterEnd = EditorDOMPoint::After(aEndOfMiddleElement);
+ Element* rightElement =
+ splitAtStartResult.isOk() && splitAtStartResult.inspect().DidSplit()
+ ? splitAtStartResult.inspect().GetNextContentAs<Element>()
+ : &aElementToSplit;
+ // MOZ_KnownLive(rightElement) because it's grabbed by splitAtStartResult or
+ // aElementToSplit whose lifetime is guaranteed by the caller.
+ Result<SplitNodeResult, nsresult> splitAtEndResult =
+ SplitNodeDeepWithTransaction(MOZ_KnownLive(*rightElement), atAfterEnd,
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
+ if (splitAtEndResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction() failed (at right)");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) after end of middle element failed");
+ } else {
+ splitAtEndResult.inspect().CopyCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+
+ if (splitAtStartResult.isOk() && splitAtStartResult.inspect().DidSplit() &&
+ splitAtEndResult.isOk() && splitAtEndResult.inspect().DidSplit()) {
+ // Note that the middle node can be computed only with the latter split
+ // result.
+ return SplitRangeOffFromNodeResult(
+ splitAtStartResult.inspect().GetPreviousContent(),
+ splitAtEndResult.inspect().GetPreviousContent(),
+ splitAtEndResult.inspect().GetNextContent(),
+ std::move(pointToPutCaret));
+ }
+ if (splitAtStartResult.isOk() && splitAtStartResult.inspect().DidSplit()) {
+ return SplitRangeOffFromNodeResult(
+ splitAtStartResult.inspect().GetPreviousContent(),
+ splitAtStartResult.inspect().GetNextContent(), nullptr,
+ std::move(pointToPutCaret));
+ }
+ if (splitAtEndResult.isOk() && splitAtEndResult.inspect().DidSplit()) {
+ return SplitRangeOffFromNodeResult(
+ nullptr, splitAtEndResult.inspect().GetPreviousContent(),
+ splitAtEndResult.inspect().GetNextContent(),
+ std::move(pointToPutCaret));
+ }
+ return SplitRangeOffFromNodeResult(nullptr, &aElementToSplit, nullptr,
+ std::move(pointToPutCaret));
+}
+
+Result<SplitRangeOffFromNodeResult, nsresult> HTMLEditor::OutdentPartOfBlock(
+ Element& aBlockElement, nsIContent& aStartOfOutdent,
+ nsIContent& aEndOfOutdent, BlockIndentedWith aBlockIndentedWith,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Result<SplitRangeOffFromNodeResult, nsresult> splitResult =
+ SplitRangeOffFromElement(aBlockElement, aStartOfOutdent, aEndOfOutdent);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitRangeOffFromElement() failed");
+ return splitResult;
+ }
+
+ SplitRangeOffFromNodeResult unwrappedSplitResult = splitResult.unwrap();
+ Element* middleElement = unwrappedSplitResult.GetMiddleContentAs<Element>();
+ if (MOZ_UNLIKELY(!middleElement)) {
+ NS_WARNING(
+ "HTMLEditor::SplitRangeOffFromElement() didn't return middle content");
+ unwrappedSplitResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*middleElement))) {
+ unwrappedSplitResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ nsresult rv = unwrappedSplitResult.SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitRangeOffFromNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "SplitRangeOffFromNodeResult::SuggestCaretPointTo() "
+ "failed, but ignored");
+
+ if (aBlockIndentedWith == BlockIndentedWith::HTML) {
+ // MOZ_KnownLive(middleElement) because of grabbed by unwrappedSplitResult.
+ Result<EditorDOMPoint, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerWithTransaction(MOZ_KnownLive(*middleElement));
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ const EditorDOMPoint& pointToPutCaret = unwrapBlockElementResult.inspect();
+ if (AllowsTransactionsToChangeSelection() && pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ return SplitRangeOffFromNodeResult(unwrappedSplitResult.GetLeftContent(),
+ nullptr,
+ unwrappedSplitResult.GetRightContent());
+ }
+
+ // MOZ_KnownLive(middleElement) because of grabbed by unwrappedSplitResult.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError = ChangeMarginStart(
+ MOZ_KnownLive(*middleElement), ChangeMargin::Decrease, aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::ChangeMarginStart(ChangeMargin::Decrease) failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaretOrError.inspect().IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return Err(rv);
+ }
+ }
+ return unwrappedSplitResult;
+}
+
+Result<CreateElementResult, nsresult> HTMLEditor::ChangeListElementType(
+ Element& aListElement, nsAtom& aNewListTag, nsAtom& aNewListItemTag) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ EditorDOMPoint pointToPutCaret;
+
+ AutoTArray<OwningNonNull<nsIContent>, 32> listElementChildren;
+ HTMLEditUtils::CollectAllChildren(aListElement, listElementChildren);
+
+ for (const OwningNonNull<nsIContent>& childContent : listElementChildren) {
+ if (!childContent->IsElement()) {
+ continue;
+ }
+ Element* childElement = childContent->AsElement();
+ if (HTMLEditUtils::IsListItem(childElement) &&
+ !childContent->IsHTMLElement(&aNewListItemTag)) {
+ // MOZ_KnownLive(childElement) because its lifetime is guaranteed by
+ // listElementChildren.
+ Result<CreateElementResult, nsresult>
+ replaceWithNewListItemElementResult =
+ ReplaceContainerAndCloneAttributesWithTransaction(
+ MOZ_KnownLive(*childElement), aNewListItemTag);
+ if (MOZ_UNLIKELY(replaceWithNewListItemElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction() "
+ "failed");
+ return replaceWithNewListItemElementResult;
+ }
+ CreateElementResult unwrappedReplaceWithNewListItemElementResult =
+ replaceWithNewListItemElementResult.unwrap();
+ unwrappedReplaceWithNewListItemElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ continue;
+ }
+ if (HTMLEditUtils::IsAnyListElement(childElement) &&
+ !childElement->IsHTMLElement(&aNewListTag)) {
+ // XXX List elements shouldn't have other list elements as their
+ // child. Why do we handle such invalid tree?
+ // -> Maybe, for bug 525888.
+ // MOZ_KnownLive(childElement) because its lifetime is guaranteed by
+ // listElementChildren.
+ Result<CreateElementResult, nsresult> convertListTypeResult =
+ ChangeListElementType(MOZ_KnownLive(*childElement), aNewListTag,
+ aNewListItemTag);
+ if (MOZ_UNLIKELY(convertListTypeResult.isErr())) {
+ NS_WARNING("HTMLEditor::ChangeListElementType() failed");
+ return convertListTypeResult;
+ }
+ CreateElementResult unwrappedConvertListTypeResult =
+ convertListTypeResult.unwrap();
+ unwrappedConvertListTypeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ continue;
+ }
+ }
+
+ if (aListElement.IsHTMLElement(&aNewListTag)) {
+ return CreateElementResult(&aListElement, std::move(pointToPutCaret));
+ }
+
+ // XXX If we replace the list element, shouldn't we create it first and then,
+ // move children into it before inserting the new list element into the
+ // DOM tree? Then, we could reduce the cost of dispatching DOM mutation
+ // events.
+ Result<CreateElementResult, nsresult> replaceWithNewListElementResult =
+ ReplaceContainerWithTransaction(aListElement, aNewListTag);
+ if (MOZ_UNLIKELY(replaceWithNewListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::ReplaceContainerWithTransaction() failed");
+ return replaceWithNewListElementResult;
+ }
+ CreateElementResult unwrappedReplaceWithNewListElementResult =
+ replaceWithNewListElementResult.unwrap();
+ unwrappedReplaceWithNewListElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ return CreateElementResult(
+ unwrappedReplaceWithNewListElementResult.UnwrapNewNode(),
+ std::move(pointToPutCaret));
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::CreateStyleForInsertText(
+ const EditorDOMPoint& aPointToInsertText, const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPointToInsertText.IsSetAndValid());
+ MOZ_ASSERT(mPendingStylesToApplyToNewContent);
+
+ const RefPtr<Element> documentRootElement = GetDocument()->GetRootElement();
+ if (NS_WARN_IF(!documentRootElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // process clearing any styles first
+ UniquePtr<PendingStyle> pendingStyle =
+ mPendingStylesToApplyToNewContent->TakeClearingStyle();
+
+ EditorDOMPoint pointToPutCaret(aPointToInsertText);
+ {
+ // Transactions may set selection, but we will set selection if necessary.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ while (pendingStyle &&
+ pointToPutCaret.GetContainer() != documentRootElement) {
+ // MOZ_KnownLive because we own pendingStyle which guarantees the lifetime
+ // of its members.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ ClearStyleAt(pointToPutCaret, pendingStyle->ToInlineStyle(),
+ pendingStyle->GetSpecifiedStyle());
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::ClearStyleAt() failed");
+ return pointToPutCaretOrError;
+ }
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ if (NS_WARN_IF(!pointToPutCaret.IsSetAndValid())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ pendingStyle = mPendingStylesToApplyToNewContent->TakeClearingStyle();
+ }
+ }
+
+ // then process setting any styles
+ const int32_t relFontSize =
+ mPendingStylesToApplyToNewContent->TakeRelativeFontSize();
+ AutoTArray<EditorInlineStyleAndValue, 32> stylesToSet;
+ mPendingStylesToApplyToNewContent->TakeAllPreservedStyles(stylesToSet);
+ if (stylesToSet.IsEmpty() && !relFontSize) {
+ return pointToPutCaret;
+ }
+
+ // We're in chrome, e.g., the email composer of Thunderbird, and there is
+ // relative font size changes, we need to keep using legacy path until we port
+ // IncrementOrDecrementFontSizeAsSubAction() to work with
+ // AutoInlineStyleSetter.
+ if (relFontSize) {
+ // we have at least one style to add; make a new text node to insert style
+ // nodes above.
+ EditorDOMPoint pointToInsertTextNode(pointToPutCaret);
+ if (pointToInsertTextNode.IsInTextNode()) {
+ // if we are in a text node, split it
+ Result<SplitNodeResult, nsresult> splitTextNodeResult =
+ SplitNodeDeepWithTransaction(
+ MOZ_KnownLive(*pointToInsertTextNode.ContainerAs<Text>()),
+ pointToInsertTextNode,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eAllowToCreateEmptyContainer) failed");
+ return splitTextNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitTextNodeResult =
+ splitTextNodeResult.unwrap();
+ unwrappedSplitTextNodeResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ pointToInsertTextNode =
+ unwrappedSplitTextNodeResult.AtSplitPoint<EditorDOMPoint>();
+ }
+ if (!pointToInsertTextNode.IsInContentNode() ||
+ !HTMLEditUtils::IsContainerNode(
+ *pointToInsertTextNode.ContainerAs<nsIContent>())) {
+ return pointToPutCaret;
+ }
+ RefPtr<Text> newEmptyTextNode = CreateTextNode(u""_ns);
+ if (!newEmptyTextNode) {
+ NS_WARNING("EditorBase::CreateTextNode() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<CreateTextResult, nsresult> insertNewTextNodeResult =
+ InsertNodeWithTransaction<Text>(*newEmptyTextNode,
+ pointToInsertTextNode);
+ if (MOZ_UNLIKELY(insertNewTextNodeResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return insertNewTextNodeResult.propagateErr();
+ }
+ insertNewTextNodeResult.inspect().IgnoreCaretPointSuggestion();
+ pointToPutCaret.Set(newEmptyTextNode, 0u);
+
+ // FIXME: If the stylesToSet have background-color style, it may
+ // be applied shorter because outer <span> element height is not
+ // computed with inner element's height.
+ HTMLEditor::FontSize incrementOrDecrement =
+ relFontSize > 0 ? HTMLEditor::FontSize::incr
+ : HTMLEditor::FontSize::decr;
+ for ([[maybe_unused]] uint32_t j : IntegerRange(Abs(relFontSize))) {
+ Result<CreateElementResult, nsresult> wrapTextInBigOrSmallElementResult =
+ SetFontSizeOnTextNode(*newEmptyTextNode, 0, UINT32_MAX,
+ incrementOrDecrement);
+ if (MOZ_UNLIKELY(wrapTextInBigOrSmallElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed");
+ return wrapTextInBigOrSmallElementResult.propagateErr();
+ }
+ // We don't need to update here because we'll suggest caret position
+ // which is computed above.
+ MOZ_ASSERT(pointToPutCaret.IsSet());
+ wrapTextInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ for (const EditorInlineStyleAndValue& styleToSet : stylesToSet) {
+ AutoInlineStyleSetter inlineStyleSetter(styleToSet);
+ // MOZ_KnownLive(...ContainerAs<nsIContent>()) because pointToPutCaret
+ // grabs the result.
+ Result<CaretPoint, nsresult> setStyleResult =
+ inlineStyleSetter.ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
+ *this, MOZ_KnownLive(*pointToPutCaret.ContainerAs<nsIContent>()));
+ if (MOZ_UNLIKELY(setStyleResult.isErr())) {
+ NS_WARNING("HTMLEditor::SetInlinePropertyOnNode() failed");
+ return setStyleResult.propagateErr();
+ }
+ // We don't need to update here because we'll suggest caret position which
+ // is computed above.
+ MOZ_ASSERT(pointToPutCaret.IsSet());
+ setStyleResult.unwrap().IgnoreCaretPointSuggestion();
+ }
+ return pointToPutCaret;
+ }
+
+ // If we have preserved commands except relative font style changes, we can
+ // use inline style setting code which reuse ancestors better.
+ AutoRangeArray ranges(pointToPutCaret);
+ if (MOZ_UNLIKELY(ranges.Ranges().IsEmpty())) {
+ NS_WARNING("AutoRangeArray::AutoRangeArray() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsresult rv =
+ SetInlinePropertiesAroundRanges(ranges, stylesToSet, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetInlinePropertiesAroundRanges() failed");
+ return Err(rv);
+ }
+ if (NS_WARN_IF(ranges.Ranges().IsEmpty())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // Now `ranges` selects new styled contents and the range may not be
+ // collapsed. We should use the deepest editable start point of the range
+ // to insert text.
+ nsINode* container = ranges.FirstRangeRef()->GetStartContainer();
+ if (MOZ_UNLIKELY(!container->IsContent())) {
+ container = ranges.FirstRangeRef()->GetChildAtStartOffset();
+ if (MOZ_UNLIKELY(!container)) {
+ NS_WARNING("How did we get lost insertion point?");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ pointToPutCaret =
+ HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
+ *container->AsContent());
+ if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return pointToPutCaret;
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AlignAsSubAction(
+ const nsAString& aAlignType, const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSetOrClearAlignment, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Some selection containers are not content node, but ignored");
+ return EditActionResult::IgnoredResult();
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ AutoRangeArray selectionRanges(SelectionRef());
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!selectionRanges.IsCollapsed() &&
+ selectionRanges.Ranges().Length() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ selectionRanges.FirstRangeRef(), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use etStartAndEnd() here.
+ nsresult rv = selectionRanges.SetBaseAndExtent(
+ extendedRange.inspect().StartRef(), extendedRange.inspect().EndRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return Err(rv);
+ }
+ }
+
+ rv = AlignContentsAtRanges(selectionRanges, aAlignType, aEditingHost);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::AlignContentsAtSelection() failed");
+ return Err(rv);
+ }
+ rv = selectionRanges.ApplyTo(SelectionRef());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::ApplyTo() failed");
+ return Err(rv);
+ }
+
+ if (MOZ_UNLIKELY(IsSelectionRangeContainerNotContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() "
+ "failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::AlignContentsAtRanges(AutoRangeArray& aRanges,
+ const nsAString& aAlignType,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(aRanges.IsInContent());
+
+ if (NS_WARN_IF(!aRanges.SaveAndTrackRanges(*this))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ EditorDOMPoint pointToPutCaret;
+
+ // Convert the selection ranges into "promoted" selection ranges: This
+ // basically just expands the range to include the immediate block parent,
+ // and then further expands to include any ancestors whose children are all
+ // in the range
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedRanges(aRanges);
+ extendedRanges.ExtendRangesToWrapLines(
+ EditSubAction::eSetOrClearAlignment,
+ BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ nsresult rv = extendedRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eSetOrClearAlignment,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eSetOrClearAlignment, CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eSetOrClearAlignment);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eSetOrClearAlignment) failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+
+ // If we don't have any nodes, or we have only a single br, then we are
+ // creating an empty alignment div. We have to do some different things for
+ // these.
+ bool createEmptyDivElement = arrayOfContents.IsEmpty();
+ if (arrayOfContents.Length() == 1) {
+ OwningNonNull<nsIContent>& content = arrayOfContents[0];
+
+ if (HTMLEditUtils::SupportsAlignAttr(content) &&
+ HTMLEditUtils::IsBlockElement(content,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ // The node is a table element, an hr, a paragraph, a div or a section
+ // header; in HTML 4, it can directly carry the ALIGN attribute and we
+ // don't need to make a div! If we are in CSS mode, all the work is done
+ // in SetBlockElementAlign().
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ SetBlockElementAlign(MOZ_KnownLive(*content->AsElement()), aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::SetBlockElementAlign() failed");
+ return pointToPutCaretOrError.unwrapErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+
+ if (content->IsHTMLElement(nsGkAtoms::br)) {
+ // The special case createEmptyDivElement code (below) that consumes
+ // `<br>` elements can cause tables to split if the start node of the
+ // selection is not in a table cell or caption, for example parent is a
+ // `<tr>`. Avoid this unnecessary splitting if possible by leaving
+ // createEmptyDivElement false so that we fall through to the normal case
+ // alignment code.
+ //
+ // XXX: It seems a little error prone for the createEmptyDivElement
+ // special case code to assume that the start node of the selection
+ // is the parent of the single node in the arrayOfContents, as the
+ // paragraph above points out. Do we rely on the selection start
+ // node because of the fact that arrayOfContents can be empty? We
+ // should probably revisit this issue. - kin
+
+ const EditorDOMPoint firstRangeStartPoint =
+ pointToPutCaret.IsSet()
+ ? pointToPutCaret
+ : aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!firstRangeStartPoint.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ nsINode* parent = firstRangeStartPoint.GetContainer();
+ createEmptyDivElement = !HTMLEditUtils::IsAnyTableElement(parent) ||
+ HTMLEditUtils::IsTableCellOrCaption(*parent);
+ }
+ }
+
+ if (createEmptyDivElement) {
+ if (MOZ_UNLIKELY(!pointToPutCaret.IsSet() && !aRanges.IsInContent())) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
+ }
+ const EditorDOMPoint pointToInsertDivElement =
+ pointToPutCaret.IsSet()
+ ? pointToPutCaret
+ : aRanges.GetFirstRangeStartPoint<EditorDOMPoint>();
+ Result<CreateElementResult, nsresult> insertNewDivElementResult =
+ InsertDivElementToAlignContents(pointToInsertDivElement, aAlignType,
+ aEditingHost);
+ if (insertNewDivElementResult.isErr()) {
+ NS_WARNING("HTMLEditor::InsertDivElementToAlignContents() failed");
+ return insertNewDivElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInsertNewDivElementResult =
+ insertNewDivElementResult.unwrap();
+ aRanges.ClearSavedRanges();
+ EditorDOMPoint pointToPutCaret =
+ unwrappedInsertNewDivElementResult.UnwrapCaretPoint();
+ nsresult rv = aRanges.Collapse(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+
+ Result<CreateElementResult, nsresult> maybeCreateDivElementResult =
+ AlignNodesAndDescendants(arrayOfContents, aAlignType, aEditingHost);
+ if (MOZ_UNLIKELY(maybeCreateDivElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::AlignNodesAndDescendants() failed");
+ return maybeCreateDivElementResult.unwrapErr();
+ }
+ maybeCreateDivElementResult.inspect().IgnoreCaretPointSuggestion();
+
+ MOZ_ASSERT(aRanges.HasSavedRanges());
+ aRanges.RestoreFromSavedRanges();
+ // If restored range is collapsed outside the latest cased <div> element,
+ // we should move caret into the <div>.
+ if (maybeCreateDivElementResult.inspect().GetNewNode() &&
+ aRanges.IsCollapsed() && !aRanges.Ranges().IsEmpty()) {
+ const auto firstRangeStartRawPoint =
+ aRanges.GetFirstRangeStartPoint<EditorRawDOMPoint>();
+ if (MOZ_LIKELY(firstRangeStartRawPoint.IsSet())) {
+ Result<EditorRawDOMPoint, nsresult> pointInNewDivOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(
+ *maybeCreateDivElementResult.inspect().GetNewNode(),
+ firstRangeStartRawPoint);
+ if (MOZ_UNLIKELY(pointInNewDivOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() failed, "
+ "but ignored");
+ } else if (pointInNewDivOrError.inspect().IsSet()) {
+ nsresult rv = aRanges.Collapse(pointInNewDivOrError.unwrap());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("AutoRangeArray::Collapse() failed");
+ return rv;
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::InsertDivElementToAlignContents(
+ const EditorDOMPoint& aPointToInsert, const nsAString& aAlignType,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ if (NS_WARN_IF(!aPointToInsert.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, aPointToInsert, BRElementNextToSplitPoint::Delete,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div, BRElementNextToSplitPoint::Delete) failed");
+ return createNewDivElementResult;
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ // We'll suggest start of the new <div>, so we don't need the suggested
+ // position.
+ unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion();
+
+ MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode());
+ RefPtr<Element> newDivElement =
+ unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ // Set up the alignment on the div, using HTML or CSS
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ SetBlockElementAlign(*newDivElement, aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SetBlockElementAlign(EditTarget::"
+ "OnlyDescendantsExceptTable) failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ // We don't need the new suggested position too.
+
+ // Put in a padding <br> element for empty last line so that it won't get
+ // deleted.
+ {
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint(newDivElement, 0u));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
+ "failed");
+ return insertPaddingBRElementResult;
+ }
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ return CreateElementResult(std::move(newDivElement),
+ EditorDOMPoint(newDivElement, 0u));
+}
+
+Result<CreateElementResult, nsresult> HTMLEditor::AlignNodesAndDescendants(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ const nsAString& aAlignType, const Element& aEditingHost) {
+ // Detect all the transitions in the array, where a transition means that
+ // adjacent nodes in the array don't have the same parent.
+ AutoTArray<bool, 64> transitionList;
+ HTMLEditor::MakeTransitionList(aArrayOfContents, transitionList);
+
+ RefPtr<Element> latestCreatedDivElement;
+ EditorDOMPoint pointToPutCaret;
+
+ // Okay, now go through all the nodes and give them an align attrib or put
+ // them in a div, or whatever is appropriate. Woohoo!
+
+ RefPtr<Element> createdDivElement;
+ const bool useCSS = IsCSSEnabled();
+ int32_t indexOfTransitionList = -1;
+ for (OwningNonNull<nsIContent>& content : aArrayOfContents) {
+ ++indexOfTransitionList;
+
+ // Ignore all non-editable nodes. Leave them be.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+
+ // The node is a table element, an hr, a paragraph, a div or a section
+ // header; in HTML 4, it can directly carry the ALIGN attribute and we
+ // don't need to nest it, just set the alignment. In CSS, assign the
+ // corresponding CSS styles in SetBlockElementAlign().
+ if (HTMLEditUtils::SupportsAlignAttr(content)) {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ SetBlockElementAlign(MOZ_KnownLive(*content->AsElement()), aAlignType,
+ EditTarget::NodeAndDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SetBlockElementAlign(EditTarget::"
+ "NodeAndDescendantsExceptTable) failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ // Clear out createdDivElement so that we don't put nodes after this one
+ // into it
+ createdDivElement = nullptr;
+ continue;
+ }
+
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ continue;
+ }
+
+ // Skip insignificant formatting text nodes to prevent unnecessary
+ // structure splitting!
+ if (content->IsText() &&
+ ((HTMLEditUtils::IsAnyTableElement(atContent.GetContainer()) &&
+ !HTMLEditUtils::IsTableCellOrCaption(*atContent.GetContainer())) ||
+ HTMLEditUtils::IsAnyListElement(atContent.GetContainer()) ||
+ HTMLEditUtils::IsEmptyNode(
+ *content,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible}))) {
+ continue;
+ }
+
+ // If it's a list item, or a list inside a list, forget any "current" div,
+ // and instead put divs inside the appropriate block (td, li, etc.)
+ if (HTMLEditUtils::IsListItem(content) ||
+ HTMLEditUtils::IsAnyListElement(content)) {
+ Element* listOrListItemElement = content->AsElement();
+ {
+ AutoEditorDOMPointOffsetInvalidator lockChild(atContent);
+ // MOZ_KnownLive(*listOrListItemElement): An element of aArrayOfContents
+ // which is array of OwningNonNull.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ RemoveAlignFromDescendants(MOZ_KnownLive(*listOrListItemElement),
+ aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveAlignFromDescendants(EditTarget::"
+ "OnlyDescendantsExceptTable) failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ if (NS_WARN_IF(!atContent.IsSetAndValid())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (useCSS) {
+ nsStyledElement* styledListOrListItemElement =
+ nsStyledElement::FromNode(listOrListItemElement);
+ if (styledListOrListItemElement &&
+ EditorElementStyle::Align().IsCSSSettable(
+ *styledListOrListItemElement)) {
+ // MOZ_KnownLive(*styledListOrListItemElement): An element of
+ // aArrayOfContents which is array of OwningNonNull.
+ Result<size_t, nsresult> result =
+ CSSEditUtils::SetCSSEquivalentToStyle(
+ WithTransaction::Yes, *this,
+ MOZ_KnownLive(*styledListOrListItemElement),
+ EditorElementStyle::Align(), &aAlignType);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
+ return result.propagateErr();
+ }
+ NS_WARNING(
+ "CSSEditUtils::SetCSSEquivalentToStyle(EditorElementStyle::"
+ "Align()) failed, but ignored");
+ }
+ }
+ createdDivElement = nullptr;
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ // If we don't use CSS, add a content to list element: they have to
+ // be inside another list, i.e., >= second level of nesting.
+ // XXX AlignContentsInAllTableCellsAndListItems() handles only list
+ // item elements and table cells. Is it intentional? Why don't
+ // we need to align contents in other type blocks?
+ // MOZ_KnownLive(*listOrListItemElement): An element of aArrayOfContents
+ // which is array of OwningNonNull.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ AlignContentsInAllTableCellsAndListItems(
+ MOZ_KnownLive(*listOrListItemElement), aAlignType);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::AlignContentsInAllTableCellsAndListItems() failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ createdDivElement = nullptr;
+ continue;
+ }
+
+ // Clear out createdDivElement so that we don't put nodes after this one
+ // into it
+ }
+
+ // Need to make a div to put things in if we haven't already, or if this
+ // node doesn't go in div we used earlier.
+ if (!createdDivElement || transitionList[indexOfTransitionList]) {
+ // First, check that our element can contain a div.
+ if (!HTMLEditUtils::CanNodeContain(*atContent.GetContainer(),
+ *nsGkAtoms::div)) {
+ // XXX Why do we return "OK" here rather than returning error or
+ // doing continue?
+ return latestCreatedDivElement
+ ? CreateElementResult(std::move(latestCreatedDivElement),
+ std::move(pointToPutCaret))
+ : CreateElementResult::NotHandled(
+ std::move(pointToPutCaret));
+ }
+
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult;
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ if (unwrappedCreateNewDivElementResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedCreateNewDivElementResult.UnwrapCaretPoint();
+ }
+
+ MOZ_ASSERT(unwrappedCreateNewDivElementResult.GetNewNode());
+ createdDivElement = unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ // Set up the alignment on the div
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ SetBlockElementAlign(*createdDivElement, aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ if (NS_WARN_IF(pointToPutCaretOrError.inspectErr() ==
+ NS_ERROR_EDITOR_DESTROYED)) {
+ return pointToPutCaretOrError.propagateErr();
+ }
+ NS_WARNING(
+ "HTMLEditor::SetBlockElementAlign(EditTarget::"
+ "OnlyDescendantsExceptTable) failed, but ignored");
+ } else if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ latestCreatedDivElement = createdDivElement;
+ }
+
+ // Tuck the node into the end of the active div
+ //
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
+ *createdDivElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ }
+
+ return latestCreatedDivElement
+ ? CreateElementResult(std::move(latestCreatedDivElement),
+ std::move(pointToPutCaret))
+ : CreateElementResult::NotHandled(std::move(pointToPutCaret));
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::AlignContentsInAllTableCellsAndListItems(
+ Element& aElement, const nsAString& aAlignType) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // Gather list of table cells or list items
+ AutoTArray<OwningNonNull<Element>, 64> arrayOfTableCellsAndListItems;
+ DOMIterator iter(aElement);
+ iter.AppendNodesToArray(
+ +[](nsINode& aNode, void*) -> bool {
+ MOZ_ASSERT(Element::FromNode(&aNode));
+ return HTMLEditUtils::IsTableCell(&aNode) ||
+ HTMLEditUtils::IsListItem(&aNode);
+ },
+ arrayOfTableCellsAndListItems);
+
+ // Now that we have the list, align their contents as requested
+ EditorDOMPoint pointToPutCaret;
+ for (auto& tableCellOrListItemElement : arrayOfTableCellsAndListItems) {
+ // MOZ_KnownLive because 'arrayOfTableCellsAndListItems' is guaranteed to
+ // keep it alive.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ AlignBlockContentsWithDivElement(
+ MOZ_KnownLive(tableCellOrListItemElement), aAlignType);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::AlignBlockContentsWithDivElement() failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::AlignBlockContentsWithDivElement(
+ Element& aBlockElement, const nsAString& aAlignType) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // XXX I don't understand why we should NOT align non-editable children
+ // with modifying EDITABLE `<div>` element.
+ nsCOMPtr<nsIContent> firstEditableContent = HTMLEditUtils::GetFirstChild(
+ aBlockElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!firstEditableContent) {
+ // This block has no editable content, nothing to align.
+ return EditorDOMPoint();
+ }
+
+ // If there is only one editable content and it's a `<div>` element,
+ // just set `align` attribute of it.
+ nsCOMPtr<nsIContent> lastEditableContent = HTMLEditUtils::GetLastChild(
+ aBlockElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (firstEditableContent == lastEditableContent &&
+ firstEditableContent->IsHTMLElement(nsGkAtoms::div)) {
+ nsresult rv = SetAttributeOrEquivalent(
+ MOZ_KnownLive(firstEditableContent->AsElement()), nsGkAtoms::align,
+ aAlignType, false);
+ if (NS_WARN_IF(Destroyed())) {
+ NS_WARNING(
+ "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align) caused "
+ "destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align) failed");
+ return Err(rv);
+ }
+ return EditorDOMPoint();
+ }
+
+ // Otherwise, we need to insert a `<div>` element to set `align` attribute.
+ // XXX Don't insert the new `<div>` element until we set `align` attribute
+ // for avoiding running mutation event listeners.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ CreateAndInsertElement(
+ WithTransaction::Yes, *nsGkAtoms::div,
+ EditorDOMPoint(&aBlockElement, 0u),
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
+ [&aAlignType](HTMLEditor& aHTMLEditor, Element& aDivElement,
+ const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ MOZ_ASSERT(!aDivElement.IsInComposedDoc());
+ // If aDivElement has not been connected yet, we do not need
+ // transaction of setting align attribute here.
+ nsresult rv = aHTMLEditor.SetAttributeOrEquivalent(
+ &aDivElement, nsGkAtoms::align, aAlignType, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeOrEquivalent("
+ "nsGkAtoms::align, \"...\", false) failed");
+ return rv;
+ });
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes, "
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ EditorDOMPoint pointToPutCaret =
+ unwrappedCreateNewDivElementResult.UnwrapCaretPoint();
+ RefPtr<Element> newDivElement =
+ unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newDivElement);
+ // XXX This is tricky and does not work with mutation event listeners.
+ // But I'm not sure what we should do if new content is inserted.
+ // Anyway, I don't think that we should move editable contents
+ // over non-editable contents. Chrome does no do that.
+ while (lastEditableContent && (lastEditableContent != newDivElement)) {
+ Result<MoveNodeResult, nsresult> moveNodeResult = MoveNodeWithTransaction(
+ *lastEditableContent, EditorDOMPoint(newDivElement, 0u));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ if (unwrappedMoveNodeResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedMoveNodeResult.UnwrapCaretPoint();
+ }
+ lastEditableContent = HTMLEditUtils::GetLastChild(
+ aBlockElement, {WalkTreeOption::IgnoreNonEditableNode});
+ }
+ return pointToPutCaret;
+}
+
+Result<EditorRawDOMRange, nsresult>
+HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ const nsRange* aRange, const Element& aEditingHost) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // This tweaks selections to be more "natural".
+ // Idea here is to adjust edges of selection ranges so that they do not cross
+ // breaks or block boundaries unless something editable beyond that boundary
+ // is also selected. This adjustment makes it much easier for the various
+ // block operations to determine what nodes to act on.
+ if (NS_WARN_IF(!aRange) || NS_WARN_IF(!aRange->IsPositioned())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ const EditorRawDOMPoint startPoint(aRange->StartRef());
+ if (NS_WARN_IF(!startPoint.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const EditorRawDOMPoint endPoint(aRange->EndRef());
+ if (NS_WARN_IF(!endPoint.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // adjusted values default to original values
+ EditorRawDOMRange newRange(startPoint, endPoint);
+
+ // Is there any intervening visible white-space? If so we can't push
+ // selection past that, it would visibly change meaning of users selection.
+ WSRunScanner wsScannerAtEnd(
+ &aEditingHost, endPoint,
+ // We should refer only the default style of HTML because we need to wrap
+ // any elements with a specific HTML element. So we should not refer
+ // actual style. For example, we want to reformat parent HTML block
+ // element even if selected in a blocked phrase element or
+ // non-HTMLelement.
+ BlockInlineCheck::UseHTMLDefaultStyle);
+ WSScanResult scanResultAtEnd =
+ wsScannerAtEnd.ScanPreviousVisibleNodeOrBlockBoundaryFrom(endPoint);
+ if (scanResultAtEnd.Failed()) {
+ NS_WARNING(
+ "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (scanResultAtEnd.ReachedSomethingNonTextContent()) {
+ // eThisBlock and eOtherBlock conveniently distinguish cases
+ // of going "down" into a block and "up" out of a block.
+ if (wsScannerAtEnd.StartsFromOtherBlockElement()) {
+ // endpoint is just after the close of a block.
+ if (nsIContent* child = HTMLEditUtils::GetLastLeafContent(
+ *wsScannerAtEnd.StartReasonOtherBlockElementPtr(),
+ {LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ newRange.SetEnd(EditorRawDOMPoint::After(*child));
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsScannerAtEnd.StartsFromCurrentBlockBoundary()) {
+ // endpoint is just after start of this block
+ if (nsIContent* child = HTMLEditUtils::GetPreviousContent(
+ endPoint, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseHTMLDefaultStyle, &aEditingHost)) {
+ newRange.SetEnd(EditorRawDOMPoint::After(*child));
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsScannerAtEnd.StartsFromBRElement()) {
+ // endpoint is just after break. lets adjust it to before it.
+ newRange.SetEnd(
+ EditorRawDOMPoint(wsScannerAtEnd.StartReasonBRElementPtr()));
+ }
+ }
+
+ // Is there any intervening visible white-space? If so we can't push
+ // selection past that, it would visibly change meaning of users selection.
+ WSRunScanner wsScannerAtStart(&aEditingHost, startPoint,
+ BlockInlineCheck::UseHTMLDefaultStyle);
+ WSScanResult scanResultAtStart =
+ wsScannerAtStart.ScanNextVisibleNodeOrBlockBoundaryFrom(startPoint);
+ if (scanResultAtStart.Failed()) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (scanResultAtStart.ReachedSomethingNonTextContent()) {
+ // eThisBlock and eOtherBlock conveniently distinguish cases
+ // of going "down" into a block and "up" out of a block.
+ if (wsScannerAtStart.EndsByOtherBlockElement()) {
+ // startpoint is just before the start of a block.
+ if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *wsScannerAtStart.EndReasonOtherBlockElementPtr(),
+ {LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ newRange.SetStart(EditorRawDOMPoint(child));
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsScannerAtStart.EndsByCurrentBlockBoundary()) {
+ // startpoint is just before end of this block
+ if (nsIContent* child = HTMLEditUtils::GetNextContent(
+ startPoint, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseHTMLDefaultStyle, &aEditingHost)) {
+ newRange.SetStart(EditorRawDOMPoint(child));
+ }
+ // else block is empty - we can leave selection alone here, i think.
+ } else if (wsScannerAtStart.EndsByBRElement()) {
+ // startpoint is just before a break. lets adjust it to after it.
+ newRange.SetStart(
+ EditorRawDOMPoint::After(*wsScannerAtStart.EndReasonBRElementPtr()));
+ }
+ }
+
+ // There is a demented possibility we have to check for. We might have a very
+ // strange selection that is not collapsed and yet does not contain any
+ // editable content, and satisfies some of the above conditions that cause
+ // tweaking. In this case we don't want to tweak the selection into a block
+ // it was never in, etc. There are a variety of strategies one might use to
+ // try to detect these cases, but I think the most straightforward is to see
+ // if the adjusted locations "cross" the old values: i.e., new end before old
+ // start, or new start after old end. If so then just leave things alone.
+
+ Maybe<int32_t> comp = nsContentUtils::ComparePoints(
+ startPoint.ToRawRangeBoundary(), newRange.EndRef().ToRawRangeBoundary());
+
+ if (NS_WARN_IF(!comp)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (*comp == 1) {
+ return EditorRawDOMRange(); // New end before old start.
+ }
+
+ comp = nsContentUtils::ComparePoints(newRange.StartRef().ToRawRangeBoundary(),
+ endPoint.ToRawRangeBoundary());
+
+ if (NS_WARN_IF(!comp)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (*comp == 1) {
+ return EditorRawDOMRange(); // New start after old end.
+ }
+
+ return newRange;
+}
+
+template <typename EditorDOMRangeType>
+already_AddRefed<nsRange> HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMRangeType& aRange) {
+ MOZ_DIAGNOSTIC_ASSERT(aRange.IsPositioned());
+ return CreateRangeIncludingAdjuscentWhiteSpaces(aRange.StartRef(),
+ aRange.EndRef());
+}
+
+template <typename EditorDOMPointType1, typename EditorDOMPointType2>
+already_AddRefed<nsRange> HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
+ const EditorDOMPointType1& aStartPoint,
+ const EditorDOMPointType2& aEndPoint) {
+ MOZ_DIAGNOSTIC_ASSERT(!aStartPoint.IsInNativeAnonymousSubtree());
+ MOZ_DIAGNOSTIC_ASSERT(!aEndPoint.IsInNativeAnonymousSubtree());
+
+ if (!aStartPoint.IsInContentNode() || !aEndPoint.IsInContentNode()) {
+ NS_WARNING_ASSERTION(aStartPoint.IsSet(), "aStartPoint was not set");
+ NS_WARNING_ASSERTION(aEndPoint.IsSet(), "aEndPoint was not set");
+ return nullptr;
+ }
+
+ const Element* const editingHost = ComputeEditingHost();
+ if (NS_WARN_IF(!editingHost)) {
+ return nullptr;
+ }
+
+ EditorDOMPoint startPoint = aStartPoint.template To<EditorDOMPoint>();
+ EditorDOMPoint endPoint = aEndPoint.template To<EditorDOMPoint>();
+ AutoRangeArray::UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement(
+ startPoint, endPoint, *editingHost);
+
+ if (NS_WARN_IF(!startPoint.IsInContentNode()) ||
+ NS_WARN_IF(!endPoint.IsInContentNode())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement() "
+ "failed");
+ return nullptr;
+ }
+
+ // For text actions, we want to look backwards (or forwards, as
+ // appropriate) for additional white-space or nbsp's. We may have to act
+ // on these later even though they are outside of the initial selection.
+ // Even if they are in another node!
+ // XXX Those scanners do not treat siblings of the text nodes. Perhaps,
+ // we should use `WSRunScanner::GetFirstASCIIWhiteSpacePointCollapsedTo()`
+ // and `WSRunScanner::GetEndOfCollapsibleASCIIWhiteSpaces()` instead.
+ if (startPoint.IsInTextNode()) {
+ while (!startPoint.IsStartOfContainer()) {
+ if (!startPoint.IsPreviousCharASCIISpaceOrNBSP()) {
+ break;
+ }
+ MOZ_ALWAYS_TRUE(startPoint.RewindOffset());
+ }
+ }
+ if (!startPoint.GetChildOrContainerIfDataNode() ||
+ !startPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
+ editingHost)) {
+ return nullptr;
+ }
+ if (endPoint.IsInTextNode()) {
+ while (!endPoint.IsEndOfContainer()) {
+ if (!endPoint.IsCharASCIISpaceOrNBSP()) {
+ break;
+ }
+ MOZ_ALWAYS_TRUE(endPoint.AdvanceOffset());
+ }
+ }
+ EditorDOMPoint lastRawPoint(endPoint);
+ if (!lastRawPoint.IsStartOfContainer()) {
+ lastRawPoint.RewindOffset();
+ }
+ if (!lastRawPoint.GetChildOrContainerIfDataNode() ||
+ !lastRawPoint.GetChildOrContainerIfDataNode()->IsInclusiveDescendantOf(
+ editingHost)) {
+ return nullptr;
+ }
+
+ RefPtr<nsRange> range =
+ nsRange::Create(startPoint.ToRawRangeBoundary(),
+ endPoint.ToRawRangeBoundary(), IgnoreErrors());
+ NS_WARNING_ASSERTION(range, "nsRange::Create() failed");
+ return range.forget();
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::MaybeSplitElementsAtEveryBRElement(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ EditSubAction aEditSubAction) {
+ // Post-process the list to break up inline containers that contain br's, but
+ // only for operations that might care, like making lists or paragraphs
+ switch (aEditSubAction) {
+ case EditSubAction::eCreateOrRemoveBlock:
+ case EditSubAction::eFormatBlockForHTMLCommand:
+ case EditSubAction::eMergeBlockContents:
+ case EditSubAction::eCreateOrChangeList:
+ case EditSubAction::eSetOrClearAlignment:
+ case EditSubAction::eSetPositionToAbsolute:
+ case EditSubAction::eIndent:
+ case EditSubAction::eOutdent: {
+ EditorDOMPoint pointToPutCaret;
+ for (size_t index : Reversed(IntegerRange(aArrayOfContents.Length()))) {
+ OwningNonNull<nsIContent>& content = aArrayOfContents[index];
+ if (HTMLEditUtils::IsInlineContent(
+ content, BlockInlineCheck::UseHTMLDefaultStyle) &&
+ HTMLEditUtils::IsContainerNode(content) && !content->IsText()) {
+ AutoTArray<OwningNonNull<nsIContent>, 24> arrayOfInlineContents;
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
+ // alive.
+ Result<EditorDOMPoint, nsresult> splitResult =
+ SplitElementsAtEveryBRElement(MOZ_KnownLive(content),
+ arrayOfInlineContents);
+ if (splitResult.isErr()) {
+ NS_WARNING("HTMLEditor::SplitElementsAtEveryBRElement() failed");
+ return splitResult;
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ // Put these nodes in aArrayOfContents, replacing the current node
+ aArrayOfContents.RemoveElementAt(index);
+ aArrayOfContents.InsertElementsAt(index, arrayOfInlineContents);
+ }
+ }
+ return pointToPutCaret;
+ }
+ default:
+ return EditorDOMPoint();
+ }
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::SplitInlineAncestorsAtRangeBoundaries(
+ RangeItem& aRangeItem, BlockInlineCheck aBlockInlineCheck,
+ const Element& aEditingHost,
+ const nsIContent* aAncestorLimiter /* = nullptr */) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ EditorDOMPoint pointToPutCaret;
+ if (!aRangeItem.Collapsed() && aRangeItem.mEndContainer &&
+ aRangeItem.mEndContainer->IsContent()) {
+ nsCOMPtr<nsIContent> mostAncestorInlineContentAtEnd =
+ HTMLEditUtils::GetMostDistantAncestorInlineElement(
+ *aRangeItem.mEndContainer->AsContent(), aBlockInlineCheck,
+ &aEditingHost, aAncestorLimiter);
+
+ if (mostAncestorInlineContentAtEnd) {
+ Result<SplitNodeResult, nsresult> splitEndInlineResult =
+ SplitNodeDeepWithTransaction(
+ *mostAncestorInlineContentAtEnd, aRangeItem.EndPoint(),
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitEndInlineResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) failed");
+ return splitEndInlineResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitEndInlineResult =
+ splitEndInlineResult.unwrap();
+ unwrappedSplitEndInlineResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ if (pointToPutCaret.IsInContentNode() &&
+ MOZ_UNLIKELY(
+ &aEditingHost !=
+ ComputeEditingHost(*pointToPutCaret.ContainerAs<nsIContent>()))) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) caused changing editing host");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ const auto splitPointAtEnd =
+ unwrappedSplitEndInlineResult.AtSplitPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!splitPointAtEnd.IsSet())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) didn't return split point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ aRangeItem.mEndContainer = splitPointAtEnd.GetContainer();
+ aRangeItem.mEndOffset = splitPointAtEnd.Offset();
+ }
+ }
+
+ if (!aRangeItem.mStartContainer || !aRangeItem.mStartContainer->IsContent()) {
+ return pointToPutCaret;
+ }
+
+ nsCOMPtr<nsIContent> mostAncestorInlineContentAtStart =
+ HTMLEditUtils::GetMostDistantAncestorInlineElement(
+ *aRangeItem.mStartContainer->AsContent(), aBlockInlineCheck,
+ &aEditingHost, aAncestorLimiter);
+
+ if (mostAncestorInlineContentAtStart) {
+ Result<SplitNodeResult, nsresult> splitStartInlineResult =
+ SplitNodeDeepWithTransaction(*mostAncestorInlineContentAtStart,
+ aRangeItem.StartPoint(),
+ SplitAtEdges::eDoNotCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitStartInlineResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) failed");
+ return splitStartInlineResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitStartInlineResult =
+ splitStartInlineResult.unwrap();
+ // XXX Why don't we check editing host like above??
+ unwrappedSplitStartInlineResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ // XXX If we split only here because of collapsed range, we're modifying
+ // only start point of aRangeItem. Shouldn't we modify end point here
+ // if it's collapsed?
+ const auto splitPointAtStart =
+ unwrappedSplitStartInlineResult.AtSplitPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!splitPointAtStart.IsSet())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eDoNotCreateEmptyContainer) didn't return split point");
+ return Err(NS_ERROR_FAILURE);
+ }
+ aRangeItem.mStartContainer = splitPointAtStart.GetContainer();
+ aRangeItem.mStartOffset = splitPointAtStart.Offset();
+ }
+
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::SplitElementsAtEveryBRElement(
+ nsIContent& aMostAncestorToBeSplit,
+ nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // First build up a list of all the break nodes inside the inline container.
+ AutoTArray<OwningNonNull<HTMLBRElement>, 24> arrayOfBRElements;
+ DOMIterator iter(aMostAncestorToBeSplit);
+ iter.AppendAllNodesToArray(arrayOfBRElements);
+
+ // If there aren't any breaks, just put inNode itself in the array
+ if (arrayOfBRElements.IsEmpty()) {
+ aOutArrayOfContents.AppendElement(aMostAncestorToBeSplit);
+ return EditorDOMPoint();
+ }
+
+ // Else we need to bust up aMostAncestorToBeSplit along all the breaks
+ nsCOMPtr<nsIContent> nextContent = &aMostAncestorToBeSplit;
+ EditorDOMPoint pointToPutCaret;
+ for (OwningNonNull<HTMLBRElement>& brElement : arrayOfBRElements) {
+ EditorDOMPoint atBRNode(brElement);
+ if (NS_WARN_IF(!atBRNode.IsSet())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitNodeDeepWithTransaction(
+ *nextContent, atBRNode, SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ unwrappedSplitNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ // Put previous node at the split point.
+ if (nsIContent* previousContent =
+ unwrappedSplitNodeResult.GetPreviousContent()) {
+ // Might not be a left node. A break might have been at the very
+ // beginning of inline container, in which case
+ // SplitNodeDeepWithTransaction() would not actually split anything.
+ aOutArrayOfContents.AppendElement(*previousContent);
+ }
+
+ // Move break outside of container and also put in node list
+ // MOZ_KnownLive because 'arrayOfBRElements' is guaranteed to keep it alive.
+ Result<MoveNodeResult, nsresult> moveBRElementResult =
+ MoveNodeWithTransaction(
+ MOZ_KnownLive(brElement),
+ unwrappedSplitNodeResult.AtNextContent<EditorDOMPoint>());
+ if (MOZ_UNLIKELY(moveBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveBRElementResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveBRElementResult = moveBRElementResult.unwrap();
+ unwrappedMoveBRElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ aOutArrayOfContents.AppendElement(brElement);
+
+ nextContent = unwrappedSplitNodeResult.GetNextContent();
+ }
+
+ // Now tack on remaining next node.
+ aOutArrayOfContents.AppendElement(*nextContent);
+
+ return pointToPutCaret;
+}
+
+// static
+void HTMLEditor::MakeTransitionList(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ nsTArray<bool>& aTransitionArray) {
+ nsINode* prevParent = nullptr;
+ aTransitionArray.EnsureLengthAtLeast(aArrayOfContents.Length());
+ for (uint32_t i = 0; i < aArrayOfContents.Length(); i++) {
+ aTransitionArray[i] = aArrayOfContents[i]->GetParentNode() != prevParent;
+ prevParent = aArrayOfContents[i]->GetParentNode();
+ }
+}
+
+Result<InsertParagraphResult, nsresult>
+HTMLEditor::HandleInsertParagraphInHeadingElement(
+ Element& aHeadingElement, const EditorDOMPoint& aPointToSplit) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ auto splitHeadingResult =
+ [this, &aPointToSplit, &aHeadingElement]()
+ MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
+ // Normalize collapsible white-spaces around the split point to keep
+ // them visible after the split. Note that this does not touch
+ // selection because of using AutoTransactionsConserveSelection in
+ // WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes().
+ Result<EditorDOMPoint, nsresult> preparationResult =
+ WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ *this, aPointToSplit, aHeadingElement);
+ if (MOZ_UNLIKELY(preparationResult.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement() "
+ "failed");
+ return preparationResult.propagateErr();
+ }
+ EditorDOMPoint pointToSplit = preparationResult.unwrap();
+ MOZ_ASSERT(pointToSplit.IsInContentNode());
+
+ // Split the header
+ Result<SplitNodeResult, nsresult> splitResult =
+ SplitNodeDeepWithTransaction(
+ aHeadingElement, pointToSplit,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ NS_WARNING_ASSERTION(
+ splitResult.isOk(),
+ "HTMLEditor::SplitNodeDeepWithTransaction(aHeadingElement, "
+ "SplitAtEdges::eAllowToCreateEmptyContainer) failed");
+ return splitResult;
+ }();
+ if (MOZ_UNLIKELY(splitHeadingResult.isErr())) {
+ NS_WARNING("Failed to splitting aHeadingElement");
+ return splitHeadingResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitHeadingResult = splitHeadingResult.unwrap();
+ unwrappedSplitHeadingResult.IgnoreCaretPointSuggestion();
+ if (MOZ_UNLIKELY(!unwrappedSplitHeadingResult.DidSplit())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eAllowToCreateEmptyContainer) didn't split aHeadingElement");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // If the left heading element is empty, put a padding <br> element for empty
+ // last line into it.
+ // FYI: leftHeadingElement is grabbed by unwrappedSplitHeadingResult so that
+ // it's safe to access anytime.
+ auto* const leftHeadingElement =
+ unwrappedSplitHeadingResult.GetPreviousContentAs<Element>();
+ MOZ_ASSERT(leftHeadingElement,
+ "SplitNodeResult::GetPreviousContent() should return something if "
+ "DidSplit() returns true");
+ MOZ_DIAGNOSTIC_ASSERT(HTMLEditUtils::IsHeader(*leftHeadingElement));
+ if (HTMLEditUtils::IsEmptyNode(
+ *leftHeadingElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint(leftHeadingElement, 0u));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction("
+ ") failed");
+ return insertPaddingBRElementResult.propagateErr();
+ }
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ }
+
+ // Put caret at start of the right head element if it's not empty.
+ auto* const rightHeadingElement =
+ unwrappedSplitHeadingResult.GetNextContentAs<Element>();
+ MOZ_ASSERT(rightHeadingElement,
+ "SplitNodeResult::GetNextContent() should return something if "
+ "DidSplit() returns true");
+ if (!HTMLEditUtils::IsEmptyBlockElement(
+ *rightHeadingElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ return InsertParagraphResult(rightHeadingElement,
+ EditorDOMPoint(rightHeadingElement, 0u));
+ }
+
+ // If the right heading element is empty, delete it.
+ // TODO: If we know the new heading element becomes empty, we stop spliting
+ // the heading element.
+ // MOZ_KnownLive(rightHeadingElement) because it's grabbed by
+ // unwrappedSplitHeadingResult.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*rightHeadingElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+
+ // Layout tells the caret to blink in a weird place if we don't place a
+ // break after the header.
+ // XXX This block is dead code unless the removed right heading element is
+ // reconnected by a mutation event listener. This is a regression of
+ // bug 1405751:
+ // https://searchfox.org/mozilla-central/diff/879f3317d1331818718e18776caa47be7f426a22/editor/libeditor/HTMLEditRules.cpp#6389
+ // However, the traditional behavior is different from the other browsers.
+ // Chrome creates new paragraph in this case. Therefore, we should just
+ // drop this block in a follow up bug.
+ if (rightHeadingElement->GetNextSibling()) {
+ // XXX Ignoring non-editable <br> element here is odd because non-editable
+ // <br> elements also work as <br> from point of view of layout.
+ nsIContent* nextEditableSibling =
+ HTMLEditUtils::GetNextSibling(*rightHeadingElement->GetNextSibling(),
+ {WalkTreeOption::IgnoreNonEditableNode});
+ if (nextEditableSibling &&
+ nextEditableSibling->IsHTMLElement(nsGkAtoms::br)) {
+ auto afterEditableBRElement = EditorDOMPoint::After(*nextEditableSibling);
+ if (NS_WARN_IF(!afterEditableBRElement.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ // Put caret at the <br> element.
+ return InsertParagraphResult::NotHandled(
+ std::move(afterEditableBRElement));
+ }
+ }
+
+ if (MOZ_UNLIKELY(!leftHeadingElement->IsInComposedDoc())) {
+ NS_WARNING("The left heading element was unexpectedly removed");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear();
+ mPendingStylesToApplyToNewContent->ClearAllStyles();
+
+ // Create a paragraph if the right heading element is not followed by an
+ // editable <br> element.
+ nsStaticAtom& newParagraphTagName =
+ &DefaultParagraphSeparatorTagName() == nsGkAtoms::br
+ ? *nsGkAtoms::p
+ : DefaultParagraphSeparatorTagName();
+ // We want a wrapper element even if we separate with a <br>.
+ // MOZ_KnownLive(newParagraphTagName) because it's available until shutdown.
+ Result<CreateElementResult, nsresult> createNewParagraphElementResult =
+ CreateAndInsertElement(WithTransaction::Yes,
+ MOZ_KnownLive(newParagraphTagName),
+ EditorDOMPoint::After(*leftHeadingElement),
+ HTMLEditor::InsertNewBRElement);
+ if (MOZ_UNLIKELY(createNewParagraphElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewParagraphElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewParagraphElementResult =
+ createNewParagraphElementResult.unwrap();
+ // Put caret at the <br> element in the following paragraph.
+ unwrappedCreateNewParagraphElementResult.IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(unwrappedCreateNewParagraphElementResult.GetNewNode());
+ EditorDOMPoint pointToPutCaret(
+ unwrappedCreateNewParagraphElementResult.GetNewNode(), 0u);
+ return InsertParagraphResult(
+ unwrappedCreateNewParagraphElementResult.UnwrapNewNode(),
+ std::move(pointToPutCaret));
+}
+
+Result<SplitNodeResult, nsresult> HTMLEditor::HandleInsertParagraphInParagraph(
+ Element& aParentDivOrP, const EditorDOMPoint& aCandidatePointToSplit,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aCandidatePointToSplit.IsSetAndValid());
+
+ // First, get a better split point to avoid to create a new empty link in the
+ // right paragraph.
+ EditorDOMPoint pointToSplit = [&]() {
+ // We shouldn't create new anchor element which has non-empty href unless
+ // splitting middle of it because we assume that users don't want to create
+ // *same* anchor element across two or more paragraphs in most cases.
+ // So, adjust selection start if it's edge of anchor element(s).
+ // XXX We don't support white-space collapsing in these cases since it needs
+ // some additional work with WhiteSpaceVisibilityKeeper but it's not
+ // usual case. E.g., |<a href="foo"><b>foo []</b> </a>|
+ if (aCandidatePointToSplit.IsStartOfContainer()) {
+ EditorDOMPoint candidatePoint(aCandidatePointToSplit);
+ for (nsIContent* container =
+ aCandidatePointToSplit.GetContainerAs<nsIContent>();
+ container && container != &aParentDivOrP;
+ container = container->GetParent()) {
+ if (HTMLEditUtils::IsLink(container)) {
+ // Found link should be only in right node. So, we shouldn't split
+ // it.
+ candidatePoint.Set(container);
+ // Even if we found an anchor element, don't break because DOM API
+ // allows to nest anchor elements.
+ }
+ // If the container is middle of its parent, stop adjusting split point.
+ if (container->GetPreviousSibling()) {
+ // XXX Should we check if previous sibling is visible content?
+ // E.g., should we ignore comment node, invisible <br> element?
+ break;
+ }
+ }
+ return candidatePoint;
+ }
+
+ // We also need to check if selection is at invisible <br> element at end
+ // of an <a href="foo"> element because editor inserts a <br> element when
+ // user types Enter key after a white-space which is at middle of
+ // <a href="foo"> element and when setting selection at end of the element,
+ // selection becomes referring the <br> element. We may need to change this
+ // behavior later if it'd be standardized.
+ if (aCandidatePointToSplit.IsEndOfContainer() ||
+ aCandidatePointToSplit.IsBRElementAtEndOfContainer()) {
+ // If there are 2 <br> elements, the first <br> element is visible. E.g.,
+ // |<a href="foo"><b>boo[]<br></b><br></a>|, we should split the <a>
+ // element. Otherwise, E.g., |<a href="foo"><b>boo[]<br></b></a>|,
+ // we should not split the <a> element and ignore inline elements in it.
+ bool foundBRElement =
+ aCandidatePointToSplit.IsBRElementAtEndOfContainer();
+ EditorDOMPoint candidatePoint(aCandidatePointToSplit);
+ for (nsIContent* container =
+ aCandidatePointToSplit.GetContainerAs<nsIContent>();
+ container && container != &aParentDivOrP;
+ container = container->GetParent()) {
+ if (HTMLEditUtils::IsLink(container)) {
+ // Found link should be only in left node. So, we shouldn't split it.
+ candidatePoint.SetAfter(container);
+ // Even if we found an anchor element, don't break because DOM API
+ // allows to nest anchor elements.
+ }
+ // If the container is middle of its parent, stop adjusting split point.
+ if (nsIContent* nextSibling = container->GetNextSibling()) {
+ if (foundBRElement) {
+ // If we've already found a <br> element, we assume found node is
+ // visible <br> or something other node.
+ // XXX Should we check if non-text data node like comment?
+ break;
+ }
+
+ // XXX Should we check if non-text data node like comment?
+ if (!nextSibling->IsHTMLElement(nsGkAtoms::br)) {
+ break;
+ }
+ foundBRElement = true;
+ }
+ }
+ return candidatePoint;
+ }
+ return aCandidatePointToSplit;
+ }();
+
+ const bool createNewParagraph = GetReturnInParagraphCreatesNewParagraph();
+ RefPtr<HTMLBRElement> brElement;
+ if (createNewParagraph && pointToSplit.GetContainer() == &aParentDivOrP) {
+ // We are try to split only the current paragraph. Therefore, we don't need
+ // to create new <br> elements around it (if left and/or right paragraph
+ // becomes empty, it'll be treated by SplitParagraphWithTransaction().
+ brElement = nullptr;
+ } else if (pointToSplit.IsInTextNode()) {
+ if (pointToSplit.IsStartOfContainer()) {
+ // If we're splitting the paragraph at start of a text node and there is
+ // no preceding visible <br> element, we need to create a <br> element to
+ // keep the inline elements containing this text node.
+ // TODO: If the parent of the text node is the splitting paragraph,
+ // obviously we don't need to do this because empty paragraphs will
+ // be treated by SplitParagraphWithTransaction(). In this case, we
+ // just need to update pointToSplit for using the same path as the
+ // previous `if` block.
+ brElement =
+ HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetPreviousSibling(
+ *pointToSplit.ContainerAs<Text>(),
+ {WalkTreeOption::IgnoreNonEditableNode}));
+ if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) ||
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ // If insertParagraph does not create a new paragraph, default to
+ // insertLineBreak.
+ if (!createNewParagraph) {
+ return SplitNodeResult::NotHandled(pointToSplit);
+ }
+ const EditorDOMPoint pointToInsertBR = pointToSplit.ParentPoint();
+ MOZ_ASSERT(pointToInsertBR.IsSet());
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToInsertBR);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ brElement = HTMLBRElement::FromNodeOrNull(
+ insertBRElementResult.inspect().GetNewNode());
+ }
+ } else if (pointToSplit.IsEndOfContainer()) {
+ // If we're splitting the paragraph at end of a text node and there is not
+ // following visible <br> element, we need to create a <br> element after
+ // the text node to make current style specified by parent inline elements
+ // keep in the right paragraph.
+ // TODO: Same as above, we don't need to do this if the text node is a
+ // direct child of the paragraph. For using the simplest path, we
+ // just need to update `pointToSplit` in the case.
+ brElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetNextSibling(
+ *pointToSplit.ContainerAs<Text>(),
+ {WalkTreeOption::IgnoreNonEditableNode}));
+ if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) ||
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ // If insertParagraph does not create a new paragraph, default to
+ // insertLineBreak.
+ if (!createNewParagraph) {
+ return SplitNodeResult::NotHandled(pointToSplit);
+ }
+ const auto pointToInsertBR =
+ EditorDOMPoint::After(*pointToSplit.ContainerAs<Text>());
+ MOZ_ASSERT(pointToInsertBR.IsSet());
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToInsertBR);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ brElement = HTMLBRElement::FromNodeOrNull(
+ insertBRElementResult.inspect().GetNewNode());
+ }
+ } else {
+ // If insertParagraph does not create a new paragraph, default to
+ // insertLineBreak.
+ if (!createNewParagraph) {
+ return SplitNodeResult::NotHandled(pointToSplit);
+ }
+
+ // If we're splitting the paragraph at middle of a text node, we should
+ // split the text node here and put a <br> element next to the left text
+ // node.
+ // XXX Why? I think that this should be handled in
+ // SplitParagraphWithTransaction() directly because I don't find
+ // the necessary case of the <br> element.
+
+ // XXX We split a text node here if caret is middle of it to insert
+ // <br> element **before** splitting aParentDivOrP. Then, if
+ // the <br> element becomes unnecessary, it'll be removed again.
+ // So this does much more complicated things than what we want to
+ // do here. We should handle this case separately to make the code
+ // much simpler.
+
+ // Normalize collapsible white-spaces around the split point to keep
+ // them visible after the split. Note that this does not touch
+ // selection because of using AutoTransactionsConserveSelection in
+ // WhiteSpaceVisibilityKeeper::ReplaceTextAndRemoveEmptyTextNodes().
+ Result<EditorDOMPoint, nsresult> pointToSplitOrError =
+ WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ *this, pointToSplit, aParentDivOrP);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(pointToSplitOrError.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement() "
+ "failed");
+ return pointToSplitOrError.propagateErr();
+ }
+ MOZ_ASSERT(pointToSplitOrError.inspect().IsSetAndValid());
+ if (pointToSplitOrError.inspect().IsSet()) {
+ pointToSplit = pointToSplitOrError.unwrap();
+ }
+ Result<SplitNodeResult, nsresult> splitParentDivOrPResult =
+ SplitNodeWithTransaction(pointToSplit);
+ if (MOZ_UNLIKELY(splitParentDivOrPResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitParentDivOrPResult;
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ splitParentDivOrPResult.inspect().IgnoreCaretPointSuggestion();
+
+ pointToSplit.SetToEndOf(
+ splitParentDivOrPResult.inspect().GetPreviousContent());
+ if (NS_WARN_IF(!pointToSplit.IsInContentNode())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // We need to put new <br> after the left node if given node was split
+ // above.
+ const auto pointToInsertBR =
+ EditorDOMPoint::After(*pointToSplit.ContainerAs<nsIContent>());
+ MOZ_ASSERT(pointToInsertBR.IsSet());
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToInsertBR);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ brElement = HTMLBRElement::FromNodeOrNull(
+ insertBRElementResult.inspect().GetNewNode());
+ }
+ } else {
+ // If we're splitting in a child element of the paragraph, and there is no
+ // <br> element around it, we should insert a <br> element at the split
+ // point and keep splitting the paragraph after the new <br> element.
+ // XXX Why? We probably need to do this if we're splitting in an inline
+ // element which and whose parents provide some styles, we should put
+ // the <br> element for making a placeholder in the left paragraph for
+ // moving to the caret, but I think that this could be handled in fewer
+ // cases than this.
+ brElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetPreviousContent(
+ pointToSplit, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, &aEditingHost));
+ if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) ||
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ // is there a BR after it?
+ brElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetNextContent(
+ pointToSplit, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::Unused, &aEditingHost));
+ if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) ||
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ // If insertParagraph does not create a new paragraph, default to
+ // insertLineBreak.
+ if (!createNewParagraph) {
+ return SplitNodeResult::NotHandled(pointToSplit);
+ }
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, pointToSplit);
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.propagateErr();
+ }
+ // We'll collapse `Selection` to the place suggested by
+ // SplitParagraphWithTransaction.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ brElement = HTMLBRElement::FromNodeOrNull(
+ insertBRElementResult.inspect().GetNewNode());
+ // We split the parent after the <br>.
+ pointToSplit.SetAfter(brElement);
+ if (NS_WARN_IF(!pointToSplit.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ }
+ }
+ }
+
+ Result<SplitNodeResult, nsresult> splitParagraphResult =
+ SplitParagraphWithTransaction(aParentDivOrP, pointToSplit, brElement);
+ if (MOZ_UNLIKELY(splitParagraphResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitParagraphWithTransaction() failed");
+ return splitParagraphResult;
+ }
+ if (MOZ_UNLIKELY(!splitParagraphResult.inspect().DidSplit())) {
+ NS_WARNING(
+ "HTMLEditor::SplitParagraphWithTransaction() didn't split the "
+ "paragraph");
+ splitParagraphResult.inspect().IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_FAILURE);
+ }
+ MOZ_ASSERT(splitParagraphResult.inspect().Handled());
+ return splitParagraphResult;
+}
+
+Result<SplitNodeResult, nsresult> HTMLEditor::SplitParagraphWithTransaction(
+ Element& aParentDivOrP, const EditorDOMPoint& aStartOfRightNode,
+ HTMLBRElement* aMayBecomeVisibleBRElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Result<EditorDOMPoint, nsresult> preparationResult =
+ WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ *this, aStartOfRightNode, aParentDivOrP);
+ if (MOZ_UNLIKELY(preparationResult.isErr())) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement() failed");
+ return preparationResult.propagateErr();
+ }
+ EditorDOMPoint pointToSplit = preparationResult.unwrap();
+ MOZ_ASSERT(pointToSplit.IsInContentNode());
+
+ // Split the paragraph.
+ Result<SplitNodeResult, nsresult> splitDivOrPResult =
+ SplitNodeDeepWithTransaction(aParentDivOrP, pointToSplit,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitDivOrPResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitDivOrPResult;
+ }
+ SplitNodeResult unwrappedSplitDivOrPResult = splitDivOrPResult.unwrap();
+ if (MOZ_UNLIKELY(!unwrappedSplitDivOrPResult.DidSplit())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeDeepWithTransaction() didn't split any nodes");
+ return unwrappedSplitDivOrPResult;
+ }
+
+ // We'll compute caret suggestion later. So the simple result is not needed.
+ unwrappedSplitDivOrPResult.IgnoreCaretPointSuggestion();
+
+ auto* const leftDivOrParagraphElement =
+ unwrappedSplitDivOrPResult.GetPreviousContentAs<Element>();
+ MOZ_ASSERT(leftDivOrParagraphElement,
+ "SplitNodeResult::GetPreviousContent() should return something if "
+ "DidSplit() returns true");
+ auto* const rightDivOrParagraphElement =
+ unwrappedSplitDivOrPResult.GetNextContentAs<Element>();
+ MOZ_ASSERT(rightDivOrParagraphElement,
+ "SplitNodeResult::GetNextContent() should return something if "
+ "DidSplit() returns true");
+
+ // Get rid of the break, if it is visible (otherwise it may be needed to
+ // prevent an empty p).
+ if (aMayBecomeVisibleBRElement &&
+ HTMLEditUtils::IsVisibleBRElement(*aMayBecomeVisibleBRElement)) {
+ nsresult rv = DeleteNodeWithTransaction(*aMayBecomeVisibleBRElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+
+ // Remove ID attribute on the paragraph from the right node.
+ // MOZ_KnownLive(rightDivOrParagraphElement) because it's grabbed by
+ // unwrappedSplitDivOrPResult.
+ nsresult rv = RemoveAttributeWithTransaction(
+ MOZ_KnownLive(*rightDivOrParagraphElement), *nsGkAtoms::id);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::id) failed");
+ return Err(rv);
+ }
+
+ // We need to ensure to both paragraphs visible even if they are empty.
+ // However, padding <br> element for empty last line isn't useful in this
+ // case because it'll be ignored by PlaintextSerializer. Additionally,
+ // it'll be exposed as <br> with Element.innerHTML. Therefore, we can use
+ // normal <br> elements for placeholder in this case. Note that Chromium
+ // also behaves so.
+ auto InsertBRElementIfEmptyBlockElement =
+ [&](Element& aElement) MOZ_CAN_RUN_SCRIPT {
+ if (!HTMLEditUtils::IsBlockElement(
+ aElement, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return NS_OK;
+ }
+
+ if (!HTMLEditUtils::IsEmptyNode(
+ aElement, {EmptyCheckOption::TreatSingleBRElementAsVisible})) {
+ return NS_OK;
+ }
+
+ // XXX: Probably, we should use
+ // InsertPaddingBRElementForEmptyLastLineWithTransaction here, and
+ // if there are some empty inline container, we should put the <br>
+ // into the last one.
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes,
+ EditorDOMPoint(&aElement, 0u));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ // After this is called twice, we'll compute new caret position.
+ // Therefore, we don't need to update selection here.
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ return NS_OK;
+ };
+
+ // MOZ_KnownLive(leftDivOrParagraphElement) because it's grabbed by
+ // splitDivOrResult.
+ rv = InsertBRElementIfEmptyBlockElement(
+ MOZ_KnownLive(*leftDivOrParagraphElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "InsertBRElementIfEmptyBlockElement(leftDivOrParagraphElement) failed");
+ return Err(rv);
+ }
+
+ if (HTMLEditUtils::IsEmptyNode(*rightDivOrParagraphElement)) {
+ // If the right paragraph is empty, it might have an empty inline element
+ // (which may contain other empty inline containers) and optionally a <br>
+ // element which may not be in the deepest inline element.
+ const RefPtr<Element> deepestInlineContainerElement =
+ [](const Element& aBlockElement) {
+ Element* result = nullptr;
+ for (Element* maybeDeepestInlineContainer =
+ Element::FromNodeOrNull(aBlockElement.GetFirstChild());
+ maybeDeepestInlineContainer &&
+ HTMLEditUtils::IsInlineContent(
+ *maybeDeepestInlineContainer,
+ BlockInlineCheck::UseComputedDisplayStyle) &&
+ HTMLEditUtils::IsContainerNode(*maybeDeepestInlineContainer);
+ maybeDeepestInlineContainer =
+ maybeDeepestInlineContainer->GetFirstElementChild()) {
+ result = maybeDeepestInlineContainer;
+ }
+ return result;
+ }(*rightDivOrParagraphElement);
+ if (deepestInlineContainerElement) {
+ RefPtr<HTMLBRElement> brElement =
+ HTMLEditUtils::GetFirstBRElement(*rightDivOrParagraphElement);
+ // If there is a <br> element and it is in the deepest inline container,
+ // we need to do nothing anymore. Let's suggest caret position as at the
+ // <br>.
+ if (brElement &&
+ brElement->GetParentNode() == deepestInlineContainerElement) {
+ brElement->SetFlags(NS_PADDING_FOR_EMPTY_LAST_LINE);
+ return SplitNodeResult(std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(brElement));
+ }
+ // Otherwise, we should put a padding <br> element into the deepest inline
+ // container and then, existing <br> element (if there is) becomes
+ // unnecessary.
+ if (brElement) {
+ nsresult rv = DeleteNodeWithTransaction(*brElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint::AtEndOf(deepestInlineContainerElement));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertPaddingBRElementForEmptyLastLineWithTransaction() failed");
+ return insertPaddingBRElementResult.propagateErr();
+ }
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ return SplitNodeResult(
+ std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(insertPaddingBRElementResult.inspect().GetNewNode()));
+ }
+
+ // If there is no inline container elements, we just need to make the
+ // right paragraph visible.
+ nsresult rv = InsertBRElementIfEmptyBlockElement(
+ MOZ_KnownLive(*rightDivOrParagraphElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "InsertBRElementIfEmptyBlockElement(rightDivOrParagraphElement) "
+ "failed");
+ return Err(rv);
+ }
+ }
+
+ // Let's put caret at start of the first leaf container.
+ nsIContent* child = HTMLEditUtils::GetFirstLeafContent(
+ *rightDivOrParagraphElement, {LeafNodeType::LeafNodeOrChildBlock},
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(!child)) {
+ return SplitNodeResult(std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(rightDivOrParagraphElement, 0u));
+ }
+ return child->IsText() || HTMLEditUtils::IsContainerNode(*child)
+ ? SplitNodeResult(std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(child, 0u))
+ : SplitNodeResult(std::move(unwrappedSplitDivOrPResult),
+ EditorDOMPoint(child));
+}
+
+Result<InsertParagraphResult, nsresult>
+HTMLEditor::HandleInsertParagraphInListItemElement(
+ Element& aListItemElement, const EditorDOMPoint& aPointToSplit,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(HTMLEditUtils::IsListItem(&aListItemElement));
+
+ // If aListItemElement is empty, then we want to outdent its content.
+ if (&aEditingHost != aListItemElement.GetParentElement() &&
+ HTMLEditUtils::IsEmptyBlockElement(
+ aListItemElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ RefPtr<Element> leftListElement = aListItemElement.GetParentElement();
+ // If the given list item element is not the last list item element of
+ // its parent nor not followed by sub list elements, split the parent
+ // before it.
+ if (!HTMLEditUtils::IsLastChild(aListItemElement,
+ {WalkTreeOption::IgnoreNonEditableNode})) {
+ Result<SplitNodeResult, nsresult> splitListItemParentResult =
+ SplitNodeWithTransaction(EditorDOMPoint(&aListItemElement));
+ if (MOZ_UNLIKELY(splitListItemParentResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitListItemParentResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitListItemParentResult =
+ splitListItemParentResult.unwrap();
+ if (MOZ_UNLIKELY(!unwrappedSplitListItemParentResult.DidSplit())) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeWithTransaction() didn't split the parent of "
+ "aListItemElement");
+ MOZ_ASSERT(
+ !unwrappedSplitListItemParentResult.HasCaretPointSuggestion());
+ return Err(NS_ERROR_FAILURE);
+ }
+ unwrappedSplitListItemParentResult.IgnoreCaretPointSuggestion();
+ leftListElement =
+ unwrappedSplitListItemParentResult.GetPreviousContentAs<Element>();
+ MOZ_DIAGNOSTIC_ASSERT(leftListElement);
+ }
+
+ auto afterLeftListElement = EditorDOMPoint::After(leftListElement);
+ if (MOZ_UNLIKELY(!afterLeftListElement.IsSet())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // If aListItemElement is in an invalid sub-list element, move it into
+ // the grand parent list element in order to outdent.
+ if (HTMLEditUtils::IsAnyListElement(afterLeftListElement.GetContainer())) {
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ MoveNodeWithTransaction(aListItemElement, afterLeftListElement);
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveListItemElementResult.propagateErr();
+ }
+ moveListItemElementResult.inspect().IgnoreCaretPointSuggestion();
+ return InsertParagraphResult(&aListItemElement,
+ EditorDOMPoint(&aListItemElement, 0u));
+ }
+
+ // Otherwise, replace the empty aListItemElement with a new paragraph.
+ nsresult rv = DeleteNodeWithTransaction(aListItemElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ nsStaticAtom& newParagraphTagName =
+ &DefaultParagraphSeparatorTagName() == nsGkAtoms::br
+ ? *nsGkAtoms::p
+ : DefaultParagraphSeparatorTagName();
+ // MOZ_KnownLive(newParagraphTagName) because it's available until shutdown.
+ Result<CreateElementResult, nsresult> createNewParagraphElementResult =
+ CreateAndInsertElement(
+ WithTransaction::Yes, MOZ_KnownLive(newParagraphTagName),
+ afterLeftListElement, HTMLEditor::InsertNewBRElement);
+ if (MOZ_UNLIKELY(createNewParagraphElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewParagraphElementResult.propagateErr();
+ }
+ createNewParagraphElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(createNewParagraphElementResult.inspect().GetNewNode());
+ EditorDOMPoint pointToPutCaret(
+ createNewParagraphElementResult.inspect().GetNewNode(), 0u);
+ return InsertParagraphResult(
+ createNewParagraphElementResult.inspect().GetNewNode(),
+ std::move(pointToPutCaret));
+ }
+
+ // If aListItemElement has some content or aListItemElement is empty but it's
+ // a child of editing host, we want a new list item at the same list level.
+ // First, sort out white-spaces.
+ Result<EditorDOMPoint, nsresult> preparationResult =
+ WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement(
+ *this, aPointToSplit, aListItemElement);
+ if (preparationResult.isErr()) {
+ NS_WARNING(
+ "WhiteSpaceVisibilityKeeper::PrepareToSplitBlockElement() failed");
+ return Err(preparationResult.unwrapErr());
+ }
+ EditorDOMPoint pointToSplit = preparationResult.unwrap();
+ MOZ_ASSERT(pointToSplit.IsInContentNode());
+
+ // Now split the list item.
+ Result<SplitNodeResult, nsresult> splitListItemResult =
+ SplitNodeDeepWithTransaction(aListItemElement, pointToSplit,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ if (MOZ_UNLIKELY(splitListItemResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
+ return splitListItemResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitListItemElement = splitListItemResult.unwrap();
+ unwrappedSplitListItemElement.IgnoreCaretPointSuggestion();
+ if (MOZ_UNLIKELY(!aListItemElement.GetParent())) {
+ NS_WARNING("Somebody disconnected the target listitem from the parent");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ // If aListItemElement is not replaced, we should not do anything anymore.
+ if (MOZ_UNLIKELY(!unwrappedSplitListItemElement.DidSplit()) ||
+ NS_WARN_IF(!unwrappedSplitListItemElement.GetNewContentAs<Element>()) ||
+ NS_WARN_IF(
+ !unwrappedSplitListItemElement.GetOriginalContentAs<Element>())) {
+ NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() didn't split");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // FYI: They are grabbed by unwrappedSplitListItemElement so that they are
+ // known live
+ // things.
+ auto& leftListItemElement =
+ *unwrappedSplitListItemElement.GetPreviousContentAs<Element>();
+ auto& rightListItemElement =
+ *unwrappedSplitListItemElement.GetNextContentAs<Element>();
+
+ // Hack: until I can change the damaged doc range code back to being
+ // extra-inclusive, I have to manually detect certain list items that may be
+ // left empty.
+ if (HTMLEditUtils::IsEmptyNode(
+ leftListItemElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint(&leftListItemElement, 0u));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction("
+ ") failed");
+ return insertPaddingBRElementResult.propagateErr();
+ }
+ // We're returning a candidate point to put caret so that we don't need to
+ // update now.
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ return InsertParagraphResult(&rightListItemElement,
+ EditorDOMPoint(&rightListItemElement, 0u));
+ }
+
+ if (HTMLEditUtils::IsEmptyNode(
+ rightListItemElement,
+ {EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ // If aListItemElement is a <dd> or a <dt> and the right list item is empty
+ // or a direct child of the editing host, replace it a new list item element
+ // whose type is the other one.
+ if (aListItemElement.IsAnyOfHTMLElements(nsGkAtoms::dd, nsGkAtoms::dt)) {
+ nsStaticAtom& nextDefinitionListItemTagName =
+ aListItemElement.IsHTMLElement(nsGkAtoms::dt) ? *nsGkAtoms::dd
+ : *nsGkAtoms::dt;
+ // MOZ_KnownLive(nextDefinitionListItemTagName) because it's available
+ // until shutdown.
+ Result<CreateElementResult, nsresult> createNewListItemElementResult =
+ CreateAndInsertElement(WithTransaction::Yes,
+ MOZ_KnownLive(nextDefinitionListItemTagName),
+ EditorDOMPoint::After(rightListItemElement));
+ if (MOZ_UNLIKELY(createNewListItemElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewListItemElementResult.propagateErr();
+ }
+ CreateElementResult unwrappedCreateNewListItemElementResult =
+ createNewListItemElementResult.unwrap();
+ unwrappedCreateNewListItemElementResult.IgnoreCaretPointSuggestion();
+ RefPtr<Element> newListItemElement =
+ unwrappedCreateNewListItemElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newListItemElement);
+ // MOZ_KnownLive(rightListItemElement) because it's grabbed by
+ // unwrappedSplitListItemElement.
+ nsresult rv =
+ DeleteNodeWithTransaction(MOZ_KnownLive(rightListItemElement));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ EditorDOMPoint pointToPutCaret(newListItemElement, 0u);
+ return InsertParagraphResult(std::move(newListItemElement),
+ std::move(pointToPutCaret));
+ }
+
+ // If aListItemElement is a <li> and the right list item becomes empty or a
+ // direct child of the editing host, copy all inline elements affecting to
+ // the style at end of the left list item element to the right list item
+ // element.
+ // MOZ_KnownLive(leftListItemElement) and
+ // MOZ_KnownLive(rightListItemElement) because they are grabbed by
+ // unwrappedSplitListItemElement.
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ CopyLastEditableChildStylesWithTransaction(
+ MOZ_KnownLive(leftListItemElement),
+ MOZ_KnownLive(rightListItemElement), aEditingHost);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CopyLastEditableChildStylesWithTransaction() failed");
+ return pointToPutCaretOrError.propagateErr();
+ }
+ return InsertParagraphResult(&rightListItemElement,
+ pointToPutCaretOrError.unwrap());
+ }
+
+ // If the right list item element is not empty, we need to consider where to
+ // put caret in it. If it has non-container inline elements, <br> or <hr>, at
+ // the element is proper position.
+ WSScanResult forwardScanFromStartOfListItemResult =
+ WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
+ &aEditingHost, EditorRawDOMPoint(&rightListItemElement, 0u),
+ BlockInlineCheck::UseComputedDisplayStyle);
+ if (MOZ_UNLIKELY(forwardScanFromStartOfListItemResult.Failed())) {
+ NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (forwardScanFromStartOfListItemResult.ReachedSpecialContent() ||
+ forwardScanFromStartOfListItemResult.ReachedBRElement() ||
+ forwardScanFromStartOfListItemResult.ReachedHRElement()) {
+ auto atFoundElement =
+ forwardScanFromStartOfListItemResult.PointAtContent<EditorDOMPoint>();
+ if (NS_WARN_IF(!atFoundElement.IsSetAndValid())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return InsertParagraphResult(&rightListItemElement,
+ std::move(atFoundElement));
+ }
+
+ // If we reached a block boundary (end of the list item or a child block),
+ // let's put deepest start of the list item or the child block.
+ if (forwardScanFromStartOfListItemResult.ReachedBlockBoundary()) {
+ return InsertParagraphResult(
+ &rightListItemElement,
+ HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>(
+ forwardScanFromStartOfListItemResult.GetContent()
+ ? *forwardScanFromStartOfListItemResult.GetContent()
+ : rightListItemElement));
+ }
+
+ // Otherwise, return the point at first visible thing.
+ // XXX This may be not meaningful position if it reached block element
+ // in aListItemElement.
+ return InsertParagraphResult(
+ &rightListItemElement,
+ forwardScanFromStartOfListItemResult.Point<EditorDOMPoint>());
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // The idea here is to put the nodes into a minimal number of blockquotes.
+ // When the user blockquotes something, they expect one blockquote. That
+ // may not be possible (for instance, if they have two table cells selected,
+ // you need two blockquotes inside the cells).
+ RefPtr<Element> curBlock, blockElementToPutCaret;
+ nsCOMPtr<nsINode> prevParent;
+
+ EditorDOMPoint pointToPutCaret;
+ for (auto& content : aArrayOfContents) {
+ // If the node is a table element or list item, dive inside
+ if (HTMLEditUtils::IsAnyTableElementButNotTable(content) ||
+ HTMLEditUtils::IsListItem(content)) {
+ // Forget any previous block
+ curBlock = nullptr;
+ // Recursion time
+ AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
+ HTMLEditUtils::CollectAllChildren(*content, childContents);
+ Result<CreateElementResult, nsresult>
+ wrapChildrenInAnotherBlockquoteResult =
+ WrapContentsInBlockquoteElementsWithTransaction(childContents,
+ aEditingHost);
+ if (MOZ_UNLIKELY(wrapChildrenInAnotherBlockquoteResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::WrapContentsInBlockquoteElementsWithTransaction() "
+ "failed");
+ return wrapChildrenInAnotherBlockquoteResult;
+ }
+ CreateElementResult unwrappedWrapChildrenInAnotherBlockquoteResult =
+ wrapChildrenInAnotherBlockquoteResult.unwrap();
+ unwrappedWrapChildrenInAnotherBlockquoteResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ if (unwrappedWrapChildrenInAnotherBlockquoteResult.GetNewNode()) {
+ blockElementToPutCaret =
+ unwrappedWrapChildrenInAnotherBlockquoteResult.UnwrapNewNode();
+ }
+ }
+
+ // If the node has different parent than previous node, further nodes in a
+ // new parent
+ if (prevParent) {
+ if (prevParent != content->GetParentNode()) {
+ // Forget any previous blockquote node we were using
+ curBlock = nullptr;
+ prevParent = content->GetParentNode();
+ }
+ } else {
+ prevParent = content->GetParentNode();
+ }
+
+ // If no curBlock, make one
+ if (!curBlock) {
+ Result<CreateElementResult, nsresult> createNewBlockquoteElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::blockquote, EditorDOMPoint(content),
+ BRElementNextToSplitPoint::Keep, aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockquoteElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::blockquote) failed");
+ return createNewBlockquoteElementResult;
+ }
+ CreateElementResult unwrappedCreateNewBlockquoteElementResult =
+ createNewBlockquoteElementResult.unwrap();
+ unwrappedCreateNewBlockquoteElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_ASSERT(unwrappedCreateNewBlockquoteElementResult.GetNewNode());
+ blockElementToPutCaret =
+ unwrappedCreateNewBlockquoteElementResult.GetNewNode();
+ curBlock = unwrappedCreateNewBlockquoteElementResult.UnwrapNewNode();
+ }
+
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to/ keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *curBlock);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ unwrappedMoveNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ return blockElementToPutCaret
+ ? CreateElementResult(std::move(blockElementToPutCaret),
+ std::move(pointToPutCaret))
+ : CreateElementResult::NotHandled(std::move(pointToPutCaret));
+}
+
+Result<EditorDOMPoint, nsresult>
+HTMLEditor::RemoveBlockContainerElementsWithTransaction(
+ const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ FormatBlockMode aFormatBlockMode, BlockInlineCheck aBlockInlineCheck) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand);
+
+ // Intent of this routine is to be used for converting to/from headers,
+ // paragraphs, pre, and address. Those blocks that pretty much just contain
+ // inline things...
+ RefPtr<Element> blockElement;
+ nsCOMPtr<nsIContent> firstContent, lastContent;
+ EditorDOMPoint pointToPutCaret;
+ for (const auto& content : aArrayOfContents) {
+ // If the current node is a format element, remove it.
+ if (HTMLEditUtils::IsFormatElementForParagraphStateCommand(content)) {
+ // Process any partial progress saved
+ if (blockElement) {
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ }
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+ // Remove current block
+ Result<EditorDOMPoint, nsresult> unwrapFormatBlockResult =
+ RemoveBlockContainerWithTransaction(
+ MOZ_KnownLive(*content->AsElement()));
+ if (MOZ_UNLIKELY(unwrapFormatBlockResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapFormatBlockResult;
+ }
+ if (unwrapFormatBlockResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapFormatBlockResult.unwrap();
+ }
+ continue;
+ }
+
+ // XXX How about, <th>, <thead>, <tfoot>, <dt>, <dl>?
+ if (content->IsAnyOfHTMLElements(
+ nsGkAtoms::table, nsGkAtoms::tr, nsGkAtoms::tbody, nsGkAtoms::td,
+ nsGkAtoms::li, nsGkAtoms::blockquote, nsGkAtoms::div) ||
+ HTMLEditUtils::IsAnyListElement(content)) {
+ // Process any partial progress saved
+ if (blockElement) {
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ }
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+ // Recursion time
+ AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
+ HTMLEditUtils::CollectAllChildren(*content, childContents);
+ Result<EditorDOMPoint, nsresult> removeBlockContainerElementsResult =
+ RemoveBlockContainerElementsWithTransaction(
+ childContents, aFormatBlockMode, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed");
+ return removeBlockContainerElementsResult;
+ }
+ if (removeBlockContainerElementsResult.inspect().IsSet()) {
+ pointToPutCaret = removeBlockContainerElementsResult.unwrap();
+ }
+ continue;
+ }
+
+ if (HTMLEditUtils::IsInlineContent(content, aBlockInlineCheck)) {
+ if (blockElement) {
+ // If so, is this node a descendant?
+ if (EditorUtils::IsDescendantOf(*content, *blockElement)) {
+ // Then we don't need to do anything different for this node
+ lastContent = content;
+ continue;
+ }
+ // Otherwise, we have progressed beyond end of blockElement, so let's
+ // handle it now. We need to remove the portion of blockElement that
+ // contains [firstContent - lastContent].
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ // Fall out and handle content
+ }
+ blockElement = HTMLEditUtils::GetAncestorElement(
+ content, HTMLEditUtils::ClosestEditableBlockElement,
+ aBlockInlineCheck);
+ if (!blockElement ||
+ !HTMLEditUtils::IsFormatElementForParagraphStateCommand(
+ *blockElement) ||
+ !HTMLEditUtils::IsRemovableNode(*blockElement)) {
+ // Not a block kind that we care about.
+ blockElement = nullptr;
+ } else {
+ firstContent = lastContent = content;
+ }
+ continue;
+ }
+
+ if (blockElement) {
+ // Some node that is already sans block style. Skip over it and process
+ // any partial progress saved.
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ continue;
+ }
+ }
+ // Process any partial progress saved
+ if (blockElement) {
+ Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult =
+ RemoveBlockContainerElementWithTransactionBetween(
+ *blockElement, *firstContent, *lastContent, aBlockInlineCheck);
+ if (MOZ_UNLIKELY(unwrapBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveBlockContainerElementWithTransactionBetween() "
+ "failed");
+ return unwrapBlockElementResult.propagateErr();
+ }
+ unwrapBlockElementResult.unwrap().MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ firstContent = lastContent = blockElement = nullptr;
+ }
+ return pointToPutCaret;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::CreateOrChangeFormatContainerElement(
+ nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents,
+ const nsStaticAtom& aNewFormatTagName, FormatBlockMode aFormatBlockMode,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // Intent of this routine is to be used for converting to/from headers,
+ // paragraphs, pre, and address. Those blocks that pretty much just contain
+ // inline things...
+ RefPtr<Element> newBlock, curBlock, blockElementToPutCaret;
+ // If we found a <br> element which should be moved into curBlock, this keeps
+ // storing the <br> element after removing it from the tree.
+ RefPtr<Element> pendingBRElementToMoveCurBlock;
+ EditorDOMPoint pointToPutCaret;
+ for (auto& content : aArrayOfContents) {
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsInContentNode())) {
+ // If given node has been removed from the document, let's ignore it
+ // since the following code may need its parent replace it with new
+ // block.
+ curBlock = nullptr;
+ newBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ continue;
+ }
+
+ // Is it already the right kind of block, or an uneditable block?
+ if (content->IsHTMLElement(&aNewFormatTagName) ||
+ (!EditorUtils::IsEditableContent(content, EditorType::HTML) &&
+ HTMLEditUtils::IsBlockElement(
+ content, BlockInlineCheck::UseHTMLDefaultStyle))) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ // Do nothing to this block
+ continue;
+ }
+
+ // If content is a format element, replace it with a new block of correct
+ // type.
+ // XXX: pre can't hold everything the others can
+ if (HTMLEditUtils::IsMozDiv(content) ||
+ HTMLEditor::IsFormatElement(aFormatBlockMode, content)) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ RefPtr<Element> expectedContainerOfNewBlock =
+ atContent.IsContainerHTMLElement(nsGkAtoms::dl) &&
+ HTMLEditUtils::IsSplittableNode(
+ *atContent.ContainerAs<Element>())
+ ? atContent.GetContainerParentAs<Element>()
+ : atContent.GetContainerAs<Element>();
+ Result<CreateElementResult, nsresult> replaceWithNewBlockElementResult =
+ ReplaceContainerAndCloneAttributesWithTransaction(
+ MOZ_KnownLive(*content->AsElement()), aNewFormatTagName);
+ if (MOZ_UNLIKELY(replaceWithNewBlockElementResult.isErr())) {
+ NS_WARNING(
+ "EditorBase::ReplaceContainerAndCloneAttributesWithTransaction() "
+ "failed");
+ return replaceWithNewBlockElementResult;
+ }
+ CreateElementResult unwrappedReplaceWithNewBlockElementResult =
+ replaceWithNewBlockElementResult.unwrap();
+ // If the new block element was moved to different element or removed by
+ // the web app via mutation event listener, we should stop handling this
+ // action since we cannot handle each of a lot of edge cases.
+ if (NS_WARN_IF(unwrappedReplaceWithNewBlockElementResult.GetNewNode()
+ ->GetParentNode() != expectedContainerOfNewBlock)) {
+ unwrappedReplaceWithNewBlockElementResult.IgnoreCaretPointSuggestion();
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+ unwrappedReplaceWithNewBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ newBlock = unwrappedReplaceWithNewBlockElementResult.UnwrapNewNode();
+ continue;
+ }
+
+ if (HTMLEditUtils::IsTable(content) ||
+ HTMLEditUtils::IsAnyListElement(content) ||
+ content->IsAnyOfHTMLElements(nsGkAtoms::tbody, nsGkAtoms::tr,
+ nsGkAtoms::td, nsGkAtoms::li,
+ nsGkAtoms::blockquote, nsGkAtoms::div)) {
+ // Forget any previous block used for previous inline nodes
+ curBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ // Recursion time
+ AutoTArray<OwningNonNull<nsIContent>, 24> childContents;
+ HTMLEditUtils::CollectAllChildren(*content, childContents);
+ if (!childContents.IsEmpty()) {
+ Result<CreateElementResult, nsresult> wrapChildrenInBlockElementResult =
+ CreateOrChangeFormatContainerElement(
+ childContents, aNewFormatTagName, aFormatBlockMode,
+ aEditingHost);
+ if (MOZ_UNLIKELY(wrapChildrenInBlockElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateOrChangeFormatContainerElement() failed");
+ return wrapChildrenInBlockElementResult;
+ }
+ CreateElementResult unwrappedWrapChildrenInBlockElementResult =
+ wrapChildrenInBlockElementResult.unwrap();
+ unwrappedWrapChildrenInBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ if (unwrappedWrapChildrenInBlockElementResult.GetNewNode()) {
+ blockElementToPutCaret =
+ unwrappedWrapChildrenInBlockElementResult.UnwrapNewNode();
+ }
+ continue;
+ }
+
+ // Make sure we can put a block here
+ Result<CreateElementResult, nsresult> createNewBlockElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ aNewFormatTagName, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(
+ nsPrintfCString(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction(%s) failed",
+ nsAtomCString(&aNewFormatTagName).get())
+ .get());
+ return createNewBlockElementResult;
+ }
+ CreateElementResult unwrappedCreateNewBlockElementResult =
+ createNewBlockElementResult.unwrap();
+ unwrappedCreateNewBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode());
+ blockElementToPutCaret =
+ unwrappedCreateNewBlockElementResult.UnwrapNewNode();
+ continue;
+ }
+
+ if (content->IsHTMLElement(nsGkAtoms::br)) {
+ if (curBlock) {
+ if (aFormatBlockMode == FormatBlockMode::XULParagraphStateCommand) {
+ // If the node is a break, we honor it by putting further nodes in a
+ // new parent.
+
+ // Forget any previous block used for previous inline nodes.
+ curBlock = nullptr;
+ pendingBRElementToMoveCurBlock = nullptr;
+ } else {
+ // If the node is a break, we need to move it into end of the curBlock
+ // if we'll move following content into curBlock.
+ pendingBRElementToMoveCurBlock = content->AsElement();
+ }
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
+ // alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*content));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ continue;
+ }
+
+ // The break is the first (or even only) node we encountered. Create a
+ // block for it.
+ Result<CreateElementResult, nsresult> createNewBlockElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ aNewFormatTagName, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(nsPrintfCString("HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWith"
+ "Transaction(%s) failed",
+ nsAtomCString(&aNewFormatTagName).get())
+ .get());
+ return createNewBlockElementResult;
+ }
+ CreateElementResult unwrappedCreateNewBlockElementResult =
+ createNewBlockElementResult.unwrap();
+ unwrappedCreateNewBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ RefPtr<Element> newBlockElement =
+ unwrappedCreateNewBlockElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newBlockElement);
+ blockElementToPutCaret = newBlockElement;
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
+ // alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
+ *newBlockElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ unwrappedMoveNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ curBlock = std::move(newBlockElement);
+ continue;
+ }
+
+ if (HTMLEditUtils::IsInlineContent(content,
+ BlockInlineCheck::UseHTMLDefaultStyle)) {
+ // If content is inline, pull it into curBlock. Note: it's assumed that
+ // consecutive inline nodes in aNodeArray are actually members of the
+ // same block parent. This happens to be true now as a side effect of
+ // how aNodeArray is constructed, but some additional logic should be
+ // added here if that should change
+ //
+ // If content is a non editable, drop it if we are going to <pre>.
+ if (&aNewFormatTagName == nsGkAtoms::pre &&
+ !EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ // Do nothing to this block
+ continue;
+ }
+
+ // If no curBlock, make one
+ if (!curBlock) {
+ Result<CreateElementResult, nsresult> createNewBlockElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ aNewFormatTagName, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) {
+ NS_WARNING(nsPrintfCString("HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWith"
+ "Transaction(%s) failed",
+ nsAtomCString(&aNewFormatTagName).get())
+ .get());
+ return createNewBlockElementResult;
+ }
+ CreateElementResult unwrappedCreateNewBlockElementResult =
+ createNewBlockElementResult.unwrap();
+ unwrappedCreateNewBlockElementResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ MOZ_ASSERT(unwrappedCreateNewBlockElementResult.GetNewNode());
+ blockElementToPutCaret =
+ unwrappedCreateNewBlockElementResult.GetNewNode();
+ curBlock = unwrappedCreateNewBlockElementResult.UnwrapNewNode();
+
+ // Update container of content.
+ atContent.Set(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ // This is possible due to mutation events, let's not assert
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ } else if (pendingBRElementToMoveCurBlock) {
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertNodeWithTransaction<Element>(
+ *pendingBRElementToMoveCurBlock,
+ EditorDOMPoint::AtEndOf(*curBlock));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction<Element>() failed");
+ return insertBRElementResult.propagateErr();
+ }
+ insertBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ pendingBRElementToMoveCurBlock = nullptr;
+ }
+
+ // XXX If content is a br, replace it with a return if going to <pre>
+
+ // This is a continuation of some inline nodes that belong together in
+ // the same block item. Use curBlock.
+ //
+ // MOZ_KnownLive because 'aArrayOfContents' is guaranteed to keep it
+ // alive. We could try to make that a rvalue ref and create a const array
+ // on the stack here, but callers are passing in auto arrays, and we don't
+ // want to introduce copies..
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *curBlock);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
+ unwrappedMoveNodeResult.MoveCaretPointTo(
+ pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
+ }
+ }
+ return blockElementToPutCaret
+ ? CreateElementResult(std::move(blockElementToPutCaret),
+ std::move(pointToPutCaret))
+ : CreateElementResult::NotHandled(std::move(pointToPutCaret));
+}
+
+Result<SplitNodeResult, nsresult>
+HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction(
+ const nsAtom& aTag, const EditorDOMPoint& aStartOfDeepestRightNode,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!aEditingHost.IsInComposedDoc())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ if (NS_WARN_IF(!aStartOfDeepestRightNode.IsSet())) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+ MOZ_ASSERT(aStartOfDeepestRightNode.IsSetAndValid());
+
+ // The point must be descendant of editing host.
+ // XXX Isn't it a valid case if it points a direct child of aEditingHost?
+ if (NS_WARN_IF(
+ !aStartOfDeepestRightNode.GetContainer()->IsInclusiveDescendantOf(
+ &aEditingHost))) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ // Look for a node that can legally contain the tag.
+ const EditorDOMPoint pointToInsert =
+ HTMLEditUtils::GetInsertionPointInInclusiveAncestor(
+ aTag, aStartOfDeepestRightNode, &aEditingHost);
+ if (MOZ_UNLIKELY(!pointToInsert.IsSet())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() reached "
+ "editing host");
+ return Err(NS_ERROR_FAILURE);
+ }
+ // If the point itself can contain the tag, we don't need to split any
+ // ancestor nodes. In this case, we should return the given split point
+ // as is.
+ if (pointToInsert.GetContainer() == aStartOfDeepestRightNode.GetContainer()) {
+ return SplitNodeResult::NotHandled(aStartOfDeepestRightNode);
+ }
+
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ SplitNodeDeepWithTransaction(MOZ_KnownLive(*pointToInsert.GetChild()),
+ aStartOfDeepestRightNode,
+ SplitAtEdges::eAllowToCreateEmptyContainer);
+ NS_WARNING_ASSERTION(splitNodeResult.isOk(),
+ "HTMLEditor::SplitNodeDeepWithTransaction(SplitAtEdges::"
+ "eAllowToCreateEmptyContainer) failed");
+ return splitNodeResult;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(
+ const nsAtom& aTagName, const EditorDOMPoint& aPointToInsert,
+ BRElementNextToSplitPoint aBRElementNextToSplitPoint,
+ const Element& aEditingHost,
+ const InitializeInsertingElement& aInitializer) {
+ MOZ_ASSERT(aPointToInsert.IsSetAndValid());
+
+ const nsCOMPtr<nsIContent> childAtPointToInsert = aPointToInsert.GetChild();
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ MaybeSplitAncestorsForInsertWithTransaction(aTagName, aPointToInsert,
+ aEditingHost);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() failed");
+ return splitNodeResult.propagateErr();
+ }
+ SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
+ DebugOnly<bool> wasCaretPositionSuggestedAtSplit =
+ unwrappedSplitNodeResult.HasCaretPointSuggestion();
+ // We'll update selection below, and nobody touches selection until then.
+ // Therefore, we don't need to touch selection here.
+ unwrappedSplitNodeResult.IgnoreCaretPointSuggestion();
+
+ // If current handling node has been moved from the container by a
+ // mutation event listener when we need to do something more for it,
+ // we should stop handling this action since we cannot handle each
+ // edge case.
+ if (childAtPointToInsert &&
+ NS_WARN_IF(!childAtPointToInsert->IsInclusiveDescendantOf(
+ unwrappedSplitNodeResult.DidSplit()
+ ? unwrappedSplitNodeResult.GetNextContent()
+ : aPointToInsert.GetContainer()))) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ auto splitPoint = unwrappedSplitNodeResult.AtSplitPoint<EditorDOMPoint>();
+ if (aBRElementNextToSplitPoint == BRElementNextToSplitPoint::Delete) {
+ // Consume a trailing br, if any. This is to keep an alignment from
+ // creating extra lines, if possible.
+ if (nsCOMPtr<nsIContent> maybeBRContent = HTMLEditUtils::GetNextContent(
+ splitPoint,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayOutsideStyle, &aEditingHost)) {
+ if (maybeBRContent->IsHTMLElement(nsGkAtoms::br) &&
+ splitPoint.GetChild()) {
+ // Making use of html structure... if next node after where we are
+ // putting our div is not a block, then the br we found is in same
+ // block we are, so it's safe to consume it.
+ if (nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling(
+ *splitPoint.GetChild(),
+ {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (!HTMLEditUtils::IsBlockElement(
+ *nextEditableSibling,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(splitPoint);
+ nsresult rv = DeleteNodeWithTransaction(*maybeBRContent);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return Err(rv);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Result<CreateElementResult, nsresult> createNewElementResult =
+ CreateAndInsertElement(WithTransaction::Yes, aTagName, splitPoint,
+ aInitializer);
+ if (MOZ_UNLIKELY(createNewElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) failed");
+ return createNewElementResult;
+ }
+ MOZ_ASSERT_IF(wasCaretPositionSuggestedAtSplit,
+ createNewElementResult.inspect().HasCaretPointSuggestion());
+ MOZ_ASSERT(createNewElementResult.inspect().GetNewNode());
+
+ // If the new block element was moved to different element or removed by
+ // the web app via mutation event listener, we should stop handling this
+ // action since we cannot handle each of a lot of edge cases.
+ if (NS_WARN_IF(
+ createNewElementResult.inspect().GetNewNode()->GetParentNode() !=
+ splitPoint.GetContainer())) {
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ return createNewElementResult;
+}
+
+nsresult HTMLEditor::JoinNearestEditableNodesWithTransaction(
+ nsIContent& aNodeLeft, nsIContent& aNodeRight,
+ EditorDOMPoint* aNewFirstChildOfRightNode) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aNewFirstChildOfRightNode);
+
+ // Caller responsible for left and right node being the same type
+ if (NS_WARN_IF(!aNodeLeft.GetParentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+ // If they don't have the same parent, first move the right node to after
+ // the left one
+ if (aNodeLeft.GetParentNode() != aNodeRight.GetParentNode()) {
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeWithTransaction(aNodeRight, EditorDOMPoint(&aNodeLeft));
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveNodeResult.unwrapErr();
+ }
+ nsresult rv = moveNodeResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ }
+
+ // Separate join rules for differing blocks
+ if (HTMLEditUtils::IsAnyListElement(&aNodeLeft) || aNodeLeft.IsText()) {
+ // For lists, merge shallow (wouldn't want to combine list items)
+ Result<JoinNodesResult, nsresult> joinNodesResult =
+ JoinNodesWithTransaction(aNodeLeft, aNodeRight);
+ if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
+ NS_WARNING("HTMLEditor::JoinNodesWithTransaction failed");
+ return joinNodesResult.unwrapErr();
+ }
+ *aNewFirstChildOfRightNode =
+ joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>();
+ return NS_OK;
+ }
+
+ // Remember the last left child, and first right child
+ nsCOMPtr<nsIContent> lastEditableChildOfLeftContent =
+ HTMLEditUtils::GetLastChild(aNodeLeft,
+ {WalkTreeOption::IgnoreNonEditableNode});
+ if (MOZ_UNLIKELY(NS_WARN_IF(!lastEditableChildOfLeftContent))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIContent> firstEditableChildOfRightContent =
+ HTMLEditUtils::GetFirstChild(aNodeRight,
+ {WalkTreeOption::IgnoreNonEditableNode});
+ if (NS_WARN_IF(!firstEditableChildOfRightContent)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // For list items, divs, etc., merge smart
+ Result<JoinNodesResult, nsresult> joinNodesResult =
+ JoinNodesWithTransaction(aNodeLeft, aNodeRight);
+ if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
+ NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
+ return joinNodesResult.unwrapErr();
+ }
+
+ if ((lastEditableChildOfLeftContent->IsText() ||
+ lastEditableChildOfLeftContent->IsElement()) &&
+ HTMLEditUtils::CanContentsBeJoined(*lastEditableChildOfLeftContent,
+ *firstEditableChildOfRightContent)) {
+ nsresult rv = JoinNearestEditableNodesWithTransaction(
+ *lastEditableChildOfLeftContent, *firstEditableChildOfRightContent,
+ aNewFirstChildOfRightNode);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::JoinNearestEditableNodesWithTransaction() failed");
+ return rv;
+ }
+ *aNewFirstChildOfRightNode =
+ joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>();
+ return NS_OK;
+}
+
+Element* HTMLEditor::GetMostDistantAncestorMailCiteElement(
+ const nsINode& aNode) const {
+ Element* mailCiteElement = nullptr;
+ const bool isPlaintextEditor = IsPlaintextMailComposer();
+ for (Element* element : aNode.InclusiveAncestorsOfType<Element>()) {
+ if ((isPlaintextEditor && element->IsHTMLElement(nsGkAtoms::pre)) ||
+ HTMLEditUtils::IsMailCite(*element)) {
+ mailCiteElement = element;
+ continue;
+ }
+ if (element->IsHTMLElement(nsGkAtoms::body)) {
+ break;
+ }
+ }
+ return mailCiteElement;
+}
+
+nsresult HTMLEditor::CacheInlineStyles(Element& Element) {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ nsresult rv = GetInlineStyles(
+ Element, *TopLevelEditSubActionDataRef().mCachedPendingStyles);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::GetInlineStyles() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::GetInlineStyles(
+ Element& aElement, AutoPendingStyleCacheArray& aPendingStyleCacheArray) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aPendingStyleCacheArray.IsEmpty());
+
+ if (!IsCSSEnabled()) {
+ // In the HTML styling mode, we should preserve the order of inline styles
+ // specified with HTML elements, then, we can keep same order as original
+ // one when we create new elements to apply the styles at new place.
+ // XXX Currently, we don't preserve all inline parents, therefore, we cannot
+ // restore all inline elements as-is. Perhaps, we should store all
+ // inline elements with more details (e.g., all attributes), and store
+ // same elements. For example, web apps may give style as:
+ // em {
+ // font-style: italic;
+ // }
+ // em em {
+ // font-style: normal;
+ // font-weight: bold;
+ // }
+ // but we cannot restore the style as-is.
+ nsString value;
+ const bool givenElementIsEditable =
+ HTMLEditUtils::IsSimplyEditableNode(aElement);
+ auto NeedToAppend = [&](nsStaticAtom& aTagName, nsStaticAtom* aAttribute) {
+ if (mPendingStylesToApplyToNewContent->GetStyleState(
+ aTagName, aAttribute) != PendingStyleState::NotUpdated) {
+ return false; // The style has already been changed.
+ }
+ if (aPendingStyleCacheArray.Contains(aTagName, aAttribute)) {
+ return false; // Already preserved
+ }
+ return true;
+ };
+ for (Element* const inclusiveAncestor :
+ aElement.InclusiveAncestorsOfType<Element>()) {
+ if (HTMLEditUtils::IsBlockElement(
+ *inclusiveAncestor,
+ BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
+ (givenElementIsEditable &&
+ !HTMLEditUtils::IsSimplyEditableNode(*inclusiveAncestor))) {
+ break;
+ }
+ if (inclusiveAncestor->IsAnyOfHTMLElements(
+ nsGkAtoms::b, nsGkAtoms::i, nsGkAtoms::u, nsGkAtoms::s,
+ nsGkAtoms::strike, nsGkAtoms::tt, nsGkAtoms::em,
+ nsGkAtoms::strong, nsGkAtoms::dfn, nsGkAtoms::code,
+ nsGkAtoms::samp, nsGkAtoms::var, nsGkAtoms::cite, nsGkAtoms::abbr,
+ nsGkAtoms::acronym, nsGkAtoms::sub, nsGkAtoms::sup)) {
+ nsStaticAtom& tagName = const_cast<nsStaticAtom&>(
+ *inclusiveAncestor->NodeInfo()->NameAtom()->AsStatic());
+ if (NeedToAppend(tagName, nullptr)) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(tagName, nullptr, EmptyString()));
+ }
+ continue;
+ }
+ if (inclusiveAncestor->IsHTMLElement(nsGkAtoms::font)) {
+ if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::face)) {
+ inclusiveAncestor->GetAttr(nsGkAtoms::face, value);
+ if (!value.IsEmpty()) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::face, value));
+ value.Truncate();
+ }
+ }
+ if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::size)) {
+ inclusiveAncestor->GetAttr(nsGkAtoms::size, value);
+ if (!value.IsEmpty()) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::size, value));
+ value.Truncate();
+ }
+ }
+ if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::color)) {
+ inclusiveAncestor->GetAttr(nsGkAtoms::color, value);
+ if (!value.IsEmpty()) {
+ aPendingStyleCacheArray.AppendElement(
+ PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::color, value));
+ value.Truncate();
+ }
+ }
+ continue;
+ }
+ }
+ return NS_OK;
+ }
+
+ for (nsStaticAtom* property : {nsGkAtoms::b,
+ nsGkAtoms::i,
+ nsGkAtoms::u,
+ nsGkAtoms::s,
+ nsGkAtoms::strike,
+ nsGkAtoms::face,
+ nsGkAtoms::size,
+ nsGkAtoms::color,
+ nsGkAtoms::tt,
+ nsGkAtoms::em,
+ nsGkAtoms::strong,
+ nsGkAtoms::dfn,
+ nsGkAtoms::code,
+ nsGkAtoms::samp,
+ nsGkAtoms::var,
+ nsGkAtoms::cite,
+ nsGkAtoms::abbr,
+ nsGkAtoms::acronym,
+ nsGkAtoms::backgroundColor,
+ nsGkAtoms::sub,
+ nsGkAtoms::sup}) {
+ const EditorInlineStyle style =
+ property == nsGkAtoms::face || property == nsGkAtoms::size ||
+ property == nsGkAtoms::color
+ ? EditorInlineStyle(*nsGkAtoms::font, property)
+ : EditorInlineStyle(*property);
+ // If type-in state is set, don't intervene
+ const PendingStyleState styleState =
+ mPendingStylesToApplyToNewContent->GetStyleState(*style.mHTMLProperty,
+ style.mAttribute);
+ if (styleState != PendingStyleState::NotUpdated) {
+ continue;
+ }
+ bool isSet = false;
+ nsString value; // Don't use nsAutoString here because it requires memcpy
+ // at creating new PendingStyleCache instance.
+ // Don't use CSS for <font size>, we don't support it usefully (bug 780035)
+ if (property == nsGkAtoms::size) {
+ isSet = HTMLEditUtils::IsInlineStyleSetByElement(aElement, style, nullptr,
+ &value);
+ } else if (style.IsCSSSettable(aElement)) {
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(*this, aElement, style,
+ value);
+ if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
+ NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError.unwrapErr();
+ }
+ isSet = isComputedCSSEquivalentToStyleOrError.unwrap();
+ }
+ if (isSet) {
+ aPendingStyleCacheArray.AppendElement(
+ style.ToPendingStyleCache(std::move(value)));
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::ReapplyCachedStyles() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+
+ // The idea here is to examine our cached list of styles and see if any have
+ // been removed. If so, add typeinstate for them, so that they will be
+ // reinserted when new content is added.
+
+ if (TopLevelEditSubActionDataRef().mCachedPendingStyles->IsEmpty() ||
+ !SelectionRef().RangeCount()) {
+ return NS_OK;
+ }
+
+ // remember if we are in css mode
+ const bool useCSS = IsCSSEnabled();
+
+ const RangeBoundary& atStartOfSelection =
+ SelectionRef().GetRangeAt(0)->StartRef();
+ const RefPtr<Element> startContainerElement =
+ atStartOfSelection.Container() &&
+ atStartOfSelection.Container()->IsContent()
+ ? atStartOfSelection.Container()->GetAsElementOrParentElement()
+ : nullptr;
+ if (NS_WARN_IF(!startContainerElement)) {
+ return NS_OK;
+ }
+
+ AutoPendingStyleCacheArray styleCacheArrayAtInsertionPoint;
+ nsresult rv =
+ GetInlineStyles(*startContainerElement, styleCacheArrayAtInsertionPoint);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetInlineStyles() failed, but ignored");
+ return NS_OK;
+ }
+
+ for (PendingStyleCache& styleCacheBeforeEdit :
+ Reversed(*TopLevelEditSubActionDataRef().mCachedPendingStyles)) {
+ bool isFirst = false, isAny = false, isAll = false;
+ nsAutoString currentValue;
+ const EditorInlineStyle inlineStyle = styleCacheBeforeEdit.ToInlineStyle();
+ if (useCSS && inlineStyle.IsCSSSettable(*startContainerElement)) {
+ // check computed style first in css case
+ // MOZ_KnownLive(styleCacheBeforeEdit.*) because they are nsStaticAtom
+ // and its instances are alive until shutting down.
+ Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
+ CSSEditUtils::IsComputedCSSEquivalentTo(*this, *startContainerElement,
+ inlineStyle, currentValue);
+ if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
+ NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
+ return isComputedCSSEquivalentToStyleOrError.unwrapErr();
+ }
+ isAny = isComputedCSSEquivalentToStyleOrError.unwrap();
+ }
+ if (!isAny) {
+ // then check typeinstate and html style
+ nsresult rv = GetInlinePropertyBase(
+ inlineStyle, &styleCacheBeforeEdit.AttributeValueOrCSSValueRef(),
+ &isFirst, &isAny, &isAll, &currentValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::GetInlinePropertyBase() failed");
+ return rv;
+ }
+ }
+ // This style has disappeared through deletion. Let's add the styles to
+ // mPendingStylesToApplyToNewContent when same style isn't applied to the
+ // node already.
+ if (isAny &&
+ !IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) {
+ continue;
+ }
+ AutoPendingStyleCacheArray::index_type index =
+ styleCacheArrayAtInsertionPoint.IndexOf(
+ styleCacheBeforeEdit.TagRef(), styleCacheBeforeEdit.GetAttribute());
+ if (index == AutoPendingStyleCacheArray::NoIndex ||
+ styleCacheBeforeEdit.AttributeValueOrCSSValueRef() !=
+ styleCacheArrayAtInsertionPoint.ElementAt(index)
+ .AttributeValueOrCSSValueRef()) {
+ mPendingStylesToApplyToNewContent->PreserveStyle(styleCacheBeforeEdit);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange(
+ const RawRangeBoundary& aStartRef, const RawRangeBoundary& aEndRef) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoTArray<OwningNonNull<Element>, 64> arrayOfEmptyElements;
+ DOMIterator iter;
+ if (NS_FAILED(iter.Init(aStartRef, aEndRef))) {
+ NS_WARNING("DOMIterator::Init() failed");
+ return NS_ERROR_FAILURE;
+ }
+ iter.AppendNodesToArray(
+ +[](nsINode& aNode, void* aSelf) {
+ MOZ_ASSERT(Element::FromNode(&aNode));
+ MOZ_ASSERT(aSelf);
+ Element* element = aNode.AsElement();
+ if (!EditorUtils::IsEditableContent(*element, EditorType::HTML) ||
+ (!HTMLEditUtils::IsListItem(element) &&
+ !HTMLEditUtils::IsTableCellOrCaption(*element))) {
+ return false;
+ }
+ return HTMLEditUtils::IsEmptyNode(
+ *element, {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible});
+ },
+ arrayOfEmptyElements, this);
+
+ // Put padding <br> elements for empty <li> and <td>.
+ EditorDOMPoint pointToPutCaret;
+ for (auto& emptyElement : arrayOfEmptyElements) {
+ // Need to put br at END of node. It may have empty containers in it and
+ // still pass the "IsEmptyNode" test, and we want the br's to be after
+ // them. Also, we want the br to be after the selection if the selection
+ // is in this node.
+ EditorDOMPoint endOfNode(EditorDOMPoint::AtEndOf(emptyElement));
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(endOfNode);
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
+ "failed");
+ return insertPaddingBRElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInsertPaddingBRElementResult =
+ insertPaddingBRElementResult.unwrap();
+ unwrappedInsertPaddingBRElementResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+ return NS_OK;
+}
+
+void HTMLEditor::SetSelectionInterlinePosition() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(SelectionRef().IsCollapsed());
+
+ // Get the (collapsed) selection location
+ const nsRange* firstRange = SelectionRef().GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return;
+ }
+
+ EditorDOMPoint atCaret(firstRange->StartRef());
+ if (NS_WARN_IF(!atCaret.IsSet())) {
+ return;
+ }
+ MOZ_ASSERT(atCaret.IsSetAndValid());
+
+ // First, let's check to see if we are after a `<br>`. We take care of this
+ // special-case first so that we don't accidentally fall through into one of
+ // the other conditionals.
+ // XXX Although I don't understand "interline position", if caret is
+ // immediately after non-editable contents, but previous editable
+ // content is `<br>`, does this do right thing?
+ if (Element* editingHost = ComputeEditingHost()) {
+ if (nsIContent* previousEditableContentInBlock =
+ HTMLEditUtils::GetPreviousContent(
+ atCaret,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
+ if (previousEditableContentInBlock->IsHTMLElement(nsGkAtoms::br)) {
+ DebugOnly<nsresult> rvIgnored = SelectionRef().SetInterlinePosition(
+ InterlinePosition::StartOfNextLine);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "StartOfNextLine) failed, but ignored");
+ return;
+ }
+ }
+ }
+
+ if (!atCaret.GetChild()) {
+ return;
+ }
+
+ // If caret is immediately after a block, set interline position to "right".
+ // XXX Although I don't understand "interline position", if caret is
+ // immediately after non-editable contents, but previous editable
+ // content is a block, does this do right thing?
+ if (nsIContent* previousEditableContentInBlockAtCaret =
+ HTMLEditUtils::GetPreviousSibling(
+ *atCaret.GetChild(), {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (HTMLEditUtils::IsBlockElement(
+ *previousEditableContentInBlockAtCaret,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ DebugOnly<nsresult> rvIgnored = SelectionRef().SetInterlinePosition(
+ InterlinePosition::StartOfNextLine);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "StartOfNextLine) failed, but ignored");
+ return;
+ }
+ }
+
+ // If caret is immediately before a block, set interline position to "left".
+ // XXX Although I don't understand "interline position", if caret is
+ // immediately before non-editable contents, but next editable
+ // content is a block, does this do right thing?
+ if (nsIContent* nextEditableContentInBlockAtCaret =
+ HTMLEditUtils::GetNextSibling(
+ *atCaret.GetChild(), {WalkTreeOption::IgnoreNonEditableNode})) {
+ if (HTMLEditUtils::IsBlockElement(
+ *nextEditableContentInBlockAtCaret,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ DebugOnly<nsresult> rvIgnored =
+ SelectionRef().SetInterlinePosition(InterlinePosition::EndOfLine);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "EndOfLine) failed, but ignored");
+ }
+ }
+}
+
+nsresult HTMLEditor::AdjustCaretPositionAndEnsurePaddingBRElement(
+ nsIEditor::EDirection aDirectionAndAmount) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(SelectionRef().IsCollapsed());
+
+ auto point = GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!point.IsInContentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If selection start is not editable, climb up the tree until editable one.
+ while (!EditorUtils::IsEditableContent(*point.ContainerAs<nsIContent>(),
+ EditorType::HTML)) {
+ point.Set(point.GetContainer());
+ if (NS_WARN_IF(!point.IsInContentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // If caret is in empty block element, we need to insert a `<br>` element
+ // because the block should have one-line height.
+ // XXX Even if only a part of the block is editable, shouldn't we put
+ // caret if the block element is now empty?
+ if (Element* const editableBlockElement =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *point.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestEditableBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle)) {
+ if (editableBlockElement &&
+ HTMLEditUtils::IsEmptyNode(
+ *editableBlockElement,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible}) &&
+ HTMLEditUtils::CanNodeContain(*point.GetContainer(), *nsGkAtoms::br)) {
+ Element* bodyOrDocumentElement = GetRoot();
+ if (NS_WARN_IF(!bodyOrDocumentElement)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (point.GetContainer() == bodyOrDocumentElement) {
+ // Our root node is completely empty. Don't add a <br> here.
+ // AfterEditInner() will add one for us when it calls
+ // EditorBase::MaybeCreatePaddingBRElementForEmptyEditor().
+ // XXX This kind of dependency between methods makes us spaghetti.
+ // Let's handle it here later.
+ // XXX This looks odd check. If active editing host is not a
+ // `<body>`, what are we doing?
+ return NS_OK;
+ }
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(point);
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction("
+ ") failed");
+ return insertPaddingBRElementResult.unwrapErr();
+ }
+ nsresult rv = insertPaddingBRElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ return NS_OK;
+ }
+ }
+
+ // XXX Perhaps, we should do something if we're in a data node but not
+ // a text node.
+ if (point.IsInTextNode()) {
+ return NS_OK;
+ }
+
+ // Do we need to insert a padding <br> element for empty last line? We do
+ // if we are:
+ // 1) prior node is in same block where selection is AND
+ // 2) prior node is a br AND
+ // 3) that br is not visible
+ RefPtr<Element> editingHost = ComputeEditingHost();
+ if (!editingHost) {
+ return NS_OK;
+ }
+
+ if (nsCOMPtr<nsIContent> previousEditableContent =
+ HTMLEditUtils::GetPreviousContent(
+ point, {WalkTreeOption::IgnoreNonEditableNode},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
+ // If caret and previous editable content are in same block element
+ // (even if it's a non-editable element), we should put a padding <br>
+ // element at end of the block.
+ const Element* const blockElementContainingCaret =
+ HTMLEditUtils::GetInclusiveAncestorElement(
+ *point.ContainerAs<nsIContent>(),
+ HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ const Element* const blockElementContainingPreviousEditableContent =
+ HTMLEditUtils::GetAncestorElement(
+ *previousEditableContent, HTMLEditUtils::ClosestBlockElement,
+ BlockInlineCheck::UseComputedDisplayStyle);
+ // If previous editable content of caret is in same block and a `<br>`
+ // element, we need to adjust interline position.
+ if (blockElementContainingCaret &&
+ blockElementContainingCaret ==
+ blockElementContainingPreviousEditableContent &&
+ point.ContainerAs<nsIContent>()->GetEditingHost() ==
+ previousEditableContent->GetEditingHost() &&
+ previousEditableContent &&
+ previousEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ // If it's an invisible `<br>` element, we need to insert a padding
+ // `<br>` element for making empty line have one-line height.
+ if (HTMLEditUtils::IsInvisibleBRElement(*previousEditableContent) &&
+ !EditorUtils::IsPaddingBRElementForEmptyLastLine(
+ *previousEditableContent)) {
+ AutoEditorDOMPointChildInvalidator lockOffset(point);
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(point);
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertPaddingBRElementForEmptyLastLineWithTransaction() failed");
+ return insertPaddingBRElementResult.unwrapErr();
+ }
+ insertPaddingBRElementResult.inspect().IgnoreCaretPointSuggestion();
+ nsresult rv = CollapseSelectionTo(EditorRawDOMPoint(
+ insertPaddingBRElementResult.inspect().GetNewNode(),
+ InterlinePosition::StartOfNextLine));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ }
+ // If it's a visible `<br>` element and next editable content is a
+ // padding `<br>` element, we need to set interline position.
+ else if (nsIContent* nextEditableContentInBlock =
+ HTMLEditUtils::GetNextContent(
+ *previousEditableContent,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle,
+ editingHost)) {
+ if (EditorUtils::IsPaddingBRElementForEmptyLastLine(
+ *nextEditableContentInBlock)) {
+ // Make it stick to the padding `<br>` element so that it will be
+ // on blank line.
+ DebugOnly<nsresult> rvIgnored = SelectionRef().SetInterlinePosition(
+ InterlinePosition::StartOfNextLine);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Selection::SetInterlinePosition(InterlinePosition::"
+ "StartOfNextLine) failed, but ignored");
+ }
+ }
+ }
+ }
+
+ // If previous editable content in same block is `<br>`, text node, `<img>`
+ // or `<hr>`, current caret position is fine.
+ if (nsIContent* previousEditableContentInBlock =
+ HTMLEditUtils::GetPreviousContent(
+ point,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
+ if (previousEditableContentInBlock->IsHTMLElement(nsGkAtoms::br) ||
+ previousEditableContentInBlock->IsText() ||
+ HTMLEditUtils::IsImage(previousEditableContentInBlock) ||
+ previousEditableContentInBlock->IsHTMLElement(nsGkAtoms::hr)) {
+ return NS_OK;
+ }
+ }
+
+ // If next editable content in same block is `<br>`, text node, `<img>` or
+ // `<hr>`, current caret position is fine.
+ if (nsIContent* nextEditableContentInBlock = HTMLEditUtils::GetNextContent(
+ point,
+ {WalkTreeOption::IgnoreNonEditableNode,
+ WalkTreeOption::StopAtBlockBoundary},
+ BlockInlineCheck::UseComputedDisplayStyle, editingHost)) {
+ if (nextEditableContentInBlock->IsText() ||
+ nextEditableContentInBlock->IsAnyOfHTMLElements(
+ nsGkAtoms::br, nsGkAtoms::img, nsGkAtoms::hr)) {
+ return NS_OK;
+ }
+ }
+
+ // Otherwise, look for a near editable content towards edit action direction.
+
+ // If there is no editable content, keep current caret position.
+ // XXX Why do we treat `nsIEditor::ePreviousWord` etc as forward direction?
+ nsIContent* nearEditableContent = HTMLEditUtils::GetAdjacentContentToPutCaret(
+ point,
+ aDirectionAndAmount == nsIEditor::ePrevious ? WalkTreeDirection::Backward
+ : WalkTreeDirection::Forward,
+ *editingHost);
+ if (!nearEditableContent) {
+ return NS_OK;
+ }
+
+ EditorRawDOMPoint pointToPutCaret =
+ HTMLEditUtils::GetGoodCaretPointFor<EditorRawDOMPoint>(
+ *nearEditableContent, aDirectionAndAmount);
+ if (!pointToPutCaret.IsSet()) {
+ NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed");
+ return NS_ERROR_FAILURE;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::RemoveEmptyNodesIn(const EditorDOMRange& aRange) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aRange.IsPositioned());
+
+ // Some general notes on the algorithm used here: the goal is to examine all
+ // the nodes in aRange, and remove the empty ones. We do this by
+ // using a content iterator to traverse all the nodes in the range, and
+ // placing the empty nodes into an array. After finishing the iteration,
+ // we delete the empty nodes in the array. (They cannot be deleted as we
+ // find them because that would invalidate the iterator.)
+ //
+ // Since checking to see if a node is empty can be costly for nodes with
+ // many descendants, there are some optimizations made. I rely on the fact
+ // that the iterator is post-order: it will visit children of a node before
+ // visiting the parent node. So if I find that a child node is not empty, I
+ // know that its parent is not empty without even checking. So I put the
+ // parent on a "skipList" which is just a voidArray of nodes I can skip the
+ // empty check on. If I encounter a node on the skiplist, i skip the
+ // processing for that node and replace its slot in the skiplist with that
+ // node's parent.
+ //
+ // An interesting idea is to go ahead and regard parent nodes that are NOT
+ // on the skiplist as being empty (without even doing the IsEmptyNode check)
+ // on the theory that if they weren't empty, we would have encountered a
+ // non-empty child earlier and thus put this parent node on the skiplist.
+ //
+ // Unfortunately I can't use that strategy here, because the range may
+ // include some children of a node while excluding others. Thus I could
+ // find all the _examined_ children empty, but still not have an empty
+ // parent.
+
+ const RawRangeBoundary endOfRange = [&]() {
+ // If the range is not collapsed and end of the range is start of a
+ // container, it means that the inclusive ancestor empty element may be
+ // created by splitting the left nodes.
+ if (aRange.Collapsed() || !aRange.IsInContentNodes() ||
+ !aRange.EndRef().IsStartOfContainer()) {
+ return aRange.EndRef().ToRawRangeBoundary();
+ }
+ nsINode* const commonAncestor =
+ nsContentUtils::GetClosestCommonInclusiveAncestor(
+ aRange.StartRef().ContainerAs<nsIContent>(),
+ aRange.EndRef().ContainerAs<nsIContent>());
+ if (!commonAncestor) {
+ return aRange.EndRef().ToRawRangeBoundary();
+ }
+ nsIContent* maybeRightContent = nullptr;
+ for (nsIContent* content : aRange.EndRef()
+ .ContainerAs<nsIContent>()
+ ->InclusiveAncestorsOfType<nsIContent>()) {
+ if (!HTMLEditUtils::IsSimplyEditableNode(*content) ||
+ content == commonAncestor) {
+ break;
+ }
+ if (aRange.StartRef().ContainerAs<nsIContent>() == content) {
+ break;
+ }
+ EmptyCheckOptions options = {
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible};
+ if (!HTMLEditUtils::IsBlockElement(
+ *content, BlockInlineCheck::UseComputedDisplayStyle)) {
+ options += EmptyCheckOption::TreatSingleBRElementAsVisible;
+ }
+ if (!HTMLEditUtils::IsEmptyNode(*content, options)) {
+ break;
+ }
+ maybeRightContent = content;
+ }
+ if (!maybeRightContent) {
+ return aRange.EndRef().ToRawRangeBoundary();
+ }
+ return EditorRawDOMPoint::After(*maybeRightContent).ToRawRangeBoundary();
+ }();
+
+ PostContentIterator postOrderIter;
+ nsresult rv =
+ postOrderIter.Init(aRange.StartRef().ToRawRangeBoundary(), endOfRange);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("PostContentIterator::Init() failed");
+ return rv;
+ }
+
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfEmptyContents,
+ arrayOfEmptyCites;
+
+ // Collect empty nodes first.
+ {
+ const bool isMailEditor = IsMailEditor();
+ AutoTArray<OwningNonNull<nsIContent>, 64> knownNonEmptyContents;
+ Maybe<AutoRangeArray> maybeSelectionRanges;
+ for (; !postOrderIter.IsDone(); postOrderIter.Next()) {
+ MOZ_ASSERT(postOrderIter.GetCurrentNode()->IsContent());
+
+ nsIContent* content = postOrderIter.GetCurrentNode()->AsContent();
+ nsIContent* parentContent = content->GetParent();
+
+ size_t idx = knownNonEmptyContents.IndexOf(content);
+ if (idx != decltype(knownNonEmptyContents)::NoIndex) {
+ // This node is on our skip list. Skip processing for this node, and
+ // replace its value in the skip list with the value of its parent
+ if (parentContent) {
+ knownNonEmptyContents[idx] = parentContent;
+ }
+ continue;
+ }
+
+ const bool isEmptyNode = [&]() {
+ if (!content->IsElement()) {
+ return false;
+ }
+ const bool isMailCite =
+ isMailEditor && HTMLEditUtils::IsMailCite(*content->AsElement());
+ const bool isCandidate = [&]() {
+ if (content->IsHTMLElement(nsGkAtoms::body)) {
+ // Don't delete the body
+ return false;
+ }
+ if (isMailCite || content->IsHTMLElement(nsGkAtoms::a) ||
+ HTMLEditUtils::IsInlineStyle(content) ||
+ HTMLEditUtils::IsAnyListElement(content) ||
+ content->IsHTMLElement(nsGkAtoms::div)) {
+ // Only consider certain nodes to be empty for purposes of removal
+ return true;
+ }
+ if (HTMLEditUtils::IsFormatElementForFormatBlockCommand(*content) ||
+ HTMLEditUtils::IsListItem(content) ||
+ content->IsHTMLElement(nsGkAtoms::blockquote)) {
+ // These node types are candidates if selection is not in them. If
+ // it is one of these, don't delete if selection inside. This is so
+ // we can create empty headings, etc., for the user to type into.
+ if (maybeSelectionRanges.isNothing()) {
+ maybeSelectionRanges.emplace(SelectionRef());
+ }
+ return !maybeSelectionRanges
+ ->IsAtLeastOneContainerOfRangeBoundariesInclusiveDescendantOf(
+ *content);
+ }
+ return false;
+ }();
+
+ if (!isCandidate) {
+ return false;
+ }
+
+ // We delete mailcites even if they have a solo br in them. Other
+ // nodes we require to be empty.
+ HTMLEditUtils::EmptyCheckOptions options{
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible};
+ if (!isMailCite) {
+ options += EmptyCheckOption::TreatSingleBRElementAsVisible;
+ } else {
+ // XXX Maybe unnecessary to specify this.
+ options += EmptyCheckOption::TreatNonEditableContentAsInvisible;
+ }
+ if (!HTMLEditUtils::IsEmptyNode(*content, options)) {
+ return false;
+ }
+
+ if (isMailCite) {
+ // mailcites go on a separate list from other empty nodes
+ arrayOfEmptyCites.AppendElement(*content);
+ }
+ // Don't delete non-editable nodes in this method because this is a
+ // clean up method to remove unnecessary nodes of the result of
+ // editing. So, we shouldn't delete non-editable nodes which were
+ // there before editing. Additionally, if the element is some special
+ // elements such as <body>, we shouldn't delete it.
+ else if (HTMLEditUtils::IsSimplyEditableNode(*content) &&
+ HTMLEditUtils::IsRemovableNode(*content)) {
+ arrayOfEmptyContents.AppendElement(*content);
+ }
+ return true;
+ }();
+ if (!isEmptyNode && parentContent) {
+ knownNonEmptyContents.AppendElement(*parentContent);
+ }
+ } // end of the for-loop iterating with postOrderIter
+ }
+
+ // now delete the empty nodes
+ for (OwningNonNull<nsIContent>& emptyContent : arrayOfEmptyContents) {
+ // MOZ_KnownLive due to bug 1622253
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(emptyContent));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+
+ // Now delete the empty mailcites. This is a separate step because we want
+ // to pull out any br's and preserve them.
+ EditorDOMPoint pointToPutCaret;
+ for (OwningNonNull<nsIContent>& emptyCite : arrayOfEmptyCites) {
+ if (!HTMLEditUtils::IsEmptyNode(
+ emptyCite,
+ {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatListItemAsVisible,
+ EmptyCheckOption::TreatTableCellAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ // We are deleting a cite that has just a `<br>`. We want to delete cite,
+ // but preserve `<br>`.
+ Result<CreateElementResult, nsresult> insertBRElementResult =
+ InsertBRElement(WithTransaction::Yes, EditorDOMPoint(emptyCite));
+ if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedInsertBRElementResult =
+ insertBRElementResult.unwrap();
+ // XXX Is this intentional selection change?
+ unwrappedInsertBRElementResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode());
+ }
+ // MOZ_KnownLive because 'arrayOfEmptyCites' is guaranteed to keep it alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(emptyCite));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+ // XXX Is this intentional selection change?
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionTo() caused destroying the editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::LiftUpListItemElement(
+ Element& aListItemElement,
+ LiftUpFromAllParentListElements aLiftUpFromAllParentListElements) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!HTMLEditUtils::IsListItem(&aListItemElement)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!aListItemElement.GetParentElement()) ||
+ NS_WARN_IF(!aListItemElement.GetParentElement()->GetParentNode())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // if it's first or last list item, don't need to split the list
+ // otherwise we do.
+ const bool isFirstListItem = HTMLEditUtils::IsFirstChild(
+ aListItemElement, {WalkTreeOption::IgnoreNonEditableNode});
+ const bool isLastListItem = HTMLEditUtils::IsLastChild(
+ aListItemElement, {WalkTreeOption::IgnoreNonEditableNode});
+
+ Element* leftListElement = aListItemElement.GetParentElement();
+ if (NS_WARN_IF(!leftListElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If it's at middle of parent list element, split the parent list element.
+ // Then, aListItem becomes the first list item of the right list element.
+ if (!isFirstListItem && !isLastListItem) {
+ EditorDOMPoint atListItemElement(&aListItemElement);
+ if (NS_WARN_IF(!atListItemElement.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(atListItemElement.IsSetAndValid());
+ Result<SplitNodeResult, nsresult> splitListItemParentResult =
+ SplitNodeWithTransaction(atListItemElement);
+ if (MOZ_UNLIKELY(splitListItemParentResult.isErr())) {
+ NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
+ return splitListItemParentResult.unwrapErr();
+ }
+ nsresult rv = splitListItemParentResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("SplitNodeResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+
+ leftListElement =
+ splitListItemParentResult.inspect().GetPreviousContentAs<Element>();
+ if (MOZ_UNLIKELY(!leftListElement)) {
+ NS_WARNING(
+ "HTMLEditor::SplitNodeWithTransaction() didn't return left list "
+ "element");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // In most cases, insert the list item into the new left list node..
+ EditorDOMPoint pointToInsertListItem(leftListElement);
+ if (NS_WARN_IF(!pointToInsertListItem.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // But when the list item was the first child of the right list, it should
+ // be inserted between the both list elements. This allows user to hit
+ // Enter twice at a list item breaks the parent list node.
+ if (!isFirstListItem) {
+ DebugOnly<bool> advanced = pointToInsertListItem.AdvanceOffset();
+ NS_WARNING_ASSERTION(advanced,
+ "Failed to advance offset to right list node");
+ }
+
+ EditorDOMPoint pointToPutCaret;
+ {
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ MoveNodeWithTransaction(aListItemElement, pointToInsertListItem);
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
+ return moveListItemElementResult.unwrapErr();
+ }
+ MoveNodeResult unwrappedMoveListItemElementResult =
+ moveListItemElementResult.unwrap();
+ unwrappedMoveListItemElementResult.MoveCaretPointTo(
+ pointToPutCaret, *this,
+ {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ }
+
+ // Unwrap list item contents if they are no longer in a list
+ // XXX If the parent list element is a child of another list element
+ // (although invalid tree), the list item element won't be unwrapped.
+ // That makes the parent ancestor element tree valid, but might be
+ // unexpected result.
+ // XXX If aListItemElement is <dl> or <dd> and current parent is <ul> or <ol>,
+ // the list items won't be unwrapped. If aListItemElement is <li> and its
+ // current parent is <dl>, there is same issue.
+ if (!HTMLEditUtils::IsAnyListElement(pointToInsertListItem.GetContainer()) &&
+ HTMLEditUtils::IsListItem(&aListItemElement)) {
+ Result<EditorDOMPoint, nsresult> unwrapOrphanListItemElementResult =
+ RemoveBlockContainerWithTransaction(aListItemElement);
+ if (MOZ_UNLIKELY(unwrapOrphanListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapOrphanListItemElementResult.unwrapErr();
+ }
+ if (AllowsTransactionsToChangeSelection() &&
+ unwrapOrphanListItemElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapOrphanListItemElementResult.unwrap();
+ }
+ if (!pointToPutCaret.IsSet()) {
+ return NS_OK;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+
+ if (pointToPutCaret.IsSet()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ }
+
+ if (aLiftUpFromAllParentListElements == LiftUpFromAllParentListElements::No) {
+ return NS_OK;
+ }
+ // XXX If aListItemElement is moved to unexpected element by mutation event
+ // listener, shouldn't we stop calling this?
+ nsresult rv = LiftUpListItemElement(aListItemElement,
+ LiftUpFromAllParentListElements::Yes);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::LiftUpListItemElement("
+ "LiftUpFromAllParentListElements::Yes) failed");
+ return rv;
+}
+
+nsresult HTMLEditor::DestroyListStructureRecursively(Element& aListElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement));
+
+ // XXX If mutation event listener inserts new child into `aListElement`,
+ // this becomes infinite loop so that we should set limit of the
+ // loop count from original child count.
+ while (aListElement.GetFirstChild()) {
+ OwningNonNull<nsIContent> child = *aListElement.GetFirstChild();
+
+ if (HTMLEditUtils::IsListItem(child)) {
+ // XXX Using LiftUpListItemElement() is too expensive for this purpose.
+ // Looks like the reason why this method uses it is, only this loop
+ // wants to work with first child of aListElement. However, what it
+ // actually does is removing <li> as container. Perhaps, we should
+ // decide destination first, and then, move contents in `child`.
+ // XXX If aListElement is is a child of another list element (although
+ // it's invalid tree), this moves the list item to outside of
+ // aListElement's parent. Is that really intentional behavior?
+ nsresult rv = LiftUpListItemElement(
+ MOZ_KnownLive(*child->AsElement()),
+ HTMLEditor::LiftUpFromAllParentListElements::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::LiftUpListItemElement(LiftUpFromAllParentListElements:"
+ ":Yes) failed");
+ return rv;
+ }
+ continue;
+ }
+
+ if (HTMLEditUtils::IsAnyListElement(child)) {
+ nsresult rv =
+ DestroyListStructureRecursively(MOZ_KnownLive(*child->AsElement()));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::DestroyListStructureRecursively() failed");
+ return rv;
+ }
+ continue;
+ }
+
+ // Delete any non-list items for now
+ // XXX This is not HTML5 aware. HTML5 allows all list elements to have
+ // <script> and <template> and <dl> element to have <div> to group
+ // some <dt> and <dd> elements. So, this may break valid children.
+ nsresult rv = DeleteNodeWithTransaction(*child);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+
+ // Delete the now-empty list
+ const Result<EditorDOMPoint, nsresult> unwrapListElementResult =
+ RemoveBlockContainerWithTransaction(aListElement);
+ if (MOZ_UNLIKELY(unwrapListElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveBlockContainerWithTransaction() failed");
+ return unwrapListElementResult.inspectErr();
+ }
+ const EditorDOMPoint& pointToPutCaret = unwrapListElementResult.inspect();
+ if (!AllowsTransactionsToChangeSelection() || !pointToPutCaret.IsSet()) {
+ return NS_OK;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed");
+ return rv;
+}
+
+nsresult HTMLEditor::EnsureSelectionInBodyOrDocumentElement() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Element> bodyOrDocumentElement = GetRoot();
+ if (NS_WARN_IF(!bodyOrDocumentElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const auto atCaret = GetFirstSelectionStartPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!atCaret.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX This does wrong things. Web apps can put any elements as sibling
+ // of `<body>` element. Therefore, this collapses `Selection` into
+ // the `<body>` element which `HTMLDocument.body` is set to. So,
+ // this makes users impossible to modify content outside of the
+ // `<body>` element even if caret is in an editing host.
+
+ // Check that selection start container is inside the <body> element.
+ // XXXsmaug this code is insane.
+ nsINode* temp = atCaret.GetContainer();
+ while (temp && !temp->IsHTMLElement(nsGkAtoms::body)) {
+ temp = temp->GetParentOrShadowHostNode();
+ }
+
+ // If we aren't in the <body> element, force the issue.
+ if (!temp) {
+ nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionToStartOf() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+ return NS_OK;
+ }
+
+ const auto selectionEndPoint = GetFirstSelectionEndPoint<EditorRawDOMPoint>();
+ if (NS_WARN_IF(!selectionEndPoint.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // check that selNode is inside body
+ // XXXsmaug this code is insane.
+ temp = selectionEndPoint.GetContainer();
+ while (temp && !temp->IsHTMLElement(nsGkAtoms::body)) {
+ temp = temp->GetParentOrShadowHostNode();
+ }
+
+ // If we aren't in the <body> element, force the issue.
+ if (!temp) {
+ nsresult rv = CollapseSelectionToStartOf(*bodyOrDocumentElement);
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING(
+ "EditorBase::CollapseSelectionToStartOf() caused destroying the "
+ "editor");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed, but ignored");
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLEditor::InsertPaddingBRElementForEmptyLastLineIfNeeded(
+ Element& aElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!HTMLEditUtils::IsBlockElement(
+ aElement, BlockInlineCheck::UseComputedDisplayStyle)) {
+ return NS_OK;
+ }
+
+ if (!HTMLEditUtils::IsEmptyNode(
+ aElement, {EmptyCheckOption::TreatSingleBRElementAsVisible,
+ EmptyCheckOption::TreatNonEditableContentAsInvisible})) {
+ return NS_OK;
+ }
+
+ Result<CreateElementResult, nsresult> insertPaddingBRElementResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ EditorDOMPoint(&aElement, 0u));
+ if (MOZ_UNLIKELY(insertPaddingBRElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
+ "failed");
+ return insertPaddingBRElementResult.unwrapErr();
+ }
+ nsresult rv = insertPaddingBRElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ return NS_OK;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::RemoveAlignFromDescendants(
+ Element& aElement, const nsAString& aAlignType, EditTarget aEditTarget) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!aElement.IsHTMLElement(nsGkAtoms::table));
+
+ const bool useCSS = IsCSSEnabled();
+
+ EditorDOMPoint pointToPutCaret;
+
+ // Let's remove all alignment hints in the children of aNode; it can
+ // be an ALIGN attribute (in case we just remove it) or a CENTER
+ // element (here we have to remove the container and keep its
+ // children). We break on tables and don't look at their children.
+ nsCOMPtr<nsIContent> nextSibling;
+ for (nsIContent* content =
+ aEditTarget == EditTarget::NodeAndDescendantsExceptTable
+ ? &aElement
+ : aElement.GetFirstChild();
+ content; content = nextSibling) {
+ // Get the next sibling before removing content from the DOM tree.
+ // XXX If next sibling is removed from the parent and/or inserted to
+ // different parent, we will behave unexpectedly. I think that
+ // we should create child list and handle it with checking whether
+ // it's still a child of expected parent.
+ nextSibling = aEditTarget == EditTarget::NodeAndDescendantsExceptTable
+ ? nullptr
+ : content->GetNextSibling();
+
+ if (content->IsHTMLElement(nsGkAtoms::center)) {
+ OwningNonNull<Element> centerElement = *content->AsElement();
+ {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ RemoveAlignFromDescendants(centerElement, aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::RemoveAlignFromDescendants(EditTarget::"
+ "OnlyDescendantsExceptTable) failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+
+ // We may have to insert a `<br>` element before first child of the
+ // `<center>` element because it should be first element of a hard line
+ // even after removing the `<center>` element.
+ {
+ Result<CreateElementResult, nsresult>
+ maybeInsertBRElementBeforeFirstChildResult =
+ EnsureHardLineBeginsWithFirstChildOf(centerElement);
+ if (MOZ_UNLIKELY(maybeInsertBRElementBeforeFirstChildResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::EnsureHardLineBeginsWithFirstChildOf() failed");
+ return maybeInsertBRElementBeforeFirstChildResult.propagateErr();
+ }
+ CreateElementResult unwrappedResult =
+ maybeInsertBRElementBeforeFirstChildResult.unwrap();
+ if (unwrappedResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedResult.UnwrapCaretPoint();
+ }
+ }
+
+ // We may have to insert a `<br>` element after last child of the
+ // `<center>` element because it should be last element of a hard line
+ // even after removing the `<center>` element.
+ {
+ Result<CreateElementResult, nsresult>
+ maybeInsertBRElementAfterLastChildResult =
+ EnsureHardLineEndsWithLastChildOf(centerElement);
+ if (MOZ_UNLIKELY(maybeInsertBRElementAfterLastChildResult.isErr())) {
+ NS_WARNING("HTMLEditor::EnsureHardLineEndsWithLastChildOf() failed");
+ return maybeInsertBRElementAfterLastChildResult.propagateErr();
+ }
+ CreateElementResult unwrappedResult =
+ maybeInsertBRElementAfterLastChildResult.unwrap();
+ if (unwrappedResult.HasCaretPointSuggestion()) {
+ pointToPutCaret = unwrappedResult.UnwrapCaretPoint();
+ }
+ }
+
+ {
+ Result<EditorDOMPoint, nsresult> unwrapCenterElementResult =
+ RemoveContainerWithTransaction(centerElement);
+ if (MOZ_UNLIKELY(unwrapCenterElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapCenterElementResult;
+ }
+ if (unwrapCenterElementResult.inspect().IsSet()) {
+ pointToPutCaret = unwrapCenterElementResult.unwrap();
+ }
+ }
+ continue;
+ }
+
+ if (!HTMLEditUtils::IsBlockElement(*content,
+ BlockInlineCheck::UseHTMLDefaultStyle) &&
+ !content->IsHTMLElement(nsGkAtoms::hr)) {
+ continue;
+ }
+
+ const OwningNonNull<Element> blockOrHRElement = *content->AsElement();
+ if (HTMLEditUtils::SupportsAlignAttr(blockOrHRElement)) {
+ nsresult rv =
+ RemoveAttributeWithTransaction(blockOrHRElement, *nsGkAtoms::align);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::align) "
+ "failed");
+ return Err(rv);
+ }
+ }
+ if (useCSS) {
+ if (blockOrHRElement->IsAnyOfHTMLElements(nsGkAtoms::table,
+ nsGkAtoms::hr)) {
+ nsresult rv = SetAttributeOrEquivalent(
+ blockOrHRElement, nsGkAtoms::align, aAlignType, false);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::SetAttributeOrEquivalent(nsGkAtoms::align) failed");
+ return Err(rv);
+ }
+ } else {
+ nsStyledElement* styledBlockOrHRElement =
+ nsStyledElement::FromNode(blockOrHRElement);
+ if (NS_WARN_IF(!styledBlockOrHRElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // MOZ_KnownLive(*styledBlockOrHRElement): It's `blockOrHRElement
+ // which is OwningNonNull.
+ nsAutoString dummyCssValue;
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ CSSEditUtils::RemoveCSSInlineStyleWithTransaction(
+ *this, MOZ_KnownLive(*styledBlockOrHRElement),
+ nsGkAtoms::textAlign, dummyCssValue);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSInlineStyleWithTransaction(nsGkAtoms::"
+ "textAlign) failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ }
+ if (!blockOrHRElement->IsHTMLElement(nsGkAtoms::table)) {
+ // unless this is a table, look at children
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ RemoveAlignFromDescendants(blockOrHRElement, aAlignType,
+ EditTarget::OnlyDescendantsExceptTable);
+ if (pointToPutCaretOrError.isErr()) {
+ NS_WARNING(
+ "HTMLEditor::RemoveAlignFromDescendants(EditTarget::"
+ "OnlyDescendantsExceptTable) failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ }
+ return pointToPutCaret;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::EnsureHardLineBeginsWithFirstChildOf(
+ Element& aRemovingContainerElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsIContent* firstEditableChild = HTMLEditUtils::GetFirstChild(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!firstEditableChild) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(
+ *firstEditableChild, BlockInlineCheck::UseComputedDisplayStyle) ||
+ firstEditableChild->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousSibling(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!previousEditableContent) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(
+ *previousEditableContent,
+ BlockInlineCheck::UseComputedDisplayStyle) ||
+ previousEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult = InsertBRElement(
+ WithTransaction::Yes, EditorDOMPoint(&aRemovingContainerElement, 0u));
+ NS_WARNING_ASSERTION(
+ insertBRElementResult.isOk(),
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult;
+}
+
+Result<CreateElementResult, nsresult>
+HTMLEditor::EnsureHardLineEndsWithLastChildOf(
+ Element& aRemovingContainerElement) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsIContent* firstEditableContent = HTMLEditUtils::GetLastChild(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!firstEditableContent) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(
+ *firstEditableContent, BlockInlineCheck::UseComputedDisplayStyle) ||
+ firstEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ nsIContent* nextEditableContent = HTMLEditUtils::GetPreviousSibling(
+ aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode});
+ if (!nextEditableContent) {
+ return CreateElementResult::NotHandled();
+ }
+
+ if (HTMLEditUtils::IsBlockElement(
+ *nextEditableContent, BlockInlineCheck::UseComputedDisplayStyle) ||
+ nextEditableContent->IsHTMLElement(nsGkAtoms::br)) {
+ return CreateElementResult::NotHandled();
+ }
+
+ Result<CreateElementResult, nsresult> insertBRElementResult = InsertBRElement(
+ WithTransaction::Yes, EditorDOMPoint::AtEndOf(aRemovingContainerElement));
+ NS_WARNING_ASSERTION(
+ insertBRElementResult.isOk(),
+ "HTMLEditor::InsertBRElement(WithTransaction::Yes) failed");
+ return insertBRElementResult;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::SetBlockElementAlign(
+ Element& aBlockOrHRElement, const nsAString& aAlignType,
+ EditTarget aEditTarget) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(HTMLEditUtils::IsBlockElement(
+ aBlockOrHRElement, BlockInlineCheck::UseHTMLDefaultStyle) ||
+ aBlockOrHRElement.IsHTMLElement(nsGkAtoms::hr));
+ MOZ_ASSERT(IsCSSEnabled() ||
+ HTMLEditUtils::SupportsAlignAttr(aBlockOrHRElement));
+
+ EditorDOMPoint pointToPutCaret;
+ if (!aBlockOrHRElement.IsHTMLElement(nsGkAtoms::table)) {
+ Result<EditorDOMPoint, nsresult> pointToPutCaretOrError =
+ RemoveAlignFromDescendants(aBlockOrHRElement, aAlignType, aEditTarget);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING("HTMLEditor::RemoveAlignFromDescendants() failed");
+ return pointToPutCaretOrError;
+ }
+ if (pointToPutCaretOrError.inspect().IsSet()) {
+ pointToPutCaret = pointToPutCaretOrError.unwrap();
+ }
+ }
+ nsresult rv = SetAttributeOrEquivalent(&aBlockOrHRElement, nsGkAtoms::align,
+ aAlignType, false);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetAttributeOrEquivalent(nsGkAtoms::align) failed");
+ return Err(rv);
+ }
+ return pointToPutCaret;
+}
+
+Result<EditorDOMPoint, nsresult> HTMLEditor::ChangeMarginStart(
+ Element& aElement, ChangeMargin aChangeMargin,
+ const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsStaticAtom& marginProperty = MarginPropertyAtomForIndent(aElement);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ nsAutoString value;
+ DebugOnly<nsresult> rvIgnored =
+ CSSEditUtils::GetSpecifiedProperty(aElement, marginProperty, value);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "CSSEditUtils::GetSpecifiedProperty() failed, but ignored");
+ float f;
+ RefPtr<nsAtom> unit;
+ CSSEditUtils::ParseLength(value, &f, getter_AddRefs(unit));
+ if (!f) {
+ unit = nsGkAtoms::px;
+ }
+ int8_t multiplier = aChangeMargin == ChangeMargin::Increase ? 1 : -1;
+ if (nsGkAtoms::in == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_IN * multiplier;
+ } else if (nsGkAtoms::cm == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_CM * multiplier;
+ } else if (nsGkAtoms::mm == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_MM * multiplier;
+ } else if (nsGkAtoms::pt == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PT * multiplier;
+ } else if (nsGkAtoms::pc == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PC * multiplier;
+ } else if (nsGkAtoms::em == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_EM * multiplier;
+ } else if (nsGkAtoms::ex == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_EX * multiplier;
+ } else if (nsGkAtoms::px == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PX * multiplier;
+ } else if (nsGkAtoms::percentage == unit) {
+ f += NS_EDITOR_INDENT_INCREMENT_PERCENT * multiplier;
+ }
+
+ if (0 < f) {
+ if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) {
+ nsAutoString newValue;
+ newValue.AppendFloat(f);
+ newValue.Append(nsDependentAtomString(unit));
+ // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must
+ // be guaranteed by caller because of MOZ_CAN_RUN_SCRIPT method.
+ // MOZ_KnownLive(merginProperty): It's nsStaticAtom.
+ nsresult rv = CSSEditUtils::SetCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), MOZ_KnownLive(marginProperty),
+ newValue);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::SetCSSPropertyWithTransaction() destroyed the "
+ "editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::SetCSSPropertyWithTransaction() failed, but ignored");
+ }
+ return EditorDOMPoint();
+ }
+
+ if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) {
+ // MOZ_KnownLive(*styledElement): It's aElement and its lifetime must
+ // be guaranteed by caller because of MOZ_CAN_RUN_SCRIPT method.
+ // MOZ_KnownLive(merginProperty): It's nsStaticAtom.
+ nsresult rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
+ *this, MOZ_KnownLive(*styledElement), MOZ_KnownLive(marginProperty),
+ value);
+ if (rv == NS_ERROR_EDITOR_DESTROYED) {
+ NS_WARNING(
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction() destroyed the "
+ "editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "CSSEditUtils::RemoveCSSPropertyWithTransaction() failed, but ignored");
+ }
+
+ // Remove unnecessary divs
+ if (!aElement.IsHTMLElement(nsGkAtoms::div) ||
+ HTMLEditUtils::ElementHasAttribute(aElement)) {
+ return EditorDOMPoint();
+ }
+ // Don't touch editing host nor node which is outside of it.
+ if (&aElement == &aEditingHost ||
+ !aElement.IsInclusiveDescendantOf(&aEditingHost)) {
+ return EditorDOMPoint();
+ }
+
+ Result<EditorDOMPoint, nsresult> unwrapDivElementResult =
+ RemoveContainerWithTransaction(aElement);
+ NS_WARNING_ASSERTION(unwrapDivElementResult.isOk(),
+ "HTMLEditor::RemoveContainerWithTransaction() failed");
+ return unwrapDivElementResult;
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::SetSelectionToAbsoluteAsSubAction(const Element& aEditingHost) {
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSetPositionToAbsolute, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ auto EnsureCaretInElementIfCollapsedOutside =
+ [&](Element& aElement) MOZ_CAN_RUN_SCRIPT {
+ if (!SelectionRef().IsCollapsed() || !SelectionRef().RangeCount()) {
+ return NS_OK;
+ }
+ const auto firstRangeStartPoint =
+ GetFirstSelectionStartPoint<EditorRawDOMPoint>();
+ if (MOZ_UNLIKELY(!firstRangeStartPoint.IsSet())) {
+ return NS_OK;
+ }
+ const Result<EditorRawDOMPoint, nsresult> pointToPutCaretOrError =
+ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside<
+ EditorRawDOMPoint>(aElement, firstRangeStartPoint);
+ if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
+ NS_WARNING(
+ "HTMLEditUtils::ComputePointToPutCaretInElementIfOutside() "
+ "failed, but ignored");
+ return NS_OK;
+ }
+ if (!pointToPutCaretOrError.inspect().IsSet()) {
+ return NS_OK;
+ }
+ nsresult rv = CollapseSelectionTo(pointToPutCaretOrError.inspect());
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionTo() failed, but ignored");
+ return NS_OK;
+ };
+
+ RefPtr<Element> focusElement = GetSelectionContainerElement();
+ if (focusElement && HTMLEditUtils::IsImage(focusElement)) {
+ nsresult rv = EnsureCaretInElementIfCollapsedOutside(*focusElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EnsureCaretInElementIfCollapsedOutside() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+ }
+
+ // XXX Why do we do this only when there is only one selection range?
+ if (!SelectionRef().IsCollapsed() && SelectionRef().RangeCount() == 1u) {
+ Result<EditorRawDOMRange, nsresult> extendedRange =
+ GetRangeExtendedToHardLineEdgesForBlockEditAction(
+ SelectionRef().GetRangeAt(0u), aEditingHost);
+ if (MOZ_UNLIKELY(extendedRange.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction() "
+ "failed");
+ return extendedRange.propagateErr();
+ }
+ // Note that end point may be prior to start point. So, we
+ // cannot use Selection::SetStartAndEndInLimit() here.
+ IgnoredErrorResult error;
+ SelectionRef().SetBaseAndExtentInLimiter(
+ extendedRange.inspect().StartRef().ToRawRangeBoundary(),
+ extendedRange.inspect().EndRef().ToRawRangeBoundary(), error);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (MOZ_UNLIKELY(error.Failed())) {
+ NS_WARNING("Selection::SetBaseAndExtentInLimiter() failed");
+ return Err(error.StealNSResult());
+ }
+ }
+
+ RefPtr<Element> divElement;
+ rv = MoveSelectedContentsToDivElementToMakeItAbsolutePosition(
+ address_of(divElement), aEditingHost);
+ // MoveSelectedContentsToDivElementToMakeItAbsolutePosition() may restore
+ // selection with AutoSelectionRestorer. Therefore, the editor might have
+ // already been destroyed now.
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MoveSelectedContentsToDivElementToMakeItAbsolutePosition()"
+ " failed");
+ return Err(rv);
+ }
+
+ if (IsSelectionRangeContainerNotContent()) {
+ NS_WARNING("Mutation event listener might have changed the selection");
+ return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
+ }
+
+ rv = MaybeInsertPaddingBRElementForEmptyLastLineAtSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "HTMLEditor::MaybeInsertPaddingBRElementForEmptyLastLineAtSelection() "
+ "failed");
+ return Err(rv);
+ }
+
+ if (!divElement) {
+ return EditActionResult::HandledResult();
+ }
+
+ rv = SetPositionToAbsoluteOrStatic(*divElement, true);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetPositionToAbsoluteOrStatic() failed");
+ return Err(rv);
+ }
+
+ rv = EnsureCaretInElementIfCollapsedOutside(*divElement);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EnsureCaretInElementIfCollapsedOutside() failed");
+ return Err(rv);
+ }
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::MoveSelectedContentsToDivElementToMakeItAbsolutePosition(
+ RefPtr<Element>* aTargetElement, const Element& aEditingHost) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(aTargetElement);
+
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ EditorDOMPoint pointToPutCaret;
+
+ // Use these ranges to construct a list of nodes to act on.
+ AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
+ {
+ AutoRangeArray extendedSelectionRanges(SelectionRef());
+ extendedSelectionRanges.ExtendRangesToWrapLines(
+ EditSubAction::eSetPositionToAbsolute,
+ BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ Result<EditorDOMPoint, nsresult> splitResult =
+ extendedSelectionRanges
+ .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
+ *this, BlockInlineCheck::UseHTMLDefaultStyle, aEditingHost);
+ if (MOZ_UNLIKELY(splitResult.isErr())) {
+ NS_WARNING(
+ "AutoRangeArray::"
+ "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() "
+ "failed");
+ return splitResult.unwrapErr();
+ }
+ if (splitResult.inspect().IsSet()) {
+ pointToPutCaret = splitResult.unwrap();
+ }
+ nsresult rv = extendedSelectionRanges.CollectEditTargetNodes(
+ *this, arrayOfContents, EditSubAction::eSetPositionToAbsolute,
+ AutoRangeArray::CollectNonEditableNodes::Yes);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "AutoRangeArray::CollectEditTargetNodes(EditSubAction::"
+ "eSetPositionToAbsolute, CollectNonEditableNodes::Yes) failed");
+ return rv;
+ }
+ }
+
+ Result<EditorDOMPoint, nsresult> splitAtBRElementsResult =
+ MaybeSplitElementsAtEveryBRElement(arrayOfContents,
+ EditSubAction::eSetPositionToAbsolute);
+ if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::"
+ "eSetPositionToAbsolute) failed");
+ return splitAtBRElementsResult.inspectErr();
+ }
+ if (splitAtBRElementsResult.inspect().IsSet()) {
+ pointToPutCaret = splitAtBRElementsResult.unwrap();
+ }
+
+ if (AllowsTransactionsToChangeSelection() &&
+ pointToPutCaret.IsSetAndValid()) {
+ nsresult rv = CollapseSelectionTo(pointToPutCaret);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionTo() failed");
+ return rv;
+ }
+ }
+
+ // If there is no visible and editable nodes in the edit targets, make an
+ // empty block.
+ // XXX Isn't this odd if there are only non-editable visible nodes?
+ if (HTMLEditUtils::IsEmptyOneHardLine(
+ arrayOfContents, BlockInlineCheck::UseHTMLDefaultStyle)) {
+ const auto atCaret =
+ EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>();
+ if (NS_WARN_IF(!atCaret.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure we can put a block here.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atCaret, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ CreateElementResult unwrappedCreateNewDivElementResult =
+ createNewDivElementResult.unwrap();
+ // We'll update selection after deleting the content nodes and nobody
+ // refers selection until then. Therefore, we don't need to update
+ // selection here.
+ unwrappedCreateNewDivElementResult.IgnoreCaretPointSuggestion();
+ RefPtr<Element> newDivElement =
+ unwrappedCreateNewDivElementResult.UnwrapNewNode();
+ MOZ_ASSERT(newDivElement);
+ // Delete anything that was in the list of nodes
+ // XXX We don't need to remove items from the array.
+ for (OwningNonNull<nsIContent>& curNode : arrayOfContents) {
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it alive.
+ nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(*curNode));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
+ return rv;
+ }
+ }
+ // Don't restore the selection
+ restoreSelectionLater.Abort();
+ nsresult rv = CollapseSelectionToStartOf(*newDivElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToStartOf() failed");
+ *aTargetElement = std::move(newDivElement);
+ return rv;
+ }
+
+ // `<div>` element to be positioned absolutely. This may have already
+ // existed or newly created by this method.
+ RefPtr<Element> targetDivElement;
+ // Newly created list element for moving selected list item elements into
+ // targetDivElement. I.e., this is created in the `<div>` element.
+ RefPtr<Element> createdListElement;
+ // If we handle a parent list item element, this is set to it. In such case,
+ // we should handle its children again.
+ RefPtr<Element> handledListItemElement;
+ for (OwningNonNull<nsIContent>& content : arrayOfContents) {
+ // Here's where we actually figure out what to do.
+ EditorDOMPoint atContent(content);
+ if (NS_WARN_IF(!atContent.IsSet())) {
+ return NS_ERROR_FAILURE; // XXX not continue??
+ }
+
+ // Ignore all non-editable nodes. Leave them be.
+ if (!EditorUtils::IsEditableContent(content, EditorType::HTML)) {
+ continue;
+ }
+
+ // If current node is a child of a list element, we need another list
+ // element in absolute-positioned `<div>` element to avoid non-selected
+ // list items are moved into the `<div>` element.
+ if (HTMLEditUtils::IsAnyListElement(atContent.GetContainer())) {
+ // If we cannot move current node to created list element, we need a
+ // list element in the target `<div>` element for the destination.
+ // Therefore, duplicate same list element into the target `<div>`
+ // element.
+ nsIContent* previousEditableContent =
+ createdListElement
+ ? HTMLEditUtils::GetPreviousSibling(
+ content, {WalkTreeOption::IgnoreNonEditableNode})
+ : nullptr;
+ if (!createdListElement ||
+ (previousEditableContent &&
+ previousEditableContent != createdListElement)) {
+ nsAtom* ULOrOLOrDLTagName =
+ atContent.GetContainer()->NodeInfo()->NameAtom();
+ if (targetDivElement) {
+ // XXX Do we need to split the container? Since we'll append new
+ // element at end of the <div> element.
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ MaybeSplitAncestorsForInsertWithTransaction(
+ MOZ_KnownLive(*ULOrOLOrDLTagName), atContent, aEditingHost);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() "
+ "failed");
+ return splitNodeResult.unwrapErr();
+ }
+ // We'll update selection after creating a list element below.
+ // Therefore, we don't need to touch selection here.
+ splitNodeResult.inspect().IgnoreCaretPointSuggestion();
+ } else {
+ // If we've not had a target <div> element yet, let's insert a <div>
+ // element with splitting the ancestors.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction(nsGkAtoms::"
+ "div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ // We'll update selection after creating a list element below.
+ // Therefor, we don't need to touch selection here.
+ createNewDivElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(createNewDivElementResult.inspect().GetNewNode());
+ targetDivElement = createNewDivElementResult.unwrap().UnwrapNewNode();
+ }
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ CreateAndInsertElement(WithTransaction::Yes,
+ MOZ_KnownLive(*ULOrOLOrDLTagName),
+ EditorDOMPoint::AtEndOf(targetDivElement));
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) "
+ "failed");
+ return createNewListElementResult.unwrapErr();
+ }
+ nsresult rv = createNewListElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ createdListElement =
+ createNewListElementResult.unwrap().UnwrapNewNode();
+ MOZ_ASSERT(createdListElement);
+ }
+ // Move current node (maybe, assumed as a list item element) into the
+ // new list element in the target `<div>` element to be positioned
+ // absolutely.
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content),
+ *createdListElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.propagateErr();
+ }
+ nsresult rv = moveNodeResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ continue;
+ }
+
+ // If contents in a list item element is selected, we should move current
+ // node into the target `<div>` element with the list item element itself
+ // because we want to keep indent level of the contents.
+ if (RefPtr<Element> listItemElement =
+ HTMLEditUtils::GetClosestAncestorListItemElement(content,
+ &aEditingHost)) {
+ if (handledListItemElement == listItemElement) {
+ // Current node has already been moved into the `<div>` element.
+ continue;
+ }
+ // If we cannot move the list item element into created list element,
+ // we need another list element in the target `<div>` element.
+ nsIContent* previousEditableContent =
+ createdListElement
+ ? HTMLEditUtils::GetPreviousSibling(
+ *listItemElement, {WalkTreeOption::IgnoreNonEditableNode})
+ : nullptr;
+ if (!createdListElement ||
+ (previousEditableContent &&
+ previousEditableContent != createdListElement)) {
+ EditorDOMPoint atListItem(listItemElement);
+ if (NS_WARN_IF(!atListItem.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ // XXX If content is the listItemElement and not in a list element,
+ // we duplicate wrong element into the target `<div>` element.
+ nsAtom* containerName =
+ atListItem.GetContainer()->NodeInfo()->NameAtom();
+ if (targetDivElement) {
+ // XXX Do we need to split the container? Since we'll append new
+ // element at end of the <div> element.
+ Result<SplitNodeResult, nsresult> splitNodeResult =
+ MaybeSplitAncestorsForInsertWithTransaction(
+ MOZ_KnownLive(*containerName), atListItem, aEditingHost);
+ if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() "
+ "failed");
+ return splitNodeResult.unwrapErr();
+ }
+ // We'll update selection after creating a list element below.
+ // Therefore, we don't need to touch selection here.
+ splitNodeResult.inspect().IgnoreCaretPointSuggestion();
+ } else {
+ // If we've not had a target <div> element yet, let's insert a <div>
+ // element with splitting the ancestors.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::"
+ "InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ // We'll update selection after creating a list element below.
+ // Therefore, we don't need to touch selection here.
+ createNewDivElementResult.inspect().IgnoreCaretPointSuggestion();
+ MOZ_ASSERT(createNewDivElementResult.inspect().GetNewNode());
+ targetDivElement = createNewDivElementResult.unwrap().UnwrapNewNode();
+ }
+ // XXX So, createdListElement may be set to a non-list element.
+ Result<CreateElementResult, nsresult> createNewListElementResult =
+ CreateAndInsertElement(WithTransaction::Yes,
+ MOZ_KnownLive(*containerName),
+ EditorDOMPoint::AtEndOf(targetDivElement));
+ if (MOZ_UNLIKELY(createNewListElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::CreateAndInsertElement(WithTransaction::Yes) "
+ "failed");
+ return createNewListElementResult.unwrapErr();
+ }
+ nsresult rv = createNewListElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "CreateElementResult::SuggestCaretPointTo() failed, but ignored");
+ createdListElement =
+ createNewListElementResult.unwrap().UnwrapNewNode();
+ MOZ_ASSERT(createdListElement);
+ }
+ // Move current list item element into the createdListElement (could be
+ // non-list element due to the above bug) in a candidate `<div>` element
+ // to be positioned absolutely.
+ Result<MoveNodeResult, nsresult> moveListItemElementResult =
+ MoveNodeToEndWithTransaction(*listItemElement, *createdListElement);
+ if (MOZ_UNLIKELY(moveListItemElementResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveListItemElementResult.unwrapErr();
+ }
+ nsresult rv = moveListItemElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return Err(rv);
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ handledListItemElement = std::move(listItemElement);
+ continue;
+ }
+
+ if (!targetDivElement) {
+ // If we meet a `<div>` element, use it as the absolute-position
+ // container.
+ // XXX This looks odd. If there are 2 or more `<div>` elements are
+ // selected, first found `<div>` element will have all other
+ // selected nodes.
+ if (content->IsHTMLElement(nsGkAtoms::div)) {
+ targetDivElement = content->AsElement();
+ MOZ_ASSERT(!createdListElement);
+ MOZ_ASSERT(!handledListItemElement);
+ continue;
+ }
+ // Otherwise, create new `<div>` element to be positioned absolutely
+ // and to contain all selected nodes.
+ Result<CreateElementResult, nsresult> createNewDivElementResult =
+ InsertElementWithSplittingAncestorsWithTransaction(
+ *nsGkAtoms::div, atContent, BRElementNextToSplitPoint::Keep,
+ aEditingHost);
+ if (MOZ_UNLIKELY(createNewDivElementResult.isErr())) {
+ NS_WARNING(
+ "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction("
+ "nsGkAtoms::div) failed");
+ return createNewDivElementResult.unwrapErr();
+ }
+ nsresult rv = createNewDivElementResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ MOZ_ASSERT(createNewDivElementResult.inspect().GetNewNode());
+ targetDivElement = createNewDivElementResult.unwrap().UnwrapNewNode();
+ }
+
+ // MOZ_KnownLive because 'arrayOfContents' is guaranteed to keep it alive.
+ Result<MoveNodeResult, nsresult> moveNodeResult =
+ MoveNodeToEndWithTransaction(MOZ_KnownLive(content), *targetDivElement);
+ if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
+ NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
+ return moveNodeResult.unwrapErr();
+ }
+ nsresult rv = moveNodeResult.inspect().SuggestCaretPointTo(
+ *this, {SuggestCaret::OnlyIfHasSuggestion,
+ SuggestCaret::OnlyIfTransactionsAllowedToDoIt,
+ SuggestCaret::AndIgnoreTrivialError});
+ if (NS_FAILED(rv)) {
+ NS_WARNING("MoveNodeResult::SuggestCaretPointTo() failed");
+ return rv;
+ }
+ NS_WARNING_ASSERTION(
+ rv != NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR,
+ "MoveNodeResult::SuggestCaretPointTo() failed, but ignored");
+ // Forget createdListElement, if any
+ createdListElement = nullptr;
+ }
+ *aTargetElement = std::move(targetDivElement);
+ return NS_OK;
+}
+
+Result<EditActionResult, nsresult>
+HTMLEditor::SetSelectionToStaticAsSubAction() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSetPositionToStatic, nsIEditor::eNext,
+ ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ RefPtr<Element> element = GetAbsolutelyPositionedSelectionContainer();
+ if (!element) {
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::GetAbsolutelyPositionedSelectionContainer() returned "
+ "nullptr");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ {
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ nsresult rv = SetPositionToAbsoluteOrStatic(*element, false);
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("HTMLEditor::SetPositionToAbsoluteOrStatic() failed");
+ return Err(rv);
+ }
+ }
+
+ // Restoring Selection might cause destroying the HTML editor.
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING("Destroying AutoSelectionRestorer caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ return EditActionResult::HandledResult();
+}
+
+Result<EditActionResult, nsresult> HTMLEditor::AddZIndexAsSubAction(
+ int32_t aChange) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this,
+ aChange < 0 ? EditSubAction::eDecreaseZIndex
+ : EditSubAction::eIncreaseZIndex,
+ nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return Err(ignoredError.StealNSResult());
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ {
+ Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
+ return result;
+ }
+ if (result.inspect().Canceled()) {
+ return result;
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
+ "failed, but ignored");
+
+ if (NS_SUCCEEDED(rv) && SelectionRef().IsCollapsed()) {
+ nsresult rv = EnsureCaretNotAfterInvisibleBRElement();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
+ "failed, but ignored");
+ if (NS_SUCCEEDED(rv)) {
+ nsresult rv = PrepareInlineStylesForCaret();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
+ }
+ }
+
+ RefPtr<Element> absolutelyPositionedElement =
+ GetAbsolutelyPositionedSelectionContainer();
+ if (!absolutelyPositionedElement) {
+ if (NS_WARN_IF(Destroyed())) {
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING(
+ "HTMLEditor::GetAbsolutelyPositionedSelectionContainer() returned "
+ "nullptr");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ nsStyledElement* absolutelyPositionedStyledElement =
+ nsStyledElement::FromNode(absolutelyPositionedElement);
+ if (NS_WARN_IF(!absolutelyPositionedStyledElement)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ {
+ AutoSelectionRestorer restoreSelectionLater(*this);
+
+ // MOZ_KnownLive(*absolutelyPositionedStyledElement): It's
+ // absolutelyPositionedElement whose type is RefPtr.
+ Result<int32_t, nsresult> result = AddZIndexWithTransaction(
+ MOZ_KnownLive(*absolutelyPositionedStyledElement), aChange);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("HTMLEditor::AddZIndexWithTransaction() failed");
+ return result.propagateErr();
+ }
+ }
+
+ // Restoring Selection might cause destroying the HTML editor.
+ if (MOZ_UNLIKELY(Destroyed())) {
+ NS_WARNING("Destroying AutoSelectionRestorer caused destroying the editor");
+ return Err(NS_ERROR_EDITOR_DESTROYED);
+ }
+
+ return EditActionResult::HandledResult();
+}
+
+nsresult HTMLEditor::OnDocumentModified() {
+ if (mPendingDocumentModifiedRunner) {
+ return NS_OK; // We've already posted same runnable into the queue.
+ }
+ mPendingDocumentModifiedRunner = NewRunnableMethod(
+ "HTMLEditor::OnModifyDocument", this, &HTMLEditor::OnModifyDocument);
+ nsContentUtils::AddScriptRunner(do_AddRef(mPendingDocumentModifiedRunner));
+ // Be aware, if OnModifyDocument() may be called synchronously, the
+ // editor might have been destroyed here.
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+} // namespace mozilla