rapier-markdown-kit 1.1.101 → 1.1.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +213 -53
  3. package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
  4. package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
  5. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
  6. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
  7. package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
  8. package/dist/agent/vendor/pretext/LICENSE +208 -0
  9. package/dist/agent/vendor/pretext/NOTICE +26 -13
  10. package/dist/agent/vendor/pretext/SOURCE.json +213 -45
  11. package/dist/agent/vendor/pretext/analysis.js +675 -1224
  12. package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
  13. package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
  14. package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
  15. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
  16. package/dist/agent/vendor/pretext/graphemes.js +98 -0
  17. package/dist/agent/vendor/pretext/han-kerning.js +253 -0
  18. package/dist/agent/vendor/pretext/layout.js +352 -290
  19. package/dist/agent/vendor/pretext/line-break.js +1154 -588
  20. package/dist/agent/vendor/pretext/line-breaks.js +892 -0
  21. package/dist/agent/vendor/pretext/line-text.js +81 -39
  22. package/dist/agent/vendor/pretext/measurement.js +1105 -176
  23. package/dist/agent/vendor/pretext/prepare.js +807 -0
  24. package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
  25. package/dist/agent/will.mjs +2 -0
  26. package/dist/kit/assets.mjs +5 -1
  27. package/dist/kit/conformance/run.mjs +4 -4
  28. package/dist/kit/export-fonts.mjs +26 -11
  29. package/dist/kit/index.mjs +1 -0
  30. package/dist/kit/ledger/carried.mjs +9 -7
  31. package/dist/kit/ledger/data.mjs +73 -13
  32. package/dist/kit/ledger/format.mjs +166 -53
  33. package/dist/kit/ledger/hash.mjs +75 -18
  34. package/dist/kit/ledger/history.mjs +143 -115
  35. package/dist/kit/ledger/inverse-source.mjs +158 -19
  36. package/dist/kit/ledger/journal-records.mjs +292 -67
  37. package/dist/kit/model.mjs +1 -1
  38. package/dist/kit/pdf-glyphs.mjs +4 -4
  39. package/dist/kit/render-markdown.mjs +259 -122
  40. package/dist/kit/render-sanitize.mjs +24 -4
  41. package/dist/kit/render-work.mjs +99 -20
  42. package/dist/kit/render.mjs +101 -65
  43. package/dist/kit/typography.mjs +2 -0
  44. package/dist/layout/hyphenation.mjs +127 -0
  45. package/dist/layout/line-plan.mjs +88 -96
  46. package/dist/layout/model.mjs +61 -80
  47. package/dist/layout/paragraph-fit.mjs +191 -0
  48. package/dist/layout/preparation.mjs +373 -0
  49. package/dist/layout/prepared-paragraph.mjs +223 -0
  50. package/dist/layout/typography.mjs +345 -111
  51. package/dist/packages/rapier-diff/index.mjs +288 -196
  52. package/dist/spec/document-fonts.mjs +13 -4
  53. package/dist/spec/md-assets.mjs +149 -30
  54. package/dist/spec/md-layout.mjs +5 -2
  55. package/dist/spec/md-marks.mjs +218 -3
  56. package/dist/spec/md-source.mjs +122 -0
  57. package/package.json +2 -1
  58. package/style.css +9 -0
  59. package/dist/agent/vendor/pretext/bidi.js +0 -151
  60. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -1,1234 +1,685 @@
1
- /*! Pretext 0.0.9 | MIT | Copyright (c) 2026 Pretext contributors */
2
- const collapsibleWhitespaceRunRe = /[ \t\n\r\f]+/g;
3
- const needsWhitespaceNormalizationRe = /[\t\n\r\f]| {2,}|^ | $/;
4
- export function normalizeWhitespaceNormal(text) {
5
- if (!needsWhitespaceNormalizationRe.test(text)) return text;
6
- let normalized = text.replace(collapsibleWhitespaceRunRe, ' ');
7
- if (normalized.charCodeAt(0) === 0x20) {
8
- normalized = normalized.slice(1);
9
- }
10
- if (normalized.length > 0 && normalized.charCodeAt(normalized.length - 1) === 0x20) {
11
- normalized = normalized.slice(0, -1);
12
- }
13
- return normalized;
14
- }
15
- function normalizeWhitespacePreWrap(text) {
16
- if (!/[\r\f]/.test(text)) return text;
17
- return text.replace(/\r\n/g, '\n').replace(/[\r\f]/g, '\n');
18
- }
19
- let sharedGraphemeSegmenter = null;
20
- export function getSharedGraphemeSegmenter() {
21
- if (sharedGraphemeSegmenter === null) {
22
- sharedGraphemeSegmenter = new Intl.Segmenter(undefined, {
23
- granularity: 'grapheme'
24
- });
25
- }
26
- return sharedGraphemeSegmenter;
27
- }
28
- let sharedWordSegmenter = null;
29
- let segmenterLocale;
30
- function getSharedWordSegmenter() {
31
- if (sharedWordSegmenter === null) {
32
- sharedWordSegmenter = new Intl.Segmenter(segmenterLocale, {
33
- granularity: 'word'
34
- });
35
- }
36
- return sharedWordSegmenter;
37
- }
38
- export function clearAnalysisCaches() {
39
- sharedGraphemeSegmenter = null;
40
- sharedWordSegmenter = null;
41
- }
42
- export function setAnalysisLocale(locale) {
43
- const nextLocale = locale && locale.length > 0 ? locale : undefined;
44
- if (segmenterLocale === nextLocale) return;
45
- segmenterLocale = nextLocale;
46
- sharedWordSegmenter = null;
47
- }
48
- const arabicScriptRe = /\p{Script=Arabic}/u;
49
- const combiningMarkRe = /\p{M}/u;
50
- const decimalDigitRe = /\p{Nd}/u;
51
- function containsArabicScript(text) {
52
- return arabicScriptRe.test(text);
53
- }
54
- const cjkRe = /[\u3000-\u30FF\u3130-\u318F\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFF00-\uFFEF\u{20000}-\u{2A6DF}\u{2A700}-\u{2EE5D}\u{2F800}-\u{2FA1F}\u{30000}-\u{33479}]/u;
55
- export function isCJK(s) {
56
- return cjkRe.test(s);
57
- }
58
- function endsWithLineStartProhibitedText(text) {
59
- const last = getLastCodePoint(text);
60
- return last !== null && (kinsokuStart.has(last) || leftStickyPunctuation.has(last));
61
- }
62
- const keepAllGlueChars = new Set([
63
- '\u00A0',
64
- '\u202F',
65
- '\u2060',
66
- '\uFEFF'
67
- ]);
68
- const keepAllDashBreakChars = new Set([
69
- '-',
70
- '\u2010',
71
- '\u2013',
72
- '\u2014'
73
- ]);
74
- function endsWithKeepAllGlueText(text) {
75
- const last = getLastCodePoint(text);
76
- return last !== null && keepAllGlueChars.has(last);
77
- }
78
- function endsWithKeepAllDashBreakText(text) {
79
- const last = getLastCodePoint(text);
80
- return last !== null && keepAllDashBreakChars.has(last);
81
- }
82
- export function canContinueKeepAllTextRun(previousText, breakAfterPunctuation) {
83
- if (endsWithKeepAllGlueText(previousText)) return false;
84
- if (!breakAfterPunctuation) return true;
85
- if (endsWithLineStartProhibitedText(previousText)) return false;
86
- if (endsWithKeepAllDashBreakText(previousText)) return false;
87
- return true;
88
- }
89
- export const kinsokuStart = new Set([
90
- '\uFF0C',
91
- '\uFF0E',
92
- '\uFF01',
93
- '\uFF1A',
94
- '\uFF1B',
95
- '\uFF1F',
96
- '\u3001',
97
- '\u3002',
98
- '\u30FB',
99
- '\uFF09',
100
- '\u3015',
101
- '\u3009',
102
- '\u300B',
103
- '\u300D',
104
- '\u300F',
105
- '\u3011',
106
- '\u3017',
107
- '\u3019',
108
- '\u301B',
109
- '\u30FC',
110
- '\u3005',
111
- '\u303B',
112
- '\u309D',
113
- '\u309E',
114
- '\u30FD',
115
- '\u30FE'
116
- ]);
117
- export const kinsokuEnd = new Set([
118
- '"',
119
- '(',
120
- '[',
121
- '{',
122
- '¡',
123
- '¿',
124
- '“',
125
- '‘',
126
- '‚',
127
- '„',
128
- '«',
129
- '‹',
130
- '\u2E18',
131
- '\uFF08',
132
- '\u3014',
133
- '\u3008',
134
- '\u300A',
135
- '\u300C',
136
- '\u300E',
137
- '\u3010',
138
- '\u3016',
139
- '\u3018',
140
- '\u301A'
141
- ]);
142
- const forwardStickyGlue = new Set([
143
- "'",
144
- '’'
145
- ]);
146
- export const leftStickyPunctuation = new Set([
147
- '.',
148
- ',',
149
- '!',
150
- '?',
151
- ':',
152
- ';',
153
- '\u060C',
154
- '\u061B',
155
- '\u061F',
156
- '\u0964',
157
- '\u0965',
158
- '\u104A',
159
- '\u104B',
160
- '\u104C',
161
- '\u104D',
162
- '\u104F',
163
- ')',
164
- ']',
165
- '}',
166
- '%',
167
- '"',
168
- '”',
169
- '’',
170
- '»',
171
- '›',
172
- '…'
173
- ]);
174
- const arabicNoSpaceTrailingPunctuation = new Set([
175
- ':',
176
- '.',
177
- '\u060C',
178
- '\u061B'
179
- ]);
180
- const myanmarMedialGlue = new Set([
181
- '\u104F'
182
- ]);
183
- const closingQuoteChars = new Set([
184
- '”',
185
- '’',
186
- '»',
187
- '›',
188
- '\u300D',
189
- '\u300F',
190
- '\u3011',
191
- '\u300B',
192
- '\u3009',
193
- '\u3015',
194
- '\uFF09'
195
- ]);
196
- function isLeftStickyPunctuationSegment(segment) {
197
- if (isPunctuationGlueCluster(segment)) return true;
198
- let sawPunctuation = false;
199
- for (const ch of segment){
200
- if (leftStickyPunctuation.has(ch) || isLineBreakNumericAffix(ch)) {
201
- sawPunctuation = true;
202
- continue;
203
- }
204
- if (sawPunctuation && combiningMarkRe.test(ch)) continue;
205
- return false;
206
- }
207
- return sawPunctuation;
208
- }
209
- function isCJKLineStartProhibitedSegment(segment) {
210
- for (const ch of segment){
211
- if (!kinsokuStart.has(ch) && !leftStickyPunctuation.has(ch)) return false;
212
- }
213
- return segment.length > 0;
214
- }
215
- function isForwardStickyClusterSegment(segment) {
216
- if (isPunctuationGlueCluster(segment)) return true;
217
- for (const ch of segment){
218
- if (!kinsokuEnd.has(ch) && !forwardStickyGlue.has(ch) && !combiningMarkRe.test(ch) && !isLineBreakNumericAffix(ch)) {
219
- return false;
220
- }
221
- }
222
- return segment.length > 0;
223
- }
224
- function isPunctuationGlueCluster(segment) {
225
- let sawPunctuation = false;
226
- for (const ch of segment){
227
- if (ch === '\\' || combiningMarkRe.test(ch)) continue;
228
- if (kinsokuEnd.has(ch) || leftStickyPunctuation.has(ch) || forwardStickyGlue.has(ch)) {
229
- sawPunctuation = true;
230
- continue;
231
- }
232
- return false;
233
- }
234
- return sawPunctuation;
235
- }
236
- function previousCodePointStart(text, end) {
237
- const last = end - 1;
238
- if (last <= 0) return Math.max(last, 0);
239
- const lastCodeUnit = text.charCodeAt(last);
240
- if (lastCodeUnit < 0xDC00 || lastCodeUnit > 0xDFFF) return last;
241
- const maybeHigh = last - 1;
242
- if (maybeHigh < 0) return last;
243
- const highCodeUnit = text.charCodeAt(maybeHigh);
244
- return highCodeUnit >= 0xD800 && highCodeUnit <= 0xDBFF ? maybeHigh : last;
245
- }
246
- function getLastCodePoint(text) {
247
- if (text.length === 0) return null;
248
- const start = previousCodePointStart(text, text.length);
249
- return text.slice(start);
250
- }
251
- function getFirstSignificantCodePoint(text) {
252
- for (const ch of text){
253
- if (!combiningMarkRe.test(ch)) return ch;
254
- }
255
- return null;
256
- }
257
- function getLastSignificantCodePoint(text, end = text.length) {
258
- for(; end > 0;){
259
- const start = previousCodePointStart(text, end);
260
- const ch = text.slice(start, end);
261
- if (!combiningMarkRe.test(ch)) return ch;
262
- end = start;
263
- }
264
- return null;
265
- }
266
- const lineBreakNumericAffixRanges = [
267
- 0x0024,
268
- 0x0025,
269
- 0x002B,
270
- 0x002B,
271
- 0x005C,
272
- 0x005C,
273
- 0x00A2,
274
- 0x00A5,
275
- 0x00B0,
276
- 0x00B1,
277
- 0x058F,
278
- 0x058F,
279
- 0x0609,
280
- 0x060B,
281
- 0x066A,
282
- 0x066A,
283
- 0x07FE,
284
- 0x07FF,
285
- 0x09F2,
286
- 0x09F3,
287
- 0x09F9,
288
- 0x09FB,
289
- 0x0AF1,
290
- 0x0AF1,
291
- 0x0BF9,
292
- 0x0BF9,
293
- 0x0D79,
294
- 0x0D79,
295
- 0x0E3F,
296
- 0x0E3F,
297
- 0x17DB,
298
- 0x17DB,
299
- 0x2030,
300
- 0x2037,
301
- 0x2057,
302
- 0x2057,
303
- 0x20A0,
304
- 0x20CF,
305
- 0x2103,
306
- 0x2103,
307
- 0x2109,
308
- 0x2109,
309
- 0x2116,
310
- 0x2116,
311
- 0x2212,
312
- 0x2213,
313
- 0xA838,
314
- 0xA838,
315
- 0xFDFC,
316
- 0xFDFC,
317
- 0xFE69,
318
- 0xFE6A,
319
- 0xFF04,
320
- 0xFF05,
321
- 0xFFE0,
322
- 0xFFE1,
323
- 0xFFE5,
324
- 0xFFE6,
325
- 0x11FDD,
326
- 0x11FE0,
327
- 0x1E2FF,
328
- 0x1E2FF,
329
- 0x1ECAC,
330
- 0x1ECAC,
331
- 0x1ECB0,
332
- 0x1ECB0
333
- ];
334
- function isCodePointInRanges(codePoint, ranges) {
335
- for(let i = 0; i < ranges.length; i += 2){
336
- if (codePoint >= ranges[i] && codePoint <= ranges[i + 1]) return true;
337
- }
338
- return false;
339
- }
340
- function isLineBreakNumericAffix(ch) {
341
- const codePoint = ch.codePointAt(0);
342
- return codePoint !== undefined && isCodePointInRanges(codePoint, lineBreakNumericAffixRanges);
343
- }
344
- function endsWithLineBreakNumericAffix(text) {
345
- const last = getLastSignificantCodePoint(text);
346
- return last !== null && isLineBreakNumericAffix(last);
347
- }
348
- function startsWithDecimalDigit(text) {
349
- const first = getFirstSignificantCodePoint(text);
350
- return first !== null && decimalDigitRe.test(first);
351
- }
352
- function splitTrailingForwardStickyCluster(text) {
353
- let splitIndex = text.length;
354
- while(splitIndex > 0){
355
- const start = previousCodePointStart(text, splitIndex);
356
- const ch = text.slice(start, splitIndex);
357
- if (combiningMarkRe.test(ch) || kinsokuEnd.has(ch) || forwardStickyGlue.has(ch)) {
358
- splitIndex = start;
359
- continue;
360
- }
361
- break;
362
- }
363
- if (splitIndex <= 0 || splitIndex === text.length) return null;
364
- return {
365
- head: text.slice(0, splitIndex),
366
- tail: text.slice(splitIndex)
367
- };
368
- }
369
- function getRepeatableSingleCharRunChar(text, isWordLike, kind) {
370
- return kind === 'text' && !isWordLike && text.length === 1 && text !== '-' && text !== '—' ? text : null;
371
- }
372
- function hasArabicNoSpacePunctuation(containsArabic, lastCodePoint) {
373
- return containsArabic && lastCodePoint !== null && arabicNoSpaceTrailingPunctuation.has(lastCodePoint);
374
- }
375
- function endsWithMyanmarMedialGlue(segment) {
376
- const lastCodePoint = getLastCodePoint(segment);
377
- return lastCodePoint !== null && myanmarMedialGlue.has(lastCodePoint);
378
- }
379
- function splitLeadingSpaceAndMarks(segment) {
380
- if (segment.length < 2 || segment[0] !== ' ') return null;
381
- const marks = segment.slice(1);
382
- if (/^\p{M}+$/u.test(marks)) {
383
- return {
384
- space: ' ',
385
- marks
386
- };
387
- }
388
- return null;
389
- }
390
- export function endsWithClosingQuote(text) {
391
- let end = text.length;
392
- while(end > 0){
393
- const start = previousCodePointStart(text, end);
394
- const ch = text.slice(start, end);
395
- if (closingQuoteChars.has(ch)) return true;
396
- if (!leftStickyPunctuation.has(ch)) return false;
397
- end = start;
398
- }
399
- return false;
400
- }
401
- function classifySegmentBreakChar(ch, whiteSpace) {
402
- if (whiteSpace === 'pre-wrap') {
403
- if (ch === ' ') return 'preserved-space';
404
- if (ch === '\t') return 'tab';
405
- if (ch === '\n') return 'hard-break';
406
- }
407
- if (ch === ' ') return 'space';
408
- if (ch === '\u00A0' || ch === '\u202F' || ch === '\u2060' || ch === '\uFEFF') {
409
- return 'glue';
410
- }
411
- if (ch === '\u200B') return 'zero-width-break';
412
- if (ch === '\u00AD') return 'soft-hyphen';
413
- return 'text';
414
- }
415
- const breakCharRe = /[\x20\t\n\xA0\xAD\u200B\u202F\u2060\uFEFF]/;
416
- function joinTextParts(parts) {
417
- return parts.length === 1 ? parts[0] : parts.join('');
418
- }
419
- function joinReversedPrefixParts(prefixParts, tail) {
420
- const parts = [];
421
- for(let i = prefixParts.length - 1; i >= 0; i--){
422
- parts.push(prefixParts[i]);
423
- }
424
- parts.push(tail);
425
- return joinTextParts(parts);
426
- }
427
- function splitSegmentByBreakKind(segment, isWordLike, start, whiteSpace) {
428
- if (!breakCharRe.test(segment)) {
429
- return [
430
- {
431
- text: segment,
432
- isWordLike,
433
- kind: 'text',
434
- start
435
- }
436
- ];
437
- }
438
- const pieces = [];
439
- let currentKind = null;
440
- let currentStart = 0;
441
- let currentWordLike = false;
442
- let offset = 0;
443
- for (const ch of segment){
444
- const kind = classifySegmentBreakChar(ch, whiteSpace);
445
- const wordLike = kind === 'text' && isWordLike;
446
- if (currentKind !== null && kind === currentKind && wordLike === currentWordLike) {
447
- offset += ch.length;
448
- continue;
449
- }
450
- if (currentKind !== null) {
451
- pieces.push({
452
- text: segment.slice(currentStart, offset),
453
- isWordLike: currentWordLike,
454
- kind: currentKind,
455
- start: start + currentStart
456
- });
457
- }
458
- currentKind = kind;
459
- currentStart = offset;
460
- currentWordLike = wordLike;
461
- offset += ch.length;
462
- }
463
- if (currentKind !== null) {
464
- pieces.push({
465
- text: segment.slice(currentStart),
466
- isWordLike: currentWordLike,
467
- kind: currentKind,
468
- start: start + currentStart
469
- });
470
- }
471
- return pieces;
472
- }
473
- function isTextRunBoundary(kind) {
474
- return kind === 'space' || kind === 'preserved-space' || kind === 'zero-width-break' || kind === 'hard-break';
475
- }
476
- const urlSchemeSegmentRe = /^[A-Za-z][A-Za-z0-9+.-]*:$/;
477
- function isUrlLikeRunStart(segmentation, index) {
478
- const text = segmentation.texts[index];
479
- if (text.startsWith('www.')) return true;
480
- return urlSchemeSegmentRe.test(text) && index + 1 < segmentation.len && segmentation.kinds[index + 1] === 'text' && segmentation.texts[index + 1] === '//';
481
- }
482
- function isUrlQueryBoundarySegment(text) {
483
- return text.includes('?') && (text.includes('://') || text.startsWith('www.'));
484
- }
485
- function mergeUrlRuns(segmentation, normalized, profile) {
486
- const texts = [];
487
- const isWordLike = [];
488
- const kinds = [];
489
- const starts = [];
490
- for(let i = 0; i < segmentation.len; i++){
491
- const start = segmentation.starts[i];
492
- let text = segmentation.texts[i];
493
- let wordLike = segmentation.isWordLike[i];
494
- const kind = segmentation.kinds[i];
495
- let queryStartOverride = -1;
496
- if (kind === 'text' && isUrlLikeRunStart(segmentation, i)) {
497
- const urlParts = [
498
- text
499
- ];
500
- let j = i + 1;
501
- while(j < segmentation.len && !isTextRunBoundary(segmentation.kinds[j]) && numericAffixBoundary(normalized, segmentation.starts[j], profile) !== false){
502
- if (queryStartOverride < 0 && isUrlLikeRunStart(segmentation, j)) {
503
- queryStartOverride = segmentation.starts[j];
504
- }
505
- const nextText = segmentation.texts[j];
506
- urlParts.push(nextText);
507
- wordLike = true;
508
- j++;
509
- if (nextText.includes('?')) break;
510
- }
511
- text = joinTextParts(urlParts);
512
- i = j - 1;
513
- }
514
- texts.push(text);
515
- isWordLike.push(wordLike);
516
- kinds.push(kind);
517
- starts.push(start);
518
- if (!isUrlQueryBoundarySegment(text)) continue;
519
- const nextIndex = i + 1;
520
- if (nextIndex >= segmentation.len || isTextRunBoundary(segmentation.kinds[nextIndex])) {
521
- continue;
522
- }
523
- const queryParts = [];
524
- const queryStart = queryStartOverride < 0 ? segmentation.starts[nextIndex] : queryStartOverride;
525
- let j = nextIndex;
526
- while(j < segmentation.len && !isTextRunBoundary(segmentation.kinds[j]) && numericAffixBoundary(normalized, segmentation.starts[j], profile) !== false){
527
- queryParts.push(segmentation.texts[j]);
528
- j++;
529
- }
530
- if (queryParts.length > 0) {
531
- texts.push(joinTextParts(queryParts));
532
- isWordLike.push(true);
533
- kinds.push('text');
534
- starts.push(queryStart);
535
- i = j - 1;
536
- }
537
- }
538
- return {
539
- len: texts.length,
540
- texts,
541
- isWordLike,
542
- kinds,
543
- starts
544
- };
545
- }
546
- const numericJoinerChars = new Set([
547
- ':',
548
- '-',
549
- '/',
550
- '×',
551
- ',',
552
- '.',
553
- '+',
554
- '\u2013',
555
- '\u2014'
556
- ]);
557
- const wordInternalSymbolRe = /[\p{P}\p{S}\p{Co}]/u;
558
- const emojiPresentationRe = /\p{Emoji_Presentation}/u;
559
- const noSpaceWordBreakAfterChars = new Set([
560
- '?',
561
- '\u058A',
562
- '-',
563
- '\u2010',
564
- '\u2012',
565
- '\u2013',
566
- '\u2014',
567
- '\u2026',
568
- '\u203C',
569
- '\u203D',
570
- '\u2049'
571
- ]);
572
- function isAsciiWordInternalSymbolCode(code) {
573
- return code >= 0x21 && code <= 0x2F && code !== 0x2D || code >= 0x3A && code <= 0x40 && code !== 0x3F || code >= 0x5B && code <= 0x60 || code >= 0x7B && code <= 0x7E;
574
- }
575
- function isNoSpaceWordInternalSymbol(ch) {
576
- const code = ch.charCodeAt(0);
577
- if (code < 0x80) return isAsciiWordInternalSymbolCode(code);
578
- return !noSpaceWordBreakAfterChars.has(ch) && !emojiPresentationRe.test(ch) && wordInternalSymbolRe.test(ch);
579
- }
580
- function isNoSpaceWordInternalSymbolSegment(text) {
581
- let sawSymbol = false;
582
- for (const ch of text){
583
- if (combiningMarkRe.test(ch)) continue;
584
- if (!isNoSpaceWordInternalSymbol(ch)) return false;
585
- sawSymbol = true;
586
- }
587
- return sawSymbol;
588
- }
589
- function endsWithNoSpaceWordJoiner(text) {
590
- for(let end = text.length; end > 0;){
591
- const start = previousCodePointStart(text, end);
592
- const ch = text.slice(start, end);
593
- if (combiningMarkRe.test(ch)) {
594
- end = start;
595
- continue;
596
- }
597
- return isNoSpaceWordInternalSymbol(ch) || isLineBreakNumericAffix(ch);
598
- }
599
- return false;
600
- }
601
- const asciiAlphabeticBoundaryRe = /[A-Za-z#&*<=>@^_`~]/;
602
- function isAsciiBoundary(left, right) {
603
- return left.charCodeAt(0) < 0x80 && right.charCodeAt(0) < 0x80;
604
- }
605
- function numericAffixBoundary(source, boundary, profile) {
606
- if (!profile.geckoAsciiLineBreaks || boundary <= 0 || boundary >= source.length) return null;
607
- const left = getLastSignificantCodePoint(source, boundary);
608
- const right = String.fromCodePoint(source.codePointAt(boundary));
609
- if (left === null || !isAsciiBoundary(left, right)) return null;
610
- const leftAffix = isLineBreakNumericAffix(left);
611
- const rightAffix = isLineBreakNumericAffix(right);
612
- if (!leftAffix && !rightAffix) return null;
613
- if (right === ')' || right === ']' || right === '}' || '!,.:;?/'.includes(right)) return true;
614
- if ('([{'.includes(left) || left === '"' || left === "'" || right === '"' || right === "'") return true;
615
- if (right === '|' || right === '-') return true;
616
- if (leftAffix && asciiAlphabeticBoundaryRe.test(right) || asciiAlphabeticBoundaryRe.test(left) && rightAffix) return true;
617
- if (rightAffix && (')]}'.includes(left) || /[0-9]/.test(left))) return true;
618
- if (leftAffix && ('([{'.includes(right) || /[0-9]/.test(right))) return true;
619
- return false;
620
- }
621
- function openingPunctuationJoinsPrevious(left, right, profile, leftEnd = left.length) {
622
- const first = right[0];
623
- if (first === undefined || !'([{'.includes(first)) return null;
624
- const last = getLastSignificantCodePoint(left, leftEnd);
625
- if (last === null) return null;
626
- if (profile.geckoAsciiLineBreaks && isAsciiBoundary(last, first)) {
627
- return asciiAlphabeticBoundaryRe.test(last) || decimalDigitRe.test(last) || '"\'([{'.includes(last) || isLineBreakNumericAffix(last);
628
- }
629
- if (last.charCodeAt(0) >= 0x80) {
630
- return !isCJK(last) && /[\p{L}\p{N}]/u.test(last) ? true : null;
631
- }
632
- return /[A-Za-z0-9]/.test(last) || "$'(/<@[^_`{".includes(last);
633
- }
634
- function canJoinNoSpaceWordBoundary(leftText, leftWordLike, rightText, rightWordLike, profile) {
635
- if (rightText[0] === '-' && isCJK(leftText)) return true;
636
- if (isCJK(leftText) || isCJK(rightText)) return false;
637
- const openingJoin = openingPunctuationJoinsPrevious(leftText, rightText, profile);
638
- if (openingJoin !== null) return openingJoin;
639
- const leftSymbol = !leftWordLike && isNoSpaceWordInternalSymbolSegment(leftText);
640
- const rightSymbol = !rightWordLike && isNoSpaceWordInternalSymbolSegment(rightText);
641
- const leftAffix = endsWithLineBreakNumericAffix(leftText);
642
- const leftEndsJoiner = (leftWordLike || leftAffix) && endsWithNoSpaceWordJoiner(leftText);
643
- if (!leftSymbol && !rightSymbol && !leftEndsJoiner) return false;
644
- return (leftWordLike || leftSymbol || leftAffix) && (rightWordLike || rightSymbol);
645
- }
646
- function segmentContainsDecimalDigit(text) {
647
- for (const ch of text){
648
- if (decimalDigitRe.test(ch)) return true;
649
- }
650
- return false;
651
- }
652
- export function isNumericRunSegment(text) {
653
- if (text.length === 0) return false;
654
- for (const ch of text){
655
- if (decimalDigitRe.test(ch) || numericJoinerChars.has(ch)) continue;
656
- return false;
657
- }
658
- return true;
659
- }
660
- function mergeNumericRuns(segmentation, normalized, profile) {
661
- const texts = [];
662
- const isWordLike = [];
663
- const kinds = [];
664
- const starts = [];
665
- function pushNumericRun(text, start) {
666
- if (text.includes('-')) {
667
- const parts = text.split('-');
668
- let shouldSplit = parts.length > 1;
669
- for(let i = 0; i < parts.length; i++){
670
- const part = parts[i];
671
- if (!shouldSplit) break;
672
- if (part.length === 0 || !segmentContainsDecimalDigit(part) || !isNumericRunSegment(part)) {
673
- shouldSplit = false;
674
- }
675
- }
676
- if (shouldSplit) {
677
- let offset = 0;
678
- for(let i = 0; i < parts.length; i++){
679
- const part = parts[i];
680
- const splitText = i < parts.length - 1 ? `${part}-` : part;
681
- texts.push(splitText);
682
- isWordLike.push(true);
683
- kinds.push('text');
684
- starts.push(start + offset);
685
- offset += splitText.length;
686
- }
687
- return;
688
- }
689
- }
690
- texts.push(text);
691
- isWordLike.push(true);
692
- kinds.push('text');
693
- starts.push(start);
694
- }
695
- for(let i = 0; i < segmentation.len; i++){
696
- const text = segmentation.texts[i];
697
- const kind = segmentation.kinds[i];
698
- if (kind === 'text' && isNumericRunSegment(text) && segmentContainsDecimalDigit(text)) {
699
- const mergedParts = [
700
- text
701
- ];
702
- let j = i + 1;
703
- while(j < segmentation.len && segmentation.kinds[j] === 'text' && isNumericRunSegment(segmentation.texts[j]) && numericAffixBoundary(normalized, segmentation.starts[j], profile) !== false){
704
- mergedParts.push(segmentation.texts[j]);
705
- j++;
706
- }
707
- pushNumericRun(joinTextParts(mergedParts), segmentation.starts[i]);
708
- i = j - 1;
709
- continue;
710
- }
711
- texts.push(text);
712
- isWordLike.push(segmentation.isWordLike[i]);
713
- kinds.push(kind);
714
- starts.push(segmentation.starts[i]);
715
- }
716
- return {
717
- len: texts.length,
718
- texts,
719
- isWordLike,
720
- kinds,
721
- starts
722
- };
723
- }
724
- function mergeNoSpaceWordChains(segmentation, normalized, profile) {
725
- const texts = [];
726
- const isWordLike = [];
727
- const kinds = [];
728
- const starts = [];
729
- let i = 0;
730
- while(i < segmentation.len){
731
- const text = segmentation.texts[i];
732
- const kind = segmentation.kinds[i];
733
- const wordLike = segmentation.isWordLike[i];
734
- if (kind === 'text') {
735
- const mergedParts = [
736
- text
737
- ];
738
- let j = i + 1;
739
- let mergedWordLike = wordLike;
740
- while(j < segmentation.len && segmentation.kinds[j] === 'text' && (numericAffixBoundary(normalized, segmentation.starts[j], profile) ?? canJoinNoSpaceWordBoundary(segmentation.texts[j - 1], segmentation.isWordLike[j - 1], segmentation.texts[j], segmentation.isWordLike[j], profile))){
741
- const nextText = segmentation.texts[j];
742
- mergedParts.push(nextText);
743
- mergedWordLike = mergedWordLike || segmentation.isWordLike[j];
744
- j++;
745
- }
746
- if (j > i + 1) {
747
- texts.push(joinTextParts(mergedParts));
748
- isWordLike.push(mergedWordLike);
749
- kinds.push('text');
750
- starts.push(segmentation.starts[i]);
751
- i = j;
752
- continue;
753
- }
754
- }
755
- texts.push(text);
756
- isWordLike.push(wordLike);
757
- kinds.push(kind);
758
- starts.push(segmentation.starts[i]);
759
- i++;
760
- }
761
- return {
762
- len: texts.length,
763
- texts,
764
- isWordLike,
765
- kinds,
766
- starts
767
- };
768
- }
769
- function mergeGlueConnectedTextRuns(segmentation) {
770
- const texts = [];
771
- const isWordLike = [];
772
- const kinds = [];
773
- const starts = [];
774
- let read = 0;
775
- while(read < segmentation.len){
776
- const textParts = [
777
- segmentation.texts[read]
778
- ];
779
- let wordLike = segmentation.isWordLike[read];
780
- let kind = segmentation.kinds[read];
781
- let start = segmentation.starts[read];
782
- if (kind === 'glue') {
783
- const glueParts = [
784
- textParts[0]
785
- ];
786
- const glueStart = start;
787
- read++;
788
- while(read < segmentation.len && segmentation.kinds[read] === 'glue'){
789
- glueParts.push(segmentation.texts[read]);
790
- read++;
791
- }
792
- const glueText = joinTextParts(glueParts);
793
- if (read < segmentation.len && segmentation.kinds[read] === 'text') {
794
- textParts[0] = glueText;
795
- textParts.push(segmentation.texts[read]);
796
- wordLike = segmentation.isWordLike[read];
797
- kind = 'text';
798
- start = glueStart;
799
- read++;
800
- } else {
801
- texts.push(glueText);
802
- isWordLike.push(false);
803
- kinds.push('glue');
804
- starts.push(glueStart);
805
- continue;
806
- }
1
+ /*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
2
+ import { getGeckoLineBreaks, isDiscardable, isEastAsianSegmentBreak, isJapaneseOrChinese, isSpaceCombiningSequenceTail, isSpaceOrTabOrSegmentBreak } from './gecko-line-breaks.js'
3
+ import { isBidiControl, } from './graphemes.js'
4
+ import { BREAK, CLUSTER_START, FORCED_BREAK, ITEM_START, SOFT_HYPHEN_BREAK, getBlinkLineBreaks, getWebKitBreakBetweenItems, getWebKitLineBreaks } from './line-breaks.js'
5
+
6
+
7
+
8
+
9
+ // No `glue` kind: no-break characters are text and the scans decide their breaks
10
+ // (RESEARCH.md, Decisions Log, 2026-09-24).
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+ // A segment's flags byte: its kind's code in the low four bits, then what else the
25
+ // walkers read of it.
26
+ export const TEXT = 0
27
+ export const SPACE = 1
28
+ export const ZERO_WIDTH_BREAK = 2
29
+ export const SOFT_HYPHEN = 3
30
+ export const PRESERVED_SPACE = 4
31
+ export const TAB = 5
32
+ export const ZERO_WIDTH_GLUE = 6
33
+ export const CONTROL = 7
34
+ // Ends its chunk: a line's walk stops there, and the next line starts after it.
35
+ export const HARD_BREAK = 8
36
+ // In a rich-inline paragraph's handle (src/rich-inline.ts), an atomic item or a box: one object with
37
+ // a break on both sides and none inside, which a line fits and paints as it does text. The start
38
+ // edge of a padded item that opens with white space, a hard break or a zero-width space is one too,
39
+ // with no break before it where the text has none there. No analysis makes one.
40
+ export const OBJECT = 9
41
+ export const KIND_BITS = 0x0F
42
+ // The segment takes letter spacing after its graphemes. Set by measurement.
43
+ export const SPACED = 0x10
44
+ // The engine's scan gives no break before the segment, so no line ends there.
45
+ export const UNBROKEN = 0x20
46
+ // The scan gives a break before the segment, in text that also has unbroken
47
+ // boundaries, where a line that overflows at one returns to the latest such break.
48
+ export const RETURNABLE = 0x40
49
+ // The engine's clusters don't split the segment, so no emergency break splits it
50
+ // either. Measurement clears it.
51
+ export const ONE_CLUSTER = 0x80
52
+
53
+
54
+ // Each kind's name by its code, as prepareWithSegments() gives them.
55
+ export const SEGMENT_KINDS = [
56
+ 'text', 'space', 'zero-width-break', 'soft-hyphen', 'preserved-space', 'tab', 'zero-width-glue', 'control', 'hard-break',
57
+ ]
58
+
59
+ // `spaceSources` holds, in the WebKit profile where normal white space collapsed, the source
60
+ // unit each normalized unit starts from, such as the TAB or LF a space came from. Null otherwise.
61
+ // `texts` holds each segment's text, `starts` where it starts in `normalized`, and `flags` its
62
+ // flags byte: its kind, UNBROKEN where the engine's scan gives no break before text, zero-width
63
+ // glue or a control, other than at a line start, and where tabs don't hang before a tab or
64
+ // the spaces after one, RETURNABLE at the other segments of text with such a boundary, which
65
+ // `hasUnbroken` tells, and ONE_CLUSTER where the scan has clusters of its own.
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+ // The items of a rich-inline paragraph in its text (src/rich-inline.ts): where each starts there,
82
+ // whether it is atomic, one U+FFFC in the text, as Blink and Gecko put an atomic inline in a
83
+ // paragraph's text (inline_node.cc:408-422, nsBidiPresUtils.cpp:1385-1396), and whether the engine
84
+ // transforms segment breaks in each item's text apart (EngineProfile,
85
+ // transformsSegmentBreaksAcrossItems). Every item starts a segment, and an atomic item is a segment
86
+ // with a break on both sides. The analysis leaves in `sourceOffsets` the offset in the text that
87
+ // each unit of its normalized text comes from. `items` holds the items themselves, whose own texts
88
+ // WebKit's scan reads (getWebKitParagraphBreaks).
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+ // The runs that collapsing changes: a lone space is already the space its run becomes.
98
+ const collapsibleWhitespaceRunRe = /[ \t\n\r\f]{2,}|[\t\n\r\f]/g
99
+ const needsWhitespaceNormalizationRe = /[\t\n\r\f]| {2,}|^ | $/
100
+
101
+ function isSegmentBreakRunSpace(code , scan ) {
102
+ return code === 0x20 || code === 0x09 || code === 0x0A || (code === 0x0D && scan === 'blink')
103
+ }
104
+
105
+ // CSS segment break transformation in normal white space. Blink and Gecko
106
+ // delete a collapsible run containing LF when a ZWSP immediately precedes or
107
+ // follows the run; WebKit turns it into a space. Gecko also deletes it between two
108
+ // East Asian wide characters other than Hangul, and on a `ja` or `zh` page next to
109
+ // East Asian punctuation, reading past default-ignorable characters on both sides
110
+ // (TransformWhiteSpaces, nsTextFrameUtils.cpp:84-209, with the predicates of
111
+ // nsUnicharUtils.cpp:500-527). Each engine collects its own run, and engines Pretext
112
+ // doesn't recognize take Blink's, as they take its scan:
113
+ // - Blink: SPACE, TAB, LF and CR.
114
+ // - Gecko: SPACE, TAB and LF, continuing through the characters Gecko discards
115
+ // (SHY and bidi controls) without ending on one, and leaving out a last SPACE
116
+ // before a combining sequence tail. Text holding a ZWSP is 16-bit in Gecko.
117
+ // Characters outside the run, such as FF, keep the ordinary collapse, or take no room in the
118
+ // Gecko profile (analyzeText).
119
+ export function removeSkippableSegmentBreaks(text , profile , language = null) {
120
+ const scan = profile.lineBreakScan
121
+ if (scan === 'webkit' || !text.includes('\n')) return text
122
+ const eastAsian = scan === 'gecko' && maybeEastAsianRe.test(text)
123
+ if (!eastAsian && !text.includes('\u200B')) return text
124
+ const japaneseOrChinese = eastAsian && isJapaneseOrChinese(language)
125
+ let result = ''
126
+ let copied = 0
127
+ // Only a run containing LF can be removed. Expand each LF to its run once;
128
+ // the next search starts where this run's scan stopped.
129
+ for (let newline = text.indexOf('\n'); newline !== -1;) {
130
+ let start = newline
131
+ for (let index = newline - 1; index >= 0; index--) {
132
+ const code = text.charCodeAt(index)
133
+ if (isSegmentBreakRunSpace(code, scan)) start = index
134
+ else if (!(scan === 'gecko' && isDiscardable(code, false))) break
135
+ }
136
+ let end = newline + 1
137
+ let index = end
138
+ for (; index < text.length; index++) {
139
+ const code = text.charCodeAt(index)
140
+ if (isSegmentBreakRunSpace(code, scan)) end = index + 1
141
+ else if (!(scan === 'gecko' && isDiscardable(code, false))) break
142
+ }
143
+ newline = text.indexOf('\n', index)
144
+ if (scan === 'gecko' && text.charCodeAt(end - 1) === 0x20 && isSpaceCombiningSequenceTail(text, end)) end--
145
+ if (text.charCodeAt(start - 1) !== 0x200B && text.charCodeAt(end) !== 0x200B &&
146
+ !(eastAsian && isEastAsianSegmentBreak(text, start, end, japaneseOrChinese))) continue
147
+ result += text.slice(copied, start)
148
+ for (let member = start; member < end; member++) {
149
+ if (!isSegmentBreakRunSpace(text.charCodeAt(member), scan)) result += text[member]
150
+ }
151
+ copied = end
152
+ }
153
+ return copied === 0 ? text : result + text.slice(copied)
154
+ }
155
+
156
+ // Where the collapsible white space that ends text[from, text.length) starts, which the analysis
157
+ // leaves out and a line end trims, or the text's length where none ends it. Gecko's white-space
158
+ // run reads through the bidi controls in it and after it (TransformText, nsTextFrameUtils.cpp:
159
+ // 319-345), so there it is the first white space after the last character that is neither, and
160
+ // the controls stay.
161
+ function getTrailingCollapsibleStart(text , from , profile ) {
162
+ let start = text.length
163
+ for (let i = text.length - 1; i >= from; i--) {
164
+ const code = text.charCodeAt(i)
165
+ if (isCollapsibleSpaceCode(code)) start = i
166
+ else if (!(profile.lineBreakScan === 'gecko' && isBidiControl(code))) break
167
+ }
168
+ return start
169
+ }
170
+
171
+ // Every East Asian wide, fullwidth or halfwidth character is at or above U+1100.
172
+ const maybeEastAsianRe = /[\u1100-\uFFFF]/
173
+
174
+ // Normal white space after the segment break transformation: each run of SPACE, TAB, LF,
175
+ // CR and FF becomes one space, or nothing at either end.
176
+ function collapseWhitespaceNormal(text ) {
177
+ if (!needsWhitespaceNormalizationRe.test(text)) return text
178
+
179
+ let normalized = text.replace(collapsibleWhitespaceRunRe, ' ')
180
+ if (normalized.charCodeAt(0) === 0x20) {
181
+ normalized = normalized.slice(1)
182
+ }
183
+ if (normalized.length > 0 && normalized.charCodeAt(normalized.length - 1) === 0x20) {
184
+ normalized = normalized.slice(0, -1)
185
+ }
186
+ return normalized
187
+ }
188
+
189
+ function normalizeWhitespacePreWrap(text ) {
190
+ if (!/[\r\f]/.test(text)) return text
191
+ return text
192
+ .replace(/\r\n/g, '\n')
193
+ .replace(/[\r\f]/g, '\n')
194
+ }
195
+
196
+ const combiningMarkRe = /\p{M}/u
197
+
198
+ function classifySegmentBreakCode(code , whiteSpace , scan ) {
199
+ if (whiteSpace === 'pre-wrap') {
200
+ if (code === 0x20) return PRESERVED_SPACE
201
+ if (code === 0x09) return TAB
202
+ if (code === 0x0A) return HARD_BREAK
203
+ }
204
+ if (code === 0x20) return SPACE
205
+ if (code === 0x200B) return ZERO_WIDTH_BREAK
206
+ if (code === 0x00AD) return SOFT_HYPHEN
207
+ // NEL (UAX #14 NL) offers a break after itself and no ordinary break before it (LB5, LB6),
208
+ // as the scans find. The WebKit profile gives NEL its own control segment for letter
209
+ // spacing: WebKit's simple text path gives NEL no letter spacing, at either sign, and its
210
+ // complex path spaces it. A NEL control segment takes spacing after text in WebKit's
211
+ // complex ranges, or before such text that starts with a combining mark. Preparation
212
+ // cannot see the page direction, so after complex text whose direction differs from the
213
+ // page's it keeps spacing Safari omits. Blink spaces NEL outside cursive runs, and release
214
+ // Gecko draws NEL with no advance while its Canvas measures a space, so both keep NEL as
215
+ // ordinary text.
216
+ if (code === 0x0085 && scan === 'webkit') return CONTROL
217
+ return TEXT
218
+ }
219
+
220
+ export function isCollapsibleSpaceCode(code ) {
221
+ return code === 0x20 || code === 0x09 || code === 0x0A || code === 0x0D || code === 0x0C
222
+ }
223
+
224
+ // WebKit and Gecko scan a text node's source, where normalization collapsed white space: in
225
+ // normal white space each run of SPACE, TAB, LF, CR and FF became one space, or nothing
226
+ // at either end, and in pre-wrap CRLF became LF. A break before a run's first unit is a
227
+ // break before what the run became. A break before a later unit, as before a CR after a
228
+ // space, follows white space, so it is a break after what the run became. A mark without a
229
+ // break goes with its unit: Gecko's cluster start, which only a unit that stays text reads,
230
+ // and WebKit's forced break after a separator, which keeps its mark at the end too, also
231
+ // where the white space that ends the text starts at a break: at an item's edge in a
232
+ // paragraph, or after a lone CR the analysis took out, whose mark it took (RESEARCH.md,
233
+ // Decisions Log, 2026-10-07).
234
+ // Fills spaceSources, when given, in normal white space.
235
+ function mapSourceLineBreaks(source , normalizedLength , sourceBreaks , whiteSpace , spaceSources ) {
236
+ const breaks = new Uint8Array(normalizedLength + 1)
237
+ let normalizedIndex = 0
238
+ if (whiteSpace === 'pre-wrap') {
239
+ for (let i = 0; i < source.length; i++, normalizedIndex++) {
240
+ breaks[normalizedIndex] = sourceBreaks[i]
241
+ if (source.charCodeAt(i) === 0x0D && source.charCodeAt(i + 1) === 0x0A) {
242
+ i++
243
+ if ((sourceBreaks[i] & BREAK) !== 0) breaks[normalizedIndex] = sourceBreaks[i]
244
+ }
245
+ }
246
+ breaks[normalizedLength] = sourceBreaks[source.length]
247
+ return breaks
248
+ }
249
+ let i = 0
250
+ while (i < source.length && isCollapsibleSpaceCode(source.charCodeAt(i))) i++
251
+ while (i < source.length) {
252
+ let end = i + 1
253
+ if (isCollapsibleSpaceCode(source.charCodeAt(i))) {
254
+ while (end < source.length && isCollapsibleSpaceCode(source.charCodeAt(end))) end++
255
+ if (end === source.length) break
256
+ }
257
+ if ((sourceBreaks[i] & BREAK) === 0 && breaks[normalizedIndex] === 0) breaks[normalizedIndex] = sourceBreaks[i]
258
+ if (spaceSources !== null) spaceSources[normalizedIndex] = source.charCodeAt(i)
259
+ const start = i
260
+ for (; i < end; i++) {
261
+ if ((sourceBreaks[i] & BREAK) !== 0) breaks[i === start ? normalizedIndex : normalizedIndex + 1] = sourceBreaks[i]
262
+ }
263
+ normalizedIndex++
264
+ }
265
+ breaks[normalizedLength] = (sourceBreaks[i] & BREAK) === 0 ? sourceBreaks[i] : sourceBreaks[i] & FORCED_BREAK
266
+ return breaks
267
+ }
268
+
269
+ // A FORCED_BREAK after U+2028 or U+2029 makes the separator a hard break in every white-space
270
+ // mode, and a SOFT_HYPHEN_BREAK makes a soft hyphen a zero-width break: the line can end there
271
+ // without a hyphen. One with only soft hyphens before it on its chunk stays a soft hyphen, since
272
+ // a zero-width break there holds a line and Firefox, which drops soft hyphens from its text runs,
273
+ // gives it none.
274
+ function classifySegmentUnit(normalized , breaks , i , code , whiteSpace , scan ) {
275
+ if ((code === 0x2028 || code === 0x2029) && (breaks[i + 1] & FORCED_BREAK) !== 0) return HARD_BREAK
276
+ if (code === 0x00AD && (breaks[i + 1] & SOFT_HYPHEN_BREAK) !== 0 && followsChunkContent(normalized, i)) return ZERO_WIDTH_BREAK
277
+ return classifySegmentBreakCode(code, whiteSpace, scan)
278
+ }
279
+
280
+ function followsChunkContent(normalized , i ) {
281
+ let j = i - 1
282
+ while (j >= 0 && normalized.charCodeAt(j) === 0x00AD) j--
283
+ return j >= 0 && normalized.charCodeAt(j) !== 0x0A
284
+ }
285
+
286
+ // Characters of these kinds share a segment when no break falls between them. Each
287
+ // tab, hard break, ZWSP and NEL control stays its own segment.
288
+ function gathersKind(kind ) {
289
+ return kind === TEXT || kind === SPACE || kind === PRESERVED_SPACE || kind === SOFT_HYPHEN
290
+ }
291
+
292
+ // A control character that stays its own text segment, measured alone: the C0 and C1
293
+ // controls that white-space normalization leaves as text, and the line and paragraph
294
+ // separators where they don't end a line.
295
+ function isControlSegmentCode(code ) {
296
+ return code < 0x20 || (code >= 0x7F && code <= 0x9F) || code === 0x2028 || code === 0x2029
297
+ }
298
+
299
+ // Segments are the text between an engine's break opportunities, split where the
300
+ // break kind changes and where an item of a rich-inline paragraph starts (ITEM_START), and a
301
+ // control stays alone. A ZWSP or soft hyphen that the scan
302
+ // doesn't break after, as at the start of a WebKit scan, before a combining mark or a
303
+ // closing bracket, or under keep-all, is zero-width glue: it stays its own zero-width
304
+ // segment, takes no letter spacing and doesn't end a line.
305
+ // Combining marks right after it, or after a control, stay apart from the text after
306
+ // them, since they shape on the grapheme before it (measureAnalysis). Where the Gecko
307
+ // scan marks cluster starts, a segment is ONE_CLUSTER unless one falls inside it.
308
+ //
309
+ // Firefox leaves soft hyphens and bidi controls out of the text run it breaks and maps a line end
310
+ // past the characters it left out (IsDiscardable, nsTextFrameUtils.cpp:32-49; nsTextFrame.cpp:
311
+ // 11161-11170), so a line ends after a run of them, never before one, and none starts with one
312
+ // after a wrap. Where the Gecko scan's text run left out a bidi control, a run of such characters
313
+ // with a control in it goes with the segment before it up to its last control: the scan finds no
314
+ // break inside the run and gives the one after it after the run. A soft hyphen after the last
315
+ // control keeps its break, and one before it offers none (GetHyphenationBreaks,
316
+ // nsTextFrame.cpp:4436-4442). A run that starts a chunk is a text segment that starts its line,
317
+ // with the text after it: Firefox trims no leading space there, since the content doesn't start
318
+ // with one (nsTextFrame.cpp:10935-10950), and the break the scan gives after the run, after a hard
319
+ // break or collapsed leading white space, can't end a line that holds nothing yet. A chunk of only
320
+ // such a run is no content of its own, so the hard break before it takes it (nsTextFrame.cpp:
321
+ // 11421-11429). After a control or marks that stay alone, the run starts a text segment. The
322
+ // profile's graphemes look past these characters (src/graphemes.ts), so a cluster extender after
323
+ // them joins the cluster before, and the text after them goes on in the segment as without them.
324
+ // A run of them ends where an item of a rich-inline paragraph starts inside it, and an item that
325
+ // starts with one starts a text segment with the text after it, as a chunk does, after the break
326
+ // the scan gives at the run, so its fragments keep the controls it starts with.
327
+ function segmentAtLineBreaks(normalized , spaceSources , breaks , whiteSpace , scan , hangTabs , dropsBidiControl ) {
328
+ const oneCluster = scan === 'gecko' ? ONE_CLUSTER : 0
329
+ const starts = []
330
+ // A plain array, which measurement copies into the prepared handle's bytes: a Uint8Array for
331
+ // each text slowed short texts' preparation (RESEARCH.md, Keeping Work Bounded).
332
+ const flags = []
333
+ let lastAlone = false
334
+ let markRun = false
335
+ // Where the last run of what the text run drops, handled at its start, ends.
336
+ let droppedEnd = -1
337
+ // The last segment's kind, -1 before the first unit, which starts one.
338
+ let lastKind = -1
339
+ for (let i = 0; i < normalized.length; i++) {
340
+ const code = normalized.charCodeAt(i)
341
+ if (dropsBidiControl && i < droppedEnd) continue
342
+ if (dropsBidiControl && isDiscardable(code, false) && (i === 0 || (breaks[i] & ITEM_START) !== 0 || !isDiscardable(normalized.charCodeAt(i - 1), false))) {
343
+ // The run of what the text run drops from here, and where its last bidi control ends.
344
+ let j = i
345
+ let controlEnd = -1
346
+ for (; j < normalized.length && isDiscardable(normalized.charCodeAt(j), false) && (j === i || (breaks[j] & ITEM_START) === 0); j++) if (isBidiControl(normalized.charCodeAt(j))) controlEnd = j + 1
347
+ if (controlEnd > 0) {
348
+ const startsItem = lastKind >= 0 && lastKind !== HARD_BREAK && (breaks[i] & ITEM_START) !== 0
349
+ const chunkStart = lastKind < 0 || lastKind === HARD_BREAK || startsItem
350
+ if (chunkStart) {
351
+ const endsChunk = j === normalized.length || classifySegmentUnit(normalized, breaks, j, normalized.charCodeAt(j), whiteSpace, scan) === HARD_BREAK
352
+ if (!endsChunk) {
353
+ if (startsItem) breaks[i] = breaks[i] | (breaks[j] & BREAK)
354
+ breaks[j] = breaks[j] & ~(BREAK | SOFT_HYPHEN_BREAK)
355
+ }
356
+ droppedEnd = j
357
+ if (endsChunk && lastKind >= 0) continue
807
358
  } else {
808
- read++;
809
- }
810
- if (kind === 'text') {
811
- while(read < segmentation.len && segmentation.kinds[read] === 'glue'){
812
- const glueParts = [];
813
- while(read < segmentation.len && segmentation.kinds[read] === 'glue'){
814
- glueParts.push(segmentation.texts[read]);
815
- read++;
816
- }
817
- const glueText = joinTextParts(glueParts);
818
- if (read < segmentation.len && segmentation.kinds[read] === 'text') {
819
- textParts.push(glueText, segmentation.texts[read]);
820
- wordLike = wordLike || segmentation.isWordLike[read];
821
- read++;
822
- continue;
823
- }
824
- textParts.push(glueText);
825
- }
826
- }
827
- texts.push(joinTextParts(textParts));
828
- isWordLike.push(wordLike);
829
- kinds.push(kind);
830
- starts.push(start);
831
- }
832
- return {
833
- len: texts.length,
834
- texts,
835
- isWordLike,
836
- kinds,
837
- starts
838
- };
839
- }
840
- function carryTrailingForwardStickyAcrossCJKBoundary(segmentation) {
841
- const { texts, kinds, starts } = segmentation;
842
- for(let i = 0; i < texts.length - 1; i++){
843
- if (kinds[i] !== 'text' || kinds[i + 1] !== 'text') continue;
844
- if (!isCJK(texts[i]) || !isCJK(texts[i + 1])) continue;
845
- const split = splitTrailingForwardStickyCluster(texts[i]);
846
- if (split === null) continue;
847
- texts[i] = split.head;
848
- texts[i + 1] = split.tail + texts[i + 1];
849
- starts[i + 1] = starts[i] + split.head.length;
850
- }
851
- }
852
- function buildMergedSegmentation(normalized, profile, whiteSpace) {
853
- const wordSegmenter = getSharedWordSegmenter();
854
- let mergedLen = 0;
855
- const mergedTexts = [];
856
- const mergedWordLike = [];
857
- const mergedKinds = [];
858
- const mergedStarts = [];
859
- let hasTail = false;
860
- let tailStart = 0;
861
- let tailEnd = 0;
862
- let tailWordLike = false;
863
- let tailKind = 'text';
864
- let tailSingleCharRunChar = null;
865
- let tailContainsCJK = false;
866
- let tailContainsArabicScript = false;
867
- let tailEndsWithClosingQuote = false;
868
- let tailEndsWithMyanmarMedialGlue = false;
869
- let tailHasArabicNoSpacePunctuation = false;
870
- for (const s of wordSegmenter.segment(normalized)){
871
- for (const piece of splitSegmentByBreakKind(s.segment, s.isWordLike ?? false, s.index, whiteSpace)){
872
- const isText = piece.kind === 'text';
873
- const repeatableSingleCharRunChar = getRepeatableSingleCharRunChar(piece.text, piece.isWordLike, piece.kind);
874
- const pieceContainsCJK = isCJK(piece.text);
875
- const pieceContainsArabicScript = containsArabicScript(piece.text);
876
- const pieceLastCodePoint = getLastCodePoint(piece.text);
877
- const pieceEndsWithClosingQuote = endsWithClosingQuote(piece.text);
878
- const pieceEndsWithMyanmarMedialGlue = endsWithMyanmarMedialGlue(piece.text);
879
- const pieceEnd = piece.start + piece.text.length;
880
- const boundaryJoin = numericAffixBoundary(normalized, piece.start, profile) ?? (tailContainsCJK || pieceContainsCJK ? null : openingPunctuationJoinsPrevious(normalized, piece.text, profile, piece.start));
881
- let appendToTail = false;
882
- if (profile.carryCJKAfterClosingQuote && isText && hasTail && tailKind === 'text' && pieceContainsCJK && tailContainsCJK && tailEndsWithClosingQuote) {
883
- appendToTail = true;
884
- } else if (isText && hasTail && tailKind === 'text' && isCJKLineStartProhibitedSegment(piece.text) && tailContainsCJK) {
885
- appendToTail = true;
886
- } else if (isText && hasTail && tailKind === 'text' && tailEndsWithMyanmarMedialGlue) {
887
- appendToTail = true;
888
- } else if (isText && hasTail && tailKind === 'text' && piece.isWordLike && pieceContainsArabicScript && tailHasArabicNoSpacePunctuation) {
889
- appendToTail = true;
890
- } else if (repeatableSingleCharRunChar !== null && hasTail && tailKind === 'text' && tailSingleCharRunChar === repeatableSingleCharRunChar && boundaryJoin !== false) {
891
- tailEnd = pieceEnd;
892
- continue;
893
- } else if (isText && !piece.isWordLike && hasTail && tailKind === 'text' && !tailContainsCJK && (isLeftStickyPunctuationSegment(piece.text) || piece.text === '-' && tailWordLike)) {
894
- appendToTail = true;
895
- }
896
- if (isText && hasTail && tailKind === 'text' && boundaryJoin !== null) appendToTail = boundaryJoin;
897
- if (appendToTail) {
898
- tailEnd = pieceEnd;
899
- tailWordLike = tailWordLike || piece.isWordLike;
900
- tailSingleCharRunChar = null;
901
- tailContainsCJK = tailContainsCJK || pieceContainsCJK;
902
- tailContainsArabicScript = tailContainsArabicScript || pieceContainsArabicScript;
903
- tailEndsWithClosingQuote = pieceEndsWithClosingQuote;
904
- tailEndsWithMyanmarMedialGlue = pieceEndsWithMyanmarMedialGlue;
905
- tailHasArabicNoSpacePunctuation = hasArabicNoSpacePunctuation(tailContainsArabicScript, pieceLastCodePoint);
906
- } else {
907
- if (hasTail) {
908
- mergedTexts[mergedLen] = normalized.slice(tailStart, tailEnd);
909
- mergedWordLike[mergedLen] = tailWordLike;
910
- mergedKinds[mergedLen] = tailKind;
911
- mergedStarts[mergedLen] = tailStart;
912
- mergedLen++;
913
- }
914
- hasTail = true;
915
- tailStart = piece.start;
916
- tailEnd = pieceEnd;
917
- tailWordLike = piece.isWordLike;
918
- tailKind = piece.kind;
919
- tailSingleCharRunChar = repeatableSingleCharRunChar;
920
- tailContainsCJK = pieceContainsCJK;
921
- tailContainsArabicScript = pieceContainsArabicScript;
922
- tailEndsWithClosingQuote = pieceEndsWithClosingQuote;
923
- tailEndsWithMyanmarMedialGlue = pieceEndsWithMyanmarMedialGlue;
924
- tailHasArabicNoSpacePunctuation = hasArabicNoSpacePunctuation(pieceContainsArabicScript, pieceLastCodePoint);
925
- }
926
- }
927
- }
928
- if (hasTail) {
929
- mergedTexts[mergedLen] = normalized.slice(tailStart, tailEnd);
930
- mergedWordLike[mergedLen] = tailWordLike;
931
- mergedKinds[mergedLen] = tailKind;
932
- mergedStarts[mergedLen] = tailStart;
933
- mergedLen++;
934
- }
935
- for(let i = 1; i < mergedLen; i++){
936
- if (mergedKinds[i] === 'text' && !mergedWordLike[i] && isPunctuationGlueCluster(mergedTexts[i]) && mergedKinds[i - 1] === 'text' && !isCJK(mergedTexts[i - 1]) && (numericAffixBoundary(normalized, mergedStarts[i], profile) ?? openingPunctuationJoinsPrevious(normalized, mergedTexts[i], profile, mergedStarts[i])) !== false) {
937
- mergedTexts[i - 1] += mergedTexts[i];
938
- mergedWordLike[i - 1] = mergedWordLike[i - 1] || mergedWordLike[i];
939
- mergedTexts[i] = '';
359
+ droppedEnd = controlEnd
940
360
  }
941
- }
942
- let nextLiveIndex = -1;
943
- let forwardStickyPrefixParts = null;
944
- for(let i = mergedLen - 1; i >= 0; i--){
945
- const text = mergedTexts[i];
946
- if (text.length === 0) continue;
947
- const nextText = forwardStickyPrefixParts?.at(-1) ?? (nextLiveIndex >= 0 ? mergedTexts[nextLiveIndex] : null);
948
- if (mergedKinds[i] === 'text' && !mergedWordLike[i] && nextText !== null && mergedKinds[nextLiveIndex] === 'text' && ((numericAffixBoundary(normalized, mergedStarts[i] + text.length, profile) ?? (isForwardStickyClusterSegment(text) && openingPunctuationJoinsPrevious(text, nextText, profile) !== false)) || text === '-' && startsWithDecimalDigit(nextText))) {
949
- if (forwardStickyPrefixParts === null) forwardStickyPrefixParts = [];
950
- forwardStickyPrefixParts.push(text);
951
- mergedStarts[nextLiveIndex] = mergedStarts[i];
952
- mergedTexts[i] = '';
953
- continue;
954
- }
955
- if (forwardStickyPrefixParts !== null) {
956
- mergedTexts[nextLiveIndex] = joinReversedPrefixParts(forwardStickyPrefixParts, mergedTexts[nextLiveIndex]);
957
- forwardStickyPrefixParts = null;
958
- }
959
- nextLiveIndex = i;
960
- }
961
- if (forwardStickyPrefixParts !== null) {
962
- mergedTexts[nextLiveIndex] = joinReversedPrefixParts(forwardStickyPrefixParts, mergedTexts[nextLiveIndex]);
963
- }
964
- let compactLen = 0;
965
- for(let read = 0; read < mergedLen; read++){
966
- const text = mergedTexts[read];
967
- if (text.length === 0) continue;
968
- if (compactLen !== read) {
969
- mergedTexts[compactLen] = text;
970
- mergedWordLike[compactLen] = mergedWordLike[read];
971
- mergedKinds[compactLen] = mergedKinds[read];
972
- mergedStarts[compactLen] = mergedStarts[read];
361
+ if (chunkStart || lastAlone || markRun) {
362
+ starts.push(i)
363
+ flags.push(TEXT | oneCluster)
364
+ lastKind = TEXT
365
+ lastAlone = false
366
+ markRun = false
973
367
  }
974
- compactLen++;
975
- }
976
- mergedTexts.length = compactLen;
977
- mergedWordLike.length = compactLen;
978
- mergedKinds.length = compactLen;
979
- mergedStarts.length = compactLen;
980
- const compacted = mergeGlueConnectedTextRuns({
981
- len: compactLen,
982
- texts: mergedTexts,
983
- isWordLike: mergedWordLike,
984
- kinds: mergedKinds,
985
- starts: mergedStarts
986
- });
987
- const mergedRuns = mergeNoSpaceWordChains(mergeNumericRuns(mergeUrlRuns(compacted, normalized, profile), normalized, profile), normalized, profile);
988
- carryTrailingForwardStickyAcrossCJKBoundary(mergedRuns);
989
- for(let i = 0; i < mergedRuns.len - 1; i++){
990
- const split = splitLeadingSpaceAndMarks(mergedRuns.texts[i]);
991
- if (split === null) continue;
992
- if (mergedRuns.kinds[i] !== 'space' && mergedRuns.kinds[i] !== 'preserved-space' || mergedRuns.kinds[i + 1] !== 'text' || !containsArabicScript(mergedRuns.texts[i + 1])) {
993
- continue;
994
- }
995
- mergedRuns.texts[i] = split.space;
996
- mergedRuns.isWordLike[i] = false;
997
- mergedRuns.kinds[i] = mergedRuns.kinds[i] === 'preserved-space' ? 'preserved-space' : 'space';
998
- mergedRuns.texts[i + 1] = split.marks + mergedRuns.texts[i + 1];
999
- mergedRuns.starts[i + 1] = mergedRuns.starts[i] + split.space.length;
1000
- }
1001
- return mergedRuns;
1002
- }
1003
- function mergeKeepAllTextSegments(normalized, segmentation, profile) {
1004
- if (segmentation.len <= 1) return segmentation;
1005
- const texts = [];
1006
- const isWordLike = [];
1007
- const kinds = [];
1008
- const starts = [];
1009
- let groupStart = -1;
1010
- let groupContainsCJK = false;
1011
- function pushOriginalText(index) {
1012
- texts.push(segmentation.texts[index]);
1013
- isWordLike.push(segmentation.isWordLike[index]);
1014
- kinds.push('text');
1015
- starts.push(segmentation.starts[index]);
1016
- }
1017
- function pushMergedText(start, end) {
1018
- let wordLike = false;
1019
- for(let i = start; i < end; i++){
1020
- wordLike = wordLike || segmentation.isWordLike[i];
1021
- }
1022
- const sourceStart = segmentation.starts[start];
1023
- const sourceEnd = end < segmentation.len ? segmentation.starts[end] : normalized.length;
1024
- texts.push(normalized.slice(sourceStart, sourceEnd));
1025
- isWordLike.push(wordLike);
1026
- kinds.push('text');
1027
- starts.push(sourceStart);
1028
- }
1029
- function flushGroup(end) {
1030
- if (groupStart < 0) return;
1031
- if (groupContainsCJK) {
1032
- if (groupStart + 1 === end) {
1033
- pushOriginalText(groupStart);
1034
- } else {
1035
- pushMergedText(groupStart, end);
1036
- }
1037
- } else {
1038
- for(let i = groupStart; i < end; i++)pushOriginalText(i);
1039
- }
1040
- groupStart = -1;
1041
- groupContainsCJK = false;
1042
- }
1043
- for(let i = 0; i < segmentation.len; i++){
1044
- const text = segmentation.texts[i];
1045
- const kind = segmentation.kinds[i];
1046
- if (kind === 'text') {
1047
- if (groupStart >= 0 && (!canContinueKeepAllTextRun(segmentation.texts[i - 1], profile.breakKeepAllAfterPunctuation) || numericAffixBoundary(normalized, segmentation.starts[i], profile) === false)) {
1048
- flushGroup(i);
1049
- }
1050
- if (groupStart < 0) groupStart = i;
1051
- groupContainsCJK = groupContainsCJK || isCJK(text);
1052
- continue;
1053
- }
1054
- flushGroup(i);
1055
- texts.push(text);
1056
- isWordLike.push(segmentation.isWordLike[i]);
1057
- kinds.push(kind);
1058
- starts.push(segmentation.starts[i]);
1059
- }
1060
- flushGroup(segmentation.len);
1061
- return {
1062
- len: texts.length,
1063
- texts,
1064
- isWordLike,
1065
- kinds,
1066
- starts
1067
- };
1068
- }
1069
- function isNumericHyphen(text, index) {
1070
- const next = text.charCodeAt(index + 1);
1071
- if (next < 0x80) {
1072
- if (next < 0x30 || next > 0x39) return false;
368
+ continue
369
+ }
370
+ }
371
+ const kind = classifySegmentUnit(normalized, breaks, i, code, whiteSpace, scan)
372
+ const alone = kind === TEXT && isControlSegmentCode(code)
373
+ const unbroken = (breaks[i] & BREAK) === 0
374
+ if (
375
+ (breaks[i] & (BREAK | ITEM_START)) === 0 && !alone && !lastAlone && !(markRun && !combiningMarkRe.test(normalized[i] )) &&
376
+ kind === lastKind && gathersKind(kind)
377
+ ) {
378
+ if ((breaks[i] & CLUSTER_START) !== 0) flags[flags.length - 1] = flags[flags.length - 1] & ~ONE_CLUSTER
379
+ continue
380
+ }
381
+ markRun = unbroken && kind === TEXT && combiningMarkRe.test(normalized[i] ) &&
382
+ (lastAlone || lastKind === ZERO_WIDTH_BREAK || lastKind === SOFT_HYPHEN || lastKind === CONTROL)
383
+ starts.push(i)
384
+ flags.push(kind | oneCluster)
385
+ lastKind = kind
386
+ lastAlone = alone
387
+ }
388
+ // A line ends only where the scan breaks, so the walkers learn where it doesn't:
389
+ // before text, zero-width glue or a control, other than at a line start. A
390
+ // ZWSP or soft hyphen there is zero-width glue. Before a space, tab or hard break
391
+ // the scan has no break either, but the line can still end there, as they hang or
392
+ // end it, so it keeps its kind. A tab that doesn't hang (EngineProfile's hangTabs) ends
393
+ // no line before itself, nor do the spaces after it, which hang only where the run of
394
+ // white space ends the line: Gecko breaks only after the whole run (nsLineBreaker.cpp:
395
+ // 318-330). A soft hyphen before the tab keeps its break (GetHyphenationBreaks,
396
+ // nsTextFrame.cpp:4409-4457).
397
+ let hasUnbroken = false
398
+ const count = flags.length
399
+ for (let j = count - 2; j >= 0; j--) {
400
+ const kind = flags[j] & KIND_BITS
401
+ const next = flags[j + 1] & KIND_BITS
402
+ if ((breaks[starts[j + 1] ] & BREAK) !== 0 || kind === HARD_BREAK) continue
403
+ if (!hangTabs && (next === TAB || (next === PRESERVED_SPACE && kind === TAB))) {
404
+ if (kind === SOFT_HYPHEN) continue
405
+ } else if (next === TEXT || next === ZERO_WIDTH_GLUE || next === CONTROL) {
406
+ if (kind === ZERO_WIDTH_BREAK || kind === SOFT_HYPHEN) flags[j] = flags[j] & ~KIND_BITS | ZERO_WIDTH_GLUE
1073
407
  } else {
1074
- const codePoint = text.codePointAt(index + 1);
1075
- if (codePoint === undefined || !decimalDigitRe.test(String.fromCodePoint(codePoint))) return false;
1076
- }
1077
- for(let end = index; end > 0;){
1078
- const start = previousCodePointStart(text, end);
1079
- const previous = text.slice(start, end);
1080
- if (!combiningMarkRe.test(previous)) return isCJK(previous) || !/[\p{L}\p{N}]/u.test(previous);
1081
- end = start;
1082
- }
1083
- return true;
1084
- }
1085
- function buildBaseCjkUnits(segText, profile) {
1086
- const units = [];
1087
- let unitStart = 0;
1088
- let unitEnd = 0;
1089
- let unitContainsCJK = false;
1090
- let unitEndsWithClosingQuote = false;
1091
- let unitIsSingleKinsokuEnd = false;
1092
- let unitHasHyphen = false;
1093
- let unitHasNumericHyphen = false;
1094
- function pushUnit() {
1095
- if (unitEnd === unitStart) return;
1096
- units.push({
1097
- text: segText.slice(unitStart, unitEnd),
1098
- start: unitStart,
1099
- overflow: unitContainsCJK ? unitHasHyphen ? 'grapheme' : 'none' : 'word-like'
1100
- });
1101
- unitStart = unitEnd;
1102
- unitContainsCJK = false;
1103
- unitEndsWithClosingQuote = false;
1104
- unitIsSingleKinsokuEnd = false;
1105
- unitHasHyphen = false;
1106
- unitHasNumericHyphen = false;
1107
- }
1108
- function startUnit(grapheme, start, graphemeContainsCJK) {
1109
- unitStart = start;
1110
- unitEnd = start + grapheme.length;
1111
- unitContainsCJK = graphemeContainsCJK;
1112
- unitHasHyphen = grapheme === '-';
1113
- unitEndsWithClosingQuote = endsWithClosingQuote(grapheme);
1114
- unitIsSingleKinsokuEnd = kinsokuEnd.has(grapheme);
1115
- }
1116
- function appendToUnit(grapheme, graphemeContainsCJK) {
1117
- unitEnd += grapheme.length;
1118
- unitContainsCJK = unitContainsCJK || graphemeContainsCJK;
1119
- unitHasHyphen = unitHasHyphen || grapheme === '-';
1120
- const graphemeEndsWithClosingQuote = endsWithClosingQuote(grapheme);
1121
- if (grapheme.length === 1 && leftStickyPunctuation.has(grapheme)) {
1122
- unitEndsWithClosingQuote = unitEndsWithClosingQuote || graphemeEndsWithClosingQuote;
1123
- } else {
1124
- unitEndsWithClosingQuote = graphemeEndsWithClosingQuote;
1125
- }
1126
- unitIsSingleKinsokuEnd = false;
1127
- }
1128
- for (const gs of getSharedGraphemeSegmenter().segment(segText)){
1129
- const grapheme = gs.segment;
1130
- const graphemeContainsCJK = isCJK(grapheme);
1131
- if (unitEnd === unitStart) {
1132
- startUnit(grapheme, gs.index, graphemeContainsCJK);
1133
- continue;
408
+ continue
409
+ }
410
+ flags[j + 1] = flags[j + 1] | UNBROKEN
411
+ hasUnbroken = true
412
+ }
413
+ if (hasUnbroken) for (let j = 0; j < count; j++) if ((flags[j] & UNBROKEN) === 0) flags[j] = flags[j] | RETURNABLE
414
+ // Each segment's text, sliced once here for measurement and the neighbours it reads: sliced
415
+ // where measurement reads it, Firefox's rich-inline preparation ran 11-19% slower (RESEARCH.md,
416
+ // Keeping Work Bounded).
417
+ const texts = []
418
+ for (let j = 0; j < count; j++) texts.push(normalized.slice(starts[j], j + 1 < count ? starts[j + 1] : normalized.length))
419
+ return { normalized, spaceSources, texts, starts, flags, hasUnbroken }
420
+ }
421
+
422
+ export function analyzeText(
423
+ text ,
424
+ profile ,
425
+ whiteSpace = 'normal',
426
+ wordBreak = 'normal',
427
+ // Preparation's language (getPreparationLanguage), which picks Chrome's and WebKit's
428
+ // line tables, WebKit's quotation remap and Gecko's rule for newlines next to East
429
+ // Asian punctuation.
430
+ language = null,
431
+ // The items of the rich-inline paragraph whose text this is, else null.
432
+ paragraph = null,
433
+ ) {
434
+ const preserve = whiteSpace === 'pre-wrap'
435
+ // Where each of a paragraph's items starts in the source the engine scans.
436
+ const scanStarts = paragraph === null ? null : []
437
+ // The source a text node's engine scans, after the segment break transformation.
438
+ let source = preserve ? text
439
+ : paragraph !== null && paragraph.ownSegmentBreaks ? removeItemsSkippableSegmentBreaks(text, paragraph.starts, profile, language, scanStarts )
440
+ : removeSkippableSegmentBreaks(text, profile, language)
441
+ if (paragraph !== null && scanStarts .length === 0) for (let k = 0; k < paragraph.starts.length; k++) scanStarts .push(paragraph.starts[k] )
442
+ let normalized = preserve ? normalizeWhitespacePreWrap(text) : collapseWhitespaceNormal(source)
443
+ const keepAll = wordBreak === 'keep-all'
444
+ let breaks
445
+ let spaceSources = null
446
+ let dropsBidiControl = false
447
+ if (profile.lineBreakScan === 'blink') {
448
+ breaks = getBlinkLineBreaks(normalized, keepAll, language)
449
+ } else {
450
+ // WebKit and Gecko scan the source. Gecko's scan collapses its white space as Firefox does.
451
+ let sourceBreaks
452
+ if (profile.lineBreakScan === 'webkit') {
453
+ sourceBreaks = paragraph === null ? getWebKitLineBreaks(source, preserve, keepAll, language) : getWebKitParagraphBreaks(source, paragraph, preserve, keepAll, language)
454
+ // A CR is no white space to WebKit (moveToNextNonWhitespacePosition, InlineItemsBuilder.cpp:
455
+ // 55-73) and stays in its text item with no advance and no letter spacing: Core Text's
456
+ // shaping gives its glyph none (CTFontShapeGlyphs, from Font::applyTransforms,
457
+ // FontCoreText.cpp:617-699; the complex path at ComplexTextController.cpp:762-768), and only
458
+ // a character that advances is spaced (WidthIterator.cpp:508-516). Every Canvas measures
459
+ // it as a space, so the source leaves it out, as the Gecko profile's does below, and white
460
+ // space on its two sides is one space, where Safari keeps two. The scan read it, so the
461
+ // unit after it keeps its own break: ICU's, after the CR, where the scan takes it, as before
462
+ // a letter above U+00FF, and none elsewhere, its table having none beside a control
463
+ // (BreakablePositions.h:179-187, 238-251; RESEARCH.md, Engine Facts, Safari (WebKit), CR and
464
+ // FF, has where). That unit also takes the CR's break, the one after white space. The CR of a
465
+ // CRLF stays, to collapse into the line feed's space; in a paragraph that is also a CR that
466
+ // ends an item before a line feed that starts the next, whose space is the line feed's item's
467
+ // (alignToSource). In the installed fonts that take WebKit's fixed-pitch shortcut, Menlo
468
+ // and so the generic monospace among them, text on simplified measuring is as wide as its
469
+ // characters are many, the CR among them (Font::determinePitch, FontCoreText.cpp:753-785;
470
+ // widthForSimpleTextWithFixedPitch, FontCascade.cpp:414-421), which Canvas can't show, so
471
+ // the profile gives those fonts up (RESEARCH.md, Decisions Log, 2026-10-06;
472
+ // ENGINE_FOLLOWUPS.md, White space and controls).
473
+ if (!preserve && source !== normalized && /\r(?!\n)/.test(source)) {
474
+ let count = 0
475
+ for (let i = 0; i < source.length; i++) {
476
+ if (source.charCodeAt(i) !== 0x0D || source.charCodeAt(i + 1) === 0x0A) sourceBreaks[count++] = sourceBreaks[i]
477
+ else sourceBreaks[i + 1] = sourceBreaks[i + 1] | sourceBreaks[i]
1134
478
  }
1135
- const attachHyphen = grapheme === '-' && unitContainsCJK;
1136
- if (attachHyphen && isNumericHyphen(segText, gs.index)) unitHasNumericHyphen = true;
1137
- if (unitIsSingleKinsokuEnd || kinsokuStart.has(grapheme) || leftStickyPunctuation.has(grapheme) || attachHyphen || unitHasNumericHyphen && !graphemeContainsCJK || profile.carryCJKAfterClosingQuote && graphemeContainsCJK && unitEndsWithClosingQuote) {
1138
- appendToUnit(grapheme, graphemeContainsCJK);
1139
- continue;
1140
- }
1141
- if (!unitContainsCJK && !graphemeContainsCJK) {
1142
- appendToUnit(grapheme, graphemeContainsCJK);
1143
- continue;
479
+ sourceBreaks[count] = sourceBreaks[source.length]
480
+ source = source.replace(/\r(?!\n)/g, '')
481
+ normalized = collapseWhitespaceNormal(source)
482
+ }
483
+ } else {
484
+ const gecko = getGeckoLineBreaks(source, preserve, keepAll, profile.graphemeTable, scanStarts)
485
+ dropsBidiControl = gecko.dropsBidiControl
486
+ sourceBreaks = gecko.breaks
487
+ // Gecko's line breaker starts again after an atomic frame (BuildTextRunsScanner::ScanFrame,
488
+ // nsTextFrame.cpp:2248-2251, 2264-2268), so the text after one has no break of the scan's at
489
+ // its start, where the scan finds one after the U+FFFC: a soft hyphen that starts that text
490
+ // hyphenates there, and the break after the atomic item comes before it (markItemStarts).
491
+ if (paragraph !== null) {
492
+ for (let k = 0; k < paragraph.atomic.length; k++) {
493
+ if (!paragraph.atomic[k]) continue
494
+ let after = scanStarts [k] + 1
495
+ while (after < source.length && isDiscardable(source.charCodeAt(after), false)) after++
496
+ sourceBreaks[after] = sourceBreaks[after] & ~SOFT_HYPHEN_BREAK
1144
497
  }
1145
- pushUnit();
1146
- startUnit(grapheme, gs.index, graphemeContainsCJK);
1147
- }
1148
- pushUnit();
1149
- return units;
1150
- }
1151
- function mergeKeepAllTextUnits(segText, units, profile) {
1152
- if (units.length <= 1) return units;
1153
- const merged = [];
1154
- let groupStart = -1;
1155
- let groupContainsCJK = false;
1156
- function pushMergedUnit(start, end) {
1157
- const sourceStart = units[start].start;
1158
- const sourceEnd = end < units.length ? units[end].start : segText.length;
1159
- merged.push({
1160
- text: segText.slice(sourceStart, sourceEnd),
1161
- start: sourceStart,
1162
- overflow: 'word-like'
1163
- });
1164
- }
1165
- function flushGroup(end) {
1166
- if (groupStart < 0) return;
1167
- if (groupContainsCJK) {
1168
- if (groupStart + 1 === end) {
1169
- merged.push(units[groupStart]);
1170
- } else {
1171
- pushMergedUnit(groupStart, end);
498
+ }
499
+ // Gecko's white-space run reads through the soft hyphens and bidi controls in it, which its
500
+ // text run drops, and keeps its segment break if it holds one, or else its first white space
501
+ // (TransformWhiteSpaces, nsTextFrameUtils.cpp:151-193); the scan takes the text as one text
502
+ // frame (transformText in src/gecko-line-breaks.ts). The white space the scan's text run left
503
+ // out of such a run leaves the source too, and so does white space before only bidi controls
504
+ // at the end, which the line end trims, and a CR or FF, which ends a run and takes no room
505
+ // (the CR of a CRLF stays, to collapse into the line feed's space); white space on its two
506
+ // sides then touches and is one space, where Firefox keeps two.
507
+ // The other units keep their breaks: none is at white space, and the unit after a CR or FF
508
+ // has its own, as a CR is one of nsLineBreaker's breakable spaces, whose run gives the unit
509
+ // after it a break (IsSegmentSpace, nsLineBreaker.h:260-264; nsLineBreaker.cpp:318-327), and
510
+ // an FF is UAX #14's BK, which the word breaker breaks after. Only a combining mark there has
511
+ // none, since a break inside a cluster holds only after a space (SetPotentialLineBreaks,
512
+ // gfxTextRun.cpp:219-226), so the break Firefox has before the CR is lost (ENGINE_FOLLOWUPS.md).
513
+ // Where the white space right after a soft hyphen leaves, the break after it stays that white
514
+ // space's, which draws no hyphen (SOFT_HYPHEN_BREAK): Gecko hyphenates only at a soft hyphen
515
+ // that ends what its text run left out (GetHyphenationBreaks, nsTextFrame.cpp:4436-4443).
516
+ const leftOut = gecko.leftOut
517
+ const trailing = !preserve && (dropsBidiControl || leftOut !== null) ? getTrailingCollapsibleStart(source, 0, profile) : source.length
518
+ if (leftOut !== null || trailing < source.length) {
519
+ // A run that goes on into that trailing white space and keeps its one white space there, as
520
+ // a segment break after a soft hyphen, keeps its first white space instead: the text before
521
+ // the trailing white space holds the run's space, as a rich-inline paragraph's run keeps the
522
+ // white space of the item it starts in (transformText in src/gecko-line-breaks.ts).
523
+ let runSpace = -1
524
+ if (leftOut !== null && trailing < source.length) {
525
+ for (let i = trailing - 1; i >= 0; i--) {
526
+ const code = source.charCodeAt(i)
527
+ if (!isSpaceOrTabOrSegmentBreak(code) && !isDiscardable(code, false)) break
528
+ if (isSpaceOrTabOrSegmentBreak(code) && leftOut[i] !== 1) {
529
+ runSpace = -1
530
+ break
1172
531
  }
1173
- } else {
1174
- for(let i = groupStart; i < end; i++)merged.push(units[i]);
532
+ if (isSpaceOrTabOrSegmentBreak(code)) runSpace = i
533
+ }
1175
534
  }
1176
- groupStart = -1;
1177
- groupContainsCJK = false;
1178
- }
1179
- for(let i = 0; i < units.length; i++){
1180
- const unit = units[i];
1181
- if (groupStart >= 0 && (!canContinueKeepAllTextRun(units[i - 1].text, profile.breakKeepAllAfterPunctuation) || numericAffixBoundary(segText, unit.start, profile) === false)) {
1182
- flushGroup(i);
535
+ // Only the units from the first one that leaves move.
536
+ const from = leftOut === null ? trailing : 0
537
+ let kept = ''
538
+ let copied = 0
539
+ let count = from
540
+ let last = from - 1
541
+ for (let i = from; i < source.length; i++) {
542
+ if (i < trailing ? leftOut !== null && leftOut[i] === 1 && i !== runSpace : isCollapsibleSpaceCode(source.charCodeAt(i))) {
543
+ kept += source.slice(copied, i)
544
+ copied = i + 1
545
+ continue
546
+ }
547
+ const breakBefore = sourceBreaks[i]
548
+ sourceBreaks[count++] = last >= 0 && last < i - 1 && (breakBefore & BREAK) !== 0 && source.charCodeAt(last) === 0x00AD ? breakBefore | SOFT_HYPHEN_BREAK : breakBefore
549
+ last = i
1183
550
  }
1184
- if (groupStart < 0) groupStart = i;
1185
- groupContainsCJK = groupContainsCJK || isCJK(unit.text);
1186
- }
1187
- flushGroup(units.length);
1188
- return merged;
1189
- }
1190
- export function getCjkTextUnits(text, profile, wordBreak) {
1191
- const units = buildBaseCjkUnits(text, profile);
1192
- return wordBreak === 'keep-all' ? mergeKeepAllTextUnits(text, units, profile) : units;
1193
- }
1194
- function isPreferredBreakGrapheme(grapheme) {
1195
- return grapheme === '-' || grapheme === '\u058A' || grapheme === '\u2010' || grapheme === '\u2012' || grapheme === '\u2013' || grapheme === '\u2014';
1196
- }
1197
- export function isIndependentSymbolRun(text) {
1198
- if (text.length === 0 || /\p{Cf}/u.test(text)) return false;
1199
- for (const { segment } of getSharedGraphemeSegmenter().segment(text)){
1200
- const base = String.fromCodePoint(segment.codePointAt(0));
1201
- if (!/[\p{P}\p{S}]/u.test(base) || emojiPresentationRe.test(base) || segment.includes('\uFE0F') || /\p{Emoji_Modifier}/u.test(base)) return false;
1202
- }
1203
- return true;
1204
- }
1205
- export function getBreakablePreferredBreaks(text) {
1206
- if (!/[-\u058A\u2010\u2012\u2013\u2014]/u.test(text)) return null;
1207
- const breaks = [];
1208
- let graphemeIndex = 0;
1209
- for (const gs of getSharedGraphemeSegmenter().segment(text)){
1210
- graphemeIndex++;
1211
- const numericSign = gs.segment === '-' && isNumericHyphen(text, gs.index);
1212
- if (isPreferredBreakGrapheme(gs.segment) && !numericSign) breaks.push(graphemeIndex);
1213
- }
1214
- return breaks.length === 0 ? null : breaks;
1215
- }
1216
- export function analyzeText(text, profile, whiteSpace = 'normal', wordBreak = 'normal') {
1217
- const normalized = whiteSpace === 'pre-wrap' ? normalizeWhitespacePreWrap(text) : normalizeWhitespaceNormal(text);
1218
- if (normalized.length === 0) {
1219
- return {
1220
- normalized,
1221
- len: 0,
1222
- texts: [],
1223
- isWordLike: [],
1224
- kinds: [],
1225
- starts: []
1226
- };
1227
- }
1228
- const mergedSegmentation = buildMergedSegmentation(normalized, profile, whiteSpace);
1229
- const segmentation = wordBreak === 'keep-all' ? mergeKeepAllTextSegments(normalized, mergedSegmentation, profile) : mergedSegmentation;
1230
- return {
1231
- normalized,
1232
- ...segmentation
1233
- };
551
+ sourceBreaks[count] = sourceBreaks[source.length]
552
+ source = kept + source.slice(copied)
553
+ normalized = collapseWhitespaceNormal(source)
554
+ }
555
+ }
556
+ if (profile.lineBreakScan === 'webkit' && !preserve && source !== normalized) spaceSources = new Uint16Array(normalized.length)
557
+ breaks = source === normalized ? sourceBreaks : mapSourceLineBreaks(source, normalized.length, sourceBreaks, whiteSpace, spaceSources)
558
+ }
559
+ if (paragraph !== null) markItemStarts(text, normalized, breaks, paragraph, profile.lineBreakScan)
560
+ return segmentAtLineBreaks(normalized, spaceSources, breaks, whiteSpace, profile.lineBreakScan, profile.hangTabs, dropsBidiControl)
561
+ }
562
+
563
+ // A paragraph's text after each item's own segment break transformation, where the engine
564
+ // transforms segment breaks in each text frame's text apart (ParagraphItems), and in
565
+ // `resultStarts` where each item starts in it.
566
+ function removeItemsSkippableSegmentBreaks(text , starts , profile , language , resultStarts ) {
567
+ if (profile.lineBreakScan === 'webkit' || !text.includes('\n')) return text
568
+ let result = ''
569
+ // Where the text that isn't in `result` yet starts. Only an item that holds a line feed can
570
+ // change, so only those are cut out and transformed, and the text between two that changed is
571
+ // copied in one piece.
572
+ let copied = 0
573
+ let newline = text.indexOf('\n')
574
+ for (let k = 0; k < starts.length; k++) {
575
+ const start = starts[k]
576
+ const end = k + 1 < starts.length ? starts[k + 1] : text.length
577
+ resultStarts.push(result.length + start - copied)
578
+ if (newline < 0 || newline >= end) continue
579
+ const item = text.slice(start, end)
580
+ const transformed = removeSkippableSegmentBreaks(item, profile, language)
581
+ if (transformed.length !== item.length) {
582
+ result += text.slice(copied, start) + transformed
583
+ copied = end
584
+ }
585
+ newline = text.indexOf('\n', end)
586
+ }
587
+ return copied === 0 ? text : result + text.slice(copied)
588
+ }
589
+
590
+ // The offset in `source` that each unit of `normalized` comes from. Normalization only removes
591
+ // white space and, in the Gecko profile, a CR or FF, and in the WebKit profile a lone CR, turns a
592
+ // run of white space into one space, which comes from the run's first unit, and turns CR, CRLF
593
+ // and FF into LF, so a greedy walk aligns the two. Under the WebKit and Gecko scans no space
594
+ // comes from a CR, nor under Gecko's from an FF, one right before the white space included:
595
+ // neither is white space to that engine (moveToNextNonWhitespacePosition,
596
+ // InlineItemsBuilder.cpp:55-73; IsSpaceOrTabOrSegmentBreak, nsTextFrameUtils.cpp:51-57), so the
597
+ // run starts after it, and the box that holds the run's first character has its space, in that
598
+ // box's font and letter spacing. WebKit makes a white-space item of its text box, as wide as
599
+ // that box's space (InlineItemsBuilder.cpp:947, 963-987), and takes out only white space that
600
+ // follows other white space, an earlier box's too (Line::appendText, InlineLine.cpp:357-365);
601
+ // Gecko transforms a frame at a time from the white-space state the frame before it left, which
602
+ // a CR or FF clears (nsTextFrameUtils.cpp:286-309, 382-386). So the space after a CR that ends
603
+ // an item is the next item's, as is the line feed of a CRLF split there, and a fragment that
604
+ // ends with a space after a CR ends after that space in its item's text, and one that starts
605
+ // with such a space starts at it. To Blink a CR is white space (Character::IsCollapsibleSpace,
606
+ // platform/text/character.h:150-153), its run's first unit, and an FF is still a space in the
607
+ // Blink and WebKit profiles.
608
+ export function alignToSource(source , normalized , scan ) {
609
+ const offsets = new Int32Array(normalized.length)
610
+ // The units no space comes from under `scan`, or -1.
611
+ const cr = scan === 'blink' ? -1 : 0x0D
612
+ const ff = scan === 'gecko' ? 0x0C : -1
613
+ let i = 0
614
+ for (let j = 0; j < normalized.length; j++) {
615
+ const unit = normalized.charCodeAt(j)
616
+ while (i < source.length) {
617
+ const code = source.charCodeAt(i)
618
+ if (code === unit || (isCollapsibleSpaceCode(code) && (unit === 0x20 ? code !== cr && code !== ff : unit === 0x0A))) break
619
+ i++
620
+ }
621
+ offsets[j] = i++
622
+ // CRLF became one LF.
623
+ if (unit === 0x0A && source.charCodeAt(i - 1) === 0x0D && source.charCodeAt(i) === 0x0A) i++
624
+ }
625
+ return offsets
626
+ }
627
+
628
+ // Marks where each item of a paragraph starts in its normalized text (ITEM_START), at the first
629
+ // unit that comes from the item or from one after it, and a break on both sides of an atomic item,
630
+ // under keep-all too: Blink breaks after an atomic inline and before one (CanBreakAfterAtomicInline
631
+ // and CanBreakAfter, line_breaker.cc:1168-1263 in core/layout/inline), WebKit finds a soft wrap
632
+ // opportunity on either side of one (InlineFormattingUtils.cpp:445-449), and Gecko records a break
633
+ // after one and breaks before one that doesn't fit (nsLineLayout.cpp:1057-1068, 1339-1340).
634
+ function markItemStarts(text , normalized , breaks , paragraph , scan ) {
635
+ const offsets = paragraph.sourceOffsets = alignToSource(text, normalized, scan)
636
+ const { starts, atomic } = paragraph
637
+ for (let k = 0, j = 0; k < starts.length; k++) {
638
+ while (j < normalized.length && offsets[j] < starts[k] ) j++
639
+ if (j === normalized.length) break
640
+ breaks[j] = breaks[j] | ITEM_START
641
+ if (atomic[k] && offsets[j] === starts[k]) {
642
+ breaks[j] = breaks[j] | BREAK
643
+ breaks[j + 1] = breaks[j + 1] | BREAK | ITEM_START
644
+ }
645
+ }
646
+ }
647
+
648
+ // Where a line may start in a paragraph's source, in WebKit: it finds breaks inside each inline
649
+ // box from that box's own text, and at a boundary between boxes from the scan over the next box's
650
+ // text with the last two characters before it as prior context (TextUtil.cpp:374-396), so a
651
+ // paragraph's breaks are each item's own scan, joined by that check (getWebKitBreakBetweenItems).
652
+ // Collapsible white space on either side of a boundary breaks there, as inside a text, but a CR,
653
+ // which is no white space to WebKit: beside one the check decides. It takes the pair of a CR and
654
+ // a character up to U+00FF from its table, which has no break for it, so at a boundary only a
655
+ // character above U+00FF right after the CR brings ICU's break, where inside a text a letter above
656
+ // U+00FF before the CR can too, before an ASCII letter. The analysis then takes a lone CR out with
657
+ // the breaks found around it (analyzeText). The source is the items' texts joined as they are, an
658
+ // atomic item as one U+FFFC, which is never scanned (removeItemsSkippableSegmentBreaks leaves
659
+ // WebKit's text alone), so a scan takes an item's own text and nothing is cut out of the source
660
+ // before the scans.
661
+ function getWebKitParagraphBreaks(source , paragraph , preserve , keepAll , language ) {
662
+ const { items, starts, atomic } = paragraph
663
+ const breaks = new Uint8Array(source.length + 1)
664
+ let previous = -1
665
+ for (let k = 0; k < starts.length; k++) {
666
+ const start = starts[k]
667
+ const end = k + 1 < starts.length ? starts[k + 1] : source.length
668
+ if (start === end) continue
669
+ // An item of one unit, as a space between two styled words or an atomic item's U+FFFC, has
670
+ // no break inside, and its scan marks its end only after a line or paragraph separator
671
+ // (getWebKitLineBreaks), so only those are scanned.
672
+ const first = source.charCodeAt(start)
673
+ if (end - start > 1 || first === 0x2028 || first === 0x2029) {
674
+ const itemBreaks = getWebKitLineBreaks(items[k] .text , preserve, keepAll, language)
675
+ for (let i = 1; i <= end - start; i++) breaks[start + i] = itemBreaks[i]
676
+ }
677
+ if (previous >= 0 && !atomic[k] && !atomic[previous]) {
678
+ const last = source.charCodeAt(start - 1)
679
+ const collapses = !preserve && ((first !== 0x0D && isCollapsibleSpaceCode(first)) || (last !== 0x0D && isCollapsibleSpaceCode(last)))
680
+ if (collapses || getWebKitBreakBetweenItems(items[previous] .text , items[k] .text , keepAll, language)) breaks[start] = breaks[start] | BREAK
681
+ }
682
+ previous = k
683
+ }
684
+ return breaks
1234
685
  }