expo-pretext 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bidi.ts ADDED
@@ -0,0 +1,175 @@
1
+ // Simplified bidi metadata helper for the rich prepareWithSegments() path,
2
+ // forked from pdf.js via Sebastian's text-layout. It classifies characters
3
+ // into bidi types, computes embedding levels, and maps them onto prepared
4
+ // segments for custom rendering. The line-breaking engine does not consume
5
+ // these levels.
6
+
7
+ import {
8
+ latin1BidiTypes,
9
+ nonLatin1BidiRanges,
10
+ type GeneratedBidiType as BidiType,
11
+ } from './generated/bidi-data'
12
+
13
+ function classifyCodePoint(codePoint: number): BidiType {
14
+ if (codePoint <= 0x00FF) return latin1BidiTypes[codePoint]!
15
+
16
+ let lo = 0
17
+ let hi = nonLatin1BidiRanges.length - 1
18
+ while (lo <= hi) {
19
+ const mid = (lo + hi) >> 1
20
+ const range = nonLatin1BidiRanges[mid]!
21
+ if (codePoint < range[0]) {
22
+ hi = mid - 1
23
+ continue
24
+ }
25
+ if (codePoint > range[1]) {
26
+ lo = mid + 1
27
+ continue
28
+ }
29
+ return range[2]
30
+ }
31
+
32
+ return 'L'
33
+ }
34
+
35
+ function computeBidiLevels(str: string): Int8Array | null {
36
+ const len = str.length
37
+ if (len === 0) return null
38
+
39
+ // eslint-disable-next-line unicorn/no-new-array
40
+ const types: BidiType[] = new Array(len)
41
+ let sawBidi = false
42
+
43
+ // Keep the resolved bidi classes aligned to UTF-16 code-unit offsets,
44
+ // because the rich prepared segments index back into the normalized string
45
+ // with JavaScript string offsets.
46
+ for (let i = 0; i < len;) {
47
+ const first = str.charCodeAt(i)
48
+ let codePoint = first
49
+ let codeUnitLength = 1
50
+
51
+ if (first >= 0xD800 && first <= 0xDBFF && i + 1 < len) {
52
+ const second = str.charCodeAt(i + 1)
53
+ if (second >= 0xDC00 && second <= 0xDFFF) {
54
+ codePoint = ((first - 0xD800) << 10) + (second - 0xDC00) + 0x10000
55
+ codeUnitLength = 2
56
+ }
57
+ }
58
+
59
+ const t = classifyCodePoint(codePoint)
60
+ if (t === 'R' || t === 'AL' || t === 'AN') sawBidi = true
61
+ for (let j = 0; j < codeUnitLength; j++) {
62
+ types[i + j] = t
63
+ }
64
+ i += codeUnitLength
65
+ }
66
+
67
+ if (!sawBidi) return null
68
+
69
+ // Use the first strong character to pick the paragraph base direction.
70
+ // Rich-path bidi metadata is only an approximation, but this keeps mixed
71
+ // LTR/RTL text aligned with the common UBA paragraph rule.
72
+ let startLevel = 0
73
+ for (let i = 0; i < len; i++) {
74
+ const t = types[i]!
75
+ if (t === 'L') {
76
+ startLevel = 0
77
+ break
78
+ }
79
+ if (t === 'R' || t === 'AL') {
80
+ startLevel = 1
81
+ break
82
+ }
83
+ }
84
+ const levels = new Int8Array(len)
85
+ for (let i = 0; i < len; i++) levels[i] = startLevel
86
+
87
+ const e: BidiType = (startLevel & 1) ? 'R' : 'L'
88
+ const sor = e
89
+
90
+ // W1-W7
91
+ let lastType: BidiType = sor
92
+ for (let i = 0; i < len; i++) {
93
+ if (types[i] === 'NSM') types[i] = lastType
94
+ else lastType = types[i]!
95
+ }
96
+ lastType = sor
97
+ for (let i = 0; i < len; i++) {
98
+ const t = types[i]!
99
+ if (t === 'EN') types[i] = lastType === 'AL' ? 'AN' : 'EN'
100
+ else if (t === 'R' || t === 'L' || t === 'AL') lastType = t
101
+ }
102
+ for (let i = 0; i < len; i++) {
103
+ if (types[i] === 'AL') types[i] = 'R'
104
+ }
105
+ for (let i = 1; i < len - 1; i++) {
106
+ if (types[i] === 'ES' && types[i - 1] === 'EN' && types[i + 1] === 'EN') {
107
+ types[i] = 'EN'
108
+ }
109
+ if (
110
+ types[i] === 'CS' &&
111
+ (types[i - 1] === 'EN' || types[i - 1] === 'AN') &&
112
+ types[i + 1] === types[i - 1]
113
+ ) {
114
+ types[i] = types[i - 1]!
115
+ }
116
+ }
117
+ for (let i = 0; i < len; i++) {
118
+ if (types[i] !== 'EN') continue
119
+ let j
120
+ for (j = i - 1; j >= 0 && types[j] === 'ET'; j--) types[j] = 'EN'
121
+ for (j = i + 1; j < len && types[j] === 'ET'; j++) types[j] = 'EN'
122
+ }
123
+ for (let i = 0; i < len; i++) {
124
+ const t = types[i]!
125
+ if (t === 'WS' || t === 'ES' || t === 'ET' || t === 'CS') types[i] = 'ON'
126
+ }
127
+ lastType = sor
128
+ for (let i = 0; i < len; i++) {
129
+ const t = types[i]!
130
+ if (t === 'EN') types[i] = lastType === 'L' ? 'L' : 'EN'
131
+ else if (t === 'R' || t === 'L') lastType = t
132
+ }
133
+
134
+ // N1-N2
135
+ for (let i = 0; i < len; i++) {
136
+ if (types[i] !== 'ON') continue
137
+ let end = i + 1
138
+ while (end < len && types[end] === 'ON') end++
139
+ const before: BidiType = i > 0 ? types[i - 1]! : sor
140
+ const after: BidiType = end < len ? types[end]! : sor
141
+ const bDir: BidiType = before !== 'L' ? 'R' : 'L'
142
+ const aDir: BidiType = after !== 'L' ? 'R' : 'L'
143
+ if (bDir === aDir) {
144
+ for (let j = i; j < end; j++) types[j] = bDir
145
+ }
146
+ i = end - 1
147
+ }
148
+ for (let i = 0; i < len; i++) {
149
+ if (types[i] === 'ON') types[i] = e
150
+ }
151
+
152
+ // I1-I2
153
+ for (let i = 0; i < len; i++) {
154
+ const t = types[i]!
155
+ if ((levels[i]! & 1) === 0) {
156
+ if (t === 'R') levels[i]!++
157
+ else if (t === 'AN' || t === 'EN') levels[i]! += 2
158
+ } else if (t === 'L' || t === 'AN' || t === 'EN') {
159
+ levels[i]!++
160
+ }
161
+ }
162
+
163
+ return levels
164
+ }
165
+
166
+ export function computeSegmentLevels(normalized: string, segStarts: number[]): Int8Array | null {
167
+ const bidiLevels = computeBidiLevels(normalized)
168
+ if (bidiLevels === null) return null
169
+
170
+ const segLevels = new Int8Array(segStarts.length)
171
+ for (let i = 0; i < segStarts.length; i++) {
172
+ segLevels[i] = bidiLevels[segStarts[i]!]!
173
+ }
174
+ return segLevels
175
+ }
package/src/build.ts ADDED
@@ -0,0 +1,503 @@
1
+ // Build prepared text handles from pre-analyzed + pre-measured data.
2
+ // Extracted from layout.ts — contains the factory logic that constructs
3
+ // the internal PreparedText structures consumed by the line-breaking engine.
4
+
5
+ import { computeSegmentLevels } from './bidi'
6
+ import {
7
+ canContinueKeepAllTextRun,
8
+ endsWithClosingQuote,
9
+ isCJK,
10
+ isNumericRunSegment,
11
+ kinsokuEnd,
12
+ kinsokuStart,
13
+ leftStickyPunctuation,
14
+ type AnalysisChunk,
15
+ type SegmentBreakKind,
16
+ type TextAnalysis,
17
+ type WhiteSpaceMode,
18
+ type WordBreakMode,
19
+ } from './analysis'
20
+ import { getEngineProfile } from './engine-profile'
21
+ import type {
22
+ TextStyle,
23
+ PreparedText,
24
+ PreparedTextWithSegments as PublicPreparedTextWithSegments,
25
+ } from './types'
26
+
27
+ // --- Grapheme segmenter ---
28
+ // Intl.Segmenter may not be available in Hermes — use a fallback
29
+ // that splits on Unicode code points via spread operator.
30
+
31
+ export interface GraphemeSegmenterLike {
32
+ segment(text: string): Iterable<{ segment: string; index: number }>
33
+ }
34
+
35
+ let sharedGraphemeSegmenter: GraphemeSegmenterLike | null = null
36
+
37
+ export function getSharedGraphemeSegmenter(): GraphemeSegmenterLike {
38
+ if (sharedGraphemeSegmenter === null) {
39
+ if (typeof Intl !== 'undefined' && typeof (Intl as any).Segmenter === 'function') {
40
+ sharedGraphemeSegmenter = new (Intl as any).Segmenter(undefined, { granularity: 'grapheme' })
41
+ } else {
42
+ // Fallback: split on code points via spread operator.
43
+ // This handles most cases correctly (including astral plane),
44
+ // but may not perfectly handle complex grapheme clusters (ZWJ emoji).
45
+ sharedGraphemeSegmenter = {
46
+ segment(text: string): Iterable<{ segment: string; index: number }> {
47
+ const chars = [...text]
48
+ let idx = 0
49
+ return chars.map(ch => {
50
+ const entry = { segment: ch, index: idx }
51
+ idx += ch.length
52
+ return entry
53
+ })
54
+ },
55
+ }
56
+ }
57
+ }
58
+ return sharedGraphemeSegmenter
59
+ }
60
+
61
+ export function clearGraphemeSegmenter(): void {
62
+ sharedGraphemeSegmenter = null
63
+ }
64
+
65
+ // --- Internal types ---
66
+
67
+ // The core parallel-array structure that the line-breaking engine operates on.
68
+ // Identical to Pretext's internal representation.
69
+ export type PreparedCore = {
70
+ widths: number[]
71
+ lineEndFitAdvances: number[]
72
+ lineEndPaintAdvances: number[]
73
+ kinds: SegmentBreakKind[]
74
+ simpleLineWalkFastPath: boolean
75
+ segLevels: Int8Array | null
76
+ breakableWidths: (number[] | null)[]
77
+ breakablePrefixWidths: (number[] | null)[]
78
+ discretionaryHyphenWidth: number
79
+ tabStopAdvance: number
80
+ chunks: PreparedLineChunk[]
81
+ style: TextStyle | null
82
+ }
83
+
84
+ export type InternalPreparedText = PreparedText & PreparedCore
85
+
86
+ export type InternalPreparedTextWithSegments = InternalPreparedText & {
87
+ segments: string[]
88
+ }
89
+
90
+ export type PreparedLineChunk = {
91
+ startSegmentIndex: number
92
+ endSegmentIndex: number
93
+ consumedEndSegmentIndex: number
94
+ }
95
+
96
+ export type PrepareOptions = {
97
+ whiteSpace?: WhiteSpaceMode
98
+ wordBreak?: WordBreakMode
99
+ }
100
+
101
+ // --- Internal helpers ---
102
+
103
+ type MeasuredTextUnit = {
104
+ text: string
105
+ start: number
106
+ }
107
+
108
+ function createEmptyPrepared(
109
+ includeSegments: boolean,
110
+ style: TextStyle | null,
111
+ ): InternalPreparedText | InternalPreparedTextWithSegments {
112
+ const base: PreparedCore = {
113
+ widths: [],
114
+ lineEndFitAdvances: [],
115
+ lineEndPaintAdvances: [],
116
+ kinds: [],
117
+ simpleLineWalkFastPath: true,
118
+ segLevels: null,
119
+ breakableWidths: [],
120
+ breakablePrefixWidths: [],
121
+ discretionaryHyphenWidth: 0,
122
+ tabStopAdvance: 0,
123
+ chunks: [],
124
+ style,
125
+ }
126
+ if (includeSegments) {
127
+ return { ...base, segments: [] } as unknown as InternalPreparedTextWithSegments
128
+ }
129
+ return base as unknown as InternalPreparedText
130
+ }
131
+
132
+ function buildBaseCjkUnits(
133
+ segText: string,
134
+ engineProfile: ReturnType<typeof getEngineProfile>,
135
+ ): MeasuredTextUnit[] {
136
+ const units: MeasuredTextUnit[] = []
137
+ let unitText = ''
138
+ let unitStart = 0
139
+
140
+ function pushUnit(): void {
141
+ if (unitText.length === 0) return
142
+ units.push({ text: unitText, start: unitStart })
143
+ unitText = ''
144
+ }
145
+
146
+ for (const gs of getSharedGraphemeSegmenter().segment(segText)) {
147
+ const grapheme = gs.segment
148
+
149
+ if (unitText.length === 0) {
150
+ unitText = grapheme
151
+ unitStart = gs.index
152
+ continue
153
+ }
154
+
155
+ if (
156
+ kinsokuEnd.has(unitText) ||
157
+ kinsokuStart.has(grapheme) ||
158
+ leftStickyPunctuation.has(grapheme) ||
159
+ (engineProfile.carryCJKAfterClosingQuote &&
160
+ isCJK(grapheme) &&
161
+ endsWithClosingQuote(unitText))
162
+ ) {
163
+ unitText += grapheme
164
+ continue
165
+ }
166
+
167
+ if (!isCJK(unitText) && !isCJK(grapheme)) {
168
+ unitText += grapheme
169
+ continue
170
+ }
171
+
172
+ pushUnit()
173
+ unitText = grapheme
174
+ unitStart = gs.index
175
+ }
176
+
177
+ pushUnit()
178
+ return units
179
+ }
180
+
181
+ function mergeKeepAllTextUnits(units: MeasuredTextUnit[]): MeasuredTextUnit[] {
182
+ if (units.length <= 1) return units
183
+
184
+ const merged: MeasuredTextUnit[] = [{ ...units[0]! }]
185
+ for (let i = 1; i < units.length; i++) {
186
+ const next = units[i]!
187
+ const previous = merged[merged.length - 1]!
188
+
189
+ if (
190
+ canContinueKeepAllTextRun(previous.text) &&
191
+ isCJK(previous.text)
192
+ ) {
193
+ previous.text += next.text
194
+ continue
195
+ }
196
+
197
+ merged.push({ ...next })
198
+ }
199
+
200
+ return merged
201
+ }
202
+
203
+ // Look up width of a sub-segment from the pre-measured width map.
204
+ function lookupWidth(
205
+ text: string,
206
+ widthMap: Map<string, number>,
207
+ parentText: string,
208
+ parentWidth: number,
209
+ ): number {
210
+ const cached = widthMap.get(text)
211
+ if (cached !== undefined) return cached
212
+
213
+ if (parentText.length > 0 && parentWidth > 0) {
214
+ return (text.length / parentText.length) * parentWidth
215
+ }
216
+ return 0
217
+ }
218
+
219
+ // Get grapheme widths for a text segment by splitting it and looking up
220
+ // individual grapheme widths from the width map, falling back to equal
221
+ // distribution from the total width.
222
+ function getGraphemeWidthsFromMap(
223
+ text: string,
224
+ totalWidth: number,
225
+ widthMap: Map<string, number>,
226
+ ): number[] {
227
+ const graphemeSegmenter = getSharedGraphemeSegmenter()
228
+ const graphemes: string[] = []
229
+ for (const gs of graphemeSegmenter.segment(text)) {
230
+ graphemes.push(gs.segment)
231
+ }
232
+ if (graphemes.length <= 1) return [totalWidth]
233
+
234
+ const widths: number[] = []
235
+ let knownTotal = 0
236
+ let unknownCount = 0
237
+ for (const g of graphemes) {
238
+ const w = widthMap.get(g)
239
+ if (w !== undefined) {
240
+ widths.push(w)
241
+ knownTotal += w
242
+ } else {
243
+ widths.push(-1)
244
+ unknownCount++
245
+ }
246
+ }
247
+
248
+ if (unknownCount > 0) {
249
+ const remaining = Math.max(0, totalWidth - knownTotal)
250
+ const perUnknown = remaining / unknownCount
251
+ for (let i = 0; i < widths.length; i++) {
252
+ if (widths[i]! < 0) widths[i] = perUnknown
253
+ }
254
+ }
255
+
256
+ return widths
257
+ }
258
+
259
+ // Build cumulative prefix widths from per-grapheme widths.
260
+ function buildPrefixWidths(graphemeWidths: number[]): number[] {
261
+ const prefixWidths: number[] = new Array(graphemeWidths.length)
262
+ let sum = 0
263
+ for (let i = 0; i < graphemeWidths.length; i++) {
264
+ sum += graphemeWidths[i]!
265
+ prefixWidths[i] = sum
266
+ }
267
+ return prefixWidths
268
+ }
269
+
270
+ function mapAnalysisChunksToPreparedChunks(
271
+ chunks: AnalysisChunk[],
272
+ preparedStartByAnalysisIndex: number[],
273
+ preparedEndSegmentIndex: number,
274
+ ): PreparedLineChunk[] {
275
+ const preparedChunks: PreparedLineChunk[] = []
276
+ for (let i = 0; i < chunks.length; i++) {
277
+ const chunk = chunks[i]!
278
+ const startSegmentIndex =
279
+ chunk.startSegmentIndex < preparedStartByAnalysisIndex.length
280
+ ? preparedStartByAnalysisIndex[chunk.startSegmentIndex]!
281
+ : preparedEndSegmentIndex
282
+ const endSegmentIndex =
283
+ chunk.endSegmentIndex < preparedStartByAnalysisIndex.length
284
+ ? preparedStartByAnalysisIndex[chunk.endSegmentIndex]!
285
+ : preparedEndSegmentIndex
286
+ const consumedEndSegmentIndex =
287
+ chunk.consumedEndSegmentIndex < preparedStartByAnalysisIndex.length
288
+ ? preparedStartByAnalysisIndex[chunk.consumedEndSegmentIndex]!
289
+ : preparedEndSegmentIndex
290
+
291
+ preparedChunks.push({
292
+ startSegmentIndex,
293
+ endSegmentIndex,
294
+ consumedEndSegmentIndex,
295
+ })
296
+ }
297
+ return preparedChunks
298
+ }
299
+
300
+ // --- Build prepared handles from pre-measured data ---
301
+
302
+ function buildFromAnalysis(
303
+ analysis: TextAnalysis,
304
+ widthMap: Map<string, number>,
305
+ includeSegments: boolean,
306
+ wordBreak: WordBreakMode,
307
+ style: TextStyle | null,
308
+ ): InternalPreparedText | InternalPreparedTextWithSegments {
309
+ const engineProfile = getEngineProfile()
310
+
311
+ const discretionaryHyphenWidth = widthMap.get('-') ?? (widthMap.get(' ') ?? 0) * 0.6
312
+ const spaceWidth = widthMap.get(' ') ?? 0
313
+ const tabStopAdvance = spaceWidth * 8
314
+
315
+ if (analysis.len === 0) return createEmptyPrepared(includeSegments, style)
316
+
317
+ const widths: number[] = []
318
+ const lineEndFitAdvances: number[] = []
319
+ const lineEndPaintAdvances: number[] = []
320
+ const kinds: SegmentBreakKind[] = []
321
+ let simpleLineWalkFastPath = analysis.chunks.length <= 1
322
+ const segStarts = includeSegments ? [] as number[] : null
323
+ const breakableWidths: (number[] | null)[] = []
324
+ const breakablePrefixWidths: (number[] | null)[] = []
325
+ const segments = includeSegments ? [] as string[] : null
326
+ const preparedStartByAnalysisIndex = Array.from<number>({ length: analysis.len })
327
+
328
+ function pushSegment(
329
+ text: string,
330
+ width: number,
331
+ lineEndFitAdvance: number,
332
+ lineEndPaintAdvance: number,
333
+ kind: SegmentBreakKind,
334
+ start: number,
335
+ breakable: number[] | null,
336
+ breakablePrefix: number[] | null,
337
+ ): void {
338
+ if (kind !== 'text' && kind !== 'space' && kind !== 'zero-width-break') {
339
+ simpleLineWalkFastPath = false
340
+ }
341
+ widths.push(width)
342
+ lineEndFitAdvances.push(lineEndFitAdvance)
343
+ lineEndPaintAdvances.push(lineEndPaintAdvance)
344
+ kinds.push(kind)
345
+ segStarts?.push(start)
346
+ breakableWidths.push(breakable)
347
+ breakablePrefixWidths.push(breakablePrefix)
348
+ if (segments !== null) segments.push(text)
349
+ }
350
+
351
+ function pushTextSegment(
352
+ text: string,
353
+ kind: SegmentBreakKind,
354
+ start: number,
355
+ wordLike: boolean,
356
+ allowOverflowBreaks: boolean,
357
+ parentText: string,
358
+ parentWidth: number,
359
+ ): void {
360
+ const width = lookupWidth(text, widthMap, parentText, parentWidth)
361
+ const lineEndFitAdvance =
362
+ kind === 'space' || kind === 'preserved-space' || kind === 'zero-width-break'
363
+ ? 0
364
+ : width
365
+ const lineEndPaintAdvance =
366
+ kind === 'space' || kind === 'zero-width-break'
367
+ ? 0
368
+ : width
369
+
370
+ if (allowOverflowBreaks && wordLike && text.length > 1) {
371
+ const graphemeWidths = getGraphemeWidthsFromMap(text, width, widthMap)
372
+ const graphemePrefixWidths =
373
+ engineProfile.preferPrefixWidthsForBreakableRuns || isNumericRunSegment(text)
374
+ ? buildPrefixWidths(graphemeWidths)
375
+ : null
376
+ pushSegment(
377
+ text,
378
+ width,
379
+ lineEndFitAdvance,
380
+ lineEndPaintAdvance,
381
+ kind,
382
+ start,
383
+ graphemeWidths,
384
+ graphemePrefixWidths,
385
+ )
386
+ return
387
+ }
388
+
389
+ pushSegment(
390
+ text,
391
+ width,
392
+ lineEndFitAdvance,
393
+ lineEndPaintAdvance,
394
+ kind,
395
+ start,
396
+ null,
397
+ null,
398
+ )
399
+ }
400
+
401
+ for (let mi = 0; mi < analysis.len; mi++) {
402
+ preparedStartByAnalysisIndex[mi] = widths.length
403
+ const segText = analysis.texts[mi]!
404
+ const segWordLike = analysis.isWordLike[mi]!
405
+ const segKind = analysis.kinds[mi]!
406
+ const segStart = analysis.starts[mi]!
407
+
408
+ if (segKind === 'soft-hyphen') {
409
+ pushSegment(
410
+ segText,
411
+ 0,
412
+ discretionaryHyphenWidth,
413
+ discretionaryHyphenWidth,
414
+ segKind,
415
+ segStart,
416
+ null,
417
+ null,
418
+ )
419
+ continue
420
+ }
421
+
422
+ if (segKind === 'hard-break') {
423
+ pushSegment(segText, 0, 0, 0, segKind, segStart, null, null)
424
+ continue
425
+ }
426
+
427
+ if (segKind === 'tab') {
428
+ pushSegment(segText, 0, 0, 0, segKind, segStart, null, null)
429
+ continue
430
+ }
431
+
432
+ const containsCJK = isCJK(segText)
433
+
434
+ if (segKind === 'text' && containsCJK) {
435
+ const parentWidth = widthMap.get(segText) ?? 0
436
+ const baseUnits = buildBaseCjkUnits(segText, engineProfile)
437
+ const measuredUnits = wordBreak === 'keep-all'
438
+ ? mergeKeepAllTextUnits(baseUnits)
439
+ : baseUnits
440
+
441
+ for (let i = 0; i < measuredUnits.length; i++) {
442
+ const unit = measuredUnits[i]!
443
+ pushTextSegment(
444
+ unit.text,
445
+ 'text',
446
+ segStart + unit.start,
447
+ segWordLike,
448
+ wordBreak === 'keep-all' || !isCJK(unit.text),
449
+ segText,
450
+ parentWidth,
451
+ )
452
+ }
453
+ continue
454
+ }
455
+
456
+ pushTextSegment(segText, segKind, segStart, segWordLike, true, segText, 0)
457
+ }
458
+
459
+ const chunks = mapAnalysisChunksToPreparedChunks(analysis.chunks, preparedStartByAnalysisIndex, widths.length)
460
+ const segLevels = segStarts === null ? null : computeSegmentLevels(analysis.normalized, segStarts)
461
+
462
+ const core: PreparedCore = {
463
+ widths,
464
+ lineEndFitAdvances,
465
+ lineEndPaintAdvances,
466
+ kinds,
467
+ simpleLineWalkFastPath,
468
+ segLevels,
469
+ breakableWidths,
470
+ breakablePrefixWidths,
471
+ discretionaryHyphenWidth,
472
+ tabStopAdvance,
473
+ chunks,
474
+ style,
475
+ }
476
+
477
+ if (segments !== null) {
478
+ return { ...core, segments } as unknown as InternalPreparedTextWithSegments
479
+ }
480
+ return core as unknown as InternalPreparedText
481
+ }
482
+
483
+ // --- Public build functions (called by prepare.ts) ---
484
+
485
+ export function buildPreparedText(
486
+ analysis: TextAnalysis,
487
+ widthMap: Map<string, number>,
488
+ style: TextStyle,
489
+ options?: PrepareOptions,
490
+ ): PreparedText {
491
+ const wordBreak = options?.wordBreak ?? 'normal'
492
+ return buildFromAnalysis(analysis, widthMap, false, wordBreak, style) as PreparedText
493
+ }
494
+
495
+ export function buildPreparedTextWithSegments(
496
+ analysis: TextAnalysis,
497
+ widthMap: Map<string, number>,
498
+ style: TextStyle,
499
+ options?: PrepareOptions,
500
+ ): PublicPreparedTextWithSegments {
501
+ const wordBreak = options?.wordBreak ?? 'normal'
502
+ return buildFromAnalysis(analysis, widthMap, true, wordBreak, style) as unknown as PublicPreparedTextWithSegments
503
+ }