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
@@ -0,0 +1,688 @@
1
+ /*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
2
+ // Break opportunities as Firefox finds them: a port of how Gecko transforms a text node's
3
+ // white space, sets up its text run and breaks its words with nsLineBreaker over ICU4X's
4
+ // line iterator, with Firefox's baked line data and the Unicode properties Gecko reads,
5
+ // which scripts/generate-engine-break-data.ts writes to src/generated/engine-break-data.ts.
6
+ //
7
+ // Sources, cited as file:line:
8
+ // - Gecko in mozilla-firefox at Firefox 156.0 (3bf8f4682).
9
+ // - icu_segmenter 2.1.2 src/line.rs, byte-identical to Firefox's third_party/rust copy.
10
+ //
11
+ // Deliberate differences:
12
+ // - Grapheme clusters come from the profile's ICU character rules (src/graphemes.ts), Chrome's,
13
+ // which give the clusters of icu_segmenter's GraphemeClusterSegmenter over Firefox's data, and
14
+ // Unicode properties from RegExp \p{...} and generated tables.
15
+ // - Text runs don't split where the script changes (gfxScriptItemizer.cpp), nor where the bidi
16
+ // level does: bidi resolution makes a text frame of each level run (nsBidiPresUtils.cpp:
17
+ // 1037-1053), and a text run doesn't go on across two (ContinueTextRunAcrossFrames,
18
+ // nsTextFrame.cpp:2023-2030). Such a split only adds a cluster start, restarts clusters within
19
+ // its word and ends a word after a space before a cluster extender, so a level split is missed
20
+ // only inside a cluster, or after a space inside one: for example at a vowel killer of a
21
+ // left-to-right script after an Arabic letter, or at a ZWJ that ends the paragraph after a
22
+ // Hebrew letter, which rule L1 puts at the paragraph's level. Both ports were removed on
23
+ // purpose (RESEARCH.md, Decisions Log, 2026-09-24 and 2026-10-01; ENGINE_FOLLOWUPS.md, Bidi
24
+ // levels, direction and script runs).
25
+ // - Inside runs of Thai, Lao, Khmer and Myanmar letters, Intl.Segmenter word boundaries stand
26
+ // in for ICU4X's LSTM models (line.rs:445-451, complex/mod.rs:135-156). Firefox's own
27
+ // Intl.Segmenter answers as those models there once breaks inside grapheme clusters are
28
+ // dropped, which setPotentialLineBreaks does.
29
+ // - The paragraph is left-to-right, since Pretext takes no direction, and word-break is
30
+ // normal or keep-all under Strict line breaking, which line-break: auto selects
31
+ // (intl/lwbrk/LineBreaker.cpp:26-31).
32
+
33
+ import {
34
+ geckoLineBreakStatesPacked,
35
+ geckoLineEotProperty,
36
+ geckoLineLastCodepointProperty,
37
+ geckoLinePropertyCount,
38
+ } from './generated/engine-break-data.js'
39
+ import { findGraphemeEnds, isBidiControl, } from './graphemes.js'
40
+ import {
41
+ BREAK as OPPORTUNITY,
42
+ CLUSTER_START,
43
+ DEFAULT_IGNORABLE,
44
+ EMOJI,
45
+ HANGUL,
46
+ MARK,
47
+ PUNCTUATION,
48
+ SOFT_HYPHEN_BREAK,
49
+ getBreakLanguage,
50
+ getClass,
51
+ getWordSegmenter,
52
+ hasProperty,
53
+ unpackClasses,
54
+ unpackTable,
55
+
56
+ } from './line-breaks.js'
57
+
58
+ const CH_SHY = 0x00ad
59
+
60
+ const isSurrogatePair = (a , b ) => (a & 0xfc00) === 0xd800 && (b & 0xfc00) === 0xdc00
61
+ const combine = (a , b ) => 0x10000 + ((a - 0xd800) << 10) + (b - 0xdc00)
62
+
63
+ // East_Asian_Width H (2), F (3) or W (5), and 0 for any other value, which the first East Asian
64
+ // segment break test unpacks (isEastAsianSegmentBreak).
65
+ let eastAsianWidths = null
66
+
67
+ // --- Character classes ---
68
+
69
+ // nsUnicodeProperties.h:202-207, nsUnicodeProperties.cpp:130-138
70
+ function isClusterExtender(cp ) {
71
+ return cp >= 0x0300 && (hasProperty(cp, MARK) || cp === 0x200c || cp === 0x200d || (cp >= 0xff9e && cp <= 0xff9f) ||
72
+ (cp >= 0x1f3fb && cp <= 0x1f3ff) || (cp >= 0xe0020 && cp <= 0xe007f))
73
+ }
74
+
75
+ // nsUnicodeProperties.h:211-214, nsUnicodeProperties.cpp:140-147
76
+ function isClusterExtenderExcludingJoiners(cp ) {
77
+ return cp >= 0x0300 && (hasProperty(cp, MARK) || (cp >= 0xff9e && cp <= 0xff9f) || (cp >= 0x1f3fb && cp <= 0x1f3ff) ||
78
+ (cp >= 0xe0020 && cp <= 0xe007f))
79
+ }
80
+
81
+ // UnicodeProperties.h:205-218
82
+ function isEastAsianWidthFHWExcludingEmoji(widths , cp ) {
83
+ const width = getClass(widths, cp)
84
+ return width === 2 || width === 3 || (width === 5 && !hasProperty(cp, EMOJI))
85
+ }
86
+
87
+ // nsUnicharUtils.cpp:500-504
88
+ function isSegmentBreakSkipChar(widths , cp ) {
89
+ return isEastAsianWidthFHWExcludingEmoji(widths, cp) && !hasProperty(cp, HANGUL) && cp !== 0x20a9
90
+ }
91
+
92
+ // nsUnicharUtils.cpp:506-527, with UnicodeProperties.h:187-199
93
+ function isEastAsianPunctuation(widths , cp ) {
94
+ return getClass(widths, cp) !== 0 && ((hasProperty(cp, PUNCTUATION) && cp !== 0x20a9) || cp === 0xff5e || cp === 0x3000)
95
+ }
96
+
97
+ // gfxFontGroup::IsInvalidChar(char16_t), gfxTextRun.h:975-992. Below U+0100 it answers as
98
+ // IsInvalidChar(uint8_t), gfxTextRun.h:971-973.
99
+ function isInvalidChar(ch ) {
100
+ if (ch >= 0x20 && ch < 0x7f) return false
101
+ if (ch <= 0x9f) return true
102
+ return ((ch & 0xff00) === 0x2000 && (ch === 0x200b || ch === 0x2028 || ch === 0x2029 || ch === 0x2060)) ||
103
+ ch === 0xfeff || isBidiControl(ch)
104
+ }
105
+
106
+ // --- 1. TransformText (nsTextFrameUtils.cpp:84-401) ---
107
+
108
+ // Whether the text run leaves out a bidi control, which only 16-bit text does (IsDiscardable), is
109
+ // noted as it leaves them out, as IsDiscardable notes a soft hyphen (HasShy): testing the text for
110
+ // one before the scan costs every text a pass (RESEARCH.md, Keeping Work Bounded). `leftOut` is
111
+ // GeckoLineBreaks'.
112
+
113
+
114
+ // IsDiscardable, nsTextFrameUtils.cpp:32-49
115
+ export function isDiscardable(ch , is8bit ) {
116
+ return ch === CH_SHY || (!is8bit && isBidiControl(ch))
117
+ }
118
+ const isSpaceOrTab = (ch ) => ch === 0x20 || ch === 0x09
119
+ // IsSpaceOrTabOrSegmentBreak, nsTextFrameUtils.cpp:51-57: a white-space run's white space, which a CR or FF isn't.
120
+ export const isSpaceOrTabOrSegmentBreak = (ch ) => ch === 0x20 || ch === 0x09 || ch === 0x0a
121
+
122
+ // IsSpaceCombiningSequenceTail(const char16_t*, int32_t), nsTextFrameUtils.cpp:24-30, on code units.
123
+ export function isSpaceCombiningSequenceTail(text , from ) {
124
+ for (let i = from; i < text.length; i++) {
125
+ const ch = text.charCodeAt(i)
126
+ if (isClusterExtenderExcludingJoiners(ch)) return true
127
+ if (!isBidiControl(ch)) return false
128
+ }
129
+ return false
130
+ }
131
+
132
+ // aLangIsJapaneseOrChinese, nsTextFrameUtils.cpp:273-285, which takes only `-` after the subtag.
133
+ export function isJapaneseOrChinese(language ) {
134
+ const breakLanguage = getBreakLanguage(language)
135
+ return (breakLanguage === 'ja' || breakLanguage === 'zh') && language .charCodeAt(2) !== 0x5f
136
+ }
137
+
138
+ // Gecko's East Asian test for the white-space run [start, end) (TransformWhiteSpaces,
139
+ // nsTextFrameUtils.cpp:120-150): the code points before and after it, past default-ignorable
140
+ // ones, both segment-break skip characters, or on a `ja` or `zh` page either one East Asian
141
+ // punctuation. Only an interior run qualifies.
142
+ export function isEastAsianSegmentBreak(text , start , end , japaneseOrChinese ) {
143
+ if (start === 0 || end >= text.length) return false
144
+ const widths = eastAsianWidths ??= unpackClasses('gecko/east_asian_width')
145
+ let before
146
+ let pos = start
147
+ do {
148
+ const low = text.charCodeAt(pos - 1)
149
+ const high = pos > 1 ? text.charCodeAt(pos - 2) : 0
150
+ if (isSurrogatePair(high, low)) { before = combine(high, low); pos -= 2 } else { before = low; pos-- }
151
+ } while (hasProperty(before, DEFAULT_IGNORABLE) && pos > 0)
152
+ let after
153
+ pos = end
154
+ do {
155
+ after = text.codePointAt(pos)
156
+ pos += after > 0xffff ? 2 : 1
157
+ } while (hasProperty(after, DEFAULT_IGNORABLE) && pos < text.length)
158
+ return (isSegmentBreakSkipChar(widths, before) && isSegmentBreakSkipChar(widths, after)) ||
159
+ (japaneseOrChinese && (isEastAsianPunctuation(widths, before) || isEastAsianPunctuation(widths, after)))
160
+ }
161
+
162
+ // The scan transforms a text node's text as one text frame's. Firefox transforms a frame at a
163
+ // time, each from the white-space state the frame before it left (mNextRunContextInfo,
164
+ // nsTextFrame.cpp:2515-2518), and bidi resolution splits a text node into a frame for each level
165
+ // run (nsBidiPresUtils.cpp:1037-1053). A frame that starts at a character the text run drops ends
166
+ // the white-space run there (nsTextFrameUtils.cpp:370-379), and a line's start trims the white
167
+ // space a frame starts with while the line holds nothing (nsTextFrame.cpp:10904-10944). A
168
+ // dropped character starts a frame only where it is a bidi control that starts a level run, and
169
+ // whether one does turns on the paragraph's direction, which Pretext doesn't take: after a
170
+ // space, U+200F between Latin words starts one in a left-to-right paragraph and none in a
171
+ // right-to-left one, and U+200E between Hebrew words the other way around. So a white-space run
172
+ // reads through every character the text run drops. That is Firefox's run wherever the control
173
+ // keeps the level of the white space before it, as a mark of the paragraph's own direction does
174
+ // outside an embedding or isolate; ENGINE_FOLLOWUPS.md, White space and controls, has the rest.
175
+ // A rich-inline paragraph's items are text frames too: `frameStarts` has where each starts in the
176
+ // input, in order, or is null for one text. A white-space run goes on from one frame into white
177
+ // space that starts the next (INCOMING_WHITESPACE, :286, :382-386), whose own scan ends with its
178
+ // frame, and a dropped character that starts a frame ends it, as it follows no white space in
179
+ // that frame. In 16px Arial at 56px Firefox fits `see this` of items `see`, ` \u00AD`,
180
+ // ` this word` on a 55.15px line, and not of `see `, `\u00AD `, `this word`, whose text in one node
181
+ // it fits.
182
+ function transformText(input , is8bit , preserveWhiteSpace , frameStarts ) {
183
+ const len = input.length
184
+ // The source index of each kept unit.
185
+ const orig = new Int32Array(len)
186
+ const skipped = new Uint8Array(len)
187
+ let leftOut = null
188
+ let n = 0
189
+ let dropsBidiControl = false
190
+ if (preserveWhiteSpace) {
191
+ // COMPRESS_NONE, nsTextFrameUtils.cpp:222-271
192
+ for (let i = 0; i < len; i++) {
193
+ const ch = input.charCodeAt(i)
194
+ if (!isDiscardable(ch, is8bit)) {
195
+ orig[n++] = i
196
+ } else {
197
+ skipped[i] = 1
198
+ dropsBidiControl ||= ch !== CH_SHY
199
+ }
200
+ }
201
+ } else {
202
+ // COMPRESS_WHITESPACE_NEWLINE, :272-387
203
+ let inWhitespace = false
204
+ let i = 0
205
+ // Where the frame a white-space run starts in ends, the next frame start past the run's start:
206
+ // the input's end for one text. Only a run reads it, so it is found where one starts.
207
+ let frame = 0
208
+ let frameEnd = frameStarts === null ? len : 0
209
+ while (i < len) {
210
+ const ch = input.charCodeAt(i)
211
+ if (!isSpaceOrTabOrSegmentBreak(ch) && !isDiscardable(ch, is8bit)) {
212
+ // A CR or FF is no white space to TransformText (:51-57), so it ends a white-space run and
213
+ // stays in the text run, which gives it no glyph and no advance: it skips a control
214
+ // character it doesn't draw as a hexbox, which is never CR and, in release and beta builds,
215
+ // no other one either (gfxFont::SplitAndInitTextRun, gfxFont.cpp:3620-3627 and :3874-3892;
216
+ // layout.css.control-characters.visible, StaticPrefList.yaml:10926-10929). Every Canvas
217
+ // measures it as a space (Firefox's: CanvasRenderingContext2D.cpp:4634-4637), so a layout
218
+ // of the source leaves it out. Kept as a control of no advance, as the profile keeps the
219
+ // other controls Firefox hides (hidesControlCharacters), it is text at a line's edges,
220
+ // where Firefox trims it, and takes text with CRLF off the simple line walk (RESEARCH.md,
221
+ // Dead Ends). A CR right before a line feed needs no mark: the run that starts there keeps
222
+ // that line feed, and the CR, a breakable space to the scan too, collapses into it.
223
+ if (ch === 0x0c || (ch === 0x0d && input.charCodeAt(i + 1) !== 0x0a)) (leftOut ??= new Uint8Array(len))[i] = 1
224
+ orig[n++] = i
225
+ inWhitespace = false
226
+ i++
227
+ continue
228
+ }
229
+ if (isSpaceOrTabOrSegmentBreak(ch)) {
230
+ if (i >= frameEnd) {
231
+ while (frame < frameStarts .length && frameStarts [frame] <= i) frame++
232
+ frameEnd = frame < frameStarts .length ? frameStarts [frame] : len
233
+ }
234
+ let keepLastSpace = false
235
+ let hasSegmentBreak = ch === 0x0a
236
+ let trailingDiscardables = 0
237
+ // How many characters the run holds that the text run drops.
238
+ let dropped = 0
239
+ // A run that goes on from the frame before, after the characters that frame's run dropped at
240
+ // its end, leaves out white space that doesn't touch the unit the run kept.
241
+ const goesOnPastDropped = inWhitespace && i > 0 && isDiscardable(input.charCodeAt(i - 1), is8bit)
242
+ let j = i + 1
243
+ for (; j < frameEnd; j++) {
244
+ const c = input.charCodeAt(j)
245
+ if (c === 0x0a) hasSegmentBreak = true
246
+ else if (isDiscardable(c, is8bit)) dropped++
247
+ else if (!isSpaceOrTab(c)) break
248
+ }
249
+ while (isDiscardable(input.charCodeAt(j - 1), is8bit)) { j--; trailingDiscardables++ } // :334-336
250
+ if (!is8bit && input.charCodeAt(j - 1) === 0x20 && j < frameEnd && isSpaceCombiningSequenceTail(input, j)) { keepLastSpace = true; j-- } // :339-345
251
+ // TransformWhiteSpaces over [i, j), :84-209. The runs it deletes whole, next to a ZWSP or
252
+ // between East Asian characters (:120-150), are gone already (removeSkippableSegmentBreaks in
253
+ // src/analysis.ts), so a run keeps one space. A space or tab in a run with a segment break
254
+ // goes (:152-162), and the run's first segment break stays as a space (:181-193).
255
+ for (let k = i; k < j; k++) {
256
+ const c = input.charCodeAt(k)
257
+ if (isDiscardable(c, is8bit)) {
258
+ skipped[k] = 1
259
+ dropsBidiControl ||= c !== CH_SHY
260
+ } else if (inWhitespace || (hasSegmentBreak && isSpaceOrTab(c))) {
261
+ skipped[k] = 1
262
+ // Where the run reads through a dropped character, white space it leaves out may not
263
+ // touch the unit it keeps, so collapsing adjacent white space wouldn't drop it.
264
+ if (dropped > trailingDiscardables || goesOnPastDropped) (leftOut ??= new Uint8Array(len))[k] = 1
265
+ } else {
266
+ orig[n++] = k
267
+ inWhitespace = true
268
+ }
269
+ }
270
+ if (keepLastSpace) orig[n++] = j++
271
+ for (let k = 0; k < trailingDiscardables; k++) {
272
+ dropsBidiControl ||= input.charCodeAt(j) !== CH_SHY
273
+ skipped[j++] = 1
274
+ }
275
+ i = j
276
+ continue
277
+ }
278
+ skipped[i] = 1 // :370-379
279
+ dropsBidiControl ||= ch !== CH_SHY
280
+ inWhitespace = false
281
+ i++
282
+ }
283
+ }
284
+ // The kept units, as slices of the input where they run unchanged: in collapsed white space, a kept
285
+ // tab or segment break becomes a space.
286
+ let text = ''
287
+ let sliceStart = 0
288
+ let sliceEnd = 0
289
+ for (let k = 0; k < n; k++) {
290
+ const i = orig[k]
291
+ const ch = input.charCodeAt(i)
292
+ if (!preserveWhiteSpace && (ch === 0x09 || ch === 0x0a)) {
293
+ text += input.slice(sliceStart, sliceEnd) + ' '
294
+ sliceStart = sliceEnd = i + 1
295
+ } else if (i !== sliceEnd) {
296
+ text += input.slice(sliceStart, sliceEnd)
297
+ sliceStart = i
298
+ sliceEnd = i + 1
299
+ } else {
300
+ sliceEnd++
301
+ }
302
+ }
303
+ text += input.slice(sliceStart, sliceEnd)
304
+ return { text, orig: orig.subarray(0, n), skipped, leftOut, dropsBidiControl }
305
+ }
306
+
307
+ // IsTrimmableSpace, nsTextFrame.cpp:921-942, in normal white space.
308
+ function isTrimmableSpace(source , pos , is8bit ) {
309
+ switch (source.charCodeAt(pos)) {
310
+ case 0x20: case 0x1680: return is8bit || !isSpaceCombiningSequenceTail(source, pos + 1)
311
+ case 0x0a: case 0x09: case 0x0d: case 0x0c: return true
312
+ default: return false
313
+ }
314
+ }
315
+
316
+ // HasCompressedLeadingWhitespace, nsTextFrame.cpp:2869-2887
317
+ function hasCompressedLeadingWhitespace(source , skipped , is8bit , preserveWhiteSpace ) {
318
+ if (preserveWhiteSpace || skipped[0] === 0) return false
319
+ for (let k = 0; k < source.length && skipped[k] === 1; k++) if (isTrimmableSpace(source, k, is8bit)) return true
320
+ return false
321
+ }
322
+
323
+ // --- 2. Text-run glyph records (gfxTextRun.cpp, gfxFont.cpp) ---
324
+ //
325
+ // Only the facts SetPotentialLineBreaks reads are recorded: which positions start a cluster and
326
+ // which are spaces. Every position belongs to one shaped word, space or invalid character.
327
+
328
+
329
+
330
+ // CompressedGlyph::MakeComplex(false, true), gfxFont.cpp:716
331
+ function extendCluster(g , i ) {
332
+ g.clusterStart[i] = 0
333
+ g.isSpace[i] = 0
334
+ }
335
+
336
+ // gfxShapedText::SetupClusterBoundaries(uint32_t, const char16_t*, uint32_t), gfxFont.cpp:708-769,
337
+ // for one shaped word, without the emergency wraps after hyphens. GraphemeClusterBreakIteratorUtf16
338
+ // reads the word alone (intl/lwbrk/Segmenter.cpp:174-187). Below U+0300 only CR and LF share a
339
+ // cluster, which the generator checks, and words hold neither, so a word of such units has a
340
+ // cluster at every unit. `ends` has room for the word's clusters.
341
+ function setupClusterBoundaries(g , text , from , to , graphemeTable , ends ) {
342
+ let ch0 = text.charCodeAt(from)
343
+ if (to - from > 1 && isSurrogatePair(ch0, text.charCodeAt(from + 1))) ch0 = combine(ch0, text.charCodeAt(from + 1))
344
+ if (isClusterExtender(ch0)) extendCluster(g, from)
345
+ let low = from
346
+ while (low < to && text.charCodeAt(low) < 0x300) low++
347
+ const count = low === to ? 0 : findGraphemeEnds(graphemeTable, text, from, to, ends)
348
+ for (let k = 0, pos = from; pos < to; k++) {
349
+ const ch = text.charCodeAt(pos)
350
+ if (ch === 0x20 || ch === 0x3000) g.isSpace[pos] = 1
351
+ else if (ch === 0x09af && pos > from && text.charCodeAt(pos - 1) === 0x09cd) extendCluster(g, pos) // BENGALI_YA after BENGALI_VIRAMA
352
+ const end = count === 0 ? pos + 1 : ends[k]
353
+ for (pos++; pos < end; pos++) extendCluster(g, pos)
354
+ }
355
+ }
356
+
357
+ // gfxFont::SplitAndInitTextRun, gfxFont.cpp:3707-3900: sets up the shaped words of the text run. A
358
+ // word ends at a space or NBSP before no cluster extender (IsBoundarySpace, :3317-3330) or at an
359
+ // invalid character (:3778-3790). A space glyph (SetSpaceGlyphIfSimple, gfxTextRun.cpp:1612-1619)
360
+ // only sets isSpace, and an invalid character (:3877-3892) keeps a zero record. Below U+0100,
361
+ // IsBoundarySpace and SetupClusterBoundaries(uint8_t) (gfxFont.cpp:771-795) answer as the char16_t
362
+ // versions, so 8-bit text and 8-bit words take the char16_t path, at any word length (:3569-3577,
363
+ // :3817-3821).
364
+ function splitAndInitTextRun(g , text , graphemeTable , ends ) {
365
+ const end = text.length
366
+ let wordStart = 0
367
+ for (let i = 0; i < end; i++) {
368
+ const ch = text.charCodeAt(i)
369
+ const boundary = (ch === 0x20 || ch === 0xa0) && !(i + 1 < end && isClusterExtender(text.charCodeAt(i + 1)))
370
+ if (!boundary && !isInvalidChar(ch)) continue
371
+ if (i > wordStart) setupClusterBoundaries(g, text, wordStart, i, graphemeTable, ends)
372
+ if (ch === 0x20) g.isSpace[i] = 1
373
+ wordStart = i + 1
374
+ }
375
+ if (end > wordStart) setupClusterBoundaries(g, text, wordStart, end, graphemeTable, ends)
376
+ }
377
+
378
+ // --- 3. ICU4X 2.1.2's line iterator for one word (icu_segmenter src/line.rs) ---
379
+
380
+ // Firefox's line data, which the first scan unpacks (getGeckoLineBreaks).
381
+
382
+ let lineData = null
383
+
384
+ // The Line_Break value, with error value 0 above U+10FFFF.
385
+ function getLineBreakClass(line , c ) {
386
+ return c > 0x10ffff ? 0 : getClass(line.classes, c)
387
+ }
388
+
389
+ // Line_Break property values of the data (line.rs:18-128).
390
+ const AI = 1, AL = 3, BK = 10, CJ = 12, CM = 14, CR = 16, H2 = 21, H3 = 22, HY = 24, ID = 25, JL = 29,
391
+ JT = 30, JV = 31, LF = 32, NL = 33, NU = 35, SA = 46, SP = 47, ZW = 53, ZWJ = 54
392
+ // BreakState bytes (provider/mod.rs:288-310).
393
+ const BREAK = 253, NO_MATCH = 254, KEEP = 255, INTERMEDIATE = 120
394
+
395
+ // LanguageIteratorUtf16 (complex/language.rs:17-45, 78-102) reads single code units: Thai, Lao,
396
+ // Burmese, Khmer, or 0 for Unknown, and Chinese and Japanese, for which the LSTM segmenter has no
397
+ // model (complex/mod.rs:161-180).
398
+ function getComplexLanguage(u ) {
399
+ if (u >= 0xe01 && u <= 0xe7f) return 1
400
+ if (u >= 0xe80 && u <= 0xeff) return 2
401
+ if ((u >= 0x1000 && u <= 0x109f) || (u >= 0xa9e0 && u <= 0xa9ff) || (u >= 0xaa60 && u <= 0xaa7f)) return 3
402
+ if ((u >= 0x1780 && u <= 0x17ff) || (u >= 0x19e0 && u <= 0x19ff)) return 4
403
+ return 0
404
+ }
405
+
406
+ // complex_language_segment_utf16 (complex/mod.rs:135-156): splits a run of SA code units by
407
+ // language, and Intl.Segmenter words supply boundaries inside each Thai, Lao, Burmese and Khmer
408
+ // slice. Every slice reports its end; other languages report nothing else (:149-151).
409
+ function segmentComplex(units ) {
410
+ const result = []
411
+ for (let i = 0; i < units.length;) {
412
+ const language = getComplexLanguage(units[i] )
413
+ let j = i + 1
414
+ while (j < units.length && getComplexLanguage(units[j] ) === language) j++
415
+ if (language !== 0) {
416
+ let slice = ''
417
+ for (let k = i; k < j; k += 4096) slice += String.fromCharCode(...units.slice(k, Math.min(j, k + 4096)))
418
+ for (const part of getWordSegmenter().segment(slice)) if (part.index > 0) result.push(i + part.index)
419
+ }
420
+ result.push(j)
421
+ i = j
422
+ }
423
+ return result
424
+ }
425
+
426
+ // KeepAll pairs (line.rs:901-907).
427
+ function isKeepAllLetter(p ) {
428
+ return p === AI || p === AL || p === ID || p === NU || p === HY || p === H2 || p === H3 || p === JL ||
429
+ p === JV || p === JT || p === CJ
430
+ }
431
+
432
+ // Utf16Indices::next (indices.rs:58-83): the code point at text[i], in a pair that ends before `end`,
433
+ // or an unpaired surrogate as a code point of its own.
434
+ function readCodePoint(text , i , end ) {
435
+ const c = text.charCodeAt(i)
436
+ if ((c & 0xfc00) === 0xd800 && i + 1 < end) {
437
+ const next = text.charCodeAt(i + 1)
438
+ if ((next & 0xfc00) === 0xdc00) return ((c & 0x3ff) << 10) + (next & 0x3ff) + 0x10000
439
+ }
440
+ return c
441
+ }
442
+
443
+ // LineBreakIterator (line.rs:820-1130) over the word text[start, end), drained: state[start + p] = 1
444
+ // at each break p inside the word that Iterator::next returns (:833-1067), which starts at the
445
+ // word's first code point after the break at 0 and stops at the word's end. The position is
446
+ // Utf16Indices' front_offset and current_pos_data (:821-823): `pos` is the current code point's
447
+ // offset in the word, `cp` its code point and `front` the next one's offset, and `pos` reaches
448
+ // the word's length past its end.
449
+ function markWordBreaks(line , text , start , end , keepAll , state ) {
450
+ const len = end - start
451
+ const states = line.states
452
+ let pos = 0
453
+ let cp = readCodePoint(text, start, end)
454
+ let front = cp > 0xffff ? 2 : 1
455
+ // Break points cached by a complex-script run, each less `cacheOffset`, from `cacheAt`.
456
+ let cache = []
457
+ let cacheAt = 0
458
+ let cacheOffset = 0
459
+
460
+ for (;;) {
461
+ // Break points cached by a complex-script run (:840-855).
462
+ if (cacheAt < cache.length) {
463
+ const firstPos = cache[cacheAt++] - cacheOffset
464
+ let i = 0
465
+ while (i !== firstPos) {
466
+ i += cp > 0xffff ? 2 : 1 // Utf16::char_len (rule_segmenter.rs:335-341)
467
+ pos = front
468
+ if (pos >= len) return
469
+ cp = readCodePoint(text, start + pos, end)
470
+ front += cp > 0xffff ? 2 : 1
471
+ }
472
+ cacheOffset += i
473
+ state[start + pos] = 1
474
+ continue
475
+ }
476
+
477
+ let lb9Left = -1 // (:858)
478
+ let lb8aAfterLb9 = false // (:861)
479
+ outer: for (;;) {
480
+ const leftCodepoint = cp
481
+ const leftProp = lb9Left >= 0 ? lb9Left : getLineBreakClass(line, leftCodepoint)
482
+ const afterZwj = lb8aAfterLb9 || (lb9Left < 0 && leftProp === ZWJ)
483
+ pos = front
484
+ if (pos >= len) return
485
+ cp = readCodePoint(text, start + pos, end)
486
+ front += cp > 0xffff ? 2 : 1
487
+ const rightProp = getLineBreakClass(line, cp)
488
+
489
+ // LB9 (:878-893)
490
+ if ((rightProp === CM || rightProp === ZWJ) && leftProp !== BK && leftProp !== CR && leftProp !== LF &&
491
+ leftProp !== NL && leftProp !== SP && leftProp !== ZW) {
492
+ lb9Left = leftProp
493
+ lb8aAfterLb9 = rightProp === ZWJ
494
+ continue
495
+ }
496
+ lb9Left = -1
497
+ lb8aAfterLb9 = false
498
+
499
+ // CSS word-break (:896-909)
500
+ if (keepAll && isKeepAllLetter(leftProp) && isKeepAllLetter(rightProp)) continue
501
+
502
+ // Complex scripts (:941-950), Utf16 line_handle_complex_language (:1263-1316), which counts
503
+ // code points one unit each: they are truncated to u16 there.
504
+ if (getLineBreakClass(line, leftCodepoint) === SA && rightProp === SA) {
505
+ const units = [leftCodepoint & 0xffff]
506
+ for (let p = pos, c = cp; ;) {
507
+ units.push(c & 0xffff)
508
+ p += c > 0xffff ? 2 : 1
509
+ if (p >= len) break
510
+ c = readCodePoint(text, start + p, end)
511
+ if (getLineBreakClass(line, c) !== SA) break
512
+ }
513
+ cache = segmentComplex(units)
514
+ cacheAt = 1
515
+ let i = 1
516
+ while (i !== cache[0]) {
517
+ i++
518
+ pos = front
519
+ if (pos >= len) return
520
+ cp = readCodePoint(text, start + pos, end)
521
+ front += cp > 0xffff ? 2 : 1
522
+ }
523
+ cacheOffset = i
524
+ break
525
+ }
526
+
527
+ const breakState = states[leftProp * geckoLinePropertyCount + rightProp] // (:702-706, 953)
528
+ if (breakState === BREAK || breakState === NO_MATCH) {
529
+ if (afterZwj) continue
530
+ break
531
+ }
532
+ if (breakState === KEEP) continue
533
+
534
+ let index = breakState >= INTERMEDIATE ? breakState - INTERMEDIATE : breakState
535
+ let prevFront = front
536
+ let prevPos = pos
537
+ let prevCp = cp
538
+ let previousIsAfterZwj = afterZwj
539
+ let leftPropPreLb9 = rightProp
540
+ // (:976-981), where lb8a_after_lb9 is false once LB9 has passed.
541
+ const isIntermediateRuleNoMatch = index > geckoLineLastCodepointProperty
542
+
543
+ for (;;) {
544
+ const innerAfterZwj = leftPropPreLb9 === ZWJ
545
+ const previousBreakStateIsCpProp = index <= geckoLineLastCodepointProperty
546
+ pos = front
547
+ if (pos >= len) { // (:990-1007)
548
+ if (states[index * geckoLinePropertyCount + geckoLineEotProperty] !== NO_MATCH) return
549
+ front = prevFront; pos = prevPos; cp = prevCp
550
+ if (previousIsAfterZwj) continue outer
551
+ break outer
552
+ }
553
+ cp = readCodePoint(text, start + pos, end)
554
+ front += cp > 0xffff ? 2 : 1
555
+ const prop = getLineBreakClass(line, cp)
556
+
557
+ if ((prop === CM || prop === ZWJ) && leftPropPreLb9 !== BK && leftPropPreLb9 !== CR && leftPropPreLb9 !== LF &&
558
+ leftPropPreLb9 !== NL && leftPropPreLb9 !== SP && leftPropPreLb9 !== ZW) { // (:1009-1019)
559
+ leftPropPreLb9 = prop
560
+ continue
561
+ }
562
+
563
+ const next = states[index * geckoLinePropertyCount + prop] // (:1021-1061)
564
+ if (next === KEEP) continue outer
565
+ if (next === NO_MATCH) {
566
+ front = prevFront; pos = prevPos; cp = prevCp
567
+ if (innerAfterZwj) {
568
+ if (isIntermediateRuleNoMatch && !previousIsAfterZwj) break outer
569
+ continue outer
570
+ }
571
+ if (previousIsAfterZwj) continue outer
572
+ break outer
573
+ }
574
+ if (next === BREAK) {
575
+ if (innerAfterZwj) continue outer
576
+ break outer
577
+ }
578
+ if (next >= INTERMEDIATE) {
579
+ index = next - INTERMEDIATE
580
+ prevFront = front; prevPos = pos; prevCp = cp
581
+ previousIsAfterZwj = innerAfterZwj
582
+ } else {
583
+ index = next
584
+ if (previousBreakStateIsCpProp) {
585
+ prevFront = front; prevPos = pos; prevCp = cp
586
+ previousIsAfterZwj = innerAfterZwj
587
+ }
588
+ }
589
+ leftPropPreLb9 = prop
590
+ }
591
+ }
592
+ state[start + pos] = 1
593
+ }
594
+ }
595
+
596
+ // --- 4. nsLineBreaker (dom/base/nsLineBreaker.cpp) ---
597
+
598
+ // kNonBreakableASCII, nsLineBreaker.cpp:33-48
599
+ const NON_BREAKABLE_ASCII = new Uint8Array([
600
+ 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0,
601
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
602
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
603
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
604
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
605
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,
606
+ ])
607
+
608
+ // The break states of one text node: nsLineBreaker::AppendText with flags 0 while no word is
609
+ // buffered (16-bit :235-400, 8-bit :505-650), after AppendInvisibleWhitespace when leading white
610
+ // space was compressed (nsTextFrame.cpp:2978-2981, nsLineBreaker.cpp:695-708), then the last
611
+ // word's FlushCurrentWord from Reset (nsLineBreaker.cpp:134-226, 710-720). Each word of more than
612
+ // ASCII letters goes to LineBreaker::ComputeBreakPositions (intl/lwbrk/LineBreaker.cpp:112-194),
613
+ // which keeps the state before its first unit (AutoRestore, :342, :604; skipSet = 1, :200-206).
614
+ function getBreakStates(line , text , is8bit , afterLeadingWhitespace , keepAll ) {
615
+ const len = text.length
616
+ const state = new Uint8Array(len)
617
+ let afterBreakableSpace = afterLeadingWhitespace
618
+ let wordStart = 0
619
+ let wordMightBeBreakable = false
620
+ for (let offset = 0; offset <= len; offset++) {
621
+ const ch = offset < len ? text.charCodeAt(offset) : -1
622
+ // nsLineBreaker::IsSegmentSpace, nsLineBreaker.h:260-264
623
+ const isSpace = ch === 0x20 || ch === 0x09 || ch === 0x0d
624
+ if (afterBreakableSpace && !isSpace && ch >= 0) state[offset] = 1
625
+ afterBreakableSpace = isSpace
626
+ if (ch >= 0 && !isSpace && (is8bit || ch !== 0x0a)) { // :332, :595
627
+ // IsNonBreakableChar, nsLineBreaker.cpp:50-56
628
+ if (!(ch >= 0x20 && ch <= 0x7f && NON_BREAKABLE_ASCII[ch - 0x20] === 1)) wordMightBeBreakable = true
629
+ continue
630
+ }
631
+ if (offset > wordStart && wordMightBeBreakable) markWordBreaks(line, text, wordStart, offset, keepAll, state)
632
+ wordMightBeBreakable = false
633
+ wordStart = offset + 1
634
+ }
635
+ return state
636
+ }
637
+
638
+ // Where a line may start in a text node's source, after the segment break transformation that
639
+ // removeSkippableSegmentBreaks applies: a BREAK at flags[i] for 0 < i < source.length, at a normal
640
+ // break (FLAG_BREAK_TYPE_NORMAL) or after a soft hyphen. gfxTextRun::SetPotentialLineBreaks
641
+ // (gfxTextRun.cpp:210-236) keeps a break only at a cluster start or after a space. A normal break
642
+ // right after a soft hyphen adds SOFT_HYPHEN_BREAK: BreakAndMeasureText takes it as the normal
643
+ // break, which neither fits nor draws a hyphen (gfxTextRun.cpp:1053-1063). A CLUSTER_START marks
644
+ // where a cluster starts without a break, where only break-word can wrap (gfxTextRun.cpp:1068-1074).
645
+ // The breaks, whether the text run left out a bidi control, and 1 at each unit that a layout of
646
+ // the source leaves out where collapsing each run of adjacent white space wouldn't, or null where
647
+ // there is none: white space the text run collapsed in a run that read past a character it drops,
648
+ // and, in normal white space, an FF or a CR that no line feed follows, which the text run keeps
649
+ // with no advance (transformText).
650
+
651
+
652
+ export function getGeckoLineBreaks(
653
+ source ,
654
+ preserveWhiteSpace ,
655
+ keepAll ,
656
+ graphemeTable ,
657
+ // Where each item of a rich-inline paragraph starts in the source, or null for one text (transformText).
658
+ frameStarts = null,
659
+ ) {
660
+ const len = source.length
661
+ const flags = new Uint8Array(len + 1)
662
+ if (len === 0) return { breaks: flags, leftOut: null, dropsBidiControl: false }
663
+ let is8bit = true
664
+ for (let i = 0; i < len && is8bit; i++) is8bit = source.charCodeAt(i) < 0x100
665
+ // CharacterDataBuffer::SetTo stores 1b text when every unit is below 256 (CharacterDataBuffer.cpp:235-286).
666
+ const tr = transformText(source, is8bit, preserveWhiteSpace, frameStarts)
667
+ const n = tr.text.length
668
+ if (n === 0) return { breaks: flags, leftOut: null, dropsBidiControl: tr.dropsBidiControl }
669
+
670
+ // The text run is built before the break sinks are set up (nsTextFrame.cpp:2860-2864), in one
671
+ // piece: Firefox splits it where the bidi level changes, which the scan leaves out (above).
672
+ const g = { clusterStart: new Uint8Array(n).fill(1), isSpace: new Uint8Array(n) }
673
+ splitAndInitTextRun(g, tr.text, graphemeTable, new Int32Array(n))
674
+
675
+ const line = lineData ??= { classes: unpackClasses('gecko/line'), states: unpackTable(geckoLineBreakStatesPacked) }
676
+ const state = getBreakStates(line, tr.text, is8bit, hasCompressedLeadingWhitespace(source, tr.skipped, is8bit, preserveWhiteSpace), keepAll)
677
+ for (let t = 1; t < n; t++) {
678
+ const rawPos = tr.orig[t]
679
+ const normal = state[t] === 1 && (g.clusterStart[t] === 1 || g.isSpace[t - 1] === 1)
680
+ const afterSoftHyphen = tr.skipped[rawPos - 1] === 1 && source.charCodeAt(rawPos - 1) === CH_SHY
681
+ if (normal || afterSoftHyphen) {
682
+ flags[rawPos] = normal && afterSoftHyphen ? OPPORTUNITY | SOFT_HYPHEN_BREAK : OPPORTUNITY
683
+ } else if (g.clusterStart[t] === 1) {
684
+ flags[rawPos] = CLUSTER_START
685
+ }
686
+ }
687
+ return { breaks: flags, leftOut: tr.leftOut, dropsBidiControl: tr.dropsBidiControl }
688
+ }