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.
- package/LICENSE +2 -2
- package/README.md +213 -53
- package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
- package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
- package/dist/agent/vendor/pretext/LICENSE +208 -0
- package/dist/agent/vendor/pretext/NOTICE +26 -13
- package/dist/agent/vendor/pretext/SOURCE.json +213 -45
- package/dist/agent/vendor/pretext/analysis.js +675 -1224
- package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
- package/dist/agent/vendor/pretext/graphemes.js +98 -0
- package/dist/agent/vendor/pretext/han-kerning.js +253 -0
- package/dist/agent/vendor/pretext/layout.js +352 -290
- package/dist/agent/vendor/pretext/line-break.js +1154 -588
- package/dist/agent/vendor/pretext/line-breaks.js +892 -0
- package/dist/agent/vendor/pretext/line-text.js +81 -39
- package/dist/agent/vendor/pretext/measurement.js +1105 -176
- package/dist/agent/vendor/pretext/prepare.js +807 -0
- package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
- package/dist/agent/will.mjs +2 -0
- package/dist/kit/assets.mjs +5 -1
- package/dist/kit/conformance/run.mjs +4 -4
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +9 -7
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +166 -53
- package/dist/kit/ledger/hash.mjs +75 -18
- package/dist/kit/ledger/history.mjs +143 -115
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/render-markdown.mjs +259 -122
- package/dist/kit/render-sanitize.mjs +24 -4
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +101 -65
- package/dist/kit/typography.mjs +2 -0
- package/dist/layout/hyphenation.mjs +127 -0
- package/dist/layout/line-plan.mjs +88 -96
- package/dist/layout/model.mjs +61 -80
- package/dist/layout/paragraph-fit.mjs +191 -0
- package/dist/layout/preparation.mjs +373 -0
- package/dist/layout/prepared-paragraph.mjs +223 -0
- package/dist/layout/typography.mjs +345 -111
- package/dist/packages/rapier-diff/index.mjs +288 -196
- package/dist/spec/document-fonts.mjs +13 -4
- package/dist/spec/md-assets.mjs +149 -30
- package/dist/spec/md-layout.mjs +5 -2
- package/dist/spec/md-marks.mjs +218 -3
- package/dist/spec/md-source.mjs +122 -0
- package/package.json +2 -1
- package/style.css +9 -0
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
|
@@ -1,308 +1,370 @@
|
|
|
1
|
-
/*! Pretext
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
};
|
|
257
|
+
startSegmentIndex,
|
|
258
|
+
startGraphemeIndex,
|
|
259
|
+
endSegmentIndex,
|
|
260
|
+
endGraphemeIndex,
|
|
261
|
+
))
|
|
262
|
+
},
|
|
263
|
+
)
|
|
235
264
|
}
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
357
|
+
|
|
358
|
+
export function clearCache() {
|
|
359
|
+
clearWordSegmenter()
|
|
360
|
+
clearMeasurementCaches()
|
|
304
361
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
}
|