tor-browser

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

commit ca50a4a628a3ea7e8b7fcd8071a48881e3a0846a
parent 4a5d93d6315c2809b13c130cd43802ce6716b904
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date:   Wed,  7 Jan 2026 01:10:20 +0000

Bug 1998077 - part 3: Add new options to ignore empty/invisible leaf nodes r=m_kato

This adds some options which ignore some types of inline elements.

For example, `IgnoreAnyEmptyInlineContainers` is useful to scan previous
or next editable point because Chrome ignores empty inline elements when
deleting from next to an inline element.

`IgnoreInvisibleInlineVoidElements` is useful to scan invisible line
break from end of a block or checking a line break is invisible because
visible inline element makes a line after the last line break. E.g.,
```html
<div>ABC<br><span></span></div>
```
This shows only one line. However,
```html
<div>ABC<br><span style="border:1px solid"></span></div>
```
This shows 2 lines in the `<div>`.

Additionally, renaming the existing `LeafNodeType` values for making
them easier to understand.

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

Diffstat:
Meditor/libeditor/EditorBase.cpp | 2--
Meditor/libeditor/HTMLEditSubActionHandler.cpp | 17+++++++++--------
Meditor/libeditor/HTMLEditUtils.cpp | 626+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Meditor/libeditor/HTMLEditUtils.h | 557+++++++++----------------------------------------------------------------------
Meditor/libeditor/HTMLEditor.cpp | 42++++++++++++++++++++----------------------
Meditor/libeditor/HTMLEditorDataTransfer.cpp | 12++++++++----
Meditor/libeditor/HTMLEditorDeleteHandler.cpp | 32+++++++++++++++++---------------
Meditor/libeditor/HTMLEditorInsertParagraphHandler.cpp | 6+++---
Meditor/libeditor/HTMLStyleEditor.cpp | 7++-----
Meditor/libeditor/TextEditor.cpp | 5+----
Meditor/libeditor/WSRunScanner.cpp | 31++++++++++++++-----------------
Meditor/libeditor/WSRunScanner.h | 34++++++++++++++++++++++++++++++++++
Meditor/libeditor/WSRunScannerNestedClasses.cpp | 167++++++++++++++++++++++++++++++++++++++-----------------------------------------
Meditor/libeditor/WhiteSpaceVisibilityKeeper.cpp | 36+++++++++++++++++-------------------
Meditor/libeditor/gtest/TestHTMLEditUtils.cpp | 184++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
15 files changed, 1018 insertions(+), 740 deletions(-)

diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp @@ -144,8 +144,6 @@ using namespace dom; using namespace widget; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; -using LeafNodeType = HTMLEditUtils::LeafNodeType; -using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; static LazyLogModule gEventLog("EditorEvent"); diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp @@ -78,8 +78,8 @@ extern LazyLogModule gTextInputLog; // Defined in EditorBase.cpp using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions; -using LeafNodeType = HTMLEditUtils::LeafNodeType; -using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; +using LeafNodeOption = HTMLEditUtils::LeafNodeOption; +using LeafNodeOptions = HTMLEditUtils::LeafNodeOptions; using WalkTextOption = HTMLEditUtils::WalkTextOption; using WalkTreeDirection = HTMLEditUtils::WalkTreeDirection; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; @@ -926,8 +926,8 @@ nsresult HTMLEditor::ReflectPaddingBRElementForEmptyEditor() { // here and at redo, or doing it everywhere else that might care. Since undo // and redo are relatively rare, it makes sense to take the (small) // performance hit here. - nsIContent* firstLeafChild = HTMLEditUtils::GetFirstLeafContent( - *mRootElement, {LeafNodeType::OnlyLeafNode}); + nsIContent* firstLeafChild = + HTMLEditUtils::GetFirstLeafContent(*mRootElement, {}); if (firstLeafChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) { mPaddingBRElementForEmptyEditor = @@ -2566,7 +2566,8 @@ HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces( // Try to put caret next to immediately after previous editable leaf. nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - newCaretPosition, {LeafNodeType::LeafNodeOrNonEditableNode}, + newCaretPosition, + {LeafNodeOption::TreatNonEditableNodeAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, editableBlockElementOrInlineEditingHost); if (previousContent && @@ -2584,7 +2585,7 @@ HTMLEditor::DeleteTextAndNormalizeSurroundingWhiteSpaces( else if (nsIContent* nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( newCaretPosition, - {LeafNodeType::LeafNodeOrNonEditableNode}, + {LeafNodeOption::TreatNonEditableNodeAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, editableBlockElementOrInlineEditingHost)) { if (HTMLEditUtils::IsSimplyEditableNode(*nextContent) && @@ -7231,7 +7232,7 @@ HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction( // endpoint is just after the close of a block. if (nsIContent* child = HTMLEditUtils::GetLastLeafContent( *prevVisibleThingOfEndPoint.ElementPtr(), - {LeafNodeType::LeafNodeOrChildBlock}, + {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseHTMLDefaultStyle)) { newRange.SetEnd(EditorRawDOMPoint::After(*child)); } @@ -7272,7 +7273,7 @@ HTMLEditor::GetRangeExtendedToHardLineEdgesForBlockEditAction( // startpoint is just before the start of a block. if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent( *nextVisibleThingOfStartPoint.ElementPtr(), - {LeafNodeType::LeafNodeOrChildBlock}, + {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseHTMLDefaultStyle)) { newRange.SetStart(EditorRawDOMPoint(child)); } diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp @@ -57,6 +57,36 @@ namespace mozilla { using namespace dom; using EditorType = EditorBase::EditorType; +template nsIContent* HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + const EditorDOMPoint&, const LeafNodeOptions&, BlockInlineCheck, + const Element*); +template nsIContent* HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + const EditorRawDOMPoint&, const LeafNodeOptions&, BlockInlineCheck, + const Element*); +template nsIContent* HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + const EditorDOMPointInText&, const LeafNodeOptions&, BlockInlineCheck, + const Element*); +template nsIContent* HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + const EditorRawDOMPointInText&, const LeafNodeOptions&, BlockInlineCheck, + const Element*); + +template nsIContent* +HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + const EditorDOMPoint&, const LeafNodeOptions&, BlockInlineCheck, + const Element*); +template nsIContent* +HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + const EditorRawDOMPoint&, const LeafNodeOptions&, BlockInlineCheck, + const Element*); +template nsIContent* +HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + const EditorDOMPointInText&, const LeafNodeOptions&, BlockInlineCheck, + const Element*); +template nsIContent* +HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + const EditorRawDOMPointInText&, const LeafNodeOptions&, BlockInlineCheck, + const Element*); + template nsIContent* HTMLEditUtils::GetPreviousContent( const EditorDOMPoint& aPoint, const WalkTreeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, const Element* aAncestorLimiter); @@ -924,9 +954,12 @@ EditorDOMPoint HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible( // because we want to make it visible. Therefore, we cannot use // WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() here. nsIContent* const previousVisibleLeafOrChildBlock = - HTMLEditUtils::GetPreviousNonEmptyLeafContentOrPreviousBlockElement( + HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( preferredPaddingLineBreakPoint, - {LeafNodeType::LeafNodeOrChildBlock}, BlockInlineCheck::Auto); + {LeafNodeOption::TreatChildBlockAsLeafNode, + LeafNodeOption::IgnoreInvisibleEmptyInlineContainers, + LeafNodeOption::IgnoreEmptyText}, + BlockInlineCheck::Auto); if (!previousVisibleLeafOrChildBlock) { // Reached current block. return true; @@ -1093,6 +1126,533 @@ bool HTMLEditUtils::PointIsImmediatelyBeforeCurrentBlockBoundary( return false; } +// static +HTMLEditUtils::LeafNodeType HTMLEditUtils::GetLeafNodeType( + const nsIContent& aContent, const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck, IgnoreChildren aIgnoreChildren) { + if (!HTMLEditUtils::IsSimplyEditableNode(aContent)) { + if (aOptions.contains(LeafNodeOption::TreatNonEditableNodeAsLeafNode)) { + return LeafNodeType::Leaf; + } + if (aOptions.contains(LeafNodeOption::IgnoreNonEditableNode)) { + return LeafNodeType::Ignore; + } + } + if (const Element* const element = Element::FromNode(&aContent)) { + // If the element is a replaced element, it should be treated as a leaf. + if (HTMLEditUtils::IsReplacedElement(*element)) { + return LeafNodeType::Leaf; + } + // We're looking for a child block, check the display-outside style. + if (aOptions.contains(LeafNodeOption::TreatChildBlockAsLeafNode) && + HTMLEditUtils::IsBlockElement( + *element, + UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { + return LeafNodeType::Leaf; + } + // Let's handle invisible void elements even if it has some children. + if (!HTMLEditUtils::IsContainerNode(*element)) { + return aOptions.contains( + LeafNodeOption::IgnoreInvisibleInlineVoidElements) && + !HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*element) + ? LeafNodeType::Ignore + : LeafNodeType::Leaf; + } + if (aIgnoreChildren == IgnoreChildren::No && aContent.HasChildNodes()) { + return LeafNodeType::NonEmptyContainer; + } + // if the element is a flow root, it's meaningful and must be visible. + if (HTMLEditUtils::IsBlockElement( + *element, aBlockInlineCheck == BlockInlineCheck::UseHTMLDefaultStyle + ? BlockInlineCheck::UseHTMLDefaultStyle + : BlockInlineCheck::UseComputedDisplayStyle)) { + return LeafNodeType::Leaf; + } + if (!HTMLEditUtils::IsContainerNode(*element)) { + return LeafNodeType::Ignore; + } + // Now the element is an empty inline container like <span></span>. + if (aOptions.contains(LeafNodeOption::IgnoreAnyEmptyInlineContainers)) { + return LeafNodeType::Ignore; + } + if (aOptions.contains( + LeafNodeOption::IgnoreInvisibleEmptyInlineContainers) && + !HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*element)) { + return LeafNodeType::Ignore; + } + return LeafNodeType::Leaf; + } + if (const Text* const text = Text::FromNode(aContent)) { + if (!text->TextDataLength()) { + return aOptions.contains(LeafNodeOption::IgnoreEmptyText) || + aOptions.contains(LeafNodeOption::IgnoreInvisibleText) + ? LeafNodeType::Ignore + : LeafNodeType::Leaf; + } + return !aOptions.contains(LeafNodeOption::IgnoreInvisibleText) || + IsVisibleTextNode(*text) + ? LeafNodeType::Leaf + : LeafNodeType::Ignore; + } + if (aContent.IsComment()) { + return aOptions.contains(LeafNodeOption::TreatCommentAsLeafNode) + ? LeafNodeType::Leaf + : LeafNodeType::Ignore; + } + return LeafNodeType::Ignore; +} + +// static +nsIContent* HTMLEditUtils::GetLastLeafContent( + const nsINode& aNode, const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck /* = BlockInlineCheck::Unused */) { + MOZ_ASSERT_IF( + aOptions.contains(LeafNodeOption::IgnoreNonEditableNode), + !aOptions.contains(LeafNodeOption::TreatNonEditableNodeAsLeafNode)); + MOZ_ASSERT_IF(aOptions.contains(LeafNodeOption::TreatChildBlockAsLeafNode), + aBlockInlineCheck != BlockInlineCheck::Unused); + // editor shouldn't touch child nodes which are replaced with native + // anonymous nodes. + if (aNode.IsElement() && + HTMLEditUtils::IsNeverElementContentsEditableByUser(*aNode.AsElement())) { + return nullptr; + } + for (nsIContent* content = aNode.GetLastChild(); content;) { + const LeafNodeType type = HTMLEditUtils::GetLeafNodeType( + *content, aOptions, aBlockInlineCheck, IgnoreChildren::No); + if (type == LeafNodeType::Leaf) { + return content; + } + if (type == LeafNodeType::NonEmptyContainer) { + content = content->GetLastChild(); + MOZ_ASSERT(content); + continue; + } + MOZ_ASSERT(type == LeafNodeType::Ignore); + nsIContent* const prevSibling = content->GetPreviousSibling(); + if (prevSibling) { + content = prevSibling; + continue; + } + // Okay, content is the first sibling but no meaningful content is not in + // current container. So, the container can be treated as an empty + // container. + nsIContent* const parent = content->GetParent(); + if (!parent || parent == &aNode) { + return nullptr; + } + content = nullptr; + for (nsIContent* const ancestor : + parent->InclusiveAncestorsOfType<nsIContent>()) { + if (ancestor == &aNode) { + return nullptr; // No meaningful leaf in aNode. + } + // All children of current content is ignorable. So, the parent + // should be treated as empty. + const LeafNodeType type = HTMLEditUtils::GetLeafNodeType( + *ancestor, aOptions, aBlockInlineCheck, IgnoreChildren::Yes); + if (type == LeafNodeType::Leaf) { + return ancestor; + } + MOZ_ASSERT(type == LeafNodeType::Ignore); + // If the ancestor has a previous sibling, check it. + if ((content = ancestor->GetPreviousSibling())) { + break; + } + // Otherwise, check the parent of the ancestor. + } + } + return nullptr; +} + +// static +nsIContent* HTMLEditUtils::GetFirstLeafContent( + const nsINode& aNode, const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck /* = BlockInlineCheck::Unused */) { + MOZ_ASSERT_IF( + aOptions.contains(LeafNodeOption::IgnoreNonEditableNode), + !aOptions.contains(LeafNodeOption::TreatNonEditableNodeAsLeafNode)); + MOZ_ASSERT_IF(aOptions.contains(LeafNodeOption::TreatChildBlockAsLeafNode), + aBlockInlineCheck != BlockInlineCheck::Unused); + // editor shouldn't touch child nodes which are replaced with native + // anonymous nodes. + if (aNode.IsElement() && + HTMLEditUtils::IsNeverElementContentsEditableByUser(*aNode.AsElement())) { + return nullptr; + } + for (nsIContent* content = aNode.GetFirstChild(); content;) { + const LeafNodeType type = HTMLEditUtils::GetLeafNodeType( + *content, aOptions, aBlockInlineCheck, IgnoreChildren::No); + if (type == LeafNodeType::Leaf) { + return content; + } + if (type == LeafNodeType::NonEmptyContainer) { + content = content->GetFirstChild(); + MOZ_ASSERT(content); + continue; + } + MOZ_ASSERT(type == LeafNodeType::Ignore); + nsIContent* const nextSibling = content->GetNextSibling(); + if (nextSibling) { + content = nextSibling; + continue; + } + // Okay, content is the last sibling but no meaningful content is not in + // current container. So, the container can be treated as an empty + // container. + nsIContent* const parent = content->GetParent(); + if (!parent || parent == &aNode) { + return nullptr; // No meaningful leaf in aNode. + } + content = nullptr; + for (nsIContent* const ancestor : + parent->InclusiveAncestorsOfType<nsIContent>()) { + if (ancestor == &aNode) { + return nullptr; + } + // All children of current content is ignorable. So, the parent + // should be treated as empty. + const LeafNodeType type = HTMLEditUtils::GetLeafNodeType( + *ancestor, aOptions, aBlockInlineCheck, IgnoreChildren::Yes); + if (type == LeafNodeType::Leaf) { + return ancestor; + } + MOZ_ASSERT(type == LeafNodeType::Ignore); + // If the ancestor has a next sibling, check it. + if ((content = ancestor->GetNextSibling())) { + break; + } + // Otherwise, check the parent of the ancestor. + } + } + return nullptr; +} + +// static +nsIContent* HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + const nsIContent& aStartContent, const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck, + const Element* aAncestorLimiter /* = nullptr */) { + MOZ_ASSERT_IF( + aOptions.contains(LeafNodeOption::IgnoreNonEditableNode), + !aOptions.contains(LeafNodeOption::TreatNonEditableNodeAsLeafNode)); + + if (&aStartContent == aAncestorLimiter) { + return nullptr; + } + + Element* container = aStartContent.GetParentElement(); + for (nsIContent* nextContent = aStartContent.GetNextSibling();;) { + if (!nextContent) { + if (!container) { + NS_WARNING("Reached orphan node while climbing up the DOM tree"); + return nullptr; + } + for (Element* const parentElement : + container->InclusiveAncestorsOfType<Element>()) { + if (parentElement == aAncestorLimiter || + HTMLEditUtils::IsBlockElement( + *parentElement, + UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { + return nullptr; + } + if (aOptions.contains(LeafNodeOption::TreatNonEditableNodeAsLeafNode) && + !parentElement->IsEditable()) { + return nullptr; + } + nextContent = parentElement->GetNextSibling(); + if (nextContent) { + container = nextContent->GetParentElement(); + break; + } + if (!parentElement->GetParentElement()) { + NS_WARNING("Reached orphan node while climbing up the DOM tree"); + return nullptr; + } + } + MOZ_ASSERT(nextContent); + } + + // We have a next content. If it's a block, return it. + if (HTMLEditUtils::IsBlockElement( + *nextContent, + PreferDisplayOutsideIfUsingDisplay( + UseComputedDisplayStyleIfAuto(aBlockInlineCheck)))) { + return nextContent; + } + LeafNodeType type = HTMLEditUtils::GetLeafNodeType( + *nextContent, aOptions, aBlockInlineCheck, IgnoreChildren::No); + if (type == LeafNodeType::Leaf) { + return nextContent; + } + if (type == LeafNodeType::Ignore) { + nextContent = nextContent->GetNextSibling(); + MOZ_ASSERT_IF(nextContent, container == nextContent->GetParentElement()); + continue; + } + MOZ_ASSERT(type == LeafNodeType::NonEmptyContainer); + if (nsIContent* const lastLeaf = HTMLEditUtils::GetFirstLeafContent( + *nextContent, aOptions, + PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { + return lastLeaf; + } + // nextContent has some nodes, but does not have meaningful nodes. + // Therefore, we can treat it as empty. + type = HTMLEditUtils::GetLeafNodeType( + *nextContent, aOptions, aBlockInlineCheck, IgnoreChildren::Yes); + if (type == LeafNodeType::Leaf) { + return nextContent; + } + MOZ_ASSERT(type == LeafNodeType::Ignore); + nextContent = nextContent->GetNextSibling(); + MOZ_ASSERT_IF(nextContent, container == nextContent->GetParentElement()); + } + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( + "Must return from the preceding for-loop"); +} + +// static +template <typename PT, typename CT> +nsIContent* HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + const EditorDOMPointBase<PT, CT>& aStartPoint, + const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, + const Element* aAncestorLimiter /* = nullptr */) { + MOZ_ASSERT(aStartPoint.IsSet()); + MOZ_ASSERT_IF( + aOptions.contains(LeafNodeOption::IgnoreNonEditableNode), + !aOptions.contains(LeafNodeOption::TreatNonEditableNodeAsLeafNode)); + + if (!aStartPoint.IsInContentNode()) { + return nullptr; + } + if (!aStartPoint.GetContainer()->IsElement()) { + return HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + *aStartPoint.template ContainerAs<nsIContent>(), aOptions, + aBlockInlineCheck, aAncestorLimiter); + } + if (!HTMLEditUtils::IsContainerNode( + *aStartPoint.template ContainerAs<Element>()) || + HTMLEditUtils::IsReplacedElement( + *aStartPoint.template ContainerAs<Element>())) { + return HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + *aStartPoint.template ContainerAs<nsIContent>(), aOptions, + aBlockInlineCheck, aAncestorLimiter); + } + + for (nsIContent* nextContent = aStartPoint.GetChild();;) { + if (!nextContent) { + if (aStartPoint.GetContainer() == aAncestorLimiter || + HTMLEditUtils::IsBlockElement( + *aStartPoint.template ContainerAs<Element>(), + UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { + // We are at end of the block. + return nullptr; + } + + // We are at end of non-block container + return HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + *aStartPoint.template ContainerAs<Element>(), aOptions, + PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck), + aAncestorLimiter); + } + + // We have a next node. If it's a block, return it. + if (HTMLEditUtils::IsBlockElement( + *nextContent, + UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { + return nextContent; + } + LeafNodeType type = HTMLEditUtils::GetLeafNodeType( + *nextContent, aOptions, aBlockInlineCheck, IgnoreChildren::No); + if (type == LeafNodeType::Leaf) { + return nextContent; + } + if (type == LeafNodeType::Ignore) { + nextContent = nextContent->GetNextSibling(); + continue; + } + MOZ_ASSERT(type == LeafNodeType::NonEmptyContainer); + if (nsIContent* const firstLeaf = HTMLEditUtils::GetFirstLeafContent( + *nextContent, aOptions, + PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { + return firstLeaf; + } + // nextContent has some nodes, but does not have meaningful nodes. + // Therefore, we can treat it as empty. + type = HTMLEditUtils::GetLeafNodeType( + *nextContent, aOptions, aBlockInlineCheck, IgnoreChildren::Yes); + if (type == LeafNodeType::Leaf) { + return nextContent; + } + MOZ_ASSERT(type == LeafNodeType::Ignore); + nextContent = nextContent->GetNextSibling(); + } + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( + "Must return from the preceding for-loop"); +} + +// static +nsIContent* HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + const nsIContent& aStartContent, const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck, + const Element* aAncestorLimiter /* = nullptr */) { + MOZ_ASSERT_IF( + aOptions.contains(LeafNodeOption::IgnoreNonEditableNode), + !aOptions.contains(LeafNodeOption::TreatNonEditableNodeAsLeafNode)); + + if (&aStartContent == aAncestorLimiter) { + return nullptr; + } + + Element* container = aStartContent.GetParentElement(); + for (nsIContent* previousContent = aStartContent.GetPreviousSibling();;) { + if (!previousContent) { + if (!container) { + NS_WARNING("Reached orphan node while climbing up the DOM tree"); + return nullptr; + } + for (Element* parentElement : + container->InclusiveAncestorsOfType<Element>()) { + if (parentElement == aAncestorLimiter || + HTMLEditUtils::IsBlockElement( + *parentElement, + UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { + return nullptr; + } + if (aOptions.contains(LeafNodeOption::TreatNonEditableNodeAsLeafNode) && + !parentElement->IsEditable()) { + return nullptr; + } + previousContent = parentElement->GetPreviousSibling(); + if (previousContent) { + container = previousContent->GetParentElement(); + break; + } + if (!parentElement->GetParentElement()) { + NS_WARNING("Reached orphan node while climbing up the DOM tree"); + return nullptr; + } + } + MOZ_ASSERT(previousContent); + } + // We have a next content. If it's a block, return it. + if (HTMLEditUtils::IsBlockElement( + *previousContent, + PreferDisplayOutsideIfUsingDisplay( + UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck)))) { + return previousContent; + } + LeafNodeType type = HTMLEditUtils::GetLeafNodeType( + *previousContent, aOptions, aBlockInlineCheck, IgnoreChildren::No); + if (type == LeafNodeType::Leaf) { + return previousContent; + } + if (type == LeafNodeType::Ignore) { + previousContent = previousContent->GetPreviousSibling(); + MOZ_ASSERT_IF(previousContent, + container == previousContent->GetParentElement()); + continue; + } + if (nsIContent* const lastLeaf = HTMLEditUtils::GetLastLeafContent( + *previousContent, aOptions, + PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { + return lastLeaf; + } + // previousContent has some nodes, but does not have meaningful nodes. + // Therefore, we can treat it as empty. + type = HTMLEditUtils::GetLeafNodeType( + *previousContent, aOptions, aBlockInlineCheck, IgnoreChildren::Yes); + if (type == LeafNodeType::Leaf) { + return previousContent; + } + MOZ_ASSERT(type == LeafNodeType::Ignore); + previousContent = previousContent->GetPreviousSibling(); + MOZ_ASSERT_IF(previousContent, + container == previousContent->GetParentElement()); + return previousContent; + } + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( + "Must return from the preceding for-loop"); +} + +// static +template <typename PT, typename CT> +nsIContent* HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + const EditorDOMPointBase<PT, CT>& aStartPoint, + const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, + const Element* aAncestorLimiter /* = nullptr */) { + MOZ_ASSERT(aStartPoint.IsSet()); + MOZ_ASSERT_IF( + aOptions.contains(LeafNodeOption::IgnoreNonEditableNode), + !aOptions.contains(LeafNodeOption::TreatNonEditableNodeAsLeafNode)); + + if (!aStartPoint.IsInContentNode()) { + return nullptr; + } + if (!aStartPoint.GetContainer()->IsElement()) { + return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + *aStartPoint.template ContainerAs<nsIContent>(), aOptions, + aBlockInlineCheck, aAncestorLimiter); + } + if (!HTMLEditUtils::IsContainerNode( + *aStartPoint.template ContainerAs<Element>()) || + HTMLEditUtils::IsReplacedElement( + *aStartPoint.template ContainerAs<Element>())) { + return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + *aStartPoint.template ContainerAs<Element>(), aOptions, + aBlockInlineCheck, aAncestorLimiter); + } + + if (aStartPoint.IsStartOfContainer()) { + if (aStartPoint.GetContainer() == aAncestorLimiter || + HTMLEditUtils::IsBlockElement( + *aStartPoint.template ContainerAs<Element>(), + UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { + // We are at start of the block. + return nullptr; + } + + // We are at start of non-block container + return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + *aStartPoint.template ContainerAs<Element>(), aOptions, + PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck), + aAncestorLimiter); + } + + for (nsIContent* previousContent = aStartPoint.GetPreviousSiblingOfChild(); + previousContent; + previousContent = previousContent->GetPreviousSibling()) { + // We have a prior node. If it's a block, return it. + if (HTMLEditUtils::IsBlockElement( + *previousContent, + UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { + return previousContent; + } + LeafNodeType type = HTMLEditUtils::GetLeafNodeType( + *previousContent, aOptions, aBlockInlineCheck, IgnoreChildren::No); + if (type == LeafNodeType::Leaf) { + return previousContent; + } + if (type == LeafNodeType::Ignore) { + continue; + } + if (nsIContent* const lastLeaf = HTMLEditUtils::GetLastLeafContent( + *previousContent, aOptions, + PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { + return lastLeaf; + } + // previousContent has some nodes, but does not have meaningful nodes. + // Therefore, we can treat it as empty. + type = HTMLEditUtils::GetLeafNodeType( + *previousContent, aOptions, aBlockInlineCheck, IgnoreChildren::Yes); + if (type == LeafNodeType::Leaf) { + return previousContent; + } + MOZ_ASSERT(type == LeafNodeType::Ignore); + } + return nullptr; +} + template <typename EditorLineBreakType> Maybe<EditorLineBreakType> HTMLEditUtils::GetUnnecessaryLineBreak( const Element& aBlockElement, ScanLineBreak aScanLineBreak) { @@ -1101,8 +1661,7 @@ Maybe<EditorLineBreakType> HTMLEditUtils::GetUnnecessaryLineBreak( WalkTreeOption::StopAtBlockBoundary}; for (nsIContent* content = aScanLineBreak == ScanLineBreak::AtEndOfBlock - ? HTMLEditUtils::GetLastLeafContent( - aBlockElement, {LeafNodeType::OnlyLeafNode}) + ? HTMLEditUtils::GetLastLeafContent(aBlockElement, {}) : HTMLEditUtils::GetPreviousContent( aBlockElement, onlyPrecedingLine, BlockInlineCheck::UseComputedDisplayStyle, @@ -1111,8 +1670,7 @@ Maybe<EditorLineBreakType> HTMLEditUtils::GetUnnecessaryLineBreak( content = aScanLineBreak == ScanLineBreak::AtEndOfBlock ? HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *content, {LeafNodeType::OnlyLeafNode}, - BlockInlineCheck::UseComputedDisplayStyle, + *content, {}, BlockInlineCheck::UseComputedDisplayStyle, &aBlockElement) : HTMLEditUtils::GetPreviousContent( *content, onlyPrecedingLine, @@ -1184,11 +1742,12 @@ Maybe<EditorLineBreakType> HTMLEditUtils::GetUnnecessaryLineBreak( BlockInlineCheck::UseComputedDisplayStyle); for (nsIContent* content = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *lastLineBreakContent, {LeafNodeType::LeafNodeOrChildBlock}, + *lastLineBreakContent, + {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, blockElement); content; content = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *content, {LeafNodeType::LeafNodeOrChildBlock}, + *content, {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, blockElement)) { if (HTMLEditUtils::IsBlockElement( *content, BlockInlineCheck::UseComputedDisplayStyle) || @@ -2029,9 +2588,9 @@ nsIContent* HTMLEditUtils::GetPreviousContent( // and want the deep-right child. nsIContent* lastLeafContent = HTMLEditUtils::GetLastLeafContent( *aPoint.GetContainer(), - {aOptions.contains(WalkTreeOption::StopAtBlockBoundary) - ? LeafNodeType::LeafNodeOrChildBlock - : LeafNodeType::OnlyLeafNode}, + aOptions.contains(WalkTreeOption::StopAtBlockBoundary) + ? LeafNodeOptions{LeafNodeOption::TreatChildBlockAsLeafNode} + : LeafNodeOptions{}, aBlockInlineCheck); if (!lastLeafContent) { return nullptr; @@ -2078,9 +2637,9 @@ nsIContent* HTMLEditUtils::GetNextContent( nsIContent* firstLeafContent = HTMLEditUtils::GetFirstLeafContent( *point.GetChild(), - {aOptions.contains(WalkTreeOption::StopAtBlockBoundary) - ? LeafNodeType::LeafNodeOrChildBlock - : LeafNodeType::OnlyLeafNode}, + aOptions.contains(WalkTreeOption::StopAtBlockBoundary) + ? LeafNodeOptions{LeafNodeOption::TreatChildBlockAsLeafNode} + : LeafNodeOptions{}, aBlockInlineCheck); if (!firstLeafContent) { return point.GetChild(); @@ -2146,15 +2705,18 @@ nsIContent* HTMLEditUtils::GetAdjacentLeafContent( // don't look inside previous sibling, since it is a block return sibling; } - const LeafNodeTypes leafNodeTypes = { - aOptions.contains(WalkTreeOption::StopAtBlockBoundary) - ? LeafNodeType::LeafNodeOrChildBlock - : LeafNodeType::OnlyLeafNode}; + LeafNodeOptions leafNodeOptions; + if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary)) { + leafNodeOptions += LeafNodeOption::TreatChildBlockAsLeafNode; + } + if (aOptions.contains(WalkTreeOption::IgnoreNonEditableNode)) { + leafNodeOptions += LeafNodeOption::IgnoreNonEditableNode; + } nsIContent* leafContent = aWalkTreeDirection == WalkTreeDirection::Forward - ? HTMLEditUtils::GetFirstLeafContent(*sibling, leafNodeTypes, + ? HTMLEditUtils::GetFirstLeafContent(*sibling, leafNodeOptions, aBlockInlineCheck) - : HTMLEditUtils::GetLastLeafContent(*sibling, leafNodeTypes, + : HTMLEditUtils::GetLastLeafContent(*sibling, leafNodeOptions, aBlockInlineCheck); return leafContent ? leafContent : sibling; } @@ -3341,6 +3903,7 @@ std::ostream& operator<<(std::ostream& aStream, "ReturnAncestorLimiterIfNoProperAncestor", "EditableElement", }; + MOZ_ASSERT(static_cast<uint32_t>(aType) < std::size(names)); return aStream << names[static_cast<uint32_t>(aType)]; } @@ -3368,6 +3931,7 @@ std::ostream& operator<<(std::ostream& aStream, "StopAtTableElement", "StopAtAnyTableElement", }; + MOZ_ASSERT(static_cast<uint32_t>(aOption) < std::size(names)); return aStream << names[static_cast<uint32_t>(aOption)]; } @@ -3396,6 +3960,7 @@ std::ostream& operator<<(std::ostream& aStream, "TreatCommentAsVisible", "SafeToAskLayout", }; + MOZ_ASSERT(static_cast<uint32_t>(aOption) < std::size(names)); return aStream << names[static_cast<uint32_t>(aOption)]; } @@ -3414,22 +3979,27 @@ std::ostream& operator<<(std::ostream& aStream, } std::ostream& operator<<(std::ostream& aStream, - const HTMLEditUtils::LeafNodeType& aLeafNodeType) { + const HTMLEditUtils::LeafNodeOption& aOption) { constexpr static const char* names[] = { - "OnlyLeafNode", - "LeafNodeOrChildBlock", - "LeafNodeOrNonEditableNode", - "OnlyEditableLeafNode", + "TreatChildBlockAsLeafNode", + "TreatNonEditableNodeAsLeafNode", + "IgnoreNonEditableNode", "TreatCommentAsLeafNode", + "IgnoreEmptyText", + "IgnoreInvisibleText", + "IgnoreInvisibleInlineVoidElements", + "IgnoreAnyEmptyInlineContainers", + "IgnoreInvisibleEmptyInlineContainers", }; - return aStream << names[static_cast<uint32_t>(aLeafNodeType)]; + MOZ_ASSERT(static_cast<uint32_t>(aOption) < std::size(names)); + return aStream << names[static_cast<uint32_t>(aOption)]; } std::ostream& operator<<(std::ostream& aStream, - const HTMLEditUtils::LeafNodeTypes& aLeafNodeTypes) { + const HTMLEditUtils::LeafNodeOptions& aOptions) { aStream << "{"; bool first = true; - for (const auto t : aLeafNodeTypes) { + for (const auto t : aOptions) { if (!first) { aStream << ", "; } diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h @@ -1422,221 +1422,86 @@ class HTMLEditUtils final { return editableContent; } - enum class LeafNodeType { - // Even if there is a child block, keep scanning a leaf content in it. - OnlyLeafNode, - // If there is a child block, return it too. Note that this does not - // mean that block siblings are not treated as leaf nodes. - LeafNodeOrChildBlock, - // If there is a non-editable element if and only if scanning from editable - // node, return it too. - LeafNodeOrNonEditableNode, - // Ignore non-editable content at walking the tree. - OnlyEditableLeafNode, - // Treat `Comment` nodes are empty leaf nodes. + enum class LeafNodeOption { + // Treat a block element as a leaf node. + TreatChildBlockAsLeafNode, + // Treat a non-editable node as a leaf node. + TreatNonEditableNodeAsLeafNode, + // Ignore non-editable content. + IgnoreNonEditableNode, + // Treat a `Comment` node as a significant leaf node. TreatCommentAsLeafNode, + // Ignore empty `Text` node. + IgnoreEmptyText, + // Ignore invisible `Text` node such as empty node or all data is collapsed. + IgnoreInvisibleText, + // Ignore invisible void elements such as <wbr> and <input type="hidden">. + IgnoreInvisibleInlineVoidElements, + // If set, ignore empty inline containers such as <span></span>. + IgnoreAnyEmptyInlineContainers, + // If set, ignore empty inline containers which is not visible. E.g., + // <span></span> is not ignored but <span style="border:1px solid"></span> + // and <span style="border:padding 1px"></span> are not ignored. + // XXX Currently, this does not work well if the inline container has only + // `::before` and/or `::after` content and the frame is dirty. + IgnoreInvisibleEmptyInlineContainers, }; - using LeafNodeTypes = EnumSet<LeafNodeType>; + using LeafNodeOptions = EnumSet<LeafNodeOption>; friend std::ostream& operator<<(std::ostream& aStream, - const LeafNodeType& aLeafNodeType); + const LeafNodeOption& aOption); friend std::ostream& operator<<(std::ostream& aStream, - const LeafNodeTypes& aLeafNodeTypes); + const LeafNodeOptions& aOptions); + + private: + enum class IgnoreChildren : bool { No, Yes }; + enum class LeafNodeType { + NonEmptyContainer, + Leaf, + Ignore, + }; + [[nodiscard]] static LeafNodeType GetLeafNodeType( + const nsIContent& aContent, const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck, IgnoreChildren aIgnoreChildren); + public: /** * GetLastLeafContent() returns rightmost leaf content in aNode. It depends - * on aLeafNodeTypes whether this which types of nodes are treated as leaf + * on aOptions whether this which types of nodes are treated as leaf * nodes. * - * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain - * LeafNodeOrCHildBlock. + * @param aBlockInlineCheck Can be Unused if aOptions does not contain + * TreatChildBlockAsLeafNode. */ - static nsIContent* GetLastLeafContent( - const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes, - BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused, - const Element* aAncestorLimiter = nullptr) { - MOZ_ASSERT_IF( - aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), - !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); - // editor shouldn't touch child nodes which are replaced with native - // anonymous nodes. - if (aNode.IsElement() && - HTMLEditUtils::IsNeverElementContentsEditableByUser( - *aNode.AsElement())) { - return nullptr; - } - for (nsIContent* content = aNode.GetLastChild(); content;) { - if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) && - !EditorUtils::IsEditableContent(*content, - EditorUtils::EditorType::HTML)) { - content = HTMLEditUtils::GetPreviousContent( - *content, {WalkTreeOption::IgnoreNonEditableNode}, - aBlockInlineCheck, aAncestorLimiter); - continue; - } - if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && - content->IsComment()) { - content = content->GetPreviousSibling(); - continue; - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && - HTMLEditUtils::IsBlockElement( - *content, - UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { - return content; - } - if (!content->HasChildren() || - HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) { - return content; - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && - !HTMLEditUtils::IsSimplyEditableNode(*content)) { - return content; - } - content = content->GetLastChild(); - } - return nullptr; - } + [[nodiscard]] static nsIContent* GetLastLeafContent( + const nsINode& aNode, const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused); /** * GetFirstLeafContent() returns leftmost leaf content in aNode. It depends - * on aLeafNodeTypes whether this scans into a block child or treat block as a + * on aOptions whether this scans into a block child or treat block as a * leaf. * - * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain - * LeafNodeOrCHildBlock. + * @param aBlockInlineCheck Can be Unused if aOptions does not contain + * TreatChildBlockAsLeafNode. */ - static nsIContent* GetFirstLeafContent( - const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes, - BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused, - const Element* aAncestorLimiter = nullptr) { - MOZ_ASSERT_IF( - aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), - !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); - // editor shouldn't touch child nodes which are replaced with native - // anonymous nodes. - if (aNode.IsElement() && - HTMLEditUtils::IsNeverElementContentsEditableByUser( - *aNode.AsElement())) { - return nullptr; - } - for (nsIContent* content = aNode.GetFirstChild(); content;) { - if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) && - !EditorUtils::IsEditableContent(*content, - EditorUtils::EditorType::HTML)) { - content = HTMLEditUtils::GetNextContent( - *content, {WalkTreeOption::IgnoreNonEditableNode}, - aBlockInlineCheck, aAncestorLimiter); - continue; - } - if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && - content->IsComment()) { - content = content->GetNextSibling(); - continue; - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && - HTMLEditUtils::IsBlockElement( - *content, - UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { - return content; - } - if (!content->HasChildren() || - HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) { - return content; - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && - !HTMLEditUtils::IsSimplyEditableNode(*content)) { - return content; - } - content = content->GetFirstChild(); - } - return nullptr; - } + [[nodiscard]] static nsIContent* GetFirstLeafContent( + const nsINode& aNode, const LeafNodeOptions& aOptions, + BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused); /** * GetNextLeafContentOrNextBlockElement() returns next leaf content or * next block element of aStartContent inside aAncestorLimiter. * * @param aStartContent The start content to scan next content. - * @param aLeafNodeTypes See LeafNodeType. + * @param aOptions See LeafNodeOption. * @param aAncestorLimiter Optional, if you set this, it must be an * inclusive ancestor of aStartContent. */ static nsIContent* GetNextLeafContentOrNextBlockElement( - const nsIContent& aStartContent, const LeafNodeTypes& aLeafNodeTypes, + const nsIContent& aStartContent, const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, - const Element* aAncestorLimiter = nullptr) { - MOZ_ASSERT_IF( - aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), - !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); - - if (&aStartContent == aAncestorLimiter) { - return nullptr; - } - - Element* container = aStartContent.GetParentElement(); - for (nsIContent* nextContent = aStartContent.GetNextSibling();;) { - if (!nextContent) { - if (!container) { - NS_WARNING("Reached orphan node while climbing up the DOM tree"); - return nullptr; - } - for (Element* parentElement : - container->InclusiveAncestorsOfType<Element>()) { - if (parentElement == aAncestorLimiter || - HTMLEditUtils::IsBlockElement( - *parentElement, - UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { - return nullptr; - } - if (aLeafNodeTypes.contains( - LeafNodeType::LeafNodeOrNonEditableNode) && - !parentElement->IsEditable()) { - return nullptr; - } - nextContent = parentElement->GetNextSibling(); - if (nextContent) { - container = nextContent->GetParentElement(); - break; - } - if (!parentElement->GetParentElement()) { - NS_WARNING("Reached orphan node while climbing up the DOM tree"); - return nullptr; - } - } - MOZ_ASSERT(nextContent); - } - - if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && - nextContent->IsComment()) { - nextContent = nextContent->GetNextSibling(); - continue; - } - - // We have a next content. If it's a block, return it. - if (HTMLEditUtils::IsBlockElement( - *nextContent, - PreferDisplayOutsideIfUsingDisplay( - UseComputedDisplayStyleIfAuto(aBlockInlineCheck)))) { - return nextContent; - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && - !nextContent->IsEditable()) { - return nextContent; - } - if (HTMLEditUtils::IsContainerNode(*nextContent)) { - // Else if it's a container, get deep leftmost child - if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent( - *nextContent, aLeafNodeTypes, aBlockInlineCheck)) { - return child; - } - } - // Else return the next content itself. - return nextContent; - } - MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( - "Must return from the preceding for-loop"); - } + const Element* aAncestorLimiter = nullptr); /** * Similar to the above method, but take a DOM point to specify scan start @@ -1645,77 +1510,8 @@ class HTMLEditUtils final { template <typename PT, typename CT> static nsIContent* GetNextLeafContentOrNextBlockElement( const EditorDOMPointBase<PT, CT>& aStartPoint, - const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck, - const Element* aAncestorLimiter = nullptr) { - MOZ_ASSERT(aStartPoint.IsSet()); - MOZ_ASSERT_IF( - aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), - !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); - NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), - "Not implemented yet"); - - if (!aStartPoint.IsInContentNode()) { - return nullptr; - } - if (aStartPoint.IsInTextNode()) { - return HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *aStartPoint.template ContainerAs<Text>(), aLeafNodeTypes, - aBlockInlineCheck, aAncestorLimiter); - } - if (!HTMLEditUtils::IsContainerNode( - *aStartPoint.template ContainerAs<nsIContent>())) { - return HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes, - aBlockInlineCheck, aAncestorLimiter); - } - - for (nsIContent* nextContent = aStartPoint.GetChild();;) { - if (!nextContent) { - if (aStartPoint.GetContainer() == aAncestorLimiter || - HTMLEditUtils::IsBlockElement( - *aStartPoint.template ContainerAs<nsIContent>(), - UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { - // We are at end of the block. - return nullptr; - } - - // We are at end of non-block container - return HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes, - PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck), - aAncestorLimiter); - } - - if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && - nextContent->IsComment()) { - nextContent = nextContent->GetNextSibling(); - continue; - } - - // We have a next node. If it's a block, return it. - if (HTMLEditUtils::IsBlockElement( - *nextContent, - UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { - return nextContent; - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && - !HTMLEditUtils::IsSimplyEditableNode(*nextContent)) { - return nextContent; - } - if (HTMLEditUtils::IsContainerNode(*nextContent)) { - // else if it's a container, get deep leftmost child - if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent( - *nextContent, aLeafNodeTypes, - PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { - return child; - } - } - // Else return the node itself - return nextContent; - } - MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( - "Must return from the preceding for-loop"); - } + const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, + const Element* aAncestorLimiter = nullptr); /** * GetPreviousLeafContentOrPreviousBlockElement() returns previous leaf @@ -1723,88 +1519,14 @@ class HTMLEditUtils final { * aAncestorLimiter. * * @param aStartContent The start content to scan previous content. - * @param aLeafNodeTypes See LeafNodeType. + * @param aOptions See LeafNodeOption. * @param aAncestorLimiter Optional, if you set this, it must be an * inclusive ancestor of aStartContent. */ static nsIContent* GetPreviousLeafContentOrPreviousBlockElement( - const nsIContent& aStartContent, const LeafNodeTypes& aLeafNodeTypes, + const nsIContent& aStartContent, const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, - const Element* aAncestorLimiter = nullptr) { - MOZ_ASSERT_IF( - aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), - !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); - NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), - "Not implemented yet"); - - if (&aStartContent == aAncestorLimiter) { - return nullptr; - } - - Element* container = aStartContent.GetParentElement(); - for (nsIContent* previousContent = aStartContent.GetPreviousSibling();;) { - if (!previousContent) { - if (!container) { - NS_WARNING("Reached orphan node while climbing up the DOM tree"); - return nullptr; - } - for (Element* parentElement : - container->InclusiveAncestorsOfType<Element>()) { - if (parentElement == aAncestorLimiter || - HTMLEditUtils::IsBlockElement( - *parentElement, - UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { - return nullptr; - } - if (aLeafNodeTypes.contains( - LeafNodeType::LeafNodeOrNonEditableNode) && - !parentElement->IsEditable()) { - return nullptr; - } - previousContent = parentElement->GetPreviousSibling(); - if (previousContent) { - container = previousContent->GetParentElement(); - break; - } - if (!parentElement->GetParentElement()) { - NS_WARNING("Reached orphan node while climbing up the DOM tree"); - return nullptr; - } - } - MOZ_ASSERT(previousContent); - } - - if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && - previousContent->IsComment()) { - previousContent = previousContent->GetPreviousSibling(); - continue; - } - - // We have a next content. If it's a block, return it. - if (HTMLEditUtils::IsBlockElement( - *previousContent, - PreferDisplayOutsideIfUsingDisplay( - UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck)))) { - return previousContent; - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && - !HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { - return previousContent; - } - if (HTMLEditUtils::IsContainerNode(*previousContent)) { - // Else if it's a container, get deep rightmost child - if (nsIContent* child = HTMLEditUtils::GetLastLeafContent( - *previousContent, aLeafNodeTypes, - PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { - return child; - } - } - // Else return the next content itself. - return previousContent; - } - MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( - "Must return from the preceding for-loop"); - } + const Element* aAncestorLimiter = nullptr); /** * Similar to the above method, but take a DOM point to specify scan start @@ -1813,168 +1535,10 @@ class HTMLEditUtils final { template <typename PT, typename CT> static nsIContent* GetPreviousLeafContentOrPreviousBlockElement( const EditorDOMPointBase<PT, CT>& aStartPoint, - const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck, - const Element* aAncestorLimiter = nullptr) { - MOZ_ASSERT(aStartPoint.IsSet()); - MOZ_ASSERT_IF( - aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), - !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); - NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), - "Not implemented yet"); - - if (!aStartPoint.IsInContentNode()) { - return nullptr; - } - if (aStartPoint.IsInTextNode()) { - return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *aStartPoint.template ContainerAs<Text>(), aLeafNodeTypes, - aBlockInlineCheck, aAncestorLimiter); - } - if (!HTMLEditUtils::IsContainerNode( - *aStartPoint.template ContainerAs<nsIContent>())) { - return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes, - aBlockInlineCheck, aAncestorLimiter); - } - - if (aStartPoint.IsStartOfContainer()) { - if (aStartPoint.GetContainer() == aAncestorLimiter || - HTMLEditUtils::IsBlockElement( - *aStartPoint.template ContainerAs<nsIContent>(), - UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { - // We are at start of the block. - return nullptr; - } - - // We are at start of non-block container - return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes, - PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck), - aAncestorLimiter); - } - - for (nsIContent* previousContent = aStartPoint.GetPreviousSiblingOfChild(); - previousContent && - (aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) || - !previousContent->IsComment()); - previousContent = previousContent->GetPreviousSibling()) { - // We have a prior node. If it's a block, return it. - if (HTMLEditUtils::IsBlockElement( - *previousContent, - UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { - return previousContent; - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && - !HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { - return previousContent; - } - if (HTMLEditUtils::IsContainerNode(*previousContent)) { - // Else if it's a container, get deep rightmost child - if (nsIContent* child = HTMLEditUtils::GetLastLeafContent( - *previousContent, aLeafNodeTypes, - PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { - return child; - } - } - // Else return the node itself - return previousContent; - } - return nullptr; - } - - /** - * Return previous non-empty leaf content or child block or non-editable - * content (depending on aLeafNodeTypes). This ignores invisible inline leaf - * element like `<b></b>` and empty `Text` nodes. So, this may return - * invisible `Text` node, but it may be useful to consider whether we need to - * insert a padding <br> element. - */ - [[nodiscard]] static nsIContent* - GetPreviousNonEmptyLeafContentOrPreviousBlockElement( - const nsIContent& aContent, const LeafNodeTypes& aLeafNodeTypes, - BlockInlineCheck aBlockInlineCheck, - const Element* aAncestorLimiter = nullptr) { - for (nsIContent* previousContent = - HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - aContent, aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter); - previousContent; - previousContent = - HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *previousContent, aLeafNodeTypes, aBlockInlineCheck, - aAncestorLimiter)) { - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && - HTMLEditUtils::IsBlockElement( - *previousContent, - PreferDisplayOutsideIfUsingDisplay( - UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck)))) { - return previousContent; // Reached block element - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && - HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { - return previousContent; // Reached non-editable content - } - Text* const previousText = Text::FromNode(previousContent); - if (!previousText) { - if (!HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) { - continue; // Ignore invisible inline elements - } - return previousContent; // Reached visible inline element - } - if (!previousText->TextDataLength()) { - continue; // Ignore empty Text nodes. - } - return previousText; // Reached non-empty text - } - return nullptr; - } + const LeafNodeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, + const Element* aAncestorLimiter = nullptr); /** - * Return previous visible leaf content or child block or non-editable content - * (depending on aLeafNodeTypes). This ignores invisible inline leaf element - * like `<b></b>` and empty `Text` nodes. So, this may return invisible - * `Text` node, but it may be useful to consider whether we need to insert a - * padding <br> element. - */ - template <typename PT, typename CT> - [[nodiscard]] static nsIContent* - GetPreviousNonEmptyLeafContentOrPreviousBlockElement( - const EditorDOMPointBase<PT, CT>& aPoint, - const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck, - const Element* aAncestorLimiter = nullptr) { - for (nsIContent* previousContent = - HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - aPoint, aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter); - previousContent; - previousContent = - HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *previousContent, aLeafNodeTypes, aBlockInlineCheck, - aAncestorLimiter)) { - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && - HTMLEditUtils::IsBlockElement( - *previousContent, - PreferDisplayOutsideIfUsingDisplay( - UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck)))) { - return previousContent; // Reached block element - } - if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && - HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { - return previousContent; // Reached non-editable content - } - Text* const previousText = Text::FromNode(previousContent); - if (!previousText) { - if (!HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) { - continue; // Ignore invisible inline elements - } - return previousContent; // Reached visible inline element - } - if (!previousText->TextDataLength()) { - continue; // Ignore empty Text nodes. - } - return previousText; // Reached non-empty text - } - return nullptr; - } - /** * Returns a content node whose inline styles should be preserved after * deleting content in a range. Typically, you should set aPoint to start * boundary of the range to delete. @@ -2470,8 +2034,7 @@ class HTMLEditUtils final { template <typename EditorLineBreakType> static Maybe<EditorLineBreakType> GetFirstLineBreak( const dom::Element& aElement) { - for (nsIContent* content = HTMLEditUtils::GetFirstLeafContent( - aElement, {LeafNodeType::OnlyLeafNode}); + for (nsIContent* content = HTMLEditUtils::GetFirstLeafContent(aElement, {}); content; content = HTMLEditUtils::GetNextContent( *content, {WalkTreeOption::IgnoreDataNodeExceptText, diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp @@ -100,8 +100,8 @@ using namespace widget; LazyLogModule gHTMLEditorFocusLog("HTMLEditorFocus"); using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; -using LeafNodeType = HTMLEditUtils::LeafNodeType; -using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; +using LeafNodeOption = HTMLEditUtils::LeafNodeOption; +using LeafNodeOptions = HTMLEditUtils::LeafNodeOptions; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; // Some utilities to handle overloading of "A" tag for link and named anchor. @@ -1041,8 +1041,8 @@ nsresult HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() const { } auto pointToPutCaret = [&]() -> EditorRawDOMPoint { - nsCOMPtr<nsIContent> lastLeafContent = HTMLEditUtils::GetLastLeafContent( - *bodyOrDocumentElement, {LeafNodeType::OnlyLeafNode}); + nsCOMPtr<nsIContent> lastLeafContent = + HTMLEditUtils::GetLastLeafContent(*bodyOrDocumentElement, {}); if (!lastLeafContent) { return EditorRawDOMPoint::AtEndOf(*bodyOrDocumentElement); } @@ -1136,11 +1136,15 @@ nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode( } } + constexpr LeafNodeOptions leafNodeOptions = { + LeafNodeOption::TreatNonEditableNodeAsLeafNode, + LeafNodeOption::TreatChildBlockAsLeafNode, + // FIXME: Ignore empty inline containers such as <span></span> because we + // cannot visually put caret into it. + }; for (nsIContent* leafContent = HTMLEditUtils::GetFirstLeafContent( - *editingHost, - {LeafNodeType::LeafNodeOrNonEditableNode, - LeafNodeType::LeafNodeOrChildBlock}, - BlockInlineCheck::UseComputedDisplayStyle, editingHost); + *editingHost, leafNodeOptions, + BlockInlineCheck::UseComputedDisplayStyle); leafContent;) { // If we meet a non-editable node first, we should move caret to start // of the container block or editing host. @@ -1177,9 +1181,7 @@ nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode( // Chromium collapses selection to start of the editing host when this // is the last leaf content. So, we don't need special handling here. leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *leafElement, - {LeafNodeType::LeafNodeOrNonEditableNode, - LeafNodeType::LeafNodeOrChildBlock}, + *leafElement, leafNodeOptions, BlockInlineCheck::UseComputedDisplayStyle, editingHost); continue; } @@ -1203,9 +1205,7 @@ nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode( } // If it's an invisible text node, keep scanning next leaf. leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *leafContent, - {LeafNodeType::LeafNodeOrNonEditableNode, - LeafNodeType::LeafNodeOrChildBlock}, + *leafContent, leafNodeOptions, BlockInlineCheck::UseComputedDisplayStyle, editingHost); continue; } @@ -1240,19 +1240,15 @@ nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode( EmptyCheckOption::TreatNonEditableContentAsInvisible}) && !HTMLEditUtils::IsNeverElementContentsEditableByUser(*leafContent)) { leafContent = HTMLEditUtils::GetFirstLeafContent( - *leafContent, - {LeafNodeType::LeafNodeOrNonEditableNode, - LeafNodeType::LeafNodeOrChildBlock}, - BlockInlineCheck::UseComputedDisplayStyle, editingHost); + *leafContent, leafNodeOptions, + BlockInlineCheck::UseComputedDisplayStyle); continue; } // Otherwise, we must meet an empty block element or a data node like // comment node. Let's ignore it. leafContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *leafContent, - {LeafNodeType::LeafNodeOrNonEditableNode, - LeafNodeType::LeafNodeOrChildBlock}, + *leafContent, leafNodeOptions, BlockInlineCheck::UseComputedDisplayStyle, editingHost); } @@ -3306,7 +3302,9 @@ already_AddRefed<Element> HTMLEditor::GetSelectedElement(const nsAtom* aTagName, return nullptr; } nsIContent* firstEditableLeaf = HTMLEditUtils::GetFirstLeafContent( - *nextSibling, {LeafNodeType::OnlyLeafNode}); + *nextSibling, + // XXX Should we ignore invisible inline elements and text nodes? + {}); if (firstEditableLeaf && firstEditableLeaf->IsHTMLElement(nsGkAtoms::br)) { return nullptr; diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp @@ -97,7 +97,7 @@ namespace mozilla { using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; -using LeafNodeType = HTMLEditUtils::LeafNodeType; +using LeafNodeOption = HTMLEditUtils::LeafNodeOption; #define kInsertCookie "_moz_Insert Here_moz_" @@ -506,9 +506,13 @@ HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML( if (!aLastInsertedPoint.GetChild() || !aLastInsertedPoint.GetChild()->IsHTMLElement(nsGkAtoms::table)) { containerContent = HTMLEditUtils::GetLastLeafContent( - *aLastInsertedPoint.GetChild(), {LeafNodeType::OnlyEditableLeafNode}, - BlockInlineCheck::Unused, - aLastInsertedPoint.GetChild()->GetAsElementOrParentElement()); + *aLastInsertedPoint.GetChild(), + { + LeafNodeOption::IgnoreNonEditableNode, + LeafNodeOption::IgnoreInvisibleText, + // FIXME: We cannot visually put caret into empty inline containers + // like <span></span> so that let's ignore them. + }); if (containerContent) { Element* mostDistantInclusiveAncestorTableElement = nullptr; for (Element* maybeTableElement = diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -60,7 +60,7 @@ using EditablePointOption = HTMLEditUtils::EditablePointOption; using EditablePointOptions = HTMLEditUtils::EditablePointOptions; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using InvisibleWhiteSpaces = HTMLEditUtils::InvisibleWhiteSpaces; -using LeafNodeType = HTMLEditUtils::LeafNodeType; +using LeafNodeOption = HTMLEditUtils::LeafNodeOption; using ScanLineBreak = HTMLEditUtils::ScanLineBreak; using TableBoundary = HTMLEditUtils::TableBoundary; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; @@ -1958,11 +1958,10 @@ nsIContent* HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: MOZ_ASSERT(mOtherBlockElement); return aDirectionAndAmount == nsIEditor::ePrevious ? HTMLEditUtils::GetLastLeafContent( - *mOtherBlockElement, {LeafNodeType::OnlyEditableLeafNode}, - BlockInlineCheck::Unused, mOtherBlockElement) + *mOtherBlockElement, {LeafNodeOption::IgnoreNonEditableNode}) : HTMLEditUtils::GetFirstLeafContent( - *mOtherBlockElement, {LeafNodeType::OnlyEditableLeafNode}, - BlockInlineCheck::Unused, mOtherBlockElement); + *mOtherBlockElement, + {LeafNodeOption::IgnoreNonEditableNode}); } nsresult HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: @@ -2552,15 +2551,15 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: return false; } - auto ScanJoinTarget = [&]() -> nsIContent* { + auto ScanJoinTarget = [&]() MOZ_NEVER_INLINE_DEBUG -> nsIContent* { nsIContent* targetContent = aDirectionAndAmount == nsIEditor::ePrevious ? HTMLEditUtils::GetPreviousContent( aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode}, - BlockInlineCheck::Unused, &aEditingHost) + BlockInlineCheck::Auto, &aEditingHost) : HTMLEditUtils::GetNextContent( aCurrentBlockElement, {WalkTreeOption::IgnoreNonEditableNode}, - BlockInlineCheck::Unused, &aEditingHost); + BlockInlineCheck::Auto, &aEditingHost); // If found content is an invisible text node, let's scan visible things. auto IsIgnorableDataNode = [](nsIContent* aContent) { return aContent && HTMLEditUtils::IsRemovableNode(*aContent) && @@ -2606,9 +2605,10 @@ bool HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner:: nsIContent* leafContent = aDirectionAndAmount == nsIEditor::ePrevious ? HTMLEditUtils::GetLastLeafContent( - *adjacentContent, {LeafNodeType::OnlyEditableLeafNode}) + *adjacentContent, {LeafNodeOption::IgnoreNonEditableNode}) : HTMLEditUtils::GetFirstLeafContent( - *adjacentContent, {LeafNodeType::OnlyEditableLeafNode}); + *adjacentContent, + {LeafNodeOption::IgnoreNonEditableNode}); mSkippedInvisibleContents.AppendElement(*targetContent); return leafContent ? leafContent : adjacentContent; } @@ -4619,9 +4619,11 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: Element::FromNodeOrNull(moveFirstLineResult.DeleteRangeRef() .GetClosestCommonInclusiveAncestor()); nsIContent* const previousVisibleLeafOrChildBlock = - HTMLEditUtils::GetPreviousNonEmptyLeafContentOrPreviousBlockElement( + HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( moveFirstLineResult.DeleteRangeRef().EndRef(), - {LeafNodeType::LeafNodeOrChildBlock}, + {LeafNodeOption::TreatChildBlockAsLeafNode, + LeafNodeOption::IgnoreInvisibleEmptyInlineContainers, + LeafNodeOption::IgnoreInvisibleText}, BlockInlineCheck::UseComputedDisplayOutsideStyle, commonAncestor); if (!previousVisibleLeafOrChildBlock) { return false; @@ -6371,7 +6373,7 @@ nsresult HTMLEditor::AutoMoveOneLineHandler:: mDestInclusiveAncestorBlock) : HTMLEditUtils::GetLastLeafContent( *mDestInclusiveAncestorBlock, - {LeafNodeType::LeafNodeOrNonEditableNode})); + {LeafNodeOption::TreatNonEditableNodeAsLeafNode})); if (!lastTextNode || !HTMLEditUtils::IsSimplyEditableNode(*lastTextNode)) { return nullptr; @@ -7211,7 +7213,7 @@ Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler:: EditorRawDOMPoint::After(mEmptyInclusiveAncestorBlockElement); scanStartPoint.IsInContentNode();) { nsIContent* const nextContent = HTMLEditUtils::GetNextContent( - scanStartPoint, {}, BlockInlineCheck::Unused, &aEditingHost); + scanStartPoint, {}, BlockInlineCheck::Auto, &aEditingHost); // Let's ignore invisible `Text`. if (nextContent && nextContent->IsText() && !HTMLEditUtils::IsVisibleTextNode(*nextContent->AsText())) { @@ -7249,7 +7251,7 @@ Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler:: scanStartPoint.IsInContentNode();) { nsIContent* const previousContent = HTMLEditUtils::GetPreviousContent( scanStartPoint, {WalkTreeOption::IgnoreNonEditableNode}, - BlockInlineCheck::Unused, &aEditingHost); + BlockInlineCheck::Auto, &aEditingHost); // Let's ignore invisible `Text`. if (previousContent && previousContent->IsText() && !HTMLEditUtils::IsVisibleTextNode(*previousContent->AsText())) { diff --git a/editor/libeditor/HTMLEditorInsertParagraphHandler.cpp b/editor/libeditor/HTMLEditorInsertParagraphHandler.cpp @@ -49,8 +49,8 @@ namespace mozilla { using namespace dom; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions; -using LeafNodeType = HTMLEditUtils::LeafNodeType; -using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; +using LeafNodeOption = HTMLEditUtils::LeafNodeOption; +using LeafNodeOptions = HTMLEditUtils::LeafNodeOptions; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; Result<EditActionResult, nsresult> @@ -1753,7 +1753,7 @@ HTMLEditor::AutoInsertParagraphHandler::SplitParagraphWithTransaction( // Let's put caret at start of the first leaf container. nsIContent* child = HTMLEditUtils::GetFirstLeafContent( - *rightDivOrParagraphElement, {LeafNodeType::LeafNodeOrChildBlock}, + *rightDivOrParagraphElement, {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle); if (MOZ_UNLIKELY(!child)) { return SplitNodeResult(std::move(splitDivOrPResult), diff --git a/editor/libeditor/HTMLStyleEditor.cpp b/editor/libeditor/HTMLStyleEditor.cpp @@ -64,8 +64,6 @@ using namespace dom; using EditablePointOption = HTMLEditUtils::EditablePointOption; using EditablePointOptions = HTMLEditUtils::EditablePointOptions; using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; -using LeafNodeType = HTMLEditUtils::LeafNodeType; -using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; template nsresult HTMLEditor::SetInlinePropertiesAsSubAction( @@ -2394,7 +2392,7 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::ClearStyleAt( // `<p><b><i>a</i></b><b><i></i></b><b><i>bc</i></b></p>`. // ^^^^^^^^^^^^^^ nsIContent* firstLeafChildOfNextNode = HTMLEditUtils::GetFirstLeafContent( - *unwrappedSplitNodeResult.GetNextContent(), {LeafNodeType::OnlyLeafNode}); + *unwrappedSplitNodeResult.GetNextContent(), {}); EditorDOMPoint atStartOfNextNode( firstLeafChildOfNextNode ? firstLeafChildOfNextNode : unwrappedSplitNodeResult.GetNextContent(), @@ -2484,8 +2482,7 @@ Result<EditorDOMPoint, nsresult> HTMLEditor::ClearStyleAt( // it was in next node of the first split. // E.g., `<p><b><i>a</i></b><b><i><br></i></b><b><i>bc</i></b></p>` nsIContent* firstLeafChildOfPreviousNode = HTMLEditUtils::GetFirstLeafContent( - *unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(), - {LeafNodeType::OnlyLeafNode}); + *unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(), {}); pointToPutCaret.Set( firstLeafChildOfPreviousNode ? firstLeafChildOfPreviousNode diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp @@ -10,8 +10,8 @@ #include "EditAction.h" #include "EditAggregateTransaction.h" #include "EditorDOMPoint.h" +#include "EditorUtils.h" #include "HTMLEditor.h" -#include "HTMLEditUtils.h" #include "InternetCiter.h" #include "PlaceholderTransaction.h" #include "gfxFontUtils.h" @@ -89,9 +89,6 @@ static void LogOrWarn(const TextEditor* aTextEditor, LazyLogModule& aLog, using namespace dom; -using LeafNodeType = HTMLEditUtils::LeafNodeType; -using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; - template EditorDOMPoint TextEditor::FindBetterInsertionPoint( const EditorDOMPoint& aPoint) const; template EditorRawDOMPoint TextEditor::FindBetterInsertionPoint( diff --git a/editor/libeditor/WSRunScanner.cpp b/editor/libeditor/WSRunScanner.cpp @@ -73,15 +73,17 @@ void WSScanResult::AssertIfInvalidData(const WSRunScanner& aScanner) const { mContent->IsHTMLElement(nsGkAtoms::br)); MOZ_ASSERT_IF(mReason == WSType::PreformattedLineBreak, EditorUtils::IsNewLinePreformatted(*mContent)); - MOZ_ASSERT_IF( - mReason == WSType::EmptyInlineContainerElement, - HTMLEditUtils::IsEmptyInlineContainer( - *mContent, - {HTMLEditUtils::EmptyCheckOption::TreatSingleBRElementAsVisible, - HTMLEditUtils::EmptyCheckOption::TreatBlockAsVisible}, - aScanner.ReferredHTMLDefaultStyle() - ? BlockInlineCheck::UseHTMLDefaultStyle - : BlockInlineCheck::UseComputedDisplayOutsideStyle)); + auto MaybeNonVoidEmptyInlineContainerElement = [&]() { + return HTMLEditUtils::IsInlineContent( + *mContent, + aScanner.ReferredHTMLDefaultStyle() + ? BlockInlineCheck::UseHTMLDefaultStyle + : BlockInlineCheck::UseComputedDisplayOutsideStyle) && + HTMLEditUtils::IsContainerNode(*mContent) && + !HTMLEditUtils::IsReplacedElement(*mContent->AsElement()); + }; + MOZ_ASSERT_IF(mReason == WSType::EmptyInlineContainerElement, + MaybeNonVoidEmptyInlineContainerElement()); MOZ_ASSERT_IF( mReason == WSType::SpecialContent, (mContent->IsText() && !mContent->IsEditable()) || @@ -90,14 +92,9 @@ void WSScanResult::AssertIfInvalidData(const WSRunScanner& aScanner) const { *mContent, aScanner.ReferredHTMLDefaultStyle() ? BlockInlineCheck::UseHTMLDefaultStyle - : BlockInlineCheck::UseComputedDisplayOutsideStyle) && - !HTMLEditUtils::IsEmptyInlineContainer( - *mContent, - {HTMLEditUtils::EmptyCheckOption::TreatSingleBRElementAsVisible, - HTMLEditUtils::EmptyCheckOption::TreatBlockAsVisible}, - aScanner.ReferredHTMLDefaultStyle() - ? BlockInlineCheck::UseHTMLDefaultStyle - : BlockInlineCheck::UseComputedDisplayOutsideStyle))); + : BlockInlineCheck::UseComputedDisplayOutsideStyle)) && + (!mContent->IsEditable() || + !MaybeNonVoidEmptyInlineContainerElement())); MOZ_ASSERT_IF( mReason == WSType::OtherBlockBoundary, HTMLEditUtils::IsBlockElement( diff --git a/editor/libeditor/WSRunScanner.h b/editor/libeditor/WSRunScanner.h @@ -526,6 +526,14 @@ class MOZ_STACK_CLASS WSRunScanner final { ReferHTMLDefaultStyle, // If set, stop scanning the DOM when it reaches a `Comment` node. StopAtComment, + // If set, ignore empty inline containers such as <span></span>. + IgnoreEmptyInlineContainers, + // If set, ignore empty inline containers which is not visible. E.g., + // <span></span> is ignored but <span style="border:1px solid"></span> + // and <span style="border:padding 1px"></span> are not ignored. + // XXX Currently, this does not work well if the inline container has only + // `::before` and/or `::after` content and the frame is dirty. + IgnoreInvisibleInlines, }; using Options = EnumSet<Option>; @@ -552,6 +560,32 @@ class MOZ_STACK_CLASS WSRunScanner final { aOptions.contains(Option::ReferHTMLDefaultStyle)); } + private: + [[nodiscard]] static HTMLEditUtils::LeafNodeOptions ToLeafNodeOptions( + const Options& aOptions) { + using LeafNodeOption = HTMLEditUtils::LeafNodeOption; + using LeafNodeOptions = HTMLEditUtils::LeafNodeOptions; + auto types = + aOptions.contains(Option::OnlyEditableNodes) + ? LeafNodeOptions{LeafNodeOption::TreatNonEditableNodeAsLeafNode} + : LeafNodeOptions{}; + if (aOptions.contains(Option::StopAtComment)) { + types += LeafNodeOption::TreatCommentAsLeafNode; + } + if (aOptions.contains(Option::IgnoreInvisibleInlines)) { + types += + LeafNodeOptions{LeafNodeOption::IgnoreInvisibleEmptyInlineContainers, + LeafNodeOption::IgnoreInvisibleInlineVoidElements, + LeafNodeOption::IgnoreInvisibleText}; + } + if (aOptions.contains(Option::IgnoreEmptyInlineContainers)) { + types += LeafNodeOptions{LeafNodeOption::IgnoreAnyEmptyInlineContainers, + LeafNodeOption::IgnoreEmptyText}; + } + return types; + } + + public: template <typename EditorDOMPointType> WSRunScanner(Options aOptions, // NOLINT(performance-unnecessary-value-param) const EditorDOMPointType& aScanStartPoint, diff --git a/editor/libeditor/WSRunScannerNestedClasses.cpp b/editor/libeditor/WSRunScannerNestedClasses.cpp @@ -24,8 +24,8 @@ using namespace dom; using AncestorType = HTMLEditUtils::AncestorType; using AncestorTypes = HTMLEditUtils::AncestorTypes; -using LeafNodeType = HTMLEditUtils::LeafNodeType; -using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes; +using LeafNodeOption = HTMLEditUtils::LeafNodeOption; +using LeafNodeOptions = HTMLEditUtils::LeafNodeOptions; template WSRunScanner::TextFragmentData::TextFragmentData(Options, const EditorDOMPoint&, @@ -245,18 +245,10 @@ WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData:: ? BlockInlineCheck::UseHTMLDefaultStyle : BlockInlineCheck::Auto; // Then, we need to check previous leaf node. - const auto leafNodeTypes = [&]() -> LeafNodeTypes { - auto types = aOptions.contains(Option::OnlyEditableNodes) - ? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode} - : LeafNodeTypes{LeafNodeType::OnlyLeafNode}; - if (aOptions.contains(Option::StopAtComment)) { - types += LeafNodeType::TreatCommentAsLeafNode; - } - return types; - }(); + const LeafNodeOptions leafNodeOptions = ToLeafNodeOptions(aOptions); nsIContent* previousLeafContentOrBlock = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - aPoint, leafNodeTypes, blockInlineCheck, &aAncestorLimiter); + aPoint, leafNodeOptions, blockInlineCheck, &aAncestorLimiter); if (!previousLeafContentOrBlock) { // No previous content means that we reached the aAncestorLimiter boundary. return BoundaryData( @@ -274,29 +266,35 @@ WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData:: WSType::OtherBlockBoundary); } + if (previousLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)) { + // <br> + return BoundaryData(aPoint, *previousLeafContentOrBlock, WSType::BRElement); + } + + if (aOptions.contains(Option::OnlyEditableNodes) && + HTMLEditUtils::IsSimplyEditableNode(*previousLeafContentOrBlock) != + HTMLEditUtils::IsSimplyEditableNode(aAncestorLimiter)) { + // Non-editable nodes (assuming the start content is editable). + return BoundaryData(aPoint, *previousLeafContentOrBlock, + WSType::SpecialContent); + } + if (previousLeafContentOrBlock->IsElement() && - HTMLEditUtils::IsEmptyInlineContainer( + HTMLEditUtils::IsInlineContent( *previousLeafContentOrBlock, - {HTMLEditUtils::EmptyCheckOption::TreatSingleBRElementAsVisible, - HTMLEditUtils::EmptyCheckOption::TreatBlockAsVisible}, - UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck))) { - // Okay, it's an empty inline container. Basically, it's invisible but may - // be visible if it has non-zero padding/margin, etc. So, the scanner user - // may need to additional check. + UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) && + HTMLEditUtils::IsContainerNode(*previousLeafContentOrBlock) && + !HTMLEditUtils::IsReplacedElement( + *previousLeafContentOrBlock->AsElement())) { + // Empty inline containers such as <span></span> return BoundaryData(aPoint, *previousLeafContentOrBlock, WSType::EmptyInlineContainerElement); } - if (!previousLeafContentOrBlock->IsText() || - (aOptions.contains(Option::OnlyEditableNodes) && - HTMLEditUtils::IsSimplyEditableNode(*previousLeafContentOrBlock) != - HTMLEditUtils::IsSimplyEditableNode(aAncestorLimiter))) { - // it's a break or a special node, like <img>, that is not a block and - // not a break but still serves as a terminator to ws runs. + if (!previousLeafContentOrBlock->IsText()) { + // Other elements like <img> or #comment. return BoundaryData(aPoint, *previousLeafContentOrBlock, - previousLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br) - ? WSType::BRElement - : WSType::SpecialContent); + WSType::SpecialContent); } if (!previousLeafContentOrBlock->AsText()->TextLength()) { @@ -420,18 +418,10 @@ WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom( : BlockInlineCheck::Auto; // Then, we need to check next leaf node. - const auto leafNodeTypes = [&]() -> LeafNodeTypes { - auto types = aOptions.contains(Option::OnlyEditableNodes) - ? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode} - : LeafNodeTypes{LeafNodeType::OnlyLeafNode}; - if (aOptions.contains(Option::StopAtComment)) { - types += LeafNodeType::TreatCommentAsLeafNode; - } - return types; - }(); + const LeafNodeOptions leafNodeOptions = ToLeafNodeOptions(aOptions); nsIContent* nextLeafContentOrBlock = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - aPoint, leafNodeTypes, blockInlineCheck, &aAncestorLimiter); + aPoint, leafNodeOptions, blockInlineCheck, &aAncestorLimiter); if (!nextLeafContentOrBlock) { // No next content means that we reached aAncestorLimiter boundary. return BoundaryData( @@ -451,30 +441,34 @@ WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom( WSType::OtherBlockBoundary); } + if (nextLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)) { + // <br> + return BoundaryData(aPoint, *nextLeafContentOrBlock, WSType::BRElement); + } + + if (aOptions.contains(Option::OnlyEditableNodes) && + HTMLEditUtils::IsSimplyEditableNode(*nextLeafContentOrBlock) != + HTMLEditUtils::IsSimplyEditableNode(aAncestorLimiter)) { + // Non-editable nodes (assuming the start content is editable). + return BoundaryData(aPoint, *nextLeafContentOrBlock, + WSType::SpecialContent); + } + if (nextLeafContentOrBlock->IsElement() && - HTMLEditUtils::IsEmptyInlineContainer( + HTMLEditUtils::IsInlineContent( *nextLeafContentOrBlock, - {HTMLEditUtils::EmptyCheckOption::TreatSingleBRElementAsVisible, - HTMLEditUtils::EmptyCheckOption::TreatBlockAsVisible}, - UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck))) { - // Okay, it's an empty inline container. Basically, it's invisible but may - // be visible if it has non-zero padding/margin, etc. So, the scanner user - // may need to additional check. + UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) && + HTMLEditUtils::IsContainerNode(*nextLeafContentOrBlock) && + !HTMLEditUtils::IsReplacedElement(*nextLeafContentOrBlock->AsElement())) { + // Empty inline containers such as <span></span> return BoundaryData(aPoint, *nextLeafContentOrBlock, WSType::EmptyInlineContainerElement); } - if (!nextLeafContentOrBlock->IsText() || - (aOptions.contains(Option::OnlyEditableNodes) && - HTMLEditUtils::IsSimplyEditableNode(*nextLeafContentOrBlock) != - HTMLEditUtils::IsSimplyEditableNode(aAncestorLimiter))) { - // we encountered a break or a special node, like <img>, - // that is not a block and not a break but still - // serves as a terminator to ws runs. + if (!nextLeafContentOrBlock->IsText()) { + // Other elements like <img>, etc. return BoundaryData(aPoint, *nextLeafContentOrBlock, - nextLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br) - ? WSType::BRElement - : WSType::SpecialContent); + WSType::SpecialContent); } if (!nextLeafContentOrBlock->AsText()->DataBuffer().GetLength()) { @@ -907,7 +901,10 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( aOptions.contains(Option::ReferHTMLDefaultStyle) ? BlockInlineCheck::UseHTMLDefaultStyle : BlockInlineCheck::Auto; + 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; @@ -921,7 +918,9 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( if (!child || HTMLEditUtils::IsBlockElement( *child, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || - HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*child)) { + ((!aOptions.contains(Option::IgnoreEmptyInlineContainers) || + !HTMLEditUtils::IsContainerNode(*child)) && + HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*child))) { return aPoint.template To<EditorRawDOMPoint>(); } if (!child->HasChildNodes()) { @@ -935,14 +934,16 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( // block because end reason content should not be the other side of the // following block boundary. nsIContent* const leafContent = HTMLEditUtils::GetFirstLeafContent( - *child, {LeafNodeType::LeafNodeOrChildBlock}, blockInlineCheck); + *child, leafNodeOptions, blockInlineCheck); if (!leafContent) { return EditorRawDOMPoint(child, 0); } if (HTMLEditUtils::IsBlockElement( *leafContent, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || - HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent)) { + ((!aOptions.contains(Option::IgnoreEmptyInlineContainers) || + !HTMLEditUtils::IsContainerNode(*leafContent)) && + HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent))) { return EditorRawDOMPoint(); } return EditorRawDOMPoint(leafContent, 0); @@ -979,24 +980,14 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( return EditorDOMPointType(); } - const auto leafNodeTypes = [&]() -> LeafNodeTypes { - auto types = aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes - ? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode, - LeafNodeType::LeafNodeOrChildBlock) - : LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock); - if (aOptions.contains(Option::StopAtComment)) { - types += LeafNodeType::TreatCommentAsLeafNode; - } - return types; - }(); for (nsIContent* nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *point.ContainerAs<nsIContent>(), leafNodeTypes, + *point.ContainerAs<nsIContent>(), leafNodeOptions, blockInlineCheck, editableBlockElementOrInlineEditingHostOrNonEditableRootElement); nextContent; nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *nextContent, leafNodeTypes, blockInlineCheck, + *nextContent, leafNodeOptions, blockInlineCheck, editableBlockElementOrInlineEditingHostOrNonEditableRootElement)) { if (!nextContent->IsText() || (aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes && @@ -1005,7 +996,9 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( HTMLEditUtils::IsBlockElement( *nextContent, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || - HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*nextContent)) { + ((!aOptions.contains(Option::IgnoreEmptyInlineContainers) || + !HTMLEditUtils::IsContainerNode(*nextContent)) && + HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*nextContent))) { break; // Reached end of current runs. } continue; @@ -1032,7 +1025,10 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( aOptions.contains(Option::ReferHTMLDefaultStyle) ? BlockInlineCheck::UseHTMLDefaultStyle : BlockInlineCheck::Auto; + 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() @@ -1048,7 +1044,9 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( HTMLEditUtils::IsBlockElement( *previousChild, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || - HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousChild)) { + ((!aOptions.contains(Option::IgnoreEmptyInlineContainers) || + !HTMLEditUtils::IsContainerNode(*previousChild)) && + HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousChild))) { return aPoint.template To<EditorRawDOMPoint>(); } if (!previousChild->HasChildren()) { @@ -1063,14 +1061,16 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( // block because end reason content should not be the other side of the // following block boundary. nsIContent* const leafContent = HTMLEditUtils::GetLastLeafContent( - *previousChild, {LeafNodeType::LeafNodeOrChildBlock}, blockInlineCheck); + *previousChild, leafNodeOptions, blockInlineCheck); if (!leafContent) { return EditorRawDOMPoint::AtEndOf(*previousChild); } if (HTMLEditUtils::IsBlockElement( *leafContent, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || - HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent)) { + ((!aOptions.contains(Option::IgnoreEmptyInlineContainers) || + !HTMLEditUtils::IsContainerNode(*leafContent)) && + HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent))) { return EditorRawDOMPoint(); } return EditorRawDOMPoint::AtEndOf(*leafContent); @@ -1108,25 +1108,16 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( return EditorDOMPointType(); } - const auto leafNodeTypes = [&]() -> LeafNodeTypes { - auto types = aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes - ? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode, - LeafNodeType::LeafNodeOrChildBlock) - : LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock); - if (aOptions.contains(Option::StopAtComment)) { - types += LeafNodeType::TreatCommentAsLeafNode; - } - return types; - }(); for ( nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *point.ContainerAs<nsIContent>(), leafNodeTypes, blockInlineCheck, + *point.ContainerAs<nsIContent>(), leafNodeOptions, + blockInlineCheck, editableBlockElementOrInlineEditingHostOrNonEditableRootElement); previousContent; previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *previousContent, leafNodeTypes, blockInlineCheck, + *previousContent, leafNodeOptions, blockInlineCheck, editableBlockElementOrInlineEditingHostOrNonEditableRootElement)) { if (!previousContent->IsText() || (aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes && @@ -1135,7 +1126,9 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( HTMLEditUtils::IsBlockElement( *previousContent, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || - HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) { + ((!aOptions.contains(Option::IgnoreEmptyInlineContainers) || + !HTMLEditUtils::IsContainerNode(*previousContent)) && + HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent))) { break; // Reached start of current runs. } continue; diff --git a/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp b/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp @@ -32,7 +32,7 @@ namespace mozilla { using namespace dom; -using LeafNodeType = HTMLEditUtils::LeafNodeType; +using LeafNodeOption = HTMLEditUtils::LeafNodeOption; using WalkTreeOption = HTMLEditUtils::WalkTreeOption; Result<EditorDOMPoint, nsresult> @@ -923,15 +923,14 @@ WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesBefore( aPoint.IsInTextNode() && aPoint.IsEndOfContainer() ? aPoint.ContainerAs<Text>() : HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - aPoint, - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, + aPoint, {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); previousContent; previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( EditorRawDOMPoint(previousContent), - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, + {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) { if (!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { // XXX Assume non-editable nodes are visible. @@ -1042,14 +1041,13 @@ WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesAfter( aPoint.IsInTextNode() && aPoint.IsStartOfContainer() ? aPoint.ContainerAs<Text>() : HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - aPoint, - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, + aPoint, {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); nextContent; nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( EditorRawDOMPoint::After(*nextContent), - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, + {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) { if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) { // XXX Assume non-editable nodes are visible. @@ -1296,13 +1294,13 @@ WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt( if (!pointToSplit.IsInTextNode() || pointToSplit.IsStartOfContainer()) { for (nsCOMPtr<nsIContent> previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - pointToSplit, {LeafNodeType::LeafNodeOrChildBlock}, + pointToSplit, {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement); previousContent; previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *previousContent, {LeafNodeType::LeafNodeOrChildBlock}, + *previousContent, {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement)) { if (auto* const textNode = Text::FromNode(previousContent)) { @@ -1337,12 +1335,12 @@ WhiteSpaceVisibilityKeeper::NormalizeWhiteSpacesToSplitAt( if (!pointToSplit.IsInTextNode() || pointToSplit.IsEndOfContainer()) { for (nsCOMPtr<nsIContent> nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - pointToSplit, {LeafNodeType::LeafNodeOrChildBlock}, + pointToSplit, {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement); nextContent; nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *nextContent, {LeafNodeType::LeafNodeOrChildBlock}, + *nextContent, {LeafNodeOption::TreatChildBlockAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, closestBlockElement)) { if (auto* const textNode = Text::FromNode(nextContent)) { if (!HTMLEditUtils::IsSimplyEditableNode(*textNode) && @@ -1697,14 +1695,14 @@ nsresult WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter( for (nsIContent* nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( aPoint, - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock, - HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatChildBlockAsLeafNode, + LeafNodeOption::TreatCommentAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); nextContent; nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( EditorRawDOMPoint::After(*nextContent), - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock, - HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatChildBlockAsLeafNode, + LeafNodeOption::TreatCommentAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) { if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) { // XXX Assume non-editable nodes are visible. @@ -1781,15 +1779,15 @@ nsresult WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore( for (nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( aPoint, - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock, - HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatChildBlockAsLeafNode, + LeafNodeOption::TreatCommentAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); previousContent; previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( EditorRawDOMPoint(previousContent), - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock, - HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatChildBlockAsLeafNode, + LeafNodeOption::TreatCommentAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) { if (!HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { // XXX Assume non-editable nodes are visible. diff --git a/editor/libeditor/gtest/TestHTMLEditUtils.cpp b/editor/libeditor/gtest/TestHTMLEditUtils.cpp @@ -1445,7 +1445,7 @@ TEST(HTMLEditUtilsTest, IsEmptyNode) struct MOZ_STACK_CLASS GetLeafNodeTest final { const char16_t* mInnerHTML; const char* mContentSelector; - const HTMLEditUtils::LeafNodeTypes mTypes; + const HTMLEditUtils::LeafNodeOptions mOptions; const char* mExpectedTargetSelector; const char* mExpectedTargetContainerSelector = nullptr; const uint32_t mExpectedTargetOffset = 0u; @@ -1469,7 +1469,7 @@ struct MOZ_STACK_CLASS GetLeafNodeTest final { friend std::ostream& operator<<(std::ostream& aStream, const GetLeafNodeTest& aTest) { return aStream << "Scan from \"" << aTest.mContentSelector - << "\" with options=" << ToString(aTest.mTypes).c_str() + << "\" with options=" << ToString(aTest.mOptions).c_str() << " in \"" << NS_ConvertUTF16toUTF8(aTest.mInnerHTML).get() << "\""; } @@ -1477,7 +1477,7 @@ struct MOZ_STACK_CLASS GetLeafNodeTest final { TEST(HTMLEditUtilsTest, GetLastLeafContent) { - using LeafNodeType = HTMLEditUtils::LeafNodeType; + using LeafNodeOption = HTMLEditUtils::LeafNodeOption; const RefPtr<Document> doc = CreateHTMLDoc(); const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); MOZ_RELEASE_ASSERT(body); @@ -1486,11 +1486,12 @@ TEST(HTMLEditUtilsTest, GetLastLeafContent) GetLeafNodeTest{u"<div><br></div>", "div", {}, "div > br"}, GetLeafNodeTest{u"<div>abc<br></div>", "div", {}, "div > br"}, GetLeafNodeTest{u"<div>abc</div>", "div", {}, nullptr, "div", 0u}, + GetLeafNodeTest{ u"<div><div><br></div></div>", "div", {}, "div > div > br"}, GetLeafNodeTest{u"<div><div><br></div></div>", "div", - {LeafNodeType::LeafNodeOrChildBlock}, + {LeafNodeOption::TreatChildBlockAsLeafNode}, "div > div"}, GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>", "div", @@ -1498,19 +1499,20 @@ TEST(HTMLEditUtilsTest, GetLastLeafContent) "div > div + div > br"}, GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>", "div", - {LeafNodeType::LeafNodeOrChildBlock}, + {LeafNodeOption::TreatChildBlockAsLeafNode}, "div > div + div"}, + GetLeafNodeTest{u"<div><!--abc--></div>", "div", {}, nullptr}, GetLeafNodeTest{u"<div><!--abc--></div>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "div", 0u}, GetLeafNodeTest{u"<div><br><!--abc--></div>", "div", {}, "div > br"}, GetLeafNodeTest{u"<div><br><!--abc--></div>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "div", 1u}, @@ -1522,10 +1524,74 @@ TEST(HTMLEditUtilsTest, GetLastLeafContent) GetLeafNodeTest{ u"<div><div><br></div><div><br></div><!--abc--></div>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "div", 2u}, + + GetLeafNodeTest{ + u"<div><span></span></div>", "div", {}, "div > span"}, + GetLeafNodeTest{u"<div><span></span></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + nullptr}, + GetLeafNodeTest{ + u"<div><br><span></span></div>", "div", {}, "div > span"}, + GetLeafNodeTest{u"<div><br><span></span></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + "div > br"}, + GetLeafNodeTest{ + u"<div><div><br></div><div><br></div><span></span></div>", + "div", + {}, + "div > span"}, + GetLeafNodeTest{ + u"<div><div><br></div><div><br></div><span></span></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + "div > div + div > br"}, + + GetLeafNodeTest{u"<div><span><!-- abc --></span></div>", + "div", + {}, + "div > span"}, + GetLeafNodeTest{u"<div><span><!-- abc --></span></div>", + "div", + {LeafNodeOption::TreatCommentAsLeafNode}, + nullptr, + "div > span", + 0u}, + GetLeafNodeTest{u"<div><span><!-- abc --></span></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + nullptr}, + GetLeafNodeTest{u"<div><span><!-- abc --></span></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers, + LeafNodeOption::TreatCommentAsLeafNode}, + nullptr, + "div > span", + 0u}, + + GetLeafNodeTest{u"<div><br><wbr></div>", "div", {}, "div > wbr"}, + GetLeafNodeTest{u"<div><br><wbr></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + "div > wbr"}, + GetLeafNodeTest{u"<div><br><wbr></div>", + "div", + {LeafNodeOption::IgnoreInvisibleInlineVoidElements}, + "div > br"}, + + GetLeafNodeTest{ + u"<div><span>abc</span> </div>", "div", {}, nullptr, "div", 1u}, + GetLeafNodeTest{u"<div><span>abc</span> </div>", + "div", + {LeafNodeOption::IgnoreInvisibleText}, + nullptr, + "div > span", + 0u}, }) { body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), doc->NodePrincipal(), IgnoreErrors()); @@ -1533,7 +1599,7 @@ TEST(HTMLEditUtilsTest, GetLastLeafContent) nsDependentCString(testData.mContentSelector), IgnoreErrors()); MOZ_RELEASE_ASSERT(target); const nsIContent* result = HTMLEditUtils::GetLastLeafContent( - *target, testData.mTypes, + *target, testData.mOptions, BlockInlineCheck::UseComputedDisplayOutsideStyle); EXPECT_EQ(result, testData.GetExpectedTarget(*body)) << "GetLastLeafContent: " << testData @@ -1543,7 +1609,7 @@ TEST(HTMLEditUtilsTest, GetLastLeafContent) TEST(HTMLEditUtilsTest, GetFirstLeafContent) { - using LeafNodeType = HTMLEditUtils::LeafNodeType; + using LeafNodeOption = HTMLEditUtils::LeafNodeOption; const RefPtr<Document> doc = CreateHTMLDoc(); const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); MOZ_RELEASE_ASSERT(body); @@ -1555,9 +1621,10 @@ TEST(HTMLEditUtilsTest, GetFirstLeafContent) GetLeafNodeTest{u"<div>abc</div>", "div", {}, nullptr, "div", 0u}, GetLeafNodeTest{ u"<div><div><br></div></div>", "div", {}, "div > div > br"}, + GetLeafNodeTest{u"<div><div><br></div></div>", "div", - {LeafNodeType::LeafNodeOrChildBlock}, + {LeafNodeOption::TreatChildBlockAsLeafNode}, "div > div"}, GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>", "div", @@ -1565,19 +1632,20 @@ TEST(HTMLEditUtilsTest, GetFirstLeafContent) "div > div > br"}, GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>", "div", - {LeafNodeType::LeafNodeOrChildBlock}, + {LeafNodeOption::TreatChildBlockAsLeafNode}, "div > div"}, + GetLeafNodeTest{u"<div><!--abc--></div>", "div", {}, nullptr}, GetLeafNodeTest{u"<div><!--abc--></div>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "div", 0u}, GetLeafNodeTest{u"<div><!--abc--><br></div>", "div", {}, "div > br"}, GetLeafNodeTest{u"<div><!--abc--><br></div>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "div", 0u}, @@ -1589,10 +1657,72 @@ TEST(HTMLEditUtilsTest, GetFirstLeafContent) GetLeafNodeTest{ u"<div><!--abc--><div><br></div><div><br></div></div>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "div", 0u}, + + GetLeafNodeTest{ + u"<div><span></span></div>", "div", {}, "div > span"}, + GetLeafNodeTest{u"<div><span></span></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + nullptr}, + GetLeafNodeTest{u"<div><span></span><br></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + "div > br"}, + GetLeafNodeTest{ + u"<div><span></span><div><br></div><div><br></div></div>", + "div", + {}, + "div > span"}, + GetLeafNodeTest{ + u"<div><span></span><div><br></div><div><br></div></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + "div > div > br"}, + + GetLeafNodeTest{u"<div><span><!-- abc --></span></div>", + "div", + {}, + "div > span"}, + GetLeafNodeTest{u"<div><span><!-- abc --></span></div>", + "div", + {LeafNodeOption::TreatCommentAsLeafNode}, + nullptr, + "div > span", + 0u}, + GetLeafNodeTest{u"<div><span><!-- abc --></span></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + nullptr}, + GetLeafNodeTest{u"<div><span><!-- abc --></span></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers, + LeafNodeOption::TreatCommentAsLeafNode}, + nullptr, + "div > span", + 0u}, + + GetLeafNodeTest{u"<div><wbr><br></div>", "div", {}, "div > wbr"}, + GetLeafNodeTest{u"<div><wbr><br></div>", + "div", + {LeafNodeOption::IgnoreAnyEmptyInlineContainers}, + "div > wbr"}, + GetLeafNodeTest{u"<div><wbr><br></div>", + "div", + {LeafNodeOption::IgnoreInvisibleInlineVoidElements}, + "div > br"}, + + GetLeafNodeTest{ + u"<div> <span>abc</span></div>", "div", {}, nullptr, "div", 0u}, + GetLeafNodeTest{u"<div> <span>abc</span></div>", + "div", + {LeafNodeOption::IgnoreInvisibleText}, + nullptr, + "div > span", + 0u}, }) { body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), doc->NodePrincipal(), IgnoreErrors()); @@ -1600,7 +1730,7 @@ TEST(HTMLEditUtilsTest, GetFirstLeafContent) nsDependentCString(testData.mContentSelector), IgnoreErrors()); MOZ_RELEASE_ASSERT(target); const nsIContent* result = HTMLEditUtils::GetFirstLeafContent( - *target, testData.mTypes, + *target, testData.mOptions, BlockInlineCheck::UseComputedDisplayOutsideStyle); EXPECT_EQ(result, testData.GetExpectedTarget(*body)) << "GetFirstLeafContent: " << testData @@ -1610,7 +1740,7 @@ TEST(HTMLEditUtilsTest, GetFirstLeafContent) TEST(HTMLEditUtilsTest, GetNextLeafContentOrNextBlockElement_Content) { - using LeafNodeType = HTMLEditUtils::LeafNodeType; + using LeafNodeOption = HTMLEditUtils::LeafNodeOption; const RefPtr<Document> doc = CreateHTMLDoc(); const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); MOZ_RELEASE_ASSERT(body); @@ -1620,7 +1750,7 @@ TEST(HTMLEditUtilsTest, GetNextLeafContentOrNextBlockElement_Content) u"<div><br></div><!--abc--><p><br></p>", "div", {}, "p"}, GetLeafNodeTest{u"<div><br></div><!--abc--><p><br></p>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "body", 1u}, @@ -1632,7 +1762,7 @@ TEST(HTMLEditUtilsTest, GetNextLeafContentOrNextBlockElement_Content) "span > br"}, GetLeafNodeTest{u"<div><br></div><span><!--abc--><br></span>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "span", 0u}, @@ -1644,7 +1774,7 @@ TEST(HTMLEditUtilsTest, GetNextLeafContentOrNextBlockElement_Content) MOZ_RELEASE_ASSERT(target); const nsIContent* result = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - *target, testData.mTypes, + *target, testData.mOptions, BlockInlineCheck::UseComputedDisplayOutsideStyle); EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode())) << "GetNextLeafContentOrNextBlockElement: " << testData @@ -1656,7 +1786,7 @@ TEST(HTMLEditUtilsTest, GetNextLeafContentOrNextBlockElement_Content) TEST(HTMLEditUtilsTest, GetPreviousLeafContentOrPreviousBlockElement_Content) { - using LeafNodeType = HTMLEditUtils::LeafNodeType; + using LeafNodeOption = HTMLEditUtils::LeafNodeOption; const RefPtr<Document> doc = CreateHTMLDoc(); const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); MOZ_RELEASE_ASSERT(body); @@ -1666,7 +1796,7 @@ TEST(HTMLEditUtilsTest, GetPreviousLeafContentOrPreviousBlockElement_Content) u"<p><br></p><!--abc--><div><br></div>", "div", {}, "p"}, GetLeafNodeTest{u"<p><br></p><!--abc--><div><br></div>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "body", 1u}, @@ -1678,7 +1808,7 @@ TEST(HTMLEditUtilsTest, GetPreviousLeafContentOrPreviousBlockElement_Content) "span > br"}, GetLeafNodeTest{u"<span><br><!--abc--></span><div><br></div>", "div", - {LeafNodeType::TreatCommentAsLeafNode}, + {LeafNodeOption::TreatCommentAsLeafNode}, nullptr, "span", 1u}, @@ -1690,7 +1820,7 @@ TEST(HTMLEditUtilsTest, GetPreviousLeafContentOrPreviousBlockElement_Content) MOZ_RELEASE_ASSERT(target); const nsIContent* result = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - *target, testData.mTypes, + *target, testData.mOptions, BlockInlineCheck::UseComputedDisplayOutsideStyle); EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode())) << "GetPreviousLeafContentOrPreviousBlockElement: " << testData @@ -1738,11 +1868,7 @@ TEST(HTMLEditUtilsTest, GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem) u"<div contenteditable><br> </div>", "div", Some(1), 2, true}, LineBreakBeforeBlockBoundaryTest{ u"<div contenteditable><br><!-- X --></div>", "div", Nothing{}, - 2, - // FIXME: Currently, this fails with a bug of WSRunScanner - // (actually, a bug of - // HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement) - false}, + 2, true}, LineBreakBeforeBlockBoundaryTest{ u"<div contenteditable><br><br></div>", "div", Nothing{}, 1, false},