tor-browser

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

commit af6ad4e82c09ec30ed88a4fe61b81f60bc1636f3
parent 5b8f66f51002a6254b998a9af11d2ee14ac80852
Author: Qiu Chaofan <qcf@ecnelises.com>
Date:   Fri, 12 Dec 2025 01:56:38 +0000

Bug 1728746 - consider grapheme cluster when deleting backwards r=masayuki

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

Diffstat:
Meditor/libeditor/AutoClonedRangeArray.cpp | 7++++---
Mlayout/generic/nsFrameSelection.h | 10+++++-----
Atesting/web-platform/mozilla/tests/editor/delete-backwards-grapheme-cluster.html | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 109 insertions(+), 8 deletions(-)

diff --git a/editor/libeditor/AutoClonedRangeArray.cpp b/editor/libeditor/AutoClonedRangeArray.cpp @@ -1459,9 +1459,10 @@ AutoClonedSelectionRangeArray::ExtendAnchorFocusRangeFor( // Different from the `eNext` case, we look for character boundary. // I'm not sure whether this inconsistency between "Delete" and // "Backspace" is intentional or not. - result = nsFrameSelection::CreateRangeExtendedToPreviousCharacterBoundary< - nsRange>(*presShell, limitersAndCaretData, anchorFocusRange, - rangeDirection); + result = nsFrameSelection:: + CreateRangeExtendedToPreviousGraphemeClusterBoundary<nsRange>( + *presShell, limitersAndCaretData, anchorFocusRange, + rangeDirection); if (NS_WARN_IF(aEditorBase.Destroyed())) { return Err(NS_ERROR_EDITOR_DESTROYED); } diff --git a/layout/generic/nsFrameSelection.h b/layout/generic/nsFrameSelection.h @@ -681,9 +681,9 @@ class nsFrameSelection final { } /** - * CreateRangeExtendedToPreviousCharacterBoundary() returns range which is - * extended from normal selection range to start of previous character - * boundary. + * CreateRangeExtendedToPreviousGraphemeClusterBoundary() returns range + * which is extended from normal selection range to start of previous + * grapheme cluster boundary. * * @param aLimitersAndCaretData The data of limiters and additional * caret data. @@ -695,13 +695,13 @@ class nsFrameSelection final { */ template <typename RangeType> MOZ_CAN_RUN_SCRIPT static mozilla::Result<RefPtr<RangeType>, nsresult> - CreateRangeExtendedToPreviousCharacterBoundary( + CreateRangeExtendedToPreviousGraphemeClusterBoundary( mozilla::PresShell& aPresShell, const mozilla::LimitersAndCaretData& aLimitersAndCaretData, const mozilla::dom::AbstractRange& aRange, nsDirection aRangeDirection) { return CreateRangeExtendedToSomewhere<RangeType>( aPresShell, aLimitersAndCaretData, aRange, aRangeDirection, - eDirPrevious, eSelectCharacter, eLogical); + eDirPrevious, eSelectCluster, eLogical); } /** diff --git a/testing/web-platform/mozilla/tests/editor/delete-backwards-grapheme-cluster.html b/testing/web-platform/mozilla/tests/editor/delete-backwards-grapheme-cluster.html @@ -0,0 +1,100 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>Deleting grapheme cluster</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../editing/include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const editingHost = document.querySelector("div[contenteditable]"); +const utils = new EditorTestUtils(editingHost); + +const flag = "\u{1F1E8}\u{1F1F3}"; +const halfWidthKatakana = "\uFF76\uFF9E"; +const rainbowFlag = "\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}"; +const family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}"; + +const tests = [ + { + name: "Backwards delete grapheme cluster", + initialInnerHTML: `a${flag}[]b`, + command: "delete", + expected: "ab" + }, + { + name: "Forwards delete grapheme cluster", + initialInnerHTML: `a[]${flag}b`, + command: "forwarddelete", + expected: "ab" + }, + { + name: "Backwards delete half-width katakana", + initialInnerHTML: `${halfWidthKatakana}[]`, + command: "delete", + expected: "\uFF76" + }, + { + name: "Forwards delete half-width katakana", + initialInnerHTML: `[]${halfWidthKatakana}`, + command: "forwarddelete", + expected: ["", "<br>"] + }, + { + name: "Backwards delete rainbow flag", + initialInnerHTML: `${rainbowFlag}[]`, + command: "delete", + expected: ["", "<br>"] + }, + { + name: "Forwards delete rainbow flag", + initialInnerHTML: `[]${rainbowFlag}`, + command: "forwarddelete", + expected: ["", "<br>"] + }, + { + name: "Backwards delete rainbow flag around text", + initialInnerHTML: `start${rainbowFlag}[]end`, + command: "delete", + expected: "startend" + }, + { + name: "Forwards delete rainbow flag around text", + initialInnerHTML: `start[]${rainbowFlag}end`, + command: "forwarddelete", + expected: "startend" + }, + { + name: "Backwards delete family emoji", + initialInnerHTML: `${family}[]`, + command: "delete", + expected: ["", "<br>"] + }, + { + name: "Forwards delete family emoji", + initialInnerHTML: `[]${family}`, + command: "forwarddelete", + expected: ["", "<br>"] + } +]; + +for (const t of tests) { + test(() => { + utils.setupEditingHost(t.initialInnerHTML); + document.execCommand(t.command); + if (Array.isArray(t.expected)) { + assert_in_array(editingHost.innerHTML, t.expected); + } else { + assert_equals(editingHost.innerHTML, t.expected); + } + }, t.name); +} +</script> +</body> +</html>