tor-browser

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

commit 521a873deb991d970c6e8761daa8342a659be29e
parent 72dc90136c2ea085b7225df230c7ba7033662c86
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date:   Thu, 18 Dec 2025 06:50:20 +0000

Bug 2005895 - Make `AutoEmptyBlockAncestorDeleter::GetNewCaretPosition()` never return point after a line break before a block boundary r=m_kato

The caret should be put before the last line break in a block if there
is no visible content between the line break and the end block boundary.

Additionally,
`WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt()`
should delete the preceding block's invisible `<br>` too.

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

Diffstat:
Meditor/libeditor/HTMLEditUtils.cpp | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/libeditor/HTMLEditUtils.h | 9+++++++++
Meditor/libeditor/HTMLEditorDeleteHandler.cpp | 38++++++++++++++++++++++++--------------
Meditor/libeditor/HTMLEditorNestedClasses.h | 4++--
Meditor/libeditor/WSRunScanner.h | 19+++++++++++++++++++
Meditor/libeditor/WhiteSpaceVisibilityKeeper.cpp | 27+++++++++++++++++++++++----
Meditor/libeditor/gtest/TestHTMLEditUtils.cpp | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/meta/editing/run/delete.html.ini | 6------
Mtesting/web-platform/tests/editing/data/delete.js | 6++++++
Mtesting/web-platform/tests/editing/data/forwarddelete.js | 6++++++
10 files changed, 210 insertions(+), 26 deletions(-)

diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp @@ -146,6 +146,31 @@ template Result<EditorRawDOMPoint, nsresult> HTMLEditUtils::ComputePointToPutCaretInElementIfOutside( const Element& aElement, const EditorRawDOMPoint& aCurrentPoint); +template Maybe<EditorLineBreak> +HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorDOMPoint&, const Element&); +template Maybe<EditorRawLineBreak> +HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorDOMPoint&, const Element&); +template Maybe<EditorLineBreak> +HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorRawDOMPoint&, const Element&); +template Maybe<EditorRawLineBreak> +HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorRawDOMPoint&, const Element&); +template Maybe<EditorLineBreak> +HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorDOMPointInText&, const Element&); +template Maybe<EditorRawLineBreak> +HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorDOMPointInText&, const Element&); +template Maybe<EditorLineBreak> +HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorRawDOMPointInText&, const Element&); +template Maybe<EditorRawLineBreak> +HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorRawDOMPointInText&, const Element&); + template bool HTMLEditUtils::IsSameCSSColorValue(const nsAString& aColorA, const nsAString& aColorB); template bool HTMLEditUtils::IsSameCSSColorValue(const nsACString& aColorA, @@ -2970,6 +2995,31 @@ HTMLEditUtils::ComputePointToPutCaretInElementIfOutside( } // static +template <typename EditorLineBreakType, typename EditorDOMPointType> +Maybe<EditorLineBreakType> +HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorDOMPointType& aPoint, const Element& aEditingHost) { + MOZ_ASSERT(aPoint.IsSet()); + if (MOZ_UNLIKELY(!aPoint.IsInContentNode())) { + return Nothing{}; + } + const WSScanResult previousThing = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary({}, aPoint, + &aEditingHost); + if (!previousThing.ReachedLineBreak()) { + return Nothing{}; // No preceding line break. + } + const WSScanResult nextThing = + WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary({}, aPoint, + &aEditingHost); + if (!nextThing.ReachedBlockBoundary()) { + return Nothing{}; // The line break is not followed by a block boundary so + // that it's a visible line break. + } + return Some(previousThing.CreateEditorLineBreak<EditorLineBreakType>()); +} + +// static bool HTMLEditUtils::IsInlineStyleSetByElement( const nsIContent& aContent, const EditorInlineStyle& aStyle, const nsAString* aValue, nsAString* aOutValue /* = nullptr */) { diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h @@ -2844,6 +2844,15 @@ class HTMLEditUtils final { const Element& aElement, const EditorDOMPointTypeInput& aCurrentPoint); /** + * Return a line break if aPoint is after a line break which is immediately + * before a block boundary. + */ + template <typename EditorLineBreakType, typename EditorDOMPointType> + static Maybe<EditorLineBreakType> + GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( + const EditorDOMPointType& aPoint, const Element& aEditingHost); + + /** * Content-based query returns true if * <mHTMLProperty mAttribute=mAttributeValue> effects aContent. If there is * such a element, but another element whose attribute value does not match diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -7191,7 +7191,8 @@ HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter:: Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler:: AutoEmptyBlockAncestorDeleter::GetNewCaretPosition( const HTMLEditor& aHTMLEditor, - nsIEditor::EDirection aDirectionAndAmount) const { + nsIEditor::EDirection aDirectionAndAmount, + const Element& aEditingHost) const { MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement); MOZ_ASSERT(mEmptyInclusiveAncestorBlockElement->GetParentElement()); MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); @@ -7206,8 +7207,7 @@ Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler:: EditorDOMPoint::After(mEmptyInclusiveAncestorBlockElement)); MOZ_ASSERT(afterEmptyBlock.IsSet()); if (nsIContent* nextContentOfEmptyBlock = HTMLEditUtils::GetNextContent( - afterEmptyBlock, {}, BlockInlineCheck::Unused, - aHTMLEditor.ComputeEditingHost())) { + afterEmptyBlock, {}, BlockInlineCheck::Unused, &aEditingHost)) { EditorDOMPoint pt = HTMLEditUtils::GetGoodCaretPointFor<EditorDOMPoint>( *nextContentOfEmptyBlock, aDirectionAndAmount); if (!pt.IsSet()) { @@ -7225,20 +7225,30 @@ Result<CaretPoint, nsresult> HTMLEditor::AutoDeleteRangesHandler:: case nsIEditor::ePreviousWord: case nsIEditor::eToBeginningOfLine: { // Collapse Selection to previous editable node of the empty block - // if there is. Otherwise, to after the empty block. + // if there is. EditorRawDOMPoint atEmptyBlock(mEmptyInclusiveAncestorBlockElement); - if (nsIContent* previousContentOfEmptyBlock = + if (nsIContent* const previousContentOfEmptyBlock = HTMLEditUtils::GetPreviousContent( atEmptyBlock, {WalkTreeOption::IgnoreNonEditableNode}, - BlockInlineCheck::Unused, aHTMLEditor.ComputeEditingHost())) { - EditorDOMPoint pt = HTMLEditUtils::GetGoodCaretPointFor<EditorDOMPoint>( - *previousContentOfEmptyBlock, aDirectionAndAmount); - if (!pt.IsSet()) { + BlockInlineCheck::Unused, &aEditingHost)) { + const EditorRawDOMPoint atEndOfPreviousContent = + HTMLEditUtils::GetGoodCaretPointFor<EditorRawDOMPoint>( + *previousContentOfEmptyBlock, aDirectionAndAmount); + if (!atEndOfPreviousContent.IsSet()) { NS_WARNING("HTMLEditUtils::GetGoodCaretPointFor() failed"); return Err(NS_ERROR_FAILURE); } - return CaretPoint(std::move(pt)); - } + // If the previous content is between a preceding line break and the + // block boundary of current empty block, let's move caret to the line + // break if there is no visible things between them. + const Maybe<EditorRawLineBreak> precedingLineBreak = + HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem< + EditorRawLineBreak>(atEndOfPreviousContent, aEditingHost); + return precedingLineBreak.isSome() + ? CaretPoint(precedingLineBreak->To<EditorDOMPoint>()) + : CaretPoint(atEndOfPreviousContent.To<EditorDOMPoint>()); + } + // Otherwise, let's put caret next to the deleting block. auto afterEmptyBlock = EditorDOMPoint::After(*mEmptyInclusiveAncestorBlockElement); if (NS_WARN_IF(!afterEmptyBlock.IsSet())) { @@ -7309,7 +7319,7 @@ HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::Run( : EditorDOMPoint()); } Result<CaretPoint, nsresult> caretPointOrError = - GetNewCaretPosition(aHTMLEditor, aDirectionAndAmount); + GetNewCaretPosition(aHTMLEditor, aDirectionAndAmount, aEditingHost); NS_WARNING_ASSERTION( caretPointOrError.isOk(), "AutoEmptyBlockAncestorDeleter::GetNewCaretPosition() failed"); @@ -7343,10 +7353,10 @@ HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter::Run( "DeleteContentNodeAndJoinTextNodesAroundIt() failed"); return caretPointOrError.propagateErr(); } + trackPointToPutCaret.Flush(StopTracking::Yes); caretPointOrError.unwrap().MoveCaretPointTo( pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion}); - trackEmptyBlockPoint.FlushAndStopTracking(); - trackPointToPutCaret.FlushAndStopTracking(); + trackEmptyBlockPoint.Flush(StopTracking::Yes); if (NS_WARN_IF(!atEmptyInclusiveAncestorBlockElement .IsInContentNodeAndValidInComposedDoc()) || NS_WARN_IF(pointToPutCaret.IsSet() && diff --git a/editor/libeditor/HTMLEditorNestedClasses.h b/editor/libeditor/HTMLEditorNestedClasses.h @@ -1690,8 +1690,8 @@ HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter final { * `mEmptyInclusiveAncestorBlockElement`. */ [[nodiscard]] Result<CaretPoint, nsresult> GetNewCaretPosition( - const HTMLEditor& aHTMLEditor, - nsIEditor::EDirection aDirectionAndAmount) const; + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + const Element& aEditingHost) const; RefPtr<Element> mEmptyInclusiveAncestorBlockElement; }; diff --git a/editor/libeditor/WSRunScanner.h b/editor/libeditor/WSRunScanner.h @@ -375,6 +375,25 @@ class MOZ_STACK_CLASS WSScanResult final { } } + friend std::ostream& operator<<(std::ostream& aStream, + const ScanDirection& aDirection) { + return aStream << (aDirection == ScanDirection::Backward + ? "ScanDirection::Backward" + : "ScanDirection::Forward"); + } + + friend std::ostream& operator<<(std::ostream& aStream, + const WSScanResult& aResult) { + aStream << "{ mReason: " << aResult.mReason; + if (aResult.mReason == WSType::NotInitialized || + aResult.mReason == WSType::InUncomposedDoc) { + return aStream << " }"; + } + return aStream << ", mContent: " << aResult.mContent + << ", mOffset: " << aResult.mOffset + << ", mDirection: " << aResult.mDirection << " }"; + } + private: nsCOMPtr<nsIContent> mContent; Maybe<uint32_t> mOffset; diff --git a/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp b/editor/libeditor/WhiteSpaceVisibilityKeeper.cpp @@ -2399,12 +2399,13 @@ WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt( WSScanResult nextThingOfCaretPoint = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( {}, pointToPutCaret); - if (nextThingOfCaretPoint.ReachedBRElement() || - nextThingOfCaretPoint.ReachedPreformattedLineBreak()) { + Maybe<EditorLineBreak> lineBreak; + if (nextThingOfCaretPoint.ReachedLineBreak()) { + lineBreak.emplace( + nextThingOfCaretPoint.CreateEditorLineBreak<EditorLineBreak>()); nextThingOfCaretPoint = WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary( - {}, nextThingOfCaretPoint - .PointAfterReachedContent<EditorRawDOMPoint>()); + {}, lineBreak->After<EditorRawDOMPoint>()); } if (nextThingOfCaretPoint.ReachedBlockBoundary()) { const EditorDOMPoint atBlockBoundary = @@ -2423,6 +2424,24 @@ WhiteSpaceVisibilityKeeper::DeleteContentNodeAndJoinTextNodesAroundIt( if (NS_WARN_IF(!aContentToDelete.IsInComposedDoc())) { return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE); } + // If the previous content ends with an invisible line break, let's + // delete it. + if (lineBreak.isSome() && lineBreak->IsInComposedDoc()) { + const WSScanResult prevThing = + WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary( + {}, lineBreak->To<EditorRawDOMPoint>(), &aEditingHost); + if (!prevThing.ReachedLineBoundary()) { + Result<EditorDOMPoint, nsresult> pointOrError = + aHTMLEditor.DeleteLineBreakWithTransaction( + lineBreak.ref(), nsIEditor::eStrip, aEditingHost); + if (MOZ_UNLIKELY(pointOrError.isErr())) { + NS_WARNING( + "HTMLEditor::DeleteLineBreakWithTransaction() failed"); + return pointOrError.propagateErr(); + } + trackPointToPutCaret->Flush(StopTracking::No); + } + } } } // Similarly, we may put caret into the following block (this is the diff --git a/editor/libeditor/gtest/TestHTMLEditUtils.cpp b/editor/libeditor/gtest/TestHTMLEditUtils.cpp @@ -1701,4 +1701,75 @@ TEST(HTMLEditUtilsTest, GetPreviousLeafContentOrPreviousBlockElement_Content) // TODO: Test GetPreviousLeafContentOrPreviousBlockElement() which takes // EditorDOMPoint +struct MOZ_STACK_CLASS LineBreakBeforeBlockBoundaryTest final { + const char16_t* const mInnerHTML; + const char* const mContainer; + const Maybe<uint32_t> + mContainerIndex; // Set if need to use CharacterData in mContainer. + const uint32_t mOffset; + const bool mExpectedResult; // true if the method return a line break + + friend std::ostream& operator<<( + std::ostream& aStream, const LineBreakBeforeBlockBoundaryTest& aTest) { + aStream << "Scan from { container: " << aTest.mContainer; + if (aTest.mContainerIndex) { + aStream << "'s " << aTest.mContainerIndex.value() + 1 << "th child"; + } + return aStream << ", offset: " << aTest.mOffset << " } in " + << NS_ConvertUTF16toUTF8(aTest.mInnerHTML).get() << "\""; + } +}; + +TEST(HTMLEditUtilsTest, GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem) +{ + const RefPtr<Document> doc = CreateHTMLDoc(); + const RefPtr<nsGenericHTMLElement> body = doc->GetBody(); + MOZ_RELEASE_ASSERT(body); + for (const auto& testData : { + LineBreakBeforeBlockBoundaryTest{u"<div contenteditable>abc</div>", + "div", Some(0), 3, false}, + LineBreakBeforeBlockBoundaryTest{u"<div contenteditable>abc</div>", + "div", Nothing{}, 1, false}, + LineBreakBeforeBlockBoundaryTest{u"<div contenteditable><br></div>", + "div", Nothing{}, 0, false}, + LineBreakBeforeBlockBoundaryTest{u"<div contenteditable><br></div>", + "div", Nothing{}, 1, true}, + LineBreakBeforeBlockBoundaryTest{ + u"<div contenteditable><br> </div>", "div", Some(1), 2, true}, + LineBreakBeforeBlockBoundaryTest{ + u"<div contenteditable><br><!-- X --></div>", "div", Nothing{}, + 2, + // FIXME: Currently, this fails with a bug of WSRunScanner + // (actually, a bug of + // HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement) + false}, + LineBreakBeforeBlockBoundaryTest{ + u"<div contenteditable><br><br></div>", "div", Nothing{}, 1, + false}, + LineBreakBeforeBlockBoundaryTest{ + u"<div contenteditable><br><p>abc</p></div>", "div", Nothing{}, + 1, true}, + }) { + body->SetInnerHTMLTrusted(nsDependentString(testData.mInnerHTML), + doc->NodePrincipal(), IgnoreErrors()); + const Element* const containerElement = body->QuerySelector( + nsDependentCString(testData.mContainer), IgnoreErrors()); + MOZ_ASSERT(containerElement); + const Element* const editingHost = + body->QuerySelector("[contenteditable]"_ns, IgnoreErrors()); + MOZ_ASSERT(editingHost); + const nsIContent* const container = + testData.mContainerIndex + ? containerElement->GetChildAt_Deprecated(*testData.mContainerIndex) + : containerElement; + MOZ_RELEASE_ASSERT(container); + const Maybe<EditorRawLineBreak> result = + HTMLEditUtils::GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem< + EditorRawLineBreak>(EditorRawDOMPoint(container, testData.mOffset), + *editingHost); + EXPECT_EQ(result.isSome(), testData.mExpectedResult) + << "GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem: " << testData; + } +} + } // namespace mozilla diff --git a/testing/web-platform/meta/editing/run/delete.html.ini b/testing/web-platform/meta/editing/run/delete.html.ini @@ -137,12 +137,6 @@ [[["delete",""\]\] "<ol><li>foo<br></ol>{}<br>" compare innerHTML] expected: FAIL - [[["defaultparagraphseparator","div"\],["delete",""\]\] "<ol><li>foo<br></ol><p>{}<br>" compare innerHTML] - expected: FAIL - - [[["defaultparagraphseparator","p"\],["delete",""\]\] "<ol><li>foo<br></ol><p>{}<br>" compare innerHTML] - expected: FAIL - [delete.html?2001-3000] expected: diff --git a/testing/web-platform/tests/editing/data/delete.js b/testing/web-platform/tests/editing/data/delete.js @@ -3500,4 +3500,10 @@ var browserTests = [ "abdef", [true], {}], + +["<p>abc<br> </p><p>{}<br></p>", + [["delete",""]], + "<p>abc</p>", + [true], + {}], ] diff --git a/testing/web-platform/tests/editing/data/forwarddelete.js b/testing/web-platform/tests/editing/data/forwarddelete.js @@ -3362,4 +3362,10 @@ var browserTests = [ "abcef", [true], {}], + +["<p>abc{}<br> </p><p><br></p>", + [["forwarddelete",""]], + "<p>abc</p>", + [true], + {}], ]