rapier-markdown-kit 1.2.32 → 1.2.40

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 (45) hide show
  1. package/README.md +16 -10
  2. package/dist/agent/vendor/pretext/NOTICE +2 -2
  3. package/dist/agent/vendor/pretext/SOURCE.json +62 -78
  4. package/dist/agent/vendor/pretext/analysis.js +24 -6
  5. package/dist/agent/vendor/pretext/gecko-line-breaks.js +1 -1
  6. package/dist/agent/vendor/pretext/generated/engine-break-data.js +1 -1
  7. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +1 -1
  8. package/dist/agent/vendor/pretext/graphemes.js +1 -1
  9. package/dist/agent/vendor/pretext/han-kerning.js +1 -1
  10. package/dist/agent/vendor/pretext/layout.js +14 -11
  11. package/dist/agent/vendor/pretext/line-break.js +170 -195
  12. package/dist/agent/vendor/pretext/line-breaks.js +1 -7
  13. package/dist/agent/vendor/pretext/line-text.js +2 -2
  14. package/dist/agent/vendor/pretext/measurement.js +454 -113
  15. package/dist/agent/vendor/pretext/prepare.js +164 -244
  16. package/dist/agent/vendor/pretext/rich-inline.js +57 -37
  17. package/dist/agent/will.mjs +73 -0
  18. package/dist/kit/ledger/data.mjs +2 -2
  19. package/dist/kit/ledger/format.mjs +86 -42
  20. package/dist/kit/ledger/hash.mjs +43 -2
  21. package/dist/kit/ledger/history.mjs +6 -0
  22. package/dist/kit/ledger/inverse-source.mjs +6 -2
  23. package/dist/kit/ledger/journal-records.mjs +4 -2
  24. package/dist/kit/ledger/merge.mjs +3 -17
  25. package/dist/kit/ledger/text-merge.mjs +191 -28
  26. package/dist/kit/rapier-ledger.mjs +2 -2
  27. package/dist/kit/render-markdown.mjs +133 -10
  28. package/dist/kit/render-print.mjs +3 -3
  29. package/dist/kit/render-sanitize.mjs +66 -23
  30. package/dist/kit/render.mjs +10 -10
  31. package/dist/kit/wiki.mjs +34 -0
  32. package/dist/kit/will.d.mts +3 -0
  33. package/dist/layout/hyphenation.mjs +6 -4
  34. package/dist/layout/line-plan.mjs +96 -0
  35. package/dist/layout/model.mjs +2 -1
  36. package/dist/layout/paragraph-fit.mjs +4 -4
  37. package/dist/layout/prepared-paragraph.mjs +19 -2
  38. package/dist/layout/typography.mjs +57 -63
  39. package/dist/spec/document-fonts.mjs +1 -0
  40. package/dist/spec/document-settings.mjs +1 -0
  41. package/dist/spec/html-blocks.mjs +10 -7
  42. package/dist/spec/md-assets.mjs +52 -12
  43. package/package.json +1 -1
  44. package/style.css +37 -28
  45. package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
@@ -1,15 +1,15 @@
1
- /*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
1
+ /*! Pretext 1.0.0 + main (0d102a5cd) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
2
2
  // Preparation's measurement phase: each segment of a text analysis measured in a font, with
3
3
  // the facts the line walkers read at a line's edges. layout.ts and rich-inline.ts prepare
4
4
  // through it, as they walk through line-break.ts.
5
5
 
6
- import { observeSegmentEntries, textMayHaveEntryGeometry, } from './entry-geometry.js'
7
6
  import { getHanKerningTrims, textMayHanKern, } from './han-kerning.js'
8
7
  import { findGraphemeEnds, } from './graphemes.js'
9
8
  import { scriptClassFacts } from './generated/engine-break-data.js'
10
9
  import { DEFAULT_IGNORABLE, getClass, hasProperty, lazyRegExp, unpackClasses, } from './line-breaks.js'
11
10
  import {
12
11
  CONTROL,
12
+ DROPPED,
13
13
  HARD_BREAK,
14
14
  KIND_BITS,
15
15
  ONE_CLUSTER,
@@ -31,19 +31,22 @@ import {
31
31
 
32
32
  getCorrectedSegmentWidth,
33
33
  getEmojiCorrection,
34
+ getExtraSpacings,
34
35
  getFollowingSpaceMetrics,
36
+ getFontKanaKerning,
35
37
  getFontMeasurement,
36
38
  getFontSpaceKerning,
37
39
  getHyphenText,
40
+ getKanaKerning,
38
41
  getSegmentFit,
39
42
  getSegmentMetrics,
40
43
  getSpaceKerning,
41
44
  getSpaceWidth,
42
45
  getTextWidth,
43
- measureWithLetterSpacing,
46
+ kanaPairRe,
47
+ noExtraSpacings,
44
48
  noSpaceKerning,
45
49
  textMayContainEmoji,
46
-
47
50
 
48
51
  zeros,
49
52
  } from './measurement.js'
@@ -77,17 +80,7 @@ const extendedPictographicRe = /\p{Extended_Pictographic}/u
77
80
  const leadingCombiningMarkRe = lazyRegExp(String.raw`^\p{M}`, 'u')
78
81
  // Decimal digits and the joiners of numbers, times and dates.
79
82
  const numericRunRe = /^[\p{Nd}:\-/×,.+\u2013\u2014]+$/u
80
- const markRunRe = /^\p{M}+$/u
81
- const nonspacingMarkRunRe = /^\p{Mn}+$/u
82
- const controlOrMarkRunRe = /^(?:[\p{Cc}\u2028\u2029]|\p{M}+)$/u
83
83
  const controlCharacterRe = /^[\p{Cc}\u2028\u2029]$/u
84
- // The fewest UTF-16 units of a long chain of mark runs that a run's context keeps after
85
- // the grapheme (getMarkContext): measured after the whole chain, a long chain prepared in
86
- // time that grows with the square of its length. Safari gives a run a width that depends
87
- // on how far it sits from the grapheme, up to 61 units in the chains measured
88
- // (RESEARCH.md, Break Opportunities From Engine Data), so a context that keeps fewer
89
- // moves widths there.
90
- const MARK_CHAIN_CONTEXT_UNITS = 96
91
84
 
92
85
  function needsComplexTextPath(text ) {
93
86
  let previousIsEmoji = false
@@ -103,20 +96,9 @@ function needsComplexTextPath(text ) {
103
96
  return false
104
97
  }
105
98
 
106
- const explicitBidiControlRe = /[\u202A-\u202E\u2066-\u2069]/
107
- // Format characters stand for bidi class BN, except the direction marks LRM,
108
- // RLM and ALM, which are strong characters like letters.
109
- const trailingFormatCharacterRe = /(?![\u200E\u200F\u061C])\p{Cf}$/u
110
- // Letters in the right-to-left blocks have bidi class R or AL, as do RLM and
111
- // ALM. Every other letter except modifier letters has class L, as does LRM.
112
- const rightToLeftLetterRe = /[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF\u200F\u{10800}-\u{10FFF}\u{1E800}-\u{1EFFF}]/u
113
- // Either of the two: a right-to-left letter or an explicit bidi control.
99
+ // A character of the right-to-left blocks, whose letters have bidi class R or AL, as RLM
100
+ // has, or an explicit bidi control.
114
101
  const mixedDirectionRe = /[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF\u200F\u{10800}-\u{10FFF}\u{1E800}-\u{1EFFF}\u202A-\u202E\u2066-\u2069]/u
115
- // The last letter or direction mark before format characters other than a soft
116
- // hyphen, and the first letter, direction mark or ASCII digit after the space,
117
- // past spaces and format characters.
118
- const letterBeforeFormatTailRe = lazyRegExp(String.raw`([\p{Lu}\p{Ll}\p{Lt}\p{Lo}\u200E\u200F\u061C])\p{M}*(?:(?![\u00AD\u200E\u200F\u061C])\p{Cf})+$`, 'u')
119
- const letterAfterSpacesRe = lazyRegExp(String.raw` (?: |(?![\u200E\u200F\u061C])\p{Cf})*([0-9\p{Lu}\p{Ll}\p{Lt}\p{Lo}\u200E\u200F\u061C])`, 'uy')
120
102
 
121
103
  function isSpaceKind(kind ) {
122
104
  return kind === SPACE || kind === PRESERVED_SPACE
@@ -283,11 +265,6 @@ function getUnspacedGraphemes(text , start , end , grapheme
283
265
  return unspaced
284
266
  }
285
267
 
286
- // Bidi class B: the characters that end a bidi paragraph.
287
- function isParagraphSeparatorCode(code ) {
288
- return code === 0x0a || code === 0x0d || (code >= 0x1c && code <= 0x1e) || code === 0x85 || code === 0x2029
289
- }
290
-
291
268
  // The lists of a rich-inline paragraph's handle that each item's measurement adds its segments
292
269
  // to; `segmentFlags` has room for them from `widths.length` on.
293
270
 
@@ -296,6 +273,75 @@ function isParagraphSeparatorCode(code ) {
296
273
 
297
274
 
298
275
 
276
+ // The functions from here to measureAnalysis() are its loop's. They are given what they read
277
+ // and not nested in it: nested, every call makes an object for the variables they read and
278
+ // one for each of them, in every engine. Where that object is as large as a handle,
279
+ // JavaScriptCore puts it in the handles' cells, one between each two, and Safari 27 ran
280
+ // layout() over 2,000 short labels 14-17% slower. Any object of 13 to 18 fields that every
281
+ // prepare() makes and drops lands there too: with one planted here, 11-13% slower.
282
+
283
+ // A WebKit text item runs to its next break opportunity, so it also owns any
284
+ // zero-width breaks before the space. The item is measured with one following
285
+ // U+0020 minus a space measured alone, which keeps the kerning between the item's
286
+ // end and that space. Under letter spacing both measurements hold the space's
287
+ // spacing (TextUtil::width over singleSpaceWidth, TextUtil.cpp:54-100, in WebKit
288
+ // 7625), so the item keeps that kerning there too. WebKit's item is never narrower
289
+ // than 0 (TextUtil.cpp:103), which the per-grapheme gap model does not represent.
290
+ // A soft hyphen before the space takes no kerning: on an RTL page WebKit needs
291
+ // about a hyphen's width more to fit such an item, and preparation cannot see the
292
+ // page direction. WebKit splits a text item where the bidi level changes before it
293
+ // measures it (breakAndComputeBidiLevels, InlineItemsBuilder.cpp:637-728), and
294
+ // format characters between a word and the space resolve with the space. Premise:
295
+ // the space resolves to the word's direction, so they stay in the word's item and
296
+ // the word keeps its kerning. Its gap is a space that resolves to the other
297
+ // direction, where Safari measures the word alone (ENGINE_FOLLOWUPS.md, Bidi
298
+ // levels, direction and script runs). Returns the zero-width breaks between the
299
+ // text and the space, or null when the text takes no kerning.
300
+ function getFollowingSpaceTail(analysis , analysisIndex , to ) {
301
+ const { normalized, spaceSources, starts, flags } = analysis
302
+ let next = analysisIndex + 1
303
+ while (next < to && (flags[next] & KIND_BITS) === ZERO_WIDTH_BREAK) next++
304
+ if (next >= to) return null
305
+ const nextKind = flags[next] & KIND_BITS
306
+ // A collapsed space's first source character: WebKit looks at the source after a text item.
307
+ const spaceStart = starts[next]
308
+ if ((nextKind !== SPACE && nextKind !== PRESERVED_SPACE) || (spaceSources === null ? normalized.charCodeAt(spaceStart) : spaceSources[spaceStart] ) !== 0x20) return null
309
+ return normalized.slice(starts[analysisIndex + 1], spaceStart)
310
+ }
311
+
312
+ // A zero-width break before the space ends the measured item, so only the
313
+ // item's kerning with the space is added to the text's own width.
314
+ function getTailKerning(item , measurement , spaceWidth ) {
315
+ return getFollowingSpaceMetrics(item, measurement).width - getSegmentMetrics(item, measurement).width - spaceWidth
316
+ }
317
+
318
+ // Pieces split by a soft hyphen are measured apart, but Blink and Gecko shape
319
+ // the unbroken text together: cursive joins, marks and kerning across the soft
320
+ // hyphen. Canvas shows how much narrower the neighbors measure joined than
321
+ // apart, which isolated widths can't show when proving that a hyphen overflows.
322
+ // A line counts it for the soft hyphens it passes, and in Gecko for the one it
323
+ // ends at (returnsFromUnfitHyphen in src/line-break.ts). WebKit's return fits each
324
+ // side as measured alone and takes none (unfitHyphenRetreat).
325
+ function getJoinedNarrowing(analysis , analysisIndex , to , before , beforeMetrics , measurement , emojiCorrection , lineFitEpsilon ) {
326
+ if (before === null) return 0
327
+ const { texts, flags } = analysis
328
+ let next = analysisIndex + 1
329
+ while (next < to && (flags[next] & KIND_BITS) === SOFT_HYPHEN) next++
330
+ if (next >= to || (flags[next] & KIND_BITS) !== TEXT) return 0
331
+ const after = texts[next]
332
+ const apart = getCorrectedSegmentWidth(before, beforeMetrics , measurement, emojiCorrection) + getTextWidth(after, measurement, emojiCorrection)
333
+ const together = getTextWidth(before + after, measurement, emojiCorrection)
334
+ return apart - together > lineFitEpsilon ? apart - together : 0
335
+ }
336
+
337
+ // A text segment's width as measured: alone, or together with the following space less
338
+ // that space, plus the item's kerning with a space that follows zero-width breaks. Apart
339
+ // from measureAnalysis()'s loop, whose code JavaScriptCore otherwise never optimizes fully
340
+ // on CJK text (RESEARCH.md, Keeping Work Bounded).
341
+ function getTextSegmentWidth(text , textMetrics , measurement , emojiCorrection , followingSpaceWidth , followingSpaceKerning ) {
342
+ return getCorrectedSegmentWidth(text, textMetrics, measurement, emojiCorrection) - followingSpaceWidth + followingSpaceKerning
343
+ }
344
+
299
345
  // Measures segments [from, to) of an analysis in a font, alone: all of a text's, or those of one
300
346
  // item of a rich-inline paragraph (RESEARCH.md, Rich Inline As One Paragraph), where only the
301
347
  // marks Blink halts and a run of U+3000 read the text around the item (getHanKerningTrims,
@@ -343,7 +389,16 @@ export function measureAnalysis(
343
389
  const hasLetterSpacing = letterSpacing !== 0
344
390
  const fontMeasurement = getFontMeasurement(font, language, hasLetterSpacing)
345
391
  const emojiCorrection = textMayContainEmoji(own) ? getEmojiCorrection(font, fontMeasurement) : 0
346
- const spaceWidth = getSpaceWidth(fontMeasurement)
392
+ // Whether the text holds a U+0020. Only then can a space follow one of its text segments
393
+ // (getFollowingSpaceTail), so a text of none, as most CJK paragraphs are, doesn't look for
394
+ // one after each segment.
395
+ const hasSpace = own.includes(' ')
396
+ // The font's space, asked for only by a text that holds a space or a tab. A web font split by
397
+ // unicode-range has the part with U+0020 loaded only once a text that holds a character of
398
+ // that part was loaded for (document.fonts.load(font, text)), so a text of other characters
399
+ // alone, a CJK label or one Cyrillic word, would measure the fallback font's space and the
400
+ // font would keep it for every later text.
401
+ const spaceWidth = hasSpace || own.includes('\t') ? getSpaceWidth(fontMeasurement) : 0
347
402
  // The advance between tab stops: eight spaces, each with its letter spacing where the
348
403
  // engine counts it (EngineProfile's letterSpaceTabStops). Gecko rounds the space and the
349
404
  // letter spacing to app units, sixtieths of a pixel, each on its own
@@ -354,143 +409,13 @@ export function measureAnalysis(
354
409
  : (spaceWidth + tabStopSpacing) * 8
355
410
  // The least a tab advances, measured at a text's first tab (EngineProfile's tabMinimumCharacter).
356
411
  let minimumTabAdvance = 0
357
- // Only a segment holding a default-ignorable code point has entry geometry, so text
358
- // without one doesn't look for it.
359
- const entryFitBasis = engineProfile.entryFitBasis !== 'disabled' && textMayHaveEntryGeometry(own) ? engineProfile.entryFitBasis : 'disabled'
360
-
361
- // A collapsed space's first source character, for engines that look at the
362
- // source after a text item.
363
- function getSpaceSourceCode(analysisIndex ) {
364
- const start = starts[analysisIndex]
365
- return analysis.spaceSources === null ? normalized.charCodeAt(start) : analysis.spaceSources[start]
366
- }
367
-
368
- // A WebKit text item runs to its next break opportunity, so it also owns any
369
- // zero-width breaks before the space. The item is measured with one following
370
- // U+0020 minus an unshaped space, which keeps the kerning between the item's
371
- // end and that space. With letter spacing the same measurement also moves the
372
- // space's gap onto the item and clamps the item at zero, which the
373
- // per-grapheme gap model does not represent, so only the unspaced case takes
374
- // the kerning. A soft hyphen before the space also takes none: on an RTL page
375
- // WebKit needs about a hyphen's width more to fit such an item, and
376
- // preparation cannot see the page direction. Returns the zero-width breaks
377
- // between the text and the space, or null when the text takes no kerning.
378
- function getFollowingSpaceTail(analysisIndex , text ) {
379
- if (!engineProfile.measureTextWithFollowingSpace || hasLetterSpacing) return null
380
- let next = analysisIndex + 1
381
- while (next < to && (flags[next] & KIND_BITS) === ZERO_WIDTH_BREAK) next++
382
- if (next >= to) return null
383
- const nextKind = flags[next] & KIND_BITS
384
- if ((nextKind !== SPACE && nextKind !== PRESERVED_SPACE) || getSpaceSourceCode(next) !== 0x20) return null
385
- const tail = normalized.slice(starts[analysisIndex + 1], starts[next])
386
- return formatTailStaysWithWord(tail === '' ? text : text + tail, starts[next] - ownStart) ? tail : null
387
- }
388
-
389
- // Text directly before such a space is measured together with the space
390
- // instead of alone, so its kerned width costs no extra Canvas call. Other
391
- // occurrences of the same text measure it alone.
392
- function getTextMetrics(text , followingSpaceTail ) {
393
- return followingSpaceTail === '' ? getFollowingSpaceMetrics(text, fontMeasurement) : getSegmentMetrics(text, fontMeasurement)
394
- }
395
-
396
- // A zero-width break before the space ends the measured item, so only the
397
- // item's kerning with the space is added to the text's own width.
398
- function getTailKerning(item ) {
399
- return getFollowingSpaceMetrics(item, fontMeasurement).width - getSegmentMetrics(item, fontMeasurement).width - spaceWidth
400
- }
401
-
402
- // WebKit splits text items where resolved bidi levels change before it
403
- // measures them. Format characters between a word and the space resolve with
404
- // that space, so they stay in the word's item, and the word's last glyph keeps
405
- // its kerning with the space, only when the space resolves to the word's
406
- // direction. Without the paragraph direction that is known when the word's last
407
- // letter or direction mark and the first one after the space have the same
408
- // direction, with only spaces and format characters between (UAX #9 N1).
409
- // Explicit embeddings, overrides and isolates end with their paragraph (UAX #9
410
- // X8), so only controls in the space's own paragraph leave its direction
411
- // unknown. Spaces arrive in order, so each paragraph is scanned once.
412
- let hasExplicitBidiControls = null
413
- let controlParagraphEnd = -1
414
- let paragraphHasExplicitBidiControls = false
415
- function spaceParagraphHasExplicitBidiControls(spaceStart ) {
416
- hasExplicitBidiControls ??= explicitBidiControlRe.test(own)
417
- if (!hasExplicitBidiControls) return false
418
- if (spaceStart < controlParagraphEnd) return paragraphHasExplicitBidiControls
419
- let start = spaceStart
420
- while (start > 0 && !isParagraphSeparatorCode(own.charCodeAt(start - 1))) start--
421
- let end = spaceStart
422
- while (end < own.length && !isParagraphSeparatorCode(own.charCodeAt(end))) end++
423
- controlParagraphEnd = end
424
- paragraphHasExplicitBidiControls = explicitBidiControlRe.test(own.slice(start, end))
425
- return paragraphHasExplicitBidiControls
426
- }
427
- function formatTailStaysWithWord(item , spaceStart ) {
428
- if (!trailingFormatCharacterRe.test(item)) return true
429
- const before = letterBeforeFormatTailRe().exec(item)
430
- if (before === null) return false
431
- letterAfterSpacesRe().lastIndex = spaceStart
432
- const after = letterAfterSpacesRe().exec(own)
433
- if (after === null) return false
434
- // An ASCII digit takes the direction of the text before it (UAX #9 W7 and N1).
435
- const next = after[1]
436
- return (next.charCodeAt(0) <= 0x39 || rightToLeftLetterRe.test(before[1] ) === rightToLeftLetterRe.test(next)) &&
437
- !spaceParagraphHasExplicitBidiControls(spaceStart)
438
- }
439
-
440
- // The source a run of combining marks shapes after when only zero-width glue,
441
- // controls or other such runs, with no break, separate the run from the grapheme
442
- // before it: that grapheme and what separates them. Without the separators, Canvas
443
- // can compose the marks with the grapheme or draw both in another font. A walk that
444
- // reaches the last run that asked takes that run's answer, so each segment is walked
445
- // and each grapheme found once, however many runs share it. Once what separates them
446
- // passes MARK_CHAIN_CONTEXT_UNITS, the context keeps the grapheme and the fewest of the
447
- // chain's last runs, each with the separators before it, that hold at least that many
448
- // units: in Chrome, Safari and Firefox, runs of 1 to 400 marks then measure as they do
449
- // after the whole chain, to 0.002px, and without the grapheme some took 25px less
450
- // (RESEARCH.md, Break Opportunities From Engine Data).
451
- let markRunIndex = -1
452
- let markBaseStart = -1 // where that run's grapheme starts in the normalized text, or -1
453
- let markChainStart = -1 // the segment after that grapheme
454
- let markChainKept = -1 // the first segment of the chain the context keeps
455
- // Asked only for a text segment no break comes before.
456
- function getMarkContext(analysisIndex , text ) {
457
- if (!markRunRe.test(text)) return null
458
- let baseStart = -1
459
- for (let k = analysisIndex - 1; k >= from; k--) {
460
- const kind = flags[k] & KIND_BITS
461
- const start = starts[k]
462
- const end = starts[k + 1]
463
- if (kind === ZERO_WIDTH_GLUE || ((kind === TEXT || kind === CONTROL) && controlOrMarkRunRe.test(texts[k] ))) {
464
- if (k !== markRunIndex) continue
465
- baseStart = markBaseStart
466
- } else if (kind === TEXT) {
467
- const ends = new Int32Array(end - start)
468
- const count = findGraphemeEnds(engineProfile.graphemeTable, normalized, start, end, ends)
469
- baseStart = count > 1 ? ends[count - 2] : start
470
- markChainStart = markChainKept = k + 1
471
- }
472
- break
473
- }
474
- markRunIndex = analysisIndex
475
- markBaseStart = baseStart
476
- if (baseStart < 0) return null
477
- const start = starts[analysisIndex]
478
- if (start - baseStart <= MARK_CHAIN_CONTEXT_UNITS) return normalized.slice(baseStart, start)
479
- // The kept part starts after the grapheme or a run of marks, moves only forward and
480
- // never holds fewer than MARK_CHAIN_CONTEXT_UNITS.
481
- for (let k = markChainKept + 1; start - starts[k] >= MARK_CHAIN_CONTEXT_UNITS; k++) {
482
- if (markRunRe.test(texts[k - 1] )) markChainKept = k
483
- }
484
- if (markChainKept === markChainStart) return normalized.slice(baseStart, start)
485
- return normalized.slice(baseStart, starts[markChainStart]) + normalized.slice(starts[markChainKept], start)
486
- }
487
412
 
488
413
  // The font's kerning with the space where words take Blink's kerning with the spaces beside
489
414
  // them, or null. Blink shapes nothing across a change of direction
490
415
  // (ShouldBreakShapingBeforeText, inline_node.cc:472-490), and which spaces share a word's
491
416
  // direction depends on the paragraph's, which preparation can't see, so text with a
492
417
  // right-to-left letter or an explicit bidi control takes none.
493
- let fontSpaceKerning = engineProfile.kernsSpacesInScriptRun && own.includes(' ') ? getFontSpaceKerning(fontMeasurement) : null
418
+ let fontSpaceKerning = engineProfile.kernsAcrossCanvasWords && own.includes(' ') ? getFontSpaceKerning(fontMeasurement) : null
494
419
  if (fontSpaceKerning !== null && mixedDirectionRe.test(own)) fontSpaceKerning = null
495
420
  const scriptRuns = { read: 0, scripts: ANY_SCRIPT, openBrackets: [] }
496
421
  // What the word before a space adds to that space, the next segment.
@@ -503,53 +428,12 @@ export function measureAnalysis(
503
428
  // the scan breaks at every segment boundary, and layout() counts it with the
504
429
  // simple stepper where it doesn't (countPreparedLines).
505
430
  let simpleKinds = !hasLetterSpacing
506
- let entryGeometry = null
507
- let lineStartProhibitions = null
508
431
  let breakableLineStartExtras = null
509
- // WebKit's line-start rule applies only in text holding a code unit above U+00FF.
510
- const keepsLineStartPunctuation = engineProfile.keepsLineStartPunctuation && /[\u0100-\uFFFF]/.test(own)
511
432
  let discretionaryHyphenContexts = null
512
433
  let previousJoinablePiece = null
513
434
  let previousJoinableMetrics = null
514
-
515
- // Pieces split by a soft hyphen are measured apart, but Blink and Gecko shape
516
- // the unbroken text together: cursive joins, marks and kerning across the soft
517
- // hyphen. Canvas shows how much narrower the neighbors measure joined than
518
- // apart, which isolated widths can't show when proving that a hyphen overflows.
519
- // WebKit's return fits each side as measured alone and takes none (unfitHyphenRetreat).
435
+ // WebKit's return from an unfit hyphen fits each side as measured alone (getJoinedNarrowing).
520
436
  const returnFitsEachSideAlone = engineProfile.unfitHyphenRetreat === 'full-width-or-first'
521
- function getJoinedNarrowing(analysisIndex , before , beforeMetrics ) {
522
- if (before === null) return 0
523
- let next = analysisIndex + 1
524
- while (next < to && (flags[next] & KIND_BITS) === SOFT_HYPHEN) next++
525
- if (next >= to || (flags[next] & KIND_BITS) !== TEXT) return 0
526
- const after = texts[next]
527
- const apart = getCorrectedSegmentWidth(before, beforeMetrics , fontMeasurement, emojiCorrection) + getTextWidth(after, fontMeasurement, emojiCorrection)
528
- const together = getTextWidth(before + after, fontMeasurement, emojiCorrection)
529
- return apart - together > engineProfile.lineFitEpsilon ? apart - together : 0
530
- }
531
-
532
- function getEntryGeometry(text , fit , width , fitBasis ) {
533
- // The fit fixes the text, font and advances, and Pretext sets no other context state.
534
- // The WebKit profile moves the advances by a following space, and it observes no
535
- // entries; the Chromium profile's kerning with a space is in the width alone, which
536
- // its fresh entries don't read.
537
- const cached = fit.entryGeometry
538
- if (cached !== null && cached.letterSpacing === letterSpacing && cached.emojiCorrection === emojiCorrection) return cached.geometry
539
- const geometry = observeSegmentEntries(text, fit.advances , letterSpacing, width, fitBasis,
540
- source => measureWithLetterSpacing(source, letterSpacing, emojiCorrection, fontMeasurement))
541
- // Replacing this last observation leaves prepared copies intact.
542
- fit.entryGeometry = { letterSpacing, emojiCorrection, geometry }
543
- return geometry
544
- }
545
-
546
- // A text segment's width as measured: alone, or together with the following space less
547
- // that space, plus the item's kerning with a space that follows zero-width breaks. Apart
548
- // from the loop below, whose code JavaScriptCore otherwise never optimizes fully on CJK
549
- // text (RESEARCH.md, Keeping Work Bounded).
550
- function getTextSegmentWidth(text , textMetrics , measuredWithSpace , followingSpaceKerning ) {
551
- return getCorrectedSegmentWidth(text, textMetrics, fontMeasurement, emojiCorrection) - (measuredWithSpace ? spaceWidth : 0) + followingSpaceKerning
552
- }
553
437
 
554
438
  // Each segment is stored at its index: `at` in the text's or the paragraph's three lists,
555
439
  // `mi - from` in the lists of the measured segments alone. Stored and not pushed, for V8:
@@ -567,8 +451,6 @@ export function measureAnalysis(
567
451
  // Graphemes that take letter spacing after them.
568
452
  let spacingGraphemeCount = 0
569
453
  let fitAdvances = null
570
- let entry = null
571
- let prohibitions = null
572
454
  switch (kind) {
573
455
  case TEXT: {
574
456
  // A control the engine hides takes no advance, only letter spacing.
@@ -576,27 +458,15 @@ export function measureAnalysis(
576
458
  spacingGraphemeCount = 1
577
459
  break
578
460
  }
579
- // Such a run of marks adds its context with the marks, minus the context, and
580
- // takes no letter spacing of its own. Only a segment no break comes before can be
581
- // one, and the test is here so that the others make no call: with it Chrome 154
582
- // prepares CJK text again 2.6% faster than with a call for every text segment, which
583
- // V8 didn't inline (RESEARCH.md, Keeping Work Bounded, JavaScript Engines, under V8's
584
- // inlining budgets and the mark context).
585
- const markContext = (segment & UNBROKEN) === 0 ? null : getMarkContext(mi, text)
586
- if (markContext !== null) {
587
- width = engineProfile.shapesMarksAcrossSoftHyphen && texts[mi - 1] === '\u00AD' && nonspacingMarkRunRe.test(text)
588
- ? 0
589
- : getTextWidth(markContext + text, fontMeasurement, emojiCorrection) - getTextWidth(markContext, fontMeasurement, emojiCorrection)
590
- break
591
- }
592
- // With an empty following-space tail, the text is measured together with the
593
- // space; with a zero-width tail, the item's kerning is added.
594
- const followingSpaceTail = getFollowingSpaceTail(mi, text)
461
+ // With an empty following-space tail, the text is measured together with the space,
462
+ // so its kerned width costs no extra Canvas call, and other occurrences of the same
463
+ // text measure it alone; with a zero-width tail, the item's kerning is added.
464
+ const followingSpaceTail = engineProfile.measureTextWithFollowingSpace && hasSpace ? getFollowingSpaceTail(analysis, mi, to) : null
595
465
  const measuredWithSpace = followingSpaceTail === ''
596
- const textMetrics = getTextMetrics(text, followingSpaceTail)
466
+ const textMetrics = measuredWithSpace ? getFollowingSpaceMetrics(text, fontMeasurement) : getSegmentMetrics(text, fontMeasurement)
597
467
  previousJoinablePiece = text
598
468
  previousJoinableMetrics = textMetrics
599
- let followingSpaceKerning = followingSpaceTail === null || measuredWithSpace ? 0 : getTailKerning(text + followingSpaceTail)
469
+ let followingSpaceKerning = followingSpaceTail === null || measuredWithSpace ? 0 : getTailKerning(text + followingSpaceTail, fontMeasurement, spaceWidth)
600
470
  if (fontSpaceKerning !== null && textMetrics.spaceKerning !== noSpaceKerning) {
601
471
  const spaceBefore = mi > from && isSpaceKind(flags[mi - 1] & KIND_BITS)
602
472
  const spaceAfter = mi + 1 < to && isSpaceKind(flags[mi + 1] & KIND_BITS)
@@ -625,17 +495,24 @@ export function measureAnalysis(
625
495
  }
626
496
  }
627
497
  }
628
- width = getTextSegmentWidth(text, textMetrics, measuredWithSpace, followingSpaceKerning)
498
+ width = getTextSegmentWidth(text, textMetrics, fontMeasurement, emojiCorrection, measuredWithSpace ? spaceWidth : 0, followingSpaceKerning)
629
499
  // The walkers put a gap after every grapheme of a spaced segment, so a grapheme
630
500
  // the engine gives none takes one back from its advance, here and in the advances
631
- // a break inside the segment falls by.
501
+ // a break inside the segment falls by, and one whose glyphs WebKit gives more or
502
+ // fewer takes the difference (EngineProfile's letterSpacesGlyphs), in text of its
503
+ // complex path.
632
504
  let unspaced = null
505
+ let extraSpacings = noExtraSpacings
633
506
  if (hasLetterSpacing) {
634
507
  spacingGraphemeCount = countRenderedSpacingGraphemes(text, kind, engineProfile.graphemeTable)
635
508
  if (cursiveSpacing) {
636
509
  unspaced = getUnspacedGraphemes(own, starts[mi] - ownStart, starts[mi] - ownStart + text.length, engineProfile.graphemeTable, engineProfile.unspacedCursive === 'run' ? scriptRuns : null)
637
510
  if (unspaced !== null) width -= unspaced.length * letterSpacing
638
511
  }
512
+ if (engineProfile.letterSpacesGlyphs) {
513
+ extraSpacings = textMetrics.extraSpacings ??= needsComplexTextPath(text) ? getExtraSpacings(text, fontMeasurement) : noExtraSpacings
514
+ for (let k = 0; k < extraSpacings.length; k++) width += extraSpacings[k] * letterSpacing
515
+ }
639
516
  }
640
517
  // Under break-word, Blink retries an overflowing line with a break allowed between
641
518
  // any two graphemes (line_breaker.cc), WebKit searches the word's grapheme prefixes
@@ -647,8 +524,7 @@ export function measureAnalysis(
647
524
  : numericRunRe.test(text) ? 'pair-context'
648
525
  : textMetrics.width < engineProfile.prefixFitMinWidth ? 'sum-graphemes'
649
526
  : engineProfile.cutWordFit
650
- const fit = getSegmentFit(text, textMetrics, fontMeasurement, emojiCorrection, fitMode,
651
- measuredWithSpace ? spaceWidth : null, engineProfile.keepsLineStartPunctuation)
527
+ const fit = getSegmentFit(text, textMetrics, fontMeasurement, emojiCorrection, fitMode, measuredWithSpace ? spaceWidth : null)
652
528
  fitAdvances = fit.advances
653
529
  if (fitAdvances === null) break
654
530
  // A difference from the advance, so it holds whatever the advances take below.
@@ -659,15 +535,14 @@ export function measureAnalysis(
659
535
  fitAdvances = fitAdvances.slice()
660
536
  fitAdvances[fitAdvances.length - 1] = fitAdvances[fitAdvances.length - 1] + followingSpaceKerning
661
537
  }
662
- // Such a segment takes no entry geometry, whose fresh widths Canvas would space
663
- // by its own rule (ENGINE_FOLLOWUPS.md, Letter spacing).
538
+ if (extraSpacings.length !== 0) {
539
+ fitAdvances = fitAdvances.slice()
540
+ for (let k = 0; k < extraSpacings.length; k++) fitAdvances[k] = fitAdvances[k] + extraSpacings[k] * letterSpacing
541
+ }
664
542
  if (unspaced !== null) {
665
543
  fitAdvances = fitAdvances.slice()
666
544
  for (let k = 0; k < unspaced.length; k++) fitAdvances[unspaced[k] ] = fitAdvances[unspaced[k] ] - letterSpacing
667
- } else if (entryFitBasis !== 'disabled') {
668
- entry = getEntryGeometry(text, fit, addInternalLetterSpacing(width, spacingGraphemeCount, letterSpacing), entryFitBasis)
669
545
  }
670
- if (keepsLineStartPunctuation) prohibitions = fit.lineStartProhibitions
671
546
  break
672
547
  }
673
548
  case SPACE:
@@ -704,17 +579,19 @@ export function measureAnalysis(
704
579
  segmentFlags[at] = (segment & ~ONE_CLUSTER) | (hasLetterSpacing && spacingGraphemeCount > 0 ? SPACED : 0)
705
580
  widths[at] = addInternalLetterSpacing(width, spacingGraphemeCount, letterSpacing)
706
581
  breakableFitAdvances[at] = fitAdvances
707
- if (entry !== null && entryGeometry === null) entryGeometry = Array.from({ length: mi - from }, () => null)
708
- if (entryGeometry !== null) entryGeometry[mi - from] = entry
709
- if (prohibitions !== null && lineStartProhibitions === null) lineStartProhibitions = Array.from({ length: mi - from }, () => null)
710
- if (lineStartProhibitions !== null) lineStartProhibitions[mi - from] = prohibitions
711
582
  if (segments !== null) segments[mi - from] = text
712
583
  // Contexts for every segment of soft hyphens, whatever its kind here, one that is glue
713
584
  // too, where the scan gives no break after it: a handle with contexts is one whose lines
714
- // return from an unfit hyphen, measured below.
585
+ // return from an unfit hyphen, measured below. One that is a zero-width break is marked
586
+ // (DROPPED), which tells it from a zero-width space, and its text is the full walker's: the
587
+ // simple walkers take every zero-width break for a zero-width space.
715
588
  if (kind !== TEXT && text.charCodeAt(0) === 0xAD) {
589
+ if (kind === ZERO_WIDTH_BREAK) {
590
+ segmentFlags[at] = segmentFlags[at] | DROPPED
591
+ simpleKinds = false
592
+ }
716
593
  discretionaryHyphenContexts ??= zeros(mi - from)
717
- discretionaryHyphenContexts[mi - from] = returnFitsEachSideAlone ? 0 : getJoinedNarrowing(mi, previousJoinablePiece, previousJoinableMetrics)
594
+ discretionaryHyphenContexts[mi - from] = returnFitsEachSideAlone ? 0 : getJoinedNarrowing(analysis, mi, to, previousJoinablePiece, previousJoinableMetrics, fontMeasurement, emojiCorrection, engineProfile.lineFitEpsilon)
718
595
  } else if (discretionaryHyphenContexts !== null) {
719
596
  discretionaryHyphenContexts[mi - from] = 0
720
597
  }
@@ -734,6 +611,13 @@ export function measureAnalysis(
734
611
  const trims = hanKerning.widthTrims
735
612
  if (trims !== null) for (let i = 0; i < trims.length; i++) widths[base + i] = widths[base + i] - trims[i]
736
613
  }
614
+ // The loop above has measured every segment in the font, so text in a font that has measured
615
+ // no kana, as in one that has answered that it kerns none, isn't looked through for kana.
616
+ let lineStartExtras = hanKerning.lineStartExtras
617
+ if (engineProfile.kernsAcrossCanvasWords && fontMeasurement.measuredKana && fontMeasurement.kanaKerning !== null && kanaPairRe.test(own)) {
618
+ const kanaKerning = getFontKanaKerning(fontMeasurement)
619
+ if (kanaKerning !== null) lineStartExtras = addKanaKerning(lineStartExtras, widths, base, analysis, from, to, fontMeasurement, kanaKerning)
620
+ }
737
621
  let lineEndTrims = hanKerning.lineEndTrims
738
622
  if (engineProfile.hangsIdeographicSpace && own.includes('\u3000')) {
739
623
  lineEndTrims = addIdeographicSpaceHangs(lineEndTrims, analysis, fontMeasurement, letterSpacing, discretionaryHyphenWidth, from, to)
@@ -744,13 +628,11 @@ export function measureAnalysis(
744
628
  simpleLineWalkFastPath: simpleKinds && !analysis.hasUnbroken,
745
629
  simpleLineCountFastPath: simpleKinds,
746
630
  breakableFitAdvances,
747
- entryGeometry,
748
631
  letterSpacing,
749
632
  discretionaryHyphenWidth,
750
633
  discretionaryHyphenContexts,
751
- lineStartProhibitions,
752
634
  breakableLineStartExtras,
753
- lineStartExtras: hanKerning.lineStartExtras,
635
+ lineStartExtras,
754
636
  lineEndTrims,
755
637
  overflowLineEndTrims: hanKerning.overflowLineEndTrims,
756
638
  tabStopAdvance,
@@ -760,6 +642,44 @@ export function measureAnalysis(
760
642
  return prepared
761
643
  }
762
644
 
645
+ // Blink's layout shapes a run of one script in one call and takes katakana for hiragana, so
646
+ // that the two stay in one run (GetScriptForOpenType, script_run_iterator.cc:20-36), so a kana
647
+ // kerns with the kana after it in fonts that pair them, as Hiragino Sans and Hiragino Mincho
648
+ // ProN do (GPOS pair positioning). Canvas cuts the two apart and preparation measures segments
649
+ // apart, so each such pair's kerning (getKanaKerning) is added here, for a font that kerns
650
+ // kana (getFontKanaKerning). The adjustment sits on the pair's first glyph, and a line that
651
+ // breaks between the two is shaped again without it (ShapingLineBreaker::ShapeLine,
652
+ // shaping_line_breaker.cc:511-584): one that tightens the pair goes on the second kana's
653
+ // segment, which gives it back where it starts a line. One that widens it stays on the first
654
+ // kana's, since Blink looks for a line's end in the run shaped whole
655
+ // (shaping_line_breaker.cc:325-333), where the first kana has it. A segment cut between two of
656
+ // its kana takes the same in its advances (getSegmentFit). `widths` holds segments [from, to)
657
+ // of the analysis from `base`, a rich-inline item's in its paragraph's, and `extras` holds
658
+ // them from index 0; a kana that starts an item takes no kerning with the item before it.
659
+ function addKanaKerning(extras , widths , base , analysis , from , to , measurement , kernings ) {
660
+ const { normalized, starts, flags } = analysis
661
+ for (let i = from; i < to; i++) {
662
+ if ((flags[i] & KIND_BITS) !== TEXT) continue
663
+ const start = starts[i]
664
+ const end = i + 1 < flags.length ? starts[i + 1] : normalized.length
665
+ const at = base + i - from
666
+ for (let k = i > from ? start : start + 1; k < end; k++) {
667
+ const kerning = getKanaKerning(normalized, start, k, measurement, kernings)
668
+ if (kerning === 0) continue
669
+ if (k > start) {
670
+ widths[at] = widths[at] + kerning
671
+ } else if (kerning > 0) {
672
+ widths[at - 1] = widths[at - 1] + kerning
673
+ } else {
674
+ widths[at] = widths[at] + kerning
675
+ extras ??= zeros(to - from)
676
+ extras[i - from] = extras[i - from] - kerning
677
+ }
678
+ }
679
+ }
680
+ return extras
681
+ }
682
+
763
683
  // Blink (Chrome 153) hangs a run of U+3000 that ends a line, as it hangs spaces: ShapingLineBreaker
764
684
  // counts U+3000 as a breakable space (IsBreakableSpace, shaping_line_breaker.cc:38-41, with
765
685
  // Character::IsOtherSpaceSeparator, character.h:156-158), so a line whose width runs out on