rapier-markdown-kit 1.1.101 → 1.1.104

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.
Files changed (60) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +213 -53
  3. package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
  4. package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
  5. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
  6. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
  7. package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
  8. package/dist/agent/vendor/pretext/LICENSE +208 -0
  9. package/dist/agent/vendor/pretext/NOTICE +26 -13
  10. package/dist/agent/vendor/pretext/SOURCE.json +213 -45
  11. package/dist/agent/vendor/pretext/analysis.js +675 -1224
  12. package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
  13. package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
  14. package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
  15. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
  16. package/dist/agent/vendor/pretext/graphemes.js +98 -0
  17. package/dist/agent/vendor/pretext/han-kerning.js +253 -0
  18. package/dist/agent/vendor/pretext/layout.js +352 -290
  19. package/dist/agent/vendor/pretext/line-break.js +1154 -588
  20. package/dist/agent/vendor/pretext/line-breaks.js +892 -0
  21. package/dist/agent/vendor/pretext/line-text.js +81 -39
  22. package/dist/agent/vendor/pretext/measurement.js +1105 -176
  23. package/dist/agent/vendor/pretext/prepare.js +807 -0
  24. package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
  25. package/dist/agent/will.mjs +2 -0
  26. package/dist/kit/assets.mjs +5 -1
  27. package/dist/kit/conformance/run.mjs +4 -4
  28. package/dist/kit/export-fonts.mjs +26 -11
  29. package/dist/kit/index.mjs +1 -0
  30. package/dist/kit/ledger/carried.mjs +9 -7
  31. package/dist/kit/ledger/data.mjs +73 -13
  32. package/dist/kit/ledger/format.mjs +166 -53
  33. package/dist/kit/ledger/hash.mjs +75 -18
  34. package/dist/kit/ledger/history.mjs +143 -115
  35. package/dist/kit/ledger/inverse-source.mjs +158 -19
  36. package/dist/kit/ledger/journal-records.mjs +292 -67
  37. package/dist/kit/model.mjs +1 -1
  38. package/dist/kit/pdf-glyphs.mjs +4 -4
  39. package/dist/kit/render-markdown.mjs +259 -122
  40. package/dist/kit/render-sanitize.mjs +24 -4
  41. package/dist/kit/render-work.mjs +99 -20
  42. package/dist/kit/render.mjs +101 -65
  43. package/dist/kit/typography.mjs +2 -0
  44. package/dist/layout/hyphenation.mjs +127 -0
  45. package/dist/layout/line-plan.mjs +88 -96
  46. package/dist/layout/model.mjs +61 -80
  47. package/dist/layout/paragraph-fit.mjs +191 -0
  48. package/dist/layout/preparation.mjs +373 -0
  49. package/dist/layout/prepared-paragraph.mjs +223 -0
  50. package/dist/layout/typography.mjs +345 -111
  51. package/dist/packages/rapier-diff/index.mjs +288 -196
  52. package/dist/spec/document-fonts.mjs +13 -4
  53. package/dist/spec/md-assets.mjs +149 -30
  54. package/dist/spec/md-layout.mjs +5 -2
  55. package/dist/spec/md-marks.mjs +218 -3
  56. package/dist/spec/md-source.mjs +122 -0
  57. package/package.json +2 -1
  58. package/style.css +9 -0
  59. package/dist/agent/vendor/pretext/bidi.js +0 -151
  60. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -1,308 +1,370 @@
1
- /*! Pretext 0.0.9 | MIT | Copyright (c) 2026 Pretext contributors */
2
- import { computeSegmentLevels } from './bidi.js';
3
- import { analyzeText, clearAnalysisCaches, getBreakablePreferredBreaks, getCjkTextUnits, getSharedGraphemeSegmenter, isCJK, isNumericRunSegment, isIndependentSymbolRun, setAnalysisLocale } from './analysis.js';
4
- import { clearMeasurementCaches, getCorrectedSegmentWidth, getSegmentBreakableFitAdvances, getEngineProfile, getFontMeasurementState, getSegmentMetrics, textMayContainEmoji } from './measurement.js';
5
- import { countPreparedLines, measurePreparedLineGeometry, normalizePreparedLineStart, stepPreparedLineGeometryFromChunk, walkPreparedLinesRaw } from './line-break.js';
6
- import { buildLineTextFromRange, clearLineTextCaches, getLineTextCache } from './line-text.js';
7
- function createEmptyPrepared(includeSegments) {
8
- if (includeSegments) {
9
- return {
10
- widths: [],
11
- lineEndFitAdvances: [],
12
- lineEndPaintAdvances: [],
13
- kinds: [],
14
- simpleLineWalkFastPath: true,
15
- segLevels: null,
16
- breakableFitAdvances: [],
17
- breakablePreferredBreaks: [],
18
- letterSpacing: 0,
19
- spacingGraphemeCounts: [],
20
- discretionaryHyphenWidth: 0,
21
- tabStopAdvance: 0,
22
- chunks: [],
23
- segments: []
24
- };
25
- }
26
- return {
27
- widths: [],
28
- lineEndFitAdvances: [],
29
- lineEndPaintAdvances: [],
30
- kinds: [],
31
- simpleLineWalkFastPath: true,
32
- segLevels: null,
33
- breakableFitAdvances: [],
34
- breakablePreferredBreaks: [],
35
- letterSpacing: 0,
36
- spacingGraphemeCounts: [],
37
- discretionaryHyphenWidth: 0,
38
- tabStopAdvance: 0,
39
- chunks: []
40
- };
1
+ /*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
2
+ // Prepare text with engine segmentation rules and cached Canvas measurements, then
3
+ // lay it out with arithmetic. Preparation touches the DOM in three places: the emoji
4
+ // correction's span, read once per font where Canvas measures an emoji wider than its
5
+ // font size; the page's `<html lang>`, unless setLocale() gave a language; and, without
6
+ // OffscreenCanvas, a canvas element never attached. Layout itself does no measurement
7
+ // or string work.
8
+ // Rich APIs add source cursors and text materialization.
9
+ // Browser measurement limitations are documented in README.md and PLATFORM_BUGS.md.
10
+ // Based on Sebastian Markbage's text-layout research (github.com/chenglou/text-layout).
11
+
12
+ import { clearWordSegmenter } from './line-breaks.js'
13
+ import {
14
+ analyzeText,
15
+ KIND_BITS,
16
+ SEGMENT_KINDS,
17
+
18
+
19
+
20
+ } from './analysis.js'
21
+ import {
22
+ clearMeasurementCaches,
23
+ getEngineProfile,
24
+ getPreparationLanguage,
25
+ readLetterSpacing,
26
+ setLocaleLanguage,
27
+ } from './measurement.js'
28
+ import { measureAnalysis } from './prepare.js'
29
+ import {
30
+ countPreparedLines,
31
+ normalizeMaxWidth,
32
+ normalizePreparedLineStart,
33
+ stepPreparedLineGeometryFromStart,
34
+ walkPreparedLinesRaw,
35
+
36
+ } from './line-break.js'
37
+ import { buildLineTextFromRange, } from './line-text.js'
38
+
39
+ // --- Public types ---
40
+
41
+
42
+
43
+ // Keep the compact height-prediction handle opaque so the public API does not accidentally
44
+ // calcify around the current parallel-array representation.
45
+
46
+
47
+
48
+
49
+ // The handle that also keeps each segment's text, which the functions that return
50
+ // line text need (layoutWithLines(), layoutNextLine() and materializeLineRange()),
51
+ // and its kind, for the app's own rendering. Its type shows those two and each
52
+ // segment's width, read-only, and none of the line walkers' other storage, which
53
+ // changes with engine fixes (RESEARCH.md, Decisions Log, 2026-10-06).
54
+
55
+
56
+
57
+
58
+
59
+
60
+ // What the two handles hold: the line walkers' data and, from prepareWithSegments(),
61
+ // the segments' text and kinds. getInternalPrepared() reads a public handle as the
62
+ // first; createLayoutLine() and layoutNextLine(), which pass theirs on to the line
63
+ // text as it came, assert the second in place.
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+
107
+ // --- Public API ---
108
+
109
+ function prepareInternal(
110
+ text ,
111
+ font ,
112
+ includeSegments ,
113
+ options ,
114
+ ) {
115
+ const wordBreak = options?.wordBreak ?? 'normal'
116
+ const engineProfile = getEngineProfile()
117
+ const letterSpacing = readLetterSpacing(options?.letterSpacing, engineProfile)
118
+ // One language read: break rules and measurement both follow it.
119
+ const language = getPreparationLanguage(engineProfile)
120
+ const analysis = analyzeText(text, engineProfile, options?.whiteSpace, wordBreak, language)
121
+ return measureAnalysis(analysis, 0, analysis.flags.length, font, includeSegments, letterSpacing, engineProfile, language, true, null)
41
122
  }
42
- function countRenderedSpacingGraphemes(text, kind) {
43
- if (kind === 'zero-width-break' || kind === 'soft-hyphen' || kind === 'hard-break') {
44
- return 0;
45
- }
46
- if (kind === 'tab') return 1;
47
- let count = 0;
48
- const graphemeSegmenter = getSharedGraphemeSegmenter();
49
- for (const _ of graphemeSegmenter.segment(text))count++;
50
- return count;
123
+
124
+ // Prepare text for layout. Segments the text, measures each segment via canvas,
125
+ // and stores the widths for fast relayout at any width. Call once per text block
126
+ // (e.g. when a comment first appears). The result is width-independent — the
127
+ // same PreparedText can be laid out at any maxWidth and lineHeight via layout().
128
+ //
129
+ // Steps:
130
+ // 1. Normalize white space as white-space: normal or pre-wrap does
131
+ // 2. Find break opportunities with the engine's own line-break scan
132
+ // 3. Split the text into segments between them ("better." as one unit)
133
+ // 4. Measure each segment via canvas measureText, cache by (segment, font).
134
+ // A run of combining marks that glue or a control separates from its grapheme
135
+ // measures after that grapheme, and WebKit measures a word with the space after it
136
+ // 5. Measure where each text segment of two or more graphemes can break under
137
+ // overflow-wrap: break-word: by graphemes, pairs or prefixes, per engine
138
+ // 6. Correct emoji canvas inflation (probed once per font)
139
+ // 7. Record what changes at a line's edges: Blink's halts of CJK punctuation,
140
+ // U+3000 hangs, how much narrower a soft hyphen's neighbors measure joined,
141
+ // fresh-line widths inside segments with invisible characters, and the
142
+ // characters WebKit keeps after an overflowing first one
143
+ export function prepare(text , font , options ) {
144
+ return prepareInternal(text, font, false, options)
51
145
  }
52
- function addInternalLetterSpacing(width, graphemeCount, letterSpacing) {
53
- return graphemeCount > 1 ? width + (graphemeCount - 1) * letterSpacing : width;
146
+
147
+ // Rich variant used by callers that need enough information to render the
148
+ // laid-out lines themselves.
149
+ export function prepareWithSegments(text , font , options ) {
150
+ const prepared = prepareInternal(text, font, true, options)
151
+ // Each segment's kind by name, from its flags.
152
+ const kinds = []
153
+ for (let i = 0; i < prepared.segmentFlags.length; i++) kinds.push(SEGMENT_KINDS[prepared.segmentFlags[i] & KIND_BITS] )
154
+ prepared.kinds = kinds
155
+ return prepared
54
156
  }
55
- function measureAnalysis(analysis, font, includeSegments, wordBreak, letterSpacing) {
56
- const engineProfile = getEngineProfile();
57
- const { cache, emojiCorrection } = getFontMeasurementState(font, textMayContainEmoji(analysis.normalized));
58
- const discretionaryHyphenWidth = getCorrectedSegmentWidth('-', getSegmentMetrics('-', cache), emojiCorrection) + (letterSpacing === 0 ? 0 : letterSpacing * 2);
59
- const spaceWidth = getCorrectedSegmentWidth(' ', getSegmentMetrics(' ', cache), emojiCorrection);
60
- const tabStopAdvance = spaceWidth * 8;
61
- const hasLetterSpacing = letterSpacing !== 0;
62
- if (analysis.len === 0) return createEmptyPrepared(includeSegments);
63
- const widths = [];
64
- const lineEndFitAdvances = [];
65
- const lineEndPaintAdvances = [];
66
- const kinds = [];
67
- let simpleLineWalkFastPath = !hasLetterSpacing;
68
- const segStarts = includeSegments ? [] : null;
69
- const breakableFitAdvances = [];
70
- const breakablePreferredBreaks = [];
71
- const spacingGraphemeCounts = [];
72
- const segments = includeSegments ? [] : null;
73
- const chunks = [];
74
- let chunkStartSegmentIndex = 0;
75
- function pushMeasuredSegment(text, width, lineEndFitAdvance, lineEndPaintAdvance, kind, start, breakableFitAdvance, breakablePreferredBreak, spacingGraphemeCount) {
76
- if (kind !== 'text' && kind !== 'space' && kind !== 'zero-width-break') {
77
- simpleLineWalkFastPath = false;
78
- }
79
- widths.push(width);
80
- lineEndFitAdvances.push(lineEndFitAdvance);
81
- lineEndPaintAdvances.push(lineEndPaintAdvance);
82
- kinds.push(kind);
83
- segStarts?.push(start);
84
- breakableFitAdvances.push(breakableFitAdvance);
85
- breakablePreferredBreaks.push(breakablePreferredBreak);
86
- if (hasLetterSpacing) spacingGraphemeCounts.push(spacingGraphemeCount);
87
- if (segments !== null) segments.push(text);
88
- }
89
- function pushMeasuredTextSegment(text, textMetrics, kind, start, allowOverflowBreaks) {
90
- const spacingGraphemeCount = hasLetterSpacing ? countRenderedSpacingGraphemes(text, kind) : 0;
91
- const width = addInternalLetterSpacing(getCorrectedSegmentWidth(text, textMetrics, emojiCorrection), spacingGraphemeCount, letterSpacing);
92
- const baseLineEndFitAdvance = kind === 'space' || kind === 'preserved-space' || kind === 'zero-width-break' ? 0 : width;
93
- const lineEndFitAdvance = baseLineEndFitAdvance === 0 ? 0 : baseLineEndFitAdvance + (spacingGraphemeCount > 0 ? letterSpacing : 0);
94
- const lineEndPaintAdvance = kind === 'space' || kind === 'zero-width-break' ? 0 : width;
95
- if (allowOverflowBreaks && text.length > 1) {
96
- let fitMode = 'sum-graphemes';
97
- if (letterSpacing !== 0) {
98
- fitMode = 'segment-prefixes';
99
- } else if (isNumericRunSegment(text)) {
100
- fitMode = 'pair-context';
101
- } else if (engineProfile.preferPrefixWidthsForBreakableRuns) {
102
- fitMode = 'segment-prefixes';
103
- }
104
- const fitAdvances = getSegmentBreakableFitAdvances(text, textMetrics, cache, emojiCorrection, fitMode);
105
- const preferredBreaks = fitAdvances === null || wordBreak === 'keep-all' ? null : getBreakablePreferredBreaks(text);
106
- pushMeasuredSegment(text, width, lineEndFitAdvance, lineEndPaintAdvance, kind, start, fitAdvances, preferredBreaks, spacingGraphemeCount);
107
- return;
108
- }
109
- pushMeasuredSegment(text, width, lineEndFitAdvance, lineEndPaintAdvance, kind, start, null, null, spacingGraphemeCount);
110
- }
111
- for(let mi = 0; mi < analysis.len; mi++){
112
- const segText = analysis.texts[mi];
113
- const segKind = analysis.kinds[mi];
114
- const segStart = analysis.starts[mi];
115
- if (segKind === 'soft-hyphen') {
116
- pushMeasuredSegment(segText, 0, discretionaryHyphenWidth, discretionaryHyphenWidth, segKind, segStart, null, null, 0);
117
- continue;
118
- }
119
- if (segKind === 'hard-break') {
120
- const endSegmentIndex = widths.length;
121
- pushMeasuredSegment(segText, 0, 0, 0, segKind, segStart, null, null, 0);
122
- chunks.push({
123
- startSegmentIndex: chunkStartSegmentIndex,
124
- endSegmentIndex,
125
- consumedEndSegmentIndex: widths.length
126
- });
127
- chunkStartSegmentIndex = widths.length;
128
- continue;
129
- }
130
- if (segKind === 'tab') {
131
- pushMeasuredSegment(segText, 0, 0, 0, segKind, segStart, null, null, hasLetterSpacing ? countRenderedSpacingGraphemes(segText, segKind) : 0);
132
- continue;
133
- }
134
- if (segKind === 'text' && isCJK(segText)) {
135
- const measuredUnits = getCjkTextUnits(segText, engineProfile, wordBreak);
136
- for(let i = 0; i < measuredUnits.length; i++){
137
- const unit = measuredUnits[i];
138
- const unitMetrics = getSegmentMetrics(unit.text, cache);
139
- pushMeasuredTextSegment(unit.text, unitMetrics, 'text', segStart + unit.start, unit.overflow === 'grapheme' || analysis.isWordLike[mi] && (wordBreak === 'keep-all' || unit.overflow === 'word-like'));
140
- }
141
- continue;
142
- }
143
- pushMeasuredTextSegment(segText, getSegmentMetrics(segText, cache), segKind, segStart, segKind === 'text' && (analysis.isWordLike[mi] || isIndependentSymbolRun(segText)));
144
- }
145
- if (chunkStartSegmentIndex < widths.length) {
146
- chunks.push({
147
- startSegmentIndex: chunkStartSegmentIndex,
148
- endSegmentIndex: widths.length,
149
- consumedEndSegmentIndex: widths.length
150
- });
151
- }
152
- const segLevels = segStarts === null ? null : computeSegmentLevels(analysis.normalized, segStarts);
153
- if (segments !== null) {
154
- return {
155
- widths,
156
- lineEndFitAdvances,
157
- lineEndPaintAdvances,
158
- kinds,
159
- simpleLineWalkFastPath,
160
- segLevels,
161
- breakableFitAdvances,
162
- breakablePreferredBreaks,
163
- letterSpacing,
164
- spacingGraphemeCounts,
165
- discretionaryHyphenWidth,
166
- tabStopAdvance,
167
- chunks,
168
- segments
169
- };
170
- }
171
- return {
172
- widths,
173
- lineEndFitAdvances,
174
- lineEndPaintAdvances,
175
- kinds,
176
- simpleLineWalkFastPath,
177
- segLevels,
178
- breakableFitAdvances,
179
- breakablePreferredBreaks,
180
- letterSpacing,
181
- spacingGraphemeCounts,
182
- discretionaryHyphenWidth,
183
- tabStopAdvance,
184
- chunks
185
- };
157
+
158
+ function getInternalPrepared(prepared ) {
159
+ return prepared
186
160
  }
187
- function prepareInternal(text, font, includeSegments, options) {
188
- const wordBreak = options?.wordBreak ?? 'normal';
189
- const letterSpacing = options?.letterSpacing ?? 0;
190
- const analysis = analyzeText(text, getEngineProfile(), options?.whiteSpace, wordBreak);
191
- return measureAnalysis(analysis, font, includeSegments, wordBreak, letterSpacing);
161
+
162
+ // Layout prepared text at a given max width and caller-provided lineHeight.
163
+ // Pure arithmetic on cached widths — no canvas calls, no DOM reads, no string
164
+ // operations, and no per-line allocations. Call on every resize. Lines break
165
+ // where the engine's page breaks them, under the CSS the README lists
166
+ // (README.md, Caveats).
167
+ export function layout(prepared , maxWidth , lineHeight ) {
168
+ // The resize hot path counts the same lines as `layoutWithLines()` without
169
+ // building line ranges or text.
170
+ const lineCount = countPreparedLines(getInternalPrepared(prepared), normalizeMaxWidth(maxWidth))
171
+ return { lineCount, height: lineCount * lineHeight }
192
172
  }
193
- export function prepare(text, font, options) {
194
- return prepareInternal(text, font, false, options);
173
+
174
+ // Reported widths are clamped at zero wherever a line is built. A line's
175
+ // advance can be negative: the rest of a word after an emergency break can
176
+ // hold only invisible characters and the word's kerning with a following
177
+ // space, and letter spacing can be strongly negative. Line breaking keeps the
178
+ // signed advance.
179
+ function createLayoutLine(
180
+ prepared ,
181
+ width ,
182
+ startSegmentIndex ,
183
+ startGraphemeIndex ,
184
+ endSegmentIndex ,
185
+ endGraphemeIndex ,
186
+ ) {
187
+ return {
188
+ text: buildLineTextFromRange(
189
+ prepared ,
190
+ startSegmentIndex,
191
+ startGraphemeIndex,
192
+ endSegmentIndex,
193
+ endGraphemeIndex,
194
+ ),
195
+ width: Math.max(0, width),
196
+ start: {
197
+ segmentIndex: startSegmentIndex,
198
+ graphemeIndex: startGraphemeIndex,
199
+ },
200
+ end: {
201
+ segmentIndex: endSegmentIndex,
202
+ graphemeIndex: endGraphemeIndex,
203
+ },
204
+ }
195
205
  }
196
- export function prepareWithSegments(text, font, options) {
197
- return prepareInternal(text, font, true, options);
206
+
207
+ function createLayoutLineRange(
208
+ width ,
209
+ startSegmentIndex ,
210
+ startGraphemeIndex ,
211
+ endSegmentIndex ,
212
+ endGraphemeIndex ,
213
+ ) {
214
+ return {
215
+ width: Math.max(0, width),
216
+ start: {
217
+ segmentIndex: startSegmentIndex,
218
+ graphemeIndex: startGraphemeIndex,
219
+ },
220
+ end: {
221
+ segmentIndex: endSegmentIndex,
222
+ graphemeIndex: endGraphemeIndex,
223
+ },
224
+ }
198
225
  }
199
- function getInternalPrepared(prepared) {
200
- return prepared;
226
+
227
+ export function materializeLineRange(
228
+ prepared ,
229
+ line ,
230
+ ) {
231
+ return createLayoutLine(
232
+ prepared,
233
+ line.width,
234
+ line.start.segmentIndex,
235
+ line.start.graphemeIndex,
236
+ line.end.segmentIndex,
237
+ line.end.graphemeIndex,
238
+ )
201
239
  }
202
- export function layout(prepared, maxWidth, lineHeight) {
203
- const lineCount = countPreparedLines(getInternalPrepared(prepared), maxWidth);
204
- return {
205
- lineCount,
206
- height: lineCount * lineHeight
207
- };
208
- }
209
- function createLayoutLine(prepared, cache, width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) {
210
- return {
211
- text: buildLineTextFromRange(prepared, cache, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex),
212
- width,
213
- start: {
214
- segmentIndex: startSegmentIndex,
215
- graphemeIndex: startGraphemeIndex
216
- },
217
- end: {
218
- segmentIndex: endSegmentIndex,
219
- graphemeIndex: endGraphemeIndex
220
- }
221
- };
222
- }
223
- function createLayoutLineRange(width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) {
224
- return {
240
+
241
+ // Batch low-level line-range pass. This is the non-materializing counterpart
242
+ // to layoutWithLines(), useful for shrinkwrap and other aggregate stats work.
243
+ // It, measureLineStats(), measureNaturalWidth() and layoutNextLineRange() return
244
+ // widths and cursors and no text, from the line-break data layout() reads, so they
245
+ // take a prepare() handle as well (RESEARCH.md, Decisions Log, 2026-10-06).
246
+ export function walkLineRanges(
247
+ prepared ,
248
+ maxWidth ,
249
+ onLine ,
250
+ ) {
251
+ return walkPreparedLinesRaw(
252
+ getInternalPrepared(prepared),
253
+ normalizeMaxWidth(maxWidth),
254
+ (width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) => {
255
+ onLine(createLayoutLineRange(
225
256
  width,
226
- start: {
227
- segmentIndex: startSegmentIndex,
228
- graphemeIndex: startGraphemeIndex
229
- },
230
- end: {
231
- segmentIndex: endSegmentIndex,
232
- graphemeIndex: endGraphemeIndex
233
- }
234
- };
257
+ startSegmentIndex,
258
+ startGraphemeIndex,
259
+ endSegmentIndex,
260
+ endGraphemeIndex,
261
+ ))
262
+ },
263
+ )
235
264
  }
236
- export function materializeLineRange(prepared, line) {
237
- return createLayoutLine(prepared, getLineTextCache(prepared), line.width, line.start.segmentIndex, line.start.graphemeIndex, line.end.segmentIndex, line.end.graphemeIndex);
265
+
266
+ export function measureLineStats(
267
+ prepared ,
268
+ maxWidth ,
269
+ ) {
270
+ const stats = { lineCount: 0, maxLineWidth: 0 }
271
+ walkPreparedLinesRaw(getInternalPrepared(prepared), normalizeMaxWidth(maxWidth), undefined, stats)
272
+ return stats
238
273
  }
239
- export function walkLineRanges(prepared, maxWidth, onLine) {
240
- if (prepared.widths.length === 0) return 0;
241
- return walkPreparedLinesRaw(getInternalPrepared(prepared), maxWidth, (width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex)=>{
242
- onLine(createLayoutLineRange(width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex));
243
- });
274
+
275
+ // Intrinsic-width helper for rich/userland layout work. This asks "how wide is
276
+ // the prepared text when container width is not the thing forcing wraps?".
277
+ // Explicit hard breaks still count, so this returns the widest forced line.
278
+ export function measureNaturalWidth(prepared ) {
279
+ return measureLineStats(prepared, Number.POSITIVE_INFINITY).maxLineWidth
244
280
  }
245
- export function measureLineStats(prepared, maxWidth) {
246
- return measurePreparedLineGeometry(getInternalPrepared(prepared), maxWidth);
281
+
282
+ // Steps one streamed line from `start` into the result's cursors: the
283
+ // normalized line start and the line end. Returns the reported width, or null
284
+ // after the last line.
285
+ function stepNextLine(
286
+ prepared ,
287
+ start ,
288
+ maxWidth ,
289
+ lineStart ,
290
+ lineEnd ,
291
+ ) {
292
+ const internal = getInternalPrepared(prepared)
293
+ lineEnd.segmentIndex = start.segmentIndex
294
+ lineEnd.graphemeIndex = start.graphemeIndex
295
+ if (!normalizePreparedLineStart(internal, lineEnd)) return null
296
+
297
+ lineStart.segmentIndex = lineEnd.segmentIndex
298
+ lineStart.graphemeIndex = lineEnd.graphemeIndex
299
+ const width = stepPreparedLineGeometryFromStart(internal, lineEnd, maxWidth)
300
+ return width === null ? null : Math.max(0, width)
247
301
  }
248
- export function measureNaturalWidth(prepared) {
249
- let maxWidth = 0;
250
- walkPreparedLinesRaw(getInternalPrepared(prepared), Number.POSITIVE_INFINITY, (width)=>{
251
- if (width > maxWidth) maxWidth = width;
252
- });
253
- return maxWidth;
302
+
303
+ export function layoutNextLine(
304
+ prepared ,
305
+ start ,
306
+ maxWidth ,
307
+ ) {
308
+ const lineStart = { segmentIndex: 0, graphemeIndex: 0 }
309
+ const end = { segmentIndex: 0, graphemeIndex: 0 }
310
+ const width = stepNextLine(prepared, start, maxWidth, lineStart, end)
311
+ if (width === null) return null
312
+
313
+ const text = buildLineTextFromRange(
314
+ prepared ,
315
+ lineStart.segmentIndex,
316
+ lineStart.graphemeIndex,
317
+ end.segmentIndex,
318
+ end.graphemeIndex,
319
+ )
320
+ return { text, width, start: lineStart, end }
254
321
  }
255
- export function layoutNextLine(prepared, start, maxWidth) {
256
- const internal = getInternalPrepared(prepared);
257
- const end = {
258
- segmentIndex: start.segmentIndex,
259
- graphemeIndex: start.graphemeIndex
260
- };
261
- const chunkIndex = normalizePreparedLineStart(internal, end);
262
- if (chunkIndex < 0) return null;
263
- const lineStartSegmentIndex = end.segmentIndex;
264
- const lineStartGraphemeIndex = end.graphemeIndex;
265
- const width = stepPreparedLineGeometryFromChunk(internal, end, chunkIndex, maxWidth);
266
- if (width === null) return null;
267
- return createLayoutLine(prepared, getLineTextCache(prepared), width, lineStartSegmentIndex, lineStartGraphemeIndex, end.segmentIndex, end.graphemeIndex);
322
+
323
+ export function layoutNextLineRange(
324
+ prepared ,
325
+ start ,
326
+ maxWidth ,
327
+ ) {
328
+ const lineStart = { segmentIndex: 0, graphemeIndex: 0 }
329
+ const end = { segmentIndex: 0, graphemeIndex: 0 }
330
+ const width = stepNextLine(prepared, start, maxWidth, lineStart, end)
331
+ return width === null ? null : { width, start: lineStart, end }
268
332
  }
269
- export function layoutNextLineRange(prepared, start, maxWidth) {
270
- const internal = getInternalPrepared(prepared);
271
- const end = {
272
- segmentIndex: start.segmentIndex,
273
- graphemeIndex: start.graphemeIndex
274
- };
275
- const chunkIndex = normalizePreparedLineStart(internal, end);
276
- if (chunkIndex < 0) return null;
277
- const lineStartSegmentIndex = end.segmentIndex;
278
- const lineStartGraphemeIndex = end.graphemeIndex;
279
- const width = stepPreparedLineGeometryFromChunk(internal, end, chunkIndex, maxWidth);
280
- if (width === null) return null;
281
- return createLayoutLineRange(width, lineStartSegmentIndex, lineStartGraphemeIndex, end.segmentIndex, end.graphemeIndex);
282
- }
283
- export function layoutWithLines(prepared, maxWidth, lineHeight) {
284
- const lines = [];
285
- if (prepared.widths.length === 0) return {
286
- lineCount: 0,
287
- height: 0,
288
- lines
289
- };
290
- const graphemeCache = getLineTextCache(prepared);
291
- const lineCount = walkPreparedLinesRaw(getInternalPrepared(prepared), maxWidth, (width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex)=>{
292
- lines.push(createLayoutLine(prepared, graphemeCache, width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex));
293
- });
294
- return {
295
- lineCount,
296
- height: lineCount * lineHeight,
297
- lines
298
- };
333
+
334
+ // Rich layout API for callers that want the actual line contents and widths.
335
+ // Caller still supplies lineHeight at layout time. Mirrors layout()'s break
336
+ // decisions, but keeps extra per-line bookkeeping so it should stay off the
337
+ // resize hot path.
338
+ export function layoutWithLines(prepared , maxWidth , lineHeight ) {
339
+ const lines = []
340
+ const lineCount = walkPreparedLinesRaw(
341
+ getInternalPrepared(prepared),
342
+ normalizeMaxWidth(maxWidth),
343
+ (width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) => {
344
+ lines.push(createLayoutLine(
345
+ prepared,
346
+ width,
347
+ startSegmentIndex,
348
+ startGraphemeIndex,
349
+ endSegmentIndex,
350
+ endGraphemeIndex,
351
+ ))
352
+ },
353
+ )
354
+
355
+ return { lineCount, height: lineCount * lineHeight, lines }
299
356
  }
300
- export function clearCache() {
301
- clearAnalysisCaches();
302
- clearLineTextCaches();
303
- clearMeasurementCaches();
357
+
358
+ export function clearCache() {
359
+ clearWordSegmenter()
360
+ clearMeasurementCaches()
304
361
  }
305
- export function setLocale(locale) {
306
- setAnalysisLocale(locale);
307
- clearCache();
362
+
363
+ // Sets the language later preparation breaks and measures under in place of
364
+ // `<html lang>`, which a worker doesn't have; an empty one is a page's without a
365
+ // language. Without a locale, preparation reads `<html lang>` again. Prepared
366
+ // handles keep theirs (RESEARCH.md, Decisions Log, 2026-09-26).
367
+ export function setLocale(locale ) {
368
+ setLocaleLanguage(locale)
369
+ clearCache()
308
370
  }