Skip to content

fix: treat Indic conjunct clusters as a single grapheme (v7) - #3151

Merged
christianhg merged 2 commits into
editor-v7.xfrom
gb9c-indic-conjunct-graphemes-v7
Aug 24, 2026
Merged

fix: treat Indic conjunct clusters as a single grapheme (v7)#3151
christianhg merged 2 commits into
editor-v7.xfrom
gb9c-indic-conjunct-graphemes-v7

Conversation

@christianhg

@christianhg christianhg commented Aug 24, 2026

Copy link
Copy Markdown
Member

Backport of #3136 to the v7 line; both commits cherry-pick clean (the touched files are identical on the two branches apart from a lint directive). See #3136 for the full narrative: the grapheme walker gains UAX #29 rule GB9c so Indic conjunct clusters move as one character, forward delete removes a whole cluster, backspace keeps its one-code-unit steps via the compound-script reinsert, and the Unicode tables ship once as composed RegExps.

All gates and both delete browser suites pass on this branch (unit 1381/1381, browser 18/18).


Note

Medium Risk
Changes grapheme-boundary and character-delete behavior for Indic scripts, so caret and Delete/Backspace can mis-step if GB9c or the compound-script reinsert is wrong.

Overview
Makes Indic conjunct clusters (consonant + virama + consonant, e.g. Devanagari क्ष) a single grapheme in getCharacterDistance, following Unicode UAX #29 GB9c. Caret movement no longer lands inside the cluster; Delete at the start removes the whole cluster.

Backspace still removes one code unit at a time via the existing compound-script reinsert. COMPOUND_SCRIPT_REGEX now also matches BMP conjunct linkers that the old script ranges missed (e.g. Gujarati). Supplementary-plane linker scripts are left out so reinsert does not split surrogates; those clusters delete as a whole on backspace.

Unicode InCB tables are shared across the new regexes so they ship once. Tests cover GB9c clustering, Indic/Gujarati backspace, ZWJ emoji (still whole-cluster), and forward delete of a conjunct.

Reviewed by Cursor Bugbot for commit f51b78a. Bugbot is set up for automated code reviews on this repo. Configure here.

`getCharacterDistance` implemented the Unicode UAX 29 grapheme rules
through GB11 and GB12/13 but predated GB9c, so an Indic conjunct
cluster (consonant + virama + consonant, e.g. Devanagari क्ष) was split
at the virama: caret movement and character positions treated the
cluster as two characters where browsers render one glyph.

Port the GB9c implementation from upstream Slate
(ianstormtaylor/slate#6074): `InCB` Consonant/Extend/Linker codepoint
classes, a GB9c entry in `NonBoundaryPairs`, and a deferred
`endsWithConjunctLinker` check that only suppresses the boundary when
the left context actually contains a conjunct linker. The character
tables are identical to upstream's; they are composed into `RegExp`s
from shared class strings instead of upstream's inline literals, so
each table ships once (~6 KB of bundled Unicode data instead of ~18).

Every consumer of `getCharacterDistance` (caret movement, positions,
deletion units) now steps over the full cluster. Forward delete at the
start of a cluster now removes the whole cluster. Backspace stays at
one code unit per press: the whole-cluster delete composes with the
compound-script reinsert in `delete-internal.ts`. That reinsert was
keyed to script ranges that miss Gujarati, Tai Tham, Balinese,
Sundanese, Javanese, and Meetei Mayek, which GB9c now glues, so
`COMPOUND_SCRIPT_REGEX` additionally matches those scripts' BMP
conjunct linkers (every GB9c cluster contains its linker). The
supplementary-plane linkers are left out deliberately: the reinsert
slices code units and would split a surrogate pair, so backspace on
those scripts removes the whole cluster.

Pinned by unit tests adapted from upstream (LTR and RTL, red on the
pre-GB9c code) and browser tests: forward delete over a cluster (red
pre-GB9c) and backspace one-code-unit parity for Devanagari (in-range
script) and Gujarati (linker-keyed, red without the regex extension).

(cherry picked from commit 78360a0)
Backspace on an Indic conjunct cluster removes one code unit at a time
(the compound-script reinsert in `delete-internal.ts`), but that
override is keyed to a hardcoded set of script ranges. Pin that a ZWJ
emoji sequence, which matches none of them, is removed whole by a
single backspace.

(cherry picked from commit dac0376)
@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f51b78a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@portabletext/editor Patch
@portabletext/plugin-character-pair-decorator Patch
@portabletext/plugin-dnd Patch
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-list-index Patch
@portabletext/plugin-markdown-shortcuts Patch
@portabletext/plugin-one-line Patch
@portabletext/plugin-paste-link Patch
@portabletext/plugin-sdk-value Patch
@portabletext/plugin-table Patch
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Ready Ready Preview Aug 24, 2026 11:40am
portable-text-example-basic Ready Ready Preview Aug 24, 2026 11:40am
portable-text-playground Ready Ready Preview Aug 24, 2026 11:40am

Request Review

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @portabletext/editor

Compared against editor-v7.x (49ada3d6)

@portabletext/editor

Metric Value vs editor-v7.x (49ada3d)
Internal (raw) 821.9 KB +7.5 KB, +0.9%
Internal (gzip) 184.0 KB +2.9 KB, +1.6%
Bundled (raw) 1.43 MB +7.5 KB, +0.5%
Bundled (gzip) 352.0 KB +3.1 KB, +0.9%
Import time 62ms -5ms, -7.1%

@portabletext/editor/behaviors

Metric Value vs editor-v7.x (49ada3d)
Internal (raw) 4.2 KB -
Internal (gzip) 1.4 KB -
Bundled (raw) 3.9 KB -
Bundled (gzip) 1.3 KB -
Import time 1ms -0ms, -11.5%

@portabletext/editor/plugins

Metric Value vs editor-v7.x (49ada3d)
Internal (raw) 5.2 KB -
Internal (gzip) 1.8 KB -
Bundled (raw) 5.0 KB -
Bundled (gzip) 1.7 KB -
Import time 5ms -0ms, -4.8%

@portabletext/editor/selectors

Metric Value vs editor-v7.x (49ada3d)
Internal (raw) 96.4 KB -
Internal (gzip) 22.3 KB -
Bundled (raw) 92.5 KB -
Bundled (gzip) 21.0 KB -
Import time 5ms -0ms, -6.1%

@portabletext/editor/traversal

Metric Value vs editor-v7.x (49ada3d)
Internal (raw) 40.7 KB -
Internal (gzip) 10.6 KB -
Bundled (raw) 41.1 KB -
Bundled (gzip) 10.6 KB -
Import time 4ms -0ms, -8.3%

@portabletext/editor/utils

Metric Value vs editor-v7.x (49ada3d)
Internal (raw) 34.1 KB -
Internal (gzip) 8.7 KB -
Bundled (raw) 32.5 KB -
Bundled (gzip) 8.5 KB -
Import time 4ms -0ms, -6.0%

🗺️ . · ./behaviors · ./plugins · ./selectors · ./traversal · ./utils · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @portabletext/markdown

Compared against editor-v7.x (49ada3d6)

Metric Value vs editor-v7.x (49ada3d)
Internal (raw) 53.0 KB -
Internal (gzip) 11.6 KB -
Bundled (raw) 348.0 KB -
Bundled (gzip) 98.2 KB -
Import time 26ms +2ms, +7.8%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@christianhg
christianhg marked this pull request as ready for review August 24, 2026 11:41
@christianhg
christianhg merged commit eafe109 into editor-v7.x Aug 24, 2026
17 of 18 checks passed
@christianhg
christianhg deleted the gb9c-indic-conjunct-graphemes-v7 branch August 24, 2026 11:41
@ecoscript ecoscript Bot mentioned this pull request Aug 24, 2026
christianhg added a commit that referenced this pull request Aug 25, 2026
Documents how a fix on `main` ships for the v6/v7 lines: the
`editor-v<N>.x` branch layout with their self-triggering release
pipelines, the verify-adapt-red-verify procedure, version-suffixed
changesets, and the backport commit/PR shape, distilled from the real
backports (#2919, #2872, #3151, #3120). Same layout as the existing
skills: the file lives in `.agents/skills/`, with a committed
`.claude/skills` symlink for Claude Code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant