tor-browser

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

commit 59dac6a3e4cdf410613f3801eb870ae4fde1530a
parent 7217cfb61c46e0f306d35d4219a08d677b7fb761
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date:   Wed,  5 Nov 2025 06:32:05 +0000

Bug 1996758 - Make `WSRunScanner` and `HTMLEditUtils` return `Comment` within optional behavior r=m_kato

They should stop at a `Comment` node or treat `Command` as "visible"
node only when:
- The caller wants to handle only with current or adjacent `Text` node
- The caller wants to list all invisible nodes until the first visible thing

In other words, most users must want to ignore `Comment` nodes.
Therefore, their default behavior should be so but they should treat
`Comment` nodes in an optional mode.

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

Diffstat:
Meditor/libeditor/HTMLEditUtils.cpp | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Meditor/libeditor/HTMLEditUtils.h | 393++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Meditor/libeditor/HTMLEditorDataTransfer.cpp | 6+++---
Meditor/libeditor/WSRunScanner.cpp | 54++++++------------------------------------------------
Meditor/libeditor/WSRunScanner.h | 34++++++++++++++++++++--------------
Meditor/libeditor/WSRunScannerNestedClasses.cpp | 148+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Meditor/libeditor/WhiteSpaceVisibilityKeeper.cpp | 14++++++++++----
Meditor/libeditor/gtest/TestHTMLEditUtils.cpp | 434+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/meta/editing/run/inserthtml.html.ini | 9---------
Mtesting/web-platform/tests/editing/data/inserthtml.js | 26+++++++++++++++++---------
10 files changed, 887 insertions(+), 306 deletions(-)

diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp @@ -1340,6 +1340,12 @@ bool HTMLEditUtils::IsEmptyNode(nsPresContext* aPresContext, : !IsVisibleTextNode(*text); } + const bool treatCommentAsVisible = + aOptions.contains(EmptyCheckOption::TreatCommentAsVisible); + if (aNode.IsComment()) { + return !treatCommentAsVisible; + } + if (!aNode.IsElement()) { return false; } @@ -1412,16 +1418,21 @@ bool HTMLEditUtils::IsEmptyNode(nsPresContext* aPresContext, return false; } + const bool treatNonEditableContentAsInvisible = + aOptions.contains(EmptyCheckOption::TreatNonEditableContentAsInvisible); bool seenBR = aSeenBR && *aSeenBR; for (nsIContent* childContent = aNode.GetFirstChild(); childContent; childContent = childContent->GetNextSibling()) { - // Is the child editable and non-empty? if so, return false - if (aOptions.contains( - EmptyCheckOption::TreatNonEditableContentAsInvisible) && - !EditorUtils::IsEditableContent(*childContent, EditorType::HTML)) { + if (childContent->IsComment()) { + if (treatCommentAsVisible) { + return false; + } + continue; + } + if (treatNonEditableContentAsInvisible && + !HTMLEditUtils::IsSimplyEditableNode(*childContent)) { continue; } - if (Text* text = Text::FromNode(childContent)) { // break out if we find we aren't empty if (aOptions.contains(EmptyCheckOption::SafeToAskLayout) @@ -3246,6 +3257,60 @@ std::ostream& operator<<(std::ostream& aStream, return aStream << "}"; } +std::ostream& operator<<(std::ostream& aStream, + const HTMLEditUtils::EmptyCheckOption& aOption) { + constexpr static const char* names[] = { + "TreatSingleBRElementAsVisible", + "TreatBlockAsVisible", + "TreatListItemAsVisible", + "TreatTableCellAsVisible", + "TreatNonEditableContentAsInvisible", + "TreatCommentAsVisible", + "SafeToAskLayout", + }; + return aStream << names[static_cast<uint32_t>(aOption)]; +} + +std::ostream& operator<<(std::ostream& aStream, + const HTMLEditUtils::EmptyCheckOptions& aOptions) { + aStream << "{"; + bool first = true; + for (const auto t : aOptions) { + if (!first) { + aStream << ", "; + } + aStream << ToString(t).c_str(); + first = false; + } + return aStream << "}"; +} + +std::ostream& operator<<(std::ostream& aStream, + const HTMLEditUtils::LeafNodeType& aLeafNodeType) { + constexpr static const char* names[] = { + "OnlyLeafNode", + "LeafNodeOrChildBlock", + "LeafNodeOrNonEditableNode", + "OnlyEditableLeafNode", + "TreatCommentAsLeafNode", + }; + return aStream << names[static_cast<uint32_t>(aLeafNodeType)]; +} + +std::ostream& operator<<(std::ostream& aStream, + const HTMLEditUtils::LeafNodeTypes& aLeafNodeTypes) { + aStream << "{"; + bool first = true; + for (const auto t : aLeafNodeTypes) { + if (!first) { + aStream << ", "; + } + aStream << ToString(t).c_str(); + first = false; + } + return aStream << "}"; +} + /****************************************************************************** * SelectedTableCellScanner ******************************************************************************/ diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h @@ -543,17 +543,14 @@ class HTMLEditUtils final { /** * IsContainerNode() returns true if aContent is a container node. */ - static bool IsContainerNode(const nsIContent& aContent) { - nsHTMLTag tagEnum; - // XXX Should this handle #cdata-section too? - if (aContent.IsText()) { - tagEnum = eHTMLTag_text; - } else { - // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some - // difference? - tagEnum = nsHTMLTags::StringTagToId(aContent.NodeName()); + [[nodiscard]] static bool IsContainerNode(const nsIContent& aContent) { + if (aContent.IsCharacterData()) { + return false; } - return HTMLEditUtils::IsContainerNode(tagEnum); + return HTMLEditUtils::IsContainerNode( + // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some + // difference? + nsHTMLTags::StringTagToId(aContent.NodeName())); } /** @@ -764,9 +761,25 @@ class HTMLEditUtils final { static bool ShouldInsertLinefeedCharacter( const EditorDOMPoint& aPointToInsert, const Element& aEditingHost); + enum class EmptyCheckOption { + TreatSingleBRElementAsVisible, + TreatBlockAsVisible, + TreatListItemAsVisible, + TreatTableCellAsVisible, + TreatNonEditableContentAsInvisible, + TreatCommentAsVisible, + SafeToAskLayout, + }; + using EmptyCheckOptions = EnumSet<EmptyCheckOption, uint32_t>; + + friend std::ostream& operator<<(std::ostream& aStream, + const EmptyCheckOption& aOption); + friend std::ostream& operator<<(std::ostream& aStream, + const EmptyCheckOptions& aOptions); + /** - * IsEmptyNode() returns false if aNode has some visible content nodes, - * list elements or table elements. + * Return false if aNode has some visible content nodes, list elements or + * table elements. * * @param aPresContext Must not be nullptr if * EmptyCheckOption::SafeToAskLayout is set. @@ -776,18 +789,21 @@ class HTMLEditUtils final { * @param aSeenBR [Out] Set to true if this meets an <br> element * before meeting visible things. */ - enum class EmptyCheckOption { - TreatSingleBRElementAsVisible, - TreatBlockAsVisible, - TreatListItemAsVisible, - TreatTableCellAsVisible, - TreatNonEditableContentAsInvisible, - SafeToAskLayout, - }; - using EmptyCheckOptions = EnumSet<EmptyCheckOption, uint32_t>; static bool IsEmptyNode(nsPresContext* aPresContext, const nsINode& aNode, const EmptyCheckOptions& aOptions = {}, bool* aSeenBR = nullptr); + + /** + * Return false if aNode has some visible content nodes, list elements or + * table elements. + * + * @param aNode The node to check whether it's empty. + * @param aOptions You can specify which type of elements are visible + * and/or whether this can access layout information. + * Must not contain EmptyCheckOption::SafeToAskLayout. + * @param aSeenBR [Out] Set to true if this meets an <br> element + * before meeting visible things. + */ static bool IsEmptyNode(const nsINode& aNode, const EmptyCheckOptions& aOptions = {}, bool* aSeenBR = nullptr) { @@ -1311,9 +1327,16 @@ class HTMLEditUtils final { LeafNodeOrNonEditableNode, // Ignore non-editable content at walking the tree. OnlyEditableLeafNode, + // Treat `Comment` nodes are empty leaf nodes. + TreatCommentAsLeafNode, }; using LeafNodeTypes = EnumSet<LeafNodeType>; + friend std::ostream& operator<<(std::ostream& aStream, + const LeafNodeType& aLeafNodeType); + friend std::ostream& operator<<(std::ostream& aStream, + const LeafNodeTypes& aLeafNodeTypes); + /** * GetLastLeafContent() returns rightmost leaf content in aNode. It depends * on aLeafNodeTypes whether this which types of nodes are treated as leaf @@ -1345,6 +1368,11 @@ class HTMLEditUtils final { aBlockInlineCheck, aAncestorLimiter); continue; } + if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && + content->IsComment()) { + content = content->GetPreviousSibling(); + continue; + } if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && HTMLEditUtils::IsBlockElement( *content, @@ -1395,6 +1423,11 @@ class HTMLEditUtils final { aBlockInlineCheck, aAncestorLimiter); continue; } + if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && + content->IsComment()) { + content = content->GetNextSibling(); + continue; + } if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && HTMLEditUtils::IsBlockElement( *content, @@ -1435,55 +1468,68 @@ class HTMLEditUtils final { return nullptr; } - nsIContent* nextContent = aStartContent.GetNextSibling(); - if (!nextContent) { - if (!aStartContent.GetParentElement()) { - NS_WARNING("Reached orphan node while climbing up the DOM tree"); - return nullptr; - } - for (Element* parentElement : aStartContent.AncestorsOfType<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) { - break; - } - if (!parentElement->GetParentElement()) { + 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); } - MOZ_ASSERT(nextContent); - } - // 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; + 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; } - // Else return the next content itself. - return nextContent; + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( + "Must return from the preceding for-loop"); } /** @@ -1517,43 +1563,52 @@ class HTMLEditUtils final { aBlockInlineCheck, aAncestorLimiter); } - nsCOMPtr<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; + 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); } - // 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; + // 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; } - // Else return the node itself - return nextContent; + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( + "Must return from the preceding for-loop"); } /** @@ -1580,56 +1635,69 @@ class HTMLEditUtils final { return nullptr; } - nsIContent* previousContent = aStartContent.GetPreviousSibling(); - if (!previousContent) { - if (!aStartContent.GetParentElement()) { - NS_WARNING("Reached orphan node while climbing up the DOM tree"); - return nullptr; - } - for (Element* parentElement : aStartContent.AncestorsOfType<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) { - break; - } - if (!parentElement->GetParentElement()) { + 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); } - MOZ_ASSERT(previousContent); - } - // 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; + 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; } - // Else return the next content itself. - return previousContent; + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( + "Must return from the preceding for-loop"); } /** @@ -1679,32 +1747,33 @@ class HTMLEditUtils final { aAncestorLimiter); } - nsCOMPtr<nsIContent> previousContent = - aStartPoint.GetPreviousSiblingOfChild(); - if (NS_WARN_IF(!previousContent)) { - return nullptr; - } - - // 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; + 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; } - // Else return the node itself - return previousContent; + return nullptr; } /** diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp @@ -500,6 +500,8 @@ HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML( // but don't cross tables nsIContent* containerContent = nullptr; + // FIXME: GetChild() might be nullptr, but it's referred as non-null in the + // block! if (!aLastInsertedPoint.GetChild() || !aLastInsertedPoint.GetChild()->IsHTMLElement(nsGkAtoms::table)) { containerContent = HTMLEditUtils::GetLastLeafContent( @@ -540,9 +542,7 @@ HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML( // Otherwise, i.e., it's an atomic element, `<table>` element or data node, // put caret after it. else { - pointToPutCaret.Set(containerContent); - DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset(); - NS_WARNING_ASSERTION(advanced, "Failed to advance offset from found node"); + pointToPutCaret.SetAfter(containerContent); } // Make sure we don't end up with selection collapsed after an invisible diff --git a/editor/libeditor/WSRunScanner.cpp b/editor/libeditor/WSRunScanner.cpp @@ -208,6 +208,9 @@ WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( return WSScanResult::Error(); } + MOZ_ASSERT_IF(!ScanOptions().contains(Option::StopAtComment), + !Comment::FromNodeOrNull( + TextFragmentDataAtStartRef().GetStartReasonContent())); switch (TextFragmentDataAtStartRef().StartRawReason()) { case WSType::CollapsibleWhiteSpaces: case WSType::NonCollapsibleCharacters: @@ -219,29 +222,6 @@ WSScanResult WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundaryFrom( return WSScanResult(*this, WSScanResult::ScanDirection::Backward, TextFragmentDataAtStartRef().StartRef(), TextFragmentDataAtStartRef().StartRawReason()); - case WSType::SpecialContent: { - const Comment* comment = Comment::FromNode( - TextFragmentDataAtStartRef().GetStartReasonContent()); - if (!comment) { - break; - } - // If we reached a comment node, we should skip it because it's always - // invisible. - while (true) { - const EditorRawDOMPoint atComment(comment); - WSRunScanner scanner(ScanOptions(), atComment, - mTextFragmentDataAtStart.GetAncestorLimiter()); - if (scanner.TextFragmentDataAtStartRef().StartRawReason() == - WSType::SpecialContent) { - if ((comment = Comment::FromNode(scanner.TextFragmentDataAtStartRef() - .GetStartReasonContent()))) { - // Reached another comment node, keep scanning... - continue; - } - } - return scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(atComment); - } - } default: break; } @@ -325,6 +305,9 @@ WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( return WSScanResult::Error(); } + MOZ_ASSERT_IF(!ScanOptions().contains(Option::StopAtComment), + !Comment::FromNodeOrNull( + TextFragmentDataAtStartRef().GetEndReasonContent())); switch (TextFragmentDataAtStartRef().EndRawReason()) { case WSType::CollapsibleWhiteSpaces: case WSType::NonCollapsibleCharacters: @@ -337,31 +320,6 @@ WSScanResult WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( return WSScanResult(*this, WSScanResult::ScanDirection::Forward, TextFragmentDataAtStartRef().EndRef(), TextFragmentDataAtStartRef().EndRawReason()); - case WSType::SpecialContent: { - const Comment* comment = - Comment::FromNode(TextFragmentDataAtStartRef().GetEndReasonContent()); - if (!comment) { - break; - } - // If we reached a comment node, we should skip it because it's always - // invisible. - while (true) { - const EditorRawDOMPoint afterComment = - EditorRawDOMPoint::After(*comment); - WSRunScanner scanner(ScanOptions(), afterComment, - mTextFragmentDataAtStart.GetAncestorLimiter()); - if (scanner.TextFragmentDataAtStartRef().EndRawReason() == - WSType::SpecialContent) { - if ((comment = Comment::FromNode(scanner.TextFragmentDataAtStartRef() - .GetEndReasonContent()))) { - // Reached another comment node, keep scanning... - continue; - } - } - return scanner.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( - afterComment); - } - } default: break; } diff --git a/editor/libeditor/WSRunScanner.h b/editor/libeditor/WSRunScanner.h @@ -398,6 +398,8 @@ class MOZ_STACK_CLASS WSRunScanner final { // If set, use the HTML default style to consider whether the found one is a // block or an inline. ReferHTMLDefaultStyle, + // If set, stop scanning the DOM when it reaches a `Comment` node. + StopAtComment, }; using Options = EnumSet<Option>; @@ -1086,11 +1088,13 @@ class MOZ_STACK_CLASS WSRunScanner final { /** * Return inclusive next point in inclusive next `Text` node from aPoint. * So, it may be in a collapsed white-space or invisible white-spaces. + * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop + * at non-editable content" in the other places, but this "ignores" them. */ template <typename EditorDOMPointType, typename PT, typename CT> [[nodiscard]] static EditorDOMPointType GetInclusiveNextCharPoint( const EditorDOMPointBase<PT, CT>& aPoint, - ReferHTMLDefaultStyle aReferHTMLDefaultStyle, + Options aOptions, // NOLINT(performance-unnecessary-value-param) IgnoreNonEditableNodes aIgnoreNonEditableNodes, const nsIContent* aFollowingLimiterContent = nullptr); @@ -1099,18 +1103,19 @@ class MOZ_STACK_CLASS WSRunScanner final { const EditorDOMPointBase<PT, CT>& aPoint, IgnoreNonEditableNodes aIgnoreNonEditableNodes) const { return GetInclusiveNextCharPoint<EditorDOMPointType>( - aPoint, ShouldReferHTMLDefaultStyle(mOptions), - aIgnoreNonEditableNodes, GetEndReasonContent()); + aPoint, mOptions, aIgnoreNonEditableNodes, GetEndReasonContent()); } /** * Return previous point in inclusive previous `Text` node from aPoint. * So, it may be in a collapsed white-space or invisible white-spaces. + * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop + * at non-editable content" in the other places, but this "ignores" them. */ template <typename EditorDOMPointType, typename PT, typename CT> [[nodiscard]] static EditorDOMPointType GetPreviousCharPoint( const EditorDOMPointBase<PT, CT>& aPoint, - ReferHTMLDefaultStyle aReferHTMLDefaultStyle, + Options aOptions, // NOLINT(performance-unnecessary-value-param) IgnoreNonEditableNodes aIgnoreNonEditableNodes, const nsIContent* aPrecedingLimiterContent = nullptr); @@ -1119,12 +1124,13 @@ class MOZ_STACK_CLASS WSRunScanner final { const EditorDOMPointBase<PT, CT>& aPoint, IgnoreNonEditableNodes aIgnoreNonEditableNodes) const { return GetPreviousCharPoint<EditorDOMPointType>( - aPoint, ShouldReferHTMLDefaultStyle(mOptions), - aIgnoreNonEditableNodes, GetStartReasonContent()); + aPoint, mOptions, aIgnoreNonEditableNodes, GetStartReasonContent()); } /** * Return end of current collapsible ASCII white-spaces. + * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop + * at non-editable content" in the other places, but this "ignores" them. * * @param aPointAtASCIIWhiteSpace Must be in a sequence of collapsible * ASCII white-spaces. @@ -1134,7 +1140,7 @@ class MOZ_STACK_CLASS WSRunScanner final { [[nodiscard]] static EditorDOMPointType GetEndOfCollapsibleASCIIWhiteSpaces( const EditorDOMPointInText& aPointAtASCIIWhiteSpace, nsIEditor::EDirection aDirectionToDelete, - ReferHTMLDefaultStyle aReferHTMLDefaultStyle, + Options aOptions, // NOLINT(performance-unnecessary-value-param) IgnoreNonEditableNodes aIgnoreNonEditableNodes, const nsIContent* aFollowingLimiterContent = nullptr); @@ -1144,13 +1150,14 @@ class MOZ_STACK_CLASS WSRunScanner final { nsIEditor::EDirection aDirectionToDelete, IgnoreNonEditableNodes aIgnoreNonEditableNodes) const { return GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointType>( - aPointAtASCIIWhiteSpace, aDirectionToDelete, - ShouldReferHTMLDefaultStyle(mOptions), aIgnoreNonEditableNodes, - GetEndReasonContent()); + aPointAtASCIIWhiteSpace, aDirectionToDelete, mOptions, + aIgnoreNonEditableNodes, GetEndReasonContent()); } /** * Return start of current collapsible ASCII white-spaces. + * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop + * at non-editable content" in the other places, but this "ignores" them. * * @param aPointAtASCIIWhiteSpace Must be in a sequence of collapsible * ASCII white-spaces. @@ -1161,7 +1168,7 @@ class MOZ_STACK_CLASS WSRunScanner final { GetFirstASCIIWhiteSpacePointCollapsedTo( const EditorDOMPointInText& aPointAtASCIIWhiteSpace, nsIEditor::EDirection aDirectionToDelete, - ReferHTMLDefaultStyle aReferHTMLDefaultStyle, + Options aOptions, // NOLINT(performance-unnecessary-value-param) IgnoreNonEditableNodes aIgnoreNonEditableNodes, const nsIContent* aPrecedingLimiterContent = nullptr); @@ -1171,9 +1178,8 @@ class MOZ_STACK_CLASS WSRunScanner final { nsIEditor::EDirection aDirectionToDelete, IgnoreNonEditableNodes aIgnoreNonEditableNodes) const { return GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointType>( - aPointAtASCIIWhiteSpace, aDirectionToDelete, - ShouldReferHTMLDefaultStyle(mOptions), aIgnoreNonEditableNodes, - GetStartReasonContent()); + aPointAtASCIIWhiteSpace, aDirectionToDelete, mOptions, + aIgnoreNonEditableNodes, GetStartReasonContent()); } /** diff --git a/editor/libeditor/WSRunScannerNestedClasses.cpp b/editor/libeditor/WSRunScannerNestedClasses.cpp @@ -39,45 +39,44 @@ template WSRunScanner::TextFragmentData::TextFragmentData( NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint, - const EditorDOMPoint&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes, - const nsIContent*); + const EditorDOMPoint&, Options, IgnoreNonEditableNodes, const nsIContent*); NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint, - const EditorRawDOMPoint&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes, + const EditorRawDOMPoint&, Options, IgnoreNonEditableNodes, const nsIContent*); NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint, - const EditorDOMPointInText&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes, + const EditorDOMPointInText&, Options, IgnoreNonEditableNodes, const nsIContent*); NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint, - const EditorRawDOMPointInText&, ReferHTMLDefaultStyle, - IgnoreNonEditableNodes, const nsIContent*); + const EditorRawDOMPointInText&, Options, IgnoreNonEditableNodes, + const nsIContent*); NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetPreviousCharPoint, const EditorDOMPoint&, - ReferHTMLDefaultStyle, IgnoreNonEditableNodes, const nsIContent*); + Options, IgnoreNonEditableNodes, const nsIContent*); NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetPreviousCharPoint, - const EditorRawDOMPoint&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes, + const EditorRawDOMPoint&, Options, IgnoreNonEditableNodes, const nsIContent*); NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetPreviousCharPoint, - const EditorDOMPointInText&, ReferHTMLDefaultStyle, IgnoreNonEditableNodes, + const EditorDOMPointInText&, Options, IgnoreNonEditableNodes, const nsIContent*); NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetPreviousCharPoint, - const EditorRawDOMPointInText&, ReferHTMLDefaultStyle, - IgnoreNonEditableNodes, const nsIContent*); + const EditorRawDOMPointInText&, Options, IgnoreNonEditableNodes, + const nsIContent*); NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces, - const EditorDOMPointInText&, nsIEditor::EDirection, ReferHTMLDefaultStyle, + const EditorDOMPointInText&, nsIEditor::EDirection, Options, IgnoreNonEditableNodes, const nsIContent*); NS_INSTANTIATE_METHOD_RETURNING_ANY_EDITOR_DOM_POINT( WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo, - const EditorDOMPointInText&, nsIEditor::EDirection, ReferHTMLDefaultStyle, + const EditorDOMPointInText&, nsIEditor::EDirection, Options, IgnoreNonEditableNodes, const nsIContent*); // FIXME: I think the scanner should not cross the <button> element boundaries. @@ -246,10 +245,15 @@ WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData:: ? BlockInlineCheck::UseHTMLDefaultStyle : BlockInlineCheck::Auto; // Then, we need to check previous leaf node. - const auto leafNodeTypes = - aOptions.contains(Option::OnlyEditableNodes) - ? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode} - : LeafNodeTypes{LeafNodeType::OnlyLeafNode}; + 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; + }(); nsIContent* previousLeafContentOrBlock = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( aPoint, leafNodeTypes, blockInlineCheck, &aAncestorLimiter); @@ -403,10 +407,15 @@ WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom( : BlockInlineCheck::Auto; // Then, we need to check next leaf node. - const auto leafNodeTypes = - aOptions.contains(Option::OnlyEditableNodes) - ? LeafNodeTypes{LeafNodeType::LeafNodeOrNonEditableNode} - : LeafNodeTypes{LeafNodeType::OnlyLeafNode}; + 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; + }(); nsIContent* nextLeafContentOrBlock = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( aPoint, leafNodeTypes, blockInlineCheck, &aAncestorLimiter); @@ -849,7 +858,7 @@ WSRunScanner::TextFragmentData::GetReplaceRangeDataAtStartOfDeletionRange( template <typename EditorDOMPointType, typename PT, typename CT> EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( const EditorDOMPointBase<PT, CT>& aPoint, - ReferHTMLDefaultStyle aReferHTMLDefaultStyle, + Options aOptions, // NOLINT(performance-unnecessary-value-param) IgnoreNonEditableNodes aIgnoreNonEditableNodes, const nsIContent* aFollowingLimiterContent /* = nullptr */) { MOZ_ASSERT(aPoint.IsSetAndValid()); @@ -859,12 +868,20 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( } const BlockInlineCheck blockInlineCheck = - static_cast<bool>(aReferHTMLDefaultStyle) + aOptions.contains(Option::ReferHTMLDefaultStyle) ? BlockInlineCheck::UseHTMLDefaultStyle : BlockInlineCheck::Auto; const EditorRawDOMPoint point = [&]() MOZ_NEVER_INLINE_DEBUG { - nsIContent* const child = - aPoint.CanContainerHaveChildren() ? aPoint.GetChild() : nullptr; + nsIContent* const child = [&]() -> nsIContent* { + nsIContent* child = + aPoint.CanContainerHaveChildren() ? aPoint.GetChild() : nullptr; + // XXX Why don't we skip non-editable nodes here? + while (child && child->IsComment() && + !aOptions.contains(Option::StopAtComment)) { + child = child->GetNextSibling(); + } + return child; + }(); if (!child || HTMLEditUtils::IsBlockElement( *child, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || @@ -872,7 +889,9 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( return aPoint.template To<EditorRawDOMPoint>(); } if (!child->HasChildNodes()) { - return EditorRawDOMPoint(child, 0); + return child->IsText() || HTMLEditUtils::IsContainerNode(*child) + ? EditorRawDOMPoint(child, 0) + : EditorRawDOMPoint::After(*child); } // FIXME: This may skip aFollowingLimiterContent, so, this utility should // take a stopper param. @@ -881,13 +900,15 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( // following block boundary. nsIContent* const leafContent = HTMLEditUtils::GetFirstLeafContent( *child, {LeafNodeType::LeafNodeOrChildBlock}, blockInlineCheck); - if (NS_WARN_IF(!leafContent) || - HTMLEditUtils::IsBlockElement( + if (HTMLEditUtils::IsBlockElement( *leafContent, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent)) { return EditorRawDOMPoint(); } + if (!leafContent) { + return EditorRawDOMPoint(child, 0); + } return EditorRawDOMPoint(leafContent, 0); }(); if (!point.IsSet()) { @@ -922,11 +943,16 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( return EditorDOMPointType(); } - const auto leafNodeTypes = - aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes - ? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode, - LeafNodeType::LeafNodeOrChildBlock) - : LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock); + 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, @@ -957,7 +983,7 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetInclusiveNextCharPoint( template <typename EditorDOMPointType, typename PT, typename CT> EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( const EditorDOMPointBase<PT, CT>& aPoint, - ReferHTMLDefaultStyle aReferHTMLDefaultStyle, + Options aOptions, // NOLINT(performance-unnecessary-value-param) IgnoreNonEditableNodes aIgnoreNonEditableNodes, const nsIContent* aPrecedingLimiterContent /* = nullptr */) { MOZ_ASSERT(aPoint.IsSetAndValid()); @@ -967,13 +993,21 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( } const BlockInlineCheck blockInlineCheck = - static_cast<bool>(aReferHTMLDefaultStyle) + aOptions.contains(Option::ReferHTMLDefaultStyle) ? BlockInlineCheck::UseHTMLDefaultStyle : BlockInlineCheck::Auto; const EditorRawDOMPoint point = [&]() MOZ_NEVER_INLINE_DEBUG { - nsIContent* const previousChild = aPoint.CanContainerHaveChildren() - ? aPoint.GetPreviousSiblingOfChild() - : nullptr; + nsIContent* const previousChild = [&]() -> nsIContent* { + nsIContent* previousChild = aPoint.CanContainerHaveChildren() + ? aPoint.GetPreviousSiblingOfChild() + : nullptr; + // XXX Why don't we skip non-editable nodes here? + while (previousChild && previousChild->IsComment() && + !aOptions.contains(Option::StopAtComment)) { + previousChild = previousChild->GetPreviousSibling(); + } + return previousChild; + }(); if (!previousChild || HTMLEditUtils::IsBlockElement( *previousChild, @@ -982,7 +1016,10 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( return aPoint.template To<EditorRawDOMPoint>(); } if (!previousChild->HasChildren()) { - return EditorRawDOMPoint::AtEndOf(*previousChild); + return previousChild->IsText() || + HTMLEditUtils::IsContainerNode(*previousChild) + ? EditorRawDOMPoint::AtEndOf(*previousChild) + : EditorRawDOMPoint::After(*previousChild); } // FIXME: This may skip aPrecedingLimiterContent, so, this utility should // take a stopper param. @@ -991,13 +1028,15 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( // following block boundary. nsIContent* const leafContent = HTMLEditUtils::GetLastLeafContent( *previousChild, {LeafNodeType::LeafNodeOrChildBlock}, blockInlineCheck); - if (NS_WARN_IF(!leafContent) || - HTMLEditUtils::IsBlockElement( + if (HTMLEditUtils::IsBlockElement( *leafContent, UseComputedDisplayOutsideStyleIfAuto(blockInlineCheck)) || HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*leafContent)) { return EditorRawDOMPoint(); } + if (!leafContent) { + return EditorRawDOMPoint::AtEndOf(*previousChild); + } return EditorRawDOMPoint::AtEndOf(*leafContent); }(); if (!point.IsSet()) { @@ -1033,11 +1072,16 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetPreviousCharPoint( return EditorDOMPointType(); } - const auto leafNodeTypes = - aIgnoreNonEditableNodes == IgnoreNonEditableNodes::Yes - ? LeafNodeTypes(LeafNodeType::LeafNodeOrNonEditableNode, - LeafNodeType::LeafNodeOrChildBlock) - : LeafNodeTypes(LeafNodeType::LeafNodeOrChildBlock); + 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( @@ -1074,7 +1118,7 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces( const EditorDOMPointInText& aPointAtASCIIWhiteSpace, nsIEditor::EDirection aDirectionToDelete, - ReferHTMLDefaultStyle aReferHTMLDefaultStyle, + Options aOptions, // NOLINT(performance-unnecessary-value-param) IgnoreNonEditableNodes aIgnoreNonEditableNodes, const nsIContent* aFollowingLimiterContent /* = nullptr */) { MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone || @@ -1149,8 +1193,8 @@ WSRunScanner::TextFragmentData::GetEndOfCollapsibleASCIIWhiteSpaces( for (EditorDOMPointInText atEndOfPreviousTextNode = afterLastWhiteSpace;;) { const auto atStartOfNextTextNode = TextFragmentData::GetInclusiveNextCharPoint<EditorDOMPointInText>( - atEndOfPreviousTextNode, aReferHTMLDefaultStyle, - aIgnoreNonEditableNodes, aFollowingLimiterContent); + atEndOfPreviousTextNode, aOptions, aIgnoreNonEditableNodes, + aFollowingLimiterContent); if (!atStartOfNextTextNode.IsSet()) { // There is no more text nodes. Return end of the previous text node. return afterLastWhiteSpace.To<EditorDOMPointType>(); @@ -1192,7 +1236,7 @@ EditorDOMPointType WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo( const EditorDOMPointInText& aPointAtASCIIWhiteSpace, nsIEditor::EDirection aDirectionToDelete, - ReferHTMLDefaultStyle aReferHTMLDefaultStyle, + Options aOptions, // NOLINT(performance-unnecessary-value-param) IgnoreNonEditableNodes aIgnoreNonEditableNodes, const nsIContent* aPrecedingLimiterContent) { MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone || @@ -1269,8 +1313,8 @@ WSRunScanner::TextFragmentData::GetFirstASCIIWhiteSpacePointCollapsedTo( for (EditorDOMPointInText atStartOfPreviousTextNode = atLastWhiteSpace;;) { const auto atLastCharOfPreviousTextNode = TextFragmentData::GetPreviousCharPoint<EditorDOMPointInText>( - atStartOfPreviousTextNode, aReferHTMLDefaultStyle, - aIgnoreNonEditableNodes, aPrecedingLimiterContent); + atStartOfPreviousTextNode, aOptions, aIgnoreNonEditableNodes, + aPrecedingLimiterContent); if (!atLastCharOfPreviousTextNode.IsSet()) { // There is no more text nodes. Return end of last text node. return atLastWhiteSpace.To<EditorDOMPointType>(); diff --git a/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp b/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp @@ -1694,12 +1694,15 @@ nsresult WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesAfter( AutoTArray<OwningNonNull<nsIContent>, 32> unnecessaryContents; for (nsIContent* nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( - aPoint, {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, + aPoint, + {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock, + HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); nextContent; nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement( EditorRawDOMPoint::After(*nextContent), - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, + {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock, + HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement)) { if (!HTMLEditUtils::IsSimplyEditableNode(*nextContent)) { // XXX Assume non-editable nodes are visible. @@ -1775,13 +1778,16 @@ nsresult WhiteSpaceVisibilityKeeper::EnsureNoInvisibleWhiteSpacesBefore( AutoTArray<OwningNonNull<nsIContent>, 32> unnecessaryContents; for (nsIContent* previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( - aPoint, {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, + aPoint, + {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock, + HTMLEditUtils::LeafNodeType::TreatCommentAsLeafNode}, BlockInlineCheck::UseComputedDisplayStyle, colsetBlockElement); previousContent; previousContent = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( EditorRawDOMPoint(previousContent), - {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock}, + {HTMLEditUtils::LeafNodeType::LeafNodeOrChildBlock, + HTMLEditUtils::LeafNodeType::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 @@ -6,6 +6,8 @@ #include "gtest/gtest.h" #include "mozilla/BasePrincipal.h" #include "mozilla/OriginAttributes.h" +#include "mozilla/dom/CDATASection.h" +#include "mozilla/dom/Comment.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Text.h" #include "EditorDOMPoint.h" @@ -15,6 +17,7 @@ #include "nsIURI.h" #include "nsNetUtil.h" #include "nsString.h" +#include "nsTextNode.h" namespace mozilla { @@ -1267,4 +1270,435 @@ TEST(HTMLEditUtilsTest, GetAncestorElement_ClosestContainerElement) } } +TEST(HTMLEditUtilsTest, IsContainerNode) +{ + const RefPtr<Document> doc = CreateHTMLDoc(); + for (const char16_t* tagName : + {u"html", u"body", u"div", u"span", u"select", u"option", u"form"}) { + const RefPtr<nsAtom> tag = NS_Atomize(tagName); + MOZ_RELEASE_ASSERT(tag); + const RefPtr<Element> element = doc->CreateHTMLElement(tag); + MOZ_RELEASE_ASSERT(element); + EXPECT_EQ(true, HTMLEditUtils::IsContainerNode(*element)) + << "IsContainerNode(<" << NS_ConvertUTF16toUTF8(tagName).get() << ">)"; + } + for (const char16_t* tagName : {u"img", u"input", u"br", u"wbr"}) { + const RefPtr<nsAtom> tag = NS_Atomize(tagName); + MOZ_RELEASE_ASSERT(tag); + const RefPtr<Element> element = doc->CreateHTMLElement(tag); + MOZ_RELEASE_ASSERT(element); + EXPECT_EQ(false, HTMLEditUtils::IsContainerNode(*element)) + << "IsContainerNode(<" << NS_ConvertUTF16toUTF8(tagName).get() << ">)"; + } + { + const RefPtr<nsTextNode> text = doc->CreateEmptyTextNode(); + MOZ_RELEASE_ASSERT(text); + EXPECT_EQ(false, HTMLEditUtils::IsContainerNode(*text)) + << "IsContainerNode(Text)"; + } + { + const RefPtr<Comment> comment = + doc->CreateComment(nsDependentString(u"abc")); + MOZ_RELEASE_ASSERT(comment); + EXPECT_EQ(false, HTMLEditUtils::IsContainerNode(*comment)) + << "IsContainerNode(Comment)"; + } +} + +struct MOZ_STACK_CLASS IsEmptyNodeTest final { + const char16_t* mInnerHTML; + const char* mTargetSelector; + const HTMLEditUtils::EmptyCheckOptions mOptions; + const bool mExpectedValue; + const bool mExpectedSeenBR; + + friend std::ostream& operator<<(std::ostream& aStream, + const IsEmptyNodeTest& aTest) { + return aStream << "Check \"" << aTest.mTargetSelector + << "\" with options=" << ToString(aTest.mOptions).c_str() + << " in \"" << NS_ConvertUTF16toUTF8(aTest.mInnerHTML).get() + << "\""; + } +}; + +TEST(HTMLEditUtilsTest, IsEmptyNode) +{ + using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption; + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + IsEmptyNodeTest{u"<div></div>", "div", {}, true, false}, + IsEmptyNodeTest{u"<div></div>", + "div", + {EmptyCheckOption::TreatBlockAsVisible}, + true, + false}, + IsEmptyNodeTest{u"<div><br></div>", "div", {}, true, true}, + IsEmptyNodeTest{u"<div><br></div>", + "div", + {EmptyCheckOption::TreatBlockAsVisible}, + true, + true}, + IsEmptyNodeTest{u"<div><br></div>", + "div", + {EmptyCheckOption::TreatSingleBRElementAsVisible}, + false, + false}, + IsEmptyNodeTest{u"<div><!--abc--></div>", "div", {}, true, false}, + IsEmptyNodeTest{u"<div><!--abc--></div>", + "div", + {EmptyCheckOption::TreatCommentAsVisible}, + false, + false}, + IsEmptyNodeTest{u"<ul><li><br></li></ul>", "ul", {}, true, true}, + IsEmptyNodeTest{u"<ul><li><br></li></ul>", + "ul", + {EmptyCheckOption::TreatListItemAsVisible}, + false, + false}, + IsEmptyNodeTest{ + u"<table><td><br></td></table>", "table", {}, true, true}, + IsEmptyNodeTest{u"<table><td><br></td></table>", + "table", + {EmptyCheckOption::TreatTableCellAsVisible}, + false, + false}, + IsEmptyNodeTest{u"<div>abc</div>", "div", {}, false, false}, + IsEmptyNodeTest{ + u"<div><span><br></span></div>", "div", {}, true, true}, + IsEmptyNodeTest{ + u"<div><div><br></div></div>", "div", {}, true, true}, + IsEmptyNodeTest{u"<div><div><br></div></div>", + "div", + {EmptyCheckOption::TreatBlockAsVisible}, + false, + false}, + IsEmptyNodeTest{u"<dl><dt><br></dt></dl>", "dl", {}, true, true}, + IsEmptyNodeTest{u"<dl><dt><br</dt></dl>", + "dl", + {EmptyCheckOption::TreatListItemAsVisible}, + false, + false}, + IsEmptyNodeTest{u"<dl><dd><br></dd></dl>", "dl", {}, true, true}, + IsEmptyNodeTest{u"<dl><dd><br</dd></dl>", + "dl", + {EmptyCheckOption::TreatListItemAsVisible}, + false, + false}, + // form controls should be always not empty. + IsEmptyNodeTest{u"<input>", "input", {}, false, false}, + IsEmptyNodeTest{u"<select></select>", "select", {}, false, false}, + IsEmptyNodeTest{u"<button></button>", "button", {}, false, false}, + IsEmptyNodeTest{ + u"<textarea></textarea>", "textarea", {}, false, false}, + IsEmptyNodeTest{u"<output></output>", "output", {}, false, false}, + IsEmptyNodeTest{ + u"<progress></progress>", "progress", {}, false, false}, + IsEmptyNodeTest{u"<meter></meter>", "meter", {}, false, false}, + // void elements should be always not empty. + IsEmptyNodeTest{u"<br>", "br", {}, false, false}, + IsEmptyNodeTest{u"<wbr>", "wbr", {}, false, false}, + IsEmptyNodeTest{u"<img>", "img", {}, false, false}, + // white-spaces should not be treated as visible in block + IsEmptyNodeTest{u"<div> </div>", "div", {}, true, false}, + IsEmptyNodeTest{u"<span> </span>", "span", {}, true, false}, + IsEmptyNodeTest{u"a<span> </span>b", "span", {}, false, false}, + // sublist's list items and table cells should be treated as visible. + IsEmptyNodeTest{u"<ul><li><ol><li><br></li></ol></li></ul>", + "ul", + {}, + false, + false}, + IsEmptyNodeTest{u"<ul><li><table><td><br></td></table></li></ul>", + "ul", + {}, + false, + false}, + IsEmptyNodeTest{ + u"<table><td><table><td><br></td></table></td></table>", + "table", + {}, + false, + false}, + IsEmptyNodeTest{u"<table><td><ul><li><br></li></ul></td></table>", + "table", + {}, + false, + false}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const target = body->QuerySelector( + nsDependentCString(testData.mTargetSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(target); + bool seenBR = false; + const bool ret = + HTMLEditUtils::IsEmptyNode(*target, testData.mOptions, &seenBR); + EXPECT_EQ(ret, testData.mExpectedValue) + << "IsEmptyNode(result): " << testData; + EXPECT_EQ(seenBR, testData.mExpectedSeenBR) + << "IsEmptyNode(seenBR): " << testData; + } +} + +struct MOZ_STACK_CLASS GetLeafNodeTest final { + const char16_t* mInnerHTML; + const char* mContentSelector; + const HTMLEditUtils::LeafNodeTypes mTypes; + const char* mExpectedTargetSelector; + const char* mExpectedTargetContainerSelector = nullptr; + const uint32_t mExpectedTargetOffset = 0u; + + nsIContent* GetExpectedTarget(nsINode& aNode) const { + if (mExpectedTargetSelector) { + return aNode.QuerySelector(nsDependentCString(mExpectedTargetSelector), + IgnoreErrors()); + } + if (!mExpectedTargetContainerSelector) { + return nullptr; + } + Element* const container = aNode.QuerySelector( + nsDependentCString(mExpectedTargetContainerSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(container); + MOZ_RELEASE_ASSERT(!mExpectedTargetOffset || + mExpectedTargetOffset < container->Length()); + return container->GetChildAt_Deprecated(mExpectedTargetOffset); + } + + friend std::ostream& operator<<(std::ostream& aStream, + const GetLeafNodeTest& aTest) { + return aStream << "Scan from \"" << aTest.mContentSelector + << "\" with options=" << ToString(aTest.mTypes).c_str() + << " in \"" << NS_ConvertUTF16toUTF8(aTest.mInnerHTML).get() + << "\""; + } +}; + +TEST(HTMLEditUtilsTest, GetLastLeafContent) +{ + using LeafNodeType = HTMLEditUtils::LeafNodeType; + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + GetLeafNodeTest{u"<div></div>", "div", {}, nullptr}, + GetLeafNodeTest{u"<div><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}, + "div > div"}, + GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>", + "div", + {}, + "div > div + div > br"}, + GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>", + "div", + {LeafNodeType::LeafNodeOrChildBlock}, + "div > div + div"}, + GetLeafNodeTest{u"<div><!--abc--></div>", "div", {}, nullptr}, + GetLeafNodeTest{u"<div><!--abc--></div>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "div", + 0u}, + GetLeafNodeTest{u"<div><br><!--abc--></div>", "div", {}, "div > br"}, + GetLeafNodeTest{u"<div><br><!--abc--></div>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "div", + 1u}, + GetLeafNodeTest{ + u"<div><div><br></div><div><br></div><!--abc--></div>", + "div", + {}, + "div > div + div > br"}, + GetLeafNodeTest{ + u"<div><div><br></div><div><br></div><!--abc--></div>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "div", + 2u}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const target = body->QuerySelector( + nsDependentCString(testData.mContentSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(target); + const nsIContent* result = HTMLEditUtils::GetLastLeafContent( + *target, testData.mTypes, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + EXPECT_EQ(result, testData.GetExpectedTarget(*body)) + << "GetLastLeafContent: " << testData + << "(Got: " << ToString(RefPtr{result}) << ")"; + } +} + +TEST(HTMLEditUtilsTest, GetFirstLeafContent) +{ + using LeafNodeType = HTMLEditUtils::LeafNodeType; + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + GetLeafNodeTest{u"<div></div>", "div", {}, nullptr}, + GetLeafNodeTest{u"<div><br></div>", "div", {}, "div > br"}, + GetLeafNodeTest{ + u"<div>abc<br></div>", "div", {}, nullptr, "div", 0u}, + 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}, + "div > div"}, + GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>", + "div", + {}, + "div > div > br"}, + GetLeafNodeTest{u"<div><div><br></div><div><br></div></div>", + "div", + {LeafNodeType::LeafNodeOrChildBlock}, + "div > div"}, + GetLeafNodeTest{u"<div><!--abc--></div>", "div", {}, nullptr}, + GetLeafNodeTest{u"<div><!--abc--></div>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "div", + 0u}, + GetLeafNodeTest{u"<div><!--abc--><br></div>", "div", {}, "div > br"}, + GetLeafNodeTest{u"<div><!--abc--><br></div>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "div", + 0u}, + GetLeafNodeTest{ + u"<div><!--abc--><div><br></div><div><br></div></div>", + "div", + {}, + "div > div > br"}, + GetLeafNodeTest{ + u"<div><!--abc--><div><br></div><div><br></div></div>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "div", + 0u}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const target = body->QuerySelector( + nsDependentCString(testData.mContentSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(target); + const nsIContent* result = HTMLEditUtils::GetFirstLeafContent( + *target, testData.mTypes, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + EXPECT_EQ(result, testData.GetExpectedTarget(*body)) + << "GetFirstLeafContent: " << testData + << "(Got: " << ToString(RefPtr{result}) << ")"; + } +} + +TEST(HTMLEditUtilsTest, GetNextLeafContentOrNextBlockElement_Content) +{ + using LeafNodeType = HTMLEditUtils::LeafNodeType; + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + GetLeafNodeTest{u"<div><br></div><p><br></p>", "div", {}, "p"}, + GetLeafNodeTest{ + u"<div><br></div><!--abc--><p><br></p>", "div", {}, "p"}, + GetLeafNodeTest{u"<div><br></div><!--abc--><p><br></p>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "body", + 1u}, + GetLeafNodeTest{ + u"<div><br></div><span><br></span>", "div", {}, "span > br"}, + GetLeafNodeTest{u"<div><br></div><span><!--abc--><br></span>", + "div", + {}, + "span > br"}, + GetLeafNodeTest{u"<div><br></div><span><!--abc--><br></span>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "span", + 0u}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const target = body->QuerySelector( + nsDependentCString(testData.mContentSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(target); + const nsIContent* result = + HTMLEditUtils::GetNextLeafContentOrNextBlockElement( + *target, testData.mTypes, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode())) + << "GetNextLeafContentOrNextBlockElement: " << testData + << "(Got: " << ToString(RefPtr{result}) << ")"; + } +} + +// TODO: Test GetNextLeafContentOrNextBlockElement() which takes EditorDOMPoint + +TEST(HTMLEditUtilsTest, GetPreviousLeafContentOrPreviousBlockElement_Content) +{ + using LeafNodeType = HTMLEditUtils::LeafNodeType; + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + GetLeafNodeTest{u"<p><br></p><div><br></div>", "div", {}, "p"}, + GetLeafNodeTest{ + u"<p><br></p><!--abc--><div><br></div>", "div", {}, "p"}, + GetLeafNodeTest{u"<p><br></p><!--abc--><div><br></div>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "body", + 1u}, + GetLeafNodeTest{ + u"<span><br></span><div><br></div>", "div", {}, "span > br"}, + GetLeafNodeTest{u"<span><br><!--abc--></span><div><br></div>", + "div", + {}, + "span > br"}, + GetLeafNodeTest{u"<span><br><!--abc--></span><div><br></div>", + "div", + {LeafNodeType::TreatCommentAsLeafNode}, + nullptr, + "span", + 1u}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const target = body->QuerySelector( + nsDependentCString(testData.mContentSelector), IgnoreErrors()); + MOZ_RELEASE_ASSERT(target); + const nsIContent* result = + HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( + *target, testData.mTypes, + BlockInlineCheck::UseComputedDisplayOutsideStyle); + EXPECT_EQ(result, testData.GetExpectedTarget(*body->GetParentNode())) + << "GetPreviousLeafContentOrPreviousBlockElement: " << testData + << "(Got: " << ToString(RefPtr{result}) << ")"; + } +} + +// TODO: Test GetPreviousLeafContentOrPreviousBlockElement() which takes +// EditorDOMPoint + } // namespace mozilla diff --git a/testing/web-platform/meta/editing/run/inserthtml.html.ini b/testing/web-platform/meta/editing/run/inserthtml.html.ini @@ -105,12 +105,3 @@ [[["inserthtml","<!--abc-->"\]\] "<p>{}<span><!--foo--><br><!--bar--></span></p>" compare innerHTML] expected: FAIL - - [[["inserthtml","<!--abc-->"\]\] "<p><span><!--foo--><br><!--bar--></span>{}</p>" compare innerHTML] - expected: FAIL - - [[["inserthtml","<!--abc-->"\]\] "<p>{}<br></p>" compare innerHTML] - expected: FAIL - - [[["inserthtml","<!--abc-->"\]\] "<p><br>{}</p>" compare innerHTML] - expected: FAIL diff --git a/testing/web-platform/tests/editing/data/inserthtml.js b/testing/web-platform/tests/editing/data/inserthtml.js @@ -477,7 +477,7 @@ var browserTests = [ {"stylewithcss":[false,true,"",false,false,""],"inserthtml":[false,false,"",false,false,""]}], ["<p>[foo]</p>", [["inserthtml","<!--abc-->"]], - "<p><!--abc-->{}</p>", + "<p><!--abc--><br></p>", [true], {"inserthtml":[false,false,"",false,false,""]}], ["<p>{}<br></p>", @@ -487,7 +487,7 @@ var browserTests = [ {"inserthtml":[false,false,"",false,false,""]}], ["<p>{}<br></p>", [["inserthtml","<!--abc-->"]], - "<p><!--abc-->{}</p>", + "<p><!--abc--><br></p>", [true], {"inserthtml":[false,false,"",false,false,""]}], ["<p><!--foo-->{}<span><br></span><!--bar--></p>", @@ -521,7 +521,8 @@ var browserTests = [ {"inserthtml":[false,false,"",false,false,""]}], ["<p><br>{}</p>", [["inserthtml","<!--abc-->"]], - "<p><!--abc--></p>", + ["<p><br><!--abc--></p>", + "<p><!--abc--><br></p>"], [true], {"inserthtml":[false,false,"",false,false,""]}], ["<p><!--foo--><span><br></span>{}<!--bar--></p>", @@ -535,18 +536,25 @@ var browserTests = [ "<p><!--foo--><span><!--abc-->{}<br></span><!--bar--></p>", [true], {"inserthtml":[false,false,"",false,false,""]}], -// TODO: Fix the insertion not occurring at caret position. -// Updating the expected value of below two tests as to how other -// browsers behave these tests will still fail and since the insertion -// position is incorrect br tag gets removed, after fixing this it shouldn't. +// Don't insert text after the invisible <br> element. In the normal cases +// (meaning without the Comment nodes), caret should be put at the <br>. +// Therefore, inserting text should be done before the <br>. Then, the <br> +// becomes unnecessary so that it should be removed. ["<p><span><!--foo--><br><!--bar--></span>{}</p>", [["inserthtml","abc"]], - "<p><span><!--foo--><br><!--bar--></span>abc</p>", + "<p><span><!--foo-->abc<!--bar--></span></p>", [true], {"inserthtml":[false,false,"",false,false,""]}], +// It may be allowed to insert new Comment after the invisible <br> element. +// Therefore, the first one expects the new position as same as the `Selection`. +// On the other hand, if the inserting content includes visible things, +// everything should be inserted before the invisible <br>. Therefore, the +// new position can be before the <br> as same as handled as inserting visible +// things. ["<p><span><!--foo--><br><!--bar--></span>{}</p>", [["inserthtml","<!--abc-->"]], - "<p><span><!--foo--><br><!--bar--></span><!--abc--></p>", + ["<p><span><!--foo--><br><!--bar--></span><!--abc--></p>", + "<p><span><!--foo--><!--abc--><br><!--bar--></span></p>"], [true], {"inserthtml":[false,false,"",false,false,""]}],