rapier-markdown-kit 1.1.101 → 1.1.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/README.md +213 -53
- package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
- package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
- package/dist/agent/vendor/pretext/LICENSE +208 -0
- package/dist/agent/vendor/pretext/NOTICE +26 -13
- package/dist/agent/vendor/pretext/SOURCE.json +213 -45
- package/dist/agent/vendor/pretext/analysis.js +675 -1224
- package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
- package/dist/agent/vendor/pretext/graphemes.js +98 -0
- package/dist/agent/vendor/pretext/han-kerning.js +253 -0
- package/dist/agent/vendor/pretext/layout.js +352 -290
- package/dist/agent/vendor/pretext/line-break.js +1154 -588
- package/dist/agent/vendor/pretext/line-breaks.js +892 -0
- package/dist/agent/vendor/pretext/line-text.js +81 -39
- package/dist/agent/vendor/pretext/measurement.js +1105 -176
- package/dist/agent/vendor/pretext/prepare.js +807 -0
- package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
- package/dist/agent/will.mjs +2 -0
- package/dist/kit/assets.mjs +5 -1
- package/dist/kit/conformance/run.mjs +4 -4
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +9 -7
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +166 -53
- package/dist/kit/ledger/hash.mjs +75 -18
- package/dist/kit/ledger/history.mjs +143 -115
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/render-markdown.mjs +259 -122
- package/dist/kit/render-sanitize.mjs +24 -4
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +101 -65
- package/dist/kit/typography.mjs +2 -0
- package/dist/layout/hyphenation.mjs +127 -0
- package/dist/layout/line-plan.mjs +88 -96
- package/dist/layout/model.mjs +61 -80
- package/dist/layout/paragraph-fit.mjs +191 -0
- package/dist/layout/preparation.mjs +373 -0
- package/dist/layout/prepared-paragraph.mjs +223 -0
- package/dist/layout/typography.mjs +345 -111
- package/dist/packages/rapier-diff/index.mjs +288 -196
- package/dist/spec/document-fonts.mjs +13 -4
- package/dist/spec/md-assets.mjs +149 -30
- package/dist/spec/md-layout.mjs +5 -2
- package/dist/spec/md-marks.mjs +218 -3
- package/dist/spec/md-source.mjs +122 -0
- package/package.json +2 -1
- package/style.css +9 -0
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
|
@@ -1,189 +1,1118 @@
|
|
|
1
|
-
/*! Pretext
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
/*! Pretext Unreleased (bac1774c0) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
|
|
2
|
+
import { findGraphemeEnds, } from './graphemes.js'
|
|
3
|
+
import { canWebKitLineStartWith, DEFAULT_IGNORABLE, getBlinkDefaultLocale, hasProperty, MARK } from './line-breaks.js'
|
|
4
|
+
import { webkitGenericFamilies, webkitGenericFamilyNames, webkitScriptLanguages, webkitScriptSubtags } from './generated/webkit-generic-families.js'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// What preparation knows of a segment in a font. Each is created with all its fields,
|
|
9
|
+
// so their reads see one shape.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// A text segment's kerning with a U+0020 beside it, in the Chromium profile (getSpaceKerning).
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// Where a segment breaks under overflow-wrap: break-word in one fit mode (getSegmentFit),
|
|
24
|
+
// replaced whole when another mode is asked for.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
// The measurement context and what preparation measured through it. Canvas resolves
|
|
343
|
+
// fonts under the context's language, the page's unless the context has a `lang` to
|
|
344
|
+
// give it preparation's. Chrome keeps a resolved font while its font string is
|
|
345
|
+
// unchanged, so the context, and every width measured through it, belong to the
|
|
346
|
+
// language it was created under: all of it is replaced when that language changes.
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
let measureState = null
|
|
364
|
+
// What preparation keeps per font. It all goes together, when the caches clear or the
|
|
365
|
+
// language changes.
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
let cachedEngineProfile = null
|
|
389
|
+
|
|
390
|
+
// Prefix fits, which preparation picks by engine, width and letter spacing
|
|
391
|
+
// (measureAnalysis in src/prepare.ts), measure every growing prefix of a
|
|
392
|
+
// segment. That suits word-sized runs, but a giant segment would prepare in time
|
|
393
|
+
// that grows with the square of its length. Past this size, the cheaper
|
|
394
|
+
// pair-context model keeps preparation linear.
|
|
395
|
+
const MAX_PREFIX_FIT_GRAPHEMES = 96
|
|
396
|
+
|
|
397
|
+
// The Canvas letterSpacing a context measures letter-spaced text under: not 0, so the
|
|
398
|
+
// engine shapes the text as its page does under letter spacing, and too small to add any
|
|
399
|
+
// width, so one measurement serves every spacing and preparation adds the spacing itself.
|
|
400
|
+
// Blink adds spacing in units of 1/65536 px (ShapeResultSpacing::SetSpacing,
|
|
401
|
+
// shape_result_spacing.cc:14-33) and Gecko's Canvas in whole app units, 1/60 px
|
|
402
|
+
// (CanvasRenderingContext2D.cpp:4771-4774), and both read 0 here: 16px Roboto `difficult`
|
|
403
|
+
// measures 54.2031px in Chrome 154 and 54.2px in Firefox 156, their pages' widths with
|
|
404
|
+
// `font-variant-ligatures: none` (2026-09-30).
|
|
405
|
+
const LETTER_SPACED_SHAPING = '0.000001px'
|
|
406
|
+
|
|
407
|
+
// Graphemes the emoji font may draw a glyph in, which Canvas widths then tell
|
|
408
|
+
// (countEmojiGlyphs): those holding an emoji-presentation character or a pictograph,
|
|
409
|
+
// or an emoji character followed by U+FE0F, such as a keycap base like `1`. U+FE0F
|
|
410
|
+
// after a letter or a space changes nothing.
|
|
411
|
+
const emojiGraphemeRe = /\p{Emoji_Presentation}|\p{Extended_Pictographic}|\p{Emoji}\uFE0F/u
|
|
412
|
+
const maybeEmojiRe = /[\p{Emoji_Presentation}\p{Extended_Pictographic}\p{Regional_Indicator}\uFE0F\u20E3]/u
|
|
413
|
+
|
|
414
|
+
const genericKeywords = ['serif', 'sans-serif', 'cursive', 'fantasy', 'monospace']
|
|
415
|
+
// A quoted family name, or an unquoted generic keyword with what precedes it.
|
|
416
|
+
const familyListItemRe = /("[^"]*"|'[^']*')|(^|,)(\s*)(serif|sans-serif|cursive|fantasy|monospace)(?=\s*(?:,|$))/gi
|
|
417
|
+
// A font string's size, with its line height, after which its family list starts.
|
|
418
|
+
const fontSizeRe = /\dpx(?:\s*\/\s*\S+)?\s+/
|
|
419
|
+
// One family of a family list, with the white space around it.
|
|
420
|
+
const familyRe = /(?:"[^"]*"|'[^']*'|[^,])+/g
|
|
421
|
+
|
|
422
|
+
// The families WebKit's page gives the generic keywords under a language, or null
|
|
423
|
+
// where they resolve as in Canvas. The page asks Core Text wherever WebKit's script
|
|
424
|
+
// for the language isn't Common: localeToScriptCode tries the language, then its last
|
|
425
|
+
// subtag as a script, then the language less that subtag (LocaleToScriptMapping.cpp:
|
|
426
|
+
// 360-377). A plain Han language becomes the first preferred language starting with
|
|
427
|
+
// zh-, else zh-hans (FontDescription.cpp:74-113); a page can't read those languages,
|
|
428
|
+
// so Pretext takes zh-hans. Core Text's answers are OS data, generated with macOS's
|
|
429
|
+
// and iOS's families (scripts/generate-webkit-generic-families.ts): where they differ,
|
|
430
|
+
// the context takes macOS's if it has it. Listing both instead would send characters
|
|
431
|
+
// macOS's family lacks to iOS's, where the page falls back by language.
|
|
432
|
+
function getWebKitGenericFamilies(language , ctx ) {
|
|
433
|
+
let tag = language.toLowerCase().replaceAll('_', '-')
|
|
434
|
+
let han = null
|
|
435
|
+
for (let at = tag; han === null;) {
|
|
436
|
+
if (webkitScriptLanguages.includes(` ${at} `)) han = at === 'zh'
|
|
437
|
+
else {
|
|
438
|
+
const cut = at.lastIndexOf('-')
|
|
439
|
+
if (cut < 0 || at.endsWith('-zyyy')) return null
|
|
440
|
+
if (webkitScriptSubtags.includes(` ${at.slice(cut + 1)} `)) han = at.endsWith('-hani')
|
|
441
|
+
at = at.slice(0, cut)
|
|
15
442
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
443
|
+
}
|
|
444
|
+
if (han) tag = 'zh-hans'
|
|
445
|
+
let row = webkitGenericFamilies[tag]
|
|
446
|
+
while (row === undefined) {
|
|
447
|
+
tag = tag.slice(0, Math.max(0, tag.lastIndexOf('-')))
|
|
448
|
+
row = webkitGenericFamilies[tag]
|
|
449
|
+
}
|
|
450
|
+
const families = []
|
|
451
|
+
for (let i = 0; i < row.length; i++) {
|
|
452
|
+
// Keywords with the same entry share its family, and the context is asked once.
|
|
453
|
+
const first = row.indexOf(row[i] )
|
|
454
|
+
if (first < i) {
|
|
455
|
+
families.push(families[first] )
|
|
456
|
+
continue
|
|
19
457
|
}
|
|
20
|
-
|
|
458
|
+
const name = webkitGenericFamilyNames[row[i] ]
|
|
459
|
+
const pair = name.indexOf('|')
|
|
460
|
+
const family = pair < 0 ? name : hasFamily(ctx, name.slice(0, pair)) ? name.slice(0, pair) : name.slice(pair + 1)
|
|
461
|
+
families.push(family === '' ? '' : `"${family}"`)
|
|
462
|
+
}
|
|
463
|
+
return families
|
|
21
464
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
};
|
|
37
|
-
cache.set(seg, metrics);
|
|
38
|
-
}
|
|
39
|
-
return metrics;
|
|
40
|
-
}
|
|
41
|
-
export function getEngineProfile() {
|
|
42
|
-
if (cachedEngineProfile !== null) return cachedEngineProfile;
|
|
43
|
-
if (typeof navigator === 'undefined') {
|
|
44
|
-
cachedEngineProfile = {
|
|
45
|
-
geckoAsciiLineBreaks: false,
|
|
46
|
-
lineFitEpsilon: 0.005,
|
|
47
|
-
carryCJKAfterClosingQuote: false,
|
|
48
|
-
breakKeepAllAfterPunctuation: true,
|
|
49
|
-
preferPrefixWidthsForBreakableRuns: false
|
|
50
|
-
};
|
|
51
|
-
return cachedEngineProfile;
|
|
52
|
-
}
|
|
53
|
-
const ua = navigator.userAgent;
|
|
54
|
-
const vendor = navigator.vendor;
|
|
55
|
-
const isSafari = vendor === 'Apple Computer, Inc.' && ua.includes('Safari/') && !ua.includes('Chrome/') && !ua.includes('Chromium/') && !ua.includes('CriOS/') && !ua.includes('FxiOS/') && !ua.includes('EdgiOS/');
|
|
56
|
-
const isChromium = ua.includes('Chrome/') || ua.includes('Chromium/') || ua.includes('CriOS/') || ua.includes('Edg/');
|
|
57
|
-
const isGecko = ua.includes('Firefox/') && !ua.includes('FxiOS/');
|
|
58
|
-
cachedEngineProfile = {
|
|
59
|
-
geckoAsciiLineBreaks: isGecko,
|
|
60
|
-
lineFitEpsilon: isSafari ? 1 / 64 : 0.005,
|
|
61
|
-
carryCJKAfterClosingQuote: isChromium,
|
|
62
|
-
breakKeepAllAfterPunctuation: !isSafari,
|
|
63
|
-
preferPrefixWidthsForBreakableRuns: isSafari
|
|
64
|
-
};
|
|
65
|
-
return cachedEngineProfile;
|
|
66
|
-
}
|
|
67
|
-
export function parseFontSize(font) {
|
|
68
|
-
const m = font.match(/(?:^|\D)(\d+(?:\.\d+)?)\s*px/);
|
|
69
|
-
return m ? parseFloat(m[1]) : 16;
|
|
70
|
-
}
|
|
71
|
-
function isEmojiGrapheme(g) {
|
|
72
|
-
return emojiPresentationRe.test(g) || g.includes('\uFE0F');
|
|
73
|
-
}
|
|
74
|
-
export function textMayContainEmoji(text) {
|
|
75
|
-
return maybeEmojiRe.test(text);
|
|
76
|
-
}
|
|
77
|
-
function getEmojiCorrection(font) {
|
|
78
|
-
let correction = emojiCorrectionCache.get(font);
|
|
79
|
-
if (correction !== undefined) return correction;
|
|
80
|
-
const fontSize = parseFontSize(font);
|
|
81
|
-
const ctx = getMeasureContext();
|
|
82
|
-
ctx.font = font;
|
|
83
|
-
const canvasW = ctx.measureText('\u{1F600}').width;
|
|
84
|
-
correction = 0;
|
|
85
|
-
if (canvasW > fontSize + 0.5 && typeof document !== 'undefined' && document.body !== null) {
|
|
86
|
-
const span = document.createElement('span');
|
|
87
|
-
span.style.font = font;
|
|
88
|
-
span.style.display = 'inline-block';
|
|
89
|
-
span.style.visibility = 'hidden';
|
|
90
|
-
span.style.position = 'absolute';
|
|
91
|
-
span.textContent = '\u{1F600}';
|
|
92
|
-
document.body.appendChild(span);
|
|
93
|
-
const domW = span.getBoundingClientRect().width;
|
|
94
|
-
document.body.removeChild(span);
|
|
95
|
-
if (canvasW - domW > 0.5) {
|
|
96
|
-
correction = canvasW - domW;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
emojiCorrectionCache.set(font, correction);
|
|
100
|
-
return correction;
|
|
101
|
-
}
|
|
102
|
-
function countEmojiGraphemes(text) {
|
|
103
|
-
let count = 0;
|
|
104
|
-
const graphemeSegmenter = getSharedGraphemeSegmenter();
|
|
105
|
-
for (const g of graphemeSegmenter.segment(text)){
|
|
106
|
-
if (isEmojiGrapheme(g.segment)) count++;
|
|
107
|
-
}
|
|
108
|
-
return count;
|
|
465
|
+
|
|
466
|
+
// Text each of macOS's families in the table draws some of: Latin, Hangul, Han,
|
|
467
|
+
// Devanagari, Khmer, Kannada, Lao, Malayalam, Myanmar, Oriya, Sinhala and Tibetan.
|
|
468
|
+
const familyProbeText = 'Hamburg 한中 नम សួ ನಮ ສະ നമ မင ନମ ආය བཀ'
|
|
469
|
+
|
|
470
|
+
// Whether the context has a family: the probe text measures differently with it first.
|
|
471
|
+
function hasFamily(ctx , family ) {
|
|
472
|
+
for (const fallback of ['monospace', 'serif']) {
|
|
473
|
+
ctx.font = `16px ${fallback}`
|
|
474
|
+
const width = ctx.measureText(familyProbeText).width
|
|
475
|
+
ctx.font = `16px "${family}", ${fallback}`
|
|
476
|
+
if (ctx.measureText(familyProbeText).width !== width) return true
|
|
477
|
+
}
|
|
478
|
+
return false
|
|
109
479
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
480
|
+
|
|
481
|
+
// The Canvas font that measures what the page draws: each unquoted generic keyword
|
|
482
|
+
// in the family list, after the size, becomes the page's families for it.
|
|
483
|
+
function getCanvasFont(font , families ) {
|
|
484
|
+
const size = fontSizeRe.exec(font)
|
|
485
|
+
if (size === null) return font
|
|
486
|
+
const start = size.index + size[0].length
|
|
487
|
+
return font.slice(0, start) + font.slice(start).replace(familyListItemRe, (item , quoted , separator , space , keyword ) => {
|
|
488
|
+
const named = quoted === undefined ? families[genericKeywords.indexOf(keyword.toLowerCase())] : ''
|
|
489
|
+
return named === '' ? item : separator + space + named
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// The string a chosen soft hyphen paints in the font where the engine takes it from the
|
|
494
|
+
// primary font (hyphenFromPrimaryFont): U+2010 where that font has a glyph for it, else `-`.
|
|
495
|
+
// Where both measure the same in the font, either does. Else Canvas tells which family draws
|
|
496
|
+
// a character from two lists: the engines draw each character with the first listed font
|
|
497
|
+
// that has its glyph (WebKit's glyphDataForVariant, FontCascadeFonts.cpp:426-439), so a family
|
|
498
|
+
// draws it where `family, monospace` and `family, serif` measure it alike and the two generic
|
|
499
|
+
// families alone don't. The primary font is the first listed family's that gives a font,
|
|
500
|
+
// whether or not it has a glyph for a space (WebKit's primaryFont, FontCascadeFonts.h:225-254;
|
|
501
|
+
// Blink's DeterminePrimarySimpleFontDataCore, font_fallback_list.cc:88-143). Canvas can't tell
|
|
502
|
+
// a family that gives no font from one that lacks the character, so the premise is that the
|
|
503
|
+
// primary font draws a space, and it is taken as the first family's that does. `-` where the
|
|
504
|
+
// generic families measure alike, which tells nothing, or the font string has no size in px.
|
|
505
|
+
// What the premise gets wrong (ENGINE_FOLLOWUPS.md, Line edges): a first family whose font
|
|
506
|
+
// has no space, an icon font, is skipped where the engines take it; a family split into faces
|
|
507
|
+
// by unicode-range is asked whole, where the engines ask only the face that holds the space;
|
|
508
|
+
// and where no listed family gives a font the answer is `-`, where the engines ask their
|
|
509
|
+
// last-resort font.
|
|
510
|
+
export function getHyphenText(measurement ) {
|
|
511
|
+
if (measurement.hyphenText !== null) return measurement.hyphenText
|
|
512
|
+
const font = measurement.canvasFont
|
|
513
|
+
const size = fontSizeRe.exec(font)
|
|
514
|
+
let hyphenText = '-'
|
|
515
|
+
if (size !== null && getSegmentMetrics('\u2010', measurement).width !== getSegmentMetrics('-', measurement).width) {
|
|
516
|
+
const context = getContext(measurement)
|
|
517
|
+
const start = size.index + size[0].length
|
|
518
|
+
const prefix = font.slice(0, start)
|
|
519
|
+
const families = font.slice(start).match(familyRe) ?? []
|
|
520
|
+
const monospace = prefix + 'monospace'
|
|
521
|
+
const serif = prefix + 'serif'
|
|
522
|
+
if (measureIn(context, monospace, ' ') !== measureIn(context, serif, ' ') && measureIn(context, monospace, '\u2010') !== measureIn(context, serif, '\u2010')) {
|
|
523
|
+
for (let i = 0; i < families.length; i++) {
|
|
524
|
+
const beforeMonospace = `${prefix}${families[i]}, monospace`
|
|
525
|
+
const beforeSerif = `${prefix}${families[i]}, serif`
|
|
526
|
+
if (measureIn(context, beforeMonospace, ' ') !== measureIn(context, beforeSerif, ' ')) continue
|
|
527
|
+
if (measureIn(context, beforeMonospace, '\u2010') === measureIn(context, beforeSerif, '\u2010')) hyphenText = '\u2010'
|
|
528
|
+
break
|
|
529
|
+
}
|
|
113
530
|
}
|
|
114
|
-
|
|
531
|
+
context.font = font
|
|
532
|
+
}
|
|
533
|
+
measurement.hyphenText = hyphenText
|
|
534
|
+
return hyphenText
|
|
115
535
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
536
|
+
|
|
537
|
+
function measureIn(context , font , text ) {
|
|
538
|
+
context.font = font
|
|
539
|
+
return context.measureText(text).width
|
|
119
540
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
541
|
+
|
|
542
|
+
// The language setLocale() gave, which preparation reads in place of the page's,
|
|
543
|
+
// or undefined.
|
|
544
|
+
let localeLanguage
|
|
545
|
+
|
|
546
|
+
export function setLocaleLanguage(locale ) {
|
|
547
|
+
localeLanguage = locale
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// The language one preparation breaks and measures under, read once: setLocale()'s, or
|
|
551
|
+
// else the page's, or null without a document. The Chromium profile lays out a page
|
|
552
|
+
// without one under Blink's default locale (laysOutUnderDefaultLocale).
|
|
553
|
+
export function getPreparationLanguage(profile ) {
|
|
554
|
+
let language = localeLanguage ?? null
|
|
555
|
+
if (localeLanguage === undefined && typeof document !== 'undefined') {
|
|
556
|
+
const lang = (document.documentElement )?.lang
|
|
557
|
+
if (typeof lang === 'string') language = lang
|
|
558
|
+
}
|
|
559
|
+
return language === '' && profile.laysOutUnderDefaultLocale ? getBlinkDefaultLocale() : language
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// The most app units a Gecko length holds (nscoord_MAX, nsCoord.h:28).
|
|
563
|
+
const MAX_APP_UNITS = (1 << 30) - 1
|
|
564
|
+
|
|
565
|
+
// A text's letter spacing in CSS px as the engine lays it out, 0 by default. CSS and
|
|
566
|
+
// Canvas ignore a non-finite one, which Pretext refuses rather than guess at. Gecko's
|
|
567
|
+
// whole app units are the float32 times 60, rounded half away from zero and clamped
|
|
568
|
+
// (DefaultLengthToAppUnits, ServoStyleConstsInlines.h:584-595), and a spacing that
|
|
569
|
+
// rounds to none keeps the text's ligatures (nsLayoutUtils.cpp:6896-6904).
|
|
570
|
+
export function readLetterSpacing(letterSpacing , profile ) {
|
|
571
|
+
const value = letterSpacing ?? 0
|
|
572
|
+
if (!Number.isFinite(value)) throw new RangeError(`letterSpacing must be a finite number of CSS px, not ${value}`)
|
|
573
|
+
if (!profile.letterSpacingInAppUnits) return value
|
|
574
|
+
const units = Math.fround(Math.fround(value) * 60)
|
|
575
|
+
return Math.min(Math.round(Math.abs(units)), MAX_APP_UNITS) * Math.sign(units) / 60
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// A zero per segment, where per-segment widths start, pushed in a loop: Array.from over
|
|
579
|
+
// `{ length }` reads every index off the object and calls its map function for each.
|
|
580
|
+
export function zeros(count ) {
|
|
581
|
+
const out = []
|
|
582
|
+
for (let i = 0; i < count; i++) out.push(0)
|
|
583
|
+
return out
|
|
584
|
+
}
|
|
585
|
+
|
|
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
|
|
598
|
+
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
|
|
603
|
+
} finally {
|
|
604
|
+
context.letterSpacing = previous
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// The lookup internalizes seg, and a string V8 hashes for the first time there gets a one-byte
|
|
609
|
+
// copy where its units fit, so Canvas is handed a Latin-1 segment one-byte, which Chrome measures
|
|
610
|
+
// as Latin (RESEARCH.md, Keeping Work Bounded, String Storage). A segment cut from a longer text
|
|
611
|
+
// is a new string, so the lookup is its first hash. A text that is one segment is the caller's
|
|
612
|
+
// own string, since a slice of a whole string is that string, and so is a rich item that is one
|
|
613
|
+
// segment (prepareRichInline() in src/rich-inline.ts): a two-byte one that something hashed
|
|
614
|
+
// before without internalizing it, as a RegExp made from it does, stays two-byte.
|
|
615
|
+
export function getSegmentMetrics(seg , measurement ) {
|
|
616
|
+
return measurement.metrics.get(seg) ?? addMetrics(measurement.metrics, seg, seg, measurement)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Metrics of seg measured together with one following U+0020.
|
|
620
|
+
export function getFollowingSpaceMetrics(seg , measurement ) {
|
|
621
|
+
return measurement.followingSpaceMetrics.get(seg) ?? addMetrics(measurement.followingSpaceMetrics, seg, seg + ' ', measurement)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function addMetrics(cache , seg , text , measurement ) {
|
|
625
|
+
const metrics = { width: getContext(measurement).measureText(text).width, emojiCount: -1, fit: null, spaceKerning: null }
|
|
626
|
+
cache.set(seg, metrics)
|
|
627
|
+
return metrics
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// The kerning of every segment that takes none.
|
|
631
|
+
export const noSpaceKerning = { after: 0, before: 0 }
|
|
632
|
+
|
|
633
|
+
// Whether a character takes no kerning with a space, and Canvas isn't asked.
|
|
634
|
+
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) ||
|
|
642
|
+
// Premise: no font kerns a Hangul syllable with the space (RESEARCH.md, Kerning At Line Edges).
|
|
643
|
+
(code >= 0xac00 && code <= 0xd7a3)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// What is kept of the font's kerning with the space glyph, or null for a font that has none:
|
|
647
|
+
// one in which U+2028 before, between and after the printable ASCII characters is as wide with
|
|
648
|
+
// kerning off. `fontKerning = 'none'` turns the `kern` feature off
|
|
649
|
+
// (FontFeatureRange::FromFontDescription, font_features.cc:39-47), and with it HarfBuzz's
|
|
650
|
+
// kerning from GPOS, `kern` and `kerx` (hb-ot-shape.cc:127-131, hb-ot-kern-table.hh:67). Asked
|
|
651
|
+
// once per font. Premise: a font that kerns no printable ASCII character with the space kerns
|
|
652
|
+
// nothing with it (RESEARCH.md, Kerning At Line Edges, has the fonts that do).
|
|
653
|
+
export function getFontSpaceKerning(measurement ) {
|
|
654
|
+
if (measurement.spaceKerning === undefined) {
|
|
655
|
+
const context = getContext(measurement)
|
|
656
|
+
let probe = '\u2028'
|
|
657
|
+
for (let code = 0x21; code <= 0x7e; code++) probe += String.fromCharCode(code) + '\u2028'
|
|
658
|
+
const kerned = context.measureText(probe).width
|
|
659
|
+
context.fontKerning = 'none'
|
|
660
|
+
const unkerned = context.measureText(probe).width
|
|
661
|
+
context.fontKerning = 'auto'
|
|
662
|
+
// Where U+2028 alone doesn't measure as the space, it doesn't stand for it.
|
|
663
|
+
measurement.spaceKerning = kerned !== unkerned && context.measureText('\u2028').width === getSegmentMetrics(' ', measurement).width ? new Map() : null
|
|
664
|
+
}
|
|
665
|
+
return measurement.spaceKerning
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// A character's kerning with a space glyph after it, or before it: the two in one string, with
|
|
669
|
+
// U+2028 for the space, less each alone. Asked of Canvas once per font and side.
|
|
670
|
+
function getCharacterSpaceKerning(code , spaceFirst , measurement , kernings ) {
|
|
671
|
+
const key = code << 1 | (spaceFirst ? 1 : 0)
|
|
672
|
+
let kerning = kernings.get(key)
|
|
673
|
+
if (kerning === undefined) {
|
|
674
|
+
kerning = 0
|
|
675
|
+
if (!takesNoSpaceKerning(code)) {
|
|
676
|
+
const character = String.fromCharCode(code)
|
|
677
|
+
const pairWidth = getContext(measurement).measureText(spaceFirst ? '\u2028' + character : character + '\u2028').width
|
|
678
|
+
kerning = pairWidth - getSegmentMetrics(character, measurement).width - getSegmentMetrics(' ', measurement).width
|
|
679
|
+
// Blink keeps a run's width as a float32 (shape_result.cc:1539-1576), so up to the pair's
|
|
680
|
+
// width / 2^22 is rounding, not kerning (RESEARCH.md, Kerning At Line Edges).
|
|
681
|
+
if (Math.abs(kerning) <= pairWidth / 0x400000) kerning = 0
|
|
129
682
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
683
|
+
kernings.set(key, kerning)
|
|
684
|
+
}
|
|
685
|
+
return kerning
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// 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
|
|
690
|
+
// draws U+2028 with the space glyph (HarfBuzzGetGlyph, harfbuzz_face.cc:103-113) and its Canvas
|
|
691
|
+
// doesn't cut there. Premise: the segment's last and first character stand for the word, past
|
|
692
|
+
// default ignorables, which HarfBuzz's lookups pass over, and a first character with a combining
|
|
693
|
+
// mark after it takes none (RESEARCH.md, Kerning At Line Edges, has the gaps).
|
|
694
|
+
export function getSpaceKerning(seg , metrics , measurement , kernings ) {
|
|
695
|
+
let first = 0
|
|
696
|
+
let last = seg.length - 1
|
|
697
|
+
while (first < last && hasProperty(seg.charCodeAt(first), DEFAULT_IGNORABLE)) first++
|
|
698
|
+
while (last > first && hasProperty(seg.charCodeAt(last), DEFAULT_IGNORABLE)) last--
|
|
699
|
+
const before = first < last && hasProperty(seg.charCodeAt(first + 1), MARK) ? 0 : getCharacterSpaceKerning(seg.charCodeAt(first), true, measurement, kernings)
|
|
700
|
+
const after = getCharacterSpaceKerning(seg.charCodeAt(last), false, measurement, kernings)
|
|
701
|
+
return metrics.spaceKerning = after === 0 && before === 0 ? noSpaceKerning : { after, before }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// The font's space, which holds no emoji. Every text asks for it, so it is kept beside the font's
|
|
705
|
+
// Map, which still answers the first one.
|
|
706
|
+
export function getSpaceWidth(measurement ) {
|
|
707
|
+
return measurement.spaceWidth ??= getSegmentMetrics(' ', measurement).width
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// A text's width in the font, less the emoji correction.
|
|
711
|
+
export function getTextWidth(text , measurement , emojiCorrection ) {
|
|
712
|
+
return getCorrectedSegmentWidth(text, getSegmentMetrics(text, measurement), measurement, emojiCorrection)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
// Engine profiles describe the layout engine, not the browser brand. Chrome,
|
|
718
|
+
// Firefox and Edge on iOS lay out with WebKit whatever their brand token (CriOS/,
|
|
719
|
+
// FxiOS/, EdgiOS/) or desktop-mode user agent, and an app's web view may name no
|
|
720
|
+
// browser at all. The user agent decides alone, so a page and its workers agree.
|
|
721
|
+
// navigator.vendor is not read: workers don't have it, and jsdom reports WebKit's
|
|
722
|
+
// beside Chromium's frozen AppleWebKit/537.36 token. WebKit froze 605.1.15, so
|
|
723
|
+
// 537.36 names Blink only beside Chrome/ or Chromium/, which Samsung's TV web
|
|
724
|
+
// views omit, and any other AppleWebKit/ version names WebKit.
|
|
725
|
+
export function getLayoutEngine(userAgent ) {
|
|
726
|
+
if (userAgent.includes('Firefox/')) return 'gecko'
|
|
727
|
+
if (userAgent.includes('AppleWebKit/537.36')) {
|
|
728
|
+
return userAgent.includes('Chrome/') || userAgent.includes('Chromium/') ? 'blink' : null
|
|
729
|
+
}
|
|
730
|
+
return userAgent.includes('AppleWebKit/') ? 'webkit' : null
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Apart from buildEngineProfile(), so that what the line walkers call for every line stays a read
|
|
734
|
+
// of the cached profile, however much the profile holds. V8 inlines a function only while its
|
|
735
|
+
// bytecode takes at most 460 bytes (max_inlined_bytecode_size), minified or not. With the builder
|
|
736
|
+
// inside, this took 454 bytes with 23 fields and 463 with a 24th, which Chrome 154's V8 no longer
|
|
737
|
+
// inlined into the line counter and the simple stepper, and its plain line APIs ran
|
|
738
|
+
// 11-18% slower. Apart, it takes 21 (Node 23, V8 12.9). Check with node --print-bytecode and
|
|
739
|
+
// --trace-turbo-inlining (RESEARCH.md, JavaScript Engines).
|
|
740
|
+
export function getEngineProfile() {
|
|
741
|
+
return cachedEngineProfile ??= buildEngineProfile()
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function buildEngineProfile() {
|
|
745
|
+
const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent
|
|
746
|
+
// Engines Pretext doesn't recognize take Blink's profile (RESEARCH.md, Decisions Log,
|
|
747
|
+
// 2026-09-26).
|
|
748
|
+
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
|
+
|
|
752
|
+
return {
|
|
753
|
+
entryFitBasis: isDesktop && engine === 'blink' ? 'fresh' : isDesktop && engine === 'gecko' ? 'original' : 'disabled',
|
|
754
|
+
lineBreakScan: engine,
|
|
755
|
+
graphemeTable: engine === 'webkit' ? 'apple/char' : engine === 'gecko' ? 'gecko/char' : 'chromium/char',
|
|
756
|
+
lineFitEpsilon: engine === 'webkit' ? 1 / 64 : 0.005,
|
|
757
|
+
cutWordFit: engine === 'blink' ? 'reshaped-lines' : 'segment-prefixes',
|
|
758
|
+
prefixFitMinWidth: engine === 'webkit' ? 0 : 80,
|
|
759
|
+
cutWordKeepsLigatures: engine === 'gecko',
|
|
760
|
+
measureTextWithFollowingSpace: engine === 'webkit',
|
|
761
|
+
kernsSpacesInScriptRun: engine === 'blink',
|
|
762
|
+
letterSpaceDiscretionaryHyphen: engine !== 'blink',
|
|
763
|
+
letterSpacingInAppUnits: engine === 'gecko',
|
|
764
|
+
canvasLetterSpacingDropsLigatures: engine !== 'webkit',
|
|
765
|
+
unspacedCursive: engine === 'blink' ? 'run' : engine === 'gecko' ? 'cluster' : 'none',
|
|
766
|
+
shapesMarksAcrossSoftHyphen: engine === 'blink',
|
|
767
|
+
unfitHyphenRetreat: engine === 'blink' ? 'reduced-width' : engine === 'gecko' ? 'full-width' : 'full-width-or-first',
|
|
768
|
+
hyphenFromPrimaryFont: engine !== 'gecko',
|
|
769
|
+
letterSpaceTabStops: engine !== 'webkit',
|
|
770
|
+
letterSpaceTabs: engine === 'webkit',
|
|
771
|
+
tabMinimumCharacter: engine === 'gecko' ? '0' : ' ',
|
|
772
|
+
tabsInAppUnits: engine === 'gecko',
|
|
773
|
+
hangTabs: engine !== 'gecko',
|
|
774
|
+
zeroWidthGlueTakesLine: engine !== 'gecko',
|
|
775
|
+
keepsLineStartPunctuation: engine === 'webkit',
|
|
776
|
+
hidesControlCharacters: engine === 'gecko',
|
|
777
|
+
hanKerning: engine === 'blink',
|
|
778
|
+
hangsIdeographicSpace: engine !== 'webkit',
|
|
779
|
+
laysOutUnderDefaultLocale: engine === 'blink',
|
|
780
|
+
namesGenericFamiliesByLanguage: engine === 'webkit',
|
|
781
|
+
hardBreakItemRetreat: engine === 'blink' ? 'item' : engine === 'webkit' ? 'fit' : 'last-grapheme',
|
|
782
|
+
paddedOpeningFit: engine === 'blink' ? 'start' : engine === 'webkit' ? 'placed' : 'both',
|
|
783
|
+
emptyAtomicAlwaysFits: engine === 'gecko',
|
|
784
|
+
hangsSpacesPerTextFrame: engine === 'gecko',
|
|
785
|
+
testsHyphenBeforeAtomic: engine !== 'webkit',
|
|
786
|
+
transformsSegmentBreaksAcrossItems: engine === 'blink',
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function parseFontSize(font ) {
|
|
791
|
+
// A failed size can restart at the next digit run, not at every digit in it.
|
|
792
|
+
const m = font.match(/(?:^|\D)(\d+(?:\.\d+)?)\s*px/)
|
|
793
|
+
return m ? parseFloat(m[1] ) : 16
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function textMayContainEmoji(text ) {
|
|
797
|
+
return maybeEmojiRe.test(text)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function getEmojiCorrection(font , measurement ) {
|
|
801
|
+
let correction = measurement.emojiCorrection
|
|
802
|
+
if (correction !== null) return correction
|
|
803
|
+
|
|
804
|
+
const fontSize = parseFontSize(font)
|
|
805
|
+
const canvasW = measurement.emojiWidth = getContext(measurement).measureText('\u{1F600}').width
|
|
806
|
+
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)
|
|
822
|
+
if (canvasW - domW > 0.5) {
|
|
823
|
+
correction = canvasW - domW
|
|
133
824
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
825
|
+
}
|
|
826
|
+
measurement.emojiCorrection = correction
|
|
827
|
+
return correction
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Canvas reports a width as a 32-bit float (CanvasRenderingContext2D.cpp:5277 in Firefox,
|
|
831
|
+
// text_metrics.cc:179 in Chrome), so a few equal advances measure that many times one
|
|
832
|
+
// only within that float's rounding, 2^-24 of the width each time one is rounded: 2e-6 px
|
|
833
|
+
// over three emoji of a bold font in Firefox 156. Sixteen roundings are allowed for, a
|
|
834
|
+
// window far finer than the steps a font's advances come in (RESEARCH.md, Content
|
|
835
|
+
// Language And Fonts).
|
|
836
|
+
const CANVAS_WIDTH_ROUNDING = 2 ** -20
|
|
837
|
+
|
|
838
|
+
// What a word's width may differ by from its graphemes' widths added up and still be their sum:
|
|
839
|
+
// 2^-17 of the width, which covers the float32 roundings of a word of 96 graphemes, each 2^-24
|
|
840
|
+
// of the width at most, and is a tenth of the least kerning of a 2,048-unit font in a 100px word
|
|
841
|
+
// at 16px (RESEARCH.md, Break Opportunities From Engine Data).
|
|
842
|
+
const WORD_SUM_ROUNDING = 2 ** -17
|
|
843
|
+
|
|
844
|
+
// How many glyphs of the emoji font draw a text: the emoji font gives every glyph one
|
|
845
|
+
// advance, the probe's, so text it draws measures a whole number of them, and none
|
|
846
|
+
// where it measures anything else.
|
|
847
|
+
function getEmojiGlyphs(text , measurement ) {
|
|
848
|
+
const width = getSegmentMetrics(text, measurement).width
|
|
849
|
+
const glyphs = Math.round(width / measurement.emojiWidth)
|
|
850
|
+
return Math.abs(width - glyphs * measurement.emojiWidth) <= width * CANVAS_WIDTH_ROUNDING ? glyphs : 0
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// The characters the emoji font shapes together inside a grapheme: emoji and pictographs,
|
|
854
|
+
// the characters that join or modify them (ZWJ, skin tones, tags, U+20E3) and the
|
|
855
|
+
// variation selectors. Every emoji character has one of the two properties. A grapheme
|
|
856
|
+
// of emojiGraphemeRe holds one.
|
|
857
|
+
const emojiStretchRe = /[\p{Extended_Pictographic}\p{Emoji_Component}\uFE0E]+/gu
|
|
858
|
+
|
|
859
|
+
// The glyphs of the emoji font in a text: what the correction is subtracted for, once
|
|
860
|
+
// each. Font fallback decides which font draws an emoji character, and Canvas shows what
|
|
861
|
+
// it decided, at one cached Canvas call per distinct stretch of a font. For a character
|
|
862
|
+
// with no selector, the named font's own glyph comes before the emoji font's
|
|
863
|
+
// (CheckCandidate, gfxTextRun.cpp:3350-3359; FontFallbackIterator::Next,
|
|
864
|
+
// font_fallback_iterator.cc:166-178), and U+FE0E asks for a text font
|
|
865
|
+
// (gfxTextRun.cpp:3270-3273; SymbolsIterator::Consume, symbols_iterator.cc:67-71). So a
|
|
866
|
+
// stretch with a glyph of another font measures as the page draws it: Menlo's own
|
|
867
|
+
// U+26A1, Inter's U+2B1C, Hiragino Sans's U+26AA, or U+231A before U+FE0E. A pictograph
|
|
868
|
+
// whose presentation is text by default takes the correction with no U+FE0F where only
|
|
869
|
+
// the emoji font has it, as U+1F336 in Arial. Two or more glyphs are a sequence the
|
|
870
|
+
// emoji font has no glyph for, drawn as its parts.
|
|
871
|
+
//
|
|
872
|
+
// Each stretch of emoji characters is asked whole, and apart from the rest of its
|
|
873
|
+
// grapheme, since each font shapes its own characters together: Firefox matches a font
|
|
874
|
+
// character by character (gfxFontGroup::FindFontForChar, gfxTextRun.cpp:3178-3194), and
|
|
875
|
+
// Chrome ends a run where emoji give way to text before it shapes
|
|
876
|
+
// (RunSegmenter::Consume, run_segmenter.cc:44-73, over SymbolsIterator::Consume,
|
|
877
|
+
// symbols_iterator.cc:34-79). So an emoji, a sequence or a flag before a combining mark
|
|
878
|
+
// of another script is still one glyph. A stretch that two fonts draw takes no
|
|
879
|
+
// correction, as a text font's pictograph joined by a ZWJ to an emoji in Firefox, and a
|
|
880
|
+
// stretch asked apart can be another font's than inside its grapheme, as a skin tone
|
|
881
|
+
// after a letter in Chrome.
|
|
882
|
+
//
|
|
883
|
+
// Those and the other gaps are in ENGINE_FOLLOWUPS.md, Emoji correction: another font's
|
|
884
|
+
// glyph exactly as wide as an emoji takes the correction, as does Firefox's box for a
|
|
885
|
+
// missing glyph at 13px, and an emoji font whose advances vary would take none.
|
|
886
|
+
function countEmojiGlyphs(text , measurement ) {
|
|
887
|
+
const ends = new Int32Array(text.length)
|
|
888
|
+
const graphemeCount = findGraphemeEnds(getEngineProfile().graphemeTable, text, 0, text.length, ends)
|
|
889
|
+
let count = 0
|
|
890
|
+
for (let i = 0, start = 0; i < graphemeCount; start = ends[i++] ) {
|
|
891
|
+
const grapheme = text.slice(start, ends[i])
|
|
892
|
+
if (!emojiGraphemeRe.test(grapheme)) continue
|
|
893
|
+
const stretches = grapheme.match(emojiStretchRe)
|
|
894
|
+
for (let s = 0; s < stretches.length; s++) count += getEmojiGlyphs(stretches[s] , measurement)
|
|
895
|
+
}
|
|
896
|
+
return count
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function getCorrectedSegmentWidth(seg , metrics , measurement , emojiCorrection ) {
|
|
900
|
+
if (emojiCorrection === 0) return metrics.width
|
|
901
|
+
if (metrics.emojiCount < 0) metrics.emojiCount = countEmojiGlyphs(seg, measurement)
|
|
902
|
+
return metrics.width - metrics.emojiCount * emojiCorrection
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function getSegmentFit(
|
|
906
|
+
seg ,
|
|
907
|
+
metrics ,
|
|
908
|
+
measurement ,
|
|
909
|
+
emojiCorrection ,
|
|
910
|
+
mode ,
|
|
911
|
+
// When metrics measured seg together with one following U+0020, the width of
|
|
912
|
+
// that space alone. The last grapheme then keeps its kerning with the space.
|
|
913
|
+
followingSpaceWidth = null,
|
|
914
|
+
// Whether to find the segment's WebKit line-start prohibitions.
|
|
915
|
+
withLineStartProhibitions = false,
|
|
916
|
+
) {
|
|
917
|
+
if (metrics.fit !== null && metrics.fit.mode === mode) return metrics.fit
|
|
918
|
+
const profile = getEngineProfile()
|
|
919
|
+
const ends = new Int32Array(seg.length)
|
|
920
|
+
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
|
+
}
|
|
926
|
+
let advances
|
|
927
|
+
let lineStartExtras = null
|
|
928
|
+
if (mode === 'reshaped-lines') {
|
|
929
|
+
// Blink shapes a line of a cut word again where the cut falls between letters shaped
|
|
930
|
+
// together (EngineProfile's cutWordFit). Premise: a line holds the letters whose width,
|
|
931
|
+
// shaped alone, fits, so the letters after the line's first take the advances they have
|
|
932
|
+
// in the word, and the first its width alone. Premise, taken for speed: a word as wide as
|
|
933
|
+
// its graphemes measured alone has nothing shaped across them, and they are its advances,
|
|
934
|
+
// with nothing more measured. Else each grapheme is measured after the one before it,
|
|
935
|
+
// which every word of the font shares, and where those don't add up to the word either,
|
|
936
|
+
// as in a ligature of three letters or the joined forms of Arabic, the word's prefixes
|
|
937
|
+
// are, up to MAX_PREFIX_FIT_GRAPHEMES (RESEARCH.md, Break Opportunities From Engine Data).
|
|
938
|
+
const width = getCorrectedSegmentWidth(seg, metrics, measurement, emojiCorrection) - (followingSpaceWidth ?? 0)
|
|
939
|
+
const alone = measureFitAdvances(seg, ends, count, 'sum-graphemes', metrics, measurement, emojiCorrection, followingSpaceWidth)
|
|
940
|
+
advances = alone
|
|
941
|
+
if (!addUpTo(alone, width)) {
|
|
942
|
+
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
|
+
}
|
|
946
|
+
lineStartExtras = [0]
|
|
947
|
+
for (let i = 1; i < count; i++) lineStartExtras.push(alone[i] - advances[i] )
|
|
142
948
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
949
|
+
} else {
|
|
950
|
+
// Past MAX_PREFIX_FIT_GRAPHEMES, prefixes give way to pairs.
|
|
951
|
+
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)
|
|
953
|
+
if (prefixes && profile.cutWordKeepsLigatures) countLigaturesOnFirstLetter(seg, ends, advances, measurement, emojiCorrection)
|
|
954
|
+
}
|
|
955
|
+
return metrics.fit = { mode, advances, lineStartExtras, lineStartProhibitions: prohibitions, entryGeometry: null }
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Moves the advance of each later letter of a ligature onto the ligature's first letter, in a
|
|
959
|
+
// segment's prefix advances, as Gecko counts it where it cuts a word (EngineProfile's
|
|
960
|
+
// cutWordKeepsLigatures). A prefix that ends inside a ligature has the first letter's glyph
|
|
961
|
+
// alone, so the next letter's advance after that prefix isn't its advance alone, in app units,
|
|
962
|
+
// 1/60 px, which Gecko's Canvas reports whole, and there Canvas is asked whether the two are a
|
|
963
|
+
// ligature (isLigature). Where a line is narrower than the ligature it starts with, Gecko cuts
|
|
964
|
+
// inside it and gives each of its letters an equal share (ComputeLigatureData,
|
|
965
|
+
// gfxTextRun.cpp:238-322); here the first letter keeps the whole advance (ENGINE_FOLLOWUPS.md,
|
|
966
|
+
// Emergency breaks inside a word).
|
|
967
|
+
function countLigaturesOnFirstLetter(seg , ends , advances , measurement , emojiCorrection ) {
|
|
968
|
+
// Text measured under letter spacing has no optional ligature, and a context that can't
|
|
969
|
+
// turn them off can't be asked.
|
|
970
|
+
if (!measurement.state.shapesLetterSpaced || measurement.letterSpacing !== '0px') return
|
|
971
|
+
// The grapheme the ligature that holds the grapheme before this one starts with, or that grapheme.
|
|
972
|
+
let first = 0
|
|
973
|
+
for (let i = 1; i < advances.length; i++) {
|
|
974
|
+
const start = ends[i - 1]
|
|
975
|
+
const alone = getTextWidth(seg.slice(start, ends[i]), measurement, emojiCorrection)
|
|
976
|
+
if (Math.round((advances[i] - alone) * 60) !== 0 && isLigature(seg.slice(i === 1 ? 0 : ends[i - 2] , ends[i]), measurement)) {
|
|
977
|
+
advances[first] = advances[first] + advances[i]
|
|
978
|
+
advances[i] = 0
|
|
979
|
+
} else {
|
|
980
|
+
first = i
|
|
162
981
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Whether two neighbouring graphemes are one of the font's optional ligatures: the pair measures
|
|
986
|
+
// otherwise without them, as the context shapes text under a letter spacing
|
|
987
|
+
// (LETTER_SPACED_SHAPING). Asked of Canvas once per pair and font. A kerned pair, letters that
|
|
988
|
+
// join, a mark on its base and a ligature the font requires measure the same, and aren't one.
|
|
989
|
+
function isLigature(pair , measurement ) {
|
|
990
|
+
let ligature = measurement.ligaturePairs.get(pair)
|
|
991
|
+
if (ligature === undefined) {
|
|
992
|
+
const context = getContext(measurement)
|
|
993
|
+
const width = getSegmentMetrics(pair, measurement).width
|
|
994
|
+
context.letterSpacing = LETTER_SPACED_SHAPING
|
|
995
|
+
ligature = context.measureText(pair).width !== width
|
|
996
|
+
context.letterSpacing = '0px'
|
|
997
|
+
measurement.ligaturePairs.set(pair, ligature)
|
|
998
|
+
}
|
|
999
|
+
return ligature
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Whether a segment's advances add up to its width, within the rounding of Canvas's widths.
|
|
1003
|
+
function addUpTo(advances , width ) {
|
|
1004
|
+
let sum = 0
|
|
1005
|
+
for (let i = 0; i < advances.length; i++) sum += advances[i]
|
|
1006
|
+
return Math.abs(sum - width) <= width * WORD_SUM_ROUNDING
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// A segment's advances per grapheme: the differences of its prefixes' widths, or each
|
|
1010
|
+
// grapheme's width alone or, as a pair, after the one before it.
|
|
1011
|
+
function measureFitAdvances(
|
|
1012
|
+
seg ,
|
|
1013
|
+
ends ,
|
|
1014
|
+
count ,
|
|
1015
|
+
from ,
|
|
1016
|
+
metrics ,
|
|
1017
|
+
measurement ,
|
|
1018
|
+
emojiCorrection ,
|
|
1019
|
+
followingSpaceWidth ,
|
|
1020
|
+
) {
|
|
1021
|
+
const prefixes = from === 'segment-prefixes'
|
|
1022
|
+
const pairs = from === 'pair-context'
|
|
1023
|
+
const advances = []
|
|
1024
|
+
let previousStart = 0
|
|
1025
|
+
let previousWidth = 0
|
|
1026
|
+
for (let i = 0, start = 0; i < count; start = ends[i++] ) {
|
|
1027
|
+
const end = ends[i]
|
|
1028
|
+
if (prefixes) {
|
|
1029
|
+
// The whole segment is the last prefix; with a following space it was
|
|
1030
|
+
// measured together with that space.
|
|
1031
|
+
const width = followingSpaceWidth !== null && i === count - 1
|
|
1032
|
+
? getCorrectedSegmentWidth(seg, metrics, measurement, emojiCorrection) - followingSpaceWidth
|
|
1033
|
+
: getTextWidth(seg.slice(0, end), measurement, emojiCorrection)
|
|
1034
|
+
advances.push(width - previousWidth)
|
|
1035
|
+
previousWidth = width
|
|
1036
|
+
continue
|
|
172
1037
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
1038
|
+
const width = getTextWidth(seg.slice(start, end), measurement, emojiCorrection)
|
|
1039
|
+
advances.push(pairs && i > 0 ? getTextWidth(seg.slice(previousStart, end), measurement, emojiCorrection) - previousWidth : width)
|
|
1040
|
+
previousStart = start
|
|
1041
|
+
previousWidth = width
|
|
1042
|
+
}
|
|
1043
|
+
// Advances that do not end in the whole segment's width take the kerning as a
|
|
1044
|
+
// difference, which needs the segment measured alone too.
|
|
1045
|
+
if (followingSpaceWidth !== null && !prefixes) {
|
|
1046
|
+
advances[count - 1] = advances[count - 1] + metrics.width - getSegmentMetrics(seg, measurement).width - followingSpaceWidth
|
|
1047
|
+
}
|
|
1048
|
+
return advances
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// What preparation measures a font's text through. Text under letter spacing has a
|
|
1052
|
+
// measurement of its own where the context shapes it as the page does: its widths,
|
|
1053
|
+
// prefixes and line-edge facts all come from that shaping. The context isn't touched
|
|
1054
|
+
// here: text that is all in the font's caches measures nothing (getContext).
|
|
1055
|
+
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.
|
|
1059
|
+
if (measureState === null || measureState.language !== language) measureState = createMeasureState(language)
|
|
1060
|
+
const state = measureState
|
|
1061
|
+
const shaped = letterSpaced && state.shapesLetterSpaced
|
|
1062
|
+
const fonts = shaped ? state.letterSpacedFonts : state.fonts
|
|
1063
|
+
let measurement = fonts.get(font)
|
|
1064
|
+
if (measurement === undefined) {
|
|
1065
|
+
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() }
|
|
1067
|
+
fonts.set(font, measurement)
|
|
1068
|
+
}
|
|
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
|
+
return measurement
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// The font's context, set to the font and its shaping. Every measurement in a font takes
|
|
1076
|
+
// its context from here as it measures, so none reads a width under another font's
|
|
1077
|
+
// setting, whatever was looked up or prepared in between, and text that is all cached,
|
|
1078
|
+
// which measures nothing, sets nothing. What changes the context after that puts it back
|
|
1079
|
+
// before it returns (getHyphenText, measureWithLetterSpacing, getFontSpaceKerning,
|
|
1080
|
+
// isLigature).
|
|
1081
|
+
export function getContext(measurement ) {
|
|
1082
|
+
const state = measurement.state
|
|
1083
|
+
if (state.font !== measurement.canvasFont) state.context.font = state.font = measurement.canvasFont
|
|
1084
|
+
if (state.letterSpacing !== measurement.letterSpacing) state.context.letterSpacing = state.letterSpacing = measurement.letterSpacing
|
|
1085
|
+
return state.context
|
|
1086
|
+
}
|
|
1087
|
+
|
|
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.
|
|
1099
|
+
if (language !== null && 'lang' in context) context.lang = language
|
|
1100
|
+
const profile = getEngineProfile()
|
|
1101
|
+
const takesLetterSpacing = typeof context.letterSpacing === 'string'
|
|
1102
|
+
return {
|
|
1103
|
+
language,
|
|
1104
|
+
context,
|
|
1105
|
+
font: '',
|
|
1106
|
+
letterSpacing: '0px',
|
|
1107
|
+
genericFamilies: language !== null && profile.namesGenericFamiliesByLanguage ? getWebKitGenericFamilies(language, context) : null,
|
|
1108
|
+
takesLetterSpacing,
|
|
1109
|
+
shapesLetterSpaced: takesLetterSpacing && profile.canvasLetterSpacingDropsLigatures,
|
|
1110
|
+
fonts: new Map(),
|
|
1111
|
+
letterSpacedFonts: new Map(),
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export function clearMeasurementCaches() {
|
|
1116
|
+
measureState?.fonts.clear()
|
|
1117
|
+
measureState?.letterSpacedFonts.clear()
|
|
189
1118
|
}
|