tor-browser

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

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

Bug 1998077 - part 2: Add `WSType::EmptyInlineContainerElement` r=m_kato

An empty inline container element such as `<span></span>` has 2
meanings, if it's visible with specified style, e.g., has non-zero
padding, it should be treated like a replaced element like `<img>`.
However, if it's invisible, it should be treated as `Comment` or
empty `Text`.  Therefore, the scanner users need a special handling for
them in some cases.  So, it should be identified with the new `WSType`.

Unfortunately, neither
`HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone` nor
`HTMLEditUtils::IsVisibleElementEvenIfLeafNode` is testable within
GTest because it does not have layout information.

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

Diffstat:
Meditor/libeditor/HTMLEditUtils.cpp | 34+++++++++++++++++++---------------
Meditor/libeditor/HTMLEditUtils.h | 3++-
Meditor/libeditor/HTMLEditorDataTransfer.cpp | 5++++-
Meditor/libeditor/HTMLEditorDeleteHandler.cpp | 17++++++++++-------
Meditor/libeditor/HTMLEditorInsertLineBreakHandler.cpp | 8++++++--
Meditor/libeditor/HTMLEditorInsertParagraphHandler.cpp | 6+++++-
Meditor/libeditor/WSRunScanner.cpp | 19+++++++++++++++++++
Meditor/libeditor/WSRunScanner.h | 151+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Meditor/libeditor/WSRunScannerNestedClasses.cpp | 46++++++++++++++++++++++++++++++++++++++++++++--
9 files changed, 246 insertions(+), 43 deletions(-)

diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp @@ -466,7 +466,8 @@ bool HTMLEditUtils::IsFlexOrGridItem(const nsIContent& aContent) { } bool HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone( - const nsIContent& aContent) { + const nsIContent& aContent, + const nsIContent* aAncestorLimiter /* = nullptr */) { if (NS_WARN_IF(!aContent.IsInComposedDoc())) { return true; } @@ -474,12 +475,14 @@ bool HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone( aContent.InclusiveFlatTreeAncestorsOfType<Element>()) { RefPtr<const ComputedStyle> elementStyle = nsComputedDOMStyle::GetComputedStyleNoFlush(element); - if (NS_WARN_IF(!elementStyle)) { - continue; + if (MOZ_LIKELY(elementStyle)) { + const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay(); + if (MOZ_UNLIKELY(styleDisplay->mDisplay == StyleDisplay::None)) { + return true; + } } - const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay(); - if (MOZ_UNLIKELY(styleDisplay->mDisplay == StyleDisplay::None)) { - return true; + if (element == aAncestorLimiter) { + break; } } return false; @@ -1542,6 +1545,10 @@ bool HTMLEditUtils::IsEmptyNode(nsPresContext* aPresContext, continue; } + if (childContent->IsComment()) { + continue; + } + MOZ_ASSERT(childContent != &aNode); if (!aOptions.contains(EmptyCheckOption::TreatSingleBRElementAsVisible) && @@ -2824,17 +2831,14 @@ nsIContent* HTMLEditUtils::GetContentToPreserveInlineStyles( if (nextVisibleThing.InVisibleOrCollapsibleCharacters()) { return nextVisibleThing.TextPtr(); } - if (nextVisibleThing.IsContentEditableRoot()) { + if (nextVisibleThing.ContentIsEditableRoot()) { break; } - // Ignore empty inline container elements because it's not visible for - // users so that using the style will appear suddenly from point of - // view of users. - if (nextVisibleThing.ReachedSpecialContent() && - nextVisibleThing.IsContentEditable() && - nextVisibleThing.ContentIsElement() && - !nextVisibleThing.ElementPtr()->HasChildNodes() && - HTMLEditUtils::IsContainerNode(*nextVisibleThing.ElementPtr())) { + // Ignore invisible empty inline container elements because it's not visible + // for users so that using the style will appear suddenly from point of view + // of users. + if (nextVisibleThing.ReachedEditableInvisibleEmptyInlineContainerElement( + &aEditingHost)) { point.SetAfter(nextVisibleThing.ElementPtr()); continue; } diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h @@ -783,7 +783,8 @@ class HTMLEditUtils final { /** * Return true if `display` of inclusive ancestor of aContent is `none`. */ - static bool IsInclusiveAncestorCSSDisplayNone(const nsIContent& aContent); + [[nodiscard]] static bool IsInclusiveAncestorCSSDisplayNone( + const nsIContent& aContent, const nsIContent* aAncestorLimiter = nullptr); /** * IsVisiblePreformattedNewLine() and IsInvisiblePreformattedNewLine() return diff --git a/editor/libeditor/HTMLEditorDataTransfer.cpp b/editor/libeditor/HTMLEditorDataTransfer.cpp @@ -559,9 +559,12 @@ HTMLEditor::HTMLWithContextInserter::GetNewCaretPointAfterInsertingHTML( {WSRunScanner::Option::OnlyEditableNodes}, EditorRawDOMPoint(prevVisibleThing.BRElementPtr())); if (prevVisibleThingOfBRElement.InVisibleOrCollapsibleCharacters()) { + // XXX Why not end of the text node? pointToPutCaret = prevVisibleThingOfBRElement .PointAfterReachedContent<EditorDOMPoint>(); - } else if (prevVisibleThingOfBRElement.ReachedSpecialContent()) { + } else if (prevVisibleThingOfBRElement.ReachedSpecialContent() || + prevVisibleThingOfBRElement + .ReachedEmptyInlineContainerElement()) { pointToPutCaret = prevVisibleThingOfBRElement .PointAfterReachedContentNode<EditorDOMPoint>(); } diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -866,7 +866,7 @@ nsresult HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDelete( if (scanFromCaretPointResult.BRElementPtr() == &aEditingHost) { return NS_OK; } - if (!scanFromCaretPointResult.IsContentEditable()) { + if (!scanFromCaretPointResult.ContentIsEditable()) { return NS_SUCCESS_DOM_NO_OPERATION; } if (scanFromCaretPointResult.ReachedInvisibleBRElement()) { @@ -1162,7 +1162,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler::Run( if (scanFromCaretPointResult.BRElementPtr() == &aEditingHost) { return EditActionResult::HandledResult(); } - if (!scanFromCaretPointResult.IsContentEditable()) { + if (!scanFromCaretPointResult.ContentIsEditable()) { return EditActionResult::CanceledResult(); } if (scanFromCaretPointResult.ReachedInvisibleBRElement()) { @@ -1276,6 +1276,7 @@ HTMLEditor::AutoDeleteRangesHandler::ComputeRangesToDeleteAroundCollapsedRanges( } if (aScanFromCaretPointResult.ReachedSpecialContent() || + aScanFromCaretPointResult.ReachedEmptyInlineContainerElement() || aScanFromCaretPointResult.ReachedBRElement() || aScanFromCaretPointResult.ReachedHRElement() || aScanFromCaretPointResult.ReachedNonEditableOtherBlockElement()) { @@ -1408,6 +1409,7 @@ HTMLEditor::AutoDeleteRangesHandler::HandleDeleteAroundCollapsedRanges( } if (aScanFromCaretPointResult.ReachedSpecialContent() || + aScanFromCaretPointResult.ReachedEmptyInlineContainerElement() || aScanFromCaretPointResult.ReachedBRElement() || aScanFromCaretPointResult.ReachedHRElement() || aScanFromCaretPointResult.ReachedNonEditableOtherBlockElement()) { @@ -1657,7 +1659,8 @@ nsIContent* HTMLEditor::AutoDeleteRangesHandler::GetAtomicContentToDelete( const WSScanResult& aScanFromCaretPointResult) { MOZ_ASSERT(aScanFromCaretPointResult.GetContent()); - if (!aScanFromCaretPointResult.ReachedSpecialContent()) { + if (!aScanFromCaretPointResult.ReachedSpecialContent() && + !aScanFromCaretPointResult.ReachedEmptyInlineContainerElement()) { return aScanFromCaretPointResult.GetContent(); } @@ -2082,7 +2085,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: const WSScanResult maybePreviousText = scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom( EditorRawDOMPoint(mBRElement)); - if (maybePreviousText.IsContentEditable() && + if (maybePreviousText.ContentIsEditable() && maybePreviousText.InVisibleOrCollapsibleCharacters() && !HTMLEditor::GetLinkElement(maybePreviousText.TextPtr())) { return maybePreviousText.PointAfterReachedContent<EditorDOMPoint>(); @@ -2090,7 +2093,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: const WSScanResult maybeNextText = scanner.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( EditorRawDOMPoint::After(*mBRElement)); - if (maybeNextText.IsContentEditable() && + if (maybeNextText.ContentIsEditable() && maybeNextText.InVisibleOrCollapsibleCharacters()) { return maybeNextText.PointAtReachedContent<EditorDOMPoint>(); } @@ -3705,7 +3708,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: const WSScanResult maybePreviousText = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( {}, startOfRightContent, &aEditingHost); - if (maybePreviousText.IsContentEditable() && + if (maybePreviousText.ContentIsEditable() && maybePreviousText.InVisibleOrCollapsibleCharacters()) { nsresult rv = aHTMLEditor.CollapseSelectionTo( maybePreviousText.PointAfterReachedContent<EditorRawDOMPoint>()); @@ -5657,7 +5660,7 @@ Result<DeleteRangeResult, nsresult> HTMLEditor::AutoDeleteRangesHandler:: const WSScanResult maybePreviousText = WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( {}, maybeDeepStartOfRightContent, &aEditingHost); - if (maybePreviousText.IsContentEditable() && + if (maybePreviousText.ContentIsEditable() && maybePreviousText.InVisibleOrCollapsibleCharacters()) { return maybePreviousText.PointAfterReachedContent<EditorDOMPoint>(); } diff --git a/editor/libeditor/HTMLEditorInsertLineBreakHandler.cpp b/editor/libeditor/HTMLEditorInsertLineBreakHandler.cpp @@ -235,10 +235,14 @@ nsresult HTMLEditor::AutoInsertLineBreakHandler::HandleInsertBRElement() { pointToPutCaret = forwardScanFromAfterBRElementResult .PointAtReachedContent<EditorDOMPoint>(); } else if (forwardScanFromAfterBRElementResult.ReachedSpecialContent()) { - // Next inserting text should be inserted into styled inline elements if - // they have first visible thing in the new line. pointToPutCaret = forwardScanFromAfterBRElementResult .PointAtReachedContent<EditorDOMPoint>(); + } else if (forwardScanFromAfterBRElementResult + .ReachedEmptyInlineContainerElement()) { + // Next inserting text should be inserted into styled inline elements if + // they have first visible thing in the new line. + pointToPutCaret = + EditorDOMPoint(forwardScanFromAfterBRElementResult.ElementPtr(), 0); } nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret); diff --git a/editor/libeditor/HTMLEditorInsertParagraphHandler.cpp b/editor/libeditor/HTMLEditorInsertParagraphHandler.cpp @@ -1060,7 +1060,9 @@ nsresult HTMLEditor::AutoInsertParagraphHandler:: if (!backwardScanFromPointToCreateNewBRElementResult .InVisibleOrCollapsibleCharacters() && !backwardScanFromPointToCreateNewBRElementResult - .ReachedSpecialContent()) { + .ReachedSpecialContent() && + !backwardScanFromPointToCreateNewBRElementResult + .ReachedEmptyInlineContainerElement()) { return NS_SUCCESS_DOM_NO_OPERATION; } const WSScanResult forwardScanFromPointAfterNewBRElementResult = @@ -1075,6 +1077,8 @@ nsresult HTMLEditor::AutoInsertParagraphHandler:: if (!forwardScanFromPointAfterNewBRElementResult .InVisibleOrCollapsibleCharacters() && !forwardScanFromPointAfterNewBRElementResult.ReachedSpecialContent() && + !forwardScanFromPointAfterNewBRElementResult + .ReachedEmptyInlineContainerElement() && // In case we're at the very end. !forwardScanFromPointAfterNewBRElementResult .ReachedCurrentBlockBoundary()) { diff --git a/editor/libeditor/WSRunScanner.cpp b/editor/libeditor/WSRunScanner.cpp @@ -36,6 +36,7 @@ void WSScanResult::AssertIfInvalidData(const WSRunScanner& aScanner) const { mReason == WSType::CollapsibleWhiteSpaces || mReason == WSType::BRElement || mReason == WSType::PreformattedLineBreak || + mReason == WSType::EmptyInlineContainerElement || mReason == WSType::SpecialContent || mReason == WSType::CurrentBlockBoundary || mReason == WSType::OtherBlockBoundary || @@ -73,6 +74,15 @@ void WSScanResult::AssertIfInvalidData(const WSRunScanner& aScanner) const { 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)); + MOZ_ASSERT_IF( mReason == WSType::SpecialContent, (mContent->IsText() && !mContent->IsEditable()) || (!mContent->IsHTMLElement(nsGkAtoms::br) && @@ -80,6 +90,13 @@ 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))); MOZ_ASSERT_IF( mReason == WSType::OtherBlockBoundary, @@ -1030,6 +1047,7 @@ WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent( if (textFragmentDataAtStart.EndsByVisibleBRElement()) { startContent = textFragmentDataAtStart.EndReasonBRElementPtr(); } else if (textFragmentDataAtStart.EndsBySpecialContent() || + textFragmentDataAtStart.EndsByEmptyInlineContainerElement() || (textFragmentDataAtStart.EndsByOtherBlockElement() && !HTMLEditUtils::IsContainerNode( *textFragmentDataAtStart @@ -1052,6 +1070,7 @@ WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent( if (textFragmentDataAtEnd.StartsFromVisibleBRElement()) { endContent = textFragmentDataAtEnd.StartReasonBRElementPtr(); } else if (textFragmentDataAtEnd.StartsFromSpecialContent() || + textFragmentDataAtEnd.EndsByEmptyInlineContainerElement() || (textFragmentDataAtEnd.StartsFromOtherBlockElement() && !HTMLEditUtils::IsContainerNode( *textFragmentDataAtEnd diff --git a/editor/libeditor/WSRunScanner.h b/editor/libeditor/WSRunScanner.h @@ -51,6 +51,11 @@ class MOZ_STACK_CLASS WSScanResult final { CollapsibleWhiteSpaces, // Visible characters except collapsible white-spaces. NonCollapsibleCharacters, + // Empty inline container elemnet such as `<span></span>`. Note that it may + // be visible if its border/padding is not 0 for example. + // NOTE: This won't be used if it's the inline editing host at the scan + // start point. + EmptyInlineContainerElement, // Special content such as `<img>`, etc. SpecialContent, // <br> element. @@ -81,6 +86,8 @@ class MOZ_STACK_CLASS WSScanResult final { return aStream << "WSType::CollapsibleWhiteSpaces"; case WSType::NonCollapsibleCharacters: return aStream << "WSType::NonCollapsibleCharacters"; + case WSType::EmptyInlineContainerElement: + return aStream << "WSType::EmptyInlineContainerElement"; case WSType::SpecialContent: return aStream << "WSType::SpecialContent"; case WSType::BRElement: @@ -180,13 +187,22 @@ class MOZ_STACK_CLASS WSScanResult final { } /** - * Returns true if found or reached content is editable. + * Return true if found or reached content is editable. */ - bool IsContentEditable() const { return mContent && mContent->IsEditable(); } + [[nodiscard]] bool ContentIsEditable() const { + return mContent && HTMLEditUtils::IsSimplyEditableNode(*mContent); + } + + /** + * Return true if found or reached content is removable node. + */ + [[nodiscard]] bool ContentIsRemovable() const { + return mContent && HTMLEditUtils::IsRemovableNode(*mContent); + } - [[nodiscard]] bool IsContentEditableRoot() const { - return mContent && mContent->IsElement() && - HTMLEditUtils::ElementIsEditableRoot(*mContent->AsElement()); + [[nodiscard]] bool ContentIsEditableRoot() const { + return ContentIsElement() && + HTMLEditUtils::ElementIsEditableRoot(*ElementPtr()); } /** @@ -258,10 +274,97 @@ class MOZ_STACK_CLASS WSScanResult final { } /** + * The scanner reached an empty inline container element such as + * <span></span>. Note that the element may be visible, e.g., may have + * non-zero border/padding. + */ + [[nodiscard]] constexpr bool ReachedEmptyInlineContainerElement() const { + return mReason == WSType::EmptyInlineContainerElement; + } + /** + * The scanner reached an editable empty inline container element such as + * <span></span>. Note that the element may be visible, e.g., may have + * non-zero border/padding. + */ + [[nodiscard]] bool ReachedEditableEmptyInlineContainerElement() const { + return ReachedEmptyInlineContainerElement() && ContentIsEditable(); + } + /** + * The scanner reached a removable empty inline container element such as + * <span></span>. Note that the element may be visible, e.g., may have + * non-zero border/padding. + */ + [[nodiscard]] bool ReachedRemovableEmptyInlineContainerElement() const { + return ReachedEmptyInlineContainerElement() && ContentIsRemovable(); + } + + /** + * The scanner reached an empty inline container element which is visible. + */ + [[nodiscard]] bool ReachedVisibleEmptyInlineContainerElement( + const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { + return ReachedEmptyInlineContainerElement() && + HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*ElementPtr()) && + !HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone( + *ElementPtr(), aAncestorLimiterToCheckDisplayNone); + } + /** + * The scanner reached an editable empty inline container element which is + * visible. + */ + [[nodiscard]] bool ReachedEditableEmptyInlineContainerElement( + const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { + return ReachedVisibleEmptyInlineContainerElement( + aAncestorLimiterToCheckDisplayNone) && + ContentIsEditable(); + } + /** + * The scanner reached a removable empty inline container element which is + * visible. + */ + [[nodiscard]] bool ReachedRemovableEmptyInlineContainerElement( + const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { + return ReachedVisibleEmptyInlineContainerElement( + aAncestorLimiterToCheckDisplayNone) && + ContentIsRemovable(); + } + + /** + * The scanner reached an empty inline container element which is invisible. + */ + [[nodiscard]] bool ReachedInvisibleEmptyInlineContainerElement( + const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { + return ReachedEmptyInlineContainerElement() && + (!HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*ElementPtr()) || + HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone( + *ElementPtr(), aAncestorLimiterToCheckDisplayNone)); + } + /** + * The scanner reached an editable empty inline container element which is + * invisible. + */ + [[nodiscard]] bool ReachedEditableInvisibleEmptyInlineContainerElement( + const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { + return ReachedInvisibleEmptyInlineContainerElement( + aAncestorLimiterToCheckDisplayNone) && + ContentIsEditable(); + } + /** + * The scanner reached a removable empty inline container element which is + * invisible. + */ + [[nodiscard]] bool ReachedRemovableInvisibleEmptyInlineContainerElement( + const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { + return ReachedEditableInvisibleEmptyInlineContainerElement( + aAncestorLimiterToCheckDisplayNone) && + ContentIsRemovable(); + } + + /** * The scanner reached <img> or something which is inline and is not a * container. */ - bool ReachedSpecialContent() const { + [[nodiscard]] constexpr bool ReachedSpecialContent() const { return mReason == WSType::SpecialContent; } @@ -397,7 +500,7 @@ class MOZ_STACK_CLASS WSScanResult final { private: nsCOMPtr<nsIContent> mContent; Maybe<uint32_t> mOffset; - WSType mReason; + WSType mReason = WSType::NotInitialized; ScanDirection mDirection = ScanDirection::Backward; }; @@ -681,9 +784,12 @@ class MOZ_STACK_CLASS WSRunScanner final { bool StartsFromNonCollapsibleCharacters() const { return mLeftWSType == WSType::NonCollapsibleCharacters; } - bool StartsFromSpecialContent() const { + [[nodiscard]] constexpr bool StartsFromSpecialContent() const { return mLeftWSType == WSType::SpecialContent; } + [[nodiscard]] constexpr bool StartsFromEmptyInlineContainerElement() const { + return mLeftWSType == WSType::EmptyInlineContainerElement; + } bool StartsFromPreformattedLineBreak() const { return mLeftWSType == WSType::PreformattedLineBreak; } @@ -698,9 +804,12 @@ class MOZ_STACK_CLASS WSRunScanner final { bool EndsByTrailingWhiteSpaces() const { return mRightWSType == WSType::TrailingWhiteSpaces; } - bool EndsBySpecialContent() const { + [[nodiscard]] constexpr bool EndsBySpecialContent() const { return mRightWSType == WSType::SpecialContent; } + [[nodiscard]] constexpr bool EndsByEmptyInlineContainerElement() const { + return mRightWSType == WSType::EmptyInlineContainerElement; + } bool EndsByBRElement() const { return mRightWSType == WSType::BRElement; } bool EndsByPreformattedLineBreak() const { return mRightWSType == WSType::PreformattedLineBreak; @@ -908,9 +1017,12 @@ class MOZ_STACK_CLASS WSRunScanner final { bool IsNonCollapsibleCharacters() const { return mReason == WSType::NonCollapsibleCharacters; } - bool IsSpecialContent() const { + [[nodiscard]] constexpr bool IsSpecialContent() const { return mReason == WSType::SpecialContent; } + [[nodiscard]] constexpr bool IsEmptyInlineContainerElement() const { + return mReason == WSType::EmptyInlineContainerElement; + } bool IsBRElement() const { return mReason == WSType::BRElement; } bool IsPreformattedLineBreak() const { return mReason == WSType::PreformattedLineBreak; @@ -960,8 +1072,9 @@ class MOZ_STACK_CLASS WSRunScanner final { EditorDOMPoint mPoint; // Must be one of WSType::NotInitialized, // WSType::NonCollapsibleCharacters, WSType::SpecialContent, - // WSType::BRElement, WSType::CurrentBlockBoundary, - // WSType::OtherBlockBoundary or WSType::InlineEditingHostBoundary. + // WSType::EmptyInlineContainerElement, WSType::BRElement, + // WSType::CurrentBlockBoundary, WSType::OtherBlockBoundary or + // WSType::InlineEditingHostBoundary. WSType mReason = WSType::NotInitialized; }; @@ -1026,7 +1139,12 @@ class MOZ_STACK_CLASS WSRunScanner final { bool StartsFromNonCollapsibleCharacters() const { return mStart.IsNonCollapsibleCharacters(); } - bool StartsFromSpecialContent() const { return mStart.IsSpecialContent(); } + [[nodiscard]] bool StartsFromSpecialContent() const { + return mStart.IsSpecialContent(); + } + [[nodiscard]] bool StartsFromEmptyInlineContainerElement() const { + return mStart.IsEmptyInlineContainerElement(); + } bool StartsFromBRElement() const { return mStart.IsBRElement(); } bool StartsFromVisibleBRElement() const { return StartsFromBRElement() && @@ -1053,7 +1171,12 @@ class MOZ_STACK_CLASS WSRunScanner final { bool EndsByNonCollapsibleCharacters() const { return mEnd.IsNonCollapsibleCharacters(); } - bool EndsBySpecialContent() const { return mEnd.IsSpecialContent(); } + [[nodiscard]] bool EndsBySpecialContent() const { + return mEnd.IsSpecialContent(); + } + [[nodiscard]] bool EndsByEmptyInlineContainerElement() const { + return mEnd.IsEmptyInlineContainerElement(); + } bool EndsByBRElement() const { return mEnd.IsBRElement(); } bool EndsByVisibleBRElement() const { return EndsByBRElement() && diff --git a/editor/libeditor/WSRunScannerNestedClasses.cpp b/editor/libeditor/WSRunScannerNestedClasses.cpp @@ -274,6 +274,19 @@ WSRunScanner::TextFragmentData::BoundaryData WSRunScanner::TextFragmentData:: WSType::OtherBlockBoundary); } + if (previousLeafContentOrBlock->IsElement() && + HTMLEditUtils::IsEmptyInlineContainer( + *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. + return BoundaryData(aPoint, *previousLeafContentOrBlock, + WSType::EmptyInlineContainerElement); + } + if (!previousLeafContentOrBlock->IsText() || (aOptions.contains(Option::OnlyEditableNodes) && HTMLEditUtils::IsSimplyEditableNode(*previousLeafContentOrBlock) != @@ -438,6 +451,19 @@ WSRunScanner::TextFragmentData::BoundaryData::ScanCollapsibleWhiteSpaceEndFrom( WSType::OtherBlockBoundary); } + if (nextLeafContentOrBlock->IsElement() && + HTMLEditUtils::IsEmptyInlineContainer( + *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. + return BoundaryData(aPoint, *nextLeafContentOrBlock, + WSType::EmptyInlineContainerElement); + } + if (!nextLeafContentOrBlock->IsText() || (aOptions.contains(Option::OnlyEditableNodes) && HTMLEditUtils::IsSimplyEditableNode(*nextLeafContentOrBlock) != @@ -599,9 +625,19 @@ WSRunScanner::TextFragmentData::VisibleWhiteSpacesDataRef() const { // If all things are obviously visible, we can return range for all of the // things quickly. const bool mayHaveInvisibleLeadingSpace = - !StartsFromNonCollapsibleCharacters() && !StartsFromSpecialContent(); + !StartsFromNonCollapsibleCharacters() && !StartsFromSpecialContent() && + !(StartsFromEmptyInlineContainerElement() && + // XXX I think we don't need to check display:none here for now + // because in the other cases, we don't do that. + HTMLEditUtils::IsVisibleElementEvenIfLeafNode( + *GetStartReasonContent())); const bool mayHaveInvisibleTrailingWhiteSpace = !EndsByNonCollapsibleCharacters() && !EndsBySpecialContent() && + !(EndsByEmptyInlineContainerElement() && + // XXX I think we don't need to check display:none here for now + // because in the other cases, we don't do that. + HTMLEditUtils::IsVisibleElementEvenIfLeafNode( + *GetEndReasonContent())) && !EndsByBRElement() && !EndsByInvisiblePreformattedLineBreak(); if (!mayHaveInvisibleLeadingSpace && !mayHaveInvisibleTrailingWhiteSpace) { @@ -1398,7 +1434,10 @@ EditorDOMPointInText WSRunScanner::TextFragmentData:: const VisibleWhiteSpacesData& visibleWhiteSpaces = VisibleWhiteSpacesDataRef(); if (!visibleWhiteSpaces.StartsFromNonCollapsibleCharacters() && - !visibleWhiteSpaces.StartsFromSpecialContent()) { + !visibleWhiteSpaces.StartsFromSpecialContent() && + !(visibleWhiteSpaces.StartsFromEmptyInlineContainerElement() && + HTMLEditUtils::IsVisibleElementEvenIfLeafNode( + *GetStartReasonContent()))) { return EditorDOMPointInText(); } return atPreviousChar; @@ -1456,6 +1495,9 @@ EditorDOMPointInText WSRunScanner::TextFragmentData:: VisibleWhiteSpacesDataRef(); if (!visibleWhiteSpaces.EndsByNonCollapsibleCharacters() && !visibleWhiteSpaces.EndsBySpecialContent() && + !(visibleWhiteSpaces.EndsByEmptyInlineContainerElement() && + HTMLEditUtils::IsVisibleElementEvenIfLeafNode( + *GetEndReasonContent())) && !visibleWhiteSpaces.EndsByBRElement()) { return EditorDOMPointInText(); }