diff options
Diffstat (limited to 'editor/libeditor/HTMLEditSubActionHandler.cpp')
-rw-r--r-- | editor/libeditor/HTMLEditSubActionHandler.cpp | 11794 |
1 files changed, 11794 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp new file mode 100644 index 0000000000..8e89d26d46 --- /dev/null +++ b/editor/libeditor/HTMLEditSubActionHandler.cpp @@ -0,0 +1,11794 @@ +/* -*- 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/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::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 (IsInPlaintextMode()) { + // 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()) { + 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; + } + default: { + if (Element* editingHost = ComputeEditingHost()) { + if (RefPtr<nsRange> extendedChangedRange = AutoRangeArray:: + CreateRangeWrappingStartAndEndLinesContainingBoundaries( + changedRange, GetTopLevelEditSubAction(), + *editingHost)) { + 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})) { + 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, {}, 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); + const RefPtr<const Element> parentBlockElementOfBRElement = + HTMLEditUtils::GetAncestorElement(*previousBRElement, + HTMLEditUtils::ClosestBlockElement); + + 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; + } + + 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"); + ignoredError.SuppressException(); + + RefPtr<Element> rootElement = GetRoot(); + if (!rootElement) { + 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(*rootElement, editorType); + for (nsIContent* rootChild = rootElement->GetFirstChild(); rootChild; + rootChild = rootChild->GetNextSibling()) { + if (EditorUtils::IsPaddingBRElementForEmptyEditor(*rootChild) || + !isRootEditable || + EditorUtils::IsEditableContent(*rootChild, editorType) || + HTMLEditUtils::IsBlockElement(*rootChild)) { + return NS_OK; + } + } + + // Skip adding the padding <br> element for empty editor if body + // is read-only. + if (IsHTMLEditor() && !HTMLEditUtils::IsSimplyEditableNode(*rootElement)) { + return NS_OK; + } + + // 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(rootElement, 0u)); + if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertBRElementResult.unwrapErr(); + } + + // Set selection. + insertBRElementResult.inspect().IgnoreCaretPointSuggestion(); + nsresult rv = CollapseSelectionToStartOf(*rootElement); + 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 || IsInPlaintextMode()) { + 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()); + // Next inserting text should be inserted into styled inline elements if + // they have first visible thing in the new line. + auto pointToPutCaret = [&]() -> EditorDOMPoint { + WSScanResult forwardScanResult = + WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + editingHost, EditorRawDOMPoint::After( + *unwrappedInsertBRElementResult.GetNewNode())); + if (forwardScanResult.InVisibleOrCollapsibleCharacters()) { + unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); + return forwardScanResult.Point<EditorDOMPoint>(); + } + if (forwardScanResult.ReachedSpecialContent()) { + unwrappedInsertBRElementResult.IgnoreCaretPointSuggestion(); + return forwardScanResult.PointAtContent<EditorDOMPoint>(); + } + return unwrappedInsertBRElementResult.UnwrapCaretPoint(); + }(); + + 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(aEditingHost) || + HTMLEditUtils::ShouldInsertLinefeedCharacter( + aCandidatePointToSplit, 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)) { + 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); + + // 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); + Result<RefPtr<Element>, nsresult> suggestBlockElementToPutCaretOrError = + FormatBlockContainerWithTransaction( + selectionRanges, + MOZ_KnownLive(HTMLEditor::ToParagraphSeparatorTagName(separator)), + 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); + 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})) { + 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()); + + bool brElementIsAfterBlock = false, brElementIsBeforeBlock = false; + + // First, insert a <br> element. + RefPtr<Element> brElement; + if (IsInPlaintextMode()) { + 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); + WSRunScanner wsRunScanner(&aEditingHost, pointToBreak); + WSScanResult backwardScanResult = + wsRunScanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(pointToBreak); + if (MOZ_UNLIKELY(backwardScanResult.Failed())) { + NS_WARNING( + "WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom() failed"); + return Err(NS_ERROR_FAILURE); + } + brElementIsAfterBlock = backwardScanResult.ReachedBlockBoundary(); + WSScanResult forwardScanResult = + wsRunScanner.ScanNextVisibleNodeOrBlockBoundaryFrom(pointToBreak); + if (MOZ_UNLIKELY(forwardScanResult.Failed())) { + NS_WARNING( + "WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); + return Err(NS_ERROR_FAILURE); + } + brElementIsBeforeBlock = forwardScanResult.ReachedBlockBoundary(); + // If the container of the break is a link, we need to split it and + // insert new <br> between the split links. + RefPtr<Element> linkNode = + HTMLEditor::GetLinkElement(pointToBreak.GetContainer()); + if (linkNode) { + 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); + } + + 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(brElement, + InterlinePosition::StartOfNextLine); + return CreateElementResult(std::move(brElement), + std::move(pointToPutCaret)); + } + + auto afterBRElement = EditorDOMPoint::After(brElement); + WSScanResult forwardScanFromAfterBRElementResult = + WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(&aEditingHost, + afterBRElement); + 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"); + } + } + + // 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) + ? 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); + 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), + "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); + 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::IsInlineElement(aMailCiteElement)) { + 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); + 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)); + 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)) { + // 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)) { + // 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); + 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); + 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); + const EditorDOMPointInText followingCharPoint = + WSRunScanner::GetInclusiveNextEditableCharPoint(editingHost, + aEndToDelete); + // Blink-compat: Normalize white-spaces in first node only when not removing + // its last character or no text nodes follow the first node. + // If removing last character of first node and there are + // following text nodes, white-spaces in following text node are + // normalized instead. + const bool removingLastCharOfStartNode = + aStartToDelete.ContainerAs<Text>() != aEndToDelete.ContainerAs<Text>() || + (aEndToDelete.IsEndOfContainer() && followingCharPoint.IsSet()); + const bool maybeNormalizePrecedingWhiteSpaces = + !removingLastCharOfStartNode && precedingCharPoint.IsSet() && + !precedingCharPoint.IsEndOfContainer() && + precedingCharPoint.ContainerAs<Text>() == + aStartToDelete.ContainerAs<Text>() && + precedingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP(); + const bool maybeNormalizeFollowingWhiteSpaces = + followingCharPoint.IsSet() && !followingCharPoint.IsEndOfContainer() && + (followingCharPoint.ContainerAs<Text>() == + aEndToDelete.ContainerAs<Text>() || + removingLastCharOfStartNode) && + followingCharPoint.IsCharCollapsibleASCIISpaceOrNBSP(); + + if (!maybeNormalizePrecedingWhiteSpaces && + !maybeNormalizeFollowingWhiteSpaces) { + return; // There are no white-spaces. + } + + // Next, consider the range to normalize. + EditorDOMPointInText startToNormalize, endToNormalize; + if (maybeNormalizePrecedingWhiteSpaces) { + Maybe<uint32_t> previousCharOffsetOfWhiteSpaces = + HTMLEditUtils::GetPreviousNonCollapsibleCharOffset( + precedingCharPoint, {WalkTextOption::TreatNBSPsCollapsible}); + startToNormalize.Set(precedingCharPoint.ContainerAs<Text>(), + previousCharOffsetOfWhiteSpaces.isSome() + ? previousCharOffsetOfWhiteSpaces.value() + 1 + : 0); + MOZ_ASSERT(!startToNormalize.IsEndOfContainer()); + } + if (maybeNormalizeFollowingWhiteSpaces) { + Maybe<uint32_t> nextCharOffsetOfWhiteSpaces = + HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset( + followingCharPoint, {WalkTextOption::TreatNBSPsCollapsible}); + if (nextCharOffsetOfWhiteSpaces.isSome()) { + endToNormalize.Set(followingCharPoint.ContainerAs<Text>(), + nextCharOffsetOfWhiteSpaces.value()); + } else { + endToNormalize.SetToEndOf(followingCharPoint.ContainerAs<Text>()); + } + MOZ_ASSERT(!endToNormalize.IsStartOfContainer()); + } + + // Next, retrieve surrounding information of white-space sequence. + // If we're removing first text node's last character, we need to + // normalize white-spaces starts from another text node. In this case, + // we need to lie for avoiding assertion in GenerateWhiteSpaceSequence(). + CharPointData previousCharPointData = + removingLastCharOfStartNode + ? CharPointData::InDifferentTextNode(CharPointType::TextEnd) + : GetPreviousCharPointDataForNormalizingWhiteSpaces( + startToNormalize.IsSet() ? startToNormalize : aStartToDelete); + CharPointData nextCharPointData = + GetInclusiveNextCharPointDataForNormalizingWhiteSpaces( + endToNormalize.IsSet() ? endToNormalize : aEndToDelete); + + // Next, compute number of white-spaces in start/end node. + uint32_t lengthInStartNode = 0, lengthInEndNode = 0; + if (startToNormalize.IsSet()) { + MOZ_ASSERT(startToNormalize.ContainerAs<Text>() == + aStartToDelete.ContainerAs<Text>()); + lengthInStartNode = aStartToDelete.Offset() - startToNormalize.Offset(); + MOZ_ASSERT(lengthInStartNode); + } + if (endToNormalize.IsSet()) { + lengthInEndNode = + endToNormalize.ContainerAs<Text>() == aEndToDelete.ContainerAs<Text>() + ? endToNormalize.Offset() - aEndToDelete.Offset() + : endToNormalize.Offset(); + MOZ_ASSERT(lengthInEndNode); + // If we normalize white-spaces in a text node, we can replace all of them + // with one ReplaceTextTransaction. + if (endToNormalize.ContainerAs<Text>() == + aStartToDelete.ContainerAs<Text>()) { + lengthInStartNode += lengthInEndNode; + lengthInEndNode = 0; + } + } + + MOZ_ASSERT(lengthInStartNode + lengthInEndNode); + + // Next, generate normalized white-spaces. + if (!lengthInEndNode) { + HTMLEditor::GenerateWhiteSpaceSequence( + aNormalizedWhiteSpacesInStartNode, lengthInStartNode, + previousCharPointData, nextCharPointData); + } else if (!lengthInStartNode) { + HTMLEditor::GenerateWhiteSpaceSequence( + aNormalizedWhiteSpacesInEndNode, lengthInEndNode, previousCharPointData, + nextCharPointData); + } else { + // For making `GenerateWhiteSpaceSequence()` simpler, we should create + // whole white-space sequence first, then, copy to the out params. + nsAutoString whiteSpaces; + HTMLEditor::GenerateWhiteSpaceSequence( + whiteSpaces, lengthInStartNode + lengthInEndNode, previousCharPointData, + nextCharPointData); + aNormalizedWhiteSpacesInStartNode = + Substring(whiteSpaces, 0, lengthInStartNode); + aNormalizedWhiteSpacesInEndNode = Substring(whiteSpaces, lengthInStartNode); + MOZ_ASSERT(aNormalizedWhiteSpacesInEndNode.Length() == lengthInEndNode); + } + + // TODO: Shrink the replacing range and string as far as possible because + // this may run a lot, i.e., HTMLEditor creates ReplaceTextTransaction + // a lot for normalizing white-spaces. Then, each transaction shouldn't + // have all white-spaces every time because once it's normalized, we + // don't need to normalize all of the sequence again, but currently + // we do. + + // Finally, extend the range. + if (startToNormalize.IsSet()) { + aStartToDelete = startToNormalize; + } + if (endToNormalize.IsSet()) { + aEndToDelete = endToNormalize; + } +} + +Result<CaretPoint, nsresult> +HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces( + const EditorDOMPointInText& aStartToDelete, + const EditorDOMPointInText& aEndToDelete, + TreatEmptyTextNodes aTreatEmptyTextNodes, + DeleteDirection aDeleteDirection) { + 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)) { + Element* editingHost = ComputeEditingHost(); + // Try to put caret next to immediately after previous editable leaf. + nsIContent* previousContent = + HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + newCaretPosition, *editableBlockElementOrInlineEditingHost, + {LeafNodeType::LeafNodeOrNonEditableNode}, editingHost); + if (previousContent && !HTMLEditUtils::IsBlockElement(*previousContent)) { + 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}, + 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>())) { + return CaretPoint(EditorDOMPoint()); + } + + WSRunScanner wsRunScanner(ComputeEditingHost(), aPointToInsert); + // 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eCreateOrChangeList, aEditingHost); + Result<EditorDOMPoint, nsresult> splitResult = + extendedRanges.SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + aHTMLEditor, 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})) { + 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}, + 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}))) { + 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. + // XXX Despite the name, HTMLEditUtils::IsInlineElement() returns true for + // non-element content nodes too. + if (HTMLEditUtils::IsInlineElement(aHandlingContent)) { + 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::IgnoreEditableState, + 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::IsInlineElement(aHandlingInlineContent)); + + // 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::IsInlineElement(aHandlingContent)) { + 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 + .ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eCreateOrChangeList, aEditingHost); + Result<EditorDOMPoint, nsresult> splitResult = + extendedSelectionRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, 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, nsAtom& blockType, + 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eCreateOrRemoveBlock, aEditingHost); + Result<EditorDOMPoint, nsresult> splitResult = + aSelectionRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, aEditingHost); + if (MOZ_UNLIKELY(splitResult.isErr())) { + NS_WARNING( + "AutoRangeArray::" + "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed"); + return splitResult.propagateErr(); + } + nsresult rv = aSelectionRanges.CollectEditTargetNodes( + *this, arrayOfContents, EditSubAction::eCreateOrRemoveBlock, + AutoRangeArray::CollectNonEditableNodes::Yes); + if (NS_FAILED(rv)) { + NS_WARNING( + "AutoRangeArray::CollectEditTargetNodes(EditSubAction::" + "eCreateOrRemoveBlock, CollectNonEditableNodes::No) failed"); + return Err(rv); + } + + Result<EditorDOMPoint, nsresult> splitAtBRElementsResult = + MaybeSplitElementsAtEveryBRElement(arrayOfContents, + EditSubAction::eCreateOrRemoveBlock); + if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { + NS_WARNING( + "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" + "eCreateOrRemoveBlock) 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)) { + if (NS_WARN_IF(aSelectionRanges.Ranges().IsEmpty())) { + return Err(NS_ERROR_FAILURE); + } + + auto pointToInsertBlock = + aSelectionRanges.GetFirstRangeStartPoint<EditorDOMPoint>(); + if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { + if (!pointToInsertBlock.IsInContentNode()) { + NS_WARNING( + "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " + "block parent because container of the point is not content"); + return Err(NS_ERROR_FAILURE); + } + // We are removing blocks (going to "body text") + const RefPtr<Element> editableBlockElement = + HTMLEditUtils::GetInclusiveAncestorElement( + *pointToInsertBlock.ContainerAs<nsIContent>(), + HTMLEditUtils::ClosestEditableBlockElement); + if (!editableBlockElement) { + NS_WARNING( + "HTMLEditor::FormatBlockContainerWithTransaction() couldn't find " + "block parent"); + return Err(NS_ERROR_FAILURE); + } + if (!HTMLEditUtils::IsFormatNode(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}, + &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}, + &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( + blockType, pointToInsertBlock, BRElementNextToSplitPoint::Keep, + aEditingHost); + if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) { + NS_WARNING( + nsPrintfCString( + "HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction(" + "%s) failed", + nsAtomCString(&blockType).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(); + } + // 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 (&blockType == 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 (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) { + Result<EditorDOMPoint, nsresult> removeBlockContainerElementsResult = + RemoveBlockContainerElementsWithTransaction(arrayOfContents); + if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) { + NS_WARNING( + "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed"); + return removeBlockContainerElementsResult.propagateErr(); + } + return RefPtr<Element>(); + } + Result<CreateElementResult, nsresult> wrapContentsInBlockElementResult = + CreateOrChangeBlockContainerElement(arrayOfContents, blockType, + aEditingHost); + if (MOZ_UNLIKELY(wrapContentsInBlockElementResult.isErr())) { + NS_WARNING("HTMLEditor::CreateOrChangeBlockContainerElement() 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); + if (editableBlockElement && + HTMLEditUtils::IsListItem(editableBlockElement)) { + arrayOfContents.AppendElement(*editableBlockElement); + } + } + + EditorDOMPoint pointToPutCaret; + if (arrayOfContents.IsEmpty()) { + { + AutoRangeArray extendedRanges(aRanges); + extendedRanges.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eIndent, aEditingHost); + Result<EditorDOMPoint, nsresult> splitResult = + extendedRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, 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)) { + 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)) { + 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eIndent, aEditingHost); + Result<EditorDOMPoint, nsresult> splitResult = + extendedRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, 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)) { + const EditorDOMPoint pointToInsertBlockquoteElement = + pointToPutCaret.IsSet() + ? std::move(pointToPutCaret) + : EditorBase::GetFirstSelectionStartPoint<EditorDOMPoint>(); + if (NS_WARN_IF(!pointToInsertBlockquoteElement.IsSet())) { + return NS_ERROR_FAILURE; + } + + // Make sure we can put a block here. + 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 (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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eOutdent, 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)) { + 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) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + EditorDOMPoint pointToPutCaret; + Result<SplitRangeOffFromNodeResult, nsresult> splitResult = + SplitRangeOffFromBlock(aBlockContainerElement, aStartOfRange, + aEndOfRange); + if (MOZ_UNLIKELY(splitResult.isErr())) { + if (splitResult.inspectErr() == NS_ERROR_EDITOR_DESTROYED) { + NS_WARNING("HTMLEditor::SplitRangeOffFromBlock() failed"); + return splitResult; + } + NS_WARNING( + "HTMLEditor::SplitRangeOffFromBlock() 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_ASSERT_IF(GetSplitNodeDirection() == SplitNodeDirection::LeftNodeIsNewOne, + rightmostElement == &aBlockContainerElement); + { + // 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::SplitRangeOffFromBlock(Element& aBlockElement, + nsIContent& aStartOfMiddleElement, + nsIContent& aEndOfMiddleElement) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // aStartOfMiddleElement and aEndOfMiddleElement must be exclusive + // descendants of aBlockElement. + MOZ_ASSERT(EditorUtils::IsDescendantOf(aStartOfMiddleElement, aBlockElement)); + MOZ_ASSERT(EditorUtils::IsDescendantOf(aEndOfMiddleElement, aBlockElement)); + + EditorDOMPoint pointToPutCaret; + // Split at the start. + Result<SplitNodeResult, nsresult> splitAtStartResult = + SplitNodeDeepWithTransaction(aBlockElement, + 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>() + : &aBlockElement; + // MOZ_KnownLive(rightElement) because it's grabbed by splitAtStartResult or + // aBlockElement 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, &aBlockElement, 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 = + SplitRangeOffFromBlock(aBlockElement, aStartOfOutdent, aEndOfOutdent); + if (MOZ_UNLIKELY(splitResult.isErr())) { + NS_WARNING("HTMLEditor::SplitRangeOffFromBlock() failed"); + return splitResult; + } + + SplitRangeOffFromNodeResult unwrappedSplitResult = splitResult.unwrap(); + Element* middleElement = unwrappedSplitResult.GetMiddleContentAs<Element>(); + if (MOZ_UNLIKELY(!middleElement)) { + NS_WARNING( + "HTMLEditor::SplitRangeOffFromBlock() 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eSetOrClearAlignment, aEditingHost); + Result<EditorDOMPoint, nsresult> splitResult = + extendedRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, 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)) { + // 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}))) { + 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); + 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})) { + 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}, + &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); + 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})) { + 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}, + &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::eMergeBlockContents: + case EditSubAction::eCreateOrChangeList: + case EditSubAction::eSetOrClearAlignment: + case EditSubAction::eSetPositionToAbsolute: + case EditSubAction::eIndent: + case EditSubAction::eOutdent: { + EditorDOMPoint pointToPutCaret; + for (int32_t i = aArrayOfContents.Length() - 1; i >= 0; i--) { + OwningNonNull<nsIContent>& content = aArrayOfContents[i]; + if (HTMLEditUtils::IsInlineElement(content) && + 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(i); + aArrayOfContents.InsertElementsAt(i, arrayOfInlineContents); + } + } + return pointToPutCaret; + } + default: + return EditorDOMPoint(); + } +} + +Result<EditorDOMPoint, nsresult> +HTMLEditor::SplitParentInlineElementsAtRangeBoundaries( + RangeItem& aRangeItem, 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(), &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(), &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})) { + 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, {})) { + 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, + GetSplitNodeDirection()); + } + 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, + GetSplitNodeDirection()); + } + 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, + GetSplitNodeDirection()); + } + + // 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}, &aEditingHost)); + if (!brElement || HTMLEditUtils::IsInvisibleBRElement(*brElement) || + EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) { + // is there a BR after it? + brElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetNextContent( + pointToSplit, {WalkTreeOption::IgnoreNonEditableNode}, + &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, + GetSplitNodeDirection()); + } + 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)) { + 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::IsInlineElement(*maybeDeepestInlineContainer) && + 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}); + 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, {})) { + 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})) { + 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)) { + // 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)); + 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) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // 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 (auto& content : aArrayOfContents) { + // If curNode is an <address>, <p>, <hn>, or <pre>, remove it. + if (HTMLEditUtils::IsFormatNode(content)) { + // Process any partial progress saved + if (blockElement) { + Result<SplitRangeOffFromNodeResult, nsresult> unwrapBlockElementResult = + RemoveBlockContainerElementWithTransactionBetween( + *blockElement, *firstContent, *lastContent); + 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); + 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); + if (MOZ_UNLIKELY(removeBlockContainerElementsResult.isErr())) { + NS_WARNING( + "HTMLEditor::RemoveBlockContainerElementsWithTransaction() failed"); + return removeBlockContainerElementsResult; + } + if (removeBlockContainerElementsResult.inspect().IsSet()) { + pointToPutCaret = removeBlockContainerElementsResult.unwrap(); + } + continue; + } + + if (HTMLEditUtils::IsInlineElement(content)) { + 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); + 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); + if (!blockElement || !HTMLEditUtils::IsFormatNode(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); + 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); + 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::CreateOrChangeBlockContainerElement( + nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents, nsAtom& aBlockTag, + 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; + 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; + continue; + } + + // Is it already the right kind of block, or an uneditable block? + if (content->IsHTMLElement(&aBlockTag) || + (!EditorUtils::IsEditableContent(content, EditorType::HTML) && + HTMLEditUtils::IsBlockElement(content))) { + // Forget any previous block used for previous inline nodes + curBlock = nullptr; + // Do nothing to this block + continue; + } + + // If content is a address, p, header, address, or pre, replace it with a + // new block of correct type. + // XXX: pre can't hold everything the others can + if (HTMLEditUtils::IsMozDiv(content) || + HTMLEditUtils::IsFormatNode(content)) { + // Forget any previous block used for previous inline nodes + curBlock = nullptr; + Result<CreateElementResult, nsresult> replaceWithNewBlockElementResult = + ReplaceContainerAndCloneAttributesWithTransaction( + MOZ_KnownLive(*content->AsElement()), aBlockTag); + 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() != atContent.GetContainer())) { + 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; + // Recursion time + AutoTArray<OwningNonNull<nsIContent>, 24> childContents; + HTMLEditUtils::CollectAllChildren(*content, childContents); + if (!childContents.IsEmpty()) { + Result<CreateElementResult, nsresult> wrapChildrenInBlockElementResult = + CreateOrChangeBlockContainerElement(childContents, aBlockTag, + aEditingHost); + if (MOZ_UNLIKELY(wrapChildrenInBlockElementResult.isErr())) { + NS_WARNING( + "HTMLEditor::CreateOrChangeBlockContainerElement() 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( + aBlockTag, atContent, BRElementNextToSplitPoint::Keep, + aEditingHost); + if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) { + NS_WARNING( + nsPrintfCString( + "HTMLEditor::" + "InsertElementWithSplittingAncestorsWithTransaction(%s) failed", + nsAtomCString(&aBlockTag).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 the node is a break, we honor it by putting further nodes in a new + // parent + if (curBlock) { + // Forget any previous block used for previous inline nodes + curBlock = nullptr; + // 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( + aBlockTag, atContent, BRElementNextToSplitPoint::Keep, + aEditingHost); + if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) { + NS_WARNING( + nsPrintfCString( + "HTMLEditor::" + "InsertElementWithSplittingAncestorsWithTransaction(%s) failed", + nsAtomCString(&aBlockTag).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::IsInlineElement(content)) { + // 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 contructed, 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 (&aBlockTag == 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( + aBlockTag, atContent, BRElementNextToSplitPoint::Keep, + aEditingHost); + if (MOZ_UNLIKELY(createNewBlockElementResult.isErr())) { + NS_WARNING(nsPrintfCString("HTMLEditor::" + "InsertElementWithSplittingAncestorsWithTr" + "ansaction(%s) failed", + nsAtomCString(&aBlockTag).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); + } + + // 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( + 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 (aStartOfDeepestRightNode.GetContainer() != &aEditingHost && + !EditorUtils::IsDescendantOf(*aStartOfDeepestRightNode.GetContainer(), + aEditingHost)) { + NS_WARNING("aStartOfDeepestRightNode was not in editing host"); + return Err(NS_ERROR_INVALID_ARG); + } + + // Look for a node that can legally contain the tag. + EditorDOMPoint pointToInsert(aStartOfDeepestRightNode); + for (; pointToInsert.IsSet(); + pointToInsert.Set(pointToInsert.GetContainer())) { + // We cannot split active editing host and its ancestor. So, there is + // no element to contain the specified element. + if (pointToInsert.GetChild() == &aEditingHost) { + NS_WARNING( + "HTMLEditor::MaybeSplitAncestorsForInsertWithTransaction() reached " + "editing host"); + return Err(NS_ERROR_FAILURE); + } + + if (HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(), aTag)) { + // Found an ancestor node which can contain the element. + break; + } + } + + // If we got lost the editing host, we can do nothing. + if (NS_WARN_IF(!pointToInsert.IsSet())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + // 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, + GetSplitNodeDirection()); + } + + 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( + 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}, + &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)) { + 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 = IsInPlaintextMode(); + 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) || + (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(kNameSpaceID_None, nsGkAtoms::face, value); + if (!value.IsEmpty()) { + aPendingStyleCacheArray.AppendElement( + PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::face, value)); + value.Truncate(); + } + } + if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::size)) { + inclusiveAncestor->GetAttr(kNameSpaceID_None, nsGkAtoms::size, value); + if (!value.IsEmpty()) { + aPendingStyleCacheArray.AppendElement( + PendingStyleCache(*nsGkAtoms::font, nsGkAtoms::size, value)); + value.Truncate(); + } + } + if (NeedToAppend(*nsGkAtoms::font, nsGkAtoms::color)) { + inclusiveAncestor->GetAttr(kNameSpaceID_None, 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, ¤tValue); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::GetInlinePropertyBase() failed"); + return rv; + } + } + // This style has disappeared through deletion. Let's add the styles to + // mPendingStylesToApplyToNewContent when same style isn't applied to the + // node already. + if (isAny && + !IsPendingStyleCachePreservingSubAction(GetTopLevelEditSubAction())) { + continue; + } + AutoPendingStyleCacheArray::index_type index = + styleCacheArrayAtInsertionPoint.IndexOf( + styleCacheBeforeEdit.TagRef(), styleCacheBeforeEdit.GetAttribute()); + if (index == AutoPendingStyleCacheArray::NoIndex || + styleCacheBeforeEdit.AttributeValueOrCSSValueRef() != + styleCacheArrayAtInsertionPoint.ElementAt(index) + .AttributeValueOrCSSValueRef()) { + mPendingStylesToApplyToNewContent->PreserveStyle(styleCacheBeforeEdit); + } + } + return NS_OK; +} + +nsresult HTMLEditor::InsertBRElementToEmptyListItemsAndTableCellsInRange( + const RawRangeBoundary& aStartRef, const RawRangeBoundary& aEndRef) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + AutoTArray<OwningNonNull<Element>, 64> arrayOfEmptyElements; + DOMIterator iter; + if (NS_FAILED(iter.Init(aStartRef, aEndRef))) { + NS_WARNING("DOMIterator::Init() failed"); + return NS_ERROR_FAILURE; + } + iter.AppendNodesToArray( + +[](nsINode& aNode, void* aSelf) { + MOZ_ASSERT(Element::FromNode(&aNode)); + MOZ_ASSERT(aSelf); + Element* element = aNode.AsElement(); + if (!EditorUtils::IsEditableContent(*element, EditorType::HTML) || + (!HTMLEditUtils::IsListItem(element) && + !HTMLEditUtils::IsTableCellOrCaption(*element))) { + return false; + } + return HTMLEditUtils::IsEmptyNode( + *element, {EmptyCheckOption::TreatSingleBRElementAsVisible}); + }, + 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}, + 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)) { + 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)) { + 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)) { + 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}, 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); + const Element* const blockElementContainingPreviousEditableContent = + HTMLEditUtils::GetAncestorElement(*previousEditableContent, + HTMLEditUtils::ClosestBlockElement); + // 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}, + 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}, + 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}, + 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}; + if (!HTMLEditUtils::IsBlockElement(*content)) { + 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::IsFormatNode(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; + } + 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})) { + // 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. + bool isFirstListItem = HTMLEditUtils::IsFirstChild( + aListItemElement, {WalkTreeOption::IgnoreNonEditableNode}); + 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)) { + return NS_OK; + } + + if (!HTMLEditUtils::IsEmptyNode( + aElement, {EmptyCheckOption::TreatSingleBRElementAsVisible})) { + 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) && + !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) || + firstEditableChild->IsHTMLElement(nsGkAtoms::br)) { + return CreateElementResult::NotHandled(); + } + + nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousSibling( + aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode}); + if (!previousEditableContent) { + return CreateElementResult::NotHandled(); + } + + if (HTMLEditUtils::IsBlockElement(*previousEditableContent) || + 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) || + firstEditableContent->IsHTMLElement(nsGkAtoms::br)) { + return CreateElementResult::NotHandled(); + } + + nsIContent* nextEditableContent = HTMLEditUtils::GetPreviousSibling( + aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode}); + if (!nextEditableContent) { + return CreateElementResult::NotHandled(); + } + + if (HTMLEditUtils::IsBlockElement(*nextEditableContent) || + 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) || + 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.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eSetPositionToAbsolute, aEditingHost); + Result<EditorDOMPoint, nsresult> splitResult = + extendedSelectionRanges + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + *this, 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)) { + 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 |