Skip to content

fix(textkit): preserve run attributes during bidi reordering#3303

Open
matangot wants to merge 1 commit intodiegomura:masterfrom
matangot:fix/bidi-reordering-nested-runs
Open

fix(textkit): preserve run attributes during bidi reordering#3303
matangot wants to merge 1 commit intodiegomura:masterfrom
matangot:fix/bidi-reordering-nested-runs

Conversation

@matangot
Copy link
Copy Markdown

@matangot matangot commented Mar 3, 2026

Summary

Fixes #2900

The bidiReordering implementation shuffled individual glyphs across run boundaries using character-level index remapping (getItemAtIndex). This caused run attributes (color, font-weight, font-style, links, etc.) to be lost when rendering RTL text with nested styled <Text> elements.

Before (broken):

<Text style={{ direction: 'rtl' }}>
  עברית <Text style={{ color: 'red' }}>קשה</Text> שפה
</Text>

Glyphs from the red-styled run were remapped into the default-styled run, losing the red color. Bold, italic, and links were similarly corrupted.

Root cause: reorderLine() used indices.slice(run.start, run.end) to get reordered character positions, then getItemAtIndex() fetched glyphs from other runs at those positions. The fetched glyphs were placed back into the current run — stripping the original run's attributes.

Fix: Replace character-level glyph shuffling with run-level reordering:

  1. Apply UAX Add meta-data to document #9 L2 algorithm at the run level to determine visual run order
  2. Reverse glyphs/positions within RTL runs (odd bidiLevel)
  3. Glyphs never cross run boundaries → all run attributes preserved

This also removes the bidi-js dependency from bidiReordering.ts (the bidi engine for computing embedding levels still uses it in engines/bidi.ts).

Test plan

  • Existing 4 bidi tests pass unchanged
  • New test: multi-run RTL preserves per-run color attribute after reordering
  • New test: LTR embedded in RTL (level 2 in level 1) reorders correctly
  • Full textkit test suite: 693 tests, 87 files, all passing

The previous bidiReordering implementation shuffled individual glyphs
across run boundaries using character-level index remapping. This
caused run attributes (color, font-weight, font-style, etc.) to be
lost when rendering RTL text with nested styled <Text> elements.

For example, `<Text dir='rtl'>עברית <Text color='red'>קשה</Text> שפה</Text>`
would render with all text in the default color because glyphs from
the red-styled run were moved into the default-styled run.

The fix replaces character-level glyph shuffling with run-level
reordering:

1. Apply UAX diegomura#9 L2 algorithm at the run level to determine visual
   run order (instead of character-level index remapping)
2. Reverse glyphs/positions within RTL runs (odd bidiLevel)
3. Preserve all run attributes intact since glyphs never cross run
   boundaries

This also removes the dependency on bidi-js in bidiReordering.ts
(the bidi engine for computing embedding levels still uses it
separately in engines/bidi.ts).

Fixes diegomura#2900
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 3, 2026

⚠️ No Changeset found

Latest commit: c1edd6a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@baderalrasheed
Copy link
Copy Markdown

Tested your fix and it resolved the issue for us in Arabic text renders.

Thanks for digging into this, a release including it would be very useful.

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.

Bidi issue with RTL languages (Hebrew, Arabic)

2 participants