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,636 +1,1202 @@
|
|
|
1
|
-
/*! Pretext
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
|
|
2
|
+
import {
|
|
3
|
+
CONTROL,
|
|
4
|
+
HARD_BREAK,
|
|
5
|
+
KIND_BITS,
|
|
6
|
+
OBJECT,
|
|
7
|
+
PRESERVED_SPACE,
|
|
8
|
+
RETURNABLE,
|
|
9
|
+
SOFT_HYPHEN,
|
|
10
|
+
SPACE,
|
|
11
|
+
SPACED,
|
|
12
|
+
TAB,
|
|
13
|
+
TEXT,
|
|
14
|
+
UNBROKEN,
|
|
15
|
+
ZERO_WIDTH_BREAK,
|
|
16
|
+
ZERO_WIDTH_GLUE,
|
|
17
|
+
} from './analysis.js'
|
|
18
|
+
|
|
19
|
+
import { getEngineProfile } from './measurement.js'
|
|
20
|
+
import { getFreshLineEnd, getSegmentEntryWidth, } from './entry-geometry.js'
|
|
21
|
+
|
|
22
|
+
const BREAK_AFTER_KINDS = 1 << SPACE | 1 << ZERO_WIDTH_BREAK | 1 << SOFT_HYPHEN | 1 << PRESERVED_SPACE | 1 << TAB
|
|
23
|
+
|
|
24
|
+
// The prepared handle's line-break data: parallel arrays per segment.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
// What the line walkers lay out: a text's handle, or the handle of a rich-inline paragraph that
|
|
68
|
+
// isn't one text's handle (src/rich-inline.ts), which also holds what its segments have of their
|
|
69
|
+
// own. A text's handle has no `items`, so its type, PreparedLineBreakData, doesn't name it.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// What a rich-inline paragraph's items give its segments, per segment, each list null where no
|
|
73
|
+
// segment has a value of its own:
|
|
74
|
+
// - `hyphenWidths`, `tabStopAdvances`, `minimumTabAdvances`: the hyphen a soft hyphen paints, the
|
|
75
|
+
// advance between a tab's stops and the least a tab advances, in the item's font, where two
|
|
76
|
+
// items differ in one. A walk reads them, and the handle's own three, at a soft hyphen, under
|
|
77
|
+
// soft-hyphen contexts and at a tab, so a paragraph whose text holds no soft hyphen and no
|
|
78
|
+
// preserved tab has none, and its handle's own three are 0;
|
|
79
|
+
// - `hyphenRooms`: the room a line that ends at the break before the segment leaves for a hyphen,
|
|
80
|
+
// where it returns there from a soft hyphen whose hyphen doesn't fit, in a paragraph with a soft
|
|
81
|
+
// hyphen under a profile that leaves such room (getHyphenRooms in src/rich-inline.ts), with one
|
|
82
|
+
// more entry for the break at the paragraph's end;
|
|
83
|
+
// - `itemEndHalts`: the line-end trim of a segment that ends its item with a closing mark Blink
|
|
84
|
+
// halts at a line's end, or the trim its retry between graphemes gives one (overflowLineEndTrims),
|
|
85
|
+
// which the mark keeps where the line goes on after it (prepareRichInline() in
|
|
86
|
+
// src/rich-inline.ts);
|
|
87
|
+
// - `insideExtras`, `fillExtras`: the item's extraWidth where a line that starts inside the
|
|
88
|
+
// segment pays it, or starts at it and fills it grapheme by grapheme, as a line that starts
|
|
89
|
+
// with the whole segment pays lineStartExtras;
|
|
90
|
+
// - `openingEdges`: the width of a segment that is the start edge of a padded item's opening
|
|
91
|
+
// (getOpeningFit in src/rich-inline.ts), which a line that takes it paints whole, whatever of
|
|
92
|
+
// it the line fitted: an object's line-end trim is the part its line doesn't fit, and an edge
|
|
93
|
+
// the engine fits none of is a preserved space, which takes no room in the run of preserved
|
|
94
|
+
// spaces and tabs it is in;
|
|
95
|
+
// - `emptyObjectSpaces`, `emptyObjectReturns`: where the engine places an object of width 0
|
|
96
|
+
// though it sticks out of the line, for each such object what a line reads of the text around
|
|
97
|
+
// it (setEmptyObjectFacts in src/rich-inline.ts): the width of the white space that ends the
|
|
98
|
+
// text before it as its last text frame's own, 0 where that text ends in other white space
|
|
99
|
+
// and -1 where it ends in none, and 1 where a line with a break before the object goes back
|
|
100
|
+
// to it, as what follows the object would stick out too.
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
function breaksAfterKind(kind ) {
|
|
123
|
+
return (1 << kind & BREAK_AFTER_KINDS) !== 0
|
|
5
124
|
}
|
|
6
|
-
|
|
7
|
-
|
|
125
|
+
|
|
126
|
+
// A line ends at a chosen soft hyphen, and paints its hyphen, where the line's end
|
|
127
|
+
// cursor is the start of the segment after one. A soft hyphen that ends the text is
|
|
128
|
+
// consumed with its line and chosen by no wrap, so the cursor at the text's end
|
|
129
|
+
// paints none.
|
|
130
|
+
export function isDiscretionaryLineEnd(
|
|
131
|
+
segmentFlags ,
|
|
132
|
+
endSegmentIndex ,
|
|
133
|
+
endGraphemeIndex ,
|
|
134
|
+
) {
|
|
135
|
+
return endGraphemeIndex === 0 && endSegmentIndex > 0 && endSegmentIndex < segmentFlags.length &&
|
|
136
|
+
(segmentFlags[endSegmentIndex - 1] & KIND_BITS) === SOFT_HYPHEN
|
|
8
137
|
}
|
|
9
|
-
|
|
10
|
-
|
|
138
|
+
|
|
139
|
+
// At a paragraph or hard-break start, ZWSP is real source: it establishes the
|
|
140
|
+
// line and offers a break after it. UAX #14 forbids an ordinary break before
|
|
141
|
+
// ZWSP. After a forced overflow break browsers can still give ZWSP its own line;
|
|
142
|
+
// that start is consumed here.
|
|
143
|
+
function consumesAtLineStart(kind , atChunkStart ) {
|
|
144
|
+
return kind === SPACE || kind === SOFT_HYPHEN || (kind === ZERO_WIDTH_BREAK && !atChunkStart)
|
|
11
145
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
146
|
+
|
|
147
|
+
// A tab's advance from `position` on the line: to the next stop, or to the one after where
|
|
148
|
+
// the next is under the minimum away. A tab on a stop is a whole stop from the next, so each
|
|
149
|
+
// tab of a run takes one. Stops no wider than 0, under a letter spacing of minus a space or
|
|
150
|
+
// less, leave a tab no advance: Gecko gives tabs no width there (GetSpacingInternal,
|
|
151
|
+
// nsTextFrame.cpp:4262-4264), and Blink's, negative by Font::TabWidth, is clamped to 0 where
|
|
152
|
+
// the tab's item is placed (ClampNegativeToZero, line_breaker.cc:1486, 1703). Gecko counts in
|
|
153
|
+
// whole app units (EngineProfile's tabsInAppUnits), so a tab exactly the minimum from its
|
|
154
|
+
// stop takes it, as many do: in Arial and Helvetica a space is half a `0`. Canvas sums come
|
|
155
|
+
// within float error of such a tie.
|
|
156
|
+
function getTabAdvance(position , tabStopAdvance , minimumAdvance , appUnits ) {
|
|
157
|
+
if (appUnits) {
|
|
158
|
+
const stop = Math.round(tabStopAdvance * 60)
|
|
159
|
+
if (stop <= 0) return 0
|
|
160
|
+
const x = Math.round(position * 60)
|
|
161
|
+
return (Math.ceil((x + Math.round(minimumAdvance * 120) / 2) / stop) * stop - x) / 60
|
|
162
|
+
}
|
|
163
|
+
if (tabStopAdvance <= 0) return 0
|
|
164
|
+
|
|
165
|
+
// How far the tab is past the stop before it, by a division and a floor, which counts back
|
|
166
|
+
// from a negative position too. `%` gives the same, within float error, but on numbers that
|
|
167
|
+
// aren't whole it is a call to the C library's fmod, which was nearly all a tab's arithmetic
|
|
168
|
+
// cost (RESEARCH.md, JavaScript Engines).
|
|
169
|
+
const pastStop = position - Math.floor(position / tabStopAdvance) * tabStopAdvance
|
|
170
|
+
const advance = tabStopAdvance - pastStop
|
|
171
|
+
return advance < minimumAdvance ? advance + tabStopAdvance : advance
|
|
34
172
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
173
|
+
|
|
174
|
+
// The advance of tab segment `index` of a rich-inline paragraph, which the line reaches at
|
|
175
|
+
// `lineWidth`: to the next stop of its item's font. Tab stops count from the line's start in
|
|
176
|
+
// every engine, never from an item's (Blink's line_breaker.cc:2963-2971, WebKit's pen position,
|
|
177
|
+
// Gecko's CalcTabWidths, nsTextFrame.cpp:4298-4378). A padded item's text starts after its start
|
|
178
|
+
// edge, half its extraWidth, all of which `lineWidth` counts, for a tab that starts the line too
|
|
179
|
+
// (lineStartExtras): the other half is the end edge, which follows the item's text on the line.
|
|
180
|
+
// Where the paragraph's items differ in letter spacing, a tab's width holds the gap after it, as
|
|
181
|
+
// every segment's holds its own (src/rich-inline.ts); else it is 0.
|
|
182
|
+
export function getItemTabAdvance(prepared , items , index , lineWidth , appUnits ) {
|
|
183
|
+
const stopAdvance = items.tabStopAdvances === null ? prepared.tabStopAdvance : items.tabStopAdvances[index]
|
|
184
|
+
const minimumAdvance = items.minimumTabAdvances === null ? prepared.minimumTabAdvance : items.minimumTabAdvances[index]
|
|
185
|
+
const endEdge = items.insideExtras === null ? 0 : items.insideExtras[index] / 2
|
|
186
|
+
return getTabAdvance(lineWidth - endEdge, stopAdvance, minimumAdvance, appUnits) + prepared.widths[index]
|
|
38
187
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
188
|
+
|
|
189
|
+
// Where a line that holds only an overflowing grapheme ends: after that grapheme and
|
|
190
|
+
// the graphemes after it that can't start a line, up to `endGraphemeIndex`.
|
|
191
|
+
function getOverflowingFirstGraphemeEnd(
|
|
192
|
+
prepared ,
|
|
193
|
+
segmentIndex ,
|
|
194
|
+
graphemeIndex ,
|
|
195
|
+
endGraphemeIndex ,
|
|
196
|
+
) {
|
|
197
|
+
const prohibitions = prepared.lineStartProhibitions?.[segmentIndex] ?? null
|
|
198
|
+
let end = graphemeIndex + 1
|
|
199
|
+
while (prohibitions !== null && end < endGraphemeIndex && prohibitions[end] === 1) end++
|
|
200
|
+
return end
|
|
42
201
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
202
|
+
|
|
203
|
+
function getTerminalLetterSpacing(
|
|
204
|
+
prepared ,
|
|
205
|
+
openingEdges ,
|
|
206
|
+
hangingKinds ,
|
|
207
|
+
startSegmentIndex ,
|
|
208
|
+
startGraphemeIndex ,
|
|
209
|
+
endSegmentIndex ,
|
|
210
|
+
endGraphemeIndex ,
|
|
211
|
+
) {
|
|
212
|
+
const { letterSpacing, segmentFlags } = prepared
|
|
213
|
+
if (letterSpacing === 0) return 0
|
|
214
|
+
|
|
215
|
+
if (endGraphemeIndex > 0) return (segmentFlags[endSegmentIndex] & SPACED) !== 0 ? letterSpacing : 0
|
|
216
|
+
|
|
217
|
+
if (isDiscretionaryLineEnd(segmentFlags, endSegmentIndex, endGraphemeIndex)) return 0
|
|
218
|
+
// A run of preserved spaces and tabs that hangs where the line wraps already
|
|
219
|
+
// charged the gap after the glyph before it. Gecko doesn't hang tabs.
|
|
220
|
+
if (endSegmentIndex > startSegmentIndex && endSegmentIndex < segmentFlags.length && (segmentFlags[endSegmentIndex] & KIND_BITS) !== HARD_BREAK &&
|
|
221
|
+
(1 << (segmentFlags[endSegmentIndex - 1] & KIND_BITS) & hangingKinds) !== 0) return 0
|
|
222
|
+
|
|
223
|
+
for (let i = endSegmentIndex - 1; i >= startSegmentIndex; i--) {
|
|
224
|
+
const flags = segmentFlags[i]
|
|
225
|
+
const kind = flags & KIND_BITS
|
|
226
|
+
// Segments that take no letter spacing, such as zero-width glue or marks
|
|
227
|
+
// shaped on the grapheme before them, leave that grapheme's gap last. A tab
|
|
228
|
+
// that takes none follows that gap, which its stop counts from. An object
|
|
229
|
+
// leaves none after itself, nor does the edge of a padded opening.
|
|
230
|
+
if (kind === SPACE || (kind !== CONTROL && kind !== TAB && (flags & SPACED) === 0 && kind !== OBJECT && (openingEdges === null || openingEdges[i] === 0))) continue
|
|
231
|
+
|
|
232
|
+
if (i === startSegmentIndex && startGraphemeIndex > 0) return letterSpacing
|
|
233
|
+
|
|
234
|
+
return (flags & SPACED) !== 0 ? letterSpacing : 0
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return 0
|
|
46
238
|
}
|
|
47
|
-
|
|
48
|
-
|
|
239
|
+
|
|
240
|
+
// The width a paragraph is laid out at: `NaN`, which a typed caller can pass, is unbounded.
|
|
241
|
+
// Every comparison fails at `NaN`, and the line loops ask some whether a segment fits and
|
|
242
|
+
// others whether it overflows, so the line APIs called once for a paragraph pass their width
|
|
243
|
+
// through here and the loops stay written for numbers. The streams, called once for each
|
|
244
|
+
// line, take their width as given (RESEARCH.md, Decisions Log, 2026-10-02).
|
|
245
|
+
export function normalizeMaxWidth(maxWidth ) {
|
|
246
|
+
return maxWidth <= Infinity ? maxWidth : Infinity
|
|
49
247
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
248
|
+
|
|
249
|
+
// Mutates `cursor` to the next renderable line start. False when no line remains.
|
|
250
|
+
// A chunk runs to a hard break or the end of the text, and the hard break ends a
|
|
251
|
+
// line however little the chunk holds: a chunk holding only its hard break, or only
|
|
252
|
+
// source a line start consumes before it, such as soft hyphens, is an empty line,
|
|
253
|
+
// which starts at its hard break. The rest of a chunk after a line that wrapped
|
|
254
|
+
// inside it is no line of its own when a line start consumes all of it.
|
|
255
|
+
export function normalizePreparedLineStart(
|
|
256
|
+
prepared ,
|
|
257
|
+
cursor ,
|
|
258
|
+
) {
|
|
259
|
+
const { segmentFlags } = prepared
|
|
260
|
+
const segmentCount = segmentFlags.length
|
|
261
|
+
let segmentIndex = cursor.segmentIndex
|
|
262
|
+
if (segmentIndex >= segmentCount) return false
|
|
263
|
+
if (cursor.graphemeIndex > 0) return true
|
|
264
|
+
|
|
265
|
+
let atChunkStart = segmentIndex === 0 || (segmentFlags[segmentIndex - 1] & KIND_BITS) === HARD_BREAK
|
|
266
|
+
while (true) {
|
|
267
|
+
const kind = segmentFlags[segmentIndex] & KIND_BITS
|
|
268
|
+
if (kind === HARD_BREAK) {
|
|
269
|
+
if (atChunkStart) {
|
|
270
|
+
cursor.segmentIndex = segmentIndex
|
|
271
|
+
cursor.graphemeIndex = 0
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
if (++segmentIndex >= segmentCount) return false
|
|
275
|
+
cursor.segmentIndex = segmentIndex
|
|
276
|
+
cursor.graphemeIndex = 0
|
|
277
|
+
atChunkStart = true
|
|
278
|
+
} else if (consumesAtLineStart(kind, atChunkStart)) {
|
|
279
|
+
if (++segmentIndex >= segmentCount) return false
|
|
280
|
+
} else {
|
|
281
|
+
cursor.segmentIndex = segmentIndex
|
|
282
|
+
cursor.graphemeIndex = 0
|
|
283
|
+
return true
|
|
62
284
|
}
|
|
63
|
-
|
|
285
|
+
}
|
|
64
286
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
287
|
+
|
|
288
|
+
// Walks every line of the text into `stats`, visiting each, and returns the line count.
|
|
289
|
+
export function walkPreparedLinesRaw(
|
|
290
|
+
prepared ,
|
|
291
|
+
maxWidth ,
|
|
292
|
+
onLine ,
|
|
293
|
+
stats = { lineCount: 0, maxLineWidth: 0 },
|
|
294
|
+
) {
|
|
295
|
+
const cursor = { segmentIndex: 0, graphemeIndex: 0 }
|
|
296
|
+
if (!prepared.simpleLineWalkFastPath) {
|
|
297
|
+
if (normalizePreparedLineStart(prepared, cursor)) walkPreparedComplexLines(prepared, cursor, maxWidth, onLine, stats)
|
|
298
|
+
return stats.lineCount
|
|
299
|
+
}
|
|
300
|
+
// A fast-path handle is one chunk of text, spaces and ZWSPs, so each line steps
|
|
301
|
+
// from where the last one ended, past what a line can't start with. One loop for
|
|
302
|
+
// both walkers, normalizing each line start and stepping through
|
|
303
|
+
// stepPreparedLineGeometryFromStart(), walked chat 6 to 32% slower in Chrome and
|
|
304
|
+
// Firefox (RESEARCH.md, Keeping Work Bounded).
|
|
305
|
+
const { segmentFlags } = prepared
|
|
306
|
+
const segmentCount = segmentFlags.length
|
|
307
|
+
while (true) {
|
|
308
|
+
let startSegmentIndex = cursor.segmentIndex
|
|
309
|
+
const atTextStart = startSegmentIndex === 0
|
|
310
|
+
while (startSegmentIndex < segmentCount && consumesAtLineStart(segmentFlags[startSegmentIndex] & KIND_BITS, atTextStart)) {
|
|
311
|
+
startSegmentIndex++
|
|
78
312
|
}
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
313
|
+
if (startSegmentIndex >= segmentCount) return stats.lineCount
|
|
314
|
+
const startGraphemeIndex = cursor.graphemeIndex
|
|
315
|
+
cursor.segmentIndex = startSegmentIndex
|
|
316
|
+
const width = stepPreparedSimpleLineGeometry(prepared, cursor, maxWidth)
|
|
317
|
+
stats.lineCount++
|
|
318
|
+
if (width > stats.maxLineWidth) stats.maxLineWidth = width
|
|
319
|
+
onLine?.(width, startSegmentIndex, startGraphemeIndex, cursor.segmentIndex, cursor.graphemeIndex)
|
|
320
|
+
}
|
|
83
321
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
322
|
+
|
|
323
|
+
// layout()'s count: the simple stepper's lines as one numeric loop, with no
|
|
324
|
+
// cursor and no per-line call. Every segment boundary of a fast-path handle is
|
|
325
|
+
// a break, so an overflowing space or ZWSP ends its line and any other segment
|
|
326
|
+
// starts the next one. The full walker costs three to five times as much per
|
|
327
|
+
// segment, so one walker for all text was rejected (RESEARCH.md, Decisions Log,
|
|
328
|
+
// 2026-09-24).
|
|
329
|
+
export function countPreparedLines(prepared , maxWidth ) {
|
|
330
|
+
// The loop takes no overflow trims, which the stepper takes for a line's first segment.
|
|
331
|
+
if (!prepared.simpleLineWalkFastPath || prepared.overflowLineEndTrims !== null) {
|
|
332
|
+
return prepared.simpleLineCountFastPath ? countSteppedLines(prepared, maxWidth) : walkPreparedLinesRaw(prepared, maxWidth)
|
|
333
|
+
}
|
|
334
|
+
const { widths, segmentFlags, breakableFitAdvances, entryGeometry, lineStartProhibitions, breakableLineStartExtras, lineStartExtras, lineEndTrims } = prepared
|
|
335
|
+
const fitLimit = Math.max(0, maxWidth) + getEngineProfile().lineFitEpsilon
|
|
336
|
+
const segmentCount = widths.length
|
|
337
|
+
let count = 0
|
|
338
|
+
// Every line starts at 0 and adds its content's widths. Firefox runs this loop
|
|
339
|
+
// about 1.6 times as long when a line's width is set from a segment's width
|
|
340
|
+
// instead (RESEARCH.md, Keeping Work Bounded).
|
|
341
|
+
let lineW = 0
|
|
342
|
+
let hasContent = false
|
|
343
|
+
|
|
344
|
+
// A ZWSP at the start of the text starts the first line.
|
|
345
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
346
|
+
const kind = segmentFlags[i] & KIND_BITS
|
|
347
|
+
const w = widths[i]
|
|
348
|
+
const endTrim = lineEndTrims === null ? 0 : lineEndTrims[i]
|
|
349
|
+
if (hasContent) {
|
|
350
|
+
// A segment that fits only by its line-end trim ends the line, as the full
|
|
351
|
+
// width it adds leaves no room after it.
|
|
352
|
+
if (lineW + w - endTrim <= fitLimit) {
|
|
353
|
+
lineW += w
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
count++
|
|
357
|
+
lineW = 0
|
|
358
|
+
hasContent = false
|
|
359
|
+
if (kind !== TEXT) continue
|
|
360
|
+
} else if (kind === SPACE || (kind === ZERO_WIDTH_BREAK && i > 0)) {
|
|
361
|
+
continue
|
|
94
362
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
363
|
+
|
|
364
|
+
const startW = lineStartExtras === null ? w : w + lineStartExtras[i]
|
|
365
|
+
const advances = breakableFitAdvances[i]
|
|
366
|
+
if (startW - endTrim <= fitLimit || advances === null) {
|
|
367
|
+
lineW += startW
|
|
368
|
+
hasContent = true
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
// An overflowing breakable segment fills lines grapheme by grapheme. A line
|
|
372
|
+
// holding only an overflowing grapheme keeps the graphemes after it that
|
|
373
|
+
// can't start a line. A line that starts inside it where it has fresh-line
|
|
374
|
+
// geometry takes the tail or its fresh prefixes.
|
|
375
|
+
const prohibitions = lineStartProhibitions?.[i] ?? null
|
|
376
|
+
const entry = entryGeometry === null ? null : entryGeometry[i]
|
|
377
|
+
const startExtras = breakableLineStartExtras?.[i] ?? null
|
|
378
|
+
let g = 0
|
|
379
|
+
while (g < advances.length) {
|
|
380
|
+
if (g > 0 && entry !== null && entry.entries[g] !== null) {
|
|
381
|
+
const end = getFreshLineEnd(entry, g, advances.length, fitLimit)
|
|
382
|
+
if (end > advances.length) {
|
|
383
|
+
lineW += getSegmentEntryWidth(entry, g, advances.length)
|
|
384
|
+
hasContent = true
|
|
385
|
+
break
|
|
106
386
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
387
|
+
count++
|
|
388
|
+
g = end
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
391
|
+
if (startExtras !== null) lineW += startExtras[g]
|
|
392
|
+
lineW += advances[g++]
|
|
393
|
+
if (prohibitions !== null && lineW > fitLimit) {
|
|
394
|
+
const kept = g
|
|
395
|
+
while (g < advances.length && prohibitions[g] === 1) lineW += advances[g++]
|
|
396
|
+
if (g > kept) {
|
|
397
|
+
count++
|
|
398
|
+
lineW = 0
|
|
399
|
+
continue
|
|
113
400
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
401
|
+
}
|
|
402
|
+
while (g < advances.length && lineW + advances[g] <= fitLimit) lineW += advances[g++]
|
|
403
|
+
if (g < advances.length) {
|
|
404
|
+
count++
|
|
405
|
+
lineW = 0
|
|
406
|
+
} else {
|
|
407
|
+
hasContent = true
|
|
408
|
+
}
|
|
118
409
|
}
|
|
119
|
-
|
|
410
|
+
}
|
|
411
|
+
return count + (hasContent ? 1 : 0)
|
|
120
412
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
413
|
+
|
|
414
|
+
// layout()'s count of text of the simple walkers' kinds where the scan gives no
|
|
415
|
+
// break at some segment boundary, as before NEL: the simple stepper's lines, except
|
|
416
|
+
// that the full walker steps a line again where the stepper ended it at such a
|
|
417
|
+
// boundary, before the segment or after the space before it, returning the line to
|
|
418
|
+
// its last break. Checking for that inside the counter's loop slowed Firefox's and
|
|
419
|
+
// Chrome's count of all other text once the check had ever held, and the line APIs
|
|
420
|
+
// keep the full walker for this text, since the stepper's widths can differ from
|
|
421
|
+
// its in the last bits (RESEARCH.md, Keeping Work Bounded).
|
|
422
|
+
function countSteppedLines(prepared , maxWidth ) {
|
|
423
|
+
const { segmentFlags } = prepared
|
|
424
|
+
const cursor = { segmentIndex: 0, graphemeIndex: 0 }
|
|
425
|
+
let count = 0
|
|
426
|
+
while (normalizePreparedLineStart(prepared, cursor)) {
|
|
427
|
+
const startSegmentIndex = cursor.segmentIndex
|
|
428
|
+
const startGraphemeIndex = cursor.graphemeIndex
|
|
429
|
+
stepPreparedSimpleLineGeometry(prepared, cursor, maxWidth)
|
|
430
|
+
if (cursor.graphemeIndex === 0 && cursor.segmentIndex < segmentFlags.length && (segmentFlags[cursor.segmentIndex] & UNBROKEN) !== 0) {
|
|
431
|
+
cursor.segmentIndex = startSegmentIndex
|
|
432
|
+
cursor.graphemeIndex = startGraphemeIndex
|
|
433
|
+
walkPreparedComplexLines(prepared, cursor, maxWidth, undefined, null, true)
|
|
132
434
|
}
|
|
133
|
-
|
|
134
|
-
|
|
435
|
+
count++
|
|
436
|
+
}
|
|
437
|
+
return count
|
|
135
438
|
}
|
|
136
|
-
|
|
137
|
-
|
|
439
|
+
|
|
440
|
+
// Whether the run of preserved spaces and tabs that hang, which starts at segment `start` right
|
|
441
|
+
// after an object on its line, an atomic rich-inline item or a box (OBJECT), stays there however
|
|
442
|
+
// far the line overflows, as no break comes before it in the text (UAX #14 LB7). Blink takes it
|
|
443
|
+
// as trailing items after the break after an atomic inline (HandleTrailingSpaces,
|
|
444
|
+
// line_breaker.cc:2426-2534), trailing on into the next item where an item's spaces reach its end
|
|
445
|
+
// (:2518-2533); WebKit gives a soft wrap opportunity after each white-space item
|
|
446
|
+
// (isAtSoftWrapOpportunity, InlineFormattingUtils.cpp:408-413) and keeps each as content that hangs
|
|
447
|
+
// (InlineContentBreaker.cpp:181-182); and Gecko lets an empty frame past the line's end
|
|
448
|
+
// (CanPlaceFrame, nsLineLayout.cpp:1217-1270). After text that overflows, the engine's retry
|
|
449
|
+
// between graphemes breaks before the white space instead. But Gecko breaks only after a run of
|
|
450
|
+
// spaces and tabs (nsLineBreaker.cpp:323, :586) and doesn't hang a tab, so where the white space
|
|
451
|
+
// runs into a tab, Firefox moves all of it to the next line with the tab. The walker asks once
|
|
452
|
+
// per run, so a long run costs one pass.
|
|
453
|
+
function staysAfterObject(segmentFlags , start , hangingKinds ) {
|
|
454
|
+
let after = start + 1
|
|
455
|
+
while (after < segmentFlags.length && (1 << (segmentFlags[after] & KIND_BITS) & hangingKinds) !== 0) after++
|
|
456
|
+
return after === segmentFlags.length || (segmentFlags[after] & KIND_BITS) !== TAB
|
|
138
457
|
}
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
458
|
+
|
|
459
|
+
// Whether a line that would end at the break before `breakSegmentIndex`, painting
|
|
460
|
+
// `breakWidth`, returns to an earlier opportunity: where it ends at a selected
|
|
461
|
+
// discretionary hyphen that doesn't fit. A return needs an overflow that isolated
|
|
462
|
+
// widths can show: the soft hyphens on the line may measure narrower joined than apart
|
|
463
|
+
// by less than the overflow.
|
|
464
|
+
function returnsFromUnfitHyphen(
|
|
465
|
+
prepared ,
|
|
466
|
+
lineStartSegmentIndex ,
|
|
467
|
+
breakSegmentIndex ,
|
|
468
|
+
breakWidth ,
|
|
469
|
+
fitLimit ,
|
|
470
|
+
) {
|
|
471
|
+
const { discretionaryHyphenContexts, segmentFlags } = prepared
|
|
472
|
+
const softHyphenIndex = breakSegmentIndex - 1
|
|
473
|
+
if (softHyphenIndex < lineStartSegmentIndex || (segmentFlags[softHyphenIndex] & KIND_BITS) !== SOFT_HYPHEN || breakWidth <= fitLimit) return false
|
|
474
|
+
const overflow = breakWidth - fitLimit
|
|
475
|
+
let narrowing = 0
|
|
476
|
+
if (discretionaryHyphenContexts !== null) for (let i = lineStartSegmentIndex; i <= softHyphenIndex; i++) narrowing += discretionaryHyphenContexts[i]
|
|
477
|
+
return narrowing < overflow
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// The full walker, for text the simple walkers don't cover: from a normalized line
|
|
481
|
+
// start, every line into `stats`, or with `singleLine` one line, whose width it
|
|
482
|
+
// returns (null without one). Every line state is a local of this one function,
|
|
483
|
+
// with no closure over it: V8 boxes a captured number, so each write to one cost
|
|
484
|
+
// 12-14ns there against about 1ns for a local. It keeps its own loop over lines:
|
|
485
|
+
// stepping one line per call, it read the handle and the profile and derived its
|
|
486
|
+
// limits again for every line, and short lines laid out 7 to 40% slower in all
|
|
487
|
+
// three browsers (RESEARCH.md, Keeping Work Bounded).
|
|
488
|
+
function walkPreparedComplexLines(
|
|
489
|
+
prepared ,
|
|
490
|
+
cursor ,
|
|
491
|
+
maxWidth ,
|
|
492
|
+
onLine ,
|
|
493
|
+
stats ,
|
|
494
|
+
singleLine = false,
|
|
495
|
+
) {
|
|
496
|
+
const {
|
|
497
|
+
widths,
|
|
498
|
+
segmentFlags,
|
|
499
|
+
breakableFitAdvances,
|
|
500
|
+
entryGeometry,
|
|
501
|
+
discretionaryHyphenWidth,
|
|
502
|
+
letterSpacing,
|
|
503
|
+
lineStartExtras,
|
|
504
|
+
lineEndTrims,
|
|
505
|
+
overflowLineEndTrims,
|
|
506
|
+
tabStopAdvance,
|
|
507
|
+
minimumTabAdvance,
|
|
508
|
+
} = prepared
|
|
509
|
+
const segmentCount = segmentFlags.length
|
|
510
|
+
const items = prepared.items
|
|
511
|
+
const hyphenWidths = items === undefined ? null : items.hyphenWidths
|
|
512
|
+
const hyphenRooms = items === undefined ? null : items.hyphenRooms
|
|
513
|
+
const itemEndHalts = items === undefined ? null : items.itemEndHalts
|
|
514
|
+
const insideExtras = items === undefined ? null : items.insideExtras
|
|
515
|
+
const fillExtras = items === undefined ? null : items.fillExtras
|
|
516
|
+
const openingEdges = items === undefined ? null : items.openingEdges
|
|
517
|
+
const emptyObjectSpaces = items === undefined ? null : items.emptyObjectSpaces
|
|
518
|
+
const emptyObjectReturns = items === undefined ? null : items.emptyObjectReturns
|
|
519
|
+
const engineProfile = getEngineProfile()
|
|
520
|
+
// Preserved spaces and tabs at the end of a line hang past it (CSS Text 3
|
|
521
|
+
// §4.1.2), so they take no room when fitting and don't size the line (§8.2).
|
|
522
|
+
// Gecko doesn't hang tabs.
|
|
523
|
+
const hangingKinds = 1 << PRESERVED_SPACE | (engineProfile.hangTabs ? 1 << TAB : 0)
|
|
524
|
+
// In Gecko a text frame's width leaves out its trailing spaces that overflow the line and
|
|
525
|
+
// keeps those that fit, whatever follows the frame (EngineProfile, hangsSpacesPerTextFrame),
|
|
526
|
+
// so in a rich-inline paragraph the run of preserved spaces that hang goes on, with what
|
|
527
|
+
// overflows, past a segment that takes no room: an object of width 0, a soft hyphen or a
|
|
528
|
+
// zero-width break, which the Gecko scan makes of a soft hyphen after white space.
|
|
529
|
+
const hangGoesOnPastEmpty = items !== undefined && engineProfile.hangsSpacesPerTextFrame
|
|
530
|
+
const testsHyphenBeforeAtomic = engineProfile.testsHyphenBeforeAtomic
|
|
531
|
+
const zeroWidthGlueTakesLine = engineProfile.zeroWidthGlueTakesLine
|
|
532
|
+
const tabsInAppUnits = engineProfile.tabsInAppUnits
|
|
533
|
+
const paddedOpeningFit = engineProfile.paddedOpeningFit
|
|
534
|
+
const hardBreakItemRetreat = engineProfile.hardBreakItemRetreat
|
|
535
|
+
// A negative width lays out as 0, as in the simple stepper.
|
|
536
|
+
const availableWidth = Math.max(0, maxWidth)
|
|
537
|
+
const fitLimit = availableWidth + engineProfile.lineFitEpsilon
|
|
538
|
+
// Preparation records soft-hyphen contexts only where the text has a soft hyphen.
|
|
539
|
+
const retreatsFromUnfitHyphen = prepared.discretionaryHyphenContexts !== null
|
|
540
|
+
// Blink's retry leaves room for the hyphen at every earlier opportunity. Gecko and
|
|
541
|
+
// WebKit return to any opportunity whose line fits. A rich-inline paragraph has that room per
|
|
542
|
+
// break (ParagraphSegmentData, hyphenRooms).
|
|
543
|
+
const reservesHyphenWidth = engineProfile.unfitHyphenRetreat === 'reduced-width'
|
|
544
|
+
const reservedHyphenWidth = reservesHyphenWidth ? discretionaryHyphenWidth : 0
|
|
545
|
+
// WebKit's return stops at the line's first opportunity, whatever its hyphen overflows:
|
|
546
|
+
// the soft hyphen the line reaches before any of its opportunities has fit, since one
|
|
547
|
+
// without a hyphen fits where the text before it did.
|
|
548
|
+
const keepsFirstBreak = retreatsFromUnfitHyphen && engineProfile.unfitHyphenRetreat === 'full-width-or-first'
|
|
549
|
+
|
|
550
|
+
let lastLineWidth = null
|
|
551
|
+
while (true) {
|
|
552
|
+
const lineStartSegmentIndex = cursor.segmentIndex
|
|
553
|
+
const lineStartGraphemeIndex = cursor.graphemeIndex
|
|
554
|
+
let lineW = 0
|
|
555
|
+
let hasContent = false
|
|
556
|
+
let lineEndSegmentIndex = lineStartSegmentIndex
|
|
557
|
+
let lineEndGraphemeIndex = lineStartGraphemeIndex
|
|
558
|
+
let pendingBreakSegmentIndex = -1
|
|
559
|
+
// A line that ends at the pending break both fits and paints this width.
|
|
560
|
+
let pendingBreakWidth = 0
|
|
561
|
+
// The opportunity the line returns to when a selected discretionary hyphen does
|
|
562
|
+
// not fit, with that line's painted width: the latest whose line leaves room for
|
|
563
|
+
// the hyphen, which Blink's retry against the width minus the hyphen finds, or
|
|
564
|
+
// in Gecko and WebKit the latest that fits at the full width (reservedHyphenWidth).
|
|
565
|
+
// Under keepsFirstBreak it is the line's first soft hyphen, whatever its hyphen
|
|
566
|
+
// overflows, until a later opportunity fits.
|
|
567
|
+
let fitBreakSegmentIndex = -1
|
|
568
|
+
let fitBreakPaintWidth = 0
|
|
569
|
+
// The latest run of preserved spaces and tabs: the segment after it, and the
|
|
570
|
+
// line's width before it, less the line-end trim of the text it follows, with
|
|
571
|
+
// the gap after the glyph before it. In a rich-inline paragraph, the run can
|
|
572
|
+
// stay on its line after an object however far it overflows (staysAfterObject),
|
|
573
|
+
// and that width also has the edges in the run that a line ending in it paints,
|
|
574
|
+
// which are hangEdgesWidth wide (ParagraphSegmentData).
|
|
575
|
+
let hangEndSegmentIndex = -1
|
|
576
|
+
let hangStartWidth = 0
|
|
577
|
+
let hangStays = false
|
|
578
|
+
let hangEdgesWidth = 0
|
|
579
|
+
// The line-end trim of the last whole segment, where only that trim let it fit, kept
|
|
580
|
+
// past segments after it that take no room at the line end, as spaces. Every later
|
|
581
|
+
// segment that takes room overflows, so the line ends before it and paints that much less.
|
|
582
|
+
let lineEndTrimmed = 0
|
|
583
|
+
// Retained line-start ZWSP establishes the line without owning a spacing gap.
|
|
584
|
+
let zeroWidthPrefix = true
|
|
585
|
+
let afterUnspacedControl = false
|
|
586
|
+
// Where the line ends and the width it paints there, once decided: -1 ends it at
|
|
587
|
+
// the line's current end. With returnsFromHyphen, a line that ends at a selected
|
|
588
|
+
// discretionary hyphen that doesn't fit returns to the recorded earlier opportunity.
|
|
589
|
+
let endSegmentIndex = -1
|
|
590
|
+
let endGraphemeIndex = 0
|
|
591
|
+
let endWidth = 0
|
|
592
|
+
let returnsFromHyphen = false
|
|
593
|
+
|
|
594
|
+
let lineWidth = null
|
|
595
|
+
if ((segmentFlags[lineStartSegmentIndex] & KIND_BITS) === HARD_BREAK) {
|
|
596
|
+
// A line that starts at a hard break is an empty chunk's (normalizePreparedLineStart),
|
|
597
|
+
// as wide as what a line pays where it starts there: inside a padded rich-inline item,
|
|
598
|
+
// its extraWidth.
|
|
599
|
+
cursor.segmentIndex = lineStartSegmentIndex + 1
|
|
600
|
+
cursor.graphemeIndex = 0
|
|
601
|
+
lineWidth = lineStartExtras === null ? 0 : lineStartExtras[lineStartSegmentIndex]
|
|
602
|
+
} else {
|
|
603
|
+
decided: {
|
|
604
|
+
// Where the line ends when its source runs out: after the chunk's hard
|
|
605
|
+
// break, or at the end of the text.
|
|
606
|
+
let consumedEndSegmentIndex
|
|
607
|
+
for (let i = lineStartSegmentIndex; ; i++) {
|
|
608
|
+
if (i >= segmentCount) {
|
|
609
|
+
consumedEndSegmentIndex = segmentCount
|
|
610
|
+
break
|
|
611
|
+
}
|
|
612
|
+
// The graphemes of segment i from fillStart go on the line one by one, after
|
|
613
|
+
// the gap fillSpacing.
|
|
614
|
+
let fillStart
|
|
615
|
+
let fillSpacing = 0
|
|
616
|
+
const flags = segmentFlags[i]
|
|
617
|
+
const kind = flags & KIND_BITS
|
|
618
|
+
if (kind === HARD_BREAK) {
|
|
619
|
+
consumedEndSegmentIndex = i + 1
|
|
620
|
+
break
|
|
621
|
+
}
|
|
622
|
+
const spaced = (flags & SPACED) !== 0
|
|
623
|
+
const breakAfter = (1 << kind & BREAK_AFTER_KINDS) !== 0
|
|
624
|
+
const startGraphemeIndex = i === lineStartSegmentIndex ? lineStartGraphemeIndex : 0
|
|
625
|
+
// The gap before a segment belongs to the grapheme before it. A control or
|
|
626
|
+
// a tab that takes no letter spacing still follows that gap but adds none
|
|
627
|
+
// after itself, and so does an object, which is no character (CSS Text 3,
|
|
628
|
+
// letter-spacing), as is the edge of a padded opening that hangs as white
|
|
629
|
+
// space; other segments that take none leave it as it was.
|
|
630
|
+
const gap = letterSpacing !== 0 && hasContent && !zeroWidthPrefix && !afterUnspacedControl ? letterSpacing : 0
|
|
631
|
+
let leadingSpacing = 0
|
|
632
|
+
if (letterSpacing !== 0 && (spaced || kind === CONTROL || kind === TAB || kind === OBJECT || (openingEdges !== null && openingEdges[i] !== 0))) {
|
|
633
|
+
leadingSpacing = gap
|
|
634
|
+
afterUnspacedControl = !spaced
|
|
635
|
+
}
|
|
636
|
+
if (kind !== ZERO_WIDTH_BREAK && kind !== ZERO_WIDTH_GLUE) zeroWidthPrefix = false
|
|
637
|
+
const w = kind !== TAB ? widths[i]
|
|
638
|
+
: items === undefined ? getTabAdvance(lineW + leadingSpacing, tabStopAdvance, minimumTabAdvance, tabsInAppUnits)
|
|
639
|
+
: getItemTabAdvance(prepared, items, i, hasContent ? lineW + leadingSpacing : lineStartExtras === null ? 0 : lineStartExtras[i] , tabsInAppUnits)
|
|
640
|
+
const advance = leadingSpacing + w
|
|
641
|
+
const endTrim = lineEndTrims === null ? 0 : lineEndTrims[i]
|
|
642
|
+
|
|
643
|
+
if (kind === SOFT_HYPHEN && startGraphemeIndex === 0) {
|
|
644
|
+
if (hasContent) {
|
|
645
|
+
// The hyphen a line that ends here paints, and whether it has to fit. What only a
|
|
646
|
+
// rich-inline paragraph has is under one test, which is all a text's soft hyphen
|
|
647
|
+
// pays for it (RESEARCH.md, Keeping Work Bounded).
|
|
648
|
+
let hyphenWidth = discretionaryHyphenWidth
|
|
649
|
+
let fitsHyphen = true
|
|
650
|
+
if (items !== undefined) {
|
|
651
|
+
// A break comes after an object, so a line returns to it from a soft hyphen right
|
|
652
|
+
// after the object whose hyphen doesn't fit: Chrome, Firefox and Safari break
|
|
653
|
+
// there with the hyphen where it fits, and else after the object.
|
|
654
|
+
if (retreatsFromUnfitHyphen && i > lineStartSegmentIndex && (segmentFlags[i - 1] & KIND_BITS) === OBJECT) {
|
|
655
|
+
fitBreakSegmentIndex = i
|
|
656
|
+
fitBreakPaintWidth = lineW
|
|
209
657
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
lineW += gw;
|
|
214
|
-
lineEndSegmentIndex = segmentIndex;
|
|
215
|
-
lineEndGraphemeIndex = g + 1;
|
|
216
|
-
}
|
|
217
|
-
const graphemeEnd = g + 1;
|
|
218
|
-
if (preferredBreaks !== null && preferredBreaks[preferredBreakIndex] === graphemeEnd) {
|
|
219
|
-
lastPreferredBreakEnd = graphemeEnd;
|
|
220
|
-
lastPreferredBreakWidth = lineW;
|
|
221
|
-
preferredBreakIndex++;
|
|
222
|
-
}
|
|
223
|
-
g++;
|
|
224
|
-
}
|
|
225
|
-
if (hasContent && lineEndSegmentIndex === segmentIndex && lineEndGraphemeIndex === fitAdvances.length) {
|
|
226
|
-
lineEndSegmentIndex = segmentIndex + 1;
|
|
227
|
-
lineEndGraphemeIndex = 0;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
let i = 0;
|
|
231
|
-
while(i < widths.length){
|
|
232
|
-
if (!hasContent) {
|
|
233
|
-
i = normalizeLineStartSegmentIndex(prepared, i);
|
|
234
|
-
if (i >= widths.length) break;
|
|
235
|
-
}
|
|
236
|
-
const w = widths[i];
|
|
237
|
-
const kind = kinds[i];
|
|
238
|
-
const breakAfter = breaksAfter(kind);
|
|
239
|
-
if (!hasContent) {
|
|
240
|
-
if (w > fitLimit && breakableFitAdvances[i] !== null) {
|
|
241
|
-
appendBreakableSegmentFrom(i, 0);
|
|
242
|
-
} else {
|
|
243
|
-
startLineAtSegment(i, w);
|
|
244
|
-
}
|
|
245
|
-
if (breakAfter) {
|
|
246
|
-
pendingBreakSegmentIndex = i + 1;
|
|
247
|
-
pendingBreakPaintWidth = lineW - w;
|
|
248
|
-
}
|
|
249
|
-
i++;
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
const newW = lineW + w;
|
|
253
|
-
if (newW > fitLimit) {
|
|
254
|
-
if (breakAfter) {
|
|
255
|
-
appendWholeSegment(i, w);
|
|
256
|
-
emitCurrentLine(i + 1, 0, lineW - w);
|
|
257
|
-
i++;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
if (pendingBreakSegmentIndex >= 0) {
|
|
261
|
-
if (lineEndSegmentIndex > pendingBreakSegmentIndex || lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0) {
|
|
262
|
-
emitCurrentLine();
|
|
263
|
-
continue;
|
|
658
|
+
if (hangGoesOnPastEmpty && hangEndSegmentIndex === i) {
|
|
659
|
+
hangEndSegmentIndex = i + 1
|
|
660
|
+
hangStartWidth = Math.max(hangStartWidth, Math.min(lineW, availableWidth))
|
|
264
661
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
662
|
+
if (hyphenWidths !== null) hyphenWidth = hyphenWidths[i]
|
|
663
|
+
// A soft hyphen's fit includes its own hyphen, but for one right before an atomic
|
|
664
|
+
// item where the engine never tests that hyphen (EngineProfile).
|
|
665
|
+
fitsHyphen = testsHyphenBeforeAtomic || i + 1 === segmentCount || (segmentFlags[i + 1] & KIND_BITS) !== OBJECT || (openingEdges !== null && openingEdges[i + 1] !== 0)
|
|
666
|
+
}
|
|
667
|
+
lineEndSegmentIndex = i + 1
|
|
668
|
+
lineEndGraphemeIndex = 0
|
|
669
|
+
if (i + 1 < segmentCount && (segmentFlags[i + 1] & KIND_BITS) !== HARD_BREAK) {
|
|
670
|
+
pendingBreakSegmentIndex = i + 1
|
|
671
|
+
pendingBreakWidth = lineW + hyphenWidth
|
|
672
|
+
if (retreatsFromUnfitHyphen && ((fitsHyphen ? pendingBreakWidth : lineW) <= fitLimit || (keepsFirstBreak && fitBreakSegmentIndex < 0))) {
|
|
673
|
+
fitBreakSegmentIndex = pendingBreakSegmentIndex
|
|
674
|
+
fitBreakPaintWidth = pendingBreakWidth
|
|
675
|
+
}
|
|
676
|
+
}
|
|
273
677
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
let lineStartSegmentIndex;
|
|
305
|
-
let lineStartGraphemeIndex;
|
|
306
|
-
let lineW;
|
|
307
|
-
let hasContent;
|
|
308
|
-
let lineEndSegmentIndex;
|
|
309
|
-
let lineEndGraphemeIndex;
|
|
310
|
-
let pendingBreakSegmentIndex;
|
|
311
|
-
let pendingBreakFitWidth;
|
|
312
|
-
let pendingBreakPaintWidth;
|
|
313
|
-
let pendingBreakKind;
|
|
314
|
-
function getCurrentLinePaintWidth() {
|
|
315
|
-
return pendingBreakKind === 'soft-hyphen' && pendingBreakSegmentIndex === lineEndSegmentIndex && lineEndGraphemeIndex === 0 ? pendingBreakPaintWidth : lineW;
|
|
316
|
-
}
|
|
317
|
-
function finishLine(endSegmentIndex = lineEndSegmentIndex, endGraphemeIndex = lineEndGraphemeIndex, width = getCurrentLinePaintWidth()) {
|
|
318
|
-
if (!hasContent) return null;
|
|
319
|
-
cursor.segmentIndex = endSegmentIndex;
|
|
320
|
-
cursor.graphemeIndex = endGraphemeIndex;
|
|
321
|
-
return finalizeLinePaintWidth(prepared, width, lineStartSegmentIndex, lineStartGraphemeIndex, endSegmentIndex, endGraphemeIndex);
|
|
322
|
-
}
|
|
323
|
-
function startLineAtSegment(segmentIndex, width) {
|
|
324
|
-
hasContent = true;
|
|
325
|
-
lineEndSegmentIndex = segmentIndex + 1;
|
|
326
|
-
lineEndGraphemeIndex = 0;
|
|
327
|
-
lineW = width;
|
|
328
|
-
}
|
|
329
|
-
function startLineAtGrapheme(segmentIndex, graphemeIndex, width) {
|
|
330
|
-
hasContent = true;
|
|
331
|
-
lineEndSegmentIndex = segmentIndex;
|
|
332
|
-
lineEndGraphemeIndex = graphemeIndex + 1;
|
|
333
|
-
lineW = width;
|
|
334
|
-
}
|
|
335
|
-
function appendWholeSegment(segmentIndex, advance) {
|
|
336
|
-
if (!hasContent) {
|
|
337
|
-
startLineAtSegment(segmentIndex, advance);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
lineW += advance;
|
|
341
|
-
lineEndSegmentIndex = segmentIndex + 1;
|
|
342
|
-
lineEndGraphemeIndex = 0;
|
|
343
|
-
}
|
|
344
|
-
function updatePendingBreakForWholeSegment(kind, breakAfter, segmentIndex, segmentWidth, leadingSpacing, advance) {
|
|
345
|
-
if (!breakAfter) return;
|
|
346
|
-
const fitAdvance = getBreakOpportunityFitContribution(prepared, kind, segmentIndex, leadingSpacing);
|
|
347
|
-
const paintAdvance = getLineEndPaintContribution(prepared, kind, segmentIndex, leadingSpacing, segmentWidth);
|
|
348
|
-
pendingBreakSegmentIndex = segmentIndex + 1;
|
|
349
|
-
pendingBreakFitWidth = lineW - advance + fitAdvance;
|
|
350
|
-
pendingBreakPaintWidth = lineW - advance + paintAdvance;
|
|
351
|
-
pendingBreakKind = kind;
|
|
352
|
-
}
|
|
353
|
-
function appendBreakableSegmentFrom(segmentIndex, startGraphemeIndex) {
|
|
354
|
-
const fitAdvances = breakableFitAdvances[segmentIndex];
|
|
355
|
-
const preferredBreaks = breakablePreferredBreaks[segmentIndex] ?? null;
|
|
356
|
-
let preferredBreakIndex = preferredBreaks === null ? -1 : getNextPreferredBreakIndex(preferredBreaks, 0, startGraphemeIndex + 1);
|
|
357
|
-
let lastPreferredBreakEnd = -1;
|
|
358
|
-
let lastPreferredBreakWidth = 0;
|
|
359
|
-
for(let g = startGraphemeIndex; g < fitAdvances.length; g++){
|
|
360
|
-
const baseGw = fitAdvances[g];
|
|
361
|
-
if (!hasContent) {
|
|
362
|
-
startLineAtGrapheme(segmentIndex, g, baseGw);
|
|
363
|
-
} else {
|
|
364
|
-
const gw = getBreakableGraphemeAdvance(prepared, true, baseGw);
|
|
365
|
-
const candidatePaintWidth = lineW + gw;
|
|
366
|
-
if (getBreakableCandidateFitWidth(prepared, candidatePaintWidth) > fitLimit) {
|
|
367
|
-
if (preferredBreaks !== null && lastPreferredBreakEnd > startGraphemeIndex) {
|
|
368
|
-
return finishLine(segmentIndex, lastPreferredBreakEnd, lastPreferredBreakWidth);
|
|
369
|
-
}
|
|
370
|
-
return finishLine();
|
|
678
|
+
continue
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// A line that ends after a whole segment charges its advance and the letter
|
|
682
|
+
// spacing gap after it. Spaces and zero-width breaks hang, and zero-width
|
|
683
|
+
// text owns no gap, though NEL does. Text that takes no letter spacing, such
|
|
684
|
+
// as zero-width glue, fits like the line that still ends with the gap before it.
|
|
685
|
+
let fitAdvance = 0
|
|
686
|
+
if (letterSpacing !== 0 && !spaced && !breakAfter && kind !== CONTROL) {
|
|
687
|
+
fitAdvance = gap + w
|
|
688
|
+
} else if (breakAfter ? kind === TAB : w !== 0 || kind === CONTROL) {
|
|
689
|
+
const contribution = w + (spaced ? letterSpacing : 0)
|
|
690
|
+
if (contribution !== 0) fitAdvance = leadingSpacing + contribution
|
|
691
|
+
}
|
|
692
|
+
// Two statements, for how the engines compile them: with them Safari 27 laid pre-wrap text
|
|
693
|
+
// out 4.3% faster than with the same tests as one `||` and Chrome 154 letter-spaced CJK
|
|
694
|
+
// text 2.8%, and Firefox 156 read within 1.5% either way (RESEARCH.md, Keeping Work
|
|
695
|
+
// Bounded, Work Done Only Where A Rule Applies; Decisions Log, 2026-10-08).
|
|
696
|
+
let hangs = (1 << kind & hangingKinds) !== 0
|
|
697
|
+
if (hangGoesOnPastEmpty && kind === ZERO_WIDTH_BREAK && hangEndSegmentIndex === i) hangs = true
|
|
698
|
+
if (hangs) {
|
|
699
|
+
if (hangEndSegmentIndex !== i) {
|
|
700
|
+
hangStartWidth = lineW - lineEndTrimmed + leadingSpacing
|
|
701
|
+
hangStays = false
|
|
702
|
+
if (items !== undefined && i > lineStartSegmentIndex && (segmentFlags[i - 1] & KIND_BITS) === OBJECT) {
|
|
703
|
+
hangStays = staysAfterObject(segmentFlags, i, hangingKinds)
|
|
704
|
+
// A run that goes down with its tab leaves from the break after the object.
|
|
705
|
+
if (!hangStays) {
|
|
706
|
+
pendingBreakSegmentIndex = i
|
|
707
|
+
pendingBreakWidth = lineW - lineEndTrimmed
|
|
371
708
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
lineEndGraphemeIndex = g + 1;
|
|
709
|
+
}
|
|
710
|
+
hangEdgesWidth = 0
|
|
375
711
|
}
|
|
376
|
-
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
preferredBreakIndex++;
|
|
712
|
+
hangEndSegmentIndex = i + 1
|
|
713
|
+
if (openingEdges !== null) {
|
|
714
|
+
hangStartWidth += openingEdges[i]
|
|
715
|
+
hangEdgesWidth += openingEdges[i]
|
|
381
716
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (i + 1 < chunk.endSegmentIndex) {
|
|
422
|
-
pendingBreakSegmentIndex = i + 1;
|
|
423
|
-
pendingBreakFitWidth = lineW + discretionaryHyphenWidth;
|
|
424
|
-
pendingBreakPaintWidth = lineW + discretionaryHyphenWidth;
|
|
425
|
-
pendingBreakKind = kind;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
continue;
|
|
717
|
+
}
|
|
718
|
+
// Where glue can't hold a line, glue at a line start isn't the line's content:
|
|
719
|
+
// the segment after it starts the line, however wide.
|
|
720
|
+
if (!hasContent && kind === ZERO_WIDTH_GLUE && !zeroWidthGlueTakesLine) {
|
|
721
|
+
lineEndSegmentIndex = i + 1
|
|
722
|
+
lineEndGraphemeIndex = 0
|
|
723
|
+
continue
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// A fresh line and a line with content admit a whole segment apart, each
|
|
727
|
+
// with its own copy of what follows admission. One path for both, testing
|
|
728
|
+
// at each step whether the line has content, laid out short lines 3 to 17%
|
|
729
|
+
// slower in all three browsers (RESEARCH.md, Keeping Work Bounded).
|
|
730
|
+
if (!hasContent) {
|
|
731
|
+
if (startGraphemeIndex > 0) {
|
|
732
|
+
fillStart = startGraphemeIndex
|
|
733
|
+
} else {
|
|
734
|
+
const startExtra = lineStartExtras === null ? 0 : lineStartExtras[i]
|
|
735
|
+
// With no break before it on the line, an overflowing segment is laid out
|
|
736
|
+
// by Blink's retry between graphemes, which halts where that lets it fit.
|
|
737
|
+
const startTrim = overflowLineEndTrims !== null && fitAdvance + startExtra - overflowLineEndTrims[i] <= fitLimit
|
|
738
|
+
? overflowLineEndTrims[i]
|
|
739
|
+
: endTrim
|
|
740
|
+
// Preserved spaces hang whole, with the advances rich inline gives them too
|
|
741
|
+
// (hardBreakItemRetreat).
|
|
742
|
+
if (fitAdvance + startExtra - startTrim > fitLimit && breakableFitAdvances[i] !== null && !hangs) {
|
|
743
|
+
fillStart = 0
|
|
744
|
+
} else {
|
|
745
|
+
hasContent = true
|
|
746
|
+
lineEndSegmentIndex = i + 1
|
|
747
|
+
lineEndGraphemeIndex = 0
|
|
748
|
+
lineW = w + startExtra
|
|
749
|
+
// What the line pays at its start stays where the white space it starts with hangs.
|
|
750
|
+
if (hangs) hangStartWidth += startExtra
|
|
751
|
+
lineEndTrimmed = fitAdvance + startExtra > fitLimit && kind !== OBJECT ? startTrim : 0
|
|
752
|
+
// A mark halted at its item's end stays halted where the line goes on (below).
|
|
753
|
+
if (itemEndHalts !== null && lineEndTrimmed !== 0 && itemEndHalts[i] !== 0) {
|
|
754
|
+
lineW -= lineEndTrimmed
|
|
755
|
+
lineEndTrimmed = 0
|
|
429
756
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
break lineLoop;
|
|
436
|
-
}
|
|
437
|
-
} else if (fitAdvance > fitLimit && breakableFitAdvances[i] !== null) {
|
|
438
|
-
const line = appendBreakableSegmentFrom(i, 0);
|
|
439
|
-
if (line !== null) {
|
|
440
|
-
lineWidth = line;
|
|
441
|
-
break lineLoop;
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
startLineAtSegment(i, w);
|
|
445
|
-
}
|
|
446
|
-
updatePendingBreakForWholeSegment(kind, breakAfter, i, w, leadingSpacing, advance);
|
|
447
|
-
continue;
|
|
757
|
+
// The break segment hangs with the gap before it, a run of preserved
|
|
758
|
+
// spaces and tabs hangs whole, and a tab that doesn't hang counts whole.
|
|
759
|
+
if (breakAfter && (i + 1 === segmentCount || (segmentFlags[i + 1] & UNBROKEN) === 0)) {
|
|
760
|
+
pendingBreakSegmentIndex = i + 1
|
|
761
|
+
pendingBreakWidth = hangs ? hangStartWidth : kind === TAB ? lineW : lineW - advance
|
|
448
762
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const currentBreakPaintWidth = lineW + getLineEndPaintContribution(prepared, kind, i, leadingSpacing, w);
|
|
453
|
-
if (breakAfter && currentBreakFitWidth <= fitLimit) {
|
|
454
|
-
appendWholeSegment(i, advance);
|
|
455
|
-
lineWidth = finishLine(i + 1, 0, currentBreakPaintWidth);
|
|
456
|
-
break lineLoop;
|
|
457
|
-
}
|
|
458
|
-
if (pendingBreakSegmentIndex >= 0 && pendingBreakFitWidth <= fitLimit) {
|
|
459
|
-
if (lineEndSegmentIndex > pendingBreakSegmentIndex || lineEndSegmentIndex === pendingBreakSegmentIndex && lineEndGraphemeIndex > 0) {
|
|
460
|
-
lineWidth = finishLine();
|
|
461
|
-
break lineLoop;
|
|
462
|
-
}
|
|
463
|
-
lineWidth = finishLine(pendingBreakSegmentIndex, 0, pendingBreakPaintWidth);
|
|
464
|
-
break lineLoop;
|
|
465
|
-
}
|
|
466
|
-
lineWidth = finishLine();
|
|
467
|
-
break lineLoop;
|
|
763
|
+
if (retreatsFromUnfitHyphen && breakAfter && pendingBreakWidth + (hyphenRooms === null || pendingBreakSegmentIndex < 0 ? reservedHyphenWidth : hyphenRooms[pendingBreakSegmentIndex] ) <= fitLimit) {
|
|
764
|
+
fitBreakSegmentIndex = pendingBreakSegmentIndex
|
|
765
|
+
fitBreakPaintWidth = pendingBreakWidth
|
|
468
766
|
}
|
|
469
|
-
|
|
470
|
-
|
|
767
|
+
continue
|
|
768
|
+
}
|
|
471
769
|
}
|
|
472
|
-
|
|
473
|
-
|
|
770
|
+
} else {
|
|
771
|
+
// A run of preserved spaces and tabs fits where the text before it fits, and after an
|
|
772
|
+
// object however far the line overflows (staysAfterObject).
|
|
773
|
+
const newFitW = hangs ? hangStartWidth - hangEdgesWidth : lineW + fitAdvance
|
|
774
|
+
// Whether the segment ends the line. What only a rich-inline paragraph has is under one
|
|
775
|
+
// test, as at a soft hyphen (above).
|
|
776
|
+
let overflows = newFitW - endTrim > fitLimit
|
|
777
|
+
if (items !== undefined) {
|
|
778
|
+
// Blink and WebKit move an object of width 0 that doesn't fit to the next line as any
|
|
779
|
+
// other. Gecko places an empty frame though it sticks out of the line (CanPlaceFrame,
|
|
780
|
+
// nsLineLayout.cpp:1264-1269; EngineProfile, emptyAtomicAlwaysFits). It sticks out
|
|
781
|
+
// where the content before it ends past the line's end, with the collapsed space
|
|
782
|
+
// before the object, which a line end trims no more once the object follows it
|
|
783
|
+
// (nsLineLayout.cpp:1017-1020), and without the preserved spaces that hang, which
|
|
784
|
+
// end at the line's end (nsTextFrame.cpp:11216-11229). The line then ends before it
|
|
785
|
+
// where the text before it ends in white space and its last frame ends past the
|
|
786
|
+
// line's end without that frame's own spaces, as the line breaks after the white
|
|
787
|
+
// space (nsTextFrame.cpp:11443-11456), and where a break comes before the object
|
|
788
|
+
// and what follows it sends the line back there (ParagraphSegmentData).
|
|
789
|
+
let placesEmptyObject = false
|
|
790
|
+
if (emptyObjectSpaces !== null && kind === OBJECT && w === 0 && newFitW > fitLimit) {
|
|
791
|
+
const contentW = hangEndSegmentIndex === i ? hangStartWidth : lineW
|
|
792
|
+
const space = emptyObjectSpaces[i]
|
|
793
|
+
placesEmptyObject = contentW <= fitLimit || !((space >= 0 && contentW - space > fitLimit) || emptyObjectReturns [i] === 1)
|
|
794
|
+
}
|
|
795
|
+
// No engine lets content make a line narrower (findWholeLine in src/rich-inline.ts),
|
|
796
|
+
// so a segment of less than no advance, as a letter under a letter spacing more
|
|
797
|
+
// negative than it is wide, doesn't bring a line that overflows back.
|
|
798
|
+
const narrowsOverflow = fitAdvance < 0 && lineW - lineEndTrimmed > fitLimit
|
|
799
|
+
overflows = (overflows || narrowsOverflow) && !(hangs && hangStays) && !placesEmptyObject
|
|
800
|
+
if (hangGoesOnPastEmpty && !overflows && hangEndSegmentIndex === i && kind === OBJECT && w === 0) {
|
|
801
|
+
hangEndSegmentIndex = i + 1
|
|
802
|
+
hangStartWidth = Math.max(hangStartWidth, Math.min(lineW + advance, availableWidth))
|
|
803
|
+
}
|
|
474
804
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
805
|
+
if (overflows) {
|
|
806
|
+
// A break segment hangs with the gap before it, after the content before
|
|
807
|
+
// it, which fits without its line-end trim. A collapsible space or ZWSP
|
|
808
|
+
// hangs even after overflowing content that started the line, as the
|
|
809
|
+
// simple stepper does; a preserved space there starts the next line. A
|
|
810
|
+
// tab that doesn't hang goes to the next line, as text does. A ZWSP also
|
|
811
|
+
// hangs right after an object, as after the start edge of its item's opening,
|
|
812
|
+
// which the line took by the part it fits; after a chip wider than the line
|
|
813
|
+
// that is Firefox's line, where Chrome and Safari give a ZWSP that ends the
|
|
814
|
+
// paragraph a line of its own (ENGINE_FOLLOWUPS.md, Rich-inline item edges).
|
|
815
|
+
const contentW = lineW - lineEndTrimmed
|
|
816
|
+
if (breakAfter && (hangs || kind !== TAB) && (contentW <= fitLimit ||
|
|
817
|
+
(pendingBreakSegmentIndex < 0 && (kind === SPACE || kind === ZERO_WIDTH_BREAK)) ||
|
|
818
|
+
(kind === ZERO_WIDTH_BREAK && (segmentFlags[i - 1] & KIND_BITS) === OBJECT))) {
|
|
819
|
+
endWidth = hangs ? hangStartWidth : contentW
|
|
820
|
+
lineW += advance
|
|
821
|
+
endSegmentIndex = i + 1
|
|
822
|
+
endGraphemeIndex = 0
|
|
823
|
+
break decided
|
|
824
|
+
}
|
|
825
|
+
// Gecko's line returns from a tab that doesn't fit to its latest break (below), as
|
|
826
|
+
// BreakAndMeasureText keeps the last break whose line fits (gfxTextRun.cpp:1086-1101).
|
|
827
|
+
// Without one it wraps before the tab, as break-word lets it before any cluster
|
|
828
|
+
// (:1069-1072), and the spaces before the tab hang.
|
|
829
|
+
if (kind === TAB && !hangs && pendingBreakSegmentIndex < 0) {
|
|
830
|
+
endSegmentIndex = i
|
|
831
|
+
endGraphemeIndex = 0
|
|
832
|
+
endWidth = hangEndSegmentIndex === i && i > lineStartSegmentIndex ? hangStartWidth : contentW
|
|
833
|
+
break decided
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const unbroken = (flags & UNBROKEN) !== 0
|
|
837
|
+
// An object with no break before it is the start edge of a padded rich-inline
|
|
838
|
+
// item's opening (src/rich-inline.ts). Blink's line trails once the preserved
|
|
839
|
+
// spaces it ends with overflow: it takes the white space after them, the tags of
|
|
840
|
+
// spans that open among it and a forced break with no fit (HandleTrailingSpaces,
|
|
841
|
+
// line_breaker.cc:2426-2534), so an edge right after such a run joins it, taking
|
|
842
|
+
// no room. The spaces overflow where the content before them fits with the start
|
|
843
|
+
// edges of the spans that open among them, half of each such edge (getOpeningFit),
|
|
844
|
+
// and the spaces after those edges don't; an overflow before the spaces or at a
|
|
845
|
+
// span's end edge returns instead. WebKit fits the content it places without the
|
|
846
|
+
// white space that hangs before it (hangingContentWidth,
|
|
847
|
+
// InlineContentBreaker.cpp:183-186), so there the edge joins the run where it fits
|
|
848
|
+
// after the content before the run.
|
|
849
|
+
if (kind === OBJECT && unbroken && hangEndSegmentIndex === i && (paddedOpeningFit === 'start'
|
|
850
|
+
? hangStartWidth - hangEdgesWidth / 2 <= fitLimit && lineW - hangEdgesWidth / 2 > fitLimit
|
|
851
|
+
: paddedOpeningFit === 'placed' && hangStartWidth - hangEdgesWidth + w - endTrim <= fitLimit)) {
|
|
852
|
+
hangEndSegmentIndex = i + 1
|
|
853
|
+
hangStartWidth += w
|
|
854
|
+
hangEdgesWidth += w
|
|
855
|
+
lineW += advance
|
|
856
|
+
lineEndSegmentIndex = i + 1
|
|
857
|
+
lineEndGraphemeIndex = 0
|
|
858
|
+
continue
|
|
859
|
+
}
|
|
860
|
+
// Where the scan gives no break before the segment, as before NEL (UAX #14
|
|
861
|
+
// LB6), the line returns to its last break. Without one, Blink and WebKit retry
|
|
862
|
+
// between graphemes, so the segment's graphemes fill it.
|
|
863
|
+
if (unbroken && pendingBreakSegmentIndex >= 0) {
|
|
864
|
+
lineEndSegmentIndex = pendingBreakSegmentIndex
|
|
865
|
+
lineEndGraphemeIndex = 0
|
|
866
|
+
}
|
|
867
|
+
if (!unbroken || pendingBreakSegmentIndex >= 0) {
|
|
868
|
+
returnsFromHyphen = true
|
|
869
|
+
break decided
|
|
870
|
+
}
|
|
871
|
+
// Blink's retry halts the segment where that lets it fit, and the line ends after it,
|
|
872
|
+
// but where the segment ends its item, whose mark stays halted while the line goes on.
|
|
873
|
+
if (overflowLineEndTrims !== null && newFitW - overflowLineEndTrims[i] <= fitLimit) {
|
|
874
|
+
lineW += advance
|
|
875
|
+
lineEndSegmentIndex = i + 1
|
|
876
|
+
lineEndGraphemeIndex = 0
|
|
877
|
+
lineEndTrimmed = overflowLineEndTrims[i]
|
|
878
|
+
if (itemEndHalts !== null && itemEndHalts[i] !== 0) {
|
|
879
|
+
lineW -= lineEndTrimmed
|
|
880
|
+
lineEndTrimmed = 0
|
|
881
|
+
}
|
|
882
|
+
continue
|
|
883
|
+
}
|
|
884
|
+
// Where the line of such an edge has no break to return to, the line ends before
|
|
885
|
+
// it, but before a hard break only in Blink, whose retry breaks between any two
|
|
886
|
+
// graphemes: WebKit and Gecko end the line before the last grapheme of the text
|
|
887
|
+
// before the edge, and keep the edge on a line that grapheme starts (EngineProfile,
|
|
888
|
+
// hardBreakItemRetreat). Of preserved spaces there, whose advances rich inline
|
|
889
|
+
// gives, Gecko moves the last and WebKit those that don't fit: all of them where a
|
|
890
|
+
// negative letter spacing leaves a space no advance, so that giving one up gives no
|
|
891
|
+
// overflow back. The ones the line keeps hang.
|
|
892
|
+
if (kind === OBJECT && hardBreakItemRetreat !== 'item' && i + 1 < segmentCount && (segmentFlags[i + 1] & KIND_BITS) === HARD_BREAK) {
|
|
893
|
+
const beforeFlags = segmentFlags[i - 1]
|
|
894
|
+
const beforeKind = beforeFlags & KIND_BITS
|
|
895
|
+
const spaceAdvances = beforeKind === PRESERVED_SPACE ? breakableFitAdvances[i - 1] : null
|
|
896
|
+
if (spaceAdvances !== null) {
|
|
897
|
+
const count = spaceAdvances.length
|
|
898
|
+
const spaceAdvance = spaceAdvances[0]
|
|
899
|
+
const moved = hardBreakItemRetreat !== 'fit' || lineW <= fitLimit ? 1 : spaceAdvance > 0 ? Math.min(count, Math.ceil((lineW - fitLimit) / spaceAdvance)) : count
|
|
900
|
+
// The start edge of the spaces' own item is no content to leave on the line.
|
|
901
|
+
const contentStart = openingEdges !== null && openingEdges[lineStartSegmentIndex] !== 0 ? lineStartSegmentIndex + 1 : lineStartSegmentIndex
|
|
902
|
+
if (i - 1 > contentStart || count - moved > lineStartGraphemeIndex) {
|
|
903
|
+
endSegmentIndex = i - 1
|
|
904
|
+
endGraphemeIndex = count - moved
|
|
905
|
+
endWidth = hangStartWidth
|
|
906
|
+
break decided
|
|
907
|
+
}
|
|
520
908
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
cursor.segmentIndex = lineEndSegmentIndex;
|
|
530
|
-
cursor.graphemeIndex = lineEndGraphemeIndex;
|
|
531
|
-
return lineW;
|
|
532
|
-
}
|
|
533
|
-
lineW += gw;
|
|
534
|
-
lineEndSegmentIndex = i;
|
|
535
|
-
lineEndGraphemeIndex = g + 1;
|
|
536
|
-
if (preferredBreaks !== null && preferredBreaks[preferredBreakIndex] === lineEndGraphemeIndex) {
|
|
537
|
-
lastPreferredBreakEnd = lineEndGraphemeIndex;
|
|
538
|
-
lastPreferredBreakWidth = lineW;
|
|
539
|
-
preferredBreakIndex++;
|
|
540
|
-
}
|
|
909
|
+
const beforeAdvances = beforeKind === TEXT || beforeKind === CONTROL ? breakableFitAdvances[i - 1] : null
|
|
910
|
+
const spacing = (beforeFlags & SPACED) !== 0 ? letterSpacing : 0
|
|
911
|
+
if (beforeAdvances !== null && (i - 1 > lineStartSegmentIndex || beforeAdvances.length - 1 > lineStartGraphemeIndex)) {
|
|
912
|
+
endSegmentIndex = i - 1
|
|
913
|
+
endGraphemeIndex = beforeAdvances.length - 1
|
|
914
|
+
endWidth = lineW - beforeAdvances[endGraphemeIndex] - spacing
|
|
915
|
+
break decided
|
|
541
916
|
}
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
917
|
+
if (beforeAdvances === null && (beforeKind === TEXT || beforeKind === CONTROL) && i - 1 > lineStartSegmentIndex) {
|
|
918
|
+
endSegmentIndex = i - 1
|
|
919
|
+
endGraphemeIndex = 0
|
|
920
|
+
endWidth = lineW - widths[i - 1] - spacing
|
|
921
|
+
break decided
|
|
545
922
|
}
|
|
923
|
+
if (spaceAdvances !== null || (beforeKind !== PRESERVED_SPACE && beforeKind !== TAB)) {
|
|
924
|
+
lineW += advance
|
|
925
|
+
lineEndSegmentIndex = i + 1
|
|
926
|
+
lineEndGraphemeIndex = 0
|
|
927
|
+
lineEndTrimmed = 0
|
|
928
|
+
continue
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (breakableFitAdvances[i] === null) {
|
|
932
|
+
returnsFromHyphen = true
|
|
933
|
+
break decided
|
|
934
|
+
}
|
|
935
|
+
fillStart = 0
|
|
936
|
+
fillSpacing = leadingSpacing
|
|
546
937
|
} else {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
938
|
+
// A break the scan gives before text is one the line can return to.
|
|
939
|
+
if ((flags & RETURNABLE) !== 0 && !breakAfter && pendingBreakSegmentIndex !== i) {
|
|
940
|
+
pendingBreakSegmentIndex = i
|
|
941
|
+
pendingBreakWidth = lineW
|
|
942
|
+
}
|
|
943
|
+
// A break the scan gives between two text segments, as after `-` or between
|
|
944
|
+
// ideographs, is an opportunity the line returns to like any other. The line up
|
|
945
|
+
// to it fit when its last segment was admitted, with the letter-spacing gap after
|
|
946
|
+
// it that lineW leaves out, so only Blink's room for the hyphen is tested here.
|
|
947
|
+
if (retreatsFromUnfitHyphen && !breakAfter && (flags & UNBROKEN) === 0 && i > lineStartSegmentIndex &&
|
|
948
|
+
!breaksAfterKind(segmentFlags[i - 1] & KIND_BITS) && (!reservesHyphenWidth || lineW + (hyphenRooms === null ? discretionaryHyphenWidth : hyphenRooms[i] ) <= fitLimit)) {
|
|
949
|
+
fitBreakSegmentIndex = i
|
|
950
|
+
fitBreakPaintWidth = lineW
|
|
951
|
+
}
|
|
952
|
+
lineEndSegmentIndex = i + 1
|
|
953
|
+
lineEndGraphemeIndex = 0
|
|
954
|
+
if (itemEndHalts !== null && newFitW > fitLimit && itemEndHalts[i] !== 0) {
|
|
955
|
+
// A closing mark that ends its item and fits the line only halted stays halted where
|
|
956
|
+
// the line goes on: the line takes it at its halted width, and what follows fits
|
|
957
|
+
// after that (ParagraphSegmentData, itemEndHalts).
|
|
958
|
+
lineW += advance - endTrim
|
|
959
|
+
lineEndTrimmed = 0
|
|
960
|
+
} else {
|
|
961
|
+
lineW += advance
|
|
962
|
+
// A segment that takes no room at the line end, as a space, leaves the glyph
|
|
963
|
+
// before it last on the line, with its trim.
|
|
964
|
+
if (fitAdvance !== 0 && !hangs) lineEndTrimmed = newFitW > fitLimit && kind !== OBJECT ? endTrim : 0
|
|
965
|
+
}
|
|
966
|
+
if (breakAfter && (i + 1 === segmentCount || (segmentFlags[i + 1] & UNBROKEN) === 0)) {
|
|
967
|
+
pendingBreakSegmentIndex = i + 1
|
|
968
|
+
pendingBreakWidth = hangs ? hangStartWidth : kind === TAB ? lineW : lineW - advance - lineEndTrimmed
|
|
969
|
+
}
|
|
970
|
+
if (retreatsFromUnfitHyphen && breakAfter && pendingBreakWidth + (hyphenRooms === null || pendingBreakSegmentIndex < 0 ? reservedHyphenWidth : hyphenRooms[pendingBreakSegmentIndex] ) <= fitLimit) {
|
|
971
|
+
fitBreakSegmentIndex = pendingBreakSegmentIndex
|
|
972
|
+
fitBreakPaintWidth = pendingBreakWidth
|
|
973
|
+
}
|
|
974
|
+
continue
|
|
551
975
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// A grapheme fits with the letter spacing after it.
|
|
979
|
+
const fitAdvances = breakableFitAdvances[i]
|
|
980
|
+
const fitCount = fitAdvances.length
|
|
981
|
+
const entry = entryGeometry === null ? null : entryGeometry[i]
|
|
982
|
+
// Entry geometry describes whole segment tails on a fresh line.
|
|
983
|
+
const freshWhole = hasContent ? null : getSegmentEntryWidth(entry, fillStart, fitCount)
|
|
984
|
+
if (freshWhole !== null) {
|
|
985
|
+
// This branch reads a width for each question: in getFreshLineEnd(), the entry's
|
|
986
|
+
// admissionFit for whether the whole tail fits and the fresh prefixes for where
|
|
987
|
+
// the line ends where it doesn't; then the tail's fresh width, freshWhole, for the
|
|
988
|
+
// line that goes on. The line takes its first grapheme even where that doesn't
|
|
989
|
+
// fit, so it advances.
|
|
990
|
+
const end = getFreshLineEnd(entry , fillStart, fitCount, fitLimit)
|
|
991
|
+
hasContent = true
|
|
992
|
+
if (end <= fitCount) {
|
|
993
|
+
lineEndSegmentIndex = i
|
|
994
|
+
lineEndGraphemeIndex = end
|
|
995
|
+
lineW = getSegmentEntryWidth(entry, fillStart, end) - letterSpacing
|
|
996
|
+
// Where the fresh prefixes reach the segment's end though the whole tail wasn't
|
|
997
|
+
// admitted, the line ends after the segment and takes nothing after it. Only
|
|
998
|
+
// an admitted tail lets the line go on with the text after it (below).
|
|
999
|
+
if (end === fitCount) {
|
|
1000
|
+
endSegmentIndex = i + 1
|
|
1001
|
+
endGraphemeIndex = 0
|
|
1002
|
+
endWidth = lineW - lineEndTrimmed
|
|
1003
|
+
}
|
|
1004
|
+
break decided
|
|
555
1005
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1006
|
+
lineEndSegmentIndex = i + 1
|
|
1007
|
+
lineEndGraphemeIndex = 0
|
|
1008
|
+
lineW = freshWhole - letterSpacing
|
|
1009
|
+
} else {
|
|
1010
|
+
for (let g = fillStart; g < fitCount; g++) {
|
|
1011
|
+
const baseGw = fitAdvances[g]
|
|
1012
|
+
if (!hasContent) {
|
|
1013
|
+
hasContent = true
|
|
1014
|
+
lineEndSegmentIndex = i
|
|
1015
|
+
lineEndGraphemeIndex = g + 1
|
|
1016
|
+
const startExtras = prepared.breakableLineStartExtras?.[i] ?? null
|
|
1017
|
+
lineW = startExtras === null ? baseGw : baseGw + startExtras[g]
|
|
1018
|
+
// A line that starts inside a padded rich-inline item, or fills its first
|
|
1019
|
+
// segment, pays the item's extraWidth (ParagraphSegmentData).
|
|
1020
|
+
if (g > 0 ? insideExtras !== null : fillExtras !== null) lineW += g > 0 ? insideExtras [i] : fillExtras [i]
|
|
1021
|
+
// A line that holds only this grapheme, overflowing, keeps the graphemes after
|
|
1022
|
+
// it that can't start a line, and ends.
|
|
1023
|
+
const end = lineW + letterSpacing > fitLimit
|
|
1024
|
+
? getOverflowingFirstGraphemeEnd(prepared, i, g, fitCount)
|
|
1025
|
+
: g + 1
|
|
1026
|
+
if (end > g + 1) {
|
|
1027
|
+
for (let k = g + 1; k < end; k++) lineW += fitAdvances[k] + letterSpacing
|
|
1028
|
+
endSegmentIndex = end === fitCount ? i + 1 : i
|
|
1029
|
+
endGraphemeIndex = end === fitCount ? 0 : end
|
|
1030
|
+
endWidth = lineW - lineEndTrimmed
|
|
1031
|
+
break decided
|
|
569
1032
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1033
|
+
} else {
|
|
1034
|
+
const candidatePaintWidth = lineW + (baseGw + (g > fillStart ? letterSpacing : fillSpacing))
|
|
1035
|
+
if (candidatePaintWidth + letterSpacing > fitLimit) break decided
|
|
1036
|
+
lineW = candidatePaintWidth
|
|
1037
|
+
lineEndSegmentIndex = i
|
|
1038
|
+
lineEndGraphemeIndex = g + 1
|
|
1039
|
+
}
|
|
573
1040
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
1041
|
+
}
|
|
1042
|
+
if (hasContent && lineEndSegmentIndex === i && lineEndGraphemeIndex === fitCount) {
|
|
1043
|
+
lineEndSegmentIndex = i + 1
|
|
1044
|
+
lineEndGraphemeIndex = 0
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// A line that ends where its source runs out at an unfit selected hyphen
|
|
1049
|
+
// returns as well.
|
|
1050
|
+
endSegmentIndex = consumedEndSegmentIndex
|
|
1051
|
+
endGraphemeIndex = 0
|
|
1052
|
+
if (pendingBreakSegmentIndex === consumedEndSegmentIndex && lineEndGraphemeIndex === 0) {
|
|
1053
|
+
endWidth = pendingBreakWidth
|
|
1054
|
+
returnsFromHyphen = true
|
|
1055
|
+
} else {
|
|
1056
|
+
endWidth = lineW - lineEndTrimmed
|
|
577
1057
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (hasContent) {
|
|
1061
|
+
if (
|
|
1062
|
+
returnsFromHyphen &&
|
|
1063
|
+
fitBreakSegmentIndex >= 0 &&
|
|
1064
|
+
pendingBreakSegmentIndex === lineEndSegmentIndex &&
|
|
1065
|
+
lineEndGraphemeIndex === 0 &&
|
|
1066
|
+
returnsFromUnfitHyphen(prepared, lineStartSegmentIndex, lineEndSegmentIndex, pendingBreakWidth, fitLimit)
|
|
1067
|
+
) {
|
|
1068
|
+
endSegmentIndex = fitBreakSegmentIndex
|
|
1069
|
+
endGraphemeIndex = 0
|
|
1070
|
+
endWidth = fitBreakPaintWidth
|
|
1071
|
+
} else if (endSegmentIndex < 0) {
|
|
1072
|
+
// A line that ends at its pending break paints the pending width, and one that wraps
|
|
1073
|
+
// right after a run of preserved spaces and tabs hangs it, with no break there too.
|
|
1074
|
+
endSegmentIndex = lineEndSegmentIndex
|
|
1075
|
+
endGraphemeIndex = lineEndGraphemeIndex
|
|
1076
|
+
endWidth = pendingBreakSegmentIndex === lineEndSegmentIndex && lineEndGraphemeIndex === 0 ? pendingBreakWidth
|
|
1077
|
+
: items !== undefined && hangEndSegmentIndex === lineEndSegmentIndex && lineEndGraphemeIndex === 0 ? hangStartWidth
|
|
1078
|
+
: lineW - lineEndTrimmed
|
|
584
1079
|
}
|
|
1080
|
+
cursor.segmentIndex = endSegmentIndex
|
|
1081
|
+
cursor.graphemeIndex = endGraphemeIndex
|
|
1082
|
+
// Preserved spaces and tabs before a hard break or the end of the text
|
|
1083
|
+
// hang only where they don't fit (CSS Text 3 §8.2).
|
|
1084
|
+
const hangsWhereUnfit =
|
|
1085
|
+
endGraphemeIndex === 0 &&
|
|
1086
|
+
hangEndSegmentIndex >= 0 &&
|
|
1087
|
+
(endSegmentIndex === hangEndSegmentIndex || endSegmentIndex === hangEndSegmentIndex + 1) &&
|
|
1088
|
+
(hangEndSegmentIndex === segmentCount || (segmentFlags[hangEndSegmentIndex] & KIND_BITS) === HARD_BREAK)
|
|
1089
|
+
const paintWidth = (hangsWhereUnfit ? lineW - lineEndTrimmed : endWidth) +
|
|
1090
|
+
getTerminalLetterSpacing(prepared, openingEdges, hangingKinds, lineStartSegmentIndex, lineStartGraphemeIndex, endSegmentIndex, endGraphemeIndex)
|
|
1091
|
+
lineWidth = hangsWhereUnfit ? Math.max(hangStartWidth, Math.min(paintWidth, availableWidth)) : paintWidth
|
|
1092
|
+
}
|
|
585
1093
|
}
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
export function stepPreparedLineGeometryFromChunk(prepared, cursor, chunkIndex, maxWidth) {
|
|
592
|
-
if (prepared.simpleLineWalkFastPath) {
|
|
593
|
-
return stepPreparedSimpleLineGeometry(prepared, cursor, maxWidth);
|
|
1094
|
+
if (lineWidth === null) break
|
|
1095
|
+
lastLineWidth = lineWidth
|
|
1096
|
+
if (stats !== null) {
|
|
1097
|
+
stats.lineCount++
|
|
1098
|
+
if (lineWidth > stats.maxLineWidth) stats.maxLineWidth = lineWidth
|
|
594
1099
|
}
|
|
595
|
-
|
|
1100
|
+
onLine?.(lineWidth, lineStartSegmentIndex, lineStartGraphemeIndex, cursor.segmentIndex, cursor.graphemeIndex)
|
|
1101
|
+
// A single-line caller owns normalization of the following line.
|
|
1102
|
+
if (singleLine || !normalizePreparedLineStart(prepared, cursor)) break
|
|
1103
|
+
}
|
|
1104
|
+
return lastLineWidth
|
|
596
1105
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1106
|
+
|
|
1107
|
+
// Steps one line of a fast-path handle from a normalized line start.
|
|
1108
|
+
function stepPreparedSimpleLineGeometry(
|
|
1109
|
+
prepared ,
|
|
1110
|
+
cursor ,
|
|
1111
|
+
maxWidth ,
|
|
1112
|
+
) {
|
|
1113
|
+
const { widths, segmentFlags, breakableFitAdvances, entryGeometry, lineStartExtras, lineEndTrims, overflowLineEndTrims } = prepared
|
|
1114
|
+
// A negative width lays out as 0, as in the complex walker.
|
|
1115
|
+
const fitLimit = Math.max(0, maxWidth) + getEngineProfile().lineFitEpsilon
|
|
1116
|
+
const start = cursor.segmentIndex
|
|
1117
|
+
|
|
1118
|
+
// The first segment of the line, or the rest of one a line ended inside. One that
|
|
1119
|
+
// overflows and can break fills the line grapheme by grapheme.
|
|
1120
|
+
const startAdvances = breakableFitAdvances[start]
|
|
1121
|
+
const startW = lineStartExtras === null ? widths[start] : widths[start] + lineStartExtras[start]
|
|
1122
|
+
const startEndTrim = lineEndTrims === null ? 0 : lineEndTrims[start]
|
|
1123
|
+
// Blink's retry between graphemes halts an overflowing first segment where that lets it fit.
|
|
1124
|
+
const startTrim = overflowLineEndTrims !== null && startW - overflowLineEndTrims[start] <= fitLimit
|
|
1125
|
+
? overflowLineEndTrims[start]
|
|
1126
|
+
: startEndTrim
|
|
1127
|
+
// A line that starts inside a segment where it has fresh-line geometry takes the
|
|
1128
|
+
// tail, and goes on, or its fresh prefixes.
|
|
1129
|
+
const entry = cursor.graphemeIndex > 0 && entryGeometry !== null ? entryGeometry[start] : null
|
|
1130
|
+
let lineW
|
|
1131
|
+
// The line-end trim of the last segment, where only that trim let it fit. Every
|
|
1132
|
+
// later segment overflows, so the line ends after it and paints that much less.
|
|
1133
|
+
let endTrimmed = 0
|
|
1134
|
+
if (entry !== null && entry.entries[cursor.graphemeIndex] !== null) {
|
|
1135
|
+
const segmentEnd = startAdvances .length
|
|
1136
|
+
const end = getFreshLineEnd(entry, cursor.graphemeIndex, segmentEnd, fitLimit)
|
|
1137
|
+
lineW = getSegmentEntryWidth(entry, cursor.graphemeIndex, Math.min(end, segmentEnd))
|
|
1138
|
+
if (end <= segmentEnd) {
|
|
1139
|
+
cursor.segmentIndex = end === segmentEnd ? start + 1 : start
|
|
1140
|
+
cursor.graphemeIndex = end === segmentEnd ? 0 : end
|
|
1141
|
+
return lineW
|
|
608
1142
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1143
|
+
} else if (cursor.graphemeIndex > 0 || (startW - startTrim > fitLimit && startAdvances !== null)) {
|
|
1144
|
+
const fitAdvances = startAdvances
|
|
1145
|
+
const startExtras = prepared.breakableLineStartExtras?.[start] ?? null
|
|
1146
|
+
let g = cursor.graphemeIndex + 1
|
|
1147
|
+
lineW = startExtras === null ? fitAdvances[g - 1] : fitAdvances[g - 1] + startExtras[g - 1]
|
|
1148
|
+
// A line that starts inside a padded rich-inline item, or fills its first segment, pays
|
|
1149
|
+
// the item's extraWidth (ParagraphSegmentData).
|
|
1150
|
+
const items = prepared.items
|
|
1151
|
+
if (items !== undefined && (g > 1 ? items.insideExtras !== null : items.fillExtras !== null)) lineW += g > 1 ? items.insideExtras [start] : items.fillExtras [start]
|
|
1152
|
+
// A line that holds only an overflowing grapheme keeps the graphemes after it
|
|
1153
|
+
// that can't start a line, and ends.
|
|
1154
|
+
const overflowEnd = lineW > fitLimit ? getOverflowingFirstGraphemeEnd(prepared, start, g - 1, fitAdvances.length) : g
|
|
1155
|
+
if (overflowEnd > g) {
|
|
1156
|
+
for (; g < overflowEnd; g++) lineW += fitAdvances[g]
|
|
1157
|
+
cursor.segmentIndex = g === fitAdvances.length ? start + 1 : start
|
|
1158
|
+
cursor.graphemeIndex = g === fitAdvances.length ? 0 : g
|
|
1159
|
+
return lineW
|
|
624
1160
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
lineCount++;
|
|
634
|
-
if (lineWidth > maxLineWidth) maxLineWidth = lineWidth;
|
|
1161
|
+
for (; g < fitAdvances.length; g++) {
|
|
1162
|
+
if (lineW + fitAdvances[g] > fitLimit) {
|
|
1163
|
+
cursor.graphemeIndex = g
|
|
1164
|
+
return lineW
|
|
1165
|
+
}
|
|
1166
|
+
lineW += fitAdvances[g]
|
|
635
1167
|
}
|
|
1168
|
+
} else {
|
|
1169
|
+
lineW = startW
|
|
1170
|
+
if (startW > fitLimit) endTrimmed = startTrim
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Every boundary of a fast-path handle is a break, so the line takes whole
|
|
1174
|
+
// segments until one overflows. An overflowing space or ZWSP hangs and ends the
|
|
1175
|
+
// line; before other text, the line leaves out a space or ZWSP it ends with.
|
|
1176
|
+
for (let i = start + 1; i < widths.length; i++) {
|
|
1177
|
+
const w = widths[i]
|
|
1178
|
+
const endTrim = lineEndTrims === null ? 0 : lineEndTrims[i]
|
|
1179
|
+
if (lineW + w - endTrim > fitLimit) {
|
|
1180
|
+
const hangs = breaksAfterKind(segmentFlags[i] & KIND_BITS)
|
|
1181
|
+
cursor.segmentIndex = hangs ? i + 1 : i
|
|
1182
|
+
cursor.graphemeIndex = 0
|
|
1183
|
+
return !hangs && breaksAfterKind(segmentFlags[i - 1] & KIND_BITS) ? lineW - widths[i - 1] : lineW - endTrimmed
|
|
1184
|
+
}
|
|
1185
|
+
lineW += w
|
|
1186
|
+
endTrimmed = lineW > fitLimit ? endTrim : 0
|
|
1187
|
+
}
|
|
1188
|
+
cursor.segmentIndex = widths.length
|
|
1189
|
+
cursor.graphemeIndex = 0
|
|
1190
|
+
return lineW - endTrimmed
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Steps one line from a normalized line start. A cursor inside a segment needs that segment's
|
|
1194
|
+
// breakable fit advances.
|
|
1195
|
+
export function stepPreparedLineGeometryFromStart(
|
|
1196
|
+
prepared ,
|
|
1197
|
+
cursor ,
|
|
1198
|
+
maxWidth ,
|
|
1199
|
+
) {
|
|
1200
|
+
if (prepared.simpleLineWalkFastPath) return stepPreparedSimpleLineGeometry(prepared, cursor, maxWidth)
|
|
1201
|
+
return walkPreparedComplexLines(prepared, cursor, maxWidth, undefined, null, true)
|
|
636
1202
|
}
|