diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /editor/libeditor/HTMLEditorDeleteHandler.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | editor/libeditor/HTMLEditorDeleteHandler.cpp | 6854 |
1 files changed, 6854 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp new file mode 100644 index 0000000000..928d9b3b4c --- /dev/null +++ b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -0,0 +1,6854 @@ +/* -*- 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 "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 "WSRunObject.h" + +#include "ErrorList.h" +#include "js/ErrorReport.h" +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/ComputedStyle.h" // for ComputedStyle +#include "mozilla/ContentIterator.h" +#include "mozilla/EditorDOMPoint.h" +#include "mozilla/EditorForwards.h" +#include "mozilla/InternalMutationEvent.h" +#include "mozilla/Maybe.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/SelectionState.h" +#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_* +#include "mozilla/Unused.h" +#include "mozilla/dom/AncestorIterator.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLBRElement.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/mozalloc.h" +#include "nsAString.h" +#include "nsAtom.h" +#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsFrameSelection.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsINode.h" +#include "nsRange.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsStyleConsts.h" // for StyleWhiteSpace +#include "nsTArray.h" + +// NOTE: This file was split from: +// https://searchfox.org/mozilla-central/rev/c409dd9235c133ab41eba635f906aa16e050c197/editor/libeditor/HTMLEditSubActionHandler.cpp + +namespace mozilla { + +using namespace dom; +using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; +using InvisibleWhiteSpaces = HTMLEditUtils::InvisibleWhiteSpaces; +using LeafNodeType = HTMLEditUtils::LeafNodeType; +using ScanLineBreak = HTMLEditUtils::ScanLineBreak; +using TableBoundary = HTMLEditUtils::TableBoundary; +using WalkTreeOption = HTMLEditUtils::WalkTreeOption; + +template Result<CaretPoint, nsresult> +HTMLEditor::DeleteTextAndTextNodesWithTransaction( + const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint, + TreatEmptyTextNodes aTreatEmptyTextNodes); +template Result<CaretPoint, nsresult> +HTMLEditor::DeleteTextAndTextNodesWithTransaction( + const EditorDOMPointInText& aStartPoint, + const EditorDOMPointInText& aEndPoint, + TreatEmptyTextNodes aTreatEmptyTextNodes); + +/***************************************************************************** + * AutoSetTemporaryAncestorLimiter + ****************************************************************************/ + +class MOZ_RAII AutoSetTemporaryAncestorLimiter final { + public: + AutoSetTemporaryAncestorLimiter(const HTMLEditor& aHTMLEditor, + Selection& aSelection, + nsINode& aStartPointNode, + AutoRangeArray* aRanges = nullptr) { + MOZ_ASSERT(aSelection.GetType() == SelectionType::eNormal); + + if (aSelection.GetAncestorLimiter()) { + return; + } + + Element* selectionRootElement = + aHTMLEditor.FindSelectionRoot(aStartPointNode); + if (!selectionRootElement) { + return; + } + aHTMLEditor.InitializeSelectionAncestorLimit(*selectionRootElement); + mSelection = &aSelection; + // Setting ancestor limiter may change ranges which were outer of + // the new limiter. Therefore, we need to reinitialize aRanges. + if (aRanges) { + aRanges->Initialize(aSelection); + } + } + + ~AutoSetTemporaryAncestorLimiter() { + if (mSelection) { + mSelection->SetAncestorLimiter(nullptr); + } + } + + private: + RefPtr<Selection> mSelection; +}; + +/***************************************************************************** + * AutoDeleteRangesHandler + ****************************************************************************/ + +class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { + public: + explicit AutoDeleteRangesHandler( + const AutoDeleteRangesHandler* aParent = nullptr) + : mParent(aParent), + mOriginalDirectionAndAmount(nsIEditor::eNone), + mOriginalStripWrappers(nsIEditor::eNoStrip) {} + + /** + * ComputeRangesToDelete() computes actual deletion ranges. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ComputeRangesToDelete( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, const Element& aEditingHost); + + /** + * Deletes content in or around aRangesToDelete. + * NOTE: This method creates SelectionBatcher. Therefore, each caller + * needs to check if the editor is still available even if this returns + * NS_OK. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete, + const Element& aEditingHost); + + private: + bool IsHandlingRecursively() const { return mParent != nullptr; } + + bool CanFallbackToDeleteRangesWithTransaction( + const AutoRangeArray& aRangesToDelete) const { + return !IsHandlingRecursively() && !aRangesToDelete.Ranges().IsEmpty() && + (!aRangesToDelete.IsCollapsed() || + EditorBase::HowToHandleCollapsedRangeFor( + mOriginalDirectionAndAmount) != + EditorBase::HowToHandleCollapsedRange::Ignore); + } + + /** + * HandleDeleteAroundCollapsedRanges() handles deletion with collapsed + * ranges. Callers must guarantee that this is called only when + * aRangesToDelete.IsCollapsed() returns true. + * + * @param aDirectionAndAmount Direction of the deletion. + * @param aStripWrappers Must be eStrip or eNoStrip. + * @param aRangesToDelete Ranges to delete. This `IsCollapsed()` must + * return true. + * @param aWSRunScannerAtCaret Scanner instance which scanned from + * caret point. + * @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret + * toward aDirectionAndAmount. + * @param aEditingHost The editing host. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + HandleDeleteAroundCollapsedRanges( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete, + const WSRunScanner& aWSRunScannerAtCaret, + const WSScanResult& aScanFromCaretPointResult, + const Element& aEditingHost); + nsresult ComputeRangesToDeleteAroundCollapsedRanges( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, const WSRunScanner& aWSRunScannerAtCaret, + const WSScanResult& aScanFromCaretPointResult, + const Element& aEditingHost) const; + + /** + * HandleDeleteNonCollapsedRanges() handles deletion with non-collapsed + * ranges. Callers must guarantee that this is called only when + * aRangesToDelete.IsCollapsed() returns false. + * + * @param aDirectionAndAmount Direction of the deletion. + * @param aStripWrappers Must be eStrip or eNoStrip. + * @param aRangesToDelete The ranges to delete. + * @param aSelectionWasCollapsed If the caller extended `Selection` + * from collapsed, set this to `Yes`. + * Otherwise, i.e., `Selection` is not + * collapsed from the beginning, set + * this to `No`. + * @param aEditingHost The editing host. + */ + enum class SelectionWasCollapsed { Yes, No }; + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + HandleDeleteNonCollapsedRanges(HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + AutoRangeArray& aRangesToDelete, + SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost); + nsresult ComputeRangesToDeleteNonCollapsedRanges( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, + SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost) const; + + /** + * Handle deletion of collapsed ranges in a text node. + * + * @param aDirectionAndAmount Must be eNext or ePrevious. + * @param aCaretPosition The position where caret is. This container + * must be a text node. + * @param aEditingHost The editing host. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> + HandleDeleteTextAroundCollapsedRanges( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, const Element& aEditingHost); + nsresult ComputeRangesToDeleteTextAroundCollapsedRanges( + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const; + + /** + * Handles deletion of collapsed selection at white-spaces in a text node. + * + * @param aDirectionAndAmount Direction of the deletion. + * @param aPointToDelete The point to delete. I.e., typically, caret + * position. + * @param aEditingHost The editing host. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> + HandleDeleteCollapsedSelectionAtWhiteSpaces( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aPointToDelete, const Element& aEditingHost); + + /** + * Handle deletion of collapsed selection in a text node. + * + * @param aDirectionAndAmount Direction of the deletion. + * @param aRangesToDelete Computed selection ranges to delete. + * @param aPointAtDeletingChar The visible char position which you want to + * delete. + * @param aEditingHost The editing host. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> + HandleDeleteCollapsedSelectionAtVisibleChar( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, + const EditorDOMPoint& aPointAtDeletingChar, const Element& aEditingHost); + + /** + * Handle deletion of atomic elements like <br>, <hr>, <img>, <input>, etc and + * data nodes except text node (e.g., comment node). Note that don't call this + * directly with `<hr>` element. + * + * @param aAtomicContent The atomic content to be deleted. + * @param aCaretPoint The caret point (i.e., selection start or + * end). + * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized + * with the caret point. + * @param aEditingHost The editing host. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> + HandleDeleteAtomicContent(HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent, + const EditorDOMPoint& aCaretPoint, + const WSRunScanner& aWSRunScannerAtCaret, + const Element& aEditingHost); + nsresult ComputeRangesToDeleteAtomicContent( + Element* aEditingHost, const nsIContent& aAtomicContent, + AutoRangeArray& aRangesToDelete) const; + + /** + * GetAtomicContnetToDelete() returns better content that is deletion of + * atomic element. If aScanFromCaretPointResult is special, since this + * point may not be editable, we look for better point to remove atomic + * content. + * + * @param aDirectionAndAmount Direction of the deletion. + * @param aWSRunScannerAtCaret WSRunScanner instance which was + * initialized with the caret point. + * @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret + * toward aDirectionAndAmount. + */ + static nsIContent* GetAtomicContentToDelete( + nsIEditor::EDirection aDirectionAndAmount, + const WSRunScanner& aWSRunScannerAtCaret, + const WSScanResult& aScanFromCaretPointResult) MOZ_NONNULL_RETURN; + + /** + * HandleDeleteAtOtherBlockBoundary() handles deletion at other block boundary + * (i.e., immediately before or after a block). If this does not join blocks, + * `Run()` may be called recursively with creating another instance. + * + * @param aDirectionAndAmount Direction of the deletion. + * @param aStripWrappers Must be eStrip or eNoStrip. + * @param aOtherBlockElement The block element which follows the caret or + * is followed by caret. + * @param aCaretPoint The caret point (i.e., selection start or + * end). + * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized + * with the caret point. + * @param aRangesToDelete Ranges to delete of the caller. This should + * be collapsed and the point should match with + * aCaretPoint. + * @param aEditingHost The editing host. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + HandleDeleteAtOtherBlockBoundary( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, Element& aOtherBlockElement, + const EditorDOMPoint& aCaretPoint, WSRunScanner& aWSRunScannerAtCaret, + AutoRangeArray& aRangesToDelete, const Element& aEditingHost); + + /** + * ExtendOrShrinkRangeToDelete() extends aRangeToDelete if there are + * an invisible <br> element and/or some parent empty elements. + * + * @param aFrameSelection If the caller wants range in selection limiter, + * set this to non-nullptr which knows the limiter. + * @param aRangeToDelete The range to be extended for deletion. This + * must not be collapsed, must be positioned. + */ + template <typename EditorDOMRangeType> + Result<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToDelete( + const HTMLEditor& aHTMLEditor, const nsFrameSelection* aFrameSelection, + const EditorDOMRangeType& aRangeToDelete) const; + + /** + * A helper method for ExtendOrShrinkRangeToDelete(). This returns shrunken + * range if aRangeToDelete selects all over list elements which have some list + * item elements to avoid to delete all list items from the list element. + */ + MOZ_NEVER_INLINE_DEBUG static EditorRawDOMRange + GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements( + const EditorRawDOMRange& aRangeToDelete); + + /** + * DeleteUnnecessaryNodes() removes unnecessary nodes around aRange. + * Note that aRange is tracked with AutoTrackDOMRange. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteUnnecessaryNodes(HTMLEditor& aHTMLEditor, EditorDOMRange& aRange); + + /** + * DeleteUnnecessaryNodesAndCollapseSelection() calls DeleteUnnecessaryNodes() + * and then, collapse selection at tracked aSelectionStartPoint or + * aSelectionEndPoint (depending on aDirectionAndAmount). + * + * @param aDirectionAndAmount Direction of the deletion. + * If nsIEditor::ePrevious, selection + * will be collapsed to aSelectionEndPoint. + * Otherwise, selection will be collapsed + * to aSelectionStartPoint. + * @param aSelectionStartPoint First selection range start after + * computing the deleting range. + * @param aSelectionEndPoint First selection range end after + * computing the deleting range. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteUnnecessaryNodesAndCollapseSelection( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aSelectionStartPoint, + const EditorDOMPoint& aSelectionEndPoint); + + /** + * If aContent is a text node that contains only collapsed white-space or + * empty and editable. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteNodeIfInvisibleAndEditableTextNode(HTMLEditor& aHTMLEditor, + nsIContent& aContent); + + /** + * DeleteParentBlocksIfEmpty() removes parent block elements if they + * don't have visible contents. Note that due performance issue of + * WhiteSpaceVisibilityKeeper, this call may be expensive. And also note that + * this removes a empty block with a transaction. So, please make sure that + * you've already created `AutoPlaceholderBatch`. + * + * @param aPoint The point whether this method climbing up the DOM + * tree to remove empty parent blocks. + * @return NS_OK if one or more empty block parents are deleted. + * NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND if the point is + * not in empty block. + * Or NS_ERROR_* if something unexpected occurs. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteParentBlocksWithTransactionIfEmpty(HTMLEditor& aHTMLEditor, + const EditorDOMPoint& aPoint); + + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> + FallbackToDeleteRangesWithTransaction(HTMLEditor& aHTMLEditor, + AutoRangeArray& aRangesToDelete) const { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteRangesWithTransaction(mOriginalDirectionAndAmount, + mOriginalStripWrappers, + aRangesToDelete); + NS_WARNING_ASSERTION(caretPointOrError.isOk(), + "HTMLEditor::DeleteRangesWithTransaction() failed"); + return caretPointOrError; + } + + /** + * ComputeRangesToDeleteRangesWithTransaction() computes target ranges + * which will be called by `EditorBase::DeleteRangesWithTransaction()`. + * TODO: We should not use it for consistency with each deletion handler + * in this and nested classes. + */ + nsresult ComputeRangesToDeleteRangesWithTransaction( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete) const; + + nsresult FallbackToComputeRangesToDeleteRangesWithTransaction( + const HTMLEditor& aHTMLEditor, AutoRangeArray& aRangesToDelete) const { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)); + nsresult rv = ComputeRangesToDeleteRangesWithTransaction( + aHTMLEditor, mOriginalDirectionAndAmount, aRangesToDelete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::" + "ComputeRangesToDeleteRangesWithTransaction() failed"); + return rv; + } + + class MOZ_STACK_CLASS AutoBlockElementsJoiner final { + public: + AutoBlockElementsJoiner() = delete; + explicit AutoBlockElementsJoiner( + AutoDeleteRangesHandler& aDeleteRangesHandler) + : mDeleteRangesHandler(&aDeleteRangesHandler), + mDeleteRangesHandlerConst(aDeleteRangesHandler) {} + explicit AutoBlockElementsJoiner( + const AutoDeleteRangesHandler& aDeleteRangesHandler) + : mDeleteRangesHandler(nullptr), + mDeleteRangesHandlerConst(aDeleteRangesHandler) {} + + /** + * PrepareToDeleteAtCurrentBlockBoundary() considers left content and right + * content which are joined for handling deletion at current block boundary + * (i.e., at start or end of the current block). + * + * @param aHTMLEditor The HTML editor. + * @param aDirectionAndAmount Direction of the deletion. + * @param aCurrentBlockElement The current block element. + * @param aCaretPoint The caret point (i.e., selection start + * or end). + * @return true if can continue to handle the + * deletion. + */ + bool PrepareToDeleteAtCurrentBlockBoundary( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint); + + /** + * PrepareToDeleteAtOtherBlockBoundary() considers left content and right + * content which are joined for handling deletion at other block boundary + * (i.e., immediately before or after a block). + * + * @param aHTMLEditor The HTML editor. + * @param aDirectionAndAmount Direction of the deletion. + * @param aOtherBlockElement The block element which follows the + * caret or is followed by caret. + * @param aCaretPoint The caret point (i.e., selection start + * or end). + * @param aWSRunScannerAtCaret WSRunScanner instance which was + * initialized with the caret point. + * @return true if can continue to handle the + * deletion. + */ + bool PrepareToDeleteAtOtherBlockBoundary( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, Element& aOtherBlockElement, + const EditorDOMPoint& aCaretPoint, + const WSRunScanner& aWSRunScannerAtCaret); + + /** + * PrepareToDeleteNonCollapsedRanges() considers left block element and + * right block element which are inclusive ancestor block element of + * start and end container of first range of aRangesToDelete. + * + * @param aHTMLEditor The HTML editor. + * @param aRangesToDelete Ranges to delete. Must not be + * collapsed. + * @return true if can continue to handle the + * deletion. + */ + bool PrepareToDeleteNonCollapsedRanges( + const HTMLEditor& aHTMLEditor, const AutoRangeArray& aRangesToDelete); + + /** + * Run() executes the joining. + * + * @param aHTMLEditor The HTML editor. + * @param aDirectionAndAmount Direction of the deletion. + * @param aStripWrappers Must be eStrip or eNoStrip. + * @param aCaretPoint The caret point (i.e., selection start + * or end). + * @param aRangesToDelete Ranges to delete of the caller. + * This should be collapsed and match + * with aCaretPoint. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete, + const Element& aEditingHost) { + switch (mMode) { + case Mode::JoinCurrentBlock: { + Result<EditActionResult, nsresult> result = + HandleDeleteAtCurrentBlockBoundary( + aHTMLEditor, aDirectionAndAmount, aCaretPoint, aEditingHost); + NS_WARNING_ASSERTION(result.isOk(), + "AutoBlockElementsJoiner::" + "HandleDeleteAtCurrentBlockBoundary() failed"); + return result; + } + case Mode::JoinOtherBlock: { + Result<EditActionResult, nsresult> result = + HandleDeleteAtOtherBlockBoundary(aHTMLEditor, aDirectionAndAmount, + aStripWrappers, aCaretPoint, + aRangesToDelete, aEditingHost); + NS_WARNING_ASSERTION(result.isOk(), + "AutoBlockElementsJoiner::" + "HandleDeleteAtOtherBlockBoundary() failed"); + return result; + } + case Mode::DeleteBRElement: { + Result<EditActionResult, nsresult> result = + DeleteBRElement(aHTMLEditor, aDirectionAndAmount, aEditingHost); + NS_WARNING_ASSERTION( + result.isOk(), + "AutoBlockElementsJoiner::DeleteBRElement() failed"); + return result; + } + case Mode::JoinBlocksInSameParent: + case Mode::DeleteContentInRanges: + case Mode::DeleteNonCollapsedRanges: + MOZ_ASSERT_UNREACHABLE( + "This mode should be handled in the other Run()"); + return Err(NS_ERROR_UNEXPECTED); + case Mode::NotInitialized: + return EditActionResult::IgnoredResult(); + } + return Err(NS_ERROR_NOT_INITIALIZED); + } + + nsresult ComputeRangesToDelete(const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPoint, + AutoRangeArray& aRangesToDelete, + const Element& aEditingHost) const { + switch (mMode) { + case Mode::JoinCurrentBlock: { + nsresult rv = ComputeRangesToDeleteAtCurrentBlockBoundary( + aHTMLEditor, aCaretPoint, aRangesToDelete, aEditingHost); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::" + "ComputeRangesToDeleteAtCurrentBlockBoundary() failed"); + return rv; + } + case Mode::JoinOtherBlock: { + nsresult rv = ComputeRangesToDeleteAtOtherBlockBoundary( + aHTMLEditor, aDirectionAndAmount, aCaretPoint, aRangesToDelete, + aEditingHost); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::" + "ComputeRangesToDeleteAtOtherBlockBoundary() failed"); + return rv; + } + case Mode::DeleteBRElement: { + nsresult rv = ComputeRangesToDeleteBRElement(aRangesToDelete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::" + "ComputeRangesToDeleteBRElement() failed"); + return rv; + } + case Mode::JoinBlocksInSameParent: + case Mode::DeleteContentInRanges: + case Mode::DeleteNonCollapsedRanges: + MOZ_ASSERT_UNREACHABLE( + "This mode should be handled in the other " + "ComputeRangesToDelete()"); + return NS_ERROR_UNEXPECTED; + case Mode::NotInitialized: + return NS_OK; + } + return NS_ERROR_NOT_IMPLEMENTED; + } + + /** + * Run() executes the joining. + * + * @param aHTMLEditor The HTML editor. + * @param aDirectionAndAmount Direction of the deletion. + * @param aStripWrappers Whether delete or keep new empty + * ancestor elements. + * @param aRangesToDelete Ranges to delete. Must not be + * collapsed. + * @param aSelectionWasCollapsed Whether selection was or was not + * collapsed when starting to handle + * deletion. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + AutoRangeArray& aRangesToDelete, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost) { + switch (mMode) { + case Mode::JoinCurrentBlock: + case Mode::JoinOtherBlock: + case Mode::DeleteBRElement: + MOZ_ASSERT_UNREACHABLE( + "This mode should be handled in the other Run()"); + return Err(NS_ERROR_UNEXPECTED); + case Mode::JoinBlocksInSameParent: { + Result<EditActionResult, nsresult> result = + JoinBlockElementsInSameParent( + aHTMLEditor, aDirectionAndAmount, aStripWrappers, + aRangesToDelete, aSelectionWasCollapsed, aEditingHost); + NS_WARNING_ASSERTION(result.isOk(), + "AutoBlockElementsJoiner::" + "JoinBlockElementsInSameParent() failed"); + return result; + } + case Mode::DeleteContentInRanges: { + Result<EditActionResult, nsresult> result = + DeleteContentInRanges(aHTMLEditor, aDirectionAndAmount, + aStripWrappers, aRangesToDelete); + NS_WARNING_ASSERTION( + result.isOk(), + "AutoBlockElementsJoiner::DeleteContentInRanges() failed"); + return result; + } + case Mode::DeleteNonCollapsedRanges: { + Result<EditActionResult, nsresult> result = + HandleDeleteNonCollapsedRanges( + aHTMLEditor, aDirectionAndAmount, aStripWrappers, + aRangesToDelete, aSelectionWasCollapsed, aEditingHost); + NS_WARNING_ASSERTION(result.isOk(), + "AutoBlockElementsJoiner::" + "HandleDeleteNonCollapsedRange() failed"); + return result; + } + case Mode::NotInitialized: + MOZ_ASSERT_UNREACHABLE( + "Call Run() after calling a preparation method"); + return EditActionResult::IgnoredResult(); + } + return Err(NS_ERROR_NOT_INITIALIZED); + } + + nsresult ComputeRangesToDelete( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost) const { + switch (mMode) { + case Mode::JoinCurrentBlock: + case Mode::JoinOtherBlock: + case Mode::DeleteBRElement: + MOZ_ASSERT_UNREACHABLE( + "This mode should be handled in the other " + "ComputeRangesToDelete()"); + return NS_ERROR_UNEXPECTED; + case Mode::JoinBlocksInSameParent: { + nsresult rv = ComputeRangesToJoinBlockElementsInSameParent( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::" + "ComputeRangesToJoinBlockElementsInSameParent() failed"); + return rv; + } + case Mode::DeleteContentInRanges: { + nsresult rv = ComputeRangesToDeleteContentInRanges( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::" + "ComputeRangesToDeleteContentInRanges() failed"); + return rv; + } + case Mode::DeleteNonCollapsedRanges: { + nsresult rv = ComputeRangesToDeleteNonCollapsedRanges( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete, + aSelectionWasCollapsed, aEditingHost); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::" + "ComputeRangesToDeleteNonCollapsedRanges() failed"); + return rv; + } + case Mode::NotInitialized: + MOZ_ASSERT_UNREACHABLE( + "Call ComputeRangesToDelete() after calling a preparation " + "method"); + return NS_ERROR_NOT_INITIALIZED; + } + return NS_ERROR_NOT_INITIALIZED; + } + + nsIContent* GetLeafContentInOtherBlockElement() const { + MOZ_ASSERT(mMode == Mode::JoinOtherBlock); + return mLeafContentInOtherBlock; + } + + private: + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + HandleDeleteAtCurrentBlockBoundary( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPoint, const Element& aEditingHost); + nsresult ComputeRangesToDeleteAtCurrentBlockBoundary( + const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint, + AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const; + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + HandleDeleteAtOtherBlockBoundary(HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + const EditorDOMPoint& aCaretPoint, + AutoRangeArray& aRangesToDelete, + const Element& aEditingHost); + // FYI: This method may modify selection, but it won't cause running + // script because of `AutoHideSelectionChanges` which blocks + // selection change listeners and the selection change event + // dispatcher. + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult + ComputeRangesToDeleteAtOtherBlockBoundary( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete, + const Element& aEditingHost) const; + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + JoinBlockElementsInSameParent( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + AutoRangeArray& aRangesToDelete, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost); + nsresult ComputeRangesToJoinBlockElementsInSameParent( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete) const; + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + DeleteBRElement(HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + const Element& aEditingHost); + nsresult ComputeRangesToDeleteBRElement( + AutoRangeArray& aRangesToDelete) const; + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + DeleteContentInRanges(HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + AutoRangeArray& aRangesToDelete); + nsresult ComputeRangesToDeleteContentInRanges( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete) const; + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + HandleDeleteNonCollapsedRanges( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + AutoRangeArray& aRangesToDelete, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost); + nsresult ComputeRangesToDeleteNonCollapsedRanges( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost) const; + + /** + * JoinNodesDeepWithTransaction() joins aLeftNode and aRightNode "deeply". + * First, they are joined simply, then, new right node is assumed as the + * child at length of the left node before joined and new left node is + * assumed as its previous sibling. Then, they will be joined again. + * And then, these steps are repeated. + * + * @param aLeftContent The node which will be removed form the tree. + * @param aRightContent The node which will be inserted the contents of + * aRightContent. + * @return The point of the first child of the last right + * node. The result is always set if this succeeded. + */ + MOZ_CAN_RUN_SCRIPT Result<EditorDOMPoint, nsresult> + JoinNodesDeepWithTransaction(HTMLEditor& aHTMLEditor, + nsIContent& aLeftContent, + nsIContent& aRightContent); + + /** + * DeleteNodesEntirelyInRangeButKeepTableStructure() removes nodes which are + * entirely in aRange. Howevers, if some nodes are part of a table, + * removes all children of them instead. I.e., this does not make damage to + * table structure at the range, but may remove table entirely if it's + * in the range. + * + * @return true if inclusive ancestor block elements at + * start and end of the range should be joined. + */ + MOZ_CAN_RUN_SCRIPT Result<bool, nsresult> + DeleteNodesEntirelyInRangeButKeepTableStructure( + HTMLEditor& aHTMLEditor, nsRange& aRange, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed); + bool NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure( + const HTMLEditor& aHTMLEditor, + const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) + const; + Result<bool, nsresult> + ComputeRangesToDeleteNodesEntirelyInRangeButKeepTableStructure( + const HTMLEditor& aHTMLEditor, nsRange& aRange, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) + const; + + /** + * DeleteContentButKeepTableStructure() removes aContent if it's an element + * which is part of a table structure. If it's a part of table structure, + * removes its all children recursively. I.e., this may delete all of a + * table, but won't break table structure partially. + * + * @param aContent The content which or whose all children should + * be removed. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteContentButKeepTableStructure(HTMLEditor& aHTMLEditor, + nsIContent& aContent); + + /** + * DeleteTextAtStartAndEndOfRange() removes text if start and/or end of + * aRange is in a text node. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult + DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange); + + class MOZ_STACK_CLASS AutoInclusiveAncestorBlockElementsJoiner final { + public: + AutoInclusiveAncestorBlockElementsJoiner() = delete; + AutoInclusiveAncestorBlockElementsJoiner( + nsIContent& aInclusiveDescendantOfLeftBlockElement, + nsIContent& aInclusiveDescendantOfRightBlockElement) + : mInclusiveDescendantOfLeftBlockElement( + aInclusiveDescendantOfLeftBlockElement), + mInclusiveDescendantOfRightBlockElement( + aInclusiveDescendantOfRightBlockElement), + mCanJoinBlocks(false), + mFallbackToDeleteLeafContent(false) {} + + bool IsSet() const { return mLeftBlockElement && mRightBlockElement; } + bool IsSameBlockElement() const { + return mLeftBlockElement && mLeftBlockElement == mRightBlockElement; + } + + const EditorDOMPoint& PointRefToPutCaret() const { + return mPointToPutCaret; + } + + /** + * Prepare for joining inclusive ancestor block elements. When this + * returns false, the deletion should be canceled. + */ + Result<bool, nsresult> Prepare(const HTMLEditor& aHTMLEditor, + const Element& aEditingHost); + + /** + * When this returns true, this can join the blocks with `Run()`. + */ + bool CanJoinBlocks() const { return mCanJoinBlocks; } + + /** + * When this returns true, `Run()` must return "ignored" so that + * caller can skip calling `Run()`. This is available only when + * `CanJoinBlocks()` returns `true`. + * TODO: This should be merged into `CanJoinBlocks()` in the future. + */ + bool ShouldDeleteLeafContentInstead() const { + MOZ_ASSERT(CanJoinBlocks()); + return mFallbackToDeleteLeafContent; + } + + /** + * ComputeRangesToDelete() extends aRangesToDelete includes the element + * boundaries between joining blocks. If they won't be joined, this + * collapses the range to aCaretPoint. + */ + nsresult ComputeRangesToDelete(const HTMLEditor& aHTMLEditor, + const EditorDOMPoint& aCaretPoint, + AutoRangeArray& aRangesToDelete) const; + + /** + * Join inclusive ancestor block elements which are found by preceding + * Preare() call. + * The right element is always joined to the left element. + * If the elements are the same type and not nested within each other, + * JoinEditableNodesWithTransaction() is called (example, joining two + * list items together into one). + * If the elements are not the same type, or one is a descendant of the + * other, we instead destroy the right block placing its children into + * left block. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run( + HTMLEditor& aHTMLEditor, const Element& aEditingHost); + + private: + /** + * This method returns true when + * `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`, + * `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` and + * `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` handle it + * with the `if` block of their main blocks. + */ + bool CanMergeLeftAndRightBlockElements() const { + if (!IsSet()) { + return false; + } + // `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()` + if (mPointContainingTheOtherBlockElement.GetContainer() == + mRightBlockElement) { + return mNewListElementTagNameOfRightListElement.isSome(); + } + // `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` + if (mPointContainingTheOtherBlockElement.GetContainer() == + mLeftBlockElement) { + return mNewListElementTagNameOfRightListElement.isSome() && + !mRightBlockElement->GetChildCount(); + } + MOZ_ASSERT(!mPointContainingTheOtherBlockElement.IsSet()); + // `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` + return mNewListElementTagNameOfRightListElement.isSome() || + mLeftBlockElement->NodeInfo()->NameAtom() == + mRightBlockElement->NodeInfo()->NameAtom(); + } + + OwningNonNull<nsIContent> mInclusiveDescendantOfLeftBlockElement; + OwningNonNull<nsIContent> mInclusiveDescendantOfRightBlockElement; + RefPtr<Element> mLeftBlockElement; + RefPtr<Element> mRightBlockElement; + Maybe<nsAtom*> mNewListElementTagNameOfRightListElement; + EditorDOMPoint mPointContainingTheOtherBlockElement; + EditorDOMPoint mPointToPutCaret; + RefPtr<dom::HTMLBRElement> mPrecedingInvisibleBRElement; + bool mCanJoinBlocks; + bool mFallbackToDeleteLeafContent; + }; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + // AutoInclusiveAncestorBlockElementsJoiner + + enum class Mode { + NotInitialized, + JoinCurrentBlock, + JoinOtherBlock, + JoinBlocksInSameParent, + DeleteBRElement, + DeleteContentInRanges, + DeleteNonCollapsedRanges, + }; + AutoDeleteRangesHandler* mDeleteRangesHandler; + const AutoDeleteRangesHandler& mDeleteRangesHandlerConst; + nsCOMPtr<nsIContent> mLeftContent; + nsCOMPtr<nsIContent> mRightContent; + nsCOMPtr<nsIContent> mLeafContentInOtherBlock; + // mSkippedInvisibleContents stores all content nodes which are skipped at + // scanning mLeftContent and mRightContent. The content nodes should be + // removed at deletion. + AutoTArray<OwningNonNull<nsIContent>, 8> mSkippedInvisibleContents; + RefPtr<dom::HTMLBRElement> mBRElement; + Mode mMode = Mode::NotInitialized; + }; // HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner + + class MOZ_STACK_CLASS AutoEmptyBlockAncestorDeleter final { + public: + /** + * ScanEmptyBlockInclusiveAncestor() scans an inclusive ancestor element + * which is empty and a block element. Then, stores the result and + * returns the found empty block element. + * + * @param aHTMLEditor The HTMLEditor. + * @param aStartContent Start content to look for empty ancestors. + */ + [[nodiscard]] Element* ScanEmptyBlockInclusiveAncestor( + const HTMLEditor& aHTMLEditor, nsIContent& aStartContent); + + /** + * ComputeTargetRanges() computes "target ranges" for deleting + * `mEmptyInclusiveAncestorBlockElement`. + */ + nsresult ComputeTargetRanges(const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + const Element& aEditingHost, + AutoRangeArray& aRangesToDelete) const; + + /** + * Deletes found empty block element by `ScanEmptyBlockInclusiveAncestor()`. + * If found one is a list item element, calls + * `MaybeInsertBRElementBeforeEmptyListItemElement()` before deleting + * the list item element. + * If found empty ancestor is not a list item element, + * `GetNewCaretPosition()` will be called to determine new caret position. + * Finally, removes the empty block ancestor. + * + * @param aHTMLEditor The HTMLEditor. + * @param aDirectionAndAmount If found empty ancestor block is a list item + * element, this is ignored. Otherwise: + * - If eNext, eNextWord or eToEndOfLine, + * collapse Selection to after found empty + * ancestor. + * - If ePrevious, ePreviousWord or + * eToBeginningOfLine, collapse Selection to + * end of previous editable node. + * - Otherwise, eNone is allowed but does + * nothing. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> Run( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount); + + private: + /** + * MaybeReplaceSubListWithNewListItem() replaces + * mEmptyInclusiveAncestorBlockElement with new list item element + * (containing <br>) if: + * - mEmptyInclusiveAncestorBlockElement is a list element + * - The parent of mEmptyInclusiveAncestorBlockElement is a list element + * - The parent becomes empty after deletion + * If this does not perform the replacement, returns "ignored". + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<EditActionResult, nsresult> + MaybeReplaceSubListWithNewListItem(HTMLEditor& aHTMLEditor); + + /** + * MaybeInsertBRElementBeforeEmptyListItemElement() inserts a `<br>` element + * if `mEmptyInclusiveAncestorBlockElement` is a list item element which + * is first editable element in its parent, and its grand parent is not a + * list element, inserts a `<br>` element before the empty list item. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<RefPtr<Element>, nsresult> + MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor); + + /** + * GetNewCaretPosition() returns new caret position after deleting + * `mEmptyInclusiveAncestorBlockElement`. + */ + [[nodiscard]] Result<CaretPoint, nsresult> GetNewCaretPosition( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount) const; + + RefPtr<Element> mEmptyInclusiveAncestorBlockElement; + }; // HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter + + const AutoDeleteRangesHandler* const mParent; + nsIEditor::EDirection mOriginalDirectionAndAmount; + nsIEditor::EStripWrappers mOriginalStripWrappers; +}; // HTMLEditor::AutoDeleteRangesHandler + +nsresult HTMLEditor::ComputeTargetRanges( + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + + Element* editingHost = ComputeEditingHost(); + if (!editingHost) { + aRangesToDelete.RemoveAllRanges(); + return NS_ERROR_EDITOR_NO_EDITABLE_RANGE; + } + + // First check for table selection mode. If so, hand off to table editor. + SelectedTableCellScanner scanner(aRangesToDelete); + if (scanner.IsInTableCellSelectionMode()) { + // If it's in table cell selection mode, we'll delete all childen in + // the all selected table cell elements, + if (scanner.ElementsRef().Length() == aRangesToDelete.Ranges().Length()) { + return NS_OK; + } + // but will ignore all ranges which does not select a table cell. + size_t removedRanges = 0; + for (size_t i = 1; i < scanner.ElementsRef().Length(); i++) { + if (HTMLEditUtils::GetTableCellElementIfOnlyOneSelected( + aRangesToDelete.Ranges()[i - removedRanges]) != + scanner.ElementsRef()[i]) { + // XXX Need to manage anchor-focus range too! + aRangesToDelete.Ranges().RemoveElementAt(i - removedRanges); + removedRanges++; + } + } + return NS_OK; + } + + aRangesToDelete.EnsureOnlyEditableRanges(*editingHost); + if (aRangesToDelete.Ranges().IsEmpty()) { + NS_WARNING( + "There is no range which we can delete entire of or around the caret"); + return NS_ERROR_EDITOR_NO_EDITABLE_RANGE; + } + AutoDeleteRangesHandler deleteHandler; + // Should we delete target ranges which cannot delete actually? + nsresult rv = deleteHandler.ComputeRangesToDelete( + *this, aDirectionAndAmount, aRangesToDelete, *editingHost); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::ComputeRangesToDelete() failed"); + return rv; +} + +Result<EditActionResult, nsresult> HTMLEditor::HandleDeleteSelection( + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip || + aStripWrappers == nsIEditor::eNoStrip); + + if (MOZ_UNLIKELY(!SelectionRef().RangeCount())) { + return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE); + } + + RefPtr<Element> editingHost = ComputeEditingHost(); + if (MOZ_UNLIKELY(!editingHost)) { + return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE); + } + + // Remember that we did a selection deletion. Used by + // CreateStyleForInsertText() + TopLevelEditSubActionDataRef().mDidDeleteSelection = true; + + if (MOZ_UNLIKELY(IsEmpty())) { + return EditActionResult::CanceledResult(); + } + + // First check for table selection mode. If so, hand off to table editor. + if (HTMLEditUtils::IsInTableCellSelectionMode(SelectionRef())) { + nsresult rv = DeleteTableCellContentsWithTransaction(); + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteTableCellContentsWithTransaction() failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); + } + + AutoRangeArray rangesToDelete(SelectionRef()); + rangesToDelete.EnsureOnlyEditableRanges(*editingHost); + if (MOZ_UNLIKELY(rangesToDelete.Ranges().IsEmpty())) { + NS_WARNING( + "There is no range which we can delete entire the ranges or around the " + "caret"); + return Err(NS_ERROR_EDITOR_NO_EDITABLE_RANGE); + } + AutoDeleteRangesHandler deleteHandler; + Result<EditActionResult, nsresult> result = deleteHandler.Run( + *this, aDirectionAndAmount, aStripWrappers, rangesToDelete, *editingHost); + if (MOZ_UNLIKELY(result.isErr()) || result.inspect().Canceled()) { + NS_WARNING_ASSERTION(result.isOk(), + "AutoDeleteRangesHandler::Run() failed"); + return result; + } + + // XXX At here, selection may have no range because of mutation event + // listeners can do anything so that we should just return NS_OK instead + // of returning error. + const auto atNewStartOfSelection = + GetFirstSelectionStartPoint<EditorDOMPoint>(); + if (NS_WARN_IF(!atNewStartOfSelection.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + if (atNewStartOfSelection.IsInContentNode()) { + nsresult rv = DeleteMostAncestorMailCiteElementIfEmpty( + MOZ_KnownLive(*atNewStartOfSelection.ContainerAs<nsIContent>())); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::DeleteMostAncestorMailCiteElementIfEmpty() failed"); + return Err(rv); + } + } + return EditActionResult::HandledResult(); +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); + + mOriginalDirectionAndAmount = aDirectionAndAmount; + mOriginalStripWrappers = nsIEditor::eNoStrip; + + if (aHTMLEditor.mPaddingBRElementForEmptyEditor) { + nsresult rv = aRangesToDelete.Collapse( + EditorRawDOMPoint(aHTMLEditor.mPaddingBRElementForEmptyEditor)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed"); + return rv; + } + + SelectionWasCollapsed selectionWasCollapsed = aRangesToDelete.IsCollapsed() + ? SelectionWasCollapsed::Yes + : SelectionWasCollapsed::No; + if (selectionWasCollapsed == SelectionWasCollapsed::Yes) { + const auto startPoint = + aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>(); + if (NS_WARN_IF(!startPoint.IsSet())) { + return NS_ERROR_FAILURE; + } + RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost(); + if (NS_WARN_IF(!editingHost)) { + return NS_ERROR_FAILURE; + } + if (startPoint.IsInContentNode()) { + AutoEmptyBlockAncestorDeleter deleter; + if (deleter.ScanEmptyBlockInclusiveAncestor( + aHTMLEditor, *startPoint.ContainerAs<nsIContent>())) { + nsresult rv = deleter.ComputeTargetRanges( + aHTMLEditor, aDirectionAndAmount, *editingHost, aRangesToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoEmptyBlockAncestorDeleter::ComputeTargetRanges() failed"); + return rv; + } + } + + // We shouldn't update caret bidi level right now, but we need to check + // whether the deletion will be canceled or not. + AutoCaretBidiLevelManager bidiLevelManager(aHTMLEditor, aDirectionAndAmount, + startPoint); + if (bidiLevelManager.Failed()) { + NS_WARNING( + "EditorBase::AutoCaretBidiLevelManager failed to initialize itself"); + return NS_ERROR_FAILURE; + } + if (bidiLevelManager.Canceled()) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + // AutoRangeArray::ExtendAnchorFocusRangeFor() will use `nsFrameSelection` + // to extend the range for deletion. But if focus event doesn't receive + // yet, ancestor isn't set. So we must set root element of editor to + // ancestor temporarily. + AutoSetTemporaryAncestorLimiter autoSetter( + aHTMLEditor, aHTMLEditor.SelectionRef(), *startPoint.GetContainer(), + &aRangesToDelete); + + Result<nsIEditor::EDirection, nsresult> extendResult = + aRangesToDelete.ExtendAnchorFocusRangeFor(aHTMLEditor, + aDirectionAndAmount); + if (extendResult.isErr()) { + NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed"); + return extendResult.unwrapErr(); + } + + // For compatibility with other browsers, we should set target ranges + // to start from and/or end after an atomic content rather than start + // from preceding text node end nor end at following text node start. + Result<bool, nsresult> shrunkenResult = + aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent( + aHTMLEditor, aDirectionAndAmount, + AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse, + editingHost); + if (shrunkenResult.isErr()) { + NS_WARNING( + "AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent() " + "failed"); + return shrunkenResult.unwrapErr(); + } + + if (!shrunkenResult.inspect() || !aRangesToDelete.IsCollapsed()) { + aDirectionAndAmount = extendResult.unwrap(); + } + + if (aDirectionAndAmount == nsIEditor::eNone) { + MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1); + if (!CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)) { + // XXX In this case, do we need to modify the range again? + return NS_SUCCESS_DOM_NO_OPERATION; + } + nsresult rv = FallbackToComputeRangesToDeleteRangesWithTransaction( + aHTMLEditor, aRangesToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::" + "FallbackToComputeRangesToDeleteRangesWithTransaction() failed"); + return rv; + } + + if (aRangesToDelete.IsCollapsed()) { + const auto caretPoint = + aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>(); + if (MOZ_UNLIKELY(NS_WARN_IF(!caretPoint.IsInContentNode()))) { + return NS_ERROR_FAILURE; + } + if (!EditorUtils::IsEditableContent(*caretPoint.ContainerAs<nsIContent>(), + EditorType::HTML)) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + WSRunScanner wsRunScannerAtCaret(editingHost, caretPoint); + WSScanResult scanFromCaretPointResult = + aDirectionAndAmount == nsIEditor::eNext + ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom( + caretPoint) + : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( + caretPoint); + if (scanFromCaretPointResult.Failed()) { + NS_WARNING( + "WSRunScanner::Scan(Next|Previous)VisibleNodeOrBlockBoundaryFrom() " + "failed"); + return NS_ERROR_FAILURE; + } + if (!scanFromCaretPointResult.GetContent()) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + if (scanFromCaretPointResult.ReachedBRElement()) { + if (scanFromCaretPointResult.BRElementPtr() == + wsRunScannerAtCaret.GetEditingHost()) { + return NS_OK; + } + if (!EditorUtils::IsEditableContent( + *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + if (HTMLEditUtils::IsInvisibleBRElement( + *scanFromCaretPointResult.BRElementPtr())) { + EditorDOMPoint newCaretPosition = + aDirectionAndAmount == nsIEditor::eNext + ? EditorDOMPoint::After( + *scanFromCaretPointResult.BRElementPtr()) + : EditorDOMPoint(scanFromCaretPointResult.BRElementPtr()); + if (NS_WARN_IF(!newCaretPosition.IsSet())) { + return NS_ERROR_FAILURE; + } + AutoHideSelectionChanges blockSelectionListeners( + aHTMLEditor.SelectionRef()); + nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPosition); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return NS_ERROR_FAILURE; + } + if (NS_WARN_IF(!aHTMLEditor.SelectionRef().RangeCount())) { + return NS_ERROR_UNEXPECTED; + } + aRangesToDelete.Initialize(aHTMLEditor.SelectionRef()); + AutoDeleteRangesHandler anotherHandler(this); + rv = anotherHandler.ComputeRangesToDelete( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aEditingHost); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "Recursive AutoDeleteRangesHandler::ComputeRangesToDelete() " + "failed"); + + rv = aHTMLEditor.CollapseSelectionTo(caretPoint); + 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 to " + "restore original selection, but ignored"); + + MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1); + // If the range is collapsed, there is no content which should + // be removed together. In this case, only the invisible `<br>` + // element should be selected. + if (aRangesToDelete.IsCollapsed()) { + nsresult rv = aRangesToDelete.SelectNode( + *scanFromCaretPointResult.BRElementPtr()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoRangeArray::SelectNode() failed"); + return rv; + } + + // Otherwise, extend the range to contain the invisible `<br>` + // element. + if (EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr()) + .IsBefore( + aRangesToDelete + .GetFirstRangeStartPoint<EditorRawDOMPoint>())) { + nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd( + EditorRawDOMPoint(scanFromCaretPointResult.BRElementPtr()) + .ToRawRangeBoundary(), + aRangesToDelete.FirstRangeRef()->EndRef()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "nsRange::SetStartAndEnd() failed"); + return rv; + } + if (aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>() + .IsBefore(EditorRawDOMPoint::After( + *scanFromCaretPointResult.BRElementPtr()))) { + nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd( + aRangesToDelete.FirstRangeRef()->StartRef(), + EditorRawDOMPoint::After( + *scanFromCaretPointResult.BRElementPtr()) + .ToRawRangeBoundary()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "nsRange::SetStartAndEnd() failed"); + return rv; + } + NS_WARNING("Was the invisible `<br>` element selected?"); + return NS_OK; + } + } + + nsresult rv = ComputeRangesToDeleteAroundCollapsedRanges( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete, + wsRunScannerAtCaret, scanFromCaretPointResult, aEditingHost); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges(" + ") failed"); + return rv; + } + } + + nsresult rv = ComputeRangesToDeleteNonCollapsedRanges( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete, selectionWasCollapsed, + aEditingHost); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::" + "ComputeRangesToDeleteNonCollapsedRanges() failed"); + return rv; +} + +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete, + const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip || + aStripWrappers == nsIEditor::eNoStrip); + MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); + + mOriginalDirectionAndAmount = aDirectionAndAmount; + mOriginalStripWrappers = aStripWrappers; + + if (MOZ_UNLIKELY(aHTMLEditor.IsEmpty())) { + return EditActionResult::CanceledResult(); + } + + // selectionWasCollapsed is used later to determine whether we should join + // blocks in HandleDeleteNonCollapsedRanges(). We don't really care about + // collapsed because it will be modified by + // AutoRangeArray::ExtendAnchorFocusRangeFor() later. + // AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner should + // happen if the original selection is collapsed and the cursor is at the end + // of a block element, in which case + // AutoRangeArray::ExtendAnchorFocusRangeFor() would always make the selection + // not collapsed. + SelectionWasCollapsed selectionWasCollapsed = aRangesToDelete.IsCollapsed() + ? SelectionWasCollapsed::Yes + : SelectionWasCollapsed::No; + + if (selectionWasCollapsed == SelectionWasCollapsed::Yes) { + const auto startPoint = + aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>(); + if (NS_WARN_IF(!startPoint.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + + // If we are inside an empty block, delete it. + if (startPoint.IsInContentNode()) { +#ifdef DEBUG + nsMutationGuard debugMutation; +#endif // #ifdef DEBUG + AutoEmptyBlockAncestorDeleter deleter; + if (deleter.ScanEmptyBlockInclusiveAncestor( + aHTMLEditor, *startPoint.ContainerAs<nsIContent>())) { + Result<EditActionResult, nsresult> result = + deleter.Run(aHTMLEditor, aDirectionAndAmount); + if (MOZ_UNLIKELY(result.isErr()) || result.inspect().Handled()) { + NS_WARNING_ASSERTION(result.isOk(), + "AutoEmptyBlockAncestorDeleter::Run() failed"); + return result; + } + } + MOZ_ASSERT(!debugMutation.Mutated(0), + "AutoEmptyBlockAncestorDeleter shouldn't modify the DOM tree " + "if it returns not handled nor error"); + } + + // Test for distance between caret and text that will be deleted. + // Note that this call modifies `nsFrameSelection` without modifying + // `Selection`. However, it does not have problem for now because + // it'll be referred by `AutoRangeArray::ExtendAnchorFocusRangeFor()` + // before modifying `Selection`. + // XXX This looks odd. `ExtendAnchorFocusRangeFor()` will extend + // anchor-focus range, but here refers the first range. + AutoCaretBidiLevelManager bidiLevelManager(aHTMLEditor, aDirectionAndAmount, + startPoint); + if (MOZ_UNLIKELY(bidiLevelManager.Failed())) { + NS_WARNING( + "EditorBase::AutoCaretBidiLevelManager failed to initialize itself"); + return Err(NS_ERROR_FAILURE); + } + bidiLevelManager.MaybeUpdateCaretBidiLevel(aHTMLEditor); + if (bidiLevelManager.Canceled()) { + return EditActionResult::CanceledResult(); + } + + // AutoRangeArray::ExtendAnchorFocusRangeFor() will use `nsFrameSelection` + // to extend the range for deletion. But if focus event doesn't receive + // yet, ancestor isn't set. So we must set root element of editor to + // ancestor temporarily. + AutoSetTemporaryAncestorLimiter autoSetter( + aHTMLEditor, aHTMLEditor.SelectionRef(), *startPoint.GetContainer(), + &aRangesToDelete); + + // Calling `ExtendAnchorFocusRangeFor()` and + // `ShrinkRangesIfStartFromOrEndAfterAtomicContent()` may move caret to + // the container of deleting atomic content. However, it may be different + // from the original caret's container. The original caret container may + // be important to put caret after deletion so that let's cache the + // original position. + Maybe<EditorDOMPoint> caretPoint; + if (aRangesToDelete.IsCollapsed() && !aRangesToDelete.Ranges().IsEmpty()) { + caretPoint = + Some(aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>()); + if (NS_WARN_IF(!caretPoint.ref().IsInContentNode())) { + return Err(NS_ERROR_FAILURE); + } + } + + Result<nsIEditor::EDirection, nsresult> extendResult = + aRangesToDelete.ExtendAnchorFocusRangeFor(aHTMLEditor, + aDirectionAndAmount); + if (MOZ_UNLIKELY(extendResult.isErr())) { + NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed"); + return extendResult.propagateErr(); + } + if (caretPoint.isSome() && + MOZ_UNLIKELY(!caretPoint.ref().IsSetAndValid())) { + NS_WARNING("The caret position became invalid"); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + // If there is only one range and it selects an atomic content, we should + // delete it with collapsed range path for making consistent behavior + // between both cases, the content is selected case and caret is at it or + // after it case. + Result<bool, nsresult> shrunkenResult = + aRangesToDelete.ShrinkRangesIfStartFromOrEndAfterAtomicContent( + aHTMLEditor, aDirectionAndAmount, + AutoRangeArray::IfSelectingOnlyOneAtomicContent::Collapse, + &aEditingHost); + if (MOZ_UNLIKELY(shrunkenResult.isErr())) { + NS_WARNING( + "AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent() " + "failed"); + return shrunkenResult.propagateErr(); + } + + if (!shrunkenResult.inspect() || !aRangesToDelete.IsCollapsed()) { + aDirectionAndAmount = extendResult.unwrap(); + } + + if (aDirectionAndAmount == nsIEditor::eNone) { + MOZ_ASSERT(aRangesToDelete.Ranges().Length() == 1); + if (!CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)) { + return EditActionResult::IgnoredResult(); + } + Result<CaretPoint, nsresult> caretPointOrError = + FallbackToDeleteRangesWithTransaction(aHTMLEditor, aRangesToDelete); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "AutoDeleteRangesHandler::FallbackToDeleteRangesWithTransaction() " + "failed"); + } + nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( + aHTMLEditor, {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"); + // Don't return "ignored" to avoid to fall it back to delete ranges + // recursively. + return EditActionResult::HandledResult(); + } + + if (aRangesToDelete.IsCollapsed()) { + // Use the original caret position for handling the deletion around + // collapsed range because the container may be different from the + // new collapsed position's container. + if (!EditorUtils::IsEditableContent( + *caretPoint.ref().ContainerAs<nsIContent>(), EditorType::HTML)) { + return EditActionResult::CanceledResult(); + } + WSRunScanner wsRunScannerAtCaret(&aEditingHost, caretPoint.ref()); + WSScanResult scanFromCaretPointResult = + aDirectionAndAmount == nsIEditor::eNext + ? wsRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom( + caretPoint.ref()) + : wsRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( + caretPoint.ref()); + if (MOZ_UNLIKELY(scanFromCaretPointResult.Failed())) { + NS_WARNING( + "WSRunScanner::Scan(Next|Previous)VisibleNodeOrBlockBoundaryFrom() " + "failed"); + return Err(NS_ERROR_FAILURE); + } + if (!scanFromCaretPointResult.GetContent()) { + return EditActionResult::CanceledResult(); + } + // Short circuit for invisible breaks. delete them and recurse. + if (scanFromCaretPointResult.ReachedBRElement()) { + if (scanFromCaretPointResult.BRElementPtr() == &aEditingHost) { + return EditActionResult::HandledResult(); + } + if (!EditorUtils::IsEditableContent( + *scanFromCaretPointResult.BRElementPtr(), EditorType::HTML)) { + return EditActionResult::CanceledResult(); + } + if (HTMLEditUtils::IsInvisibleBRElement( + *scanFromCaretPointResult.BRElementPtr())) { + // TODO: We should extend the range to delete again before/after + // the caret point and use `HandleDeleteNonCollapsedRanges()` + // instead after we would create delete range computation + // method at switching to the new white-space normalizer. + Result<CaretPoint, nsresult> caretPointOrError = + WhiteSpaceVisibilityKeeper:: + DeleteContentNodeAndJoinTextNodesAroundIt( + aHTMLEditor, + MOZ_KnownLive(*scanFromCaretPointResult.BRElementPtr()), + caretPoint.ref(), aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "WhiteSpaceVisibilityKeeper::" + "DeleteContentNodeAndJoinTextNodesAroundIt() failed"); + return caretPointOrError.propagateErr(); + } + if (caretPointOrError.inspect().HasCaretPointSuggestion()) { + caretPoint = Some(caretPointOrError.unwrap().UnwrapCaretPoint()); + } + if (NS_WARN_IF(!caretPoint->IsSetAndValid())) { + return Err(NS_ERROR_FAILURE); + } + AutoRangeArray rangesToDelete(caretPoint.ref()); + if (aHTMLEditor.MayHaveMutationEventListeners( + NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED | + NS_EVENT_BITS_MUTATION_NODEREMOVED | + NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT)) { + // Let's check whether there is new invisible `<br>` element + // for avoiding infinite recursive calls. + WSRunScanner wsRunScannerAtCaret(&aEditingHost, caretPoint.ref()); + WSScanResult scanFromCaretPointResult = + aDirectionAndAmount == nsIEditor::eNext + ? wsRunScannerAtCaret + .ScanNextVisibleNodeOrBlockBoundaryFrom( + caretPoint.ref()) + : wsRunScannerAtCaret + .ScanPreviousVisibleNodeOrBlockBoundaryFrom( + caretPoint.ref()); + if (MOZ_UNLIKELY(scanFromCaretPointResult.Failed())) { + NS_WARNING( + "WSRunScanner::Scan(Next|Previous)" + "VisibleNodeOrBlockBoundaryFrom() failed"); + return Err(NS_ERROR_FAILURE); + } + if (MOZ_UNLIKELY( + scanFromCaretPointResult.ReachedInvisibleBRElement())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + } + AutoDeleteRangesHandler anotherHandler(this); + Result<EditActionResult, nsresult> result = + anotherHandler.Run(aHTMLEditor, aDirectionAndAmount, + aStripWrappers, rangesToDelete, aEditingHost); + NS_WARNING_ASSERTION( + result.isOk(), "Recursive AutoDeleteRangesHandler::Run() failed"); + return result; + } + } + + Result<EditActionResult, nsresult> result = + HandleDeleteAroundCollapsedRanges( + aHTMLEditor, aDirectionAndAmount, aStripWrappers, aRangesToDelete, + wsRunScannerAtCaret, scanFromCaretPointResult, aEditingHost); + NS_WARNING_ASSERTION(result.isOk(), + "AutoDeleteRangesHandler::" + "HandleDeleteAroundCollapsedRanges() failed"); + return result; + } + } + + Result<EditActionResult, nsresult> result = HandleDeleteNonCollapsedRanges( + aHTMLEditor, aDirectionAndAmount, aStripWrappers, aRangesToDelete, + selectionWasCollapsed, aEditingHost); + NS_WARNING_ASSERTION( + result.isOk(), + "AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges() failed"); + return result; +} + +nsresult +HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, const WSRunScanner& aWSRunScannerAtCaret, + const WSScanResult& aScanFromCaretPointResult, + const Element& aEditingHost) const { + if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() || + aScanFromCaretPointResult.InNonCollapsibleCharacters() || + aScanFromCaretPointResult.ReachedPreformattedLineBreak()) { + nsresult rv = aRangesToDelete.Collapse( + aScanFromCaretPointResult.Point<EditorRawDOMPoint>()); + if (MOZ_UNLIKELY(NS_FAILED(rv))) { + NS_WARNING("AutoRangeArray::Collapse() failed"); + return NS_ERROR_FAILURE; + } + rv = ComputeRangesToDeleteTextAroundCollapsedRanges( + aDirectionAndAmount, aRangesToDelete, aEditingHost); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::" + "ComputeRangesToDeleteTextAroundCollapsedRanges() failed"); + return rv; + } + + if (aScanFromCaretPointResult.ReachedSpecialContent() || + aScanFromCaretPointResult.ReachedBRElement() || + aScanFromCaretPointResult.ReachedHRElement() || + aScanFromCaretPointResult.ReachedNonEditableOtherBlockElement()) { + if (aScanFromCaretPointResult.GetContent() == + aWSRunScannerAtCaret.GetEditingHost()) { + return NS_OK; + } + nsIContent* atomicContent = GetAtomicContentToDelete( + aDirectionAndAmount, aWSRunScannerAtCaret, aScanFromCaretPointResult); + if (!HTMLEditUtils::IsRemovableNode(*atomicContent)) { + NS_WARNING( + "AutoDeleteRangesHandler::GetAtomicContentToDelete() cannot find " + "removable atomic content"); + return NS_ERROR_FAILURE; + } + nsresult rv = ComputeRangesToDeleteAtomicContent( + aWSRunScannerAtCaret.GetEditingHost(), *atomicContent, aRangesToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::ComputeRangesToDeleteAtomicContent() failed"); + return rv; + } + + if (aScanFromCaretPointResult.ReachedOtherBlockElement()) { + if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { + return NS_ERROR_FAILURE; + } + AutoBlockElementsJoiner joiner(*this); + if (!joiner.PrepareToDeleteAtOtherBlockBoundary( + aHTMLEditor, aDirectionAndAmount, + *aScanFromCaretPointResult.ElementPtr(), + aWSRunScannerAtCaret.ScanStartRef(), aWSRunScannerAtCaret)) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + nsresult rv = joiner.ComputeRangesToDelete( + aHTMLEditor, aDirectionAndAmount, aWSRunScannerAtCaret.ScanStartRef(), + aRangesToDelete, aEditingHost); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::ComputeRangesToDelete() " + "failed (other block boundary)"); + return rv; + } + + if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) { + if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { + return NS_ERROR_FAILURE; + } + AutoBlockElementsJoiner joiner(*this); + if (!joiner.PrepareToDeleteAtCurrentBlockBoundary( + aHTMLEditor, aDirectionAndAmount, + *aScanFromCaretPointResult.ElementPtr(), + aWSRunScannerAtCaret.ScanStartRef())) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + nsresult rv = joiner.ComputeRangesToDelete( + aHTMLEditor, aDirectionAndAmount, aWSRunScannerAtCaret.ScanStartRef(), + aRangesToDelete, aEditingHost); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::ComputeRangesToDelete() " + "failed (current block boundary)"); + return rv; + } + + return NS_OK; +} + +Result<EditActionResult, nsresult> +HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete, + const WSRunScanner& aWSRunScannerAtCaret, + const WSScanResult& aScanFromCaretPointResult, + const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable()); + MOZ_ASSERT(aRangesToDelete.IsCollapsed()); + MOZ_ASSERT(aDirectionAndAmount != nsIEditor::eNone); + MOZ_ASSERT(aWSRunScannerAtCaret.ScanStartRef().IsInContentNode()); + MOZ_ASSERT(EditorUtils::IsEditableContent( + *aWSRunScannerAtCaret.ScanStartRef().ContainerAs<nsIContent>(), + EditorType::HTML)); + + if (StaticPrefs::editor_white_space_normalization_blink_compatible()) { + if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() || + aScanFromCaretPointResult.InNonCollapsibleCharacters() || + aScanFromCaretPointResult.ReachedPreformattedLineBreak()) { + nsresult rv = aRangesToDelete.Collapse( + aScanFromCaretPointResult.Point<EditorRawDOMPoint>()); + if (NS_FAILED(rv)) { + NS_WARNING("AutoRangeArray::Collapse() failed"); + return Err(NS_ERROR_FAILURE); + } + Result<CaretPoint, nsresult> caretPointOrError = + HandleDeleteTextAroundCollapsedRanges( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "AutoDeleteRangesHandler::HandleDeleteTextAroundCollapsedRanges() " + "failed"); + return caretPointOrError.propagateErr(); + } + rv = caretPointOrError.unwrap().SuggestCaretPointTo( + aHTMLEditor, {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::SuggestCaretPoint() failed, but ignored"); + return EditActionResult::HandledResult(); + } + } + + if (aScanFromCaretPointResult.InCollapsibleWhiteSpaces() || + aScanFromCaretPointResult.ReachedPreformattedLineBreak()) { + Result<CaretPoint, nsresult> caretPointOrError = + HandleDeleteCollapsedSelectionAtWhiteSpaces( + aHTMLEditor, aDirectionAndAmount, + aWSRunScannerAtCaret.ScanStartRef(), aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "AutoDeleteRangesHandler::" + "HandleDeleteCollapsedSelectionAtWhiteSpaces() failed"); + return caretPointOrError.propagateErr(); + } + nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo( + aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion}); + 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(); + } + + if (aScanFromCaretPointResult.InNonCollapsibleCharacters()) { + if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsText())) { + return Err(NS_ERROR_FAILURE); + } + Result<CaretPoint, nsresult> caretPointOrError = + HandleDeleteCollapsedSelectionAtVisibleChar( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete, + aScanFromCaretPointResult.Point<EditorDOMPoint>(), aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "AutoDeleteRangesHandler::" + "HandleDeleteCollapsedSelectionAtVisibleChar() failed"); + return caretPointOrError.propagateErr(); + } + nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo( + aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion}); + 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(); + } + + if (aScanFromCaretPointResult.ReachedSpecialContent() || + aScanFromCaretPointResult.ReachedBRElement() || + aScanFromCaretPointResult.ReachedHRElement() || + aScanFromCaretPointResult.ReachedNonEditableOtherBlockElement()) { + if (aScanFromCaretPointResult.GetContent() == &aEditingHost) { + return EditActionResult::HandledResult(); + } + nsCOMPtr<nsIContent> atomicContent = GetAtomicContentToDelete( + aDirectionAndAmount, aWSRunScannerAtCaret, aScanFromCaretPointResult); + if (MOZ_UNLIKELY(!HTMLEditUtils::IsRemovableNode(*atomicContent))) { + NS_WARNING( + "AutoDeleteRangesHandler::GetAtomicContentToDelete() cannot find " + "removable atomic content"); + return Err(NS_ERROR_FAILURE); + } + Result<CaretPoint, nsresult> caretPointOrError = HandleDeleteAtomicContent( + aHTMLEditor, *atomicContent, aWSRunScannerAtCaret.ScanStartRef(), + aWSRunScannerAtCaret, aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("AutoDeleteRangesHandler::HandleDeleteAtomicContent() failed"); + return caretPointOrError.propagateErr(); + } + nsresult rv = caretPointOrError.unwrap().SuggestCaretPointTo( + aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion}); + 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(); + } + + if (aScanFromCaretPointResult.ReachedOtherBlockElement()) { + if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { + return Err(NS_ERROR_FAILURE); + } + AutoBlockElementsJoiner joiner(*this); + if (!joiner.PrepareToDeleteAtOtherBlockBoundary( + aHTMLEditor, aDirectionAndAmount, + *aScanFromCaretPointResult.ElementPtr(), + aWSRunScannerAtCaret.ScanStartRef(), aWSRunScannerAtCaret)) { + return EditActionResult::CanceledResult(); + } + Result<EditActionResult, nsresult> result = joiner.Run( + aHTMLEditor, aDirectionAndAmount, aStripWrappers, + aWSRunScannerAtCaret.ScanStartRef(), aRangesToDelete, aEditingHost); + NS_WARNING_ASSERTION( + result.isOk(), + "AutoBlockElementsJoiner::Run() failed (other block boundary)"); + return result; + } + + if (aScanFromCaretPointResult.ReachedCurrentBlockBoundary()) { + if (NS_WARN_IF(!aScanFromCaretPointResult.GetContent()->IsElement())) { + return Err(NS_ERROR_FAILURE); + } + AutoBlockElementsJoiner joiner(*this); + if (!joiner.PrepareToDeleteAtCurrentBlockBoundary( + aHTMLEditor, aDirectionAndAmount, + *aScanFromCaretPointResult.ElementPtr(), + aWSRunScannerAtCaret.ScanStartRef())) { + return EditActionResult::CanceledResult(); + } + Result<EditActionResult, nsresult> result = joiner.Run( + aHTMLEditor, aDirectionAndAmount, aStripWrappers, + aWSRunScannerAtCaret.ScanStartRef(), aRangesToDelete, aEditingHost); + NS_WARNING_ASSERTION( + result.isOk(), + "AutoBlockElementsJoiner::Run() failed (current block boundary)"); + return result; + } + + MOZ_ASSERT_UNREACHABLE("New type of reached content hasn't been handled yet"); + return EditActionResult::IgnoredResult(); +} + +nsresult HTMLEditor::AutoDeleteRangesHandler:: + ComputeRangesToDeleteTextAroundCollapsedRanges( + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const { + MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext || + aDirectionAndAmount == nsIEditor::ePrevious); + + const auto caretPosition = + aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>(); + MOZ_ASSERT(caretPosition.IsSetAndValid()); + if (MOZ_UNLIKELY(NS_WARN_IF(!caretPosition.IsInContentNode()))) { + return NS_ERROR_FAILURE; + } + + EditorDOMRangeInTexts rangeToDelete; + if (aDirectionAndAmount == nsIEditor::eNext) { + Result<EditorDOMRangeInTexts, nsresult> result = + WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom(caretPosition, + aEditingHost); + if (result.isErr()) { + NS_WARNING( + "WSRunScanner::GetRangeInTextNodesToForwardDeleteFrom() failed"); + return result.unwrapErr(); + } + rangeToDelete = result.unwrap(); + if (!rangeToDelete.IsPositioned()) { + return NS_OK; // no range to delete, but consume it. + } + } else { + Result<EditorDOMRangeInTexts, nsresult> result = + WSRunScanner::GetRangeInTextNodesToBackspaceFrom(caretPosition, + aEditingHost); + if (result.isErr()) { + NS_WARNING("WSRunScanner::GetRangeInTextNodesToBackspaceFrom() failed"); + return result.unwrapErr(); + } + rangeToDelete = result.unwrap(); + if (!rangeToDelete.IsPositioned()) { + return NS_OK; // no range to delete, but consume it. + } + } + + nsresult rv = aRangesToDelete.SetStartAndEnd(rangeToDelete.StartRef(), + rangeToDelete.EndRef()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoArrayRanges::SetStartAndEnd() failed"); + return rv; +} + +Result<CaretPoint, nsresult> +HTMLEditor::AutoDeleteRangesHandler::HandleDeleteTextAroundCollapsedRanges( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(aDirectionAndAmount == nsIEditor::eNext || + aDirectionAndAmount == nsIEditor::ePrevious); + + nsresult rv = ComputeRangesToDeleteTextAroundCollapsedRanges( + aDirectionAndAmount, aRangesToDelete, aEditingHost); + if (NS_FAILED(rv)) { + return Err(NS_ERROR_FAILURE); + } + if (MOZ_UNLIKELY(aRangesToDelete.IsCollapsed())) { + return CaretPoint(EditorDOMPoint()); // no range to delete + } + + // FYI: rangeToDelete does not contain newly empty inline ancestors which + // are removed by DeleteTextAndNormalizeSurroundingWhiteSpaces(). + // So, if `getTargetRanges()` needs to include parent empty elements, + // we need to extend the range with + // HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement(). + EditorRawDOMRange rangeToDelete(aRangesToDelete.FirstRangeRef()); + if (MOZ_UNLIKELY(!rangeToDelete.IsInTextNodes())) { + NS_WARNING("The extended range to delete character was not in text nodes"); + return Err(NS_ERROR_FAILURE); + } + + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextAndNormalizeSurroundingWhiteSpaces( + rangeToDelete.StartRef().AsInText(), + rangeToDelete.EndRef().AsInText(), + TreatEmptyTextNodes::RemoveAllEmptyInlineAncestors, + aDirectionAndAmount == nsIEditor::eNext ? DeleteDirection::Forward + : DeleteDirection::Backward); + aHTMLEditor.TopLevelEditSubActionDataRef().mDidNormalizeWhitespaces = true; + NS_WARNING_ASSERTION( + caretPointOrError.isOk(), + "HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces() failed"); + return caretPointOrError; +} + +Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + HandleDeleteCollapsedSelectionAtWhiteSpaces( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aPointToDelete, const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible()); + + EditorDOMPoint pointToPutCaret; + if (aDirectionAndAmount == nsIEditor::eNext) { + Result<CaretPoint, nsresult> caretPointOrError = + WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace( + aHTMLEditor, aPointToDelete, aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "WhiteSpaceVisibilityKeeper::DeleteInclusiveNextWhiteSpace() failed"); + return caretPointOrError; + } + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + } else { + Result<CaretPoint, nsresult> caretPointOrError = + WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace( + aHTMLEditor, aPointToDelete, aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "WhiteSpaceVisibilityKeeper::DeletePreviousWhiteSpace() failed"); + return caretPointOrError; + } + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + } + const auto newCaretPosition = + aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>(); + if (MOZ_UNLIKELY(!newCaretPosition.IsSet())) { + NS_WARNING("There was no selection range"); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( + newCaretPosition); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()" + " failed"); + return caretPointOrError; + } + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); + return CaretPoint(std::move(pointToPutCaret)); +} + +Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + HandleDeleteCollapsedSelectionAtVisibleChar( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, + const EditorDOMPoint& aPointAtDeletingChar, + const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable()); + MOZ_ASSERT(!StaticPrefs::editor_white_space_normalization_blink_compatible()); + MOZ_ASSERT(aPointAtDeletingChar.IsSet()); + MOZ_ASSERT(aPointAtDeletingChar.IsInTextNode()); + + OwningNonNull<Text> visibleTextNode = + *aPointAtDeletingChar.ContainerAs<Text>(); + EditorDOMPoint startToDelete, endToDelete; + // FIXME: This does not care grapheme cluster of complicate character + // sequence like Emoji. + // TODO: Investigate what happens if a grapheme cluster which should be + // delete once is split to multiple text nodes. + // TODO: We should stop using this path, instead, we should extend the range + // before calling this method. + if (aDirectionAndAmount == nsIEditor::ePrevious) { + if (MOZ_UNLIKELY(aPointAtDeletingChar.IsStartOfContainer())) { + return Err(NS_ERROR_UNEXPECTED); + } + startToDelete = aPointAtDeletingChar.PreviousPoint(); + endToDelete = aPointAtDeletingChar; + // Bug 1068979: delete both codepoints if surrogate pair + if (!startToDelete.IsStartOfContainer()) { + const nsTextFragment* text = &visibleTextNode->TextFragment(); + if (text->IsLowSurrogateFollowingHighSurrogateAt( + startToDelete.Offset())) { + startToDelete.RewindOffset(); + } + } + } else { + if (NS_WARN_IF(aRangesToDelete.Ranges().IsEmpty()) || + NS_WARN_IF(aRangesToDelete.FirstRangeRef()->GetStartContainer() != + aPointAtDeletingChar.GetContainer()) || + NS_WARN_IF(aRangesToDelete.FirstRangeRef()->GetEndContainer() != + aPointAtDeletingChar.GetContainer())) { + return Err(NS_ERROR_FAILURE); + } + startToDelete = aRangesToDelete.FirstRangeRef()->StartRef(); + endToDelete = aRangesToDelete.FirstRangeRef()->EndRef(); + } + + { + Result<CaretPoint, nsresult> caretPointOrError = + WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints( + aHTMLEditor, &startToDelete, &endToDelete, aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "WhiteSpaceVisibilityKeeper::PrepareToDeleteRangeAndTrackPoints() " + "failed"); + return caretPointOrError.propagateErr(); + } + // Ignore caret position because we'll set caret position below + caretPointOrError.unwrap().IgnoreCaretPointSuggestion(); + } + + if (aHTMLEditor.MayHaveMutationEventListeners( + NS_EVENT_BITS_MUTATION_NODEREMOVED | + NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT | + NS_EVENT_BITS_MUTATION_ATTRMODIFIED | + NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED) && + (NS_WARN_IF(!startToDelete.IsSetAndValid()) || + NS_WARN_IF(!startToDelete.IsInTextNode()) || + NS_WARN_IF(!endToDelete.IsSetAndValid()) || + NS_WARN_IF(!endToDelete.IsInTextNode()) || + NS_WARN_IF(startToDelete.ContainerAs<Text>() != visibleTextNode) || + NS_WARN_IF(endToDelete.ContainerAs<Text>() != visibleTextNode) || + NS_WARN_IF(startToDelete.Offset() >= endToDelete.Offset()))) { + NS_WARNING("Mutation event listener changed the DOM tree"); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + EditorDOMPoint pointToPutCaret = startToDelete; + { + AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction( + visibleTextNode, startToDelete.Offset(), + endToDelete.Offset() - startToDelete.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.propagateErr(); + } + trackPointToPutCaret.FlushAndStopTracking(); + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + } + + // XXX When Backspace key is pressed, Chromium removes following empty + // text nodes when removing the last character of the non-empty text + // node. However, Edge never removes empty text nodes even if + // selection is in the following empty text node(s). For now, we + // should keep our traditional behavior same as Edge for backward + // compatibility. + // XXX When Delete key is pressed, Edge removes all preceding empty + // text nodes when removing the first character of the non-empty + // text node. Chromium removes only selected empty text node and + // following empty text nodes and the first character of the + // non-empty text node. For now, we should keep our traditional + // behavior same as Chromium for backward compatibility. + { + AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + nsresult rv = + DeleteNodeIfInvisibleAndEditableTextNode(aHTMLEditor, visibleTextNode); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode() " + "failed, but ignored"); + } + + if (NS_WARN_IF(!pointToPutCaret.IsSet())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + // XXX `Selection` may be modified by mutation event listeners so + // that we should use EditorDOMPoint::AtEndOf(visibleTextNode) + // instead. (Perhaps, we don't and/or shouldn't need to do this + // if the text node is preformatted.) + AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( + pointToPutCaret); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "HTMLEditor::InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()" + " failed"); + return caretPointOrError.propagateErr(); + } + trackPointToPutCaret.FlushAndStopTracking(); + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); + // Remember that we did a ranged delete for the benefit of + // AfterEditInner(). + aHTMLEditor.TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange = true; + return CaretPoint(std::move(pointToPutCaret)); +} + +// static +nsIContent* HTMLEditor::AutoDeleteRangesHandler::GetAtomicContentToDelete( + nsIEditor::EDirection aDirectionAndAmount, + const WSRunScanner& aWSRunScannerAtCaret, + const WSScanResult& aScanFromCaretPointResult) { + MOZ_ASSERT(aScanFromCaretPointResult.GetContent()); + + if (!aScanFromCaretPointResult.ReachedSpecialContent()) { + return aScanFromCaretPointResult.GetContent(); + } + + if (!aScanFromCaretPointResult.GetContent()->IsText() || + HTMLEditUtils::IsRemovableNode(*aScanFromCaretPointResult.GetContent())) { + return aScanFromCaretPointResult.GetContent(); + } + + // aScanFromCaretPointResult is non-removable text node. + // Since we try removing atomic content, we look for removable node from + // scanned point that is non-removable text. + nsIContent* removableRoot = aScanFromCaretPointResult.GetContent(); + while (removableRoot && !HTMLEditUtils::IsRemovableNode(*removableRoot)) { + removableRoot = removableRoot->GetParent(); + } + + if (removableRoot) { + return removableRoot; + } + + // Not found better content. This content may not be removable. + return aScanFromCaretPointResult.GetContent(); +} + +nsresult +HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAtomicContent( + Element* aEditingHost, const nsIContent& aAtomicContent, + AutoRangeArray& aRangesToDelete) const { + EditorDOMRange rangeToDelete = + WSRunScanner::GetRangesForDeletingAtomicContent(aEditingHost, + aAtomicContent); + if (!rangeToDelete.IsPositioned()) { + NS_WARNING("WSRunScanner::GetRangeForDeleteAContentNode() failed"); + return NS_ERROR_FAILURE; + } + nsresult rv = aRangesToDelete.SetStartAndEnd(rangeToDelete.StartRef(), + rangeToDelete.EndRef()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoRangeArray::SetStartAndEnd() failed"); + return rv; +} + +Result<CaretPoint, nsresult> +HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAtomicContent( + HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent, + const EditorDOMPoint& aCaretPoint, const WSRunScanner& aWSRunScannerAtCaret, + const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!HTMLEditUtils::IsInvisibleBRElement(aAtomicContent)); + MOZ_ASSERT(&aAtomicContent != aWSRunScannerAtCaret.GetEditingHost()); + + EditorDOMPoint pointToPutCaret = aCaretPoint; + { + AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + Result<CaretPoint, nsresult> caretPointOrError = + WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt( + aHTMLEditor, aAtomicContent, aCaretPoint, aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "WhiteSpaceVisibilityKeeper::" + "DeleteContentNodeAndJoinTextNodesAroundIt() failed"); + return caretPointOrError; + } + trackPointToPutCaret.FlushAndStopTracking(); + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, + {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt}); + if (NS_WARN_IF(!pointToPutCaret.IsSet())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + } + + { + AutoTrackDOMPoint trackPointToPutCaret(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary( + pointToPutCaret); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "HTMLEditor::" + "InsertBRElementIfHardLineIsEmptyAndEndsWithBlockBoundary()" + " failed"); + return caretPointOrError; + } + trackPointToPutCaret.FlushAndStopTracking(); + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion}); + if (NS_WARN_IF(!pointToPutCaret.IsSet())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + } + return CaretPoint(std::move(pointToPutCaret)); +} + +bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + PrepareToDeleteAtOtherBlockBoundary( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, Element& aOtherBlockElement, + const EditorDOMPoint& aCaretPoint, + const WSRunScanner& aWSRunScannerAtCaret) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(aCaretPoint.IsSetAndValid()); + + mMode = Mode::JoinOtherBlock; + + // Make sure it's not a table element. If so, cancel the operation + // (translation: users cannot backspace or delete across table cells) + if (HTMLEditUtils::IsAnyTableElement(&aOtherBlockElement)) { + return false; + } + + // First find the adjacent node in the block + if (aDirectionAndAmount == nsIEditor::ePrevious) { + mLeafContentInOtherBlock = HTMLEditUtils::GetLastLeafContent( + aOtherBlockElement, {LeafNodeType::OnlyEditableLeafNode}, + &aOtherBlockElement); + mLeftContent = mLeafContentInOtherBlock; + mRightContent = aCaretPoint.GetContainerAs<nsIContent>(); + } else { + mLeafContentInOtherBlock = HTMLEditUtils::GetFirstLeafContent( + aOtherBlockElement, {LeafNodeType::OnlyEditableLeafNode}, + &aOtherBlockElement); + mLeftContent = aCaretPoint.GetContainerAs<nsIContent>(); + mRightContent = mLeafContentInOtherBlock; + } + + // Next to a block. See if we are between the block and a `<br>`. + // If so, we really want to delete the `<br>`. Else join content at + // selection to the block. + WSScanResult scanFromCaretResult = + aDirectionAndAmount == nsIEditor::eNext + ? aWSRunScannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom( + aCaretPoint) + : aWSRunScannerAtCaret.ScanNextVisibleNodeOrBlockBoundaryFrom( + aCaretPoint); + // If we found a `<br>` element, we need to delete it instead of joining the + // contents. + if (scanFromCaretResult.ReachedBRElement()) { + mBRElement = scanFromCaretResult.BRElementPtr(); + mMode = Mode::DeleteBRElement; + return true; + } + + return mLeftContent && mRightContent; +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + ComputeRangesToDeleteBRElement(AutoRangeArray& aRangesToDelete) const { + MOZ_ASSERT(mBRElement); + // XXX Why don't we scan invisible leading white-spaces which follows the + // `<br>` element? + nsresult rv = aRangesToDelete.SelectNode(*mBRElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::SelectNode() failed"); + return rv; +} + +Result<EditActionResult, nsresult> +HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner::DeleteBRElement( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(mBRElement); + + // If we're deleting selection (not replacing with new content), we should + // put caret to end of preceding text node if there is. Then, users can type + // text in it like the other browsers. + EditorDOMPoint pointToPutCaret = [&]() { + if (!MayEditActionDeleteAroundCollapsedSelection( + aHTMLEditor.GetEditAction())) { + return EditorDOMPoint(); + } + WSRunScanner scanner(&aEditingHost, EditorRawDOMPoint(mBRElement)); + WSScanResult maybePreviousText = + scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom( + EditorRawDOMPoint(mBRElement)); + if (maybePreviousText.IsContentEditable() && + maybePreviousText.InVisibleOrCollapsibleCharacters() && + !HTMLEditor::GetLinkElement(maybePreviousText.TextPtr())) { + return maybePreviousText.Point<EditorDOMPoint>(); + } + WSScanResult maybeNextText = scanner.ScanNextVisibleNodeOrBlockBoundaryFrom( + EditorRawDOMPoint::After(*mBRElement)); + if (maybeNextText.IsContentEditable() && + maybeNextText.InVisibleOrCollapsibleCharacters()) { + return maybeNextText.Point<EditorDOMPoint>(); + } + return EditorDOMPoint(); + }(); + + // If we found a `<br>` element, we should delete it instead of joining the + // contents. + nsresult rv = + aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(*mBRElement)); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return Err(rv); + } + + if (mLeftContent && mRightContent && + HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) != + HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent)) { + return EditActionResult::HandledResult(); + } + + // Put selection at edge of block and we are done. + if (NS_WARN_IF(!mLeafContentInOtherBlock)) { + // XXX This must be odd case. The other block can be empty. + return Err(NS_ERROR_FAILURE); + } + + if (pointToPutCaret.IsSet()) { + nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_SUCCEEDED(rv)) { + // If we prefer to use style in the previous line, we should forget + // previous styles since the caret position has all styles which we want + // to use with new content. + if (nsIEditor::DirectionIsBackspace(aDirectionAndAmount)) { + aHTMLEditor.TopLevelEditSubActionDataRef() + .mCachedPendingStyles->Clear(); + } + // And we don't want to keep extending a link at ex-end of the previous + // paragraph. + if (HTMLEditor::GetLinkElement(pointToPutCaret.GetContainer())) { + aHTMLEditor.mPendingStylesToApplyToNewContent + ->ClearLinkAndItsSpecifiedStyle(); + } + } else { + NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored"); + } + return EditActionResult::HandledResult(); + } + + EditorRawDOMPoint newCaretPosition = + HTMLEditUtils::GetGoodCaretPointFor<EditorRawDOMPoint>( + *mLeafContentInOtherBlock, aDirectionAndAmount); + if (MOZ_UNLIKELY(!newCaretPosition.IsSet())) { + NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed"); + return Err(NS_ERROR_FAILURE); + } + rv = aHTMLEditor.CollapseSelectionTo(newCaretPosition); + 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(); +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + ComputeRangesToDeleteAtOtherBlockBoundary( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete, + const Element& aEditingHost) const { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(aCaretPoint.IsSetAndValid()); + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mRightContent); + + if (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) != + HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent)) { + if (!mDeleteRangesHandlerConst.CanFallbackToDeleteRangesWithTransaction( + aRangesToDelete)) { + nsresult rv = aRangesToDelete.Collapse(aCaretPoint); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoRangeArray::Collapse() failed"); + return rv; + } + nsresult rv = mDeleteRangesHandlerConst + .FallbackToComputeRangesToDeleteRangesWithTransaction( + aHTMLEditor, aRangesToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::" + "FallbackToComputeRangesToDeleteRangesWithTransaction() failed"); + return rv; + } + + AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent, + *mRightContent); + Result<bool, nsresult> canJoinThem = + joiner.Prepare(aHTMLEditor, aEditingHost); + if (canJoinThem.isErr()) { + NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed"); + return canJoinThem.unwrapErr(); + } + if (canJoinThem.inspect() && joiner.CanJoinBlocks() && + !joiner.ShouldDeleteLeafContentInstead()) { + nsresult rv = + joiner.ComputeRangesToDelete(aHTMLEditor, aCaretPoint, aRangesToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoInclusiveAncestorBlockElementsJoiner::ComputeRangesToDelete() " + "failed"); + return rv; + } + + // If AutoInclusiveAncestorBlockElementsJoiner didn't handle it and it's not + // canceled, user may want to modify the start leaf node or the last leaf + // node of the block. + if (mLeafContentInOtherBlock == aCaretPoint.GetContainer()) { + return NS_OK; + } + + AutoHideSelectionChanges hideSelectionChanges(aHTMLEditor.SelectionRef()); + + // If it's ignored, it didn't modify the DOM tree. In this case, user must + // want to delete nearest leaf node in the other block element. + // TODO: We need to consider this before calling ComputeRangesToDelete() for + // computing the deleting range. + EditorRawDOMPoint newCaretPoint = + aDirectionAndAmount == nsIEditor::ePrevious + ? EditorRawDOMPoint::AtEndOf(*mLeafContentInOtherBlock) + : EditorRawDOMPoint(mLeafContentInOtherBlock, 0); + // If new caret position is same as current caret position, we can do + // nothing anymore. + if (aRangesToDelete.IsCollapsed() && + aRangesToDelete.FocusRef() == newCaretPoint.ToRawRangeBoundary()) { + return NS_OK; + } + // TODO: Stop modifying the `Selection` for computing the targer ranges. + nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPoint); + 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"); + if (NS_SUCCEEDED(rv)) { + aRangesToDelete.Initialize(aHTMLEditor.SelectionRef()); + AutoDeleteRangesHandler anotherHandler(mDeleteRangesHandlerConst); + rv = anotherHandler.ComputeRangesToDelete(aHTMLEditor, aDirectionAndAmount, + aRangesToDelete, aEditingHost); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "Recursive AutoDeleteRangesHandler::ComputeRangesToDelete() failed"); + } + // Restore selection. + nsresult rvCollapsingSelectionTo = + aHTMLEditor.CollapseSelectionTo(aCaretPoint); + if (MOZ_UNLIKELY(rvCollapsingSelectionTo == NS_ERROR_EDITOR_DESTROYED)) { + NS_WARNING( + "EditorBase::CollapseSelectionTo() caused destroying the editor"); + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvCollapsingSelectionTo), + "EditorBase::CollapseSelectionTo() failed to restore caret position"); + return NS_SUCCEEDED(rv) && NS_SUCCEEDED(rvCollapsingSelectionTo) + ? NS_OK + : NS_ERROR_FAILURE; +} + +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::HandleDeleteAtOtherBlockBoundary( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + const EditorDOMPoint& aCaretPoint, AutoRangeArray& aRangesToDelete, + const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(aCaretPoint.IsSetAndValid()); + MOZ_ASSERT(mDeleteRangesHandler); + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mRightContent); + + if (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) != + HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent)) { + // If we have not deleted `<br>` element and are not called recursively, + // we should call `DeleteRangesWithTransaction()` here. + if (!mDeleteRangesHandler->CanFallbackToDeleteRangesWithTransaction( + aRangesToDelete)) { + return EditActionResult::IgnoredResult(); + } + Result<CaretPoint, nsresult> caretPointOrError = + mDeleteRangesHandler->FallbackToDeleteRangesWithTransaction( + aHTMLEditor, aRangesToDelete); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "AutoDeleteRangesHandler::FallbackToDeleteRangesWithTransaction() " + "failed"); + return caretPointOrError.propagateErr(); + } + nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( + aHTMLEditor, {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"); + // Don't return "ignored" to avoid to fall it back to delete ranges + // recursively. + return EditActionResult::HandledResult(); + } + + // Else we are joining content to block + AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent, + *mRightContent); + Result<bool, nsresult> canJoinThem = + joiner.Prepare(aHTMLEditor, aEditingHost); + if (MOZ_UNLIKELY(canJoinThem.isErr())) { + NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed"); + return canJoinThem.propagateErr(); + } + + if (!canJoinThem.inspect()) { + nsresult rv = aHTMLEditor.CollapseSelectionTo(aCaretPoint); + 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::CanceledResult(); + } + + auto result = EditActionResult::IgnoredResult(); + EditorDOMPoint pointToPutCaret(aCaretPoint); + if (joiner.CanJoinBlocks()) { + { + AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), + &pointToPutCaret); + Result<EditActionResult, nsresult> joinResult = + joiner.Run(aHTMLEditor, aEditingHost); + if (MOZ_UNLIKELY(joinResult.isErr())) { + NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Run() failed"); + return joinResult; + } + result |= joinResult.unwrap(); +#ifdef DEBUG + if (joiner.ShouldDeleteLeafContentInstead()) { + NS_ASSERTION( + result.Ignored(), + "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` " + "returning ignored, but returned not ignored"); + } else { + NS_ASSERTION( + !result.Ignored(), + "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` " + "returning handled, but returned ignored"); + } +#endif // #ifdef DEBUG + // If we're deleting selection (not replacing with new content) and + // AutoInclusiveAncestorBlockElementsJoiner computed new caret position, + // we should use it. Otherwise, we should keep the our traditional + // behavior. + if (result.Handled() && joiner.PointRefToPutCaret().IsSet()) { + nsresult rv = + aHTMLEditor.CollapseSelectionTo(joiner.PointRefToPutCaret()); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored"); + return result; + } + // If we prefer to use style in the previous line, we should forget + // previous styles since the caret position has all styles which we want + // to use with new content. + if (nsIEditor::DirectionIsBackspace(aDirectionAndAmount)) { + aHTMLEditor.TopLevelEditSubActionDataRef() + .mCachedPendingStyles->Clear(); + } + // And we don't want to keep extending a link at ex-end of the previous + // paragraph. + if (HTMLEditor::GetLinkElement( + joiner.PointRefToPutCaret().GetContainer())) { + aHTMLEditor.mPendingStylesToApplyToNewContent + ->ClearLinkAndItsSpecifiedStyle(); + } + return result; + } + } + + // If AutoInclusiveAncestorBlockElementsJoiner didn't handle it and it's not + // canceled, user may want to modify the start leaf node or the last leaf + // node of the block. + if (result.Ignored() && + mLeafContentInOtherBlock != aCaretPoint.GetContainer()) { + // If it's ignored, it didn't modify the DOM tree. In this case, user + // must want to delete nearest leaf node in the other block element. + // TODO: We need to consider this before calling Run() for computing the + // deleting range. + EditorRawDOMPoint newCaretPoint = + aDirectionAndAmount == nsIEditor::ePrevious + ? EditorRawDOMPoint::AtEndOf(*mLeafContentInOtherBlock) + : EditorRawDOMPoint(mLeafContentInOtherBlock, 0); + // If new caret position is same as current caret position, we can do + // nothing anymore. + if (aRangesToDelete.IsCollapsed() && + aRangesToDelete.FocusRef() == newCaretPoint.ToRawRangeBoundary()) { + return EditActionResult::CanceledResult(); + } + nsresult rv = aHTMLEditor.CollapseSelectionTo(newCaretPoint); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return Err(rv); + } + AutoRangeArray rangesToDelete(aHTMLEditor.SelectionRef()); + AutoDeleteRangesHandler anotherHandler(mDeleteRangesHandler); + Result<EditActionResult, nsresult> fallbackResult = + anotherHandler.Run(aHTMLEditor, aDirectionAndAmount, aStripWrappers, + rangesToDelete, aEditingHost); + if (MOZ_UNLIKELY(fallbackResult.isErr())) { + NS_WARNING("Recursive AutoDeleteRangesHandler::Run() failed"); + return fallbackResult; + } + result |= fallbackResult.unwrap(); + return result; + } + } else { + result.MarkAsHandled(); + } + + // Otherwise, we must have deleted the selection as user expected. + nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret); + 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 result; +} + +bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + PrepareToDeleteAtCurrentBlockBoundary( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + + // At edge of our block. Look beside it and see if we can join to an + // adjacent block + mMode = Mode::JoinCurrentBlock; + + // Don't break the basic structure of the HTML document. + if (aCurrentBlockElement.IsAnyOfHTMLElements(nsGkAtoms::html, nsGkAtoms::head, + nsGkAtoms::body)) { + return false; + } + + // Make sure it's not a table element. If so, cancel the operation + // (translation: users cannot backspace or delete across table cells) + if (HTMLEditUtils::IsAnyTableElement(&aCurrentBlockElement)) { + return false; + } + + Element* editingHost = aHTMLEditor.ComputeEditingHost(); + if (NS_WARN_IF(!editingHost)) { + return false; + } + + auto ScanJoinTarget = [&]() -> nsIContent* { + nsIContent* targetContent = + aDirectionAndAmount == nsIEditor::ePrevious + ? HTMLEditUtils::GetPreviousContent( + aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode}, + editingHost) + : HTMLEditUtils::GetNextContent( + aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode}, + editingHost); + // If found content is an invisible text node, let's scan visible things. + auto IsIgnorableDataNode = [](nsIContent* aContent) { + return aContent && HTMLEditUtils::IsRemovableNode(*aContent) && + ((aContent->IsText() && + aContent->AsText()->TextIsOnlyWhitespace() && + !HTMLEditUtils::IsVisibleTextNode(*aContent->AsText())) || + (aContent->IsCharacterData() && !aContent->IsText())); + }; + if (!IsIgnorableDataNode(targetContent)) { + return targetContent; + } + MOZ_ASSERT(mSkippedInvisibleContents.IsEmpty()); + for (nsIContent* adjacentContent = + aDirectionAndAmount == nsIEditor::ePrevious + ? HTMLEditUtils::GetPreviousContent( + *targetContent, {WalkTreeOption::StopAtBlockBoundary}, + editingHost) + : HTMLEditUtils::GetNextContent( + *targetContent, {WalkTreeOption::StopAtBlockBoundary}, + editingHost); + adjacentContent; + adjacentContent = + aDirectionAndAmount == nsIEditor::ePrevious + ? HTMLEditUtils::GetPreviousContent( + *adjacentContent, {WalkTreeOption::StopAtBlockBoundary}, + editingHost) + : HTMLEditUtils::GetNextContent( + *adjacentContent, {WalkTreeOption::StopAtBlockBoundary}, + editingHost)) { + // If non-editable element is found, we should not skip it to avoid + // joining too far nodes. + if (!HTMLEditUtils::IsSimplyEditableNode(*adjacentContent)) { + break; + } + // If block element is found, we should join last leaf content in it. + if (HTMLEditUtils::IsBlockElement(*adjacentContent)) { + nsIContent* leafContent = + aDirectionAndAmount == nsIEditor::ePrevious + ? HTMLEditUtils::GetLastLeafContent( + *adjacentContent, {LeafNodeType::OnlyEditableLeafNode}) + : HTMLEditUtils::GetFirstLeafContent( + *adjacentContent, {LeafNodeType::OnlyEditableLeafNode}); + mSkippedInvisibleContents.AppendElement(*targetContent); + return leafContent ? leafContent : adjacentContent; + } + // Only when the found node is an invisible text node or a non-text data + // node, we should keep scanning. + if (IsIgnorableDataNode(adjacentContent)) { + mSkippedInvisibleContents.AppendElement(*targetContent); + targetContent = adjacentContent; + continue; + } + // Otherwise, we find a visible things. We should join with last found + // invisible text node. + break; + } + return targetContent; + }; + + if (aDirectionAndAmount == nsIEditor::ePrevious) { + mLeftContent = ScanJoinTarget(); + mRightContent = aCaretPoint.GetContainerAs<nsIContent>(); + } else { + mRightContent = ScanJoinTarget(); + mLeftContent = aCaretPoint.GetContainerAs<nsIContent>(); + } + + // Nothing to join + if (!mLeftContent || !mRightContent) { + return false; + } + + // Don't cross table boundaries. + return HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mLeftContent) == + HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*mRightContent); +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + ComputeRangesToDeleteAtCurrentBlockBoundary( + const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint, + AutoRangeArray& aRangesToDelete, const Element& aEditingHost) const { + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mRightContent); + + AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent, + *mRightContent); + Result<bool, nsresult> canJoinThem = + joiner.Prepare(aHTMLEditor, aEditingHost); + if (canJoinThem.isErr()) { + NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed"); + return canJoinThem.unwrapErr(); + } + if (canJoinThem.inspect()) { + nsresult rv = + joiner.ComputeRangesToDelete(aHTMLEditor, aCaretPoint, aRangesToDelete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoInclusiveAncestorBlockElementsJoiner::" + "ComputeRangesToDelete() failed"); + return rv; + } + + // In this case, nothing will be deleted so that the affected range should + // be collapsed. + nsresult rv = aRangesToDelete.Collapse(aCaretPoint); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed"); + return rv; +} + +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::HandleDeleteAtCurrentBlockBoundary( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aCaretPoint, const Element& aEditingHost) { + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mRightContent); + + AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent, + *mRightContent); + Result<bool, nsresult> canJoinThem = + joiner.Prepare(aHTMLEditor, aEditingHost); + if (MOZ_UNLIKELY(canJoinThem.isErr())) { + NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed"); + return Err(canJoinThem.unwrapErr()); + } + + if (!canJoinThem.inspect()) { + nsresult rv = aHTMLEditor.CollapseSelectionTo(aCaretPoint); + 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::CanceledResult(); + } + + EditActionResult result = EditActionResult::IgnoredResult(); + EditorDOMPoint pointToPutCaret(aCaretPoint); + if (joiner.CanJoinBlocks()) { + AutoTrackDOMPoint tracker(aHTMLEditor.RangeUpdaterRef(), &pointToPutCaret); + Result<EditActionResult, nsresult> joinResult = + joiner.Run(aHTMLEditor, aEditingHost); + if (MOZ_UNLIKELY(joinResult.isErr())) { + NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Run() failed"); + return joinResult; + } + result |= joinResult.unwrap(); +#ifdef DEBUG + if (joiner.ShouldDeleteLeafContentInstead()) { + NS_ASSERTION(result.Ignored(), + "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` " + "returning ignored, but returned not ignored"); + } else { + NS_ASSERTION(!result.Ignored(), + "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` " + "returning handled, but returned ignored"); + } +#endif // #ifdef DEBUG + + // Cleaning up invisible nodes which are skipped at scanning mLeftContent or + // mRightContent. + for (const OwningNonNull<nsIContent>& content : mSkippedInvisibleContents) { + nsresult rv = + aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(content)); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return Err(rv); + } + } + mSkippedInvisibleContents.Clear(); + + // If we're deleting selection (not replacing with new content) and + // AutoInclusiveAncestorBlockElementsJoiner computed new caret position, we + // should use it. Otherwise, we should keep the our traditional behavior. + if (result.Handled() && joiner.PointRefToPutCaret().IsSet()) { + nsresult rv = + aHTMLEditor.CollapseSelectionTo(joiner.PointRefToPutCaret()); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed, but ignored"); + return result; + } + // If we prefer to use style in the previous line, we should forget + // previous styles since the caret position has all styles which we want + // to use with new content. + if (nsIEditor::DirectionIsBackspace(aDirectionAndAmount)) { + aHTMLEditor.TopLevelEditSubActionDataRef() + .mCachedPendingStyles->Clear(); + } + // And we don't want to keep extending a link at ex-end of the previous + // paragraph. + if (HTMLEditor::GetLinkElement( + joiner.PointRefToPutCaret().GetContainer())) { + aHTMLEditor.mPendingStylesToApplyToNewContent + ->ClearLinkAndItsSpecifiedStyle(); + } + return result; + } + } + // This should claim that trying to join the block means that + // this handles the action because the caller shouldn't do anything + // anymore in this case. + result.MarkAsHandled(); + + nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret); + 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 result; +} + +nsresult +HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteNonCollapsedRanges( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost) const { + MOZ_ASSERT(!aRangesToDelete.IsCollapsed()); + + if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->StartRef().IsSet()) || + NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->EndRef().IsSet())) { + return NS_ERROR_FAILURE; + } + + if (aRangesToDelete.Ranges().Length() == 1) { + nsFrameSelection* frameSelection = + aHTMLEditor.SelectionRef().GetFrameSelection(); + if (NS_WARN_IF(!frameSelection)) { + return NS_ERROR_FAILURE; + } + Result<EditorRawDOMRange, nsresult> result = ExtendOrShrinkRangeToDelete( + aHTMLEditor, frameSelection, + EditorRawDOMRange(aRangesToDelete.FirstRangeRef())); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete() failed"); + return NS_ERROR_FAILURE; + } + EditorRawDOMRange newRange(result.unwrap()); + if (MOZ_UNLIKELY(NS_FAILED(aRangesToDelete.FirstRangeRef()->SetStartAndEnd( + newRange.StartRef().ToRawRangeBoundary(), + newRange.EndRef().ToRawRangeBoundary())))) { + NS_WARNING("nsRange::SetStartAndEnd() failed"); + return NS_ERROR_FAILURE; + } + if (MOZ_UNLIKELY( + NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()))) { + return NS_ERROR_FAILURE; + } + if (NS_WARN_IF(aRangesToDelete.FirstRangeRef()->Collapsed())) { + return NS_OK; // Hmm, there is nothing to delete...? + } + } + + if (!aHTMLEditor.IsInPlaintextMode()) { + EditorDOMRange firstRange(aRangesToDelete.FirstRangeRef()); + EditorDOMRange extendedRange = + WSRunScanner::GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries( + aHTMLEditor.ComputeEditingHost(), + EditorDOMRange(aRangesToDelete.FirstRangeRef())); + if (firstRange != extendedRange) { + nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd( + extendedRange.StartRef().ToRawRangeBoundary(), + extendedRange.EndRef().ToRawRangeBoundary()); + if (NS_FAILED(rv)) { + NS_WARNING("nsRange::SetStartAndEnd() failed"); + return NS_ERROR_FAILURE; + } + } + } + + if (aRangesToDelete.FirstRangeRef()->GetStartContainer() == + aRangesToDelete.FirstRangeRef()->GetEndContainer()) { + if (!aRangesToDelete.FirstRangeRef()->Collapsed()) { + nsresult rv = ComputeRangesToDeleteRangesWithTransaction( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction(" + ") failed"); + return rv; + } + // `DeleteUnnecessaryNodesAndCollapseSelection()` may delete parent + // elements, but it does not affect computing target ranges. Therefore, + // we don't need to touch aRangesToDelete in this case. + return NS_OK; + } + + Element* startCiteNode = aHTMLEditor.GetMostDistantAncestorMailCiteElement( + *aRangesToDelete.FirstRangeRef()->GetStartContainer()); + Element* endCiteNode = aHTMLEditor.GetMostDistantAncestorMailCiteElement( + *aRangesToDelete.FirstRangeRef()->GetEndContainer()); + + if (startCiteNode && !endCiteNode) { + aDirectionAndAmount = nsIEditor::eNext; + } else if (!startCiteNode && endCiteNode) { + aDirectionAndAmount = nsIEditor::ePrevious; + } + + AutoBlockElementsJoiner joiner(*this); + if (!joiner.PrepareToDeleteNonCollapsedRanges(aHTMLEditor, aRangesToDelete)) { + return NS_ERROR_FAILURE; + } + nsresult rv = joiner.ComputeRangesToDelete( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete, aSelectionWasCollapsed, + aEditingHost); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::ComputeRangesToDelete() failed"); + return rv; +} + +Result<EditActionResult, nsresult> +HTMLEditor::AutoDeleteRangesHandler::HandleDeleteNonCollapsedRanges( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, AutoRangeArray& aRangesToDelete, + SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.IsCollapsed()); + + if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->StartRef().IsSet()) || + NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->EndRef().IsSet())) { + return Err(NS_ERROR_FAILURE); + } + + MOZ_ASSERT_IF(aRangesToDelete.Ranges().Length() == 1, + aRangesToDelete.IsFirstRangeEditable(aEditingHost)); + + // Else we have a non-collapsed selection. First adjust the selection. + // XXX Why do we extend selection only when there is only one range? + if (aRangesToDelete.Ranges().Length() == 1) { + nsFrameSelection* frameSelection = + aHTMLEditor.SelectionRef().GetFrameSelection(); + if (NS_WARN_IF(!frameSelection)) { + return Err(NS_ERROR_FAILURE); + } + Result<EditorRawDOMRange, nsresult> result = ExtendOrShrinkRangeToDelete( + aHTMLEditor, frameSelection, + EditorRawDOMRange(aRangesToDelete.FirstRangeRef())); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete() failed"); + return Err(NS_ERROR_FAILURE); + } + EditorRawDOMRange newRange(result.unwrap()); + if (NS_FAILED(aRangesToDelete.FirstRangeRef()->SetStartAndEnd( + newRange.StartRef().ToRawRangeBoundary(), + newRange.EndRef().ToRawRangeBoundary()))) { + NS_WARNING("nsRange::SetStartAndEnd() failed"); + return Err(NS_ERROR_FAILURE); + } + if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned())) { + return Err(NS_ERROR_FAILURE); + } + if (NS_WARN_IF(aRangesToDelete.FirstRangeRef()->Collapsed())) { + // Hmm, there is nothing to delete...? + // In this case, the callers want collapsed selection. Therefore, we need + // to change the `Selection` here. + nsresult rv = aHTMLEditor.CollapseSelectionTo( + aRangesToDelete.GetFirstRangeStartPoint<EditorRawDOMPoint>()); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); + } + MOZ_ASSERT(aRangesToDelete.IsFirstRangeEditable(aEditingHost)); + } + + // Remember that we did a ranged delete for the benefit of AfterEditInner(). + aHTMLEditor.TopLevelEditSubActionDataRef().mDidDeleteNonCollapsedRange = true; + + // Figure out if the endpoints are in nodes that can be merged. Adjust + // surrounding white-space in preparation to delete selection. + if (!aHTMLEditor.IsInPlaintextMode()) { + { + AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(), + &aRangesToDelete.FirstRangeRef()); + Result<CaretPoint, nsresult> caretPointOrError = + WhiteSpaceVisibilityKeeper::PrepareToDeleteRange( + aHTMLEditor, EditorDOMRange(aRangesToDelete.FirstRangeRef()), + aEditingHost); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("WhiteSpaceVisibilityKeeper::PrepareToDeleteRange() failed"); + return caretPointOrError.propagateErr(); + } + // Ignore caret point suggestion because there was + // AutoTransactionsConserveSelection. + caretPointOrError.unwrap().IgnoreCaretPointSuggestion(); + } + if (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()) || + (aHTMLEditor.MayHaveMutationEventListeners() && + NS_WARN_IF(!aRangesToDelete.IsFirstRangeEditable(aEditingHost)))) { + NS_WARNING( + "WhiteSpaceVisibilityKeeper::PrepareToDeleteRange() made the first " + "range invalid"); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + } + + // XXX This is odd. We do we simply use `DeleteRangesWithTransaction()` + // only when **first** range is in same container? + if (aRangesToDelete.FirstRangeRef()->GetStartContainer() == + aRangesToDelete.FirstRangeRef()->GetEndContainer()) { + // Because of previous DOM tree changes, the range may be collapsed. + // If we've already removed all contents in the range, we shouldn't + // delete anything around the caret. + if (!aRangesToDelete.FirstRangeRef()->Collapsed()) { + { + AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(), + &aRangesToDelete.FirstRangeRef()); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteRangesWithTransaction( + aDirectionAndAmount, aStripWrappers, aRangesToDelete); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("EditorBase::DeleteRangesWithTransaction() failed"); + return caretPointOrError.propagateErr(); + } + nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( + aHTMLEditor, {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 (NS_WARN_IF(!aRangesToDelete.FirstRangeRef()->IsPositioned()) || + (aHTMLEditor.MayHaveMutationEventListeners( + NS_EVENT_BITS_MUTATION_NODEREMOVED | + NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT | + NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED) && + NS_WARN_IF(!aRangesToDelete.IsFirstRangeEditable(aEditingHost)))) { + NS_WARNING( + "EditorBase::DeleteRangesWithTransaction() made the first range " + "invalid"); + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + } + // However, even if the range is removed, we may need to clean up the + // containers which become empty. + nsresult rv = DeleteUnnecessaryNodesAndCollapseSelection( + aHTMLEditor, aDirectionAndAmount, + EditorDOMPoint(aRangesToDelete.FirstRangeRef()->StartRef()), + EditorDOMPoint(aRangesToDelete.FirstRangeRef()->EndRef())); + if (NS_FAILED(rv)) { + NS_WARNING( + "AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection(" + ") failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); + } + + if (NS_WARN_IF( + !aRangesToDelete.FirstRangeRef()->GetStartContainer()->IsContent()) || + NS_WARN_IF( + !aRangesToDelete.FirstRangeRef()->GetEndContainer()->IsContent())) { + return Err(NS_ERROR_FAILURE); + } + + // Figure out mailcite ancestors + RefPtr<Element> startCiteNode = + aHTMLEditor.GetMostDistantAncestorMailCiteElement( + *aRangesToDelete.FirstRangeRef()->GetStartContainer()); + RefPtr<Element> endCiteNode = + aHTMLEditor.GetMostDistantAncestorMailCiteElement( + *aRangesToDelete.FirstRangeRef()->GetEndContainer()); + + // If we only have a mailcite at one of the two endpoints, set the + // directionality of the deletion so that the selection will end up + // outside the mailcite. + if (startCiteNode && !endCiteNode) { + aDirectionAndAmount = nsIEditor::eNext; + } else if (!startCiteNode && endCiteNode) { + aDirectionAndAmount = nsIEditor::ePrevious; + } + + AutoBlockElementsJoiner joiner(*this); + if (!joiner.PrepareToDeleteNonCollapsedRanges(aHTMLEditor, aRangesToDelete)) { + return Err(NS_ERROR_FAILURE); + } + Result<EditActionResult, nsresult> result = + joiner.Run(aHTMLEditor, aDirectionAndAmount, aStripWrappers, + aRangesToDelete, aSelectionWasCollapsed, aEditingHost); + NS_WARNING_ASSERTION(result.isOk(), "AutoBlockElementsJoiner::Run() failed"); + return result; +} + +bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + PrepareToDeleteNonCollapsedRanges(const HTMLEditor& aHTMLEditor, + const AutoRangeArray& aRangesToDelete) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.IsCollapsed()); + + mLeftContent = HTMLEditUtils::GetInclusiveAncestorElement( + *aRangesToDelete.FirstRangeRef()->GetStartContainer()->AsContent(), + HTMLEditUtils::ClosestEditableBlockElement); + mRightContent = HTMLEditUtils::GetInclusiveAncestorElement( + *aRangesToDelete.FirstRangeRef()->GetEndContainer()->AsContent(), + HTMLEditUtils::ClosestEditableBlockElement); + // Note that mLeftContent and/or mRightContent can be nullptr if editing host + // is an inline element. If both editable ancestor block is exactly same + // one or one reaches an inline editing host, we can just delete the content + // in ranges. + if (mLeftContent == mRightContent || !mLeftContent || !mRightContent) { + MOZ_ASSERT_IF(!mLeftContent || !mRightContent, + aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->AsContent() + ->GetEditingHost() == aRangesToDelete.FirstRangeRef() + ->GetEndContainer() + ->AsContent() + ->GetEditingHost()); + mMode = Mode::DeleteContentInRanges; + return true; + } + + // If left block and right block are adjuscent siblings and they are same + // type of elements, we can merge them after deleting the selected contents. + // MOOSE: this could conceivably screw up a table.. fix me. + if (mLeftContent->GetParentNode() == mRightContent->GetParentNode() && + HTMLEditUtils::CanContentsBeJoined(*mLeftContent, *mRightContent) && + // XXX What's special about these three types of block? + (mLeftContent->IsHTMLElement(nsGkAtoms::p) || + HTMLEditUtils::IsListItem(mLeftContent) || + HTMLEditUtils::IsHeader(*mLeftContent))) { + mMode = Mode::JoinBlocksInSameParent; + return true; + } + + mMode = Mode::DeleteNonCollapsedRanges; + return true; +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + ComputeRangesToDeleteContentInRanges( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete) const { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.IsCollapsed()); + MOZ_ASSERT(mMode == Mode::DeleteContentInRanges); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->AsContent() + ->GetEditingHost()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->AsContent() + ->GetEditingHost() == aRangesToDelete.FirstRangeRef() + ->GetEndContainer() + ->AsContent() + ->GetEditingHost()); + MOZ_ASSERT(!mLeftContent == !mRightContent); + MOZ_ASSERT_IF(mLeftContent, mLeftContent->IsElement()); + MOZ_ASSERT_IF(mLeftContent, aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->IsInclusiveDescendantOf(mLeftContent)); + MOZ_ASSERT_IF(mRightContent, mRightContent->IsElement()); + MOZ_ASSERT_IF(mRightContent, aRangesToDelete.FirstRangeRef() + ->GetEndContainer() + ->IsInclusiveDescendantOf(mRightContent)); + MOZ_ASSERT_IF(!mLeftContent, + HTMLEditUtils::IsInlineElement(*aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->AsContent() + ->GetEditingHost())); + + nsresult rv = + mDeleteRangesHandlerConst.ComputeRangesToDeleteRangesWithTransaction( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::" + "ComputeRangesToDeleteRangesWithTransaction() failed"); + return rv; +} + +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::DeleteContentInRanges( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + AutoRangeArray& aRangesToDelete) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.IsCollapsed()); + MOZ_ASSERT(mMode == Mode::DeleteContentInRanges); + MOZ_ASSERT(mDeleteRangesHandler); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->AsContent() + ->GetEditingHost()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->AsContent() + ->GetEditingHost() == aRangesToDelete.FirstRangeRef() + ->GetEndContainer() + ->AsContent() + ->GetEditingHost()); + MOZ_ASSERT_IF(mLeftContent, mLeftContent->IsElement()); + MOZ_ASSERT_IF(mLeftContent, aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->IsInclusiveDescendantOf(mLeftContent)); + MOZ_ASSERT_IF(mRightContent, mRightContent->IsElement()); + MOZ_ASSERT_IF(mRightContent, aRangesToDelete.FirstRangeRef() + ->GetEndContainer() + ->IsInclusiveDescendantOf(mRightContent)); + MOZ_ASSERT_IF(!mLeftContent, + HTMLEditUtils::IsInlineElement(*aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->AsContent() + ->GetEditingHost())); + + // XXX This is also odd. We do we simply use + // `DeleteRangesWithTransaction()` only when **first** range is in + // same block? + { + AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(), + &aRangesToDelete.FirstRangeRef()); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteRangesWithTransaction( + aDirectionAndAmount, aStripWrappers, aRangesToDelete); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + if (NS_WARN_IF(caretPointOrError.inspectErr() == + NS_ERROR_EDITOR_DESTROYED)) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING( + "EditorBase::DeleteRangesWithTransaction() failed, but ignored"); + } else { + nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( + aHTMLEditor, {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"); + } + } + nsresult rv = + mDeleteRangesHandler->DeleteUnnecessaryNodesAndCollapseSelection( + aHTMLEditor, aDirectionAndAmount, + EditorDOMPoint(aRangesToDelete.FirstRangeRef()->StartRef()), + EditorDOMPoint(aRangesToDelete.FirstRangeRef()->EndRef())); + if (NS_FAILED(rv)) { + NS_WARNING( + "AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection() " + "failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + ComputeRangesToJoinBlockElementsInSameParent( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete) const { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.IsCollapsed()); + MOZ_ASSERT(mMode == Mode::JoinBlocksInSameParent); + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mLeftContent->IsElement()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->IsInclusiveDescendantOf(mLeftContent)); + MOZ_ASSERT(mRightContent); + MOZ_ASSERT(mRightContent->IsElement()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetEndContainer() + ->IsInclusiveDescendantOf(mRightContent)); + MOZ_ASSERT(mLeftContent->GetParentNode() == mRightContent->GetParentNode()); + + nsresult rv = + mDeleteRangesHandlerConst.ComputeRangesToDeleteRangesWithTransaction( + aHTMLEditor, aDirectionAndAmount, aRangesToDelete); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::" + "ComputeRangesToDeleteRangesWithTransaction() failed"); + return rv; +} + +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::JoinBlockElementsInSameParent( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + AutoRangeArray& aRangesToDelete, + SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.IsCollapsed()); + MOZ_ASSERT(mMode == Mode::JoinBlocksInSameParent); + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mLeftContent->IsElement()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->IsInclusiveDescendantOf(mLeftContent)); + MOZ_ASSERT(mRightContent); + MOZ_ASSERT(mRightContent->IsElement()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetEndContainer() + ->IsInclusiveDescendantOf(mRightContent)); + MOZ_ASSERT(mLeftContent->GetParentNode() == mRightContent->GetParentNode()); + + const bool backspaceInRightBlock = + aSelectionWasCollapsed == SelectionWasCollapsed::Yes && + nsIEditor::DirectionIsBackspace(aDirectionAndAmount); + + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteRangesWithTransaction(aDirectionAndAmount, + aStripWrappers, aRangesToDelete); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("EditorBase::DeleteRangesWithTransaction() failed"); + return caretPointOrError.propagateErr(); + } + + nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( + aHTMLEditor, {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 (NS_WARN_IF(!mLeftContent->GetParentNode()) || + NS_WARN_IF(!mRightContent->GetParentNode()) || + NS_WARN_IF(mLeftContent->GetParentNode() != + mRightContent->GetParentNode())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + auto startOfRightContent = + HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>( + *mRightContent); + AutoTrackDOMPoint trackStartOfRightContent(aHTMLEditor.RangeUpdaterRef(), + &startOfRightContent); + Result<EditorDOMPoint, nsresult> atFirstChildOfTheLastRightNodeOrError = + JoinNodesDeepWithTransaction(aHTMLEditor, MOZ_KnownLive(*mLeftContent), + MOZ_KnownLive(*mRightContent)); + if (MOZ_UNLIKELY(atFirstChildOfTheLastRightNodeOrError.isErr())) { + NS_WARNING("HTMLEditor::JoinNodesDeepWithTransaction() failed"); + return atFirstChildOfTheLastRightNodeOrError.propagateErr(); + } + MOZ_ASSERT(atFirstChildOfTheLastRightNodeOrError.inspect().IsSet()); + trackStartOfRightContent.FlushAndStopTracking(); + if (NS_WARN_IF(!startOfRightContent.IsSet()) || + NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + // If we're deleting selection (not replacing with new content) and the joined + // point follows a text node, we should put caret to end of the preceding text + // node because the other browsers insert following inputs into there. + if (MayEditActionDeleteAroundCollapsedSelection( + aHTMLEditor.GetEditAction())) { + WSRunScanner scanner(&aEditingHost, startOfRightContent); + WSScanResult maybePreviousText = + scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent); + if (maybePreviousText.IsContentEditable() && + maybePreviousText.InVisibleOrCollapsibleCharacters()) { + nsresult rv = aHTMLEditor.CollapseSelectionTo( + maybePreviousText.Point<EditorRawDOMPoint>()); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return Err(rv); + } + // If we prefer to use style in the previous line, we should forget + // previous styles since the caret position has all styles which we want + // to use with new content. + if (backspaceInRightBlock) { + aHTMLEditor.TopLevelEditSubActionDataRef() + .mCachedPendingStyles->Clear(); + } + // And we don't want to keep extending a link at ex-end of the previous + // paragraph. + if (HTMLEditor::GetLinkElement(maybePreviousText.TextPtr())) { + aHTMLEditor.mPendingStylesToApplyToNewContent + ->ClearLinkAndItsSpecifiedStyle(); + } + return EditActionResult::HandledResult(); + } + } + + // Otherwise, we should put caret at start of the right content. + rv = aHTMLEditor.CollapseSelectionTo( + atFirstChildOfTheLastRightNodeOrError.inspect()); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); +} + +Result<bool, nsresult> +HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + ComputeRangesToDeleteNodesEntirelyInRangeButKeepTableStructure( + const HTMLEditor& aHTMLEditor, nsRange& aRange, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) + const { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + + AutoTArray<OwningNonNull<nsIContent>, 10> arrayOfTopChildren; + DOMSubtreeIterator iter; + nsresult rv = iter.Init(aRange); + if (NS_FAILED(rv)) { + NS_WARNING("DOMSubtreeIterator::Init() failed"); + return Err(rv); + } + iter.AppendAllNodesToArray(arrayOfTopChildren); + return NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure( + aHTMLEditor, arrayOfTopChildren, aSelectionWasCollapsed); +} + +Result<bool, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::DeleteNodesEntirelyInRangeButKeepTableStructure( + HTMLEditor& aHTMLEditor, nsRange& aRange, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + + // Build a list of direct child nodes in the range + AutoTArray<OwningNonNull<nsIContent>, 10> arrayOfTopChildren; + DOMSubtreeIterator iter; + nsresult rv = iter.Init(aRange); + if (NS_FAILED(rv)) { + NS_WARNING("DOMSubtreeIterator::Init() failed"); + return Err(rv); + } + iter.AppendAllNodesToArray(arrayOfTopChildren); + + // Now that we have the list, delete non-table elements + bool needsToJoinLater = + NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure( + aHTMLEditor, arrayOfTopChildren, aSelectionWasCollapsed); + for (auto& content : arrayOfTopChildren) { + // XXX After here, the child contents in the array may have been moved + // to somewhere or removed. We should handle it. + // + // MOZ_KnownLive because 'arrayOfTopChildren' is guaranteed to + // keep it alive. + // + // Even with https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 fixed + // this might need to stay, because 'arrayOfTopChildren' is not const, + // so it's not obvious how to prove via static analysis that it won't + // change and release us. + nsresult rv = + DeleteContentButKeepTableStructure(aHTMLEditor, MOZ_KnownLive(content)); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoBlockElementsJoiner::DeleteContentButKeepTableStructure() failed, " + "but ignored"); + } + return needsToJoinLater; +} + +bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure( + const HTMLEditor& aHTMLEditor, + const nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) + const { + // If original selection was collapsed, we need always to join the nodes. + // XXX Why? + if (aSelectionWasCollapsed == + AutoDeleteRangesHandler::SelectionWasCollapsed::No) { + return true; + } + // If something visible is deleted, no need to join. Visible means + // all nodes except non-visible textnodes and breaks. + if (aArrayOfContents.IsEmpty()) { + return true; + } + for (const OwningNonNull<nsIContent>& content : aArrayOfContents) { + if (content->IsText()) { + if (HTMLEditUtils::IsInVisibleTextFrames(aHTMLEditor.GetPresContext(), + *content->AsText())) { + return false; + } + continue; + } + // XXX If it's an element node, we should check whether it has visible + // frames or not. + if (!content->IsElement() || + HTMLEditUtils::IsEmptyNode( + *content->AsElement(), + {EmptyCheckOption::TreatSingleBRElementAsVisible})) { + continue; + } + if (!HTMLEditUtils::IsInvisibleBRElement(*content)) { + return false; + } + } + return true; +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange) { + EditorDOMPoint rangeStart(aRange.StartRef()); + EditorDOMPoint rangeEnd(aRange.EndRef()); + if (rangeStart.IsInTextNode() && !rangeStart.IsEndOfContainer()) { + // Delete to last character + OwningNonNull<Text> textNode = *rangeStart.ContainerAs<Text>(); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction( + textNode, rangeStart.Offset(), + rangeStart.GetContainer()->Length() - rangeStart.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( + aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt, + SuggestCaret::AndIgnoreTrivialError}); + 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"); + } + if (rangeEnd.IsInTextNode() && !rangeEnd.IsStartOfContainer()) { + // Delete to first character + OwningNonNull<Text> textNode = *rangeEnd.ContainerAs<Text>(); + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction(textNode, 0, rangeEnd.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( + aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt, + SuggestCaret::AndIgnoreTrivialError}); + 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"); + } + return NS_OK; +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + ComputeRangesToDeleteNonCollapsedRanges( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost) const { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.IsCollapsed()); + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mLeftContent->IsElement()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->IsInclusiveDescendantOf(mLeftContent)); + MOZ_ASSERT(mRightContent); + MOZ_ASSERT(mRightContent->IsElement()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetEndContainer() + ->IsInclusiveDescendantOf(mRightContent)); + + for (OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) { + Result<bool, nsresult> result = + ComputeRangesToDeleteNodesEntirelyInRangeButKeepTableStructure( + aHTMLEditor, range, aSelectionWasCollapsed); + if (result.isErr()) { + NS_WARNING( + "AutoBlockElementsJoiner::" + "ComputeRangesToDeleteNodesEntirelyInRangeButKeepTableStructure() " + "failed"); + return result.unwrapErr(); + } + if (!result.unwrap()) { + return NS_OK; + } + } + + AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent, + *mRightContent); + Result<bool, nsresult> canJoinThem = + joiner.Prepare(aHTMLEditor, aEditingHost); + if (canJoinThem.isErr()) { + NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed"); + return canJoinThem.unwrapErr(); + } + + if (!canJoinThem.unwrap()) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + if (!joiner.CanJoinBlocks()) { + return NS_OK; + } + + nsresult rv = joiner.ComputeRangesToDelete(aHTMLEditor, EditorDOMPoint(), + aRangesToDelete); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoInclusiveAncestorBlockElementsJoiner::ComputeRangesToDelete() " + "failed"); + return rv; +} + +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::HandleDeleteNonCollapsedRanges( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers, + AutoRangeArray& aRangesToDelete, + AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, + const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.IsCollapsed()); + MOZ_ASSERT(mDeleteRangesHandler); + MOZ_ASSERT(mLeftContent); + MOZ_ASSERT(mLeftContent->IsElement()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetStartContainer() + ->IsInclusiveDescendantOf(mLeftContent)); + MOZ_ASSERT(mRightContent); + MOZ_ASSERT(mRightContent->IsElement()); + MOZ_ASSERT(aRangesToDelete.FirstRangeRef() + ->GetEndContainer() + ->IsInclusiveDescendantOf(mRightContent)); + + const bool backspaceInRightBlock = + aSelectionWasCollapsed == SelectionWasCollapsed::Yes && + nsIEditor::DirectionIsBackspace(aDirectionAndAmount); + + // Otherwise, delete every nodes in all ranges, then, clean up something. + EditActionResult result = EditActionResult::IgnoredResult(); + EditorDOMPoint pointToPutCaret; + while (true) { + AutoTrackDOMRange firstRangeTracker(aHTMLEditor.RangeUpdaterRef(), + &aRangesToDelete.FirstRangeRef()); + + bool joinInclusiveAncestorBlockElements = true; + for (auto& range : aRangesToDelete.Ranges()) { + Result<bool, nsresult> deleteResult = + DeleteNodesEntirelyInRangeButKeepTableStructure( + aHTMLEditor, MOZ_KnownLive(range), aSelectionWasCollapsed); + if (MOZ_UNLIKELY(deleteResult.isErr())) { + NS_WARNING( + "AutoBlockElementsJoiner::" + "DeleteNodesEntirelyInRangeButKeepTableStructure() failed"); + return deleteResult.propagateErr(); + } + // XXX Completely odd. Why don't we join blocks around each range? + joinInclusiveAncestorBlockElements &= deleteResult.unwrap(); + } + + // Check endpoints for possible text deletion. We can assume that if + // text node is found, we can delete to end or to begining as + // appropriate, since the case where both sel endpoints in same text + // node was already handled (we wouldn't be here) + nsresult rv = DeleteTextAtStartAndEndOfRange( + aHTMLEditor, MOZ_KnownLive(aRangesToDelete.FirstRangeRef())); + if (NS_FAILED(rv)) { + NS_WARNING( + "AutoBlockElementsJoiner::DeleteTextAtStartAndEndOfRange() failed"); + return Err(rv); + } + + if (!joinInclusiveAncestorBlockElements) { + break; + } + + AutoInclusiveAncestorBlockElementsJoiner joiner(*mLeftContent, + *mRightContent); + Result<bool, nsresult> canJoinThem = + joiner.Prepare(aHTMLEditor, aEditingHost); + if (canJoinThem.isErr()) { + NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Prepare() failed"); + return canJoinThem.propagateErr(); + } + + // If we're joining blocks: if deleting forward the selection should + // be collapsed to the end of the selection, if deleting backward the + // selection should be collapsed to the beginning of the selection. + // But if we're not joining then the selection should collapse to the + // beginning of the selection if we'redeleting forward, because the + // end of the selection will still be in the next block. And same + // thing for deleting backwards (selection should collapse to the end, + // because the beginning will still be in the first block). See Bug + // 507936. + if (aDirectionAndAmount == nsIEditor::eNext) { + aDirectionAndAmount = nsIEditor::ePrevious; + } else { + aDirectionAndAmount = nsIEditor::eNext; + } + + if (!canJoinThem.inspect()) { + result.MarkAsCanceled(); + break; + } + + if (!joiner.CanJoinBlocks()) { + break; + } + + Result<EditActionResult, nsresult> joinResult = + joiner.Run(aHTMLEditor, aEditingHost); + if (MOZ_UNLIKELY(joinResult.isErr())) { + NS_WARNING("AutoInclusiveAncestorBlockElementsJoiner::Run() failed"); + return joinResult; + } + result |= joinResult.unwrap(); +#ifdef DEBUG + if (joiner.ShouldDeleteLeafContentInstead()) { + NS_ASSERTION(result.Ignored(), + "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` " + "returning ignored, but returned not ignored"); + } else { + NS_ASSERTION(!result.Ignored(), + "Assumed `AutoInclusiveAncestorBlockElementsJoiner::Run()` " + "returning handled, but returned ignored"); + } +#endif // #ifdef DEBUG + pointToPutCaret = joiner.PointRefToPutCaret(); + break; + } + + // If we're deleting selection (not replacing with new content) and + // AutoInclusiveAncestorBlockElementsJoiner computed new caret position, we + // should use it. Otherwise, we should keep the traditional behavior. + if (result.Handled() && pointToPutCaret.IsSet()) { + EditorDOMRange range(aRangesToDelete.FirstRangeRef()); + nsresult rv = + mDeleteRangesHandler->DeleteUnnecessaryNodes(aHTMLEditor, range); + if (NS_FAILED(rv)) { + NS_WARNING("AutoDeleteRangesHandler::DeleteUnnecessaryNodes() failed"); + return Err(rv); + } + rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return Err(rv); + } + // If we prefer to use style in the previous line, we should forget + // previous styles since the caret position has all styles which we want + // to use with new content. + if (backspaceInRightBlock) { + aHTMLEditor.TopLevelEditSubActionDataRef().mCachedPendingStyles->Clear(); + } + // And we don't want to keep extending a link at ex-end of the previous + // paragraph. + if (HTMLEditor::GetLinkElement(pointToPutCaret.GetContainer())) { + aHTMLEditor.mPendingStylesToApplyToNewContent + ->ClearLinkAndItsSpecifiedStyle(); + } + return result; + } + + nsresult rv = + mDeleteRangesHandler->DeleteUnnecessaryNodesAndCollapseSelection( + aHTMLEditor, aDirectionAndAmount, + EditorDOMPoint(aRangesToDelete.FirstRangeRef()->StartRef()), + EditorDOMPoint(aRangesToDelete.FirstRangeRef()->EndRef())); + if (NS_FAILED(rv)) { + NS_WARNING( + "AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection() " + "failed"); + return Err(rv); + } + + result.MarkAsHandled(); + return result; +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::DeleteUnnecessaryNodes( + HTMLEditor& aHTMLEditor, EditorDOMRange& aRange) { + MOZ_ASSERT(aHTMLEditor.IsTopLevelEditSubActionDataAvailable()); + MOZ_ASSERT(EditorUtils::IsEditableContent( + *aRange.StartRef().ContainerAs<nsIContent>(), EditorType::HTML)); + MOZ_ASSERT(EditorUtils::IsEditableContent( + *aRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)); + + // If we're handling DnD, this is called to delete dragging item from the + // tree. In this case, we should remove parent blocks if it becomes empty. + if (aHTMLEditor.GetEditAction() == EditAction::eDrop || + aHTMLEditor.GetEditAction() == EditAction::eDeleteByDrag) { + MOZ_ASSERT(aRange.Collapsed() || + (aRange.StartRef().GetContainer()->GetNextSibling() == + aRange.EndRef().GetContainer() && + aRange.StartRef().IsEndOfContainer() && + aRange.EndRef().IsStartOfContainer())); + AutoTrackDOMRange trackRange(aHTMLEditor.RangeUpdaterRef(), &aRange); + + nsresult rv = DeleteParentBlocksWithTransactionIfEmpty(aHTMLEditor, + aRange.StartRef()); + if (NS_FAILED(rv)) { + NS_WARNING( + "HTMLEditor::DeleteParentBlocksWithTransactionIfEmpty() failed"); + return rv; + } + aHTMLEditor.TopLevelEditSubActionDataRef().mDidDeleteEmptyParentBlocks = + rv == NS_OK; + // If we removed parent blocks, Selection should be collapsed at where + // the most ancestor empty block has been. + if (aHTMLEditor.TopLevelEditSubActionDataRef() + .mDidDeleteEmptyParentBlocks) { + return NS_OK; + } + } + + if (NS_WARN_IF(!aRange.IsInContentNodes()) || + NS_WARN_IF(!EditorUtils::IsEditableContent( + *aRange.StartRef().ContainerAs<nsIContent>(), EditorType::HTML)) || + NS_WARN_IF(!EditorUtils::IsEditableContent( + *aRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML))) { + return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; + } + + // We might have left only collapsed white-space in the start/end nodes + AutoTrackDOMRange trackRange(aHTMLEditor.RangeUpdaterRef(), &aRange); + + OwningNonNull<nsIContent> startContainer = + *aRange.StartRef().ContainerAs<nsIContent>(); + OwningNonNull<nsIContent> endContainer = + *aRange.EndRef().ContainerAs<nsIContent>(); + nsresult rv = + DeleteNodeIfInvisibleAndEditableTextNode(aHTMLEditor, startContainer); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode() " + "failed to remove start node, but ignored"); + // If we've not handled the selection end container, and it's still + // editable, let's handle it. + if (aRange.InSameContainer() || + !EditorUtils::IsEditableContent( + *aRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)) { + return NS_OK; + } + rv = DeleteNodeIfInvisibleAndEditableTextNode(aHTMLEditor, endContainer); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode() " + "failed to remove end node, but ignored"); + return NS_OK; +} + +nsresult +HTMLEditor::AutoDeleteRangesHandler::DeleteUnnecessaryNodesAndCollapseSelection( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const EditorDOMPoint& aSelectionStartPoint, + const EditorDOMPoint& aSelectionEndPoint) { + EditorDOMRange range(aSelectionStartPoint, aSelectionEndPoint); + nsresult rv = DeleteUnnecessaryNodes(aHTMLEditor, range); + if (NS_FAILED(rv)) { + NS_WARNING("AutoDeleteRangesHandler::DeleteUnnecessaryNodes() failed"); + return rv; + } + + if (aHTMLEditor.GetEditAction() == EditAction::eDrop || + aHTMLEditor.GetEditAction() == EditAction::eDeleteByDrag) { + // If we removed parent blocks, Selection should be collapsed at where + // the most ancestor empty block has been. + // XXX I think that if the range is not in active editing host, we should + // not try to collapse selection here. + if (aHTMLEditor.TopLevelEditSubActionDataRef() + .mDidDeleteEmptyParentBlocks) { + nsresult rv = aHTMLEditor.CollapseSelectionTo(range.StartRef()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; + } + } + + rv = aHTMLEditor.CollapseSelectionTo( + aDirectionAndAmount == nsIEditor::ePrevious ? range.EndRef() + : range.StartRef()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return rv; +} + +nsresult +HTMLEditor::AutoDeleteRangesHandler::DeleteNodeIfInvisibleAndEditableTextNode( + HTMLEditor& aHTMLEditor, nsIContent& aContent) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + + Text* text = aContent.GetAsText(); + if (!text) { + return NS_OK; + } + + if (!HTMLEditUtils::IsRemovableFromParentNode(*text) || + HTMLEditUtils::IsVisibleTextNode(*text)) { + return NS_OK; + } + + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed"); + return rv; +} + +nsresult +HTMLEditor::AutoDeleteRangesHandler::DeleteParentBlocksWithTransactionIfEmpty( + HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint) { + MOZ_ASSERT(aPoint.IsSet()); + MOZ_ASSERT(aHTMLEditor.mPlaceholderBatch); + + // First, check there is visible contents before the point in current block. + RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost(); + WSRunScanner wsScannerForPoint(editingHost, aPoint); + if (!wsScannerForPoint.StartsFromCurrentBlockBoundary()) { + // If there is visible node before the point, we shouldn't remove the + // parent block. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + if (NS_WARN_IF(!wsScannerForPoint.GetStartReasonContent()) || + NS_WARN_IF(!wsScannerForPoint.GetStartReasonContent()->GetParentNode())) { + return NS_ERROR_FAILURE; + } + if (editingHost == wsScannerForPoint.GetStartReasonContent()) { + // If we reach editing host, there is no parent blocks which can be removed. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + if (HTMLEditUtils::IsTableCellOrCaption( + *wsScannerForPoint.GetStartReasonContent())) { + // If we reach a <td>, <th> or <caption>, we shouldn't remove it even + // becomes empty because removing such element changes the structure of + // the <table>. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + // Next, check there is visible contents after the point in current block. + WSScanResult forwardScanFromPointResult = + wsScannerForPoint.ScanNextVisibleNodeOrBlockBoundaryFrom(aPoint); + if (forwardScanFromPointResult.Failed()) { + NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom() failed"); + return NS_ERROR_FAILURE; + } + if (forwardScanFromPointResult.ReachedBRElement()) { + // XXX In my understanding, this is odd. The end reason may not be + // same as the reached <br> element because the equality is + // guaranteed only when ReachedCurrentBlockBoundary() returns true. + // However, looks like that this code assumes that + // GetEndReasonContent() returns the (or a) <br> element. + NS_ASSERTION(wsScannerForPoint.GetEndReasonContent() == + forwardScanFromPointResult.BRElementPtr(), + "End reason is not the reached <br> element"); + // If the <br> element is visible, we shouldn't remove the parent block. + if (HTMLEditUtils::IsVisibleBRElement( + *wsScannerForPoint.GetEndReasonContent())) { + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + if (wsScannerForPoint.GetEndReasonContent()->GetNextSibling()) { + WSScanResult scanResult = + WSRunScanner::ScanNextVisibleNodeOrBlockBoundary( + editingHost, EditorRawDOMPoint::After( + *wsScannerForPoint.GetEndReasonContent())); + if (scanResult.Failed()) { + NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed"); + return NS_ERROR_FAILURE; + } + if (!scanResult.ReachedCurrentBlockBoundary()) { + // If we couldn't reach the block's end after the invisible <br>, + // that means that there is visible content. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + } + } else if (!forwardScanFromPointResult.ReachedCurrentBlockBoundary()) { + // If we couldn't reach the block's end, the block has visible content. + return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND; + } + + // Delete the parent block. + EditorDOMPoint nextPoint( + wsScannerForPoint.GetStartReasonContent()->GetParentNode(), 0); + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction( + MOZ_KnownLive(*wsScannerForPoint.GetStartReasonContent())); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + // If we reach editing host, return NS_OK. + if (nextPoint.GetContainer() == editingHost) { + return NS_OK; + } + + // Otherwise, we need to check whether we're still in empty block or not. + + // If we have mutation event listeners, the next point is now outside of + // editing host or editing hos has been changed. + if (aHTMLEditor.MayHaveMutationEventListeners( + NS_EVENT_BITS_MUTATION_NODEREMOVED | + NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT | + NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED)) { + Element* newEditingHost = aHTMLEditor.ComputeEditingHost(); + if (NS_WARN_IF(!newEditingHost) || + NS_WARN_IF(newEditingHost != editingHost)) { + return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; + } + if (NS_WARN_IF(!EditorUtils::IsDescendantOf(*nextPoint.GetContainer(), + *newEditingHost))) { + return NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE; + } + } + + rv = DeleteParentBlocksWithTransactionIfEmpty(aHTMLEditor, nextPoint); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoDeleteRangesHandler::" + "DeleteParentBlocksWithTransactionIfEmpty() failed"); + return rv; +} + +nsresult +HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteRangesWithTransaction( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + AutoRangeArray& aRangesToDelete) const { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); + + EditorBase::HowToHandleCollapsedRange howToHandleCollapsedRange = + EditorBase::HowToHandleCollapsedRangeFor(aDirectionAndAmount); + if (NS_WARN_IF(aRangesToDelete.IsCollapsed() && + howToHandleCollapsedRange == + EditorBase::HowToHandleCollapsedRange::Ignore)) { + return NS_ERROR_FAILURE; + } + + auto extendRangeToSelectCharacterForward = + [](nsRange& aRange, const EditorRawDOMPointInText& aCaretPoint) -> void { + const nsTextFragment& textFragment = + aCaretPoint.ContainerAs<Text>()->TextFragment(); + if (!textFragment.GetLength()) { + return; + } + if (textFragment.IsHighSurrogateFollowedByLowSurrogateAt( + aCaretPoint.Offset())) { + DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd( + aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset(), + aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() + 2); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "nsRange::SetStartAndEnd() failed"); + return; + } + DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd( + aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset(), + aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() + 1); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "nsRange::SetStartAndEnd() failed"); + }; + auto extendRangeToSelectCharacterBackward = + [](nsRange& aRange, const EditorRawDOMPointInText& aCaretPoint) -> void { + if (aCaretPoint.IsStartOfContainer()) { + return; + } + const nsTextFragment& textFragment = + aCaretPoint.ContainerAs<Text>()->TextFragment(); + if (!textFragment.GetLength()) { + return; + } + if (textFragment.IsLowSurrogateFollowingHighSurrogateAt( + aCaretPoint.Offset() - 1)) { + DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd( + aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() - 2, + aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "nsRange::SetStartAndEnd() failed"); + return; + } + DebugOnly<nsresult> rvIgnored = aRange.SetStartAndEnd( + aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset() - 1, + aCaretPoint.ContainerAs<Text>(), aCaretPoint.Offset()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "nsRange::SetStartAndEnd() failed"); + }; + + RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost(); + for (OwningNonNull<nsRange>& range : aRangesToDelete.Ranges()) { + // If it's not collapsed, `DeleteRangeTransaction::Create()` will be called + // with it and `DeleteRangeTransaction` won't modify the range. + if (!range->Collapsed()) { + continue; + } + + if (howToHandleCollapsedRange == + EditorBase::HowToHandleCollapsedRange::Ignore) { + continue; + } + + // In the other cases, `EditorBase::CreateTransactionForCollapsedRange()` + // will handle the collapsed range. + EditorRawDOMPoint caretPoint(range->StartRef()); + if (howToHandleCollapsedRange == + EditorBase::HowToHandleCollapsedRange::ExtendBackward && + caretPoint.IsStartOfContainer()) { + nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousContent( + *caretPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode}, + editingHost); + if (!previousEditableContent) { + continue; + } + if (!previousEditableContent->IsText()) { + IgnoredErrorResult ignoredError; + range->SelectNode(*previousEditableContent, ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "nsRange::SelectNode() failed"); + continue; + } + + extendRangeToSelectCharacterBackward( + range, + EditorRawDOMPointInText::AtEndOf(*previousEditableContent->AsText())); + continue; + } + + if (howToHandleCollapsedRange == + EditorBase::HowToHandleCollapsedRange::ExtendForward && + caretPoint.IsEndOfContainer()) { + nsIContent* nextEditableContent = HTMLEditUtils::GetNextContent( + *caretPoint.GetContainer(), {WalkTreeOption::IgnoreNonEditableNode}, + editingHost); + if (!nextEditableContent) { + continue; + } + + if (!nextEditableContent->IsText()) { + IgnoredErrorResult ignoredError; + range->SelectNode(*nextEditableContent, ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "nsRange::SelectNode() failed"); + continue; + } + + extendRangeToSelectCharacterForward( + range, EditorRawDOMPointInText(nextEditableContent->AsText(), 0)); + continue; + } + + if (caretPoint.IsInTextNode()) { + if (howToHandleCollapsedRange == + EditorBase::HowToHandleCollapsedRange::ExtendBackward) { + extendRangeToSelectCharacterBackward( + range, EditorRawDOMPointInText(caretPoint.ContainerAs<Text>(), + caretPoint.Offset())); + continue; + } + extendRangeToSelectCharacterForward( + range, EditorRawDOMPointInText(caretPoint.ContainerAs<Text>(), + caretPoint.Offset())); + continue; + } + + nsIContent* editableContent = + howToHandleCollapsedRange == + EditorBase::HowToHandleCollapsedRange::ExtendBackward + ? HTMLEditUtils::GetPreviousContent( + caretPoint, {WalkTreeOption::IgnoreNonEditableNode}, + editingHost) + : HTMLEditUtils::GetNextContent( + caretPoint, {WalkTreeOption::IgnoreNonEditableNode}, + editingHost); + if (!editableContent) { + continue; + } + while (editableContent && editableContent->IsCharacterData() && + !editableContent->Length()) { + editableContent = + howToHandleCollapsedRange == + EditorBase::HowToHandleCollapsedRange::ExtendBackward + ? HTMLEditUtils::GetPreviousContent( + *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, + editingHost) + : HTMLEditUtils::GetNextContent( + *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, + editingHost); + } + if (!editableContent) { + continue; + } + + if (!editableContent->IsText()) { + IgnoredErrorResult ignoredError; + range->SelectNode(*editableContent, ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "nsRange::SelectNode() failed"); + continue; + } + + if (howToHandleCollapsedRange == + EditorBase::HowToHandleCollapsedRange::ExtendBackward) { + extendRangeToSelectCharacterBackward( + range, EditorRawDOMPointInText::AtEndOf(*editableContent->AsText())); + continue; + } + extendRangeToSelectCharacterForward( + range, EditorRawDOMPointInText(editableContent->AsText(), 0)); + } + + return NS_OK; +} + +template <typename EditorDOMPointType> +Result<CaretPoint, nsresult> HTMLEditor::DeleteTextAndTextNodesWithTransaction( + const EditorDOMPointType& aStartPoint, const EditorDOMPointType& aEndPoint, + TreatEmptyTextNodes aTreatEmptyTextNodes) { + if (NS_WARN_IF(!aStartPoint.IsSet()) || NS_WARN_IF(!aEndPoint.IsSet())) { + return Err(NS_ERROR_INVALID_ARG); + } + + // MOOSE: this routine needs to be modified to preserve the integrity of the + // wsFragment info. + + if (aStartPoint == aEndPoint) { + // Nothing to delete + return CaretPoint(EditorDOMPoint()); + } + + RefPtr<Element> editingHost = ComputeEditingHost(); + auto DeleteEmptyContentNodeWithTransaction = + [this, &aTreatEmptyTextNodes, &editingHost](nsIContent& aContent) + MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> nsresult { + OwningNonNull<nsIContent> nodeToRemove = aContent; + if (aTreatEmptyTextNodes == + TreatEmptyTextNodes::RemoveAllEmptyInlineAncestors) { + Element* emptyParentElementToRemove = + HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( + nodeToRemove, editingHost); + if (emptyParentElementToRemove) { + nodeToRemove = *emptyParentElementToRemove; + } + } + nsresult rv = DeleteNodeWithTransaction(nodeToRemove); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + }; + + if (aStartPoint.GetContainer() == aEndPoint.GetContainer() && + aStartPoint.IsInTextNode()) { + if (aTreatEmptyTextNodes != + TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries && + aStartPoint.IsStartOfContainer() && aEndPoint.IsEndOfContainer()) { + nsresult rv = DeleteEmptyContentNodeWithTransaction( + MOZ_KnownLive(*aStartPoint.template ContainerAs<Text>())); + if (NS_FAILED(rv)) { + NS_WARNING("deleteEmptyContentNodeWithTransaction() failed"); + return Err(rv); + } + return CaretPoint(EditorDOMPoint()); + } + RefPtr<Text> textNode = aStartPoint.template ContainerAs<Text>(); + Result<CaretPoint, nsresult> caretPointOrError = + DeleteTextWithTransaction(*textNode, aStartPoint.Offset(), + aEndPoint.Offset() - aStartPoint.Offset()); + NS_WARNING_ASSERTION(caretPointOrError.isOk(), + "HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError; + } + + RefPtr<nsRange> range = + nsRange::Create(aStartPoint.ToRawRangeBoundary(), + aEndPoint.ToRawRangeBoundary(), IgnoreErrors()); + if (!range) { + NS_WARNING("nsRange::Create() failed"); + return Err(NS_ERROR_FAILURE); + } + + // Collect editable text nodes in the given range. + AutoTArray<OwningNonNull<Text>, 16> arrayOfTextNodes; + DOMIterator iter; + if (NS_FAILED(iter.Init(*range))) { + return CaretPoint(EditorDOMPoint()); // Nothing to delete in the range. + } + iter.AppendNodesToArray( + +[](nsINode& aNode, void*) { + MOZ_ASSERT(aNode.IsText()); + return HTMLEditUtils::IsSimplyEditableNode(aNode); + }, + arrayOfTextNodes); + EditorDOMPoint pointToPutCaret; + for (OwningNonNull<Text>& textNode : arrayOfTextNodes) { + if (textNode == aStartPoint.GetContainer()) { + if (aStartPoint.IsEndOfContainer()) { + continue; + } + if (aStartPoint.IsStartOfContainer() && + aTreatEmptyTextNodes != + TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) { + AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), + &pointToPutCaret); + nsresult rv = DeleteEmptyContentNodeWithTransaction( + MOZ_KnownLive(*aStartPoint.template ContainerAs<Text>())); + if (NS_FAILED(rv)) { + NS_WARNING("DeleteEmptyContentNodeWithTransaction() failed"); + return Err(rv); + } + continue; + } + AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), + &pointToPutCaret); + Result<CaretPoint, nsresult> caretPointOrError = + DeleteTextWithTransaction(MOZ_KnownLive(textNode), + aStartPoint.Offset(), + textNode->Length() - aStartPoint.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError; + } + trackPointToPutCaret.FlushAndStopTracking(); + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); + continue; + } + + if (textNode == aEndPoint.GetContainer()) { + if (aEndPoint.IsStartOfContainer()) { + break; + } + if (aEndPoint.IsEndOfContainer() && + aTreatEmptyTextNodes != + TreatEmptyTextNodes::KeepIfContainerOfRangeBoundaries) { + AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), + &pointToPutCaret); + nsresult rv = DeleteEmptyContentNodeWithTransaction( + MOZ_KnownLive(*aEndPoint.template ContainerAs<Text>())); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "DeleteEmptyContentNodeWithTransaction() failed"); + return Err(rv); + } + AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), + &pointToPutCaret); + Result<CaretPoint, nsresult> caretPointOrError = + DeleteTextWithTransaction(MOZ_KnownLive(textNode), 0, + aEndPoint.Offset()); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError; + } + trackPointToPutCaret.FlushAndStopTracking(); + caretPointOrError.unwrap().MoveCaretPointTo( + pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); + return CaretPoint(pointToPutCaret); + } + + nsresult rv = + DeleteEmptyContentNodeWithTransaction(MOZ_KnownLive(textNode)); + if (NS_FAILED(rv)) { + NS_WARNING("DeleteEmptyContentNodeWithTransaction() failed"); + return Err(rv); + } + } + + return CaretPoint(pointToPutCaret); +} + +Result<EditorDOMPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::JoinNodesDeepWithTransaction( + HTMLEditor& aHTMLEditor, nsIContent& aLeftContent, + nsIContent& aRightContent) { + // While the rightmost children and their descendants of the left node match + // the leftmost children and their descendants of the right node, join them + // up. + + nsCOMPtr<nsIContent> leftContentToJoin = &aLeftContent; + nsCOMPtr<nsIContent> rightContentToJoin = &aRightContent; + nsCOMPtr<nsINode> parentNode = aRightContent.GetParentNode(); + + EditorDOMPoint ret; + while (leftContentToJoin && rightContentToJoin && parentNode && + HTMLEditUtils::CanContentsBeJoined(*leftContentToJoin, + *rightContentToJoin)) { + // Do the join + Result<JoinNodesResult, nsresult> joinNodesResult = + aHTMLEditor.JoinNodesWithTransaction(*leftContentToJoin, + *rightContentToJoin); + if (MOZ_UNLIKELY(joinNodesResult.isErr())) { + NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed"); + return joinNodesResult.propagateErr(); + } + + ret = joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>(); + if (NS_WARN_IF(!ret.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + + if (parentNode->IsText()) { + // We've joined all the way down to text nodes, we're done! + return ret; + } + + // Get new left and right nodes, and begin anew + rightContentToJoin = ret.GetCurrentChildAtOffset(); + if (rightContentToJoin) { + leftContentToJoin = rightContentToJoin->GetPreviousSibling(); + } else { + leftContentToJoin = nullptr; + } + + // Skip over non-editable nodes + while (leftContentToJoin && !EditorUtils::IsEditableContent( + *leftContentToJoin, EditorType::HTML)) { + leftContentToJoin = leftContentToJoin->GetPreviousSibling(); + } + if (!leftContentToJoin) { + return ret; + } + + while (rightContentToJoin && !EditorUtils::IsEditableContent( + *rightContentToJoin, EditorType::HTML)) { + rightContentToJoin = rightContentToJoin->GetNextSibling(); + } + if (!rightContentToJoin) { + return ret; + } + } + + if (!ret.IsSet()) { + NS_WARNING("HTMLEditor::JoinNodesDeepWithTransaction() joined no contents"); + return Err(NS_ERROR_FAILURE); + } + return ret; +} + +Result<bool, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner::Prepare( + const HTMLEditor& aHTMLEditor, const Element& aEditingHost) { + mLeftBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( + mInclusiveDescendantOfLeftBlockElement, + HTMLEditUtils::ClosestEditableBlockElementExceptHRElement); + mRightBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( + mInclusiveDescendantOfRightBlockElement, + HTMLEditUtils::ClosestEditableBlockElementExceptHRElement); + + if (NS_WARN_IF(!IsSet())) { + mCanJoinBlocks = false; + return Err(NS_ERROR_UNEXPECTED); + } + + // Don't join the blocks if both of them are basic structure of the HTML + // document (Note that `<body>` can be joined with its children). + if (mLeftBlockElement->IsAnyOfHTMLElements(nsGkAtoms::html, nsGkAtoms::head, + nsGkAtoms::body) && + mRightBlockElement->IsAnyOfHTMLElements(nsGkAtoms::html, nsGkAtoms::head, + nsGkAtoms::body)) { + mCanJoinBlocks = false; + return false; + } + + if (HTMLEditUtils::IsAnyTableElement(mLeftBlockElement) || + HTMLEditUtils::IsAnyTableElement(mRightBlockElement)) { + // Do not try to merge table elements, cancel the deletion. + mCanJoinBlocks = false; + return false; + } + + // Bail if both blocks the same + if (IsSameBlockElement()) { + mCanJoinBlocks = true; // XXX Anyway, Run() will ingore this case. + mFallbackToDeleteLeafContent = true; + return true; + } + + // Joining a list item to its parent is a NOP. + if (HTMLEditUtils::IsAnyListElement(mLeftBlockElement) && + HTMLEditUtils::IsListItem(mRightBlockElement) && + mRightBlockElement->GetParentNode() == mLeftBlockElement) { + mCanJoinBlocks = false; + return true; + } + + // Special rule here: if we are trying to join list items, and they are in + // different lists, join the lists instead. + if (HTMLEditUtils::IsListItem(mLeftBlockElement) && + HTMLEditUtils::IsListItem(mRightBlockElement)) { + // XXX leftListElement and/or rightListElement may be not list elements. + Element* leftListElement = mLeftBlockElement->GetParentElement(); + Element* rightListElement = mRightBlockElement->GetParentElement(); + EditorDOMPoint atChildInBlock; + if (leftListElement && rightListElement && + leftListElement != rightListElement && + !EditorUtils::IsDescendantOf(*leftListElement, *mRightBlockElement, + &atChildInBlock) && + !EditorUtils::IsDescendantOf(*rightListElement, *mLeftBlockElement, + &atChildInBlock)) { + // There are some special complications if the lists are descendants of + // the other lists' items. Note that it is okay for them to be + // descendants of the other lists themselves, which is the usual case for + // sublists in our implementation. + MOZ_DIAGNOSTIC_ASSERT(!atChildInBlock.IsSet()); + mLeftBlockElement = leftListElement; + mRightBlockElement = rightListElement; + mNewListElementTagNameOfRightListElement = + Some(leftListElement->NodeInfo()->NameAtom()); + } + } + + if (!EditorUtils::IsDescendantOf(*mLeftBlockElement, *mRightBlockElement, + &mPointContainingTheOtherBlockElement)) { + Unused << EditorUtils::IsDescendantOf( + *mRightBlockElement, *mLeftBlockElement, + &mPointContainingTheOtherBlockElement); + } + + if (mPointContainingTheOtherBlockElement.GetContainer() == + mRightBlockElement) { + mPrecedingInvisibleBRElement = + WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound( + aHTMLEditor.ComputeEditingHost(), + EditorDOMPoint::AtEndOf(mLeftBlockElement)); + // `WhiteSpaceVisibilityKeeper:: + // MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()` + // returns ignored when: + // - No preceding invisible `<br>` element and + // - mNewListElementTagNameOfRightListElement is nothing and + // - There is no content to move from right block element. + if (!mPrecedingInvisibleBRElement) { + if (CanMergeLeftAndRightBlockElements()) { + // Always marked as handled in this case. + mFallbackToDeleteLeafContent = false; + } else { + // Marked as handled only when it actually moves a content node. + Result<bool, nsresult> firstLineHasContent = + AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine( + mPointContainingTheOtherBlockElement + .NextPoint<EditorDOMPoint>(), + aEditingHost); + mFallbackToDeleteLeafContent = + firstLineHasContent.isOk() && !firstLineHasContent.inspect(); + } + } else { + // Marked as handled when deleting the invisible `<br>` element. + mFallbackToDeleteLeafContent = false; + } + } else if (mPointContainingTheOtherBlockElement.GetContainer() == + mLeftBlockElement) { + mPrecedingInvisibleBRElement = + WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound( + aHTMLEditor.ComputeEditingHost(), + mPointContainingTheOtherBlockElement); + // `WhiteSpaceVisibilityKeeper:: + // MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` + // returns ignored when: + // - No preceding invisible `<br>` element and + // - mNewListElementTagNameOfRightListElement is some and + // - The right block element has no children + // or, + // - No preceding invisible `<br>` element and + // - mNewListElementTagNameOfRightListElement is nothing and + // - There is no content to move from right block element. + if (!mPrecedingInvisibleBRElement) { + if (CanMergeLeftAndRightBlockElements()) { + // Marked as handled only when it actualy moves a content node. + Result<bool, nsresult> rightBlockHasContent = + aHTMLEditor.CanMoveChildren(*mRightBlockElement, + *mLeftBlockElement); + mFallbackToDeleteLeafContent = + rightBlockHasContent.isOk() && !rightBlockHasContent.inspect(); + } else { + // Marked as handled only when it actually moves a content node. + Result<bool, nsresult> firstLineHasContent = + AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine( + EditorDOMPoint(mRightBlockElement, 0u), aEditingHost); + mFallbackToDeleteLeafContent = + firstLineHasContent.isOk() && !firstLineHasContent.inspect(); + } + } else { + // Marked as handled when deleting the invisible `<br>` element. + mFallbackToDeleteLeafContent = false; + } + } else { + mPrecedingInvisibleBRElement = + WSRunScanner::GetPrecedingBRElementUnlessVisibleContentFound( + aHTMLEditor.ComputeEditingHost(), + EditorDOMPoint::AtEndOf(mLeftBlockElement)); + // `WhiteSpaceVisibilityKeeper:: + // MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` always + // return "handled". + mFallbackToDeleteLeafContent = false; + } + + mCanJoinBlocks = true; + return true; +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + AutoInclusiveAncestorBlockElementsJoiner::ComputeRangesToDelete( + const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint, + AutoRangeArray& aRangesToDelete) const { + MOZ_ASSERT(!aRangesToDelete.Ranges().IsEmpty()); + MOZ_ASSERT(mLeftBlockElement); + MOZ_ASSERT(mRightBlockElement); + + if (IsSameBlockElement()) { + if (!aCaretPoint.IsSet()) { + return NS_OK; // The ranges are not collapsed, keep them as-is. + } + nsresult rv = aRangesToDelete.Collapse(aCaretPoint); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::Collapse() failed"); + return rv; + } + + EditorDOMPoint pointContainingTheOtherBlock; + if (!EditorUtils::IsDescendantOf(*mLeftBlockElement, *mRightBlockElement, + &pointContainingTheOtherBlock)) { + Unused << EditorUtils::IsDescendantOf( + *mRightBlockElement, *mLeftBlockElement, &pointContainingTheOtherBlock); + } + EditorDOMRange range = + WSRunScanner::GetRangeForDeletingBlockElementBoundaries( + aHTMLEditor, *mLeftBlockElement, *mRightBlockElement, + pointContainingTheOtherBlock); + if (!range.IsPositioned()) { + NS_WARNING( + "WSRunScanner::GetRangeForDeletingBlockElementBoundaries() failed"); + return NS_ERROR_FAILURE; + } + if (!aCaretPoint.IsSet()) { + // Don't shrink the original range. + bool noNeedToChangeStart = false; + const auto atStart = + aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>(); + if (atStart.IsBefore(range.StartRef())) { + // If the range starts from end of a container, and computed block + // boundaries range starts from an invisible `<br>` element, we + // may need to shrink the range. + Element* editingHost = aHTMLEditor.ComputeEditingHost(); + NS_WARNING_ASSERTION(editingHost, "There was no editing host"); + nsIContent* nextContent = + atStart.IsEndOfContainer() && range.StartRef().GetChild() && + HTMLEditUtils::IsInvisibleBRElement( + *range.StartRef().GetChild()) + ? HTMLEditUtils::GetNextContent( + *atStart.ContainerAs<nsIContent>(), + {WalkTreeOption::IgnoreDataNodeExceptText, + WalkTreeOption::StopAtBlockBoundary}, + editingHost) + : nullptr; + if (!nextContent || nextContent != range.StartRef().GetChild()) { + noNeedToChangeStart = true; + range.SetStart( + aRangesToDelete.GetFirstRangeStartPoint<EditorDOMPoint>()); + } + } + if (range.EndRef().IsBefore( + aRangesToDelete.GetFirstRangeEndPoint<EditorRawDOMPoint>())) { + if (noNeedToChangeStart) { + return NS_OK; // We don't need to modify the range. + } + range.SetEnd(aRangesToDelete.GetFirstRangeEndPoint<EditorDOMPoint>()); + } + } + // XXX Oddly, we join blocks only at the first range. + nsresult rv = aRangesToDelete.FirstRangeRef()->SetStartAndEnd( + range.StartRef().ToRawRangeBoundary(), + range.EndRef().ToRawRangeBoundary()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoRangeArray::SetStartAndEnd() failed"); + return rv; +} + +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner::Run( + HTMLEditor& aHTMLEditor, const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(mLeftBlockElement); + MOZ_ASSERT(mRightBlockElement); + + if (IsSameBlockElement()) { + return EditActionResult::IgnoredResult(); + } + + if (!mCanJoinBlocks) { + return EditActionResult::HandledResult(); + } + + EditorDOMPoint startOfRightContent; + + // If the left block element is in the right block element, move the hard + // line including the right block element to end of the left block. + // However, if we are merging list elements, we don't join them. + Result<EditActionResult, nsresult> result(NS_ERROR_NOT_INITIALIZED); + if (mPointContainingTheOtherBlockElement.GetContainer() == + mRightBlockElement) { + startOfRightContent = mPointContainingTheOtherBlockElement.NextPoint(); + if (Element* element = startOfRightContent.GetChildAs<Element>()) { + startOfRightContent = + HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>( + *element); + } + AutoTrackDOMPoint trackStartOfRightBlock(aHTMLEditor.RangeUpdaterRef(), + &startOfRightContent); + result = WhiteSpaceVisibilityKeeper:: + MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement( + aHTMLEditor, MOZ_KnownLive(*mLeftBlockElement), + MOZ_KnownLive(*mRightBlockElement), + mPointContainingTheOtherBlockElement, + mNewListElementTagNameOfRightListElement, + MOZ_KnownLive(mPrecedingInvisibleBRElement), aEditingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "WhiteSpaceVisibilityKeeper::" + "MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement() " + "failed"); + return result; + } + if (NS_WARN_IF(!startOfRightContent.IsSet()) || + NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + } + + // If the right block element is in the left block element: + // - move list item elements in the right block element to where the left + // list element is + // - or first hard line in the right block element to where: + // - the left block element is. + // - or the given left content in the left block is. + else if (mPointContainingTheOtherBlockElement.GetContainer() == + mLeftBlockElement) { + startOfRightContent = + HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>( + *mRightBlockElement); + AutoTrackDOMPoint trackStartOfRightBlock(aHTMLEditor.RangeUpdaterRef(), + &startOfRightContent); + result = WhiteSpaceVisibilityKeeper:: + MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement( + aHTMLEditor, MOZ_KnownLive(*mLeftBlockElement), + MOZ_KnownLive(*mRightBlockElement), + mPointContainingTheOtherBlockElement, + MOZ_KnownLive(*mInclusiveDescendantOfLeftBlockElement), + mNewListElementTagNameOfRightListElement, + MOZ_KnownLive(mPrecedingInvisibleBRElement), aEditingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "WhiteSpaceVisibilityKeeper::" + "MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement() " + "failed"); + return result; + } + trackStartOfRightBlock.FlushAndStopTracking(); + if (NS_WARN_IF(!startOfRightContent.IsSet()) || + NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + } + + // Normal case. Blocks are siblings, or at least close enough. An example + // of the latter is <p>paragraph</p><ul><li>one<li>two<li>three</ul>. The + // first li and the p are not true siblings, but we still want to join them + // if you backspace from li into p. + else { + MOZ_ASSERT(!mPointContainingTheOtherBlockElement.IsSet()); + + startOfRightContent = + HTMLEditUtils::GetDeepestEditableStartPointOf<EditorDOMPoint>( + *mRightBlockElement); + AutoTrackDOMPoint trackStartOfRightBlock(aHTMLEditor.RangeUpdaterRef(), + &startOfRightContent); + result = WhiteSpaceVisibilityKeeper:: + MergeFirstLineOfRightBlockElementIntoLeftBlockElement( + aHTMLEditor, MOZ_KnownLive(*mLeftBlockElement), + MOZ_KnownLive(*mRightBlockElement), + mNewListElementTagNameOfRightListElement, + MOZ_KnownLive(mPrecedingInvisibleBRElement), aEditingHost); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "WhiteSpaceVisibilityKeeper::" + "MergeFirstLineOfRightBlockElementIntoLeftBlockElement() failed"); + return result; + } + trackStartOfRightBlock.FlushAndStopTracking(); + if (NS_WARN_IF(!startOfRightContent.IsSet()) || + NS_WARN_IF(!startOfRightContent.GetContainer()->IsInComposedDoc())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + } + + // If we're deleting selection (meaning not replacing selection with new + // content), we should put caret to end of preceding text node if there is. + // Then, users can type text into it like the other browsers. + if (MayEditActionDeleteAroundCollapsedSelection( + aHTMLEditor.GetEditAction())) { + WSRunScanner scanner(&aEditingHost, startOfRightContent); + WSScanResult maybePreviousText = + scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(startOfRightContent); + if (maybePreviousText.IsContentEditable() && + maybePreviousText.InVisibleOrCollapsibleCharacters()) { + mPointToPutCaret = maybePreviousText.Point<EditorDOMPoint>(); + } + } + return result; +} + +// static +Result<bool, nsresult> +HTMLEditor::AutoMoveOneLineHandler::CanMoveOrDeleteSomethingInLine( + const EditorDOMPoint& aPointInHardLine, const Element& aEditingHost) { + if (NS_WARN_IF(!aPointInHardLine.IsSet()) || + NS_WARN_IF(aPointInHardLine.IsInNativeAnonymousSubtree())) { + return Err(NS_ERROR_INVALID_ARG); + } + + RefPtr<nsRange> oneLineRange = + AutoRangeArray::CreateRangeWrappingStartAndEndLinesContainingBoundaries( + aPointInHardLine, aPointInHardLine, + EditSubAction::eMergeBlockContents, aEditingHost); + if (!oneLineRange || oneLineRange->Collapsed() || + !oneLineRange->IsPositioned() || + !oneLineRange->GetStartContainer()->IsContent() || + !oneLineRange->GetEndContainer()->IsContent()) { + return false; + } + + // If there is only a padding `<br>` element in a empty block, it's selected + // by `UpdatePointsToSelectAllChildrenIfCollapsedInEmptyBlockElement()`. + // However, it won't be moved. Although it'll be deleted, + // AutoMoveOneLineHandler returns "ignored". Therefore, we should return + // `false` in this case. + if (nsIContent* childContent = oneLineRange->GetChildAtStartOffset()) { + if (childContent->IsHTMLElement(nsGkAtoms::br) && + childContent->GetParent()) { + if (const Element* blockElement = + HTMLEditUtils::GetInclusiveAncestorElement( + *childContent->GetParent(), + HTMLEditUtils::ClosestBlockElement)) { + if (HTMLEditUtils::IsEmptyNode(*blockElement)) { + return false; + } + } + } + } + + nsINode* commonAncestor = oneLineRange->GetClosestCommonInclusiveAncestor(); + // Currently, we move non-editable content nodes too. + EditorRawDOMPoint startPoint(oneLineRange->StartRef()); + if (!startPoint.IsEndOfContainer()) { + return true; + } + EditorRawDOMPoint endPoint(oneLineRange->EndRef()); + if (!endPoint.IsStartOfContainer()) { + return true; + } + if (startPoint.GetContainer() != commonAncestor) { + while (true) { + EditorRawDOMPoint pointInParent(startPoint.GetContainerAs<nsIContent>()); + if (NS_WARN_IF(!pointInParent.IsInContentNode())) { + return Err(NS_ERROR_FAILURE); + } + if (pointInParent.GetContainer() == commonAncestor) { + startPoint = pointInParent; + break; + } + if (!pointInParent.IsEndOfContainer()) { + return true; + } + } + } + if (endPoint.GetContainer() != commonAncestor) { + while (true) { + EditorRawDOMPoint pointInParent(endPoint.GetContainerAs<nsIContent>()); + if (NS_WARN_IF(!pointInParent.IsInContentNode())) { + return Err(NS_ERROR_FAILURE); + } + if (pointInParent.GetContainer() == commonAncestor) { + endPoint = pointInParent; + break; + } + if (!pointInParent.IsStartOfContainer()) { + return true; + } + } + } + // If start point and end point in the common ancestor are direct siblings, + // there is no content to move or delete. + // E.g., `<b>abc<br>[</b><i>]<br>def</i>`. + return startPoint.GetNextSiblingOfChild() != endPoint.GetChild(); +} + +nsresult HTMLEditor::AutoMoveOneLineHandler::Prepare( + HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointInHardLine, + const Element& aEditingHost) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(aPointInHardLine.IsInContentNode()); + MOZ_ASSERT(mPointToInsert.IsSetAndValid()); + + if (NS_WARN_IF(mPointToInsert.IsInNativeAnonymousSubtree())) { + return Err(NS_ERROR_INVALID_ARG); + } + + mSrcInclusiveAncestorBlock = + aPointInHardLine.IsInContentNode() + ? HTMLEditUtils::GetInclusiveAncestorElement( + *aPointInHardLine.ContainerAs<nsIContent>(), + HTMLEditUtils::ClosestBlockElement) + : nullptr; + mDestInclusiveAncestorBlock = + mPointToInsert.IsInContentNode() + ? HTMLEditUtils::GetInclusiveAncestorElement( + *mPointToInsert.ContainerAs<nsIContent>(), + HTMLEditUtils::ClosestBlockElement) + : nullptr; + mMovingToParentBlock = + mDestInclusiveAncestorBlock && mSrcInclusiveAncestorBlock && + mDestInclusiveAncestorBlock != mSrcInclusiveAncestorBlock && + mSrcInclusiveAncestorBlock->IsInclusiveDescendantOf( + mDestInclusiveAncestorBlock); + mTopmostSrcAncestorBlockInDestBlock = + mMovingToParentBlock + ? AutoMoveOneLineHandler:: + GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement( + *mSrcInclusiveAncestorBlock, *mDestInclusiveAncestorBlock) + : nullptr; + MOZ_ASSERT_IF(mMovingToParentBlock, mTopmostSrcAncestorBlockInDestBlock); + + mPreserveWhiteSpaceStyle = + AutoMoveOneLineHandler::ConsiderWhetherPreserveWhiteSpaceStyle( + aPointInHardLine.GetContainerAs<nsIContent>(), + mDestInclusiveAncestorBlock); + + AutoRangeArray rangesToWrapTheLine(aPointInHardLine); + rangesToWrapTheLine.ExtendRangesToWrapLinesToHandleBlockLevelEditAction( + EditSubAction::eMergeBlockContents, aEditingHost); + MOZ_ASSERT(rangesToWrapTheLine.Ranges().Length() <= 1u); + mLineRange = EditorDOMRange(rangesToWrapTheLine.FirstRangeRef()); + return NS_OK; +} + +Result<CaretPoint, nsresult> +HTMLEditor::AutoMoveOneLineHandler::SplitToMakeTheLineIsolated( + HTMLEditor& aHTMLEditor, const nsIContent& aNewContainer, + const Element& aEditingHost, + nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) const { + AutoRangeArray rangesToWrapTheLine(mLineRange); + Result<EditorDOMPoint, nsresult> splitResult = + rangesToWrapTheLine + .SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries( + aHTMLEditor, aEditingHost, &aNewContainer); + if (MOZ_UNLIKELY(splitResult.isErr())) { + NS_WARNING( + "AutoRangeArray::" + "SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries() failed"); + return Err(splitResult.unwrapErr()); + } + EditorDOMPoint pointToPutCaret; + if (splitResult.inspect().IsSet()) { + pointToPutCaret = splitResult.unwrap(); + } + nsresult rv = rangesToWrapTheLine.CollectEditTargetNodes( + aHTMLEditor, aOutArrayOfContents, EditSubAction::eMergeBlockContents, + AutoRangeArray::CollectNonEditableNodes::Yes); + if (NS_FAILED(rv)) { + NS_WARNING( + "AutoRangeArray::CollectEditTargetNodes(EditSubAction::" + "eMergeBlockContents, CollectNonEditableNodes::Yes) failed"); + return Err(rv); + } + return CaretPoint(pointToPutCaret); +} + +// static +Element* HTMLEditor::AutoMoveOneLineHandler:: + GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement( + Element& aBlockElement, const Element& aAncestorElement) { + MOZ_ASSERT(aBlockElement.IsInclusiveDescendantOf(&aAncestorElement)); + MOZ_ASSERT(HTMLEditUtils::IsBlockElement(aBlockElement)); + + if (&aBlockElement == &aAncestorElement) { + return nullptr; + } + + Element* lastBlockAncestor = &aBlockElement; + for (Element* element : aBlockElement.InclusiveAncestorsOfType<Element>()) { + if (element == &aAncestorElement) { + return lastBlockAncestor; + } + if (HTMLEditUtils::IsBlockElement(*lastBlockAncestor)) { + lastBlockAncestor = element; + } + } + return nullptr; +} + +// static +HTMLEditor::PreserveWhiteSpaceStyle +HTMLEditor::AutoMoveOneLineHandler::ConsiderWhetherPreserveWhiteSpaceStyle( + const nsIContent* aContentInLine, + const Element* aInclusiveAncestorBlockOfInsertionPoint) { + if (MOZ_UNLIKELY(!aInclusiveAncestorBlockOfInsertionPoint)) { + return PreserveWhiteSpaceStyle::No; + } + + // If we move content from or to <pre>, we don't need to preserve the + // white-space style for compatibility with both our traditional behavior + // and the other browsers. + + // TODO: If `white-space` is specified by non-UA stylesheet, we should + // preserve it even if the right block is <pre> for compatibility with the + // other browsers. + const auto IsInclusiveDescendantOfPre = [](const nsIContent& aContent) { + // If the content has different `white-space` style from <pre>, we + // shouldn't treat it as a descendant of <pre> because web apps or + // the user intent to treat the white-spaces in aContent not as `pre`. + if (EditorUtils::GetComputedWhiteSpaceStyle(aContent).valueOr( + StyleWhiteSpace::Normal) != StyleWhiteSpace::Pre) { + return false; + } + for (const Element* element : + aContent.InclusiveAncestorsOfType<Element>()) { + if (element->IsHTMLElement(nsGkAtoms::pre)) { + return true; + } + } + return false; + }; + if (IsInclusiveDescendantOfPre(*aInclusiveAncestorBlockOfInsertionPoint) || + MOZ_UNLIKELY(!aContentInLine) || + IsInclusiveDescendantOfPre(*aContentInLine)) { + return PreserveWhiteSpaceStyle::No; + } + return PreserveWhiteSpaceStyle::Yes; +} + +Result<MoveNodeResult, nsresult> HTMLEditor::AutoMoveOneLineHandler::Run( + HTMLEditor& aHTMLEditor, const Element& aEditingHost) { + EditorDOMPoint pointToInsert(NextInsertionPointRef()); + MOZ_ASSERT(pointToInsert.IsInContentNode()); + + EditorDOMPoint pointToPutCaret; + AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents; + { + AutoTrackDOMPoint tackPointToInsert(aHTMLEditor.RangeUpdaterRef(), + &pointToInsert); + + Result<CaretPoint, nsresult> splitAtLineEdgesResult = + SplitToMakeTheLineIsolated( + aHTMLEditor, + MOZ_KnownLive(*pointToInsert.ContainerAs<nsIContent>()), + aEditingHost, arrayOfContents); + if (MOZ_UNLIKELY(splitAtLineEdgesResult.isErr())) { + NS_WARNING("AutoMoveOneLineHandler::SplitToMakeTheLineIsolated() failed"); + return splitAtLineEdgesResult.propagateErr(); + } + splitAtLineEdgesResult.unwrap().MoveCaretPointTo( + pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); + + Result<EditorDOMPoint, nsresult> splitAtBRElementsResult = + aHTMLEditor.MaybeSplitElementsAtEveryBRElement( + arrayOfContents, EditSubAction::eMergeBlockContents); + if (MOZ_UNLIKELY(splitAtBRElementsResult.isErr())) { + NS_WARNING( + "HTMLEditor::MaybeSplitElementsAtEveryBRElement(EditSubAction::" + "eMergeBlockContents) failed"); + return splitAtBRElementsResult.propagateErr(); + } + if (splitAtBRElementsResult.inspect().IsSet()) { + pointToPutCaret = splitAtBRElementsResult.unwrap(); + } + } + + if (!pointToInsert.IsSetAndValid()) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + + if (aHTMLEditor.AllowsTransactionsToChangeSelection() && + pointToPutCaret.IsSet()) { + nsresult rv = aHTMLEditor.CollapseSelectionTo(pointToPutCaret); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return Err(rv); + } + } + + if (arrayOfContents.IsEmpty()) { + return MoveNodeResult::IgnoredResult(std::move(pointToInsert)); + } + + // Track the range which contains the moved contents. + if (ForceMoveToEndOfContainer()) { + pointToInsert = NextInsertionPointRef(); + } + EditorDOMRange movedContentRange(pointToInsert); + MoveNodeResult moveContentsInLineResult = + MoveNodeResult::IgnoredResult(pointToInsert); + for (const OwningNonNull<nsIContent>& content : arrayOfContents) { + { + AutoEditorDOMRangeChildrenInvalidator lockOffsets(movedContentRange); + // If the content is a block element, move all children of it to the + // new container, and then, remove the (probably) empty block element. + if (HTMLEditUtils::IsBlockElement(content)) { + Result<MoveNodeResult, nsresult> moveChildrenResult = + aHTMLEditor.MoveChildrenWithTransaction( + MOZ_KnownLive(*content->AsElement()), pointToInsert, + mPreserveWhiteSpaceStyle, RemoveIfCommentNode::Yes); + if (MOZ_UNLIKELY(moveChildrenResult.isErr())) { + NS_WARNING("HTMLEditor::MoveChildrenWithTransaction() failed"); + moveContentsInLineResult.IgnoreCaretPointSuggestion(); + return moveChildrenResult; + } + moveContentsInLineResult |= moveChildrenResult.inspect(); + moveContentsInLineResult.MarkAsHandled(); + // MOZ_KnownLive due to bug 1620312 + nsresult rv = + aHTMLEditor.DeleteNodeWithTransaction(MOZ_KnownLive(content)); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + moveContentsInLineResult.IgnoreCaretPointSuggestion(); + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed, but ignored"); + } + // If the moving content is a comment node or an empty inline node, we + // don't want it to appear in the dist paragraph. + else if (content->IsComment() || + HTMLEditUtils::IsEmptyInlineContainer( + content, {EmptyCheckOption::TreatSingleBRElementAsVisible, + EmptyCheckOption::TreatListItemAsVisible, + EmptyCheckOption::TreatTableCellAsVisible})) { + nsCOMPtr<nsIContent> emptyContent = + HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( + content, &aEditingHost, + pointToInsert.ContainerAs<nsIContent>()); + if (!emptyContent) { + emptyContent = content; + } + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*emptyContent); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + moveContentsInLineResult.IgnoreCaretPointSuggestion(); + return Err(rv); + } + } else { + // MOZ_KnownLive due to bug 1620312 + Result<MoveNodeResult, nsresult> moveNodeOrChildrenResult = + aHTMLEditor.MoveNodeOrChildrenWithTransaction( + MOZ_KnownLive(content), pointToInsert, mPreserveWhiteSpaceStyle, + RemoveIfCommentNode::Yes); + if (MOZ_UNLIKELY(moveNodeOrChildrenResult.isErr())) { + NS_WARNING("HTMLEditor::MoveNodeOrChildrenWithTransaction() failed"); + moveContentsInLineResult.IgnoreCaretPointSuggestion(); + return moveNodeOrChildrenResult; + } + moveContentsInLineResult |= moveNodeOrChildrenResult.inspect(); + } + } + // For backward compatibility, we should move contents to end of the + // container if the instance is created without specific insertion point. + if (ForceMoveToEndOfContainer()) { + pointToInsert = NextInsertionPointRef(); + movedContentRange.SetEnd(pointToInsert); + } + // And also if pointToInsert has been made invalid with removing preceding + // children, we should move the content to the end of the container. + else if (aHTMLEditor.MayHaveMutationEventListeners() && + MOZ_UNLIKELY(!moveContentsInLineResult.NextInsertionPointRef() + .IsSetAndValid())) { + mPointToInsert.SetToEndOf(mPointToInsert.GetContainer()); + pointToInsert = NextInsertionPointRef(); + movedContentRange.SetEnd(pointToInsert); + } else { + MOZ_DIAGNOSTIC_ASSERT( + moveContentsInLineResult.NextInsertionPointRef().IsSet()); + mPointToInsert = moveContentsInLineResult.NextInsertionPointRef(); + pointToInsert = NextInsertionPointRef(); + if (!aHTMLEditor.MayHaveMutationEventListeners() || + movedContentRange.EndRef().IsBefore(pointToInsert)) { + movedContentRange.SetEnd(pointToInsert); + } + } + } + + // Nothing has been moved, we don't need to clean up unnecessary <br> element. + // And also if we're not moving content into a block, we can quit right now. + if (moveContentsInLineResult.Ignored() || + MOZ_UNLIKELY(!mDestInclusiveAncestorBlock)) { + return moveContentsInLineResult; + } + + // If we couldn't track the range to clean up, we should just stop cleaning up + // because returning error from here may change the behavior of web apps using + // mutation event listeners. + if (MOZ_UNLIKELY(!movedContentRange.IsPositioned() || + movedContentRange.Collapsed())) { + return moveContentsInLineResult; + } + + nsresult rv = DeleteUnnecessaryTrailingLineBreakInMovedLineEnd( + aHTMLEditor, movedContentRange, aEditingHost); + if (NS_FAILED(rv)) { + NS_WARNING( + "AutoMoveOneLineHandler::" + "DeleteUnnecessaryTrailingLineBreakInMovedLineEnd() failed"); + moveContentsInLineResult.IgnoreCaretPointSuggestion(); + return Err(rv); + } + return moveContentsInLineResult; +} + +nsresult HTMLEditor::AutoMoveOneLineHandler:: + DeleteUnnecessaryTrailingLineBreakInMovedLineEnd( + HTMLEditor& aHTMLEditor, const EditorDOMRange& aMovedContentRange, + const Element& aEditingHost) const { + MOZ_ASSERT(mDestInclusiveAncestorBlock); + MOZ_ASSERT(aMovedContentRange.IsPositioned()); + MOZ_ASSERT(!aMovedContentRange.Collapsed()); + + // If we didn't preserve white-space for backward compatibility and + // white-space becomes not preformatted, we need to clean it up the last text + // node if it ends with a preformatted line break. + if (mPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No) { + const RefPtr<Text> textNodeEndingWithUnnecessaryLineBreak = [&]() -> Text* { + Text* lastTextNode = Text::FromNodeOrNull( + mMovingToParentBlock + ? HTMLEditUtils::GetPreviousContent( + *mTopmostSrcAncestorBlockInDestBlock, + {WalkTreeOption::StopAtBlockBoundary}, + mDestInclusiveAncestorBlock) + : HTMLEditUtils::GetLastLeafContent( + *mDestInclusiveAncestorBlock, + {LeafNodeType::LeafNodeOrNonEditableNode})); + if (!lastTextNode || + !HTMLEditUtils::IsSimplyEditableNode(*lastTextNode)) { + return nullptr; + } + const nsTextFragment& textFragment = lastTextNode->TextFragment(); + const char16_t lastCh = + textFragment.GetLength() + ? textFragment.CharAt(textFragment.GetLength() - 1u) + : 0; + return lastCh == HTMLEditUtils::kNewLine && + !EditorUtils::IsNewLinePreformatted(*lastTextNode) + ? lastTextNode + : nullptr; + }(); + if (textNodeEndingWithUnnecessaryLineBreak) { + if (textNodeEndingWithUnnecessaryLineBreak->TextDataLength() == 1u) { + const RefPtr<Element> inlineElement = + HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( + *textNodeEndingWithUnnecessaryLineBreak, &aEditingHost); + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction( + inlineElement ? static_cast<nsIContent&>(*inlineElement) + : static_cast<nsIContent&>( + *textNodeEndingWithUnnecessaryLineBreak)); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return Err(rv); + } + } else { + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction( + *textNodeEndingWithUnnecessaryLineBreak, + textNodeEndingWithUnnecessaryLineBreak->TextDataLength() - 1u, + 1u); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.propagateErr(); + } + nsresult rv = caretPointOrError.inspect().SuggestCaretPointTo( + aHTMLEditor, {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"); + } + } + } + + nsCOMPtr<nsIContent> lastLineBreakContent = + mMovingToParentBlock + ? HTMLEditUtils::GetUnnecessaryLineBreakContent( + *mTopmostSrcAncestorBlockInDestBlock, + ScanLineBreak::BeforeBlock) + : HTMLEditUtils::GetUnnecessaryLineBreakContent( + *mDestInclusiveAncestorBlock, ScanLineBreak::AtEndOfBlock); + if (!lastLineBreakContent) { + return NS_OK; + } + EditorRawDOMPoint atUnnecessaryLineBreak(lastLineBreakContent); + if (NS_WARN_IF(!atUnnecessaryLineBreak.IsSet())) { + return NS_ERROR_FAILURE; + } + // If the found unnecessary line break is not what we moved above, we + // shouldn't remove it. E.g., the web app may have inserted it intentionally. + MOZ_ASSERT(aMovedContentRange.StartRef().IsSetAndValid()); + MOZ_ASSERT(aMovedContentRange.EndRef().IsSetAndValid()); + if (!aMovedContentRange.Contains(atUnnecessaryLineBreak)) { + return NS_OK; + } + + AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor); + // If it's a text node and ending with a preformatted line break, we should + // delete it. + if (Text* textNode = Text::FromNode(lastLineBreakContent)) { + MOZ_ASSERT(EditorUtils::IsNewLinePreformatted(*textNode)); + if (textNode->TextDataLength() > 1) { + Result<CaretPoint, nsresult> caretPointOrError = + aHTMLEditor.DeleteTextWithTransaction( + MOZ_KnownLive(*textNode), textNode->TextDataLength() - 1u, 1u); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING("HTMLEditor::DeleteTextWithTransaction() failed"); + return caretPointOrError.unwrapErr(); + } + // IgnoreCaretPointSuggestion() because of dontChangeMySelection above. + caretPointOrError.unwrap().IgnoreCaretPointSuggestion(); + return NS_OK; + } + } else { + MOZ_ASSERT(lastLineBreakContent->IsHTMLElement(nsGkAtoms::br)); + } + // If last line break content is the only content of its inline parent, we + // should remove the parent too. + if (const RefPtr<Element> inlineElement = + HTMLEditUtils::GetMostDistantAncestorEditableEmptyInlineElement( + *lastLineBreakContent, &aEditingHost)) { + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*inlineElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + // Or if the text node has only the preformatted line break or <br> element, + // we should remove it. + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(*lastLineBreakContent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed"); + return rv; +} + +Result<bool, nsresult> HTMLEditor::CanMoveNodeOrChildren( + const nsIContent& aContent, const nsINode& aNewContainer) const { + if (HTMLEditUtils::CanNodeContain(aNewContainer, aContent)) { + return true; + } + if (aContent.IsElement()) { + return CanMoveChildren(*aContent.AsElement(), aNewContainer); + } + return true; +} + +Result<MoveNodeResult, nsresult> HTMLEditor::MoveNodeOrChildrenWithTransaction( + nsIContent& aContentToMove, const EditorDOMPoint& aPointToInsert, + PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle, + RemoveIfCommentNode aRemoveIfCommentNode) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aPointToInsert.IsInContentNode()); + + const auto destWhiteSpaceStyle = [&]() -> Maybe<StyleWhiteSpace> { + if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No || + !aPointToInsert.IsInContentNode()) { + return Nothing(); + } + auto style = EditorUtils::GetComputedWhiteSpaceStyle( + *aPointToInsert.ContainerAs<nsIContent>()); + if (NS_WARN_IF(style.isSome() && + style.value() == StyleWhiteSpace::PreSpace)) { + return Nothing(); + } + return style; + }(); + const auto srcWhiteSpaceStyle = [&]() -> Maybe<StyleWhiteSpace> { + if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No) { + return Nothing(); + } + auto style = EditorUtils::GetComputedWhiteSpaceStyle(aContentToMove); + if (NS_WARN_IF(style.isSome() && + style.value() == StyleWhiteSpace::PreSpace)) { + return Nothing(); + } + return style; + }(); + const auto GetWhiteSpaceStyleValue = [](StyleWhiteSpace aStyleWhiteSpace) { + switch (aStyleWhiteSpace) { + case StyleWhiteSpace::Normal: + return u"normal"_ns; + case StyleWhiteSpace::Pre: + return u"pre"_ns; + case StyleWhiteSpace::Nowrap: + return u"nowrap"_ns; + case StyleWhiteSpace::PreWrap: + return u"pre-wrap"_ns; + case StyleWhiteSpace::PreLine: + return u"pre-line"_ns; + case StyleWhiteSpace::BreakSpaces: + return u"break-spaces"_ns; + case StyleWhiteSpace::PreSpace: + MOZ_ASSERT_UNREACHABLE("Don't handle -moz-pre-space"); + return u""_ns; + default: + MOZ_ASSERT_UNREACHABLE("Handle the new white-space value"); + return u""_ns; + } + }; + + if (aRemoveIfCommentNode == RemoveIfCommentNode::Yes && + aContentToMove.IsComment()) { + EditorDOMPoint pointToInsert(aPointToInsert); + { + AutoTrackDOMPoint trackPointToInsert(RangeUpdaterRef(), &pointToInsert); + nsresult rv = DeleteNodeWithTransaction(aContentToMove); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return Err(rv); + } + } + if (NS_WARN_IF(!pointToInsert.IsSetAndValid())) { + return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + } + return MoveNodeResult::HandledResult(std::move(pointToInsert)); + } + + // Check if this node can go into the destination node + if (HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(), + aContentToMove)) { + EditorDOMPoint pointToInsert(aPointToInsert); + // Preserve white-space in the new position with using `style` attribute. + // This is additional path from point of view of our traditional behavior. + // Therefore, ignore errors especially if we got unexpected DOM tree. + if (destWhiteSpaceStyle.isSome() && srcWhiteSpaceStyle.isSome() && + destWhiteSpaceStyle.value() != srcWhiteSpaceStyle.value()) { + // Set `white-space` with `style` attribute if it's nsStyledElement. + if (nsStyledElement* styledElement = + nsStyledElement::FromNode(&aContentToMove)) { + DebugOnly<nsresult> rvIgnored = + CSSEditUtils::SetCSSPropertyWithTransaction( + *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::white_space, + GetWhiteSpaceStyleValue(srcWhiteSpaceStyle.value())); + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "CSSEditUtils::SetCSSPropertyWithTransaction(" + "nsGkAtoms::white_space) failed, but ignored"); + } + // Otherwise, if the dest container can have <span> element and <span> + // element can have the moving content node, we should insert it. + else if (HTMLEditUtils::CanNodeContain(*aPointToInsert.GetContainer(), + *nsGkAtoms::span) && + HTMLEditUtils::CanNodeContain(*nsGkAtoms::span, + aContentToMove)) { + RefPtr<Element> newSpanElement = CreateHTMLContent(nsGkAtoms::span); + if (NS_WARN_IF(!newSpanElement)) { + return Err(NS_ERROR_FAILURE); + } + nsAutoString styleAttrValue(u"white-space: "_ns); + styleAttrValue.Append( + GetWhiteSpaceStyleValue(srcWhiteSpaceStyle.value())); + IgnoredErrorResult error; + newSpanElement->SetAttr(nsGkAtoms::style, styleAttrValue, error); + NS_WARNING_ASSERTION(!error.Failed(), + "Element::SetAttr(nsGkAtoms::span) failed"); + if (MOZ_LIKELY(!error.Failed())) { + Result<CreateElementResult, nsresult> insertSpanElementResult = + InsertNodeWithTransaction<Element>(*newSpanElement, + aPointToInsert); + if (MOZ_UNLIKELY(insertSpanElementResult.isErr())) { + if (NS_WARN_IF(insertSpanElementResult.inspectErr() == + NS_ERROR_EDITOR_DESTROYED)) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING( + "HTMLEditor::InsertNodeWithTransaction() failed, but ignored"); + } else { + // We should move the node into the new <span> to preserve the + // style. + pointToInsert.Set(newSpanElement, 0u); + // We should put caret after aContentToMove after moving it so that + // we do not need the suggested caret point here. + insertSpanElementResult.inspect().IgnoreCaretPointSuggestion(); + } + } + } + } + // If it can, move it there. + Result<MoveNodeResult, nsresult> moveNodeResult = + MoveNodeWithTransaction(aContentToMove, pointToInsert); + NS_WARNING_ASSERTION(moveNodeResult.isOk(), + "HTMLEditor::MoveNodeWithTransaction() failed"); + // XXX This is odd to override the handled state here, but stopping this + // hits an NS_ASSERTION in WhiteSpaceVisibilityKeeper:: + // MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement. + if (moveNodeResult.isOk()) { + MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap(); + unwrappedMoveNodeResult.MarkAsHandled(); + return unwrappedMoveNodeResult; + } + return moveNodeResult; + } + + // If it can't, move its children (if any), and then delete it. + auto moveNodeResult = + [&]() MOZ_CAN_RUN_SCRIPT -> Result<MoveNodeResult, nsresult> { + if (!aContentToMove.IsElement()) { + return MoveNodeResult::HandledResult(aPointToInsert); + } + Result<MoveNodeResult, nsresult> moveChildrenResult = + MoveChildrenWithTransaction(MOZ_KnownLive(*aContentToMove.AsElement()), + aPointToInsert, aPreserveWhiteSpaceStyle, + aRemoveIfCommentNode); + NS_WARNING_ASSERTION(moveChildrenResult.isOk(), + "HTMLEditor::MoveChildrenWithTransaction() failed"); + return moveChildrenResult; + }(); + if (MOZ_UNLIKELY(moveNodeResult.isErr())) { + return moveNodeResult; // Already warned in the lambda. + } + + nsresult rv = DeleteNodeWithTransaction(aContentToMove); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + moveNodeResult.inspect().IgnoreCaretPointSuggestion(); + return Err(rv); + } + if (!MayHaveMutationEventListeners()) { + return moveNodeResult; + } + // Mutation event listener may make `offset` value invalid with + // removing some previous children while we call + // `DeleteNodeWithTransaction()` so that we should adjust it here. + if (moveNodeResult.inspect().NextInsertionPointRef().IsSetAndValid()) { + return moveNodeResult; + } + moveNodeResult.inspect().IgnoreCaretPointSuggestion(); + return MoveNodeResult::HandledResult( + EditorDOMPoint::AtEndOf(*aPointToInsert.GetContainer())); +} + +Result<bool, nsresult> HTMLEditor::CanMoveChildren( + const Element& aElement, const nsINode& aNewContainer) const { + if (NS_WARN_IF(&aElement == &aNewContainer)) { + return Err(NS_ERROR_FAILURE); + } + for (nsIContent* childContent = aElement.GetFirstChild(); childContent; + childContent = childContent->GetNextSibling()) { + Result<bool, nsresult> result = + CanMoveNodeOrChildren(*childContent, aNewContainer); + if (result.isErr() || result.inspect()) { + return result; + } + } + return false; +} + +Result<MoveNodeResult, nsresult> HTMLEditor::MoveChildrenWithTransaction( + Element& aElement, const EditorDOMPoint& aPointToInsert, + PreserveWhiteSpaceStyle aPreserveWhiteSpaceStyle, + RemoveIfCommentNode aRemoveIfCommentNode) { + MOZ_ASSERT(aPointToInsert.IsSet()); + + if (NS_WARN_IF(&aElement == aPointToInsert.GetContainer())) { + return Err(NS_ERROR_INVALID_ARG); + } + + MoveNodeResult moveChildrenResult = + MoveNodeResult::IgnoredResult(aPointToInsert); + while (aElement.GetFirstChild()) { + Result<MoveNodeResult, nsresult> moveNodeOrChildrenResult = + MoveNodeOrChildrenWithTransaction( + MOZ_KnownLive(*aElement.GetFirstChild()), + moveChildrenResult.NextInsertionPointRef(), + aPreserveWhiteSpaceStyle, aRemoveIfCommentNode); + if (MOZ_UNLIKELY(moveNodeOrChildrenResult.isErr())) { + NS_WARNING("HTMLEditor::MoveNodeOrChildrenWithTransaction() failed"); + moveChildrenResult.IgnoreCaretPointSuggestion(); + return moveNodeOrChildrenResult; + } + moveChildrenResult |= moveNodeOrChildrenResult.inspect(); + } + return moveChildrenResult; +} + +void HTMLEditor::MoveAllChildren(nsINode& aContainer, + const EditorRawDOMPoint& aPointToInsert, + ErrorResult& aError) { + MOZ_ASSERT(!aError.Failed()); + + if (!aContainer.HasChildren()) { + return; + } + nsIContent* firstChild = aContainer.GetFirstChild(); + if (NS_WARN_IF(!firstChild)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + nsIContent* lastChild = aContainer.GetLastChild(); + if (NS_WARN_IF(!lastChild)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert, aError); + NS_WARNING_ASSERTION(!aError.Failed(), + "HTMLEditor::MoveChildrenBetween() failed"); +} + +void HTMLEditor::MoveChildrenBetween(nsIContent& aFirstChild, + nsIContent& aLastChild, + const EditorRawDOMPoint& aPointToInsert, + ErrorResult& aError) { + nsCOMPtr<nsINode> oldContainer = aFirstChild.GetParentNode(); + if (NS_WARN_IF(oldContainer != aLastChild.GetParentNode()) || + NS_WARN_IF(!aPointToInsert.IsInContentNode()) || + NS_WARN_IF(!aPointToInsert.CanContainerHaveChildren())) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + // First, store all children which should be moved to the new container. + AutoTArray<nsCOMPtr<nsIContent>, 10> children; + for (nsIContent* child = &aFirstChild; child; + child = child->GetNextSibling()) { + children.AppendElement(child); + if (child == &aLastChild) { + break; + } + } + + if (NS_WARN_IF(children.LastElement() != &aLastChild)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + nsCOMPtr<nsIContent> newContainer = aPointToInsert.ContainerAs<nsIContent>(); + nsCOMPtr<nsIContent> nextNode = aPointToInsert.GetChild(); + for (size_t i = children.Length(); i > 0; --i) { + nsCOMPtr<nsIContent>& child = children[i - 1]; + if (child->GetParentNode() != oldContainer) { + // If the child has been moved to different container, we shouldn't + // touch it. + continue; + } + if (NS_WARN_IF(!HTMLEditUtils::IsRemovableNode(*child))) { + aError.Throw(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + return; + } + oldContainer->RemoveChild(*child, aError); + if (NS_WARN_IF(Destroyed())) { + aError.Throw(NS_ERROR_EDITOR_DESTROYED); + return; + } + if (aError.Failed()) { + NS_WARNING("nsINode::RemoveChild() failed"); + return; + } + if (nextNode) { + // If we're not appending the children to the new container, we should + // check if referring next node of insertion point is still in the new + // container. + EditorRawDOMPoint pointToInsert(nextNode); + if (NS_WARN_IF(!pointToInsert.IsSet()) || + NS_WARN_IF(pointToInsert.GetContainer() != newContainer)) { + // The next node of insertion point has been moved by mutation observer. + // Let's stop moving the remaining nodes. + // XXX Or should we move remaining children after the last moved child? + aError.Throw(NS_ERROR_FAILURE); + return; + } + } + if (NS_WARN_IF( + !EditorUtils::IsEditableContent(*newContainer, EditorType::HTML))) { + aError.Throw(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); + return; + } + newContainer->InsertBefore(*child, nextNode, aError); + if (NS_WARN_IF(Destroyed())) { + aError.Throw(NS_ERROR_EDITOR_DESTROYED); + return; + } + if (aError.Failed()) { + NS_WARNING("nsINode::InsertBefore() failed"); + return; + } + // If the child was inserted or appended properly, the following children + // should be inserted before it. Otherwise, keep using current position. + if (child->GetParentNode() == newContainer) { + nextNode = child; + } + } +} + +void HTMLEditor::MovePreviousSiblings(nsIContent& aChild, + const EditorRawDOMPoint& aPointToInsert, + ErrorResult& aError) { + MOZ_ASSERT(!aError.Failed()); + + if (NS_WARN_IF(!aChild.GetParentNode())) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + nsIContent* firstChild = aChild.GetParentNode()->GetFirstChild(); + if (NS_WARN_IF(!firstChild)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + nsIContent* lastChild = + &aChild == firstChild ? firstChild : aChild.GetPreviousSibling(); + if (NS_WARN_IF(!lastChild)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + MoveChildrenBetween(*firstChild, *lastChild, aPointToInsert, aError); + NS_WARNING_ASSERTION(!aError.Failed(), + "HTMLEditor::MoveChildrenBetween() failed"); +} + +void HTMLEditor::MoveInclusiveNextSiblings( + nsIContent& aChild, const EditorRawDOMPoint& aPointToInsert, + ErrorResult& aError) { + MOZ_ASSERT(!aError.Failed()); + + if (NS_WARN_IF(!aChild.GetParentNode())) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + nsIContent* lastChild = aChild.GetParentNode()->GetLastChild(); + if (NS_WARN_IF(!lastChild)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + MoveChildrenBetween(aChild, *lastChild, aPointToInsert, aError); + NS_WARNING_ASSERTION(!aError.Failed(), + "HTMLEditor::MoveChildrenBetween() failed"); +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: + DeleteContentButKeepTableStructure(HTMLEditor& aHTMLEditor, + nsIContent& aContent) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + + if (!HTMLEditUtils::IsAnyTableElementButNotTable(&aContent)) { + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + + // XXX For performance, this should just call + // DeleteContentButKeepTableStructure() while there are children in + // aContent. If we need to avoid infinite loop because mutation event + // listeners can add unexpected nodes into aContent, we should just loop + // only original count of the children. + AutoTArray<OwningNonNull<nsIContent>, 10> childList; + for (nsIContent* child = aContent.GetFirstChild(); child; + child = child->GetNextSibling()) { + childList.AppendElement(*child); + } + + for (const auto& child : childList) { + // MOZ_KnownLive because 'childList' is guaranteed to + // keep it alive. + nsresult rv = + DeleteContentButKeepTableStructure(aHTMLEditor, MOZ_KnownLive(child)); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::DeleteContentButKeepTableStructure() failed"); + return rv; + } + } + return NS_OK; +} + +nsresult HTMLEditor::DeleteMostAncestorMailCiteElementIfEmpty( + nsIContent& aContent) { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // The element must be `<blockquote type="cite">` or + // `<span _moz_quote="true">`. + RefPtr<Element> mailCiteElement = + GetMostDistantAncestorMailCiteElement(aContent); + if (!mailCiteElement) { + return NS_OK; + } + bool seenBR = false; + if (!HTMLEditUtils::IsEmptyNode(*mailCiteElement, + {EmptyCheckOption::TreatListItemAsVisible, + EmptyCheckOption::TreatTableCellAsVisible}, + &seenBR)) { + return NS_OK; + } + EditorDOMPoint atEmptyMailCiteElement(mailCiteElement); + { + AutoEditorDOMPointChildInvalidator lockOffset(atEmptyMailCiteElement); + nsresult rv = DeleteNodeWithTransaction(*mailCiteElement); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return rv; + } + } + + if (!atEmptyMailCiteElement.IsSet() || !seenBR) { + NS_WARNING_ASSERTION( + atEmptyMailCiteElement.IsSet(), + "Mutation event listener might changed the DOM tree during " + "EditorBase::DeleteNodeWithTransaction(), but ignored"); + return NS_OK; + } + + Result<CreateElementResult, nsresult> insertBRElementResult = + InsertBRElement(WithTransaction::Yes, atEmptyMailCiteElement); + if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { + NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); + return insertBRElementResult.unwrapErr(); + } + MOZ_ASSERT(insertBRElementResult.inspect().GetNewNode()); + insertBRElementResult.inspect().IgnoreCaretPointSuggestion(); + nsresult rv = CollapseSelectionTo( + EditorRawDOMPoint(insertBRElementResult.inspect().GetNewNode())); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::::CollapseSelectionTo() failed, but ignored"); + return NS_OK; +} + +Element* HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter:: + ScanEmptyBlockInclusiveAncestor(const HTMLEditor& aHTMLEditor, + nsIContent& aStartContent) { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!mEmptyInclusiveAncestorBlockElement); + + // If we are inside an empty block, delete it. + // Note: do NOT delete table elements this way. + // Note: do NOT delete non-editable block element. + Element* editableBlockElement = HTMLEditUtils::GetInclusiveAncestorElement( + aStartContent, HTMLEditUtils::ClosestEditableBlockElement); + if (!editableBlockElement) { + return nullptr; + } + // XXX Perhaps, this is slow loop. If empty blocks are nested, then, + // each block checks whether it's empty or not. However, descendant + // blocks are checked again and again by IsEmptyNode(). Perhaps, it + // should be able to take "known empty element" for avoiding same checks. + while (editableBlockElement && + HTMLEditUtils::IsRemovableFromParentNode(*editableBlockElement) && + !HTMLEditUtils::IsAnyTableElement(editableBlockElement) && + HTMLEditUtils::IsEmptyNode(*editableBlockElement)) { + // If the removable empty list item is a child of editing host list element, + // we should not delete it. + if (HTMLEditUtils::IsListItem(editableBlockElement)) { + Element* const parentElement = editableBlockElement->GetParentElement(); + if (parentElement && HTMLEditUtils::IsAnyListElement(parentElement) && + !HTMLEditUtils::IsRemovableFromParentNode(*parentElement) && + HTMLEditUtils::IsEmptyNode(*parentElement)) { + break; + } + } + mEmptyInclusiveAncestorBlockElement = editableBlockElement; + editableBlockElement = HTMLEditUtils::GetAncestorElement( + *mEmptyInclusiveAncestorBlockElement, + HTMLEditUtils::ClosestEditableBlockElement); + } + if (!mEmptyInclusiveAncestorBlockElement) { + return nullptr; + } + + // XXX Because of not checking whether found block element is editable + // in the above loop, empty ediable block element may be overwritten + // with empty non-editable clock element. Therefore, we fail to + // remove the found empty nodes. + if (NS_WARN_IF(!mEmptyInclusiveAncestorBlockElement->IsEditable()) || + NS_WARN_IF(!mEmptyInclusiveAncestorBlockElement->GetParentElement())) { + mEmptyInclusiveAncestorBlockElement = nullptr; + } + return mEmptyInclusiveAncestorBlockElement; +} + +nsresult HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter:: + ComputeTargetRanges(const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount, + const Element& aEditingHost, + AutoRangeArray& aRangesToDelete) const { + MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement); + + // We'll delete `mEmptyInclusiveAncestorBlockElement` node from the tree, but + // we should return the range from start/end of next/previous editable content + // to end/start of the element for compatiblity with the other browsers. + switch (aDirectionAndAmount) { + case nsIEditor::eNone: + break; + case nsIEditor::ePrevious: + case nsIEditor::ePreviousWord: + case nsIEditor::eToBeginningOfLine: { + EditorRawDOMPoint startPoint = + HTMLEditUtils::GetPreviousEditablePoint<EditorRawDOMPoint>( + *mEmptyInclusiveAncestorBlockElement, &aEditingHost, + // In this case, we don't join block elements so that we won't + // delete invisible trailing whitespaces in the previous element. + InvisibleWhiteSpaces::Preserve, + // In this case, we won't join table cells so that we should + // get a range which is in a table cell even if it's in a + // table. + TableBoundary::NoCrossAnyTableElement); + if (!startPoint.IsSet()) { + NS_WARNING( + "HTMLEditUtils::GetPreviousEditablePoint() didn't return a valid " + "point"); + return NS_ERROR_FAILURE; + } + nsresult rv = aRangesToDelete.SetStartAndEnd( + startPoint, + EditorRawDOMPoint::AtEndOf(mEmptyInclusiveAncestorBlockElement)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoRangeArray::SetStartAndEnd() failed"); + return rv; + } + case nsIEditor::eNext: + case nsIEditor::eNextWord: + case nsIEditor::eToEndOfLine: { + EditorRawDOMPoint endPoint = + HTMLEditUtils::GetNextEditablePoint<EditorRawDOMPoint>( + *mEmptyInclusiveAncestorBlockElement, &aEditingHost, + // In this case, we don't join block elements so that we won't + // delete invisible trailing whitespaces in the next element. + InvisibleWhiteSpaces::Preserve, + // In this case, we won't join table cells so that we should + // get a range which is in a table cell even if it's in a + // table. + TableBoundary::NoCrossAnyTableElement); + if (!endPoint.IsSet()) { + NS_WARNING( + "HTMLEditUtils::GetNextEditablePoint() didn't return a valid " + "point"); + return NS_ERROR_FAILURE; + } + nsresult rv = aRangesToDelete.SetStartAndEnd( + EditorRawDOMPoint(mEmptyInclusiveAncestorBlockElement, 0), endPoint); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "AutoRangeArray::SetStartAndEnd() failed"); + return rv; + } + default: + MOZ_ASSERT_UNREACHABLE("Handle the nsIEditor::EDirection value"); + break; + } + // No direction, let's select the element to be deleted. + nsresult rv = + aRangesToDelete.SelectNode(*mEmptyInclusiveAncestorBlockElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::SelectNode() failed"); + return rv; +} + +Result<RefPtr<Element>, nsresult> +HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter:: + MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor) { + MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement); + MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement()); + MOZ_ASSERT(HTMLEditUtils::IsListItem(mEmptyInclusiveAncestorBlockElement)); + + // If the found empty block is a list item element and its grand parent + // (i.e., parent of list element) is NOT a list element, insert <br> + // element before the list element which has the empty list item. + // This odd list structure may occur if `Document.execCommand("indent")` + // is performed for list items. + // XXX Chrome does not remove empty list elements when last content in + // last list item is deleted. We should follow it since current + // behavior is annoying when you type new list item with selecting + // all list items. + if (!HTMLEditUtils::IsFirstChild(*mEmptyInclusiveAncestorBlockElement, + {WalkTreeOption::IgnoreNonEditableNode})) { + return RefPtr<Element>(); + } + + EditorDOMPoint atParentOfEmptyListItem( + mEmptyInclusiveAncestorBlockElement->GetParentElement()); + if (NS_WARN_IF(!atParentOfEmptyListItem.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + if (HTMLEditUtils::IsAnyListElement(atParentOfEmptyListItem.GetContainer())) { + return RefPtr<Element>(); + } + Result<CreateElementResult, nsresult> insertBRElementResult = + aHTMLEditor.InsertBRElement(WithTransaction::Yes, + atParentOfEmptyListItem); + if (MOZ_UNLIKELY(insertBRElementResult.isErr())) { + NS_WARNING("HTMLEditor::InsertBRElement(WithTransaction::Yes) failed"); + return insertBRElementResult.propagateErr(); + } + CreateElementResult unwrappedInsertBRElementResult = + insertBRElementResult.unwrap(); + nsresult rv = unwrappedInsertBRElementResult.SuggestCaretPointTo( + aHTMLEditor, {SuggestCaret::OnlyIfHasSuggestion, + SuggestCaret::OnlyIfTransactionsAllowedToDoIt, + SuggestCaret::AndIgnoreTrivialError}); + if (NS_FAILED(rv)) { + NS_WARNING("CreateElementResult::SuggestCaretPointTo() failed"); + return Err(rv); + } + MOZ_ASSERT(unwrappedInsertBRElementResult.GetNewNode()); + return unwrappedInsertBRElementResult.UnwrapNewNode(); +} + +Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoEmptyBlockAncestorDeleter::GetNewCaretPosition( + const HTMLEditor& aHTMLEditor, + nsIEditor::EDirection aDirectionAndAmount) const { + MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement); + MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement()); + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + + switch (aDirectionAndAmount) { + case nsIEditor::eNext: + case nsIEditor::eNextWord: + case nsIEditor::eToEndOfLine: { + // Collapse Selection to next node of after empty block element + // if there is. Otherwise, to just after the empty block. + auto afterEmptyBlock( + EditorDOMPoint::After(mEmptyInclusiveAncestorBlockElement)); + MOZ_ASSERT(afterEmptyBlock.IsSet()); + if (nsIContent* nextContentOfEmptyBlock = HTMLEditUtils::GetNextContent( + afterEmptyBlock, {}, aHTMLEditor.ComputeEditingHost())) { + EditorDOMPoint pt = HTMLEditUtils::GetGoodCaretPointFor<EditorDOMPoint>( + *nextContentOfEmptyBlock, aDirectionAndAmount); + if (!pt.IsSet()) { + NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed"); + return Err(NS_ERROR_FAILURE); + } + return CaretPoint(std::move(pt)); + } + if (NS_WARN_IF(!afterEmptyBlock.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + return CaretPoint(std::move(afterEmptyBlock)); + } + case nsIEditor::ePrevious: + case nsIEditor::ePreviousWord: + case nsIEditor::eToBeginningOfLine: { + // Collapse Selection to previous editable node of the empty block + // if there is. Otherwise, to after the empty block. + EditorRawDOMPoint atEmptyBlock(mEmptyInclusiveAncestorBlockElement); + if (nsIContent* previousContentOfEmptyBlock = + HTMLEditUtils::GetPreviousContent( + atEmptyBlock, {WalkTreeOption::IgnoreNonEditableNode}, + aHTMLEditor.ComputeEditingHost())) { + EditorDOMPoint pt = HTMLEditUtils::GetGoodCaretPointFor<EditorDOMPoint>( + *previousContentOfEmptyBlock, aDirectionAndAmount); + if (!pt.IsSet()) { + NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed"); + return Err(NS_ERROR_FAILURE); + } + return CaretPoint(std::move(pt)); + } + auto afterEmptyBlock = + EditorDOMPoint::After(*mEmptyInclusiveAncestorBlockElement); + if (NS_WARN_IF(!afterEmptyBlock.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + return CaretPoint(std::move(afterEmptyBlock)); + } + case nsIEditor::eNone: { + // Collapse selection at the removing block when we are replacing + // selected content. + EditorDOMPoint atEmptyBlock(mEmptyInclusiveAncestorBlockElement); + if (NS_WARN_IF(!atEmptyBlock.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + return CaretPoint(std::move(atEmptyBlock)); + } + default: + MOZ_CRASH( + "AutoEmptyBlockAncestorDeleter doesn't support this action yet"); + return Err(NS_ERROR_FAILURE); + } +} + +Result<EditActionResult, nsresult> +HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::Run( + HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount) { + MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement); + MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement()); + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + + { + Result<EditActionResult, nsresult> result = + MaybeReplaceSubListWithNewListItem(aHTMLEditor); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "AutoEmptyBlockAncestorDeleter::MaybeReplaceSubListWithNewListItem() " + "failed"); + return result; + } + if (result.inspect().Handled()) { + return result; + } + } + + if (HTMLEditUtils::IsListItem(mEmptyInclusiveAncestorBlockElement)) { + Result<RefPtr<Element>, nsresult> result = + MaybeInsertBRElementBeforeEmptyListItemElement(aHTMLEditor); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "AutoEmptyBlockAncestorDeleter::" + "MaybeInsertBRElementBeforeEmptyListItemElement() failed"); + return result.propagateErr(); + } + // If a `<br>` element is inserted, caret should be moved to after it. + if (RefPtr<Element> brElement = result.unwrap()) { + nsresult rv = + aHTMLEditor.CollapseSelectionTo(EditorRawDOMPoint(brElement)); + if (NS_FAILED(rv)) { + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed"); + return Err(rv); + } + } + } else { + Result<CaretPoint, nsresult> result = + GetNewCaretPosition(aHTMLEditor, aDirectionAndAmount); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING("AutoEmptyBlockAncestorDeleter::GetNewCaretPosition() failed"); + return result.propagateErr(); + } + MOZ_ASSERT(result.inspect().HasCaretPointSuggestion()); + nsresult rv = result.inspect().SuggestCaretPointTo(aHTMLEditor, {}); + if (NS_FAILED(rv)) { + NS_WARNING("CaretPoint::SuggestCaretPointTo() failed"); + return Err(rv); + } + } + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction( + MOZ_KnownLive(*mEmptyInclusiveAncestorBlockElement)); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); +} + +Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: + AutoEmptyBlockAncestorDeleter::MaybeReplaceSubListWithNewListItem( + HTMLEditor& aHTMLEditor) { + // If we're deleting sublist element and it's the last list item of its parent + // list, we should replace it with a list element. + if (!HTMLEditUtils::IsAnyListElement(mEmptyInclusiveAncestorBlockElement)) { + return EditActionResult::IgnoredResult(); + } + RefPtr<Element> parentElement = + mEmptyInclusiveAncestorBlockElement->GetParentElement(); + if (!parentElement || !HTMLEditUtils::IsAnyListElement(parentElement) || + !HTMLEditUtils::IsEmptyNode(*parentElement)) { + return EditActionResult::IgnoredResult(); + } + + nsCOMPtr<nsINode> nextSibling = + mEmptyInclusiveAncestorBlockElement->GetNextSibling(); + nsresult rv = aHTMLEditor.DeleteNodeWithTransaction( + MOZ_KnownLive(*mEmptyInclusiveAncestorBlockElement)); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed"); + return Err(rv); + } + Result<CreateElementResult, nsresult> insertListItemResult = + aHTMLEditor.CreateAndInsertElement( + WithTransaction::Yes, + parentElement->IsHTMLElement(nsGkAtoms::dl) ? *nsGkAtoms::dd + : *nsGkAtoms::li, + !nextSibling || nextSibling->GetParentNode() != parentElement + ? EditorDOMPoint::AtEndOf(*parentElement) + : EditorDOMPoint(nextSibling), + [](HTMLEditor& aHTMLEditor, Element& aNewElement, + const EditorDOMPoint& aPointToInsert) -> nsresult { + RefPtr<Element> brElement = + aHTMLEditor.CreateHTMLContent(nsGkAtoms::br); + if (MOZ_UNLIKELY(!brElement)) { + NS_WARNING( + "EditorBase::CreateHTMLContent(nsGkAtoms::br) failed, but " + "ignored"); + return NS_OK; // Just gives up to insert <br> + } + IgnoredErrorResult error; + aNewElement.AppendChild(*brElement, error); + NS_WARNING_ASSERTION(!error.Failed(), + "nsINode::AppendChild() failed, but ignored"); + return NS_OK; + }); + if (MOZ_UNLIKELY(insertListItemResult.isErr())) { + NS_WARNING("HTMLEditor::CreateAndInsertElement() failed"); + return insertListItemResult.propagateErr(); + } + CreateElementResult unwrappedInsertListItemResult = + insertListItemResult.unwrap(); + unwrappedInsertListItemResult.IgnoreCaretPointSuggestion(); + rv = aHTMLEditor.CollapseSelectionTo( + EditorRawDOMPoint(unwrappedInsertListItemResult.GetNewNode(), 0u)); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); +} + +template <typename EditorDOMRangeType> +Result<EditorRawDOMRange, nsresult> +HTMLEditor::AutoDeleteRangesHandler::ExtendOrShrinkRangeToDelete( + const HTMLEditor& aHTMLEditor, const nsFrameSelection* aFrameSelection, + const EditorDOMRangeType& aRangeToDelete) const { + MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); + MOZ_ASSERT(!aRangeToDelete.Collapsed()); + MOZ_ASSERT(aRangeToDelete.IsPositioned()); + + const nsIContent* commonAncestor = nsIContent::FromNodeOrNull( + nsContentUtils::GetClosestCommonInclusiveAncestor( + aRangeToDelete.StartRef().GetContainer(), + aRangeToDelete.EndRef().GetContainer())); + if (MOZ_UNLIKELY(NS_WARN_IF(!commonAncestor))) { + return Err(NS_ERROR_FAILURE); + } + + // Look for the common ancestor's block element. It's fine that we get + // non-editable block element which is ancestor of inline editing host + // because the following code checks editing host too. + const Element* const maybeNonEditableBlockElement = + HTMLEditUtils::GetInclusiveAncestorElement( + *commonAncestor, HTMLEditUtils::ClosestBlockElement); + if (NS_WARN_IF(!maybeNonEditableBlockElement)) { + return Err(NS_ERROR_FAILURE); + } + + // Set up for loops and cache our root element + RefPtr<Element> editingHost = aHTMLEditor.ComputeEditingHost(); + if (NS_WARN_IF(!editingHost)) { + return Err(NS_ERROR_FAILURE); + } + + // If only one list element is selected, and if the list element is empty, + // we should delete only the list element. Or if the list element is not + // empty, we should make the list has only one empty list item element. + if (const Element* maybeListElement = + HTMLEditUtils::GetElementIfOnlyOneSelected(aRangeToDelete)) { + if (HTMLEditUtils::IsAnyListElement(maybeListElement) && + !HTMLEditUtils::IsEmptyAnyListElement(*maybeListElement)) { + EditorRawDOMRange range = + HTMLEditUtils::GetRangeSelectingAllContentInAllListItems< + EditorRawDOMRange>(*maybeListElement); + if (range.IsPositioned()) { + if (EditorUtils::IsEditableContent( + *range.StartRef().ContainerAs<nsIContent>(), + EditorType::HTML) && + EditorUtils::IsEditableContent( + *range.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)) { + return range; + } + } + // If the first and/or last list item is not editable, we need to do more + // complicated things probably, but we just delete the list element with + // invisible things around it for now since it must be rare case. + } + // Otherwise, if the list item is empty, we should delete it with invisible + // things around it. + } + + // Find previous visible things before start of selection + EditorRawDOMRange rangeToDelete(aRangeToDelete); + if (rangeToDelete.StartRef().GetContainer() != maybeNonEditableBlockElement && + rangeToDelete.StartRef().GetContainer() != editingHost) { + for (;;) { + WSScanResult backwardScanFromStartResult = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + editingHost, rangeToDelete.StartRef()); + if (!backwardScanFromStartResult.ReachedCurrentBlockBoundary()) { + break; + } + MOZ_ASSERT(backwardScanFromStartResult.GetContent() == + WSRunScanner(editingHost, rangeToDelete.StartRef()) + .GetStartReasonContent()); + // We want to keep looking up. But stop if we are crossing table + // element boundaries, or if we hit the root. + if (HTMLEditUtils::IsAnyTableElement( + backwardScanFromStartResult.GetContent()) || + backwardScanFromStartResult.GetContent() == + maybeNonEditableBlockElement || + backwardScanFromStartResult.GetContent() == editingHost) { + break; + } + // Don't cross list element boundary because we don't want to delete list + // element at start position unless it's empty. + if (HTMLEditUtils::IsAnyListElement( + backwardScanFromStartResult.GetContent()) && + !HTMLEditUtils::IsEmptyAnyListElement( + *backwardScanFromStartResult.ElementPtr())) { + break; + } + rangeToDelete.SetStart( + backwardScanFromStartResult.PointAtContent<EditorRawDOMPoint>()); + } + if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint( + rangeToDelete.StartRef().GetContainer())) { + NS_WARNING("Computed start container was out of selection limiter"); + return Err(NS_ERROR_FAILURE); + } + } + + // Expand selection endpoint only if we don't pass an invisible `<br>`, or if + // we really needed to pass that `<br>` (i.e., its block is now totally + // selected). + + // Find next visible things after end of selection + EditorDOMPoint atFirstInvisibleBRElement; + if (rangeToDelete.EndRef().GetContainer() != maybeNonEditableBlockElement && + rangeToDelete.EndRef().GetContainer() != editingHost) { + for (;;) { + WSRunScanner wsScannerAtEnd(editingHost, rangeToDelete.EndRef()); + WSScanResult forwardScanFromEndResult = + wsScannerAtEnd.ScanNextVisibleNodeOrBlockBoundaryFrom( + rangeToDelete.EndRef()); + if (forwardScanFromEndResult.ReachedBRElement()) { + // XXX In my understanding, this is odd. The end reason may not be + // same as the reached <br> element because the equality is + // guaranteed only when ReachedCurrentBlockBoundary() returns true. + // However, looks like that this code assumes that + // GetEndReasonContent() returns the (or a) <br> element. + NS_ASSERTION(wsScannerAtEnd.GetEndReasonContent() == + forwardScanFromEndResult.BRElementPtr(), + "End reason is not the reached <br> element"); + if (HTMLEditUtils::IsVisibleBRElement( + *wsScannerAtEnd.GetEndReasonContent())) { + break; + } + if (!atFirstInvisibleBRElement.IsSet()) { + atFirstInvisibleBRElement = + rangeToDelete.EndRef().To<EditorDOMPoint>(); + } + rangeToDelete.SetEnd( + EditorRawDOMPoint::After(*wsScannerAtEnd.GetEndReasonContent())); + continue; + } + + if (forwardScanFromEndResult.ReachedCurrentBlockBoundary()) { + MOZ_ASSERT(forwardScanFromEndResult.GetContent() == + wsScannerAtEnd.GetEndReasonContent()); + // We want to keep looking up. But stop if we are crossing table + // element boundaries, or if we hit the root. + if (HTMLEditUtils::IsAnyTableElement( + forwardScanFromEndResult.GetContent()) || + forwardScanFromEndResult.GetContent() == + maybeNonEditableBlockElement || + forwardScanFromEndResult.GetContent() == editingHost) { + break; + } + rangeToDelete.SetEnd( + forwardScanFromEndResult.PointAfterContent<EditorRawDOMPoint>()); + continue; + } + + break; + } + + if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint( + rangeToDelete.EndRef().GetContainer())) { + NS_WARNING("Computed end container was out of selection limiter"); + return Err(NS_ERROR_FAILURE); + } + } + + // If range boundaries are in list element, and the positions are very + // start/end of first/last list item, we may need to shrink the ranges for + // preventing to remove only all list item elements. + { + EditorRawDOMRange rangeToDeleteListOrLeaveOneEmptyListItem = + AutoDeleteRangesHandler:: + GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements( + rangeToDelete); + if (rangeToDeleteListOrLeaveOneEmptyListItem.IsPositioned()) { + rangeToDelete = std::move(rangeToDeleteListOrLeaveOneEmptyListItem); + } + } + + if (atFirstInvisibleBRElement.IsInContentNode()) { + // Find block node containing invisible `<br>` element. + if (const RefPtr<const Element> editableBlockContainingBRElement = + HTMLEditUtils::GetInclusiveAncestorElement( + *atFirstInvisibleBRElement.ContainerAs<nsIContent>(), + HTMLEditUtils::ClosestEditableBlockElement)) { + if (rangeToDelete.Contains( + EditorRawDOMPoint(editableBlockContainingBRElement))) { + return rangeToDelete; + } + // Otherwise, the new range should end at the invisible `<br>`. + if (aFrameSelection && !aFrameSelection->IsValidSelectionPoint( + atFirstInvisibleBRElement.GetContainer())) { + NS_WARNING( + "Computed end container (`<br>` element) was out of selection " + "limiter"); + return Err(NS_ERROR_FAILURE); + } + rangeToDelete.SetEnd(atFirstInvisibleBRElement); + } + } + + return rangeToDelete; +} + +// static +EditorRawDOMRange HTMLEditor::AutoDeleteRangesHandler:: + GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements( + const EditorRawDOMRange& aRangeToDelete) { + MOZ_ASSERT(aRangeToDelete.IsPositionedAndValid()); + + auto GetDeepestEditableStartPointOfList = [](Element& aListElement) { + Element* const firstListItemElement = + HTMLEditUtils::GetFirstListItemElement(aListElement); + if (MOZ_UNLIKELY(!firstListItemElement)) { + return EditorRawDOMPoint(); + } + if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(*firstListItemElement, + EditorType::HTML))) { + return EditorRawDOMPoint(firstListItemElement); + } + return HTMLEditUtils::GetDeepestEditableStartPointOf<EditorRawDOMPoint>( + *firstListItemElement); + }; + + auto GetDeepestEditableEndPointOfList = [](Element& aListElement) { + Element* const lastListItemElement = + HTMLEditUtils::GetLastListItemElement(aListElement); + if (MOZ_UNLIKELY(!lastListItemElement)) { + return EditorRawDOMPoint(); + } + if (MOZ_UNLIKELY(!EditorUtils::IsEditableContent(*lastListItemElement, + EditorType::HTML))) { + return EditorRawDOMPoint::After(*lastListItemElement); + } + return HTMLEditUtils::GetDeepestEditableEndPointOf<EditorRawDOMPoint>( + *lastListItemElement); + }; + + Element* const startListElement = + aRangeToDelete.StartRef().IsInContentNode() + ? HTMLEditUtils::GetClosestInclusiveAncestorAnyListElement( + *aRangeToDelete.StartRef().ContainerAs<nsIContent>()) + : nullptr; + Element* const endListElement = + aRangeToDelete.EndRef().IsInContentNode() + ? HTMLEditUtils::GetClosestInclusiveAncestorAnyListElement( + *aRangeToDelete.EndRef().ContainerAs<nsIContent>()) + : nullptr; + if (!startListElement && !endListElement) { + return EditorRawDOMRange(); + } + + // FIXME: If there are invalid children, we cannot handle first/last list item + // elements properly. In that case, we should treat list elements and list + // item elements as normal block elements. + if (startListElement && + NS_WARN_IF(!HTMLEditUtils::IsValidListElement( + *startListElement, HTMLEditUtils::TreatSubListElementAs::Valid))) { + return EditorRawDOMRange(); + } + if (endListElement && startListElement != endListElement && + NS_WARN_IF(!HTMLEditUtils::IsValidListElement( + *endListElement, HTMLEditUtils::TreatSubListElementAs::Valid))) { + return EditorRawDOMRange(); + } + + const bool startListElementIsEmpty = + startListElement && + HTMLEditUtils::IsEmptyAnyListElement(*startListElement); + const bool endListElementIsEmpty = + startListElement == endListElement + ? startListElementIsEmpty + : endListElement && + HTMLEditUtils::IsEmptyAnyListElement(*endListElement); + // If both list elements are empty, we should not shrink the range since + // we want to delete the list. + if (startListElementIsEmpty && endListElementIsEmpty) { + return EditorRawDOMRange(); + } + + // There may be invisible white-spaces and there are elements in the + // list items. Therefore, we need to compare the deepest positions + // and range boundaries. + EditorRawDOMPoint deepestStartPointOfStartList = + startListElement ? GetDeepestEditableStartPointOfList(*startListElement) + : EditorRawDOMPoint(); + EditorRawDOMPoint deepestEndPointOfEndList = + endListElement ? GetDeepestEditableEndPointOfList(*endListElement) + : EditorRawDOMPoint(); + if (MOZ_UNLIKELY(!deepestStartPointOfStartList.IsSet() && + !deepestEndPointOfEndList.IsSet())) { + // FIXME: This does not work well if there is non-list-item contents in the + // list elements. Perhaps, for fixing this invalid cases, we need to wrap + // the content into new list item like Chrome. + return EditorRawDOMRange(); + } + + // We don't want to shrink the range into empty sublist. + if (deepestStartPointOfStartList.IsSet()) { + for (nsIContent* const maybeList : + deepestStartPointOfStartList.GetContainer() + ->InclusiveAncestorsOfType<nsIContent>()) { + if (aRangeToDelete.StartRef().GetContainer() == maybeList) { + break; + } + if (HTMLEditUtils::IsAnyListElement(maybeList) && + HTMLEditUtils::IsEmptyAnyListElement(*maybeList->AsElement())) { + deepestStartPointOfStartList.Set(maybeList); + } + } + } + if (deepestEndPointOfEndList.IsSet()) { + for (nsIContent* const maybeList : + deepestEndPointOfEndList.GetContainer() + ->InclusiveAncestorsOfType<nsIContent>()) { + if (aRangeToDelete.EndRef().GetContainer() == maybeList) { + break; + } + if (HTMLEditUtils::IsAnyListElement(maybeList) && + HTMLEditUtils::IsEmptyAnyListElement(*maybeList->AsElement())) { + deepestEndPointOfEndList.SetAfter(maybeList); + } + } + } + + const EditorRawDOMPoint deepestEndPointOfStartList = + startListElement ? GetDeepestEditableEndPointOfList(*startListElement) + : EditorRawDOMPoint(); + MOZ_ASSERT_IF(deepestStartPointOfStartList.IsSet(), + deepestEndPointOfStartList.IsSet()); + MOZ_ASSERT_IF(!deepestStartPointOfStartList.IsSet(), + !deepestEndPointOfStartList.IsSet()); + + const bool rangeStartsFromBeginningOfStartList = + deepestStartPointOfStartList.IsSet() && + aRangeToDelete.StartRef().EqualsOrIsBefore(deepestStartPointOfStartList); + const bool rangeEndsByEndingOfStartListOrLater = + !deepestEndPointOfStartList.IsSet() || + deepestEndPointOfStartList.EqualsOrIsBefore(aRangeToDelete.EndRef()); + const bool rangeEndsByEndingOfEndList = + deepestEndPointOfEndList.IsSet() && + deepestEndPointOfEndList.EqualsOrIsBefore(aRangeToDelete.EndRef()); + + EditorRawDOMRange newRangeToDelete; + // If all over the list element at start boundary is selected, we should + // shrink the range to start from the first list item to avoid to delete + // all list items. + if (!startListElementIsEmpty && rangeStartsFromBeginningOfStartList && + rangeEndsByEndingOfStartListOrLater) { + newRangeToDelete.SetStart(EditorRawDOMPoint( + deepestStartPointOfStartList.ContainerAs<nsIContent>(), 0u)); + } + // If all over the list element at end boundary is selected, and... + if (!endListElementIsEmpty && rangeEndsByEndingOfEndList) { + // If the range starts before the range at end boundary of the range, + // we want to delete the list completely, thus, we should extend the + // range to contain the list element. + if (aRangeToDelete.StartRef().IsBefore( + EditorRawDOMPoint(endListElement, 0u))) { + newRangeToDelete.SetEnd(EditorRawDOMPoint::After(*endListElement)); + MOZ_ASSERT_IF(newRangeToDelete.StartRef().IsSet(), + newRangeToDelete.IsPositionedAndValid()); + } + // Otherwise, if the range starts in the end list element, we shouldn't + // delete the list. Therefore, we should shrink the range to end by end + // of the last list item element to avoid to delete all list items. + else { + newRangeToDelete.SetEnd(EditorRawDOMPoint::AtEndOf( + *deepestEndPointOfEndList.ContainerAs<nsIContent>())); + MOZ_ASSERT_IF(newRangeToDelete.StartRef().IsSet(), + newRangeToDelete.IsPositionedAndValid()); + } + } + + if (!newRangeToDelete.StartRef().IsSet() && + !newRangeToDelete.EndRef().IsSet()) { + return EditorRawDOMRange(); + } + + if (!newRangeToDelete.StartRef().IsSet()) { + newRangeToDelete.SetStart(aRangeToDelete.StartRef()); + MOZ_ASSERT(newRangeToDelete.IsPositionedAndValid()); + } + if (!newRangeToDelete.EndRef().IsSet()) { + newRangeToDelete.SetEnd(aRangeToDelete.EndRef()); + MOZ_ASSERT(newRangeToDelete.IsPositionedAndValid()); + } + + return newRangeToDelete; +} + +} // namespace mozilla |