diff --git a/src/layout.test.ts b/src/layout.test.ts index a37ebd1..1df4517 100644 --- a/src/layout.test.ts +++ b/src/layout.test.ts @@ -818,6 +818,30 @@ describe('layout invariants', () => { expect(actual).toEqual(expected.lines) }) + test('streaming canary preserves exact continuation cursors after a ZWSP break opportunity', () => { + const text = 'بام \u200DB bا \u00ADb\u060C b\f \u061F🚀\u061Fع ر 本 \u061F\na a A\u200B 語 語\u200Dح' + const prepared = prepareWithSegments(text, FONT) + const width = 56.57 + const expected = layoutWithLines(prepared, width, LINE_HEIGHT) + + expect(collectStreamedLines(prepared, width)).toEqual(expected.lines) + + const streamedRanges = [] + let cursor = { segmentIndex: 0, graphemeIndex: 0 } + while (true) { + const line = layoutNextLineRange(prepared, cursor, width) + if (line === null) break + streamedRanges.push(line) + cursor = line.end + } + + expect(streamedRanges).toEqual(expected.lines.map(line => ({ + width: line.width, + start: line.start, + end: line.end, + }))) + }) + test('layout and layoutWithLines stay aligned when ZWSP triggers narrow grapheme breaking', () => { const cases = [ 'alpha\u200Bbeta', diff --git a/src/line-break.ts b/src/line-break.ts index 363e0d8..4d51805 100644 --- a/src/line-break.ts +++ b/src/line-break.ts @@ -42,6 +42,8 @@ function normalizeSimpleLineStartSegmentIndex( prepared: PreparedLineBreakData, segmentIndex: number, ): number { + if (segmentIndex > 0) return segmentIndex + while (segmentIndex < prepared.widths.length) { const kind = prepared.kinds[segmentIndex]! if (kind !== 'space' && kind !== 'zero-width-break' && kind !== 'soft-hyphen') break @@ -113,6 +115,12 @@ function normalizeLineStartInChunk( } if (segmentIndex < chunk.startSegmentIndex) segmentIndex = chunk.startSegmentIndex + if (segmentIndex > chunk.startSegmentIndex) { + cursor.segmentIndex = segmentIndex + cursor.graphemeIndex = 0 + return chunkIndex + } + while (segmentIndex < chunk.endSegmentIndex) { const kind = prepared.kinds[segmentIndex]! if (kind !== 'space' && kind !== 'zero-width-break' && kind !== 'soft-hyphen') {