tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 12eb928cb50025afa509f0c2a9ff85e6adc37c36
parent 58b4243d101d51384ac4713b5c9916328af95733
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date:   Wed,  7 Jan 2026 01:10:21 +0000

Bug 1998077 - part 6: Reimplement `HTMLEditUtils::Get(First|Last)Child()` and `HTMLEditUtils::Get(Next|Previous)Sibling()` with `LeafNodeOptions` r=m_kato

`WalkTreeOptions` is similar to `LeafNodeOptions` and it's hard to
maintain both of them.  Additionally, `LeafNodeOptions` can specify
which type of leaf nodes should be ignored at scanning.  Therefore,
`LeafNodeOptions` can be used for them.

Differential Revision: https://phabricator.services.mozilla.com/D277523

Diffstat:
Meditor/libeditor/HTMLEditSubActionHandler.cpp | 97++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Meditor/libeditor/HTMLEditUtils.cpp | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Meditor/libeditor/HTMLEditUtils.h | 259++++++++++++++++++++++++++++++++++++-------------------------------------------
Meditor/libeditor/HTMLEditor.cpp | 45+++++++++++++++++++++++++--------------------
Meditor/libeditor/HTMLEditorDeleteHandler.cpp | 6++++--
Meditor/libeditor/HTMLEditorInsertParagraphHandler.cpp | 11+++++++----
Meditor/libeditor/HTMLStyleEditor.cpp | 30++++++++++++++++++++----------
Meditor/libeditor/WSRunScannerNestedClasses.cpp | 45++++++++++++++++++++++-----------------------
Meditor/libeditor/WhiteSpaceVisibilityKeeper.cpp | 10++++++----
Meditor/libeditor/gtest/TestHTMLEditUtils.cpp | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 616 insertions(+), 245 deletions(-)

diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp @@ -4430,8 +4430,10 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::IndentListChildWithTransaction( // same as the parent list element's tag, we can move it to start of the // sub-list. if (nsIContent* const nextEditableSibling = HTMLEditUtils::GetNextSibling( - aContentMovingToSubList, {WalkTreeOption::IgnoreWhiteSpaceOnlyText, - WalkTreeOption::IgnoreNonEditableNode})) { + aContentMovingToSubList, + {LeafNodeOption::IgnoreInvisibleText, + LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (HTMLEditUtils::IsListElement(*nextEditableSibling) && aPointInListElement.GetContainer()->NodeInfo()->NameAtom() == nextEditableSibling->NodeInfo()->NameAtom() && @@ -4453,8 +4455,9 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::IndentListChildWithTransaction( if (const nsCOMPtr<nsIContent> previousEditableSibling = HTMLEditUtils::GetPreviousSibling( aContentMovingToSubList, - {WalkTreeOption::IgnoreWhiteSpaceOnlyText, - WalkTreeOption::IgnoreNonEditableNode})) { + {LeafNodeOption::IgnoreInvisibleText, + LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (HTMLEditUtils::IsListElement(*previousEditableSibling) && aPointInListElement.GetContainer()->NodeInfo()->NameAtom() == previousEditableSibling->NodeInfo()->NameAtom() && @@ -4477,8 +4480,9 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::IndentListChildWithTransaction( nsIContent* previousEditableSibling = *aSubListElement ? HTMLEditUtils::GetPreviousSibling( aContentMovingToSubList, - {WalkTreeOption::IgnoreWhiteSpaceOnlyText, - WalkTreeOption::IgnoreNonEditableNode}) + {LeafNodeOption::IgnoreInvisibleText, + LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle) : nullptr; if (!*aSubListElement || (previousEditableSibling && previousEditableSibling != *aSubListElement)) { @@ -5184,10 +5188,11 @@ nsresult HTMLEditor::HandleHTMLIndentAroundRanges( } // check to see if subListElement is still appropriate. Which it is if // content is still right after it in the same list. - nsIContent* previousEditableSibling = + nsIContent* const previousEditableSibling = subListElement ? HTMLEditUtils::GetPreviousSibling( - *listItem, {WalkTreeOption::IgnoreNonEditableNode}) + *listItem, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle) : nullptr; if (!subListElement || (previousEditableSibling && previousEditableSibling != subListElement)) { @@ -7101,8 +7106,9 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::AlignBlockContentsWithDivElement( // XXX I don't understand why we should NOT align non-editable children // with modifying EDITABLE `<div>` element. const nsCOMPtr<nsIContent> firstEditableContent = - HTMLEditUtils::GetFirstChild(aBlockElement, - {WalkTreeOption::IgnoreNonEditableNode}); + HTMLEditUtils::GetFirstChild( + aBlockElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (!firstEditableContent) { // This block has no editable content, nothing to align. return EditorDOMPoint(); @@ -7111,7 +7117,8 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::AlignBlockContentsWithDivElement( // If there is only one editable content and it's a `<div>` element, // just set `align` attribute of it. const nsCOMPtr<nsIContent> lastEditableContent = HTMLEditUtils::GetLastChild( - aBlockElement, {WalkTreeOption::IgnoreNonEditableNode}); + aBlockElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (firstEditableContent == lastEditableContent && firstEditableContent->IsHTMLElement(nsGkAtoms::div)) { // XXX Chrome uses `style="text-align: foo"` instead of the legacy `align` @@ -8331,9 +8338,11 @@ HTMLEditor::InsertElementWithSplittingAncestorsWithTransaction( // Making use of html structure... if next node after where we are // putting our div is not a block, then the br we found is in same // block we are, so it's safe to consume it. - if (nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling( - *splitPoint.GetChild(), - {WalkTreeOption::IgnoreNonEditableNode})) { + if (nsIContent* const nextEditableSibling = + HTMLEditUtils::GetNextSibling( + *splitPoint.GetChild(), + {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (!HTMLEditUtils::IsBlockElement( *nextEditableSibling, BlockInlineCheck::UseComputedDisplayOutsideStyle)) { @@ -8420,16 +8429,18 @@ nsresult HTMLEditor::JoinNearestEditableNodesWithTransaction( } // Remember the last left child, and first right child - nsCOMPtr<nsIContent> lastEditableChildOfLeftContent = - HTMLEditUtils::GetLastChild(aNodeLeft, - {WalkTreeOption::IgnoreNonEditableNode}); + const nsCOMPtr<nsIContent> lastEditableChildOfLeftContent = + HTMLEditUtils::GetLastChild( + aNodeLeft, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (MOZ_UNLIKELY(NS_WARN_IF(!lastEditableChildOfLeftContent))) { return NS_ERROR_FAILURE; } - nsCOMPtr<nsIContent> firstEditableChildOfRightContent = - HTMLEditUtils::GetFirstChild(aNodeRight, - {WalkTreeOption::IgnoreNonEditableNode}); + const nsCOMPtr<nsIContent> firstEditableChildOfRightContent = + HTMLEditUtils::GetFirstChild( + aNodeRight, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (NS_WARN_IF(!firstEditableChildOfRightContent)) { return NS_ERROR_FAILURE; } @@ -8829,9 +8840,10 @@ void HTMLEditor::SetSelectionInterlinePosition() { // XXX Although I don't understand "interline position", if caret is // immediately after non-editable contents, but previous editable // content is a block, does this do right thing? - if (nsIContent* previousEditableContentInBlockAtCaret = + if (nsIContent* const previousEditableContentInBlockAtCaret = HTMLEditUtils::GetPreviousSibling( - *atCaret.GetChild(), {WalkTreeOption::IgnoreNonEditableNode})) { + *atCaret.GetChild(), {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (HTMLEditUtils::IsBlockElement( *previousEditableContentInBlockAtCaret, BlockInlineCheck::UseComputedDisplayStyle)) { @@ -8848,9 +8860,10 @@ void HTMLEditor::SetSelectionInterlinePosition() { // XXX Although I don't understand "interline position", if caret is // immediately before non-editable contents, but next editable // content is a block, does this do right thing? - if (nsIContent* nextEditableContentInBlockAtCaret = + if (nsIContent* const nextEditableContentInBlockAtCaret = HTMLEditUtils::GetNextSibling( - *atCaret.GetChild(), {WalkTreeOption::IgnoreNonEditableNode})) { + *atCaret.GetChild(), {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (HTMLEditUtils::IsBlockElement( *nextEditableContentInBlockAtCaret, BlockInlineCheck::UseComputedDisplayStyle)) { @@ -9341,9 +9354,11 @@ nsresult HTMLEditor::LiftUpListItemElement( // if it's first or last list item, don't need to split the list // otherwise we do. const bool isFirstListItem = HTMLEditUtils::IsFirstChild( - aListItemElement, {WalkTreeOption::IgnoreNonEditableNode}); + aListItemElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); const bool isLastListItem = HTMLEditUtils::IsLastChild( - aListItemElement, {WalkTreeOption::IgnoreNonEditableNode}); + aListItemElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); Element* leftListElement = aListItemElement.GetParentElement(); if (NS_WARN_IF(!leftListElement)) { @@ -9925,8 +9940,9 @@ HTMLEditor::EnsureHardLineBeginsWithFirstChildOf( Element& aRemovingContainerElement) { MOZ_ASSERT(IsEditActionDataAvailable()); - nsIContent* firstEditableChild = HTMLEditUtils::GetFirstChild( - aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode}); + nsIContent* const firstEditableChild = HTMLEditUtils::GetFirstChild( + aRemovingContainerElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (!firstEditableChild) { return CreateElementResult::NotHandled(); } @@ -9937,8 +9953,9 @@ HTMLEditor::EnsureHardLineBeginsWithFirstChildOf( return CreateElementResult::NotHandled(); } - nsIContent* previousEditableContent = HTMLEditUtils::GetPreviousSibling( - aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode}); + nsIContent* const previousEditableContent = HTMLEditUtils::GetPreviousSibling( + aRemovingContainerElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (!previousEditableContent) { return CreateElementResult::NotHandled(); } @@ -9970,8 +9987,9 @@ HTMLEditor::EnsureHardLineEndsWithLastChildOf( Element& aRemovingContainerElement) { MOZ_ASSERT(IsEditActionDataAvailable()); - nsIContent* firstEditableContent = HTMLEditUtils::GetLastChild( - aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode}); + nsIContent* const firstEditableContent = HTMLEditUtils::GetLastChild( + aRemovingContainerElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (!firstEditableContent) { return CreateElementResult::NotHandled(); } @@ -9982,8 +10000,9 @@ HTMLEditor::EnsureHardLineEndsWithLastChildOf( return CreateElementResult::NotHandled(); } - nsIContent* nextEditableContent = HTMLEditUtils::GetPreviousSibling( - aRemovingContainerElement, {WalkTreeOption::IgnoreNonEditableNode}); + nsIContent* const nextEditableContent = HTMLEditUtils::GetPreviousSibling( + aRemovingContainerElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (!nextEditableContent) { return CreateElementResult::NotHandled(); } @@ -10481,10 +10500,11 @@ nsresult HTMLEditor::MoveSelectedContentsToDivElementToMakeItAbsolutePosition( // list element in the target `<div>` element for the destination. // Therefore, duplicate same list element into the target `<div>` // element. - nsIContent* previousEditableContent = + nsIContent* const previousEditableContent = createdListElement ? HTMLEditUtils::GetPreviousSibling( - content, {WalkTreeOption::IgnoreNonEditableNode}) + content, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle) : nullptr; if (!createdListElement || (previousEditableContent && @@ -10588,10 +10608,11 @@ nsresult HTMLEditor::MoveSelectedContentsToDivElementToMakeItAbsolutePosition( } // If we cannot move the list item element into created list element, // we need another list element in the target `<div>` element. - nsIContent* previousEditableContent = + nsIContent* const previousEditableContent = createdListElement ? HTMLEditUtils::GetPreviousSibling( - *listItemElement, {WalkTreeOption::IgnoreNonEditableNode}) + *listItemElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle) : nullptr; if (!createdListElement || (previousEditableContent && diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp @@ -1634,6 +1634,77 @@ nsIContent* HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElementImpl( return nullptr; } +nsIContent* HTMLEditUtils::GetSibling(const nsIContent& aContent, + WalkTreeDirection aDirection, + const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck) { + MOZ_ASSERT(aBlockInlineCheck != BlockInlineCheck::Unused); + aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); + for (nsIContent* sibling = aDirection == WalkTreeDirection::Backward + ? aContent.GetPreviousSibling() + : aContent.GetNextSibling(); + sibling; sibling = aDirection == WalkTreeDirection::Backward + ? sibling->GetPreviousSibling() + : sibling->GetNextSibling()) { + const LeafNodeType leafNodeType = HTMLEditUtils::GetLeafNodeType( + *sibling, aOptions, aBlockInlineCheck, IgnoreChildren::No); + if (leafNodeType == LeafNodeType::Ignore) { + continue; + } + if (HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) { + return sibling; + } + if (leafNodeType == LeafNodeType::NonEmptyContainer) { + if (HTMLEditUtils::GetFirstLeafContent(*sibling, aOptions, + aBlockInlineCheck)) { + return sibling; // Has meaningful child so that it's meaningful. + } + if (HTMLEditUtils::GetLeafNodeType(*sibling, aOptions, aBlockInlineCheck, + IgnoreChildren::Yes) == + LeafNodeType::Ignore) { + continue; // The sibling itself can be ignored. + } + } + return sibling; + } + return nullptr; +} + +nsIContent* HTMLEditUtils::GetFirstOrLastChild( + const nsINode& aNode, FirstOrLastChild aFirstOrLastChild, + const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck) { + MOZ_ASSERT(aBlockInlineCheck != BlockInlineCheck::Unused); + aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); + for (nsIContent* child = aFirstOrLastChild == FirstOrLastChild::First + ? aNode.GetFirstChild() + : aNode.GetLastChild(); + child; child = aFirstOrLastChild == FirstOrLastChild::First + ? child->GetNextSibling() + : child->GetPreviousSibling()) { + const LeafNodeType leafNodeType = HTMLEditUtils::GetLeafNodeType( + *child, aOptions, aBlockInlineCheck, IgnoreChildren::No); + if (leafNodeType == LeafNodeType::Ignore) { + continue; + } + if (HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) { + return child; + } + if (leafNodeType == LeafNodeType::NonEmptyContainer) { + if (HTMLEditUtils::GetFirstLeafContent(*child, aOptions, + aBlockInlineCheck)) { + return child; // Has meaningful child so that it's meaningful. + } + if (HTMLEditUtils::GetLeafNodeType(*child, aOptions, aBlockInlineCheck, + IgnoreChildren::Yes) == + LeafNodeType::Ignore) { + continue; // The child itself can be ignored. + } + } + return child; + } + return nullptr; +} + template <typename EditorLineBreakType> Maybe<EditorLineBreakType> HTMLEditUtils::GetUnnecessaryLineBreak( const Element& aBlockElement, ScanLineBreak aScanLineBreak) { @@ -3301,7 +3372,8 @@ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside( if (nodeBefore) { // selection is after block. put at end of block. const nsIContent* lastEditableContent = HTMLEditUtils::GetLastChild( - aElement, {WalkTreeOption::IgnoreNonEditableNode}); + aElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (!lastEditableContent) { lastEditableContent = &aElement; } @@ -3315,7 +3387,8 @@ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside( // selection is before block. put at start of block. const nsIContent* firstEditableContent = HTMLEditUtils::GetFirstChild( - aElement, {WalkTreeOption::IgnoreNonEditableNode}); + aElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (!firstEditableContent) { firstEditableContent = &aElement; } @@ -3394,7 +3467,8 @@ size_t HTMLEditUtils::CollectChildren( size_t numberOfFoundChildren = 0; for (nsIContent* content = - GetFirstChild(aNode, {WalkTreeOption::IgnoreNonEditableNode}); + GetFirstChild(aNode, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); content; content = content->GetNextSibling()) { if ((aOptions.contains(CollectChildrenOption::CollectListChildren) && (HTMLEditUtils::IsListElement(*content) || diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h @@ -1129,147 +1129,6 @@ class HTMLEditUtils final { aFoundLinkElement); } - enum class WalkTreeOption { - IgnoreNonEditableNode, // Ignore non-editable nodes and their children. - IgnoreDataNodeExceptText, // Ignore data nodes which are not text node. - IgnoreWhiteSpaceOnlyText, // Ignore text nodes having only white-spaces. - StopAtBlockBoundary, // Stop waking the tree at a block boundary. - }; - using WalkTreeOptions = EnumSet<WalkTreeOption>; - /** - * GetPreviousSibling() return the preceding sibling of aContent which matches - * with aOption. - * - * @param aBlockInlineCheck Can be Unused if aOptions does not contain - * StopAtBlockBoundary. - */ - static nsIContent* GetPreviousSibling( - const nsIContent& aContent, const WalkTreeOptions& aOptions, - BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { - aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); - for (nsIContent* sibling = aContent.GetPreviousSibling(); sibling; - sibling = sibling->GetPreviousSibling()) { - if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) { - continue; - } - if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && - HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) { - return nullptr; - } - return sibling; - } - return nullptr; - } - - /** - * GetNextSibling() return the following sibling of aContent which matches - * with aOption. - * - * @param aBlockInlineCheck Can be Unused if aOptions does not contain - * StopAtBlockBoundary. - */ - static nsIContent* GetNextSibling( - const nsIContent& aContent, const WalkTreeOptions& aOptions, - BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { - aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); - for (nsIContent* sibling = aContent.GetNextSibling(); sibling; - sibling = sibling->GetNextSibling()) { - if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) { - continue; - } - if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && - HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) { - return nullptr; - } - return sibling; - } - return nullptr; - } - - /** - * Return the last child of aNode which matches with aOption. - * - * @param aBlockInlineCheck Can be unused if aOptions does not contain - * StopAtBlockBoundary. - */ - static nsIContent* GetLastChild( - const nsINode& aNode, const WalkTreeOptions& aOptions, - BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { - aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); - for (nsIContent* child = aNode.GetLastChild(); child; - child = child->GetPreviousSibling()) { - if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) { - continue; - } - if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && - HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) { - return nullptr; - } - return child; - } - return nullptr; - } - - /** - * Return the first child of aNode which matches with aOption. - * - * @param aBlockInlineCheck Can be unused if aOptions does not contain - * StopAtBlockBoundary. - */ - static nsIContent* GetFirstChild( - const nsINode& aNode, const WalkTreeOptions& aOptions, - BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { - aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); - for (nsIContent* child = aNode.GetFirstChild(); child; - child = child->GetNextSibling()) { - if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) { - continue; - } - if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && - HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) { - return nullptr; - } - return child; - } - return nullptr; - } - - /** - * Return true if aContent is the last child of aNode with ignoring all - * children which do not match with aOption. - * - * @param aBlockInlineCheck Can be unused if aOptions does not contain - * StopAtBlockBoundary. - */ - static bool IsLastChild( - const nsIContent& aContent, const WalkTreeOptions& aOptions, - BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { - nsINode* parentNode = aContent.GetParentNode(); - if (!parentNode) { - return false; - } - return HTMLEditUtils::GetLastChild(*parentNode, aOptions, - aBlockInlineCheck) == &aContent; - } - - /** - * Return true if aContent is the first child of aNode with ignoring all - * children which do not match with aOption. - * - * @param aBlockInlineCheck Can be unused if aOptions does not contain - * StopAtBlockBoundary. - */ - static bool IsFirstChild( - const nsIContent& aContent, const WalkTreeOptions& aOptions, - BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { - nsINode* parentNode = aContent.GetParentNode(); - if (!parentNode) { - return false; - } - return HTMLEditUtils::GetFirstChild(*parentNode, aOptions, - aBlockInlineCheck) == &aContent; - } - /** * GetAdjacentContentToPutCaret() walks the DOM tree to find an editable node * near aPoint where may be a good point to put caret and keep typing or @@ -1413,6 +1272,7 @@ class HTMLEditUtils final { private: enum class StopAtBlockSibling : bool { No, Yes }; + static nsIContent* GetNextLeafContentOrNextBlockElementImpl( const nsIContent& aStartContent, StopAtBlockSibling aStopAtBlockSibling, const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, @@ -1571,6 +1431,115 @@ class HTMLEditUtils final { aAncestorLimiter); } + private: + static nsIContent* GetSibling(const nsIContent& aContent, + WalkTreeDirection aDirection, + const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck); + + public: + /** + * Return the preceding sibling of aContent with ignoring leaf nodes which are + * specified by aOptions. + * + * @param aOptions If a sibling is a leaf node or does not have meaningful + * children against aOptions, the sibling is ignored. + * NOTE: LeafNodeOption::TreatChildBlockAsLeafNode is + * ignored. + */ + static nsIContent* GetPreviousSibling(const nsIContent& aContent, + const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck) { + return GetSibling(aContent, WalkTreeDirection::Backward, aOptions, + aBlockInlineCheck); + } + + /** + * Return the following sibling of aContent with ignoring leaf nodes which are + * specified by aOptions. + * + * @param aOptions If a sibling is a leaf node or does not have meaningful + * children against aOptions, the sibling is ignored. + * NOTE: LeafNodeOption::TreatChildBlockAsLeafNode is + * ignored. + */ + static nsIContent* GetNextSibling(const nsIContent& aContent, + const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck) { + return GetSibling(aContent, WalkTreeDirection::Forward, aOptions, + aBlockInlineCheck); + } + + private: + enum class FirstOrLastChild { First, Last }; + static nsIContent* GetFirstOrLastChild(const nsINode& aNode, + FirstOrLastChild aFirstOrLastChild, + const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck); + + public: + /** + * Return the last child of aNode with ignoring leaf nodes which are specified + * by aOptions. + * + * @param aOptions If a child is a leaf node or does not have meaningful + * children against aOptions, the child is ignored. + * NOTE: LeafNodeOption::TreatChildBlockAsLeafNode is + * ignored. + */ + static nsIContent* GetLastChild(const nsINode& aNode, + const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck) { + return GetFirstOrLastChild(aNode, FirstOrLastChild::Last, aOptions, + aBlockInlineCheck); + } + + /** + * Return the first child of aNode with ignoring leaf nodes which are + * specified by aOptions. + * + * @param aOptions If a child is a leaf node or does not have meaningful + * children against aOptions, the child is ignored. + * NOTE: LeafNodeOption::TreatChildBlockAsLeafNode is + * ignored. + */ + static nsIContent* GetFirstChild(const nsINode& aNode, + const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck) { + return GetFirstOrLastChild(aNode, FirstOrLastChild::First, aOptions, + aBlockInlineCheck); + } + + /** + * Return true if aContent is the last child of aNode with ignoring all + * children which are specified by aOptions. + */ + static bool IsLastChild(const nsIContent& aContent, + const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck) { + nsINode* const parentNode = aContent.GetParentNode(); + if (MOZ_UNLIKELY(!parentNode)) { + return false; + } + return HTMLEditUtils::GetLastChild(*parentNode, aOptions, + aBlockInlineCheck) == &aContent; + } + + /** + * Return true if aContent is the first child of aNode with ignoring all + * children which are specified by aOptions. + */ + static bool IsFirstChild(const nsIContent& aContent, + const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck) { + nsINode* const parentNode = aContent.GetParentNode(); + if (MOZ_UNLIKELY(!parentNode)) { + return false; + } + return HTMLEditUtils::GetFirstChild(*parentNode, aOptions, + aBlockInlineCheck) == &aContent; + } + /** * Returns a content node whose inline styles should be preserved after * deleting content in a range. Typically, you should set aPoint to start @@ -2021,6 +1990,14 @@ class HTMLEditUtils final { return GetTableCellElementIfOnlyOneSelected(*firstRange); } + enum class WalkTreeOption { + IgnoreNonEditableNode, // Ignore non-editable nodes and their children. + IgnoreDataNodeExceptText, // Ignore data nodes which are not text node. + IgnoreWhiteSpaceOnlyText, // Ignore text nodes having only white-spaces. + StopAtBlockBoundary, // Stop waking the tree at a block boundary. + }; + using WalkTreeOptions = EnumSet<WalkTreeOption>; + /** * GetInclusiveFirstChildWhichHasOneChild() returns the deepest element whose * tag name is one of `aFirstElementName` and `aOtherElementNames...` if and diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp @@ -1988,8 +1988,9 @@ nsresult HTMLEditor::InsertElementAtSelectionAsAction( // check for inserting a whole table at the end of a block. If so insert // a br after it. if (!aElement->IsHTMLElement(nsGkAtoms::table) || - !HTMLEditUtils::IsLastChild(*aElement, - {WalkTreeOption::IgnoreNonEditableNode})) { + !HTMLEditUtils::IsLastChild( + *aElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { return NS_OK; } @@ -4932,16 +4933,18 @@ HTMLEditor::RemoveBlockContainerWithTransaction(Element& aElement) { } EditorDOMPoint pointToPutCaret; if (HTMLEditUtils::CanNodeContain(*parentElement, *nsGkAtoms::br)) { - if (nsCOMPtr<nsIContent> child = HTMLEditUtils::GetFirstChild( - aElement, {WalkTreeOption::IgnoreNonEditableNode})) { + if (const nsCOMPtr<nsIContent> child = HTMLEditUtils::GetFirstChild( + aElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { // The case of aNode not being empty. We need a br at start unless: // 1) previous sibling of aNode is a block, OR // 2) previous sibling of aNode is a br, OR // 3) first child of aNode is a block OR // 4) either is null - if (nsIContent* previousSibling = HTMLEditUtils::GetPreviousSibling( - aElement, {WalkTreeOption::IgnoreNonEditableNode})) { + if (nsIContent* const previousSibling = HTMLEditUtils::GetPreviousSibling( + aElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (!HTMLEditUtils::IsBlockElement( *previousSibling, BlockInlineCheck::UseComputedDisplayOutsideStyle) && @@ -4971,14 +4974,15 @@ HTMLEditor::RemoveBlockContainerWithTransaction(Element& aElement) { // 3) last child of aNode is a br OR // 4) either is null - if (nsIContent* nextSibling = HTMLEditUtils::GetNextSibling( - aElement, {WalkTreeOption::IgnoreNonEditableNode})) { + if (nsIContent* const nextSibling = HTMLEditUtils::GetNextSibling( + aElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (nextSibling && !HTMLEditUtils::IsBlockElement( *nextSibling, BlockInlineCheck::UseComputedDisplayStyle)) { - if (nsIContent* lastChild = HTMLEditUtils::GetLastChild( - aElement, {WalkTreeOption::IgnoreNonEditableNode}, - BlockInlineCheck::Unused)) { + if (nsIContent* const lastChild = HTMLEditUtils::GetLastChild( + aElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (!HTMLEditUtils::IsBlockElement( *lastChild, BlockInlineCheck::UseComputedDisplayStyle) && !lastChild->IsHTMLElement(nsGkAtoms::br)) { @@ -5001,8 +5005,10 @@ HTMLEditor::RemoveBlockContainerWithTransaction(Element& aElement) { } } } - } else if (nsIContent* previousSibling = HTMLEditUtils::GetPreviousSibling( - aElement, {WalkTreeOption::IgnoreNonEditableNode})) { + } else if (nsIContent* const previousSibling = + HTMLEditUtils::GetPreviousSibling( + aElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { // The case of aNode being empty. We need a br at start unless: // 1) previous sibling of aNode is a block, OR // 2) previous sibling of aNode is a br, OR @@ -5013,7 +5019,8 @@ HTMLEditor::RemoveBlockContainerWithTransaction(Element& aElement) { *previousSibling, BlockInlineCheck::UseComputedDisplayStyle) && !previousSibling->IsHTMLElement(nsGkAtoms::br)) { if (nsIContent* nextSibling = HTMLEditUtils::GetNextSibling( - aElement, {WalkTreeOption::IgnoreNonEditableNode})) { + aElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { if (!HTMLEditUtils::IsBlockElement( *nextSibling, BlockInlineCheck::UseComputedDisplayStyle) && !nextSibling->IsHTMLElement(nsGkAtoms::br)) { @@ -6678,12 +6685,10 @@ HTMLEditor::CopyLastEditableChildStylesWithTransaction( // Look for the deepest last editable leaf node in aPreviousBlock. // Then, if found one is a <br> element, look for non-<br> element. - nsIContent* deepestEditableContent = nullptr; - for (nsCOMPtr<nsIContent> child = &aPreviousBlock; child; - child = HTMLEditUtils::GetLastChild( - *child, {WalkTreeOption::IgnoreNonEditableNode})) { - deepestEditableContent = child; - } + nsIContent* deepestEditableContent = HTMLEditUtils::GetPreviousLeafContent( + EditorRawDOMPoint::AtEndOf(aPreviousBlock), + {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); while (deepestEditableContent && deepestEditableContent->IsHTMLElement(nsGkAtoms::br)) { deepestEditableContent = HTMLEditUtils::GetPreviousLeafContent( diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -7160,8 +7160,10 @@ HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter:: // 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})) { + if (!HTMLEditUtils::IsFirstChild( + *mEmptyInclusiveAncestorBlockElement, + {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { return CreateLineBreakResult::NotHandled(); } diff --git a/editor/libeditor/HTMLEditorInsertParagraphHandler.cpp b/editor/libeditor/HTMLEditorInsertParagraphHandler.cpp @@ -1220,7 +1220,8 @@ bool HTMLEditor::AutoInsertParagraphHandler::ShouldCreateNewParagraph( const auto* const precedingBRElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetPreviousSibling( *aPointToSplit.ContainerAs<Text>(), - {WalkTreeOption::IgnoreNonEditableNode})); + {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)); return !IsNullOrInvisibleBRElementOrPaddingOneForEmptyLastLine( precedingBRElement); } @@ -1232,7 +1233,8 @@ bool HTMLEditor::AutoInsertParagraphHandler::ShouldCreateNewParagraph( const auto* const followingBRElement = HTMLBRElement::FromNodeOrNull(HTMLEditUtils::GetNextSibling( *aPointToSplit.ContainerAs<Text>(), - {WalkTreeOption::IgnoreNonEditableNode})); + {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)); return !IsNullOrInvisibleBRElementOrPaddingOneForEmptyLastLine( followingBRElement); } @@ -1834,8 +1836,9 @@ HTMLEditor::AutoInsertParagraphHandler::HandleInListItemElement( // If the given list item element is not the last list item element of // its parent nor not followed by sub list elements, split the parent // before it. - if (!HTMLEditUtils::IsLastChild(aListItemElement, - {WalkTreeOption::IgnoreNonEditableNode})) { + if (!HTMLEditUtils::IsLastChild( + aListItemElement, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle)) { Result<SplitNodeResult, nsresult> splitListItemParentResult = mHTMLEditor.SplitNodeWithTransaction( EditorDOMPoint(&aListItemElement)); diff --git a/editor/libeditor/HTMLStyleEditor.cpp b/editor/libeditor/HTMLStyleEditor.cpp @@ -64,6 +64,7 @@ using namespace dom; using EditablePointOption = HTMLEditUtils::EditablePointOption; using EditablePointOptions = HTMLEditUtils::EditablePointOptions; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; +using LeafNodeOption = HTMLEditUtils::LeafNodeOption; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; template nsresult HTMLEditor::SetInlinePropertiesAsSubAction( @@ -950,7 +951,8 @@ HTMLEditor::AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode( if (mAttribute) { // Look for siblings that are correct type of node nsIContent* sibling = HTMLEditUtils::GetPreviousSibling( - *middleTextNode, {WalkTreeOption::IgnoreNonEditableNode}); + *middleTextNode, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (sibling && sibling->IsElement()) { OwningNonNull<Element> element(*sibling->AsElement()); Result<bool, nsresult> result = @@ -979,7 +981,8 @@ HTMLEditor::AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode( } } sibling = HTMLEditUtils::GetNextSibling( - *middleTextNode, {WalkTreeOption::IgnoreNonEditableNode}); + *middleTextNode, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (sibling && sibling->IsElement()) { OwningNonNull<Element> element(*sibling->AsElement()); Result<bool, nsresult> result = @@ -1060,10 +1063,13 @@ Result<CaretPoint, nsresult> HTMLEditor::AutoInlineStyleSetter::ApplyStyle( } // First check if there's an adjacent sibling we can put our node into. - nsCOMPtr<nsIContent> previousSibling = HTMLEditUtils::GetPreviousSibling( - aContent, {WalkTreeOption::IgnoreNonEditableNode}); - nsCOMPtr<nsIContent> nextSibling = HTMLEditUtils::GetNextSibling( - aContent, {WalkTreeOption::IgnoreNonEditableNode}); + const nsCOMPtr<nsIContent> previousSibling = + HTMLEditUtils::GetPreviousSibling( + aContent, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + const nsCOMPtr<nsIContent> nextSibling = HTMLEditUtils::GetNextSibling( + aContent, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (RefPtr<Element> previousElement = Element::FromNodeOrNull(previousSibling)) { Result<bool, nsresult> canMoveIntoPreviousSibling = @@ -4172,7 +4178,8 @@ Result<CreateElementResult, nsresult> HTMLEditor::SetFontSizeOnTextNode( aIncrementOrDecrement == FontSize::incr ? nsGkAtoms::big : nsGkAtoms::small; nsCOMPtr<nsIContent> sibling = HTMLEditUtils::GetPreviousSibling( - *textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode}); + *textNodeForTheRange, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { // Previous sib is already right kind of inline node; slide this over Result<MoveNodeResult, nsresult> moveTextNodeResult = @@ -4188,7 +4195,8 @@ Result<CreateElementResult, nsresult> HTMLEditor::SetFontSizeOnTextNode( return CreateElementResult::NotHandled(std::move(pointToPutCaret)); } sibling = HTMLEditUtils::GetNextSibling( - *textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode}); + *textNodeForTheRange, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { // Following sib is already right kind of inline node; slide this over Result<MoveNodeResult, nsresult> moveTextNodeResult = @@ -4322,7 +4330,8 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::SetFontSizeWithBigOrSmallElement( // Next, if next or previous is <big> or <small>, move aContent into it. nsCOMPtr<nsIContent> sibling = HTMLEditUtils::GetPreviousSibling( - aContent, {WalkTreeOption::IgnoreNonEditableNode}); + aContent, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { Result<MoveNodeResult, nsresult> moveNodeResult = MoveNodeToEndWithTransaction(aContent, *sibling); @@ -4337,7 +4346,8 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::SetFontSizeWithBigOrSmallElement( } sibling = HTMLEditUtils::GetNextSibling( - aContent, {WalkTreeOption::IgnoreNonEditableNode}); + aContent, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) { Result<MoveNodeResult, nsresult> moveNodeResult = MoveNodeWithTransaction(aContent, EditorDOMPoint(sibling, 0u)); diff --git a/editor/libeditor/WSRunScannerNestedClasses.cpp b/editor/libeditor/WSRunScannerNestedClasses.cpp @@ -904,17 +904,17 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( const LeafNodeOptions leafNodeOptions = ToLeafNodeOptions(aOptions) + LeafNodeOption::TreatChildBlockAsLeafNode; const EditorRawDOMPoint point = [&]() MOZ_NEVER_INLINE_DEBUG { - // TODO: Use HTMLEditUtils::GetNextSibling(). - nsIContent* const child = [&]() -> nsIContent* { - nsIContent* child = - aPoint.CanContainerHaveChildren() ? aPoint.GetChild() : nullptr; - // XXX Why don't we skip non-editable nodes here? - while (child && child->IsComment() && - !aOptions.contains(Option::StopAtComment)) { - child = child->GetNextSibling(); - } - return child; - }(); + if (!aPoint.CanContainerHaveChildren()) { + return aPoint.template To<EditorRawDOMPoint>(); + } + nsIContent* const child = + aPoint.GetPreviousSiblingOfChild() + ? HTMLEditUtils::GetNextSibling( + *aPoint.GetPreviousSiblingOfChild(), leafNodeOptions, + UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) + : HTMLEditUtils::GetFirstChild( + *aPoint.GetContainer(), leafNodeOptions, + UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)); if (!child || HTMLEditUtils::IsBlockElement( *child, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || @@ -1028,18 +1028,17 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( const LeafNodeOptions leafNodeOptions = ToLeafNodeOptions(aOptions) + LeafNodeOption::TreatChildBlockAsLeafNode; const EditorRawDOMPoint point = [&]() MOZ_NEVER_INLINE_DEBUG { - // TODO: Use HTMLEditUtils::GetPreviousSibling(). - nsIContent* const previousChild = [&]() -> nsIContent* { - nsIContent* previousChild = aPoint.CanContainerHaveChildren() - ? aPoint.GetPreviousSiblingOfChild() - : nullptr; - // XXX Why don't we skip non-editable nodes here? - while (previousChild && previousChild->IsComment() && - !aOptions.contains(Option::StopAtComment)) { - previousChild = previousChild->GetPreviousSibling(); - } - return previousChild; - }(); + if (!aPoint.CanContainerHaveChildren()) { + return aPoint.template To<EditorRawDOMPoint>(); + } + nsIContent* const previousChild = + aPoint.GetChild() + ? HTMLEditUtils::GetPreviousSibling( + *aPoint.GetChild(), leafNodeOptions, + UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) + : HTMLEditUtils::GetLastChild( + *aPoint.GetContainer(), leafNodeOptions, + UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)); if (!previousChild || HTMLEditUtils::IsBlockElement( *previousChild, diff --git a/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp b/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp @@ -2523,9 +2523,10 @@ WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt( } } - nsCOMPtr<nsIContent> previousEditableSibling = + const nsCOMPtr<nsIContent> previousEditableSibling = HTMLEditUtils::GetPreviousSibling( - aContentToDelete, {WalkTreeOption::IgnoreNonEditableNode}); + aContentToDelete, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); // Delete the node, and join like nodes if appropriate nsresult rv = aHTMLEditor.DeleteNodeWithTransaction(aContentToDelete); if (NS_FAILED(rv)) { @@ -2548,8 +2549,9 @@ WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt( return CaretPoint(std::move(pointToPutCaret)); } - nsIContent* nextEditableSibling = HTMLEditUtils::GetNextSibling( - *previousEditableSibling, {WalkTreeOption::IgnoreNonEditableNode}); + nsIContent* const nextEditableSibling = HTMLEditUtils::GetNextSibling( + *previousEditableSibling, {LeafNodeOption::IgnoreNonEditableNode}, + BlockInlineCheck::UseComputedDisplayOutsideStyle); if (aCaretPoint.GetContainer() != nextEditableSibling) { return CaretPoint(std::move(pointToPutCaret)); } diff --git a/editor/libeditor/gtest/TestHTMLEditUtils.cpp b/editor/libeditor/gtest/TestHTMLEditUtils.cpp @@ -1921,6 +1921,284 @@ TEST(HTMLEditUtilsTest, GetPreviousLeafContent_Content) // TODO: Test GetPreviousLeafContent() which takes EditorDOMPoint +TEST(HTMLEditUtilsTest, GetPreviousSibling) +{ + using LeafNodeOption = HTMLEditUtils::LeafNodeOption; + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + GetLeafNodeTest{u"<div><p><br></p></div>", "div > p", {}, nullptr}, + GetLeafNodeTest{u"<div><p><br></p><p><br></p></div>", + "div > p + p", + {}, + "div > p"}, + GetLeafNodeTest{u"<div><p><br></p><!-- comment --><p><br></p></div>", + "div p + p", + {}, + "div > p"}, + GetLeafNodeTest{u"<div><p><br></p><!-- comment --><p><br></p></div>", + "div > p + p", + {LeafNodeOption::TreatCommentAsLeafNode}, + nullptr, + "div", + 1u}, + GetLeafNodeTest{u"<div><p><br></p> <p><br></p></div>", + "div > p + p", + {}, + nullptr, + "div", + 1u}, + GetLeafNodeTest{u"<div><p><br></p> <p><br></p></div>", + "div > p + p", + {LeafNodeOption::IgnoreInvisibleText}, + "div > p"}, + GetLeafNodeTest{ + u"<div contenteditable><p><br></p><p " + u"contenteditable=\"false\"><br></p><p><br></p></div>", + "div > p + p + p", + {}, + "div > p + p"}, + GetLeafNodeTest{ + u"<div contenteditable><p><br></p><p " + u"contenteditable=\"false\"><br></p><p><br></p></div>", + "div > p + p + p", + {LeafNodeOption::IgnoreNonEditableNode}, + "div > p"}, + GetLeafNodeTest{ + u"<div><b>abc</b><s><!-- comment --></s><i>def</i></div>", + "div > i", + {}, + "div > s"}, + GetLeafNodeTest{ + u"<div><b>abc</b><s><!-- comment --></s><i>def</i></div>", + "div > i", + {LeafNodeOption::IgnoreInvisibleEmptyInlineContainers}, + "div > b"}, + GetLeafNodeTest{ + u"<div><b>abc</b><s><!-- comment --></s><i>def</i></div>", + "div > i", + {LeafNodeOption::IgnoreInvisibleEmptyInlineContainers, + LeafNodeOption::TreatCommentAsLeafNode}, + "div > s"}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const target = body->QuerySelector( + nsDependentCString(testData.mContentSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(target); + const nsIContent* result = HTMLEditUtils::GetPreviousSibling( + *target, testData.mOptions, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode())) + << "GetPreviousSibling: " << testData + << "(Got: " << ToString(RefPtr{result}) << ")"; + } +} + +TEST(HTMLEditUtilsTest, GetNextSibling) +{ + using LeafNodeOption = HTMLEditUtils::LeafNodeOption; + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + GetLeafNodeTest{u"<div><p><br></p></div>", "div > p", {}, nullptr}, + GetLeafNodeTest{u"<div><p><br></p><p><br></p></div>", + "div > p", + {}, + "div > p + p"}, + GetLeafNodeTest{u"<div><p><br></p><!-- comment --><p><br></p></div>", + "div > p", + {}, + "div > p + p"}, + GetLeafNodeTest{u"<div><p><br></p><!-- comment --><p><br></p></div>", + "div > p", + {LeafNodeOption::TreatCommentAsLeafNode}, + nullptr, + "div", + 1u}, + GetLeafNodeTest{u"<div><p><br></p> <p><br></p></div>", + "div > p", + {}, + nullptr, + "div", + 1u}, + GetLeafNodeTest{u"<div><p><br></p> <p><br></p></div>", + "div > p", + {LeafNodeOption::IgnoreInvisibleText}, + "div > p + p"}, + GetLeafNodeTest{ + u"<div contenteditable><p><br></p><p " + u"contenteditable=\"false\"><br></p><p><br></p></div>", + "div p + p", + {}, + "div > p + p + p"}, + GetLeafNodeTest{ + u"<div contenteditable><p><br></p><p " + u"contenteditable=\"false\"><br></p><p><br></p></div>", + "div p", + {LeafNodeOption::IgnoreNonEditableNode}, + "div > p + p + p"}, + GetLeafNodeTest{ + u"<div><b>abc</b><s><!-- comment --></s><i>def</i></div>", + "div > b", + {}, + "div > s"}, + GetLeafNodeTest{ + u"<div><b>abc</b><s><!-- comment --></s><i>def</i></div>", + "div > b", + {LeafNodeOption::IgnoreInvisibleEmptyInlineContainers}, + "div > i"}, + GetLeafNodeTest{ + u"<div><b>abc</b><s><!-- comment --></s><i>def</i></div>", + "div > b", + {LeafNodeOption::IgnoreInvisibleEmptyInlineContainers, + LeafNodeOption::TreatCommentAsLeafNode}, + "div > s"}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const target = body->QuerySelector( + nsDependentCString(testData.mContentSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(target); + const nsIContent* result = HTMLEditUtils::GetNextSibling( + *target, testData.mOptions, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode())) + << "GetNextSibling: " << testData + << "(Got: " << ToString(RefPtr{result}) << ")"; + } +} + +TEST(HTMLEditUtilsTest, GetFirstChild) +{ + using LeafNodeOption = HTMLEditUtils::LeafNodeOption; + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + GetLeafNodeTest{u"<div></div>", "div", {}, nullptr}, + GetLeafNodeTest{u"<div><p><br></p></div>", "div", {}, "div > p"}, + GetLeafNodeTest{ + u"<div><!-- comment --><p><br></p></div>", "div", {}, "div > p"}, + GetLeafNodeTest{u"<div><!-- comment --><p><br></p></div>", + "div", + {LeafNodeOption::TreatCommentAsLeafNode}, + nullptr, + "div", + 0u}, + GetLeafNodeTest{ + u"<div> <p><br></p></div>", "div", {}, nullptr, "div", 0u}, + GetLeafNodeTest{u"<div> <p><br></p></div>", + "div", + {LeafNodeOption::IgnoreInvisibleText}, + "div > p"}, + GetLeafNodeTest{ + u"<div contenteditable><p " + u"contenteditable=\"false\"><br></p><p><br></p></div>", + "div", + {}, + "div > p"}, + GetLeafNodeTest{ + u"<div contenteditable><p " + u"contenteditable=\"false\"><br></p><p><br></p></div>", + "div", + {LeafNodeOption::IgnoreNonEditableNode}, + "div > p + p"}, + GetLeafNodeTest{u"<div><s><!-- comment --></s><i>def</i></div>", + "div", + {}, + "div > s"}, + GetLeafNodeTest{ + u"<div><s><!-- comment --></s><i>def</i></div>", + "div", + {LeafNodeOption::IgnoreInvisibleEmptyInlineContainers}, + "div > i"}, + GetLeafNodeTest{ + u"<div><s><!-- comment --></s><i>def</i></div>", + "div", + {LeafNodeOption::IgnoreInvisibleEmptyInlineContainers, + LeafNodeOption::TreatCommentAsLeafNode}, + "div > s"}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const target = body->QuerySelector( + nsDependentCString(testData.mContentSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(target); + const nsIContent* result = HTMLEditUtils::GetFirstChild( + *target, testData.mOptions, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode())) + << "GetFirstChild: " << testData << "(Got: " << ToString(RefPtr{result}) + << ")"; + } +} + +TEST(HTMLEditUtilsTest, GetLastChild) +{ + using LeafNodeOption = HTMLEditUtils::LeafNodeOption; + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + GetLeafNodeTest{u"<div></div>", "div", {}, nullptr}, + GetLeafNodeTest{u"<div><p><br></p></div>", "div", {}, "div > p"}, + GetLeafNodeTest{ + u"<div><p><br></p><!-- comment --></div>", "div", {}, "div > p"}, + GetLeafNodeTest{u"<div><p><br></p><!-- comment --></div>", + "div", + {LeafNodeOption::TreatCommentAsLeafNode}, + nullptr, + "div", + 1u}, + GetLeafNodeTest{ + u"<div><p><br></p> </div>", "div", {}, nullptr, "div", 1u}, + GetLeafNodeTest{u"<div><p><br></p> </div>", + "div", + {LeafNodeOption::IgnoreInvisibleText}, + "div > p"}, + GetLeafNodeTest{u"<div contenteditable><p><br></p><p " + u"contenteditable=\"false\"><br></p></div>", + "div", + {}, + "div > p + p"}, + GetLeafNodeTest{u"<div contenteditable><p><br></p><p " + u"contenteditable=\"false\"><br></p></div>", + "div", + {LeafNodeOption::IgnoreNonEditableNode}, + "div > p"}, + GetLeafNodeTest{u"<div><i>def</i><s><!-- comment --></s></div>", + "div", + {}, + "div > s"}, + GetLeafNodeTest{ + u"<div><i>def</i><s><!-- comment --></s></div>", + "div", + {LeafNodeOption::IgnoreInvisibleEmptyInlineContainers}, + "div > i"}, + GetLeafNodeTest{ + u"<div><i>def</i><s><!-- comment --></s></div>", + "div", + {LeafNodeOption::IgnoreInvisibleEmptyInlineContainers, + LeafNodeOption::TreatCommentAsLeafNode}, + "div > s"}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const target = body->QuerySelector( + nsDependentCString(testData.mContentSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(target); + const nsIContent* result = HTMLEditUtils::GetLastChild( + *target, testData.mOptions, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode())) + << "GetLastChild: " << testData << "(Got: " << ToString(RefPtr{result}) + << ")"; + } +} + struct MOZ_STACK_CLASS LineBreakBeforeBlockBoundaryTest final { const char16_t* const mInnerHTML; const char* const mContainer;