rapier-markdown-kit 1.2.33 → 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 +15 -9
  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 +28 -2
  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 +25 -23
  45. package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
@@ -1,8 +1,7 @@
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
  import { findGraphemeEnds, } from './graphemes.js'
3
- import { canWebKitLineStartWith, DEFAULT_IGNORABLE, getBlinkDefaultLocale, hasProperty, MARK } from './line-breaks.js'
3
+ import { DEFAULT_IGNORABLE, getBlinkDefaultLocale, hasProperty, MARK } from './line-breaks.js'
4
4
  import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages, webkitScriptSubtags } from './generated/webkit-generic-families.js'
5
-
6
5
 
7
6
 
8
7
  // What preparation knows of a segment in a font. Each is created with all its fields,
@@ -12,6 +11,9 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
12
11
 
13
12
 
14
13
 
14
+
15
+
16
+
15
17
 
16
18
 
17
19
  // A text segment's kerning with a U+0020 beside it, in the Chromium profile (getSpaceKerning).
@@ -28,23 +30,9 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
28
30
 
29
31
 
30
32
 
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
33
 
41
34
 
42
35
 
43
-
44
-
45
-
46
-
47
-
48
36
 
49
37
 
50
38
 
@@ -102,13 +90,16 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
102
90
 
103
91
 
104
92
 
105
-
93
+
94
+
106
95
 
107
96
 
108
97
 
109
98
 
99
+
100
+
110
101
 
111
-
102
+
112
103
 
113
104
 
114
105
 
@@ -135,11 +126,23 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
135
126
 
136
127
 
137
128
 
138
-
129
+
130
+
131
+
132
+
133
+
134
+
139
135
 
140
-
141
-
142
-
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
144
+
145
+
143
146
 
144
147
 
145
148
 
@@ -167,6 +170,24 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
167
170
 
168
171
 
169
172
 
173
+
174
+
175
+
176
+
177
+
178
+
179
+
180
+
181
+
182
+
183
+
184
+
185
+
186
+
187
+
188
+
189
+
190
+
170
191
 
171
192
 
172
193
 
@@ -213,12 +234,6 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
213
234
 
214
235
 
215
236
 
216
-
217
-
218
-
219
-
220
-
221
-
222
237
 
223
238
 
224
239
 
@@ -234,7 +249,7 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
234
249
 
235
250
 
236
251
 
237
-
252
+
238
253
 
239
254
 
240
255
 
@@ -242,6 +257,14 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
242
257
 
243
258
 
244
259
 
260
+
261
+
262
+
263
+
264
+
265
+
266
+
267
+
245
268
 
246
269
 
247
270
 
@@ -307,6 +330,20 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
307
330
 
308
331
 
309
332
 
333
+
334
+
335
+
336
+
337
+
338
+
339
+
340
+
341
+
342
+
343
+
344
+
345
+
346
+
310
347
 
311
348
 
312
349
 
@@ -349,6 +386,11 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
349
386
 
350
387
 
351
388
 
389
+
390
+
391
+
392
+
393
+
352
394
 
353
395
 
354
396
 
@@ -359,6 +401,7 @@ import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages,
359
401
 
360
402
 
361
403
 
404
+
362
405
 
363
406
  let measureState = null
364
407
  // What preparation keeps per font. It all goes together, when the caches clear or the
@@ -378,12 +421,19 @@ let measureState = null
378
421
 
379
422
 
380
423
 
424
+
425
+
426
+
427
+
428
+
429
+
381
430
 
382
431
 
383
432
 
384
433
 
385
434
 
386
435
 
436
+
387
437
 
388
438
  let cachedEngineProfile = null
389
439
 
@@ -404,6 +454,19 @@ const MAX_PREFIX_FIT_GRAPHEMES = 96
404
454
  // `font-variant-ligatures: none` (2026-09-30).
405
455
  const LETTER_SPACED_SHAPING = '0.000001px'
406
456
 
457
+ // The most segments the fonts' caches hold between them when a preparation starts
458
+ // (getPreparationLanguage): past it they are all dropped, as clearCache() drops them, before
459
+ // that preparation measures anything. A handle keeps its own widths, so nothing prepared
460
+ // changes; text prepared afterwards is measured again. Prose adds segments ever more slowly,
461
+ // but ids, numbers and URLs never repeat, so without a bound the caches grow for as long as the
462
+ // page lives. The bound is a safety net well above what a page uses, not a tuned size: the
463
+ // 11,290 plain texts of the harness's real-usage sample, 883,084 units, fill in one font 71,294
464
+ // segments in the Chromium profile, 90,023 in Gecko's and 115,367 in WebKit's, 241-363 B each in
465
+ // JavaScriptCore, and a bound under a page's own count has its text measured again and again:
466
+ // at 65,536, preparing those texts a second time asks Canvas 81,286 to 196,534 times, where
467
+ // under no bound it asks nothing (the harness's stand-in Canvas, 2026-10-12).
468
+ const MAX_CACHED_SEGMENTS = 1 << 18
469
+
407
470
  // Graphemes the emoji font may draw a glyph in, which Canvas widths then tell
408
471
  // (countEmojiGlyphs): those holding an emoji-presentation character or a pictograph,
409
472
  // or an emoji character followed by U+FE0F, such as a keycap base like `1`. U+FE0F
@@ -551,6 +614,11 @@ export function setLocaleLanguage(locale ) {
551
614
  // else the page's, or null without a document. The Chromium profile lays out a page
552
615
  // without one under Blink's default locale (laysOutUnderDefaultLocale).
553
616
  export function getPreparationLanguage(profile ) {
617
+ // A preparation starts with this read, so the fonts' caches are bounded here, between two
618
+ // preparations (MAX_CACHED_SEGMENTS), and not where a font is looked up: a rich-inline
619
+ // paragraph looks one up for each of its items, and caches dropped between two of them would
620
+ // have one paragraph measured partly from the caches and partly anew.
621
+ if (measureState !== null && measureState.segments > MAX_CACHED_SEGMENTS) clearMeasurementCaches()
554
622
  let language = localeLanguage ?? null
555
623
  if (localeLanguage === undefined && typeof document !== 'undefined') {
556
624
  const lang = (document.documentElement )?.lang
@@ -583,26 +651,62 @@ export function zeros(count ) {
583
651
  return out
584
652
  }
585
653
 
586
- // A text's width as Canvas spaces it under a letter spacing, or null where the context
587
- // can't take the spacing. Under none, that is the text's width in the font, which the
588
- // font's segment cache asks once. Under a spacing it is asked each time, borrowing the
589
- // font's context for the synchronous call, never enters a segment cache, and
590
- // letterSpacing is restored even when assignment or measurement fails.
591
- export function measureWithLetterSpacing(text , letterSpacing , emojiCorrection , measurement ) {
592
- if (!measurement.state.takesLetterSpacing) return null
593
- if (letterSpacing === 0) return getTextWidth(text, measurement, emojiCorrection)
594
- // Counted before the spacing is set: the count measures stretches into the font's segment cache.
595
- const corrected = emojiCorrection === 0 ? 0 : countEmojiGlyphs(text, measurement) * emojiCorrection
596
- const context = getContext(measurement)
597
- const previous = context.letterSpacing
654
+ // The Canvas letterSpacing a text is measured under to count the glyphs it spaces: far
655
+ // wider than any text asked about, so the text's width over it rounds to the count with
656
+ // nothing taken off, and a power of two, which float32 widths add whole. A text 2^19 px
657
+ // wide under no spacing would count a glyph more.
658
+ const GLYPH_COUNT_SPACING = 2 ** 20
659
+
660
+ // The spacings of a segment whose graphemes each take one, and of any segment where Canvas
661
+ // can't be asked.
662
+ export const noExtraSpacings = []
663
+
664
+ // Per grapheme of a text segment, how many letter spacings it takes more than one, where
665
+ // the engine spaces glyphs (EngineProfile's letterSpacesGlyphs). Canvas spaces by the
666
+ // engine's rule, so a prefix of the segment measured under a letter spacing holds that
667
+ // spacing once for each of its glyphs with an advance, and a grapheme takes what its
668
+ // prefix has more than the prefix before it: WebKit fits a cut word by its prefixes too
669
+ // (TextUtil::breakWord). A grapheme can take none, as the alef of lam-alef, or fewer,
670
+ // where it joins the glyphs before it into one. Past MAX_PREFIX_FIT_GRAPHEMES each
671
+ // grapheme is asked alone, which keeps a giant segment linear and misses a glyph two of
672
+ // its graphemes share. A text's count is kept for the font (FontMeasurement's
673
+ // glyphCounts), so words that start alike ask about a prefix once: one more Canvas call
674
+ // for each prefix new to the font, which WebKit shapes each time, since it keeps no width
675
+ // measured under a letter spacing (TextMeasurementCache.h:165-167). Under a context that
676
+ // takes no letterSpacing every grapheme takes one.
677
+ export function getExtraSpacings(seg , measurement ) {
678
+ if (!measurement.state.takesLetterSpacing) return noExtraSpacings
679
+ const ends = new Int32Array(seg.length)
680
+ const count = findGraphemeEnds(getEngineProfile().graphemeTable, seg, 0, seg.length, ends)
681
+ const prefixes = count <= MAX_PREFIX_FIT_GRAPHEMES
682
+ const counts = measurement.glyphCounts
683
+ // The font's context once Canvas is asked, under the counting spacing from then on, so
684
+ // it changes its spacing once, and not at all for a segment whose prefixes are counted.
685
+ let context = null
686
+ let extras = noExtraSpacings
598
687
  try {
599
- context.letterSpacing = `${letterSpacing}px`
600
- if (Number.parseFloat(context.letterSpacing) !== letterSpacing) return null
601
- const width = context.measureText(text).width - corrected
602
- return Number.isFinite(width) ? width : null
688
+ let before = 0
689
+ for (let i = 0, start = 0; i < count; start = ends[i++] ) {
690
+ const text = seg.slice(prefixes ? 0 : start, ends[i])
691
+ let glyphs = counts.get(text)
692
+ if (glyphs === undefined) {
693
+ if (context === null) {
694
+ context = getContext(measurement)
695
+ context.letterSpacing = `${GLYPH_COUNT_SPACING}px`
696
+ }
697
+ glyphs = Math.round(context.measureText(text).width / GLYPH_COUNT_SPACING)
698
+ counts.set(text, glyphs)
699
+ }
700
+ const extra = glyphs - (prefixes ? before : 0) - 1
701
+ before = glyphs
702
+ if (extra === 0 && extras.length === 0) continue
703
+ if (extras.length === 0) extras = zeros(count)
704
+ extras[i] = extra
705
+ }
603
706
  } finally {
604
- context.letterSpacing = previous
707
+ if (context !== null) context.letterSpacing = measurement.letterSpacing
605
708
  }
709
+ return extras
606
710
  }
607
711
 
608
712
  // The lookup internalizes seg, and a string V8 hashes for the first time there gets a one-byte
@@ -622,23 +726,37 @@ export function getFollowingSpaceMetrics(seg , measurement
622
726
  }
623
727
 
624
728
  function addMetrics(cache , seg , text , measurement ) {
625
- const metrics = { width: getContext(measurement).measureText(text).width, emojiCount: -1, fit: null, spaceKerning: null }
729
+ const metrics = { width: getContext(measurement).measureText(text).width, emojiCount: -1, fit: null, spaceKerning: null, extraSpacings: null }
626
730
  cache.set(seg, metrics)
731
+ measurement.state.segments++
732
+ if (!measurement.measuredKana && kanaRe.test(seg)) measurement.measuredKana = true
627
733
  return metrics
628
734
  }
629
735
 
736
+ // A character of the Hiragana and Katakana blocks, U+3041 to U+30FF, two of them in a row, and
737
+ // how many the blocks hold.
738
+ const kanaRe = /[\u3041-\u30FF]/
739
+ export const kanaPairRe = /[\u3041-\u30FF]{2}/
740
+ const KANA_COUNT = 0x30ff - 0x3041 + 1
741
+
630
742
  // The kerning of every segment that takes none.
631
743
  export const noSpaceKerning = { after: 0, before: 0 }
632
744
 
745
+ // Whether a character is a kana or an ideograph, which Canvas shapes as a word of its own: it
746
+ // cuts a string before one that follows another, and keeps with a letter the marks and CJK
747
+ // punctuation after it (NextWordEndIndex, plain_text_node.cc:92-153, over
748
+ // kIsCjkIdeographOrSymbolRanges, character_property_data.h:40-80, of which these are the
749
+ // letters).
750
+ function isCanvasWordLetter(code ) {
751
+ return (code >= 0x3041 && code <= 0x3096) || (code >= 0x30a1 && code <= 0x30fa) ||
752
+ (code >= 0x3400 && code <= 0x9fff) || (code >= 0xf900 && code <= 0xfaff)
753
+ }
754
+
633
755
  // Whether a character takes no kerning with a space, and Canvas isn't asked.
634
756
  function takesNoSpaceKerning(code ) {
635
- // A combining mark or half of a surrogate pair is part of a longer cluster.
636
- return hasProperty(code, MARK) || (code & 0xf800) === 0xd800 ||
637
- // Canvas shapes each ideograph and kana as a word of its own, so it shows no kerning beside
638
- // one (NextWordEndIndex, plain_text_node.cc:92-153, over kIsCjkIdeographOrSymbolRanges,
639
- // character_property_data.h:40-80, of which these are the letters).
640
- (code >= 0x3041 && code <= 0x3096) || (code >= 0x30a1 && code <= 0x30fa) ||
641
- (code >= 0x3400 && code <= 0x9fff) || (code >= 0xf900 && code <= 0xfaff) ||
757
+ // A combining mark or half of a surrogate pair is part of a longer cluster. Canvas shows no
758
+ // kerning beside a letter it shapes as a word of its own.
759
+ return hasProperty(code, MARK) || (code & 0xf800) === 0xd800 || isCanvasWordLetter(code) ||
642
760
  // Premise: no font kerns a Hangul syllable with the space (RESEARCH.md, Kerning At Line Edges).
643
761
  (code >= 0xac00 && code <= 0xd7a3)
644
762
  }
@@ -686,7 +804,7 @@ function getCharacterSpaceKerning(code , spaceFirst , measurement
686
804
  }
687
805
 
688
806
  // The kerning Blink's layout gives a text segment's edges with a U+0020 beside them
689
- // (EngineProfile.kernsSpacesInScriptRun), read from Canvas with U+2028 for the space: Blink
807
+ // (EngineProfile.kernsAcrossCanvasWords), read from Canvas with U+2028 for the space: Blink
690
808
  // draws U+2028 with the space glyph (HarfBuzzGetGlyph, harfbuzz_face.cc:103-113) and its Canvas
691
809
  // doesn't cut there. Premise: the segment's last and first character stand for the word, past
692
810
  // default ignorables, which HarfBuzz's lookups pass over, and a first character with a combining
@@ -707,6 +825,138 @@ export function getSpaceWidth(measurement ) {
707
825
  return measurement.spaceWidth ??= getSegmentMetrics(' ', measurement).width
708
826
  }
709
827
 
828
+ // Twelve kana in a row, nine of whose eleven pairs or more kern in each Japanese face measured
829
+ // that kerns kana at all (getFontKanaKerning).
830
+ const KANA_PROBE = '\u30D7\u30C0\u30B0\u30BF\u30CE\u30E0\u30D6\u30A4\u30E1\u3048\u305A\u307A'
831
+
832
+ // The font's context for kana: Canvas shapes a string whole, as Blink's layout shapes a run,
833
+ // under text-rendering: optimizeLegibility where the lookups of the first font with a space
834
+ // cover the space glyph (FontFallbackList::ComputeCanShapeWordByWord, font_fallback_list.cc:264-277),
835
+ // as those of the Japanese fonts of macOS do. It is a context of its own: the first measures
836
+ // everything else as it did, and a kana alone measures the same on both. It is set to the
837
+ // list the font's kana are asked in (getFontKanaKerning), and isn't asked whether it takes the
838
+ // list (contextTakesFont): the font's own list is one the first context, made the same way,
839
+ // took, and where a shorter one is no font string, as a list cut inside a calc() size, the
840
+ // context keeps the list before it and reads what it read under that one, so the search goes
841
+ // on to the next list or ends on the font's own.
842
+ function getWholeRunContext(measurement ) {
843
+ const state = measurement.state
844
+ if (state.wholeRunContext === null) {
845
+ state.wholeRunContext = createContext(state.language)
846
+ state.wholeRunContext.textRendering = 'optimizeLegibility'
847
+ }
848
+ if (state.wholeRunFont !== measurement.kanaFont) state.wholeRunContext.font = state.wholeRunFont = measurement.kanaFont
849
+ return state.wholeRunContext
850
+ }
851
+
852
+ // Whether two measurements of one string are of the same glyphs at the same places: as wide,
853
+ // with the same ink edges, to float32 rounding (getKanaKerning has the bound).
854
+ function sameInk(a , b ) {
855
+ const bound = a.width / 0x400000
856
+ return Math.abs(a.width - b.width) <= bound && Math.abs(a.actualBoundingBoxLeft - b.actualBoundingBoxLeft) <= bound &&
857
+ Math.abs(a.actualBoundingBoxRight - b.actualBoundingBoxRight) <= bound
858
+ }
859
+
860
+ // What is kept of the font's kerning between kana, or null for a font that shows none: one in
861
+ // which KANA_PROBE, shaped whole, is as wide with kerning off (getFontSpaceKerning has what
862
+ // `fontKerning` turns off). Canvas shapes a string whole by the list's first font alone, where
863
+ // Blink's layout shapes the kana of a run together whatever font leads the list
864
+ // (InlineNode::ShapeText, inline_node.cc:1551-1718), so a list that shows no kerning is asked
865
+ // again without its first family, then without its first two, and so on: once the first
866
+ // font's lookups cover the space glyph, as the kana's own font's do, Canvas shapes the probe
867
+ // whole. A shorter list stands for the font only where the same font draws its kana, which
868
+ // two answers tell. It draws the probe with the same glyphs, as wide and with the same ink
869
+ // edges with kerning off: the first that doesn't ends the search, since a family left out
870
+ // drew the kana. And the kerning Canvas does show under the font's own list, a kana's with a
871
+ // U+3002 it keeps in the kana's word (NextWordEndIndex, plain_text_node.cc:129-153), is the
872
+ // shorter list's and isn't nothing: Hiragino Sans GB draws Hiragino Sans's kana and kerns
873
+ // none. A font that shows no kerning under any list stays as wide as Canvas measures its kana
874
+ // apart. Asked once per font, with two measurements for each list tried and three more for
875
+ // one that shows kerning. Premises: a font that kerns kana kerns one of the probe's pairs
876
+ // (RESEARCH.md, Kerning At Line Edges, has the fonts measured), and one of its hiragana with
877
+ // U+3002 (ENGINE_FOLLOWUPS.md, Kerning between kana).
878
+ export function getFontKanaKerning(measurement ) {
879
+ if (measurement.kanaKerning === undefined) {
880
+ measurement.kanaKerning = null
881
+ const font = measurement.canvasFont
882
+ const size = fontSizeRe.exec(font)
883
+ const start = size === null ? 0 : size.index + size[0].length
884
+ const families = size === null ? null : font.slice(start).match(familyRe)
885
+ const lists = families === null ? 1 : families.length
886
+ let own = null
887
+ for (let i = 0; i < lists; i++) {
888
+ const list = i === 0 ? font : font.slice(0, start) + families .slice(i).join(',')
889
+ measurement.kanaFont = list
890
+ const context = getWholeRunContext(measurement)
891
+ const kerned = context.measureText(KANA_PROBE).width
892
+ context.fontKerning = 'none'
893
+ const unkerned = context.measureText(KANA_PROBE)
894
+ context.fontKerning = 'auto'
895
+ if (own === null) own = unkerned
896
+ else if (!sameInk(own, unkerned)) break
897
+ if (kerned === unkerned.width) continue
898
+ if (i > 0) {
899
+ // Each hiragana letter before a U+3002: Canvas shapes each pair as one word under any list.
900
+ let pairs = ''
901
+ for (let code = 0x3041; code <= 0x3096; code++) pairs += String.fromCharCode(code, 0x3002)
902
+ const shorter = context.measureText(pairs).width
903
+ measurement.kanaFont = font
904
+ const ownContext = getWholeRunContext(measurement)
905
+ const ownKerned = ownContext.measureText(pairs).width
906
+ ownContext.fontKerning = 'none'
907
+ const ownUnkerned = ownContext.measureText(pairs).width
908
+ ownContext.fontKerning = 'auto'
909
+ if (ownKerned === ownUnkerned || Math.abs(shorter - ownKerned) > ownKerned / 0x400000) break
910
+ measurement.kanaFont = list
911
+ }
912
+ measurement.kanaKerning = new Float64Array(KANA_COUNT * KANA_COUNT).fill(NaN)
913
+ break
914
+ }
915
+ if (measurement.kanaKerning === null) measurement.kanaFont = font
916
+ }
917
+ return measurement.kanaKerning
918
+ }
919
+
920
+ // The kerning Blink's layout gives text[at - 1] and text[at] that no measured width holds, or
921
+ // 0: that of two characters of the kana blocks that were measured apart. `start` is where the
922
+ // segment that holds text[at] starts. Two segments are measured apart, so a segment's first
923
+ // character takes its kerning with the one before it. Inside a segment Canvas cuts before a
924
+ // kana letter where the word before it holds a letter already. A mark such as U+30FC stays in
925
+ // the word before it, and a word that marks or CJK punctuation began takes the first letter
926
+ // after them (NextWordEndIndex's has_any_script, plain_text_node.cc:129-153), so Canvas has
927
+ // measured those pairs together; a character below the CJK ranges is in a word of other
928
+ // scripts, which ends before the letter. Each pair is asked of Canvas once per font and kept
929
+ // in the font's table by its two characters, where a pair not asked about yet is NaN.
930
+ export function getKanaKerning(text , start , at , measurement , kernings ) {
931
+ const second = text.charCodeAt(at)
932
+ if (second < 0x3041 || second > 0x30ff) return 0
933
+ const first = text.charCodeAt(at - 1)
934
+ if (first < 0x3041 || first > 0x30ff) return 0
935
+ if (at > start) {
936
+ if (!isCanvasWordLetter(second)) return 0
937
+ // What Canvas's word held before the marks that end it.
938
+ let k = at - 1
939
+ let code = first
940
+ while (code >= 0x3041 && code <= 0x30ff && !isCanvasWordLetter(code)) {
941
+ if (k === start) return 0
942
+ code = text.charCodeAt(--k)
943
+ }
944
+ if (code >= 0x2e80 && !isCanvasWordLetter(code)) return 0
945
+ }
946
+ const index = (first - 0x3041) * KANA_COUNT + second - 0x3041
947
+ const kerning = kernings[index]
948
+ return Number.isNaN(kerning) ? kernings[index] = measureKanaKerning(text.slice(at - 1, at + 1), measurement) : kerning
949
+ }
950
+
951
+ // The kerning of two kana in a row: the two in one string on the context that shapes a string
952
+ // whole, less each alone.
953
+ function measureKanaKerning(pair , measurement ) {
954
+ const pairWidth = getWholeRunContext(measurement).measureText(pair).width
955
+ const kerning = pairWidth - getSegmentMetrics(pair[0] , measurement).width - getSegmentMetrics(pair[1] , measurement).width
956
+ // As for a character and a space, up to the pair's width / 2^22 is rounding.
957
+ return Math.abs(kerning) <= pairWidth / 0x400000 ? 0 : kerning
958
+ }
959
+
710
960
  // A text's width in the font, less the emoji correction.
711
961
  export function getTextWidth(text , measurement , emojiCorrection ) {
712
962
  return getCorrectedSegmentWidth(text, getSegmentMetrics(text, measurement), measurement, emojiCorrection)
@@ -742,15 +992,14 @@ export function getEngineProfile() {
742
992
  }
743
993
 
744
994
  function buildEngineProfile() {
745
- const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent
995
+ // A navigator can lack a userAgent, as React Native's does, where lib.dom's type says it is a
996
+ // string: such a runtime names no engine, like one with no navigator.
997
+ const ua = typeof navigator === 'undefined' ? '' : (navigator ).userAgent ?? ''
746
998
  // Engines Pretext doesn't recognize take Blink's profile (RESEARCH.md, Decisions Log,
747
999
  // 2026-09-26).
748
1000
  const engine = getLayoutEngine(ua) ?? 'blink'
749
- // Fresh-entry observations are verified only for desktop Blink and Gecko.
750
- const isDesktop = /Windows NT|Macintosh|X11/.test(ua) && !/Android|Mobile|iPhone|iPad|iPod/.test(ua)
751
1001
 
752
1002
  return {
753
- entryFitBasis: isDesktop && engine === 'blink' ? 'fresh' : isDesktop && engine === 'gecko' ? 'original' : 'disabled',
754
1003
  lineBreakScan: engine,
755
1004
  graphemeTable: engine === 'webkit' ? 'apple/char' : engine === 'gecko' ? 'gecko/char' : 'chromium/char',
756
1005
  lineFitEpsilon: engine === 'webkit' ? 1 / 64 : 0.005,
@@ -758,12 +1007,12 @@ function buildEngineProfile() {
758
1007
  prefixFitMinWidth: engine === 'webkit' ? 0 : 80,
759
1008
  cutWordKeepsLigatures: engine === 'gecko',
760
1009
  measureTextWithFollowingSpace: engine === 'webkit',
761
- kernsSpacesInScriptRun: engine === 'blink',
1010
+ kernsAcrossCanvasWords: engine === 'blink',
762
1011
  letterSpaceDiscretionaryHyphen: engine !== 'blink',
763
1012
  letterSpacingInAppUnits: engine === 'gecko',
764
1013
  canvasLetterSpacingDropsLigatures: engine !== 'webkit',
765
1014
  unspacedCursive: engine === 'blink' ? 'run' : engine === 'gecko' ? 'cluster' : 'none',
766
- shapesMarksAcrossSoftHyphen: engine === 'blink',
1015
+ letterSpacesGlyphs: engine === 'webkit',
767
1016
  unfitHyphenRetreat: engine === 'blink' ? 'reduced-width' : engine === 'gecko' ? 'full-width' : 'full-width-or-first',
768
1017
  hyphenFromPrimaryFont: engine !== 'gecko',
769
1018
  letterSpaceTabStops: engine !== 'webkit',
@@ -772,15 +1021,15 @@ function buildEngineProfile() {
772
1021
  tabsInAppUnits: engine === 'gecko',
773
1022
  hangTabs: engine !== 'gecko',
774
1023
  zeroWidthGlueTakesLine: engine !== 'gecko',
775
- keepsLineStartPunctuation: engine === 'webkit',
776
1024
  hidesControlCharacters: engine === 'gecko',
777
1025
  hanKerning: engine === 'blink',
778
1026
  hangsIdeographicSpace: engine !== 'webkit',
779
1027
  laysOutUnderDefaultLocale: engine === 'blink',
780
- namesGenericFamiliesByLanguage: engine === 'webkit',
1028
+ namesGenericFamiliesByLanguage: engine === 'webkit' && !/^(?:Linux|Win32|Haiku|$)/.test(navigator.platform),
781
1029
  hardBreakItemRetreat: engine === 'blink' ? 'item' : engine === 'webkit' ? 'fit' : 'last-grapheme',
782
1030
  paddedOpeningFit: engine === 'blink' ? 'start' : engine === 'webkit' ? 'placed' : 'both',
783
1031
  emptyAtomicAlwaysFits: engine === 'gecko',
1032
+ zeroWidthSpaceStaysAfterAtomic: engine === 'gecko',
784
1033
  hangsSpacesPerTextFrame: engine === 'gecko',
785
1034
  testsHyphenBeforeAtomic: engine !== 'webkit',
786
1035
  transformsSegmentBreaksAcrossItems: engine === 'blink',
@@ -802,31 +1051,71 @@ export function getEmojiCorrection(font , measurement )
802
1051
  if (correction !== null) return correction
803
1052
 
804
1053
  const fontSize = parseFontSize(font)
1054
+ // Canvas's width is there already when an earlier text asked and the page had no box to read.
1055
+ const askedBefore = measurement.emojiWidth !== 0
805
1056
  const canvasW = measurement.emojiWidth = getContext(measurement).measureText('\u{1F600}').width
806
1057
  correction = 0
807
- // document.body is null until the parser reaches <body>, which lib.dom's type leaves out.
808
- if (
809
- canvasW > fontSize + 0.5 &&
810
- typeof document !== 'undefined' &&
811
- (document.body ) !== null
812
- ) {
813
- const span = document.createElement('span')
814
- span.style.font = font
815
- span.style.display = 'inline-block'
816
- span.style.visibility = 'hidden'
817
- span.style.position = 'absolute'
818
- span.textContent = '\u{1F600}'
819
- document.body.appendChild(span)
820
- const domW = span.getBoundingClientRect().width
821
- document.body.removeChild(span)
1058
+ if (canvasW > fontSize + 0.5 && typeof document !== 'undefined') {
1059
+ const domW = readEmojiWidth(font)
1060
+ // A document with no box for the span, as in an iframe that is display: none, has no
1061
+ // width to read. The text goes uncorrected, as in a worker, and nothing is kept: the next
1062
+ // text that may hold an emoji asks again.
1063
+ if (Number.isNaN(domW)) return 0
822
1064
  if (canvasW - domW > 0.5) {
823
1065
  correction = canvasW - domW
1066
+ // Advances cut from a segment before this reading hold emoji uncorrected.
1067
+ if (askedBefore) {
1068
+ for (const metrics of measurement.metrics.values()) metrics.fit = null
1069
+ for (const metrics of measurement.followingSpaceMetrics.values()) metrics.fit = null
1070
+ }
824
1071
  }
825
1072
  }
826
1073
  measurement.emojiCorrection = correction
827
1074
  return correction
828
1075
  }
829
1076
 
1077
+ // The page's width of one emoji in the font, or NaN where the page has no box for it. The
1078
+ // span takes two things from the page, the font asked for and the page's language, which
1079
+ // picks fonts as it does for Canvas (-webkit-locale is where Blink and WebKit keep it, and
1080
+ // `all` resets it; Gecko's -x-lang is internal and `all` leaves it). Everything else is at
1081
+ // its initial value, and important, so that nothing the page declares reaches the span,
1082
+ // inherited from <body> or matched by a rule: a letter-spacing, a text-indent or a padding
1083
+ // would be read as part of the emoji, and a vertical writing-mode gives a line's height.
1084
+ function readEmojiWidth(font ) {
1085
+ // The span goes in <body>, beside the page's text, and in <html> where that gives it no
1086
+ // box: document.body is null until the parser reaches <body>, and a <body> that is
1087
+ // display: none, as a page hidden until it is ready, has no boxes. A document whose root
1088
+ // isn't <html>, as an SVG file's, has neither a body nor a head, and lays out no HTML.
1089
+ // lib.dom's types leave the nulls out.
1090
+ const body = document.body
1091
+ if (body === null && (document.head ) === null) return NaN
1092
+ const span = document.createElement('span')
1093
+ span.style.cssText = 'all:initial!important;-webkit-locale:inherit!important;display:inline-block!important;visibility:hidden!important;position:absolute!important'
1094
+ span.style.setProperty('font', font, 'important')
1095
+ span.textContent = '\u{1F600}'
1096
+ let width = body === null ? NaN : readWidthIn(body, span)
1097
+ if (Number.isNaN(width)) width = readWidthIn(document.documentElement, span)
1098
+ return width
1099
+ }
1100
+
1101
+ // The span's width under a parent. Its resolved width is its box's used width in the span's
1102
+ // own pixels, and `auto`, so NaN, where it has no box (nsComputedDOMStyle::DoGetWidth,
1103
+ // nsComputedDOMStyle.cpp:1803-1814; StyleExtractorCustom.h:570-576 in WebKit; Blink hands the
1104
+ // property the element's LayoutObject, css_computed_style_declaration.cc:381-383), but it is
1105
+ // written with six significant digits. Its rectangle has every digit, but is the box as
1106
+ // drawn: a transform or a zoom above the span scales it, and it is 0 with no box, which read
1107
+ // as an emoji 0px wide. So the rectangle's width is taken where it is the resolved one
1108
+ // within those digits, and the resolved one where something scales the span. The rectangle
1109
+ // is asked first: before the parser reaches <body>, Firefox makes the document's first boxes
1110
+ // for a rectangle and not for a resolved width, which is `auto` until then.
1111
+ function readWidthIn(parent , span ) {
1112
+ parent.appendChild(span)
1113
+ const drawn = span.getBoundingClientRect().width
1114
+ const width = parseFloat(getComputedStyle(span).width)
1115
+ parent.removeChild(span)
1116
+ return Math.abs(drawn - width) <= width * 1e-5 ? drawn : width
1117
+ }
1118
+
830
1119
  // Canvas reports a width as a 32-bit float (CanvasRenderingContext2D.cpp:5277 in Firefox,
831
1120
  // text_metrics.cc:179 in Chrome), so a few equal advances measure that many times one
832
1121
  // only within that float's rounding, 2^-24 of the width each time one is rounded: 2e-6 px
@@ -911,20 +1200,16 @@ export function getSegmentFit(
911
1200
  // When metrics measured seg together with one following U+0020, the width of
912
1201
  // that space alone. The last grapheme then keeps its kerning with the space.
913
1202
  followingSpaceWidth = null,
914
- // Whether to find the segment's WebKit line-start prohibitions.
915
- withLineStartProhibitions = false,
916
1203
  ) {
917
1204
  if (metrics.fit !== null && metrics.fit.mode === mode) return metrics.fit
918
1205
  const profile = getEngineProfile()
919
1206
  const ends = new Int32Array(seg.length)
920
1207
  const count = findGraphemeEnds(profile.graphemeTable, seg, 0, seg.length, ends)
921
- if (count <= 1) return metrics.fit = { mode, advances: null, lineStartExtras: null, lineStartProhibitions: null, entryGeometry: null }
922
- let prohibitions = null
923
- if (withLineStartProhibitions) {
924
- for (let i = 1; i < count; i++) if (!canWebKitLineStartWith(seg.charCodeAt(ends[i - 1] ))) (prohibitions ??= new Uint8Array(count))[i] = 1
925
- }
1208
+ if (count <= 1) return metrics.fit = { mode, advances: null, lineStartExtras: null }
926
1209
  let advances
927
1210
  let lineStartExtras = null
1211
+ // Whether each advance is of a grapheme measured after the one before it alone.
1212
+ let pairs = false
928
1213
  if (mode === 'reshaped-lines') {
929
1214
  // Blink shapes a line of a cut word again where the cut falls between letters shaped
930
1215
  // together (EngineProfile's cutWordFit). Premise: a line holds the letters whose width,
@@ -940,19 +1225,42 @@ export function getSegmentFit(
940
1225
  advances = alone
941
1226
  if (!addUpTo(alone, width)) {
942
1227
  advances = measureFitAdvances(seg, ends, count, 'pair-context', metrics, measurement, emojiCorrection, followingSpaceWidth)
943
- if (count <= MAX_PREFIX_FIT_GRAPHEMES && !addUpTo(advances, width)) {
944
- advances = measureFitAdvances(seg, ends, count, 'segment-prefixes', metrics, measurement, emojiCorrection, followingSpaceWidth)
945
- }
1228
+ pairs = count > MAX_PREFIX_FIT_GRAPHEMES || addUpTo(advances, width)
1229
+ if (!pairs) advances = measureFitAdvances(seg, ends, count, 'segment-prefixes', metrics, measurement, emojiCorrection, followingSpaceWidth)
946
1230
  lineStartExtras = [0]
947
1231
  for (let i = 1; i < count; i++) lineStartExtras.push(alone[i] - advances[i] )
948
1232
  }
949
1233
  } else {
950
1234
  // Past MAX_PREFIX_FIT_GRAPHEMES, prefixes give way to pairs.
951
1235
  const prefixes = mode === 'segment-prefixes' && count <= MAX_PREFIX_FIT_GRAPHEMES
952
- advances = measureFitAdvances(seg, ends, count, mode === 'segment-prefixes' && !prefixes ? 'pair-context' : mode, metrics, measurement, emojiCorrection, followingSpaceWidth)
1236
+ pairs = mode !== 'sum-graphemes' && !prefixes
1237
+ advances = measureFitAdvances(seg, ends, count, pairs ? 'pair-context' : mode, metrics, measurement, emojiCorrection, followingSpaceWidth)
953
1238
  if (prefixes && profile.cutWordKeepsLigatures) countLigaturesOnFirstLetter(seg, ends, advances, measurement, emojiCorrection)
954
1239
  }
955
- return metrics.fit = { mode, advances, lineStartExtras, lineStartProhibitions: prohibitions, entryGeometry: null }
1240
+ // Two kana that Canvas shapes apart inside the segment, as in a keep-all group, kern in
1241
+ // Blink's layout as two segments do, and a line cut between them is shaped again without
1242
+ // the kerning (addKanaKerning in prepare.ts has the rule): one that tightens the pair goes
1243
+ // on the second kana's advance, which gives it back where a line starts with it, and one
1244
+ // that widens it on the first's. Canvas cuts only before a kana letter, which starts a
1245
+ // grapheme. An advance measured after the grapheme before it alone is of a string that
1246
+ // starts at that grapheme, and Canvas's words are read from there: a mark that starts
1247
+ // it is in one word with the letter after it, whose advance has their kerning already.
1248
+ if (profile.kernsAcrossCanvasWords && measurement.kanaKerning !== null && kanaPairRe.test(seg)) {
1249
+ const kernings = getFontKanaKerning(measurement)
1250
+ if (kernings !== null) {
1251
+ for (let g = 1; g < count; g++) {
1252
+ const kerning = getKanaKerning(seg, pairs && g > 1 ? ends[g - 2] : 0, ends[g - 1] , measurement, kernings)
1253
+ if (kerning > 0) {
1254
+ advances[g - 1] = advances[g - 1] + kerning
1255
+ } else if (kerning < 0) {
1256
+ advances[g] = advances[g] + kerning
1257
+ lineStartExtras ??= zeros(count)
1258
+ lineStartExtras[g] = lineStartExtras[g] - kerning
1259
+ }
1260
+ }
1261
+ }
1262
+ }
1263
+ return metrics.fit = { mode, advances, lineStartExtras }
956
1264
  }
957
1265
 
958
1266
  // Moves the advance of each later letter of a ligature onto the ligature's first letter, in a
@@ -1053,31 +1361,57 @@ function measureFitAdvances(
1053
1361
  // prefixes and line-edge facts all come from that shaping. The context isn't touched
1054
1362
  // here: text that is all in the font's caches measures nothing (getContext).
1055
1363
  export function getFontMeasurement(font , language , letterSpaced ) {
1056
- // Preparation starts here, with the language it resolved. After that language
1057
- // changes, start again with a new context and empty caches; clearing the caches
1058
- // alone would re-measure with fonts resolved under the old language.
1364
+ // Measuring starts here, for a text and for each item of a rich-inline paragraph, with the
1365
+ // language the preparation resolved. After that language changes, start again with a new
1366
+ // context and empty caches; clearing the caches alone would re-measure with fonts resolved
1367
+ // under the old language.
1059
1368
  if (measureState === null || measureState.language !== language) measureState = createMeasureState(language)
1060
1369
  const state = measureState
1061
1370
  const shaped = letterSpaced && state.shapesLetterSpaced
1062
1371
  const fonts = shaped ? state.letterSpacedFonts : state.fonts
1372
+ // The first measurement after a lookup sets the font again, the same string too: Firefox's
1373
+ // context takes a face added to document.fonts only when its font is assigned, and the
1374
+ // question below leaves the context on a font of its own, whether it throws or not.
1375
+ state.font = ''
1063
1376
  let measurement = fonts.get(font)
1064
1377
  if (measurement === undefined) {
1065
1378
  const canvasFont = state.genericFamilies === null ? font : getCanvasFont(font, state.genericFamilies)
1066
- measurement = { state, canvasFont, letterSpacing: shaped ? LETTER_SPACED_SHAPING : '0px', metrics: new Map(), followingSpaceMetrics: new Map(), spaceKerning: undefined, emojiCorrection: null, emojiWidth: 0, hyphenText: null, spaceWidth: null, hanKerning: undefined, ligaturePairs: new Map() }
1379
+ if (!contextTakesFont(state.context, canvasFont)) throw new RangeError(`font must be a font string Canvas takes, a size in px then a family, such as 16px Inter, not "${font}"`)
1380
+ measurement = { state, canvasFont, letterSpacing: shaped ? LETTER_SPACED_SHAPING : '0px', metrics: new Map(), followingSpaceMetrics: new Map(), spaceKerning: undefined, measuredKana: false, kanaKerning: undefined, kanaFont: canvasFont, emojiCorrection: null, emojiWidth: 0, hyphenText: null, spaceWidth: null, hanKerning: undefined, ligaturePairs: new Map(), glyphCounts: new Map() }
1067
1381
  fonts.set(font, measurement)
1068
1382
  }
1069
- // The first measurement after a lookup sets the font again, the same string too: Firefox's
1070
- // context takes a face added to document.fonts only when its font is assigned.
1071
- state.font = ''
1072
1383
  return measurement
1073
1384
  }
1074
1385
 
1386
+ // Whether a Canvas context takes a font string. A context ignores a string that isn't a CSS font
1387
+ // shorthand and keeps the font it had (the HTML standard's font setter): a family before its
1388
+ // size, a string with no family, `var(--font)`, or an empty string, which
1389
+ // getComputedStyle(element).font is where the element sets a longhand the shorthand can't hold.
1390
+ // Firefox's OffscreenCanvas also ignores a size relative to a font, such as 1.2em or 120%, which
1391
+ // it has no element to resolve against (SetFontInternalDisconnected,
1392
+ // CanvasRenderingContext2D.cpp:4478-4490), and Firefox's and Safari's a system font such as
1393
+ // `caption` (Firefox 157.0.1, Safari 27.0, 2026-10-11). Every width would then be measured in
1394
+ // whatever font the context held last, so Pretext refuses such a string rather than guess at it,
1395
+ // as it refuses a letter spacing that isn't finite (readLetterSpacing). The context is set to a
1396
+ // font of the question's, then to the caller's, and still reads the first only where the
1397
+ // caller's was ignored; a caller's font that reads back as the first is asked again after
1398
+ // another. A context that doesn't read the question's font back as written refuses nothing.
1399
+ // Only the context's `font` is written and read, no element and no style, and nothing is
1400
+ // measured. Asked once for each font measurement, never for a text.
1401
+ function contextTakesFont(context , font ) {
1402
+ context.font = '1px serif'
1403
+ context.font = font
1404
+ if (context.font !== '1px serif') return true
1405
+ context.font = '2px serif'
1406
+ context.font = font
1407
+ return context.font !== '2px serif'
1408
+ }
1409
+
1075
1410
  // The font's context, set to the font and its shaping. Every measurement in a font takes
1076
1411
  // its context from here as it measures, so none reads a width under another font's
1077
1412
  // setting, whatever was looked up or prepared in between, and text that is all cached,
1078
1413
  // which measures nothing, sets nothing. What changes the context after that puts it back
1079
- // before it returns (getHyphenText, measureWithLetterSpacing, getFontSpaceKerning,
1080
- // isLigature).
1414
+ // before it returns (getHyphenText, getExtraSpacings, getFontSpaceKerning, isLigature).
1081
1415
  export function getContext(measurement ) {
1082
1416
  const state = measurement.state
1083
1417
  if (state.font !== measurement.canvasFont) state.context.font = state.font = measurement.canvasFont
@@ -1085,18 +1419,20 @@ export function getContext(measurement )
1085
1419
  return state.context
1086
1420
  }
1087
1421
 
1088
- function createMeasureState(language ) {
1089
- let context
1090
- if (typeof OffscreenCanvas !== 'undefined') {
1091
- context = new OffscreenCanvas(1, 1).getContext('2d')
1092
- } else if (typeof document !== 'undefined') {
1093
- context = document.createElement('canvas').getContext('2d')
1094
- } else {
1095
- throw new Error('Text measurement requires OffscreenCanvas or a DOM canvas context.')
1096
- }
1097
- // A context's `lang` follows the page's, and preparation's can be setLocale()'s or
1098
- // Blink's default locale instead.
1422
+ // A context's `lang` follows the page's, and preparation's can be setLocale()'s or Blink's
1423
+ // default locale instead.
1424
+ function createContext(language ) {
1425
+ // jsdom and happy-dom have a document whose canvas gives no 2D context: getContext() returns null there.
1426
+ let context = null
1427
+ if (typeof OffscreenCanvas !== 'undefined') context = new OffscreenCanvas(1, 1).getContext('2d')
1428
+ else if (typeof document !== 'undefined') context = document.createElement('canvas').getContext('2d')
1429
+ if (context === null) throw new Error('Text measurement requires OffscreenCanvas or a DOM canvas context.')
1099
1430
  if (language !== null && 'lang' in context) context.lang = language
1431
+ return context
1432
+ }
1433
+
1434
+ function createMeasureState(language ) {
1435
+ const context = createContext(language)
1100
1436
  const profile = getEngineProfile()
1101
1437
  const takesLetterSpacing = typeof context.letterSpacing === 'string'
1102
1438
  return {
@@ -1104,15 +1440,20 @@ function createMeasureState(language ) {
1104
1440
  context,
1105
1441
  font: '',
1106
1442
  letterSpacing: '0px',
1443
+ wholeRunContext: null,
1444
+ wholeRunFont: '',
1107
1445
  genericFamilies: language !== null && profile.namesGenericFamiliesByLanguage ? getWebKitGenericFamilies(language, context) : null,
1108
1446
  takesLetterSpacing,
1109
1447
  shapesLetterSpaced: takesLetterSpacing && profile.canvasLetterSpacingDropsLigatures,
1110
1448
  fonts: new Map(),
1111
1449
  letterSpacedFonts: new Map(),
1450
+ segments: 0,
1112
1451
  }
1113
1452
  }
1114
1453
 
1115
1454
  export function clearMeasurementCaches() {
1116
- measureState?.fonts.clear()
1117
- measureState?.letterSpacedFonts.clear()
1455
+ if (measureState === null) return
1456
+ measureState.fonts.clear()
1457
+ measureState.letterSpacedFonts.clear()
1458
+ measureState.segments = 0
1118
1459
  }