rapier-markdown-kit 1.1.102 → 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,291 +1,1260 @@
1
- /*! Pretext 0.0.9 | MIT | Copyright (c) 2026 Pretext contributors */
2
- import { prepareWithSegments } from './layout.js';
3
- import { buildLineTextFromRange, getLineTextCache } from './line-text.js';
4
- import { stepPreparedLineGeometry } from './line-break.js';
5
- import { getFontMeasurementState, getSegmentMetrics } from './measurement.js';
6
- const EMPTY_LAYOUT_CURSOR = {
7
- segmentIndex: 0,
8
- graphemeIndex: 0
9
- };
10
- const RICH_INLINE_START_CURSOR = {
11
- itemIndex: 0,
12
- segmentIndex: 0,
13
- graphemeIndex: 0
14
- };
15
- function getInternalPreparedRichInline(prepared) {
16
- return prepared;
1
+ /*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
2
+
3
+ import {
4
+ alignToSource,
5
+ analyzeText,
6
+ isCollapsibleSpaceCode,
7
+ HARD_BREAK,
8
+ KIND_BITS,
9
+ OBJECT,
10
+ PRESERVED_SPACE,
11
+ RETURNABLE,
12
+ SOFT_HYPHEN,
13
+ SPACE,
14
+ SPACED,
15
+ TAB,
16
+ TEXT,
17
+ UNBROKEN,
18
+ ZERO_WIDTH_BREAK,
19
+ ZERO_WIDTH_GLUE,
20
+
21
+
22
+ } from './analysis.js'
23
+ import { getSegmentEntryWidth, } from './entry-geometry.js'
24
+ import { buildLineTextFromRange, buildRangeText, getGraphemeEnds, } from './line-text.js'
25
+ import {
26
+ getItemTabAdvance,
27
+ normalizeMaxWidth,
28
+ normalizePreparedLineStart,
29
+ stepPreparedLineGeometryFromStart,
30
+ walkPreparedLinesRaw,
31
+
32
+
33
+ } from './line-break.js'
34
+ import { getEngineProfile, getFontMeasurement, getPreparationLanguage, getSpaceWidth, readLetterSpacing, zeros, } from './measurement.js'
35
+ import { measureAnalysis, } from './prepare.js'
36
+
37
+ // Helper for rich-text inline flow under `white-space: normal` or `pre-wrap`: one paragraph's text
38
+ // broken across its items, as a browser lays out the text of an inline formatting context across
39
+ // its spans. The items' texts join into the paragraph's text, an atomic item or a box as one
40
+ // U+FFFC, which is analyzed once, as prepare() analyzes a text, with a segment starting wherever an
41
+ // item does. Each item's segments are measured in its font, and the text walkers lay the paragraph
42
+ // out. So:
43
+ // - white space collapses across item boundaries, and under pre-wrap preserved spaces hang
44
+ // across them, tab stops count from the line's start, and hard breaks end lines
45
+ // - break opportunities across item boundaries are the engine's: from the text the items join in
46
+ // Blink and Gecko, and in WebKit from each item's own text and the last two characters before
47
+ // it, under the paragraph's word-break; a run that continues across items wraps together
48
+ // - atomic inline boxes like pills, and boxes the app sizes and paints, such as images, are one
49
+ // segment each, which a line can break before and after
50
+ // - an item's extra horizontal chrome, such as padding and borders, is part of its width on
51
+ // every line the item reaches
52
+ // A line's fragments are its segments cut where the item changes.
53
+
54
+
55
+
56
+ const PRESERVED_WHITE_SPACE = 1 << PRESERVED_SPACE | 1 << TAB
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+ // An object inside a line that the app sizes and paints, such as an image, a custom emoji, a formula
68
+ // or a badge: an atomic inline with no text, as an <img> or an empty inline-block is, which a line
69
+ // can break before and after. An item without text is one.
70
+
71
+
72
+
73
+
74
+
75
+ // How the paragraph the items make wraps, one setting for all of them, as CSS on the
76
+ // element that holds the spans sets it.
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+ // A place in the paragraph: an item, and a segment and grapheme of that item's part of the
87
+ // paragraph's text, whose segments no app sees. What a line or a fragment range gives is for
88
+ // passing back. A materialized fragment's type has no cursors (RESEARCH.md, Decisions Log,
89
+ // 2026-10-10): its place in its item's text is its sourceStart and sourceEnd, and the line to
90
+ // pass back is the range it was materialized from.
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+
113
+
114
+
115
+
116
+ // A materialized fragment as materializeRichInlineLineRange() builds it: with its range's
117
+ // cursors, which are off its type only (RESEARCH.md, Decisions Log, 2026-10-10).
118
+
119
+
120
+
121
+
122
+
123
+
124
+
125
+
126
+
127
+
128
+
129
+
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
144
+
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+
154
+
155
+
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+
167
+
168
+
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+ const RICH_INLINE_START_CURSOR = {
177
+ itemIndex: 0,
178
+ segmentIndex: 0,
179
+ graphemeIndex: 0,
17
180
  }
18
- function cloneCursor(cursor) {
19
- return {
20
- segmentIndex: cursor.segmentIndex,
21
- graphemeIndex: cursor.graphemeIndex
22
- };
181
+
182
+ function getInternalPreparedRichInline(prepared ) {
183
+ return prepared
23
184
  }
24
- function isLineStartCursor(cursor) {
25
- return cursor.segmentIndex === 0 && cursor.graphemeIndex === 0;
26
- }
27
- function isCollapsibleBoundaryWhitespace(code) {
28
- return code === 0x20 || code === 0x09 || code === 0x0A || code === 0x0C || code === 0x0D;
29
- }
30
- function getCollapsedSpaceWidth(font, letterSpacing) {
31
- const { cache } = getFontMeasurementState(font, false);
32
- return getSegmentMetrics(' ', cache).width + letterSpacing;
33
- }
34
- function measureWholeItem(prepared) {
35
- const end = {
36
- segmentIndex: 0,
37
- graphemeIndex: 0
38
- };
39
- return stepPreparedLineGeometry(prepared, end, Number.POSITIVE_INFINITY);
40
- }
41
- function endsInsideFirstSegment(segmentIndex, graphemeIndex) {
42
- return segmentIndex === 0 && graphemeIndex > 0;
43
- }
44
- export function prepareRichInline(items) {
45
- const preparedItems = Array.from({
46
- length: items.length
47
- });
48
- let pendingGapWidth = null;
49
- let breakAfterPreviousItem = false;
50
- for(let index = 0; index < items.length; index++){
51
- const item = items[index];
52
- const letterSpacing = item.letterSpacing ?? 0;
53
- let start = 0;
54
- while(start < item.text.length && isCollapsibleBoundaryWhitespace(item.text.charCodeAt(start)))start++;
55
- if (start === item.text.length) {
56
- if (start > 0 && pendingGapWidth === null) {
57
- pendingGapWidth = getCollapsedSpaceWidth(item.font, letterSpacing);
58
- }
59
- continue;
60
- }
61
- let end = item.text.length;
62
- while(end > start && isCollapsibleBoundaryWhitespace(item.text.charCodeAt(end - 1)))end--;
63
- const hasLeadingWhitespace = start > 0;
64
- const hasTrailingWhitespace = end < item.text.length;
65
- const trimmedText = item.text.slice(start, end);
66
- const gapBefore = pendingGapWidth ?? (hasLeadingWhitespace ? getCollapsedSpaceWidth(item.font, letterSpacing) : 0);
67
- const prepared = prepareWithSegments(trimmedText, item.font, letterSpacing === 0 ? undefined : {
68
- letterSpacing
69
- });
70
- const wholeWidth = measureWholeItem(prepared);
71
- const establishesLine = wholeWidth !== null || prepared.kinds.includes('zero-width-break');
72
- const preparedItem = {
73
- break: item.break ?? 'normal',
74
- breakBefore: pendingGapWidth !== null || hasLeadingWhitespace || breakAfterPreviousItem,
75
- establishesLine,
76
- extraWidth: item.extraWidth ?? 0,
77
- gapBefore,
78
- naturalWidth: wholeWidth ?? 0,
79
- prepared
80
- };
81
- preparedItems[index] = preparedItem;
82
- if (establishesLine) breakAfterPreviousItem = prepared.kinds.at(-1) === 'zero-width-break';
83
- pendingGapWidth = hasTrailingWhitespace ? getCollapsedSpaceWidth(item.font, letterSpacing) : null;
185
+
186
+ export function prepareRichInline(items , options ) {
187
+ const paragraphOnly = options?.rapierParagraph === true
188
+ const whiteSpace = options?.whiteSpace ?? 'normal'
189
+ const wordBreak = options?.wordBreak ?? 'normal'
190
+ const preserve = whiteSpace === 'pre-wrap'
191
+ // One language read for the paragraph's analysis and every item's measurement.
192
+ const profile = getEngineProfile()
193
+ const language = getPreparationLanguage(profile)
194
+ // Whether a line can end before preserved white space that starts an item: WebKit finds a soft
195
+ // wrap opportunity next to every white-space item (isAtSoftWrapOpportunity,
196
+ // InlineFormattingUtils.cpp:406-418), which is part of how it finds breaks between inline items,
197
+ // so the profile's scan names it.
198
+ const whiteSpaceItemBreaks = profile.lineBreakScan === 'webkit'
199
+
200
+ // A paragraph of one styled run that isn't atomic and has no extraWidth is the most common one,
201
+ // and is that run's text: its handle is the one prepareWithSegments() makes, laid out as
202
+ // layout()'s walkers lay it out. So is a paragraph whose other items are empty, which are
203
+ // dropped (below).
204
+ let onlyIndex = -1
205
+ for (let index = 0; index < items.length; index++) {
206
+ if (items[index] .text === '') continue
207
+ if (onlyIndex >= 0) {
208
+ onlyIndex = -1
209
+ break
84
210
  }
85
- return {
86
- items: preparedItems
87
- };
88
- }
89
- function stepRichInlineLine(flow, maxWidth, cursor, collectFragment) {
90
- if (flow.items.length === 0 || cursor.itemIndex >= flow.items.length) return null;
91
- const safeWidth = Math.max(1, maxWidth);
92
- let hasContent = false;
93
- let lineWidth = 0;
94
- let remainingWidth = safeWidth;
95
- let itemIndex = cursor.itemIndex;
96
- lineLoop: while(itemIndex < flow.items.length){
97
- const item = flow.items[itemIndex];
98
- if (item === undefined) {
99
- itemIndex++;
100
- cursor.segmentIndex = 0;
101
- cursor.graphemeIndex = 0;
102
- continue;
103
- }
104
- if (!isLineStartCursor(cursor) && cursor.segmentIndex === item.prepared.segments.length && cursor.graphemeIndex === 0) {
105
- itemIndex++;
106
- cursor.segmentIndex = 0;
107
- cursor.graphemeIndex = 0;
108
- continue;
109
- }
110
- if (!item.establishesLine) {
111
- collectFragment?.(itemIndex, 0, 0, cloneCursor(EMPTY_LAYOUT_CURSOR), {
112
- segmentIndex: item.prepared.segments.length,
113
- graphemeIndex: 0
114
- });
115
- itemIndex++;
116
- cursor.segmentIndex = 0;
117
- cursor.graphemeIndex = 0;
118
- continue;
119
- }
120
- const gapBefore = hasContent ? item.gapBefore : 0;
121
- const atItemStart = isLineStartCursor(cursor);
122
- if (item.break === 'never') {
123
- if (!atItemStart) {
124
- itemIndex++;
125
- cursor.segmentIndex = 0;
126
- cursor.graphemeIndex = 0;
127
- continue;
128
- }
129
- const occupiedWidth = item.naturalWidth + item.extraWidth;
130
- const totalWidth = gapBefore + occupiedWidth;
131
- if (hasContent && totalWidth > remainingWidth) break lineLoop;
132
- collectFragment?.(itemIndex, gapBefore, occupiedWidth, cloneCursor(EMPTY_LAYOUT_CURSOR), {
133
- segmentIndex: item.prepared.segments.length,
134
- graphemeIndex: 0
135
- });
136
- hasContent = true;
137
- lineWidth += totalWidth;
138
- remainingWidth = safeWidth - lineWidth;
139
- itemIndex++;
140
- cursor.segmentIndex = 0;
141
- cursor.graphemeIndex = 0;
142
- continue;
143
- }
144
- const reservedWidth = gapBefore + item.extraWidth;
145
- if (hasContent && reservedWidth > remainingWidth) break lineLoop;
146
- if (atItemStart) {
147
- const totalWidth = reservedWidth + item.naturalWidth;
148
- if (totalWidth <= remainingWidth) {
149
- collectFragment?.(itemIndex, gapBefore, item.naturalWidth + item.extraWidth, cloneCursor(EMPTY_LAYOUT_CURSOR), {
150
- segmentIndex: item.prepared.segments.length,
151
- graphemeIndex: 0
152
- });
153
- hasContent = true;
154
- lineWidth += totalWidth;
155
- remainingWidth = safeWidth - lineWidth;
156
- itemIndex++;
157
- cursor.segmentIndex = 0;
158
- cursor.graphemeIndex = 0;
159
- continue;
160
- }
211
+ onlyIndex = index
212
+ }
213
+ const only = onlyIndex < 0 ? undefined : items[onlyIndex]
214
+ if (only !== undefined && only.text !== undefined && only.break !== 'never' && (only.extraWidth ?? 0) === 0) {
215
+ const analysis = analyzeText(only.text, profile, whiteSpace, wordBreak, language)
216
+ const data = measureAnalysis(analysis, 0, analysis.flags.length, only.font, true, readLetterSpacing(only.letterSpacing, profile), profile, language, !paragraphOnly, null, paragraphOnly ? '-' : null)
217
+ const itemSegments = []
218
+ for (let index = 0; index <= items.length; index++) itemSegments.push(index <= onlyIndex ? 0 : data.segmentFlags.length)
219
+ return (paragraphOnly ? paragraphSegments : findWholeLine)({
220
+ data, onlyItem: onlyIndex, wholeWidth: null, wholeStart: 0, wholeEnd: 0, itemSegments, segmentItems: null, gapSegments: null, plainWidths: false,
221
+ sourceStarts: null, sourceEnds: null, sourceUnits: null, text: only.text,
222
+ } )
223
+ }
224
+
225
+ // The paragraph's text: the items' texts joined, an atomic item or a box as one U+FFFC. An atomic
226
+ // item's own white space is none of the paragraph's: its inline-block lays its text out as a
227
+ // paragraph of its own, whose lines drop white space at their start and end. Blink puts the box in
228
+ // the outer paragraph as one U+FFFC (inline_node.cc:408-422) and removes its own paragraph's
229
+ // leading and trailing spaces (inline_items_builder.cc:869-871, ExitBlock at 1622-1629); WebKit's
230
+ // atomic inline box is one item of the outer line (InlineItemsBuilder.cpp:1073-1074), and its own
231
+ // lines collapse leading white space (Line::appendText, InlineLine.cpp:348-373) and remove
232
+ // trailing (InlineLineBuilder.cpp:646); Gecko's text run stops at the box
233
+ // (BuildTextRunsScanner::ScanFrame, nsTextFrame.cpp:2248-2254), and its own lines skip leading
234
+ // white space (nsTextFrame.cpp:10935-10944) and trim trailing (nsBlockFrame.cpp:5844). So an
235
+ // atomic item of only white space is still an object, as wide as its extraWidth. An item whose
236
+ // text is empty, atomic or not, is dropped, with no fragment and no width: apps empty a run to
237
+ // hide it (RESEARCH.md, Objects Inside A Line).
238
+ let source = ''
239
+ const starts = []
240
+ const atomic = []
241
+ // Whether an item is an object or has extraWidth, so that a line of it alone isn't its text's.
242
+ let paddedOrObject = false
243
+ // How many text items have extraWidth: each may get one segment more than the analysis gives it,
244
+ // the start edge of its opening (below).
245
+ let padded = 0
246
+ // The letter spacing the text items that aren't atomic share, and whether two of them differ: the
247
+ // walkers take one for the handle, so where items differ, each segment's width holds its own.
248
+ let sharedSpacing = null
249
+ let spacingsDiffer = false
250
+ // Whether an item may make a line narrower: under a negative letter spacing or extraWidth.
251
+ let mayNarrow = false
252
+ for (let index = 0; index < items.length; index++) {
253
+ const item = items[index]
254
+ starts.push(source.length)
255
+ if (item.text === undefined) {
256
+ // A width that isn't finite would give lines of width NaN or Infinity, and Chrome breaks lines
257
+ // around a negative one otherwise than a negative extraWidth does (RESEARCH.md, Objects Inside
258
+ // A Line), so Pretext refuses both.
259
+ if (!Number.isFinite(item.width) || item.width < 0) throw new RangeError(`Item ${index} has no text, so it's a box, whose width must be a finite number of CSS px, at least 0, not ${item.width}`)
260
+ atomic.push(true)
261
+ source += ''
262
+ paddedOrObject = true
263
+ continue
264
+ }
265
+ const isAtomic = item.break === 'never' && item.text !== ''
266
+ atomic.push(isAtomic)
267
+ if (isAtomic) {
268
+ source += ''
269
+ } else {
270
+ source += item.text
271
+ if (item.text !== '') {
272
+ const letterSpacing = readLetterSpacing(item.letterSpacing, profile)
273
+ if (sharedSpacing !== null && sharedSpacing !== letterSpacing) spacingsDiffer = true
274
+ sharedSpacing ??= letterSpacing
275
+ if (letterSpacing < 0) mayNarrow = true
276
+ }
277
+ }
278
+ if (isAtomic || (item.extraWidth ?? 0) !== 0) paddedOrObject = true
279
+ if (!isAtomic && (item.extraWidth ?? 0) !== 0) padded++
280
+ if ((item.extraWidth ?? 0) < 0) mayNarrow = true
281
+ }
282
+ const paragraph = { items, starts, atomic, ownSegmentBreaks: !profile.transformsSegmentBreaksAcrossItems, sourceOffsets: null }
283
+ const analysis = analyzeText(source, profile, whiteSpace, wordBreak, language, paragraph)
284
+ const offsets = paragraph.sourceOffsets
285
+ const count = analysis.flags.length
286
+
287
+ // The paragraph's lists, which each item's measurement adds its segments to (measureAnalysis).
288
+ const widths = []
289
+ const flags = new Uint8Array(count + padded)
290
+ const breakableFitAdvances = []
291
+ const lists = { widths, segmentFlags: flags, breakableFitAdvances }
292
+ const segments = []
293
+ const sourceStarts = []
294
+ const sourceEnds = []
295
+ let sourceUnits = null
296
+ const itemSegments = []
297
+ // What only some segments have, each made whole at the first one that does: a zero or a null for
298
+ // every segment the paragraph can have, `flags.length` of them, which is more than it has where
299
+ // a padded item gets no start edge, and the segment's value stored at its index. Chrome 154,
300
+ // Firefox 156 and Safari 27 prepare CJK styled paragraphs again up to 8% faster with the lists
301
+ // made whole than with each filled by a call and a push a segment (RESEARCH.md, Keeping Work
302
+ // Bounded, JavaScript Engines, under A paragraph's sparse lists made whole).
303
+ let entryGeometry = null
304
+ let lineStartProhibitions = null
305
+ let breakableLineStartExtras = null
306
+ let lineStartExtras = null
307
+ let lineEndTrims = null
308
+ let overflowLineEndTrims = null
309
+ let itemEndHalts = null
310
+ // A line returns from a soft hyphen whose hyphen doesn't fit on a handle with soft-hyphen
311
+ // contexts, which an item's measurement makes for text with one.
312
+ let discretionaryHyphenContexts = null
313
+ let insideExtras = null
314
+ let fillExtras = null
315
+ let openingEdges = null
316
+ // Each text item's hyphen width, tab stop advance and least tab advance, and whether two items
317
+ // differ in one, in a paragraph whose text has what reads them: a soft hyphen or, preserved, a
318
+ // tab (ParagraphSegmentData). Another paragraph keeps none, and its handle's own are 0.
319
+ const readsItemFonts = source.includes('\u00AD') || (preserve && source.includes('\t'))
320
+ const hyphenWidths = zeros(readsItemFonts ? items.length : 0)
321
+ const tabStopAdvances = zeros(readsItemFonts ? items.length : 0)
322
+ const minimumTabAdvances = zeros(readsItemFonts ? items.length : 0)
323
+ let firstTextItem = -1
324
+ let fontsDiffer = false
325
+ let simple = !analysis.hasUnbroken
326
+ // Whether a segment got no break before it here (below), an item's start edge or white space that
327
+ // goes on from the item before, in a paragraph whose analysis found none, so its other segments
328
+ // aren't marked as breaks to return to.
329
+ let marksReturnable = false
330
+ // Where the item at hand starts among the paragraph's segments.
331
+ let itemStart = 0
332
+ // Whether the paragraph holds an object of width 0.
333
+ let hasEmptyObject = false
334
+
335
+ for (let from = 0; from < count;) {
336
+ // The item this segment starts in, and the segments that start in it.
337
+ const offset = offsets[analysis.starts[from] ]
338
+ let index = itemSegments.length === 0 ? 0 : itemSegments.length - 1
339
+ while (index + 1 < starts.length && starts[index + 1] <= offset) index++
340
+ while (itemSegments.length <= index) itemSegments.push(widths.length)
341
+ const itemEnd = index + 1 < starts.length ? starts[index + 1] : source.length
342
+ let to = from + 1
343
+ while (to < count && offsets[analysis.starts[to] ] < itemEnd) to++
344
+ const item = items[index]
345
+ const previousItemStart = itemStart
346
+ itemStart = widths.length
347
+
348
+ if (item.text === undefined || atomic[index]) {
349
+ // One object: a box's width, or the width of the item's text on a line of its own, laid out
350
+ // in normal white space, as a chip's box with `white-space: nowrap` collapses its own, plus
351
+ // its extraWidth: all three browsers lay out a pre-wrap paragraph of 15px Helvetica Neue with
352
+ // the 12px chip ` @bob ` 6.6px narrower than the chip's text with its spaces. It is prepared
353
+ // without emergency breaks, as it is only laid out whole.
354
+ let width = 0
355
+ let text = ''
356
+ let textStart = 0
357
+ let textEnd = 0
358
+ if (item.text === undefined) {
359
+ width = item.width
360
+ } else {
361
+ const ownAnalysis = analyzeText(item.text, profile, 'normal', wordBreak, language)
362
+ const own = measureAnalysis(ownAnalysis, 0, ownAnalysis.flags.length, item.font, false, readLetterSpacing(item.letterSpacing, profile), profile, language, false, null, paragraphOnly ? '-' : null)
363
+ const start = { segmentIndex: 0, graphemeIndex: 0 }
364
+ if (normalizePreparedLineStart(own, start)) width = stepPreparedLineGeometryFromStart(own, start, Number.POSITIVE_INFINITY)
365
+ width += item.extraWidth ?? 0
366
+ // The chip's text as a line of it paints it (buildLineTextFromRange).
367
+ for (let i = 0; i < ownAnalysis.texts.length; i++) {
368
+ const kind = own.segmentFlags[i] & KIND_BITS
369
+ const ownText = ownAnalysis.texts[i]
370
+ if (kind !== SOFT_HYPHEN && kind !== HARD_BREAK && !((kind === ZERO_WIDTH_GLUE || kind === ZERO_WIDTH_BREAK) && ownText.charCodeAt(0) === 0x00AD)) text += ownText
161
371
  }
162
- const availableWidth = Math.max(1, remainingWidth - reservedWidth);
163
- const lineEnd = {
164
- segmentIndex: cursor.segmentIndex,
165
- graphemeIndex: cursor.graphemeIndex
166
- };
167
- const lineWidthForItem = stepPreparedLineGeometry(item.prepared, lineEnd, availableWidth);
168
- if (lineWidthForItem === null) {
169
- itemIndex++;
170
- cursor.segmentIndex = 0;
171
- cursor.graphemeIndex = 0;
172
- continue;
372
+ while (isCollapsibleSpaceCode(item.text.charCodeAt(textStart))) textStart++
373
+ textEnd = item.text.length
374
+ while (textEnd > textStart && isCollapsibleSpaceCode(item.text.charCodeAt(textEnd - 1))) textEnd--
375
+ }
376
+ // Gecko places an object of width 0 otherwise than the simple walker does (walkPreparedComplexLines).
377
+ if (width === 0) {
378
+ simple = false
379
+ hasEmptyObject = true
380
+ }
381
+ flags[widths.length] = OBJECT
382
+ widths.push(width)
383
+ segments.push(text)
384
+ breakableFitAdvances.push(null)
385
+ sourceStarts.push(textStart)
386
+ sourceEnds.push(textEnd)
387
+ from = to
388
+ continue
389
+ }
390
+
391
+ const letterSpacing = readLetterSpacing(item.letterSpacing, profile)
392
+ if (to === from + 1 && (analysis.flags[from] & KIND_BITS) === SPACE && analysis.texts[from] === ' ') {
393
+ // An item of only collapsible white space, as between two styled words: its one space, as
394
+ // measureAnalysis() measures one.
395
+ if (letterSpacing !== 0) simple = false
396
+ flags[widths.length] = SPACE | (analysis.flags[from] & (UNBROKEN | RETURNABLE)) | (letterSpacing !== 0 ? SPACED : 0)
397
+ widths.push(getSpaceWidth(getFontMeasurement(item.font, language, letterSpacing !== 0)) + (spacingsDiffer ? letterSpacing : 0))
398
+ segments.push(' ')
399
+ breakableFitAdvances.push(null)
400
+ sourceStarts.push(offset - starts[index] )
401
+ sourceEnds.push(offset + 1 - starts[index] )
402
+ from = to
403
+ continue
404
+ }
405
+ // Every line the item reaches pays its extraWidth, as its fragment on that line paints it
406
+ // (box-decoration-break: clone). The first of its segments whose width a line counts, after
407
+ // the collapsed space before its text, holds it for a line that comes into the item or starts
408
+ // with it, and a line that starts later in the item pays it there (ParagraphSegmentData). An
409
+ // item that opens with preserved white space, a hard break or a zero-width space, which a line
410
+ // can end with after content that fills it, gets a start edge of its own before them: an empty
411
+ // segment as wide as the extraWidth, which fits by the edges the engine fits where a line takes
412
+ // the item's opening and no more of it (getOpeningFit), and no break comes before it, as none
413
+ // comes before white space, a hard break or a zero-width space in the text (UAX #14 LB6, LB7),
414
+ // but after an object in Blink and Gecko, and before a zero-width space after an object or
415
+ // white space, where the browsers break.
416
+ const extraWidth = item.extraWidth ?? 0
417
+ let first = to
418
+ if (extraWidth !== 0) {
419
+ first = from
420
+ while (first < to && ((analysis.flags[first] & KIND_BITS) === SOFT_HYPHEN || (first === from && (analysis.flags[from] & KIND_BITS) === SPACE))) first++
421
+ const firstKind = first < to ? analysis.flags[first] & KIND_BITS : TEXT
422
+ if (firstKind === PRESERVED_SPACE || firstKind === TAB || firstKind === HARD_BREAK || (firstKind === ZERO_WIDTH_BREAK && first === from)) {
423
+ const at = widths.length
424
+ const afterObject = at > 0 && (flags[at - 1] & KIND_BITS) === OBJECT
425
+ // Whether the item follows preserved spaces that follow text in their own text item. Blink
426
+ // ends a text item at each character it makes a control item (IsControlItemCharacter,
427
+ // inline_items_builder.cc:177-184; AppendPreserveWhitespace, :1082-1126): a run of preserved
428
+ // tabs (:1098-1110), a line feed, whose forced break the spaces after it follow as a text item
429
+ // of their own (:1084-1097, 1023-1033), and a lone CR or FF (:1119-1125), which the analysis
430
+ // takes as a line feed. So spaces after a tab or a hard break, which is a line feed in the
431
+ // one profile that reads this, follow no text. A ZWNJ, the other such character, starts a
432
+ // text item and is text in it (:1112-1118), as it is here. An item of only spaces after its
433
+ // own start edge follows none either. Blink also ends a text item where a bidi run ends
434
+ // inside it (InlineItem::SetBidiLevel, inline_item.cc:207-236), which no profile resolves:
435
+ // the premise is that the spaces are at the level of the text before them, and its gap is
436
+ // spaces after right-to-left text (ENGINE_FOLLOWUPS.md, Rich-inline item edges).
437
+ const afterTextSpaces = at - 1 > previousItemStart && (flags[at - 1] & KIND_BITS) === PRESERVED_SPACE && (1 << (flags[at - 2] & KIND_BITS) & (1 << TAB | 1 << HARD_BREAK)) === 0 &&
438
+ (openingEdges === null || openingEdges[at - 2] === 0)
439
+ const fit = getOpeningFit(analysis.flags, from, to, extraWidth, afterTextSpaces, profile)
440
+ // In WebKit a break comes before white space that starts an item (whiteSpaceItemBreaks), and
441
+ // none before a hard break, after an object too (nextWrapOpportunity,
442
+ // InlineFormattingUtils.cpp:469-475).
443
+ const breaksBefore = at === 0 || (firstKind === ZERO_WIDTH_BREAK ? afterObject || (1 << (flags[at - 1] & KIND_BITS) & (1 << SPACE | PRESERVED_WHITE_SPACE)) !== 0
444
+ : whiteSpaceItemBreaks ? firstKind !== HARD_BREAK : afterObject)
445
+ // A line that takes the opening paints the whole edge, whatever of it the line fitted
446
+ // (ParagraphSegmentData, openingEdges). An opening no edge of which is fitted takes no room
447
+ // among the white space around it, which hangs past it. Else it is an object, whose line-end
448
+ // trim is what the line doesn't fit: the white space after it stays on its line, as the line
449
+ // keeps the opening it took.
450
+ flags[at] = fit === 0 ? PRESERVED_SPACE : breaksBefore ? OBJECT : OBJECT | UNBROKEN
451
+ widths.push(extraWidth)
452
+ ;(openingEdges ??= zeroList(flags.length))[at] = extraWidth
453
+ if (!breaksBefore && !analysis.hasUnbroken) marksReturnable = true
454
+ // Where the engine ends a line inside the preserved spaces before a padded hard break that
455
+ // doesn't fit (hardBreakItemRetreat), those spaces get an advance each, as text a line can
456
+ // end inside has.
457
+ if (firstKind === HARD_BREAK && !breaksBefore && profile.hardBreakItemRetreat !== 'item' && (flags[at - 1] & KIND_BITS) === PRESERVED_SPACE && segments[at - 1] !== '') {
458
+ const spaces = segments[at - 1] .length
459
+ const advances = []
460
+ for (let g = 0; g < spaces; g++) advances.push(widths[at - 1] / spaces)
461
+ breakableFitAdvances[at - 1] = advances
173
462
  }
174
- if (cursor.segmentIndex === lineEnd.segmentIndex && cursor.graphemeIndex === lineEnd.graphemeIndex) {
175
- itemIndex++;
176
- cursor.segmentIndex = 0;
177
- cursor.graphemeIndex = 0;
178
- continue;
463
+ // The simple walkers take every boundary for a break and a line-end trim out of the line's width.
464
+ simple = false
465
+ segments.push('')
466
+ breakableFitAdvances.push(null)
467
+ // The edge comes before the item's segments, so before the space or soft hyphens that lead them.
468
+ sourceStarts.push(offset - starts[index] )
469
+ sourceEnds.push(offset - starts[index] )
470
+ if (fit !== 0 && fit !== extraWidth) (lineEndTrims ??= zeroList(flags.length))[at] = extraWidth - fit
471
+ first = from - 1
472
+ }
473
+ }
474
+ // An item that is one segment holding all of its text, as a styled word, is measured by the
475
+ // item's own string, not the equal slice of the paragraph's text: a font's widths are kept by
476
+ // text, and a string the caller hands in again keeps its hash, where the slice is hashed anew
477
+ // at every preparation.
478
+ if (to === from + 1 && analysis.texts[from] === item.text) analysis.texts[from] = item.text
479
+ // The item's segments, measured in its font onto the end of the paragraph's lists; `sub` holds
480
+ // what else measurement gives them, from the item's first segment.
481
+ const itemAt = widths.length
482
+ const sub = measureAnalysis(analysis, from, to, item.font, false, letterSpacing, profile, language, !paragraphOnly, lists, paragraphOnly ? '-' : null)
483
+ simple &&= sub.simpleLineCountFastPath
484
+ if (readsItemFonts) {
485
+ // The gap before the hyphen is the letter spacing after the grapheme before it.
486
+ hyphenWidths[index] = spacingsDiffer ? sub.discretionaryHyphenWidth - letterSpacing : sub.discretionaryHyphenWidth
487
+ tabStopAdvances[index] = sub.tabStopAdvance
488
+ minimumTabAdvances[index] = sub.minimumTabAdvance
489
+ if (firstTextItem < 0) firstTextItem = index
490
+ else if (hyphenWidths[index] !== hyphenWidths[firstTextItem] || tabStopAdvances[index] !== tabStopAdvances[firstTextItem] || minimumTabAdvances[index] !== minimumTabAdvances[firstTextItem]) fontsDiffer = true
491
+ }
492
+ for (let i = from; i < to; i++) {
493
+ const s = i - from
494
+ const at = itemAt + s
495
+ const spaced = spacingsDiffer && (flags[at] & SPACED) !== 0
496
+ const holdsExtra = i === first
497
+ if (spaced || holdsExtra) {
498
+ let advances = breakableFitAdvances[at] ?? null
499
+ if (advances !== null) {
500
+ // The cached advances are shared by every occurrence of this text.
501
+ advances = breakableFitAdvances[at] = advances.slice()
502
+ if (spaced) for (let g = 0; g < advances.length; g++) advances[g] = advances[g] + letterSpacing
503
+ if (holdsExtra) advances[0] = advances[0] + extraWidth
179
504
  }
180
- const itemOccupiedWidth = lineWidthForItem + item.extraWidth;
181
- const lineWidthContribution = gapBefore + itemOccupiedWidth;
182
- if (hasContent && atItemStart && lineWidthContribution > remainingWidth) break lineLoop;
183
- if (hasContent && atItemStart && item.breakBefore && endsInsideFirstSegment(lineEnd.segmentIndex, lineEnd.graphemeIndex)) {
184
- break lineLoop;
505
+ if (spaced) widths[at] = widths[at] + letterSpacing
506
+ if (holdsExtra) widths[at] = widths[at] + extraWidth
507
+ }
508
+ // No break comes before white space (UAX #14 LB7), so none inside a run of preserved spaces
509
+ // and tabs that goes on from the item before, but in WebKit (whiteSpaceItemBreaks).
510
+ if (s === 0 && at > 0 && !whiteSpaceItemBreaks && (1 << (flags[at] & KIND_BITS) & PRESERVED_WHITE_SPACE) !== 0 && (1 << (flags[at - 1] & KIND_BITS) & PRESERVED_WHITE_SPACE) !== 0) {
511
+ flags[at] = flags[at] | UNBROKEN
512
+ if (!analysis.hasUnbroken) marksReturnable = true
513
+ }
514
+ const normalizedStart = analysis.starts[i]
515
+ const normalizedEnd = i + 1 < count ? analysis.starts[i + 1] : analysis.normalized.length
516
+ const sourceEnd = offsets[normalizedEnd - 1] + 1
517
+ // Where the segment's text ends in the normalized text.
518
+ let textEnd = normalizedEnd
519
+ sourceStarts.push(offsets[normalizedStart] - starts[index] )
520
+ if (sourceEnd > itemEnd) {
521
+ // The Gecko analysis keeps the soft hyphens and bidi controls Firefox's text run drops with
522
+ // the segment before them, across items: the segment's text here is its own item's part, and
523
+ // the rest, which paints nothing, is in no fragment.
524
+ while (offsets[textEnd - 1] >= itemEnd) textEnd--
525
+ segments.push(analysis.normalized.slice(normalizedStart, textEnd))
526
+ sourceEnds.push(itemEnd - starts[index] )
527
+ } else {
528
+ segments.push(analysis.texts[i] )
529
+ // CRLF is one line feed, which ends with the item where the next item holds the line feed.
530
+ sourceEnds.push((preserve && sourceEnd < itemEnd && source.charCodeAt(sourceEnd - 1) === 0x0D && source.charCodeAt(sourceEnd) === 0x0A ? sourceEnd + 1 : sourceEnd) - starts[index] )
531
+ }
532
+ const units = getSourceUnits(offsets, normalizedStart, textEnd, starts[index] )
533
+ if (units !== null) (sourceUnits ??= new Array (flags.length).fill(null))[at] = units
534
+ if (sub.entryGeometry !== null && sub.entryGeometry[s] !== null) (entryGeometry ??= new Array (flags.length).fill(null))[at] = sub.entryGeometry[s]
535
+ if (sub.lineStartProhibitions !== null && sub.lineStartProhibitions[s] !== null) (lineStartProhibitions ??= new Array (flags.length).fill(null))[at] = sub.lineStartProhibitions[s]
536
+ if (sub.breakableLineStartExtras !== null && sub.breakableLineStartExtras[s] !== null) (breakableLineStartExtras ??= new Array (flags.length).fill(null))[at] = sub.breakableLineStartExtras[s]
537
+ const startExtra = (sub.lineStartExtras === null ? 0 : sub.lineStartExtras[s] ) + (i > first ? extraWidth : 0)
538
+ if (startExtra !== 0) (lineStartExtras ??= zeroList(flags.length))[at] = startExtra
539
+ if (sub.lineEndTrims !== null) {
540
+ const endTrim = sub.lineEndTrims[s]
541
+ if (endTrim !== 0) (lineEndTrims ??= zeroList(flags.length))[at] = endTrim
542
+ // A closing mark that ends its item, halted at a line's end, stays halted where the line
543
+ // goes on. Blink breaks an item's text that doesn't fit its line, and where the text fits
544
+ // with its last mark halted, which it tries where a break comes right after the mark, that
545
+ // is the item's result, and the line takes what follows after it
546
+ // (ShapingLineBreaker::ShapeLine, shaping_line_breaker.cc:342-363; LineBreaker::BreakText,
547
+ // line_breaker.cc:1736-1758): in 16px Hiragino Sans, Chrome 154 lays out `文字」` and a span
548
+ // `i` as one 43.81px line at 44-47px. A run of U+3000 that ends the item has a line-end
549
+ // trim too, its hang, which is no halt. An item with extraWidth keeps the halt at a line's
550
+ // end only. Blink fits its text before its end edge: where the text fits there the mark
551
+ // stays whole though the edge overflows, so Chrome leaves the mark of a span `文字」` with
552
+ // 4px of padding on each side whole at 52-55px and wraps the `i` after it, and where it
553
+ // doesn't the mark is halted and the line goes on, as at 51.81-51.94px, where the `i`
554
+ // fits after it. The extraWidth doesn't split the end edge off, so the line can't tell
555
+ // the two and takes the first for both. Its gap is what follows such a mark and is
556
+ // narrower than the halt less that edge, which wraps where Chrome keeps it
557
+ // (ENGINE_FOLLOWUPS.md, Rich-inline item edges). The walkers that take every boundary for
558
+ // a break end the line after a trimmed segment, so the paragraph isn't theirs.
559
+ if (endTrim !== 0 && i === to - 1 && to < count && extraWidth === 0 && !analysis.texts[i] .endsWith('\u3000')) {
560
+ ;(itemEndHalts ??= zeroList(flags.length))[at] = endTrim
561
+ simple = false
185
562
  }
186
- collectFragment?.(itemIndex, gapBefore, itemOccupiedWidth, cloneCursor(cursor), {
187
- segmentIndex: lineEnd.segmentIndex,
188
- graphemeIndex: lineEnd.graphemeIndex
189
- });
190
- hasContent = true;
191
- lineWidth += lineWidthContribution;
192
- remainingWidth = safeWidth - lineWidth;
193
- if (lineEnd.segmentIndex === item.prepared.segments.length && lineEnd.graphemeIndex === 0) {
194
- itemIndex++;
195
- cursor.segmentIndex = 0;
196
- cursor.graphemeIndex = 0;
197
- continue;
563
+ }
564
+ if (sub.overflowLineEndTrims !== null) {
565
+ const retryTrim = sub.overflowLineEndTrims[s]
566
+ if (retryTrim !== 0) (overflowLineEndTrims ??= zeroList(flags.length))[at] = retryTrim
567
+ // Where no break comes after the mark, as before a period or a no-break space that starts
568
+ // the next item, Blink halts it only on a line it lays out again with a break after every
569
+ // grapheme (HandleOverflow, line_breaker.cc:4259-4264), and there too the halted text is the
570
+ // item's result and the line goes on: Chrome 154 lays out `文字」`, a span `.` and `字` as
571
+ // `文` / `字」.` / `字` at 28.5-31.75px, where their text in one node ends the second line
572
+ // after the mark.
573
+ if (retryTrim !== 0 && i === to - 1 && to < count && extraWidth === 0) {
574
+ ;(itemEndHalts ??= zeroList(flags.length))[at] = retryTrim
575
+ simple = false
198
576
  }
199
- cursor.segmentIndex = lineEnd.segmentIndex;
200
- cursor.graphemeIndex = lineEnd.graphemeIndex;
201
- break;
577
+ }
578
+ if (sub.discretionaryHyphenContexts !== null) (discretionaryHyphenContexts ??= zeroList(flags.length))[at] = sub.discretionaryHyphenContexts[s]
579
+ if (i >= first && first < to) (insideExtras ??= zeroList(flags.length))[at] = extraWidth
580
+ if (i > first && first < to) (fillExtras ??= zeroList(flags.length))[at] = extraWidth
202
581
  }
203
- if (!hasContent) return null;
204
- cursor.itemIndex = itemIndex;
205
- return lineWidth;
582
+ from = to
583
+ }
584
+ const segmentCount = widths.length
585
+ while (itemSegments.length <= items.length) itemSegments.push(segmentCount)
586
+ const segmentFlags = segmentCount === flags.length ? flags : flags.subarray(0, segmentCount)
587
+ // A paragraph whose lines return from an unfit hyphen marks every break the scan gives before
588
+ // text, so its walk records each as the line's latest break (pendingBreakSegmentIndex in
589
+ // walkPreparedComplexLines). The return itself reads no mark, as a text's doesn't; what the marks
590
+ // still decide is where the Gecko profile ends a line that an object of width 0 sticks out of
591
+ // (ENGINE_FOLLOWUPS.md, Rich-inline item edges).
592
+ if (marksReturnable || (discretionaryHyphenContexts !== null && !analysis.hasUnbroken)) for (let i = 0; i < segmentCount; i++) if ((segmentFlags[i] & UNBROKEN) === 0) segmentFlags[i] = segmentFlags[i] | RETURNABLE
593
+
594
+ const segmentItems = new Int32Array(segmentCount)
595
+ const gapSegments = new Uint8Array(segmentCount)
596
+ for (let index = 0; index < items.length; index++) {
597
+ const firstSegment = itemSegments[index]
598
+ const end = itemSegments[index + 1]
599
+ for (let i = firstSegment; i < end; i++) {
600
+ segmentItems[i] = index
601
+ if ((i === firstSegment || i === end - 1) && (segmentFlags[i] & KIND_BITS) === SPACE && segments[i] .length === 1) gapSegments[i] = 1
602
+ }
603
+ }
604
+
605
+ let segmentHyphenWidths = null
606
+ let segmentTabStopAdvances = null
607
+ let segmentMinimumTabAdvances = null
608
+ if (fontsDiffer) {
609
+ segmentHyphenWidths = []
610
+ segmentTabStopAdvances = []
611
+ segmentMinimumTabAdvances = []
612
+ for (let i = 0; i < segmentCount; i++) {
613
+ const index = segmentItems[i]
614
+ segmentHyphenWidths.push(hyphenWidths[index] )
615
+ segmentTabStopAdvances.push(tabStopAdvances[index] )
616
+ segmentMinimumTabAdvances.push(minimumTabAdvances[index] )
617
+ }
618
+ }
619
+ const segmentData = {
620
+ hyphenWidths: segmentHyphenWidths, tabStopAdvances: segmentTabStopAdvances, minimumTabAdvances: segmentMinimumTabAdvances,
621
+ itemEndHalts,
622
+ hyphenRooms: discretionaryHyphenContexts !== null && profile.unfitHyphenRetreat === 'reduced-width' ? getHyphenRooms(hyphenWidths, itemSegments, segmentItems) : null,
623
+ insideExtras, fillExtras, openingEdges,
624
+ emptyObjectSpaces: null, emptyObjectReturns: null,
625
+ }
626
+ // The properties measureAnalysis() gives a text's handle, in its order, then the paragraph's own.
627
+ const data = {
628
+ widths,
629
+ segmentFlags,
630
+ simpleLineWalkFastPath: simple,
631
+ simpleLineCountFastPath: false,
632
+ breakableFitAdvances,
633
+ entryGeometry,
634
+ letterSpacing: spacingsDiffer || sharedSpacing === null ? 0 : sharedSpacing,
635
+ discretionaryHyphenWidth: firstTextItem < 0 ? 0 : hyphenWidths[firstTextItem] ,
636
+ discretionaryHyphenContexts,
637
+ lineStartProhibitions,
638
+ breakableLineStartExtras,
639
+ lineStartExtras,
640
+ lineEndTrims,
641
+ overflowLineEndTrims,
642
+ tabStopAdvance: firstTextItem < 0 ? 0 : tabStopAdvances[firstTextItem] ,
643
+ minimumTabAdvance: firstTextItem < 0 ? 0 : minimumTabAdvances[firstTextItem] ,
644
+ items: segmentData,
645
+ }
646
+ data.segments = segments
647
+ if (hasEmptyObject && profile.emptyAtomicAlwaysFits) setEmptyObjectFacts(segmentData, data, items, itemSegments, segmentItems, source, starts)
648
+
649
+ let onlyItem = -1
650
+ for (let index = 0; index < items.length && !paddedOrObject; index++) {
651
+ if (itemSegments[index] === itemSegments[index + 1]) continue
652
+ if (onlyItem >= 0) {
653
+ onlyItem = -1
654
+ break
655
+ }
656
+ onlyItem = index
657
+ }
658
+ const flow = {
659
+ data, onlyItem, wholeWidth: null, wholeStart: 0, wholeEnd: 0, itemSegments, segmentItems, gapSegments,
660
+ plainWidths: data.letterSpacing === 0 && itemEndHalts === null && !(preserve && source.includes('\t')),
661
+ sourceStarts, sourceEnds, sourceUnits, text: '',
662
+ }
663
+ return paragraphOnly ? paragraphSegments(flow) : mayNarrow && onlyItem < 0 && narrowsLine(data, itemSegments) ? flow : findWholeLine(flow)
206
664
  }
207
- export function layoutNextRichInlineLineRange(prepared, maxWidth, start = RICH_INLINE_START_CURSOR) {
208
- const flow = getInternalPreparedRichInline(prepared);
209
- const end = {
210
- itemIndex: start.itemIndex,
211
- segmentIndex: start.segmentIndex,
212
- graphemeIndex: start.graphemeIndex
213
- };
214
- const fragments = [];
215
- const width = stepRichInlineLine(flow, maxWidth, end, (itemIndex, gapBefore, occupiedWidth, fragmentStart, fragmentEnd)=>{
216
- fragments.push({
217
- itemIndex,
218
- gapBefore,
219
- occupiedWidth,
220
- start: fragmentStart,
221
- end: fragmentEnd
222
- });
223
- });
224
- if (width === null) return null;
225
- return {
226
- fragments,
227
- width,
228
- end
229
- };
665
+
666
+ // Per segment, the room a line that ends at the break before it leaves for a hyphen, where the line
667
+ // returns there from a soft hyphen whose hyphen doesn't fit, with a last entry for the break at the
668
+ // paragraph's end. Blink breaks a text item again against the width less the item's own hyphen
669
+ // where the hyphen of the soft hyphen it ends at overflows (LineBreaker::BreakText,
670
+ // line_breaker.cc:1705-1719), so a break inside the item that holds the soft hyphen leaves room
671
+ // for that item's hyphen. Where none in the item does, the line goes back over the items before it
672
+ // to the latest break that fits, with no room (HandleOverflow, :4105-4112), so the break before an
673
+ // item's first segment leaves none. The premise is that a break inside an item is returned to from
674
+ // a soft hyphen of that item: for one inside an earlier item Blink asks only that the line fit
675
+ // (:4138-4158), and this leaves room for that item's hyphen (ENGINE_FOLLOWUPS.md, Rich-inline item
676
+ // edges).
677
+ function getHyphenRooms(hyphenWidths , itemSegments , segmentItems ) {
678
+ const rooms = []
679
+ for (let i = 0; i < segmentItems.length; i++) {
680
+ const index = segmentItems[i]
681
+ rooms.push(i === itemSegments[index] ? 0 : hyphenWidths[index] )
682
+ }
683
+ rooms.push(0)
684
+ return rooms
230
685
  }
231
- function materializeFragmentText(item, fragment) {
232
- return buildLineTextFromRange(item.prepared, getLineTextCache(item.prepared), fragment.start.segmentIndex, fragment.start.graphemeIndex, fragment.end.segmentIndex, fragment.end.graphemeIndex);
686
+
687
+ // A soft hyphen, as a segment of any kind: Gecko drops soft hyphens from a text frame's text
688
+ // (IsDiscardable, nsTextFrameUtils.cpp:32-49).
689
+ function isSoftHyphenSegment(data , i ) {
690
+ const kind = data.segmentFlags[i] & KIND_BITS
691
+ return kind === SOFT_HYPHEN || ((kind === ZERO_WIDTH_BREAK || kind === ZERO_WIDTH_GLUE) && data.segments[i] .charCodeAt(0) === 0x00AD)
233
692
  }
234
- export function materializeRichInlineLineRange(prepared, line) {
235
- const flow = getInternalPreparedRichInline(prepared);
236
- const fragments = [];
237
- for(let i = 0; i < line.fragments.length; i++){
238
- const fragment = line.fragments[i];
239
- const item = flow.items[fragment.itemIndex];
240
- if (item === undefined) throw new Error('Missing rich-text inline item for fragment');
241
- fragments.push({
242
- itemIndex: fragment.itemIndex,
243
- text: materializeFragmentText(item, fragment),
244
- gapBefore: fragment.gapBefore,
245
- occupiedWidth: fragment.occupiedWidth,
246
- start: fragment.start,
247
- end: fragment.end
248
- });
693
+
694
+ // What a line reads of the text around each object of width 0 where the engine places one though
695
+ // it sticks out of the line, as Gecko does an empty frame (ParagraphSegmentData;
696
+ // walkPreparedComplexLines has when it sticks out). Each item is a text frame.
697
+ // - The white space that ends the text before the object, read past the soft hyphens Gecko
698
+ // drops. Gecko's line breaker leaves a break at the end of a text run that ends in a space or a
699
+ // tab, whatever its advance (nsLineBreaker::Reset, nsLineBreaker.cpp:710-719; IsSegmentSpace,
700
+ // nsLineBreaker.h:260-264), and the run's last frame breaks the line there where it ends past the
701
+ // line's end without its own trailing spaces (nsTextFrame.cpp:11443-11456), which a tab isn't
702
+ // among (gfxTextRun.cpp:1152-1160). So a collapsed space counts as that frame's own where it is
703
+ // in the item right before the object; one in an item before that, as before an item of soft
704
+ // hyphens alone, an empty frame, is inside the line's width. Preserved spaces that end their
705
+ // item hang, which the line's width leaves out already. Preserved spaces before the soft hyphens
706
+ // that end their item hang in Firefox and not here, so they aren't read as white space
707
+ // (ENGINE_FOLLOWUPS.md).
708
+ // - Whether a line with a break before the object goes back to it. The break comes after white
709
+ // space, after an atomic item (nsLineLayout.cpp:1057-1069) and after a soft hyphen that ends the
710
+ // frame before the object, whatever the hyphen's width (HasSoftHyphenBefore,
711
+ // nsTextFrame.cpp:11432-11439); other text leaves no break at its end, so the line's first break
712
+ // is the one after the object, which stays. The break after a frame that sticks out doesn't count
713
+ // as one that fits (nsLineLayout.cpp:1260), and the line remembers its last break that fits
714
+ // (NotifyOptionalBreakPosition, :1506-1513), so a text frame or a span with a width that comes
715
+ // next sticks out too and sends the line back to before the object (:1323-1334;
716
+ // nsBlockFrame.cpp:5361-5379). The object stays where the line ends without going back: at the
717
+ // paragraph's end, before an atomic item with a width, which moves down whole (:1337-1341), and
718
+ // before unpadded text whose first piece has no width. A text frame that doesn't fit ends at its
719
+ // first break, whether or not that fits (gfxTextRun::BreakAndMeasureText, gfxTextRun.cpp:1091-1099,
720
+ // 1177-1185), so its first piece is all of it on this line: a ZWSP, a hard break, preserved
721
+ // spaces, which hang, or the collapsed space that starts the text's own node, trimmed where the
722
+ // frame breaks after it (nsTextFrame.cpp:11202-11213). Padding is its span's width whatever the
723
+ // text starts with. White space in a node of its own has no break inside, so its frame is whole
724
+ // and keeps its width, as is a node of white space and soft hyphens. An item of soft hyphens
725
+ // alone takes no room and is passed over, as is each object of width 0, which stays for the same
726
+ // reason as the first: one answer serves a run of them, found from the paragraph's end back.
727
+ // White space that ends the paragraph in a node of its own gets no frame
728
+ // (nsCSSFrameConstructor.cpp:5278-5286), and after other text of its item, as after soft
729
+ // hyphens, it is that frame's width.
730
+ function setEmptyObjectFacts(
731
+ segmentData ,
732
+ data ,
733
+ items ,
734
+ itemSegments ,
735
+ segmentItems ,
736
+ source ,
737
+ starts ,
738
+ ) {
739
+ const { segmentFlags, widths, letterSpacing } = data
740
+ const count = segmentFlags.length
741
+ const spaces = zeros(count)
742
+ const returns = new Uint8Array(count)
743
+ // The item whose white space ends the paragraph's text after other text of its own, or -1.
744
+ let runStart = source.length
745
+ while (runStart > 0 && isCollapsibleSpaceCode(source.charCodeAt(runStart - 1))) runStart--
746
+ let endSpaceItem = -1
747
+ if (runStart < source.length) {
748
+ let index = starts.length - 1
749
+ while (index > 0 && starts[index] > runStart) index--
750
+ if (itemSegments[index] < itemSegments[index + 1] ) endSpaceItem = index
751
+ }
752
+ // Whether what follows an object keeps it on its line, for an object right before the item at
753
+ // hand, from the paragraph's end back.
754
+ let keeps = true
755
+ for (let index = items.length - 1; index >= 0; index--) {
756
+ const first = itemSegments[index]
757
+ const end = itemSegments[index + 1]
758
+ if (first === end) continue
759
+ const kind = segmentFlags[first] & KIND_BITS
760
+ const item = items[index]
761
+ if (item.text === undefined || item.break === 'never') {
762
+ if (widths[first] !== 0) {
763
+ keeps = true
764
+ continue
765
+ }
766
+ let before = first - 1
767
+ while (before >= 0 && isSoftHyphenSegment(data, before)) before--
768
+ const beforeKind = before < 0 ? TEXT : segmentFlags[before] & KIND_BITS
769
+ let space = -1
770
+ if (beforeKind === SPACE) space = segmentItems[before] === segmentItems[first - 1] ? widths[before] + ((segmentFlags[before] & SPACED) !== 0 ? letterSpacing : 0) : 0
771
+ else if (beforeKind === TAB || (beforeKind === PRESERVED_SPACE && segmentItems[before] !== segmentItems[before + 1])) space = 0
772
+ spaces[first] = space
773
+ const lastKind = first === 0 ? TEXT : segmentFlags[first - 1] & KIND_BITS
774
+ if (!keeps && (space >= 0 || lastKind === OBJECT || lastKind === SOFT_HYPHEN)) returns[first] = 1
775
+ continue
249
776
  }
250
- return {
251
- fragments,
252
- width: line.width,
253
- end: line.end
254
- };
255
- }
256
- export function walkRichInlineLineRanges(prepared, maxWidth, onLine) {
257
- let lineCount = 0;
258
- const cursor = {
259
- ...RICH_INLINE_START_CURSOR
260
- };
261
- while(true){
262
- const line = layoutNextRichInlineLineRange(prepared, maxWidth, cursor);
263
- if (line === null) return lineCount;
264
- cursor.itemIndex = line.end.itemIndex;
265
- cursor.segmentIndex = line.end.segmentIndex;
266
- cursor.graphemeIndex = line.end.graphemeIndex;
267
- onLine(line);
268
- lineCount++;
777
+ if (endSpaceItem === index) keeps = false
778
+ let softHyphens = true
779
+ let holdsSpace = false
780
+ let establishesLine = false
781
+ for (let i = first; i < end; i++) {
782
+ if (isSoftHyphenSegment(data, i)) continue
783
+ softHyphens = false
784
+ if ((segmentFlags[i] & KIND_BITS) === SPACE) holdsSpace = true
785
+ else establishesLine = true
269
786
  }
787
+ if (softHyphens) continue
788
+ if ((item.extraWidth ?? 0) > 0 || (holdsSpace && !establishesLine)) keeps = false
789
+ else if (kind === SPACE) keeps = true
790
+ else keeps = (kind === ZERO_WIDTH_BREAK && !isSoftHyphenSegment(data, first)) || kind === HARD_BREAK || kind === PRESERVED_SPACE
791
+ }
792
+ segmentData.emptyObjectSpaces = spaces
793
+ segmentData.emptyObjectReturns = returns
794
+ }
795
+
796
+ // Finds the paragraph's line where nothing wraps it, which is taken without a walk at any width it
797
+ // fits: most paragraphs of a chat are one line. That is a premise. Blink, Gecko and WebKit each fit
798
+ // a line at prefixes of it, after each item, at each break opportunity and frame end, and after each
799
+ // run between two wrap opportunities (line_breaker.cc:1009-1147; gfxTextRun.cpp:1085-1109;
800
+ // InlineLineBuilder.cpp:538-604), and none lets text make a line narrower (ClampNegativeToZero,
801
+ // line_breaker.cc:1703; nsTextFrame.cpp:11272-11273; InlineLine.cpp:440), so where no item makes
802
+ // the line narrower its width only grows from item to item and the walkers take the same line. A
803
+ // paragraph with such an item, one of no advance under negative letter spacing or one with a
804
+ // negative extraWidth, has no such line and is walked (narrowsLine). Inside an item the premise's
805
+ // gap is left: under negative letter spacing a line can overflow at a break and come back under
806
+ // the line's width after it, where the text walkers end the line at the break and the paragraph
807
+ // takes it whole, so a paragraph of one item can take fewer lines than layoutWithLines() gives its
808
+ // text. Blink takes a text item whole where its shaped width fits (ShapingLineBreaker::ShapeLine,
809
+ // shaping_line_breaker.cc:281-297), which is that line; ENGINE_FOLLOWUPS.md, Negative letter
810
+ // spacing and hanging spaces, has the counts. The walk hands walkPreparedLinesRaw() a visitor of
811
+ // its own, which costs later walks of one-item CJK paragraphs 4-6% in Firefox 156 and a stream
812
+ // over a page of mostly such paragraphs 8% in Safari 27; the line stepped with no visitor was
813
+ // measured and left out (RESEARCH.md, Decisions Log, 2026-10-09, a loop of its own for the count
814
+ // of lines).
815
+ function findWholeLine(flow ) {
816
+ const lineCount = walkPreparedLinesRaw(flow.data, Number.POSITIVE_INFINITY, (width, startSegmentIndex, _startGraphemeIndex, endSegmentIndex) => {
817
+ flow.wholeWidth = width
818
+ flow.wholeStart = startSegmentIndex
819
+ flow.wholeEnd = endSegmentIndex
820
+ })
821
+ if (lineCount !== 1) flow.wholeWidth = null
822
+ return flow
270
823
  }
271
- export function measureRichInlineStats(prepared, maxWidth) {
272
- const flow = getInternalPreparedRichInline(prepared);
273
- let lineCount = 0;
274
- let maxLineWidth = 0;
275
- const cursor = {
276
- itemIndex: 0,
277
- segmentIndex: 0,
278
- graphemeIndex: 0
279
- };
280
- while(true){
281
- const lineWidth = stepRichInlineLine(flow, maxWidth, cursor);
282
- if (lineWidth === null) {
283
- return {
284
- lineCount,
285
- maxLineWidth
286
- };
824
+
825
+ // Whether an item of the paragraph takes less than no room on a line: its segments' widths, with
826
+ // the letter spacing after each one that takes it, add up to less than 0.
827
+ function narrowsLine(data , itemSegments ) {
828
+ const { widths, segmentFlags, letterSpacing } = data
829
+ for (let index = 0; index + 1 < itemSegments.length; index++) {
830
+ let width = 0
831
+ for (let i = itemSegments[index] ; i < itemSegments[index + 1] ; i++) width += widths[i] + ((segmentFlags[i] & SPACED) !== 0 ? letterSpacing : 0)
832
+ if (width < 0) return true
833
+ }
834
+ return false
835
+ }
836
+
837
+ // Whether the paragraph's whole line fits `maxWidth`, as the walkers fit a line. The line functions
838
+ // hand it their width clamped at 0, as the walkers clamp the one they are handed. The line's width
839
+ // here is signed, and under 0 where letter spacing is more negative than the letters are wide:
840
+ // fitted against a width under 0 as given, such a paragraph would be one line down to its own
841
+ // width and walked under it, where the walkers can end a line that the whole fit takes
842
+ // (findWholeLine; RESEARCH.md, Decisions Log, 2026-10-09, rich inline's width).
843
+ function fitsWhole(flow , maxWidth ) {
844
+ return flow.wholeWidth !== null && flow.wholeWidth <= maxWidth + getEngineProfile().lineFitEpsilon
845
+ }
846
+
847
+ // `count` zeros, where a paragraph's list of numbers starts: a copy of one list of zeros, which
848
+ // grows to the longest asked for, up to 1024; a longer list is pushed. A copy is one allocation,
849
+ // where zeros pushed one at a time regrow the list as it fills: a list of 127 costs 18 ns copied
850
+ // and 141 pushed in V8's shell, 44 and 293 in SpiderMonkey's, 59 and 204 in JavaScriptCore's.
851
+ const ZEROS = []
852
+ function zeroList(count ) {
853
+ if (count > 1024) return zeros(count)
854
+ while (ZEROS.length < count) ZEROS.push(0)
855
+ return ZEROS.slice(0, count)
856
+ }
857
+
858
+ // How much of a padded item's extraWidth a line fits where it takes the item's opening, the
859
+ // white space, hard break or zero-width space that starts it, and no more of it; a line that goes
860
+ // on into the item fits all of it. Blink adds a span's start edge to the line when it opens
861
+ // (HandleOpenTag, line_breaker.cc:3957-3976), and only a test-only flag narrows the line for its
862
+ // cloned end edge (BoxDecorationBreakCloneLineBreaking, :454-461), so it fits half: the extraWidth
863
+ // isn't split by side (ENGINE_FOLLOWUPS.md). The items a line takes after the break it returns to
864
+ // stay on it where they are all trailable, white space with the tags of spans that open and close
865
+ // among it (RewindOverflow, :4332-4424), so Blink fits no edge of a span of only white space. Nor
866
+ // does it after preserved spaces that follow text in one item, which its return breaks before
867
+ // (HandleOverflow, :4163-4185, at the run's start that ShapingLineBreaker::ShapeLine breaks at,
868
+ // shaping_line_breaker.cc:490-495): the line then trails them, taking the open tag and white
869
+ // space or a forced break after them with no fit (HandleTrailingSpaces, :2426-2534). WebKit fits
870
+ // a box that opens in the content it places without its cloned end edge
871
+ // (placedClonedDecorationWidth, InlineLineBuilder.cpp:1501-1523), where a hard break or white
872
+ // space starts it, after the white space before it, which the line counts (the walker's line
873
+ // width holds it), but that content runs on past the inline box ends after a line break or white
874
+ // space (nextWrapOpportunity, InlineFormattingUtils.cpp:470-475, 530-538), so it fits the end
875
+ // edge too of an item of white space that ends there. Gecko fits a frame's whole width, its
876
+ // cloned end edge too (CanPlaceFrame, nsLineLayout.cpp:1217-1270).
877
+ function getOpeningFit(flags , from , to , extraWidth , afterTextSpaces , profile ) {
878
+ const fit = profile.paddedOpeningFit
879
+ const firstKind = flags[from] & KIND_BITS
880
+ const opensWithWhiteSpace = firstKind === PRESERVED_SPACE || (firstKind === TAB && profile.hangTabs)
881
+ const opensWithBreak = firstKind === HARD_BREAK || firstKind === ZERO_WIDTH_BREAK
882
+ let whiteSpaceEnd = from
883
+ while (whiteSpaceEnd < to && ((flags[whiteSpaceEnd] & KIND_BITS) === PRESERVED_SPACE || (flags[whiteSpaceEnd] & KIND_BITS) === TAB)) whiteSpaceEnd++
884
+ if (fit === 'start' && (opensWithWhiteSpace || opensWithBreak)) return afterTextSpaces || whiteSpaceEnd === to ? 0 : extraWidth / 2
885
+ if (fit === 'placed' && (opensWithWhiteSpace || opensWithBreak)) {
886
+ const onlyOpening = whiteSpaceEnd === to || (whiteSpaceEnd === to - 1 && (flags[whiteSpaceEnd] & KIND_BITS) === HARD_BREAK)
887
+ return onlyOpening ? extraWidth : extraWidth / 2
888
+ }
889
+ return extraWidth
890
+ }
891
+
892
+ // The width on a line of graphemes [from, to) of segment i, to its end where `to` is -1: a line
893
+ // starts or ends inside its first and its last segment only. Where they start the line
894
+ // (`startsLine`), the first of them adds what a line that starts with it does
895
+ // (breakableLineStartExtras), as the walker's line does.
896
+ function getPartWidth(data , i , from , to , startsLine ) {
897
+ const { breakableFitAdvances, breakableLineStartExtras, entryGeometry, letterSpacing, items } = data
898
+ const advances = breakableFitAdvances[i]
899
+ if (to < 0) to = advances.length
900
+ const entry = from > 0 && entryGeometry !== null ? entryGeometry[i] : null
901
+ const entryWidth = entry === null ? null : getSegmentEntryWidth(entry, from, to)
902
+ let w = 0
903
+ if (entryWidth !== null) {
904
+ w = entryWidth
905
+ } else {
906
+ for (let g = from; g < to; g++) w += advances[g] + letterSpacing
907
+ const startExtras = startsLine && breakableLineStartExtras !== null ? breakableLineStartExtras[i] : null
908
+ if (startExtras !== null) w += startExtras[from]
909
+ }
910
+ if (items !== undefined && (from > 0 ? items.insideExtras !== null : items.fillExtras !== null)) w += from > 0 ? items.insideExtras [i] : items.fillExtras [i]
911
+ return w
912
+ }
913
+
914
+ // A line of a paragraph whose only item with segments has no extraWidth (onlyItem), as most of a
915
+ // chat's are: one fragment of that item, as wide as the line. Every segment is that item's, so a
916
+ // place in the paragraph is the same place in the item. A function of its own, which the walk and
917
+ // the stream call for such a paragraph: with it Safari 27 walked and streamed the chat demo's
918
+ // paragraphs 8.0% and 8.6% faster than with that line as a branch of createLine() and Chrome 154
919
+ // walked them 4.2% faster, and it cost Chrome's walk of items that are a word or a space each 5.3%
920
+ // and Firefox 156's walk of the demo's styled paragraphs 2.2% (RESEARCH.md, Rich Inline As One
921
+ // Paragraph; Decisions Log, 2026-10-08).
922
+ function createOnlyItemLine(
923
+ flow ,
924
+ width ,
925
+ startSegmentIndex ,
926
+ startGraphemeIndex ,
927
+ endSegmentIndex ,
928
+ endGraphemeIndex ,
929
+ ) {
930
+ const { onlyItem } = flow
931
+ const ended = endSegmentIndex >= flow.data.segmentFlags.length
932
+ return {
933
+ fragments: [{
934
+ itemIndex: onlyItem,
935
+ gapBefore: 0,
936
+ gapItemIndex: -1,
937
+ occupiedWidth: width,
938
+ start: { segmentIndex: startSegmentIndex, graphemeIndex: startGraphemeIndex },
939
+ end: { segmentIndex: endSegmentIndex, graphemeIndex: endGraphemeIndex },
940
+ }],
941
+ width: Math.max(0, width),
942
+ end: { itemIndex: ended ? flow.itemSegments.length - 1 : onlyItem, segmentIndex: ended ? 0 : endSegmentIndex, graphemeIndex: endGraphemeIndex },
943
+ }
944
+ }
945
+
946
+ // A line of the paragraph from the text walkers' line: its segments cut into fragments where the
947
+ // item changes. A collapsed space at an item's start or end is no fragment's text but the gap
948
+ // before the next fragment on the line, as that item's white space made it. Each fragment's width
949
+ // is its segments' on this line, and the fragments add up to the line's width: what the line's
950
+ // width leaves out at its end comes out of the last fragments and the gaps before them, from the
951
+ // line's end back, as white space that hangs there, but never out of the edge of a padded item's
952
+ // opening, which the line paints whole (getOpeningFit), and what it adds, the hyphen of a soft
953
+ // hyphen the line ends at, goes to the last one. One loop over the line's segments: where a
954
+ // fragment or a gap starts, the paragraph's lists give where that fragment ends, so every other
955
+ // segment only adds its width. A line where no segment's width depends on the line (`bare`), as
956
+ // nearly every line of styled prose is, has a loop of its own that adds bare widths: in one loop
957
+ // with the widths that tabs, letter spacing, halts and a line's edges need, Chrome walked the chat
958
+ // demo's styled paragraphs about 19% slower and Safari 8% (two runs against one build). The widths
959
+ // of the two segments a line can start or end inside are found before the loops, and neither holds
960
+ // another loop or a search: a search for the item, or a call or a loop inside one for a width it
961
+ // rarely needs, cost Firefox a third to a half of a walk's time, and a loop inside for each
962
+ // fragment's widths 9% on items of one segment (RESEARCH.md, Rich Inline As One Paragraph).
963
+ function createLine(
964
+ flow ,
965
+ width ,
966
+ startSegmentIndex ,
967
+ startGraphemeIndex ,
968
+ endSegmentIndex ,
969
+ endGraphemeIndex ,
970
+ ) {
971
+ const { data, itemSegments } = flow
972
+ const { widths, segmentFlags, lineStartExtras, letterSpacing } = data
973
+ const items = data.items
974
+ const segmentItems = flow.segmentItems
975
+ const gapSegments = flow.gapSegments
976
+ const itemCount = itemSegments.length - 1
977
+ const endItemIndex = endSegmentIndex >= segmentItems.length ? itemCount : segmentItems[endSegmentIndex]
978
+ const end = { itemIndex: endItemIndex, segmentIndex: endSegmentIndex - itemSegments[endItemIndex] , graphemeIndex: endGraphemeIndex }
979
+ const tabsInAppUnits = getEngineProfile().tabsInAppUnits
980
+ const itemEndHalts = items.itemEndHalts
981
+ const fragments = []
982
+ const lastSegmentIndex = endGraphemeIndex > 0 ? endSegmentIndex : endSegmentIndex - 1
983
+ const firstPartWidth = startSegmentIndex <= lastSegmentIndex && (startGraphemeIndex > 0 || startSegmentIndex === endSegmentIndex)
984
+ ? getPartWidth(data, startSegmentIndex, startGraphemeIndex, startSegmentIndex < endSegmentIndex ? -1 : endGraphemeIndex, true) : 0
985
+ const lastPartWidth = endGraphemeIndex > 0 && endSegmentIndex > startSegmentIndex ? getPartWidth(data, endSegmentIndex, 0, endGraphemeIndex, false) : 0
986
+ // Whether each of the line's segments is as wide on it as its width: in a paragraph of plain
987
+ // widths, a line that starts and ends between segments and adds nothing at its start.
988
+ const bare = flow.plainWidths && startGraphemeIndex === 0 && endGraphemeIndex === 0 && (lineStartExtras === null || lineStartExtras[startSegmentIndex] === 0)
989
+ // The line's width so far, and the gap that waits for a fragment.
990
+ let lineW = 0
991
+ let gap = 0
992
+ let gapItemIndex = -1
993
+ // The last fragment made, its width so far, and the segment after its last.
994
+ let fragment = null
995
+ let occupiedWidth = 0
996
+ let to = startSegmentIndex
997
+ if (bare) {
998
+ for (let i = startSegmentIndex; i <= lastSegmentIndex; i++) {
999
+ if (i === to) {
1000
+ if (fragment !== null) fragment.occupiedWidth = occupiedWidth
1001
+ const itemIndex = segmentItems[i]
1002
+ if (gapSegments[i] !== 0) {
1003
+ gap = widths[i]
1004
+ gapItemIndex = itemIndex
1005
+ to = i + 1
1006
+ continue
287
1007
  }
288
- lineCount++;
289
- if (lineWidth > maxLineWidth) maxLineWidth = lineWidth;
1008
+ const first = itemSegments[itemIndex]
1009
+ to = Math.min(itemSegments[itemIndex + 1] , lastSegmentIndex + 1)
1010
+ if (gapSegments[to - 1] !== 0) to--
1011
+ fragment = { itemIndex, gapBefore: gap, gapItemIndex, occupiedWidth: 0, start: { segmentIndex: i - first, graphemeIndex: 0 }, end: { segmentIndex: to - first, graphemeIndex: 0 } }
1012
+ fragments.push(fragment)
1013
+ lineW += gap
1014
+ gap = 0
1015
+ gapItemIndex = -1
1016
+ occupiedWidth = 0
1017
+ }
1018
+ occupiedWidth += widths[i]
1019
+ lineW += widths[i]
1020
+ }
1021
+ } else {
1022
+ for (let i = startSegmentIndex; i <= lastSegmentIndex; i++) {
1023
+ if (i === to) {
1024
+ if (fragment !== null) fragment.occupiedWidth = occupiedWidth
1025
+ const itemIndex = segmentItems[i]
1026
+ if (gapSegments[i] !== 0) {
1027
+ // A collapsed space is no tab and no halted mark, and no line starts with one or ends inside one.
1028
+ gap = widths[i] + (letterSpacing !== 0 && (segmentFlags[i] & SPACED) !== 0 ? letterSpacing : 0)
1029
+ gapItemIndex = itemIndex
1030
+ to = i + 1
1031
+ continue
1032
+ }
1033
+ // The fragment's segments end at `to`: the item's on the line, less the collapsed space that ends it.
1034
+ const first = itemSegments[itemIndex]
1035
+ to = Math.min(itemSegments[itemIndex + 1] , lastSegmentIndex + 1)
1036
+ if (gapSegments[to - 1] !== 0) to--
1037
+ const endsInside = to > endSegmentIndex
1038
+ fragment = {
1039
+ itemIndex,
1040
+ gapBefore: gap,
1041
+ gapItemIndex,
1042
+ occupiedWidth: 0,
1043
+ start: { segmentIndex: i - first, graphemeIndex: i === startSegmentIndex ? startGraphemeIndex : 0 },
1044
+ end: { segmentIndex: (endsInside ? endSegmentIndex : to) - first, graphemeIndex: endsInside ? endGraphemeIndex : 0 },
1045
+ }
1046
+ fragments.push(fragment)
1047
+ lineW += gap
1048
+ gap = 0
1049
+ gapItemIndex = -1
1050
+ occupiedWidth = 0
1051
+ }
1052
+ // A segment's width on the line, with the letter spacing after each of its graphemes, as a
1053
+ // fragment's width counts it.
1054
+ let w
1055
+ if ((i > startSegmentIndex || startGraphemeIndex === 0) && i < endSegmentIndex) {
1056
+ const startExtra = i === startSegmentIndex && lineStartExtras !== null ? lineStartExtras[i] : 0
1057
+ w = startExtra + ((segmentFlags[i] & KIND_BITS) !== TAB ? widths[i] : getItemTabAdvance(data, items, i, lineW + startExtra, tabsInAppUnits))
1058
+ if (letterSpacing !== 0 && (segmentFlags[i] & SPACED) !== 0) w += letterSpacing
1059
+ } else {
1060
+ w = i === startSegmentIndex ? firstPartWidth : lastPartWidth
1061
+ }
1062
+ // A mark that ends its item was halted where the line went on after it: the line is then
1063
+ // narrower than its segments up to the mark (ParagraphSegmentData, itemEndHalts), since what
1064
+ // follows a halted mark fits in less than the halt. Premise: what follows a mark on its line
1065
+ // takes no less than no room. Its gap is an item after the mark whose letter spacing is more
1066
+ // negative than its letters are wide, where a mark that wasn't halted gives its halt to the
1067
+ // line's last fragment; the line's width and breaks are the walker's either way. The 1e-6px
1068
+ // is room for float error: the walker adds the same advances in another order, so where
1069
+ // nothing after a mark that wasn't halted takes room, the segments up to it can add up to
1070
+ // more than the line by a few units in the last place of the line's width (one is 6e-14px on
1071
+ // a line of 300px, 6e-11px on one of 280,000px), and the mark would read as halted, its halt
1072
+ // going to the line's last fragment.
1073
+ if (itemEndHalts !== null && itemEndHalts[i] !== 0 && lineW + w - width > 1e-6) w -= itemEndHalts[i]
1074
+ occupiedWidth += w
1075
+ lineW += w
1076
+ }
1077
+ }
1078
+ if (fragment !== null) fragment.occupiedWidth = occupiedWidth
1079
+
1080
+ let rest = lineW - width
1081
+ if (rest < 0 && fragments.length > 0) fragments[fragments.length - 1] .occupiedWidth -= rest
1082
+ const openingEdges = items.openingEdges
1083
+ for (let k = fragments.length - 1; k >= 0 && rest > 0; k--) {
1084
+ const last = fragments[k]
1085
+ // The edge of a padded opening stays painted: it is the first segment of its item, so of a fragment.
1086
+ const edge = openingEdges === null || last.start.graphemeIndex > 0 ? 0 : openingEdges[itemSegments[last.itemIndex] + last.start.segmentIndex]
1087
+ if (last.occupiedWidth > edge) {
1088
+ const part = Math.min(rest, last.occupiedWidth - edge)
1089
+ last.occupiedWidth -= part
1090
+ rest -= part
1091
+ }
1092
+ if (rest > 0 && last.gapBefore > 0) {
1093
+ const part = Math.min(rest, last.gapBefore)
1094
+ last.gapBefore -= part
1095
+ rest -= part
1096
+ // A gap taken whole is none, and so is one with under 1e-9px left, which could only be float
1097
+ // error. That needs a width the line leaves out, such as a mark's halt, to be as wide as the
1098
+ // gap to the last bits, which no rule makes it.
1099
+ if (last.gapBefore < 1e-9) {
1100
+ last.gapBefore = 0
1101
+ last.gapItemIndex = -1
1102
+ }
290
1103
  }
1104
+ }
1105
+ return { fragments, width: Math.max(0, width), end }
1106
+ }
1107
+
1108
+ // The paragraph's segment a cursor names, or the segment count for one past the last item.
1109
+ function getSegmentIndex(flow , cursor ) {
1110
+ const { itemSegments } = flow
1111
+ return cursor.itemIndex < itemSegments.length ? itemSegments[cursor.itemIndex] + cursor.segmentIndex : flow.data.segmentFlags.length
1112
+ }
1113
+
1114
+ export function layoutNextRichInlineLineRange(
1115
+ prepared ,
1116
+ maxWidth ,
1117
+ start = RICH_INLINE_START_CURSOR,
1118
+ ) {
1119
+ const flow = getInternalPreparedRichInline(prepared)
1120
+ const safeWidth = Math.max(0, maxWidth)
1121
+ if (start.itemIndex === 0 && start.segmentIndex === 0 && start.graphemeIndex === 0 && fitsWhole(flow, safeWidth)) {
1122
+ return flow.onlyItem >= 0 ? createOnlyItemLine(flow, flow.wholeWidth , flow.wholeStart, 0, flow.wholeEnd, 0) : createLine(flow, flow.wholeWidth , flow.wholeStart, 0, flow.wholeEnd, 0)
1123
+ }
1124
+ const lineEnd = { segmentIndex: getSegmentIndex(flow, start), graphemeIndex: start.graphemeIndex }
1125
+ if (!normalizePreparedLineStart(flow.data, lineEnd)) return null
1126
+ const startSegmentIndex = lineEnd.segmentIndex
1127
+ const startGraphemeIndex = lineEnd.graphemeIndex
1128
+ const width = stepPreparedLineGeometryFromStart(flow.data, lineEnd, safeWidth)
1129
+ if (width === null) return null
1130
+ return flow.onlyItem >= 0 ? createOnlyItemLine(flow, width, startSegmentIndex, startGraphemeIndex, lineEnd.segmentIndex, lineEnd.graphemeIndex)
1131
+ : createLine(flow, width, startSegmentIndex, startGraphemeIndex, lineEnd.segmentIndex, lineEnd.graphemeIndex)
1132
+ }
1133
+
1134
+ // Where units [from, to) of a paragraph's normalized text are in the text of the item that starts
1135
+ // at `itemStart`, from the offset in the paragraph's text each unit comes from, where they aren't
1136
+ // one stretch of it: Gecko removes a line feed between two ideographs, and white space after a bidi
1137
+ // control that follows white space. Else null.
1138
+ function getSourceUnits(offsets , from , to , itemStart ) {
1139
+ if (to <= from || offsets[to - 1] - offsets[from] === to - 1 - from) return null
1140
+ const units = []
1141
+ for (let unit = from; unit < to; unit++) units.push(offsets[unit] - itemStart)
1142
+ return units
1143
+ }
1144
+
1145
+ // Per segment of a paragraph that is one text's handle, where its text starts and ends in the
1146
+ // item's.
1147
+ function findSegmentSources(flow ) {
1148
+ const { segments } = flow.data
1149
+ const offsets = alignToSource(flow.text, segments.join(''), getEngineProfile().lineBreakScan)
1150
+ const sourceStarts = []
1151
+ const sourceEnds = []
1152
+ const sourceUnits = []
1153
+ for (let i = 0, at = 0; i < segments.length; at += segments[i++] .length) {
1154
+ const end = offsets[at + segments[i] .length - 1] + 1
1155
+ sourceStarts.push(offsets[at] )
1156
+ sourceEnds.push(flow.text.charCodeAt(end - 1) === 0x0D && flow.text.charCodeAt(end) === 0x0A && segments[i] === '\n' ? end + 1 : end)
1157
+ sourceUnits.push(getSourceUnits(offsets, at, at + segments[i] .length, 0))
1158
+ }
1159
+ flow.sourceStarts = sourceStarts
1160
+ flow.sourceEnds = sourceEnds
1161
+ flow.sourceUnits = sourceUnits
1162
+ }
1163
+
1164
+ // Where unit `unit` of a segment's text is in its item's text.
1165
+ function getSourceOffset(flow , segmentIndex , unit ) {
1166
+ const units = flow.sourceUnits === null ? null : flow.sourceUnits[segmentIndex]
1167
+ return units === null ? flow.sourceStarts [segmentIndex] + unit : units[unit]
1168
+ }
1169
+
1170
+ // Bridge from cheap range walking to full fragment text. Lets callers do
1171
+ // shrinkwrap/virtualization/probing work first, then only pay for text on the
1172
+ // lines they actually render.
1173
+ export function materializeRichInlineLineRange(
1174
+ prepared ,
1175
+ line ,
1176
+ ) {
1177
+ const flow = getInternalPreparedRichInline(prepared)
1178
+ const { data, itemSegments } = flow
1179
+ if (flow.sourceStarts === null) findSegmentSources(flow)
1180
+ const sourceStarts = flow.sourceStarts
1181
+ const sourceEnds = flow.sourceEnds
1182
+ const lineEndSegmentIndex = getSegmentIndex(flow, line.end)
1183
+ const fragments = []
1184
+
1185
+ for (let i = 0; i < line.fragments.length; i++) {
1186
+ const fragment = line.fragments[i]
1187
+ const first = itemSegments[fragment.itemIndex]
1188
+ if (first === undefined || fragment.itemIndex + 1 >= itemSegments.length) throw new Error('Missing rich-text inline item for fragment')
1189
+ // A range kept from a longer paragraph can end past its item in this one: its text stops at the item's end.
1190
+ const itemEnd = itemSegments[fragment.itemIndex + 1]
1191
+ const startSegmentIndex = Math.min(first + fragment.start.segmentIndex, itemEnd)
1192
+ const startGraphemeIndex = fragment.start.graphemeIndex
1193
+ const endSegmentIndex = Math.min(first + fragment.end.segmentIndex, itemEnd)
1194
+ const endGraphemeIndex = endSegmentIndex < itemEnd ? fragment.end.graphemeIndex : 0
1195
+ // Only the fragment a line ends with shows the hyphen of a soft hyphen it ends at.
1196
+ const endsLine = i === line.fragments.length - 1 && endSegmentIndex === lineEndSegmentIndex && endGraphemeIndex === line.end.graphemeIndex
1197
+ let sourceStart = startSegmentIndex < itemEnd ? sourceStarts[startSegmentIndex] : startSegmentIndex > first ? sourceEnds[startSegmentIndex - 1] : 0
1198
+ if (startGraphemeIndex > 0 && startSegmentIndex < itemEnd) sourceStart = getSourceOffset(flow, startSegmentIndex, getGraphemeEnds(data, startSegmentIndex)[startGraphemeIndex - 1] )
1199
+ let sourceEnd = sourceStart
1200
+ if (endGraphemeIndex > 0) sourceEnd = getSourceOffset(flow, endSegmentIndex, getGraphemeEnds(data, endSegmentIndex)[endGraphemeIndex - 1] - 1) + 1
1201
+ else if (endSegmentIndex > startSegmentIndex) sourceEnd = sourceEnds[endSegmentIndex - 1]
1202
+ fragments.push({
1203
+ itemIndex: fragment.itemIndex,
1204
+ text: endsLine ? buildLineTextFromRange(data, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex)
1205
+ : buildRangeText(data, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex),
1206
+ gapBefore: fragment.gapBefore,
1207
+ gapItemIndex: fragment.gapItemIndex,
1208
+ occupiedWidth: fragment.occupiedWidth,
1209
+ start: fragment.start,
1210
+ end: fragment.end,
1211
+ sourceStart,
1212
+ sourceEnd,
1213
+ })
1214
+ }
1215
+
1216
+ return {
1217
+ fragments,
1218
+ width: line.width,
1219
+ end: line.end,
1220
+ }
1221
+ }
1222
+
1223
+ export function walkRichInlineLineRanges(
1224
+ prepared ,
1225
+ maxWidth ,
1226
+ onLine ,
1227
+ ) {
1228
+ const flow = getInternalPreparedRichInline(prepared)
1229
+ const safeWidth = Math.max(0, normalizeMaxWidth(maxWidth))
1230
+ if (fitsWhole(flow, safeWidth)) {
1231
+ onLine(flow.onlyItem >= 0 ? createOnlyItemLine(flow, flow.wholeWidth , flow.wholeStart, 0, flow.wholeEnd, 0) : createLine(flow, flow.wholeWidth , flow.wholeStart, 0, flow.wholeEnd, 0))
1232
+ return 1
1233
+ }
1234
+ return walkPreparedLinesRaw(flow.data, safeWidth, (width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) => {
1235
+ onLine(flow.onlyItem >= 0 ? createOnlyItemLine(flow, width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex)
1236
+ : createLine(flow, width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex))
1237
+ })
1238
+ }
1239
+
1240
+ export function measureRichInlineStats(
1241
+ prepared ,
1242
+ maxWidth ,
1243
+ ) {
1244
+ const flow = getInternalPreparedRichInline(prepared)
1245
+ const safeWidth = Math.max(0, normalizeMaxWidth(maxWidth))
1246
+ if (fitsWhole(flow, safeWidth)) return { lineCount: 1, maxLineWidth: Math.max(0, flow.wholeWidth ) }
1247
+ const stats = { lineCount: 0, maxLineWidth: 0 }
1248
+ walkPreparedLinesRaw(flow.data, safeWidth, undefined, stats)
1249
+ return stats
1250
+ }
1251
+
1252
+ // Rapier local extension: exact segment/source facts for paragraph-wide fitting.
1253
+ // Shares analysis, shaping and source alignment with the wrapping entry point.
1254
+ function paragraphSegments(flow) {
1255
+ if (flow.sourceStarts === null) findSegmentSources(flow)
1256
+ return flow
1257
+ }
1258
+ export function prepareRichInlineSegments(items, options) {
1259
+ return prepareRichInline(items, {...options, rapierParagraph: true})
291
1260
  }