Skip to content

Commit 4e24685

Browse files
chenglousepcnt
andcommitted
fix: use first strong character for rich bidi metadata
Co-authored-by: Sepcnt <30561671+sepcnt@users.noreply.github.com>
1 parent db940b6 commit 4e24685

2 files changed

Lines changed: 54 additions & 1 deletion

File tree

src/bidi.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,21 @@ function computeBidiLevels(str: string): Int8Array | null {
7878

7979
if (numBidi === 0) return null
8080

81-
const startLevel = (len / numBidi) < 0.3 ? 0 : 1
81+
// Use the first strong character to pick the paragraph base direction.
82+
// Rich-path bidi metadata is only an approximation, but this keeps mixed
83+
// LTR/RTL text aligned with the common UBA paragraph rule.
84+
let startLevel = 0
85+
for (let i = 0; i < len; i++) {
86+
const t = types[i]!
87+
if (t === 'L') {
88+
startLevel = 0
89+
break
90+
}
91+
if (t === 'R' || t === 'AL') {
92+
startLevel = 1
93+
break
94+
}
95+
}
8296
const levels = new Int8Array(len)
8397
for (let i = 0; i < len; i++) levels[i] = startLevel
8498

src/layout.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type TestLayoutCursor = {
4343

4444
type TestPreparedTextWithSegments = {
4545
segments: string[]
46+
segLevels?: Int8Array | null
4647
}
4748

4849
type TestLayoutLine = {
@@ -213,6 +214,20 @@ function terminalCursor(prepared: TestPreparedTextWithSegments): TestLayoutCurso
213214
return { segmentIndex: prepared.segments.length, graphemeIndex: 0 }
214215
}
215216

217+
function getNonSpaceSegmentLevels(
218+
prepared: TestPreparedTextWithSegments,
219+
): Array<{ level: number, text: string }> {
220+
if (prepared.segLevels === null || prepared.segLevels === undefined) return []
221+
222+
const levels: Array<{ level: number, text: string }> = []
223+
for (let i = 0; i < prepared.segments.length; i++) {
224+
const text = prepared.segments[i]!
225+
if (text.trim().length === 0) continue
226+
levels.push({ level: prepared.segLevels[i]!, text })
227+
}
228+
return levels
229+
}
230+
216231
class TestCanvasRenderingContext2D {
217232
font = ''
218233

@@ -543,6 +558,30 @@ describe('prepare invariants', () => {
543558
const latin = prepare('hello world', FONT)
544559
expect(layout(latin, 200, LINE_HEIGHT)).toEqual({ lineCount: 1, height: LINE_HEIGHT })
545560
})
561+
562+
test('pure LTR text skips rich bidi metadata', () => {
563+
expect(prepareWithSegments('hello world', FONT).segLevels).toBeNull()
564+
})
565+
566+
test('rich bidi metadata uses the first strong character for paragraph direction', () => {
567+
const ltrFirst = prepareWithSegments('one اثنان three', FONT)
568+
expect(ltrFirst.segLevels).not.toBeNull()
569+
expect(ltrFirst.segLevels).toHaveLength(ltrFirst.segments.length)
570+
expect(getNonSpaceSegmentLevels(ltrFirst)).toEqual([
571+
{ text: 'one', level: 0 },
572+
{ text: 'اثنان', level: 1 },
573+
{ text: 'three', level: 0 },
574+
])
575+
576+
const rtlFirst = prepareWithSegments('123 واحد three', FONT)
577+
expect(rtlFirst.segLevels).not.toBeNull()
578+
expect(rtlFirst.segLevels).toHaveLength(rtlFirst.segments.length)
579+
expect(getNonSpaceSegmentLevels(rtlFirst)).toEqual([
580+
{ text: '123', level: 2 },
581+
{ text: 'واحد', level: 1 },
582+
{ text: 'three', level: 2 },
583+
])
584+
})
546585
})
547586

548587
describe('inline-flow invariants', () => {

0 commit comments

Comments
 (0)