@turntrout/subfont 1.5.1 → 1.7.0
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/CHANGELOG.md +7 -0
- package/README.md +52 -21
- package/lib/FontTracerPool.js +49 -1
- package/lib/HeadlessBrowser.js +11 -3
- package/lib/collectTextsByPage.js +496 -651
- package/lib/concurrencyLimit.js +3 -1
- package/lib/escapeJsStringLiteral.js +13 -0
- package/lib/extractVisibleText.js +6 -2
- package/lib/fontConverter.js +25 -0
- package/lib/fontConverterWorker.js +16 -0
- package/lib/fontFaceHelpers.js +16 -4
- package/lib/fontFeatureHelpers.js +249 -0
- package/lib/fontTracerWorker.js +0 -10
- package/lib/gatherStylesheetsWithPredicates.js +4 -5
- package/lib/normalizeFontPropertyValue.js +1 -1
- package/lib/progress.js +101 -0
- package/lib/sfntCache.js +10 -7
- package/lib/subfont.js +80 -52
- package/lib/subsetFontWithGlyphs.js +41 -22
- package/lib/subsetFonts.js +223 -211
- package/lib/subsetGeneration.js +13 -1
- package/lib/unquote.js +9 -4
- package/lib/variationAxes.js +3 -32
- package/lib/warnAboutMissingGlyphs.js +36 -25
- package/lib/wasmQueue.js +6 -2
- package/package.json +2 -2
|
@@ -18,22 +18,15 @@ const {
|
|
|
18
18
|
uniqueChars,
|
|
19
19
|
uniqueCharsFromArray,
|
|
20
20
|
} = require('./fontFaceHelpers');
|
|
21
|
+
const {
|
|
22
|
+
createPageProgress,
|
|
23
|
+
logTracedPage,
|
|
24
|
+
makePhaseTracker,
|
|
25
|
+
} = require('./progress');
|
|
21
26
|
|
|
22
27
|
const fontRelevantCssRegex =
|
|
23
28
|
/font-family|font-weight|font-style|font-stretch|font-display|@font-face|font-variation|font-feature/i;
|
|
24
29
|
|
|
25
|
-
// font-tracer defaults omit font-size. We need it for opsz axis mapping.
|
|
26
|
-
const PROPS_TO_RETURN = [
|
|
27
|
-
'font-family',
|
|
28
|
-
'font-style',
|
|
29
|
-
'font-weight',
|
|
30
|
-
'font-variant',
|
|
31
|
-
'font-stretch',
|
|
32
|
-
'font-variation-settings',
|
|
33
|
-
'animation-timing-function',
|
|
34
|
-
'font-size',
|
|
35
|
-
];
|
|
36
|
-
|
|
37
30
|
// The \s before style ensures we don't match data-style or similar.
|
|
38
31
|
const inlineFontStyleRegex =
|
|
39
32
|
/(?:^|\s)style\s*=\s*["'][^"']*\b(?:font-family|font-weight|font-style|font-stretch|font\s*:)/i;
|
|
@@ -47,241 +40,10 @@ const fontFaceTraversalTypes = new Set(['HtmlStyle', 'SvgStyle', 'CssImport']);
|
|
|
47
40
|
// the overhead of worker thread startup exceeds the parallelism benefit).
|
|
48
41
|
const MIN_PAGES_FOR_WORKER_POOL = 4;
|
|
49
42
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'font-variant-east-asian',
|
|
55
|
-
'font-variant-ligatures',
|
|
56
|
-
'font-variant-numeric',
|
|
57
|
-
'font-variant-position',
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
// Map font-variant-* CSS values to their corresponding OpenType feature tags.
|
|
61
|
-
const fontVariantToOTTags = {
|
|
62
|
-
'font-variant-ligatures': {
|
|
63
|
-
'common-ligatures': ['liga', 'clig'],
|
|
64
|
-
'no-common-ligatures': ['liga', 'clig'],
|
|
65
|
-
'discretionary-ligatures': ['dlig'],
|
|
66
|
-
'no-discretionary-ligatures': ['dlig'],
|
|
67
|
-
'historical-ligatures': ['hlig'],
|
|
68
|
-
'no-historical-ligatures': ['hlig'],
|
|
69
|
-
contextual: ['calt'],
|
|
70
|
-
'no-contextual': ['calt'],
|
|
71
|
-
},
|
|
72
|
-
'font-variant-caps': {
|
|
73
|
-
'small-caps': ['smcp'],
|
|
74
|
-
'all-small-caps': ['smcp', 'c2sc'],
|
|
75
|
-
'petite-caps': ['pcap'],
|
|
76
|
-
'all-petite-caps': ['pcap', 'c2pc'],
|
|
77
|
-
unicase: ['unic'],
|
|
78
|
-
'titling-caps': ['titl'],
|
|
79
|
-
},
|
|
80
|
-
'font-variant-numeric': {
|
|
81
|
-
'lining-nums': ['lnum'],
|
|
82
|
-
'oldstyle-nums': ['onum'],
|
|
83
|
-
'proportional-nums': ['pnum'],
|
|
84
|
-
'tabular-nums': ['tnum'],
|
|
85
|
-
'diagonal-fractions': ['frac'],
|
|
86
|
-
'stacked-fractions': ['afrc'],
|
|
87
|
-
ordinal: ['ordn'],
|
|
88
|
-
'slashed-zero': ['zero'],
|
|
89
|
-
},
|
|
90
|
-
'font-variant-position': {
|
|
91
|
-
sub: ['subs'],
|
|
92
|
-
super: ['sups'],
|
|
93
|
-
},
|
|
94
|
-
'font-variant-east-asian': {
|
|
95
|
-
jis78: ['jp78'],
|
|
96
|
-
jis83: ['jp83'],
|
|
97
|
-
jis90: ['jp90'],
|
|
98
|
-
jis04: ['jp04'],
|
|
99
|
-
simplified: ['smpl'],
|
|
100
|
-
traditional: ['trad'],
|
|
101
|
-
'proportional-width': ['pwid'],
|
|
102
|
-
'full-width': ['fwid'],
|
|
103
|
-
ruby: ['ruby'],
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// Extract OpenType feature tags referenced by a CSS declaration.
|
|
108
|
-
function extractFeatureTagsFromDecl(prop, value) {
|
|
109
|
-
const tags = new Set();
|
|
110
|
-
const propLower = prop.toLowerCase();
|
|
111
|
-
|
|
112
|
-
if (propLower === 'font-feature-settings') {
|
|
113
|
-
// Parse quoted 4-letter tags: "liga" 1, 'dlig', etc.
|
|
114
|
-
const re = /["']([a-zA-Z0-9]{4})["']/g;
|
|
115
|
-
let m;
|
|
116
|
-
while ((m = re.exec(value)) !== null) {
|
|
117
|
-
tags.add(m[1]);
|
|
118
|
-
}
|
|
119
|
-
return tags;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (propLower === 'font-variant-alternates') {
|
|
123
|
-
const v = value.toLowerCase();
|
|
124
|
-
if (v.includes('historical-forms')) tags.add('hist');
|
|
125
|
-
if (/stylistic\s*\(/.test(v)) tags.add('salt');
|
|
126
|
-
if (/swash\s*\(/.test(v)) tags.add('swsh');
|
|
127
|
-
if (/ornaments\s*\(/.test(v)) tags.add('ornm');
|
|
128
|
-
if (/annotation\s*\(/.test(v)) tags.add('nalt');
|
|
129
|
-
if (/styleset\s*\(/.test(v)) {
|
|
130
|
-
for (let i = 1; i <= 20; i++) {
|
|
131
|
-
tags.add(`ss${String(i).padStart(2, '0')}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (/character-variant\s*\(/.test(v)) {
|
|
135
|
-
for (let i = 1; i <= 99; i++) {
|
|
136
|
-
tags.add(`cv${String(i).padStart(2, '0')}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return tags;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const mapping = fontVariantToOTTags[propLower];
|
|
143
|
-
if (mapping) {
|
|
144
|
-
// Split into tokens for exact keyword matching — substring matching
|
|
145
|
-
// would falsely trigger e.g. "sub" inside "super".
|
|
146
|
-
const tokens = new Set(value.toLowerCase().split(/\s+/));
|
|
147
|
-
for (const [keyword, otTags] of Object.entries(mapping)) {
|
|
148
|
-
if (tokens.has(keyword)) {
|
|
149
|
-
for (const t of otTags) tags.add(t);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return tags;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Collect feature tags from all feature-related declarations in a CSS rule.
|
|
157
|
-
function ruleFeatureTags(rule) {
|
|
158
|
-
const tags = new Set();
|
|
159
|
-
let hasFeatureDecl = false;
|
|
160
|
-
for (const node of rule.nodes) {
|
|
161
|
-
if (
|
|
162
|
-
node.type === 'decl' &&
|
|
163
|
-
featureSettingsProps.has(node.prop.toLowerCase())
|
|
164
|
-
) {
|
|
165
|
-
hasFeatureDecl = true;
|
|
166
|
-
for (const t of extractFeatureTagsFromDecl(node.prop, node.value)) {
|
|
167
|
-
tags.add(t);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return hasFeatureDecl ? tags : null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function ruleFontFamily(rule) {
|
|
175
|
-
for (let i = rule.nodes.length - 1; i >= 0; i--) {
|
|
176
|
-
const node = rule.nodes[i];
|
|
177
|
-
if (node.type === 'decl' && node.prop.toLowerCase() === 'font-family') {
|
|
178
|
-
return node.value;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Add all items from `tags` into the Set stored at `key` in `map`,
|
|
185
|
-
// creating the Set if it doesn't exist yet.
|
|
186
|
-
function addTagsToMapEntry(map, key, tags) {
|
|
187
|
-
let s = map.get(key);
|
|
188
|
-
if (!s) {
|
|
189
|
-
s = new Set();
|
|
190
|
-
map.set(key, s);
|
|
191
|
-
}
|
|
192
|
-
for (const t of tags) s.add(t);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Record the OT tags from a single CSS rule into featureTagsByFamily,
|
|
196
|
-
// keyed by font-family (or '*' when no font-family is specified).
|
|
197
|
-
function recordRuleFeatureTags(rule, featureTagsByFamily) {
|
|
198
|
-
const tags = ruleFeatureTags(rule);
|
|
199
|
-
if (!tags) return null;
|
|
200
|
-
|
|
201
|
-
const fontFamily = ruleFontFamily(rule);
|
|
202
|
-
if (!fontFamily) {
|
|
203
|
-
if (featureTagsByFamily) addTagsToMapEntry(featureTagsByFamily, '*', tags);
|
|
204
|
-
return true; // signals "all families"
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const families = cssFontParser.parseFontFamily(fontFamily);
|
|
208
|
-
if (featureTagsByFamily) {
|
|
209
|
-
for (const family of families) {
|
|
210
|
-
addTagsToMapEntry(featureTagsByFamily, family.toLowerCase(), tags);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return families;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Determine which font-families use font-feature-settings or font-variant-*.
|
|
217
|
-
// Returns null (none detected), a Set of lowercase family names, or true (all).
|
|
218
|
-
// Also populates featureTagsByFamily with the OT tags per family (lowercase).
|
|
219
|
-
function findFontFamiliesWithFeatureSettings(
|
|
220
|
-
stylesheetsWithPredicates,
|
|
221
|
-
featureTagsByFamily
|
|
222
|
-
) {
|
|
223
|
-
let result = null;
|
|
224
|
-
for (const { asset } of stylesheetsWithPredicates) {
|
|
225
|
-
if (!asset || !asset.parseTree) continue;
|
|
226
|
-
asset.parseTree.walkRules((rule) => {
|
|
227
|
-
if (result === true && !featureTagsByFamily) return;
|
|
228
|
-
|
|
229
|
-
const recorded = recordRuleFeatureTags(rule, featureTagsByFamily);
|
|
230
|
-
if (!recorded) return;
|
|
231
|
-
|
|
232
|
-
if (recorded === true) {
|
|
233
|
-
result = true;
|
|
234
|
-
} else if (result !== true) {
|
|
235
|
-
if (!result) result = new Set();
|
|
236
|
-
for (const family of recorded) {
|
|
237
|
-
result.add(family.toLowerCase());
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
if (result === true && !featureTagsByFamily) break;
|
|
242
|
-
}
|
|
243
|
-
return result;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Determine whether a template's font families use feature settings, and
|
|
247
|
-
// collect the corresponding OT feature tags from featureTagsByFamily.
|
|
248
|
-
function resolveFeatureSettings(
|
|
249
|
-
fontFamilies,
|
|
250
|
-
fontFamiliesWithFeatureSettings,
|
|
251
|
-
featureTagsByFamily
|
|
252
|
-
) {
|
|
253
|
-
let hasFontFeatureSettings = false;
|
|
254
|
-
if (fontFamiliesWithFeatureSettings === true) {
|
|
255
|
-
hasFontFeatureSettings = true;
|
|
256
|
-
} else if (fontFamiliesWithFeatureSettings instanceof Set) {
|
|
257
|
-
for (const f of fontFamilies) {
|
|
258
|
-
if (fontFamiliesWithFeatureSettings.has(f.toLowerCase())) {
|
|
259
|
-
hasFontFeatureSettings = true;
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
let fontFeatureTags;
|
|
266
|
-
if (hasFontFeatureSettings && featureTagsByFamily) {
|
|
267
|
-
const tags = new Set();
|
|
268
|
-
const globalTags = featureTagsByFamily.get('*');
|
|
269
|
-
if (globalTags) {
|
|
270
|
-
for (const t of globalTags) tags.add(t);
|
|
271
|
-
}
|
|
272
|
-
for (const f of fontFamilies) {
|
|
273
|
-
const familyTags = featureTagsByFamily.get(f.toLowerCase());
|
|
274
|
-
if (familyTags) {
|
|
275
|
-
for (const t of familyTags) tags.add(t);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (tags.size > 0) {
|
|
279
|
-
fontFeatureTags = [...tags];
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return { hasFontFeatureSettings, fontFeatureTags };
|
|
284
|
-
}
|
|
43
|
+
const {
|
|
44
|
+
findFontFamiliesWithFeatureSettings,
|
|
45
|
+
resolveFeatureSettings,
|
|
46
|
+
} = require('./fontFeatureHelpers');
|
|
285
47
|
|
|
286
48
|
const allInitialValues = require('./initialValueByProp');
|
|
287
49
|
const initialValueByProp = {
|
|
@@ -398,16 +160,16 @@ function computeSnappedGlobalEntries(declarations, globalTextByProps) {
|
|
|
398
160
|
textAndProps,
|
|
399
161
|
...snapped,
|
|
400
162
|
fontVariationSettings: textAndProps.props['font-variation-settings'],
|
|
401
|
-
fontSize: textAndProps.props['font-size'],
|
|
402
163
|
});
|
|
403
164
|
}
|
|
404
165
|
}
|
|
405
166
|
return entries;
|
|
406
167
|
}
|
|
407
168
|
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
169
|
+
// Fill in fontUsageTemplates/pageTextIndex/preloadIndex on the cached
|
|
170
|
+
// declarations entry. No-op on repeat calls — results are shared across
|
|
171
|
+
// pages that resolve to the same @font-face set.
|
|
172
|
+
function populateGlobalFontUsages(
|
|
411
173
|
cached,
|
|
412
174
|
accumulatedFontFaceDeclarations,
|
|
413
175
|
text
|
|
@@ -505,9 +267,6 @@ function getOrComputeGlobalFontUsages(
|
|
|
505
267
|
.map((e) => e.fontVariationSettings)
|
|
506
268
|
.filter((fvs) => fvs && fvs.toLowerCase() !== 'normal')
|
|
507
269
|
);
|
|
508
|
-
const fontSizes = new Set(
|
|
509
|
-
fontEntries.map((e) => e.fontSize).filter((fs) => fs !== undefined)
|
|
510
|
-
);
|
|
511
270
|
// Use first entry's relations for size computation, or extra's if no entries
|
|
512
271
|
const fontRelations =
|
|
513
272
|
fontEntries.length > 0
|
|
@@ -543,7 +302,6 @@ function getOrComputeGlobalFontUsages(
|
|
|
543
302
|
fontStretches,
|
|
544
303
|
fontWeights,
|
|
545
304
|
fontVariationSettings,
|
|
546
|
-
fontSizes,
|
|
547
305
|
});
|
|
548
306
|
}
|
|
549
307
|
|
|
@@ -552,88 +310,117 @@ function getOrComputeGlobalFontUsages(
|
|
|
552
310
|
cached.preloadIndex = textAndPropsToFontUrl;
|
|
553
311
|
}
|
|
554
312
|
|
|
555
|
-
//
|
|
556
|
-
//
|
|
313
|
+
// Trace fonts across the given pages. Uses a worker pool when the workload
|
|
314
|
+
// justifies the thread-startup overhead; otherwise falls back to sequential
|
|
315
|
+
// in-process tracing (required when a HeadlessBrowser is driving things).
|
|
557
316
|
async function tracePages(
|
|
558
317
|
pagesNeedingFullTrace,
|
|
559
|
-
{
|
|
318
|
+
{
|
|
319
|
+
headlessBrowser,
|
|
320
|
+
concurrency,
|
|
321
|
+
console,
|
|
322
|
+
memoizedGetCssRulesByProperty,
|
|
323
|
+
debug = false,
|
|
324
|
+
}
|
|
560
325
|
) {
|
|
326
|
+
const totalPages = pagesNeedingFullTrace.length;
|
|
327
|
+
if (totalPages === 0) return;
|
|
328
|
+
|
|
561
329
|
const useWorkerPool =
|
|
562
|
-
!headlessBrowser &&
|
|
563
|
-
|
|
330
|
+
!headlessBrowser && totalPages >= MIN_PAGES_FOR_WORKER_POOL;
|
|
331
|
+
|
|
332
|
+
const progress = createPageProgress({
|
|
333
|
+
total: totalPages,
|
|
334
|
+
console,
|
|
335
|
+
label: 'Tracing fonts',
|
|
336
|
+
});
|
|
564
337
|
|
|
565
338
|
if (useWorkerPool) {
|
|
566
339
|
const maxWorkers =
|
|
567
340
|
concurrency > 0 ? concurrency : Math.min(os.cpus().length, 8);
|
|
568
|
-
const numWorkers = Math.min(maxWorkers,
|
|
341
|
+
const numWorkers = Math.min(maxWorkers, totalPages);
|
|
569
342
|
const pool = new FontTracerPool(numWorkers);
|
|
570
343
|
await pool.init();
|
|
571
344
|
|
|
572
345
|
try {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
pd.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (console) {
|
|
584
|
-
console.warn(
|
|
585
|
-
`Worker fontTracer failed for ${pd.htmlOrSvgAsset.url}, falling back to main thread: ${err.message}`
|
|
346
|
+
progress.banner(
|
|
347
|
+
` Tracing fonts across ${totalPages} pages using ${numWorkers} worker${numWorkers === 1 ? '' : 's'}...`
|
|
348
|
+
);
|
|
349
|
+
await Promise.all(
|
|
350
|
+
pagesNeedingFullTrace.map(async (pd) => {
|
|
351
|
+
const pageStart = debug ? Date.now() : 0;
|
|
352
|
+
try {
|
|
353
|
+
pd.textByProps = await pool.trace(
|
|
354
|
+
pd.htmlOrSvgAsset.text || '',
|
|
355
|
+
pd.stylesheetsWithPredicates
|
|
586
356
|
);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
if (console) {
|
|
359
|
+
console.warn(
|
|
360
|
+
`Worker fontTracer failed for ${pd.htmlOrSvgAsset.url}, falling back to main thread: ${err.message}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
pd.textByProps = fontTracer(pd.htmlOrSvgAsset.parseTree, {
|
|
364
|
+
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
365
|
+
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
366
|
+
asset: pd.htmlOrSvgAsset,
|
|
367
|
+
});
|
|
587
368
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
await Promise.all(tracePromises);
|
|
369
|
+
const idx = progress.tick();
|
|
370
|
+
logTracedPage(
|
|
371
|
+
console,
|
|
372
|
+
debug,
|
|
373
|
+
idx,
|
|
374
|
+
totalPages,
|
|
375
|
+
pd.htmlOrSvgAsset,
|
|
376
|
+
pageStart
|
|
377
|
+
);
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
progress.done();
|
|
601
381
|
} finally {
|
|
602
382
|
await pool.destroy();
|
|
603
383
|
}
|
|
604
|
-
} else
|
|
605
|
-
|
|
606
|
-
|
|
384
|
+
} else {
|
|
385
|
+
progress.banner(
|
|
386
|
+
` Tracing fonts across ${totalPages} page${totalPages === 1 ? '' : 's'} (single-threaded${headlessBrowser ? ' + headless browser' : ''})...`
|
|
387
|
+
);
|
|
607
388
|
for (let pi = 0; pi < totalPages; pi++) {
|
|
608
389
|
const pd = pagesNeedingFullTrace[pi];
|
|
390
|
+
const pageStart = debug ? Date.now() : 0;
|
|
609
391
|
pd.textByProps = fontTracer(pd.htmlOrSvgAsset.parseTree, {
|
|
610
392
|
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
611
393
|
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
612
394
|
asset: pd.htmlOrSvgAsset,
|
|
613
|
-
propsToReturn: PROPS_TO_RETURN,
|
|
614
395
|
});
|
|
615
396
|
if (headlessBrowser) {
|
|
616
397
|
pd.textByProps.push(
|
|
617
398
|
...(await headlessBrowser.tracePage(pd.htmlOrSvgAsset))
|
|
618
399
|
);
|
|
619
400
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
401
|
+
const idx = progress.tick();
|
|
402
|
+
logTracedPage(
|
|
403
|
+
console,
|
|
404
|
+
debug,
|
|
405
|
+
idx,
|
|
406
|
+
totalPages,
|
|
407
|
+
pd.htmlOrSvgAsset,
|
|
408
|
+
pageStart
|
|
409
|
+
);
|
|
623
410
|
}
|
|
411
|
+
progress.done();
|
|
624
412
|
}
|
|
625
413
|
}
|
|
626
414
|
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
415
|
+
// For each page that shares a representative's CSS configuration, copy the
|
|
416
|
+
// representative's font-variant props and overlay this page's visible text.
|
|
417
|
+
// Returns the number of pages that had to fall back to a full trace
|
|
418
|
+
// (because inline style attributes made the fast path unsafe).
|
|
630
419
|
function processFastPathPages(
|
|
631
420
|
fastPathPages,
|
|
632
|
-
{ memoizedGetCssRulesByProperty
|
|
421
|
+
{ memoizedGetCssRulesByProperty }
|
|
633
422
|
) {
|
|
634
|
-
if (fastPathPages.length === 0) return;
|
|
635
|
-
|
|
636
|
-
const fastPathStart = Date.now();
|
|
423
|
+
if (fastPathPages.length === 0) return 0;
|
|
637
424
|
|
|
638
425
|
const repDataCache = new Map();
|
|
639
426
|
function getRepData(representativePd) {
|
|
@@ -682,7 +469,6 @@ function processFastPathPages(
|
|
|
682
469
|
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
683
470
|
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
684
471
|
asset: pd.htmlOrSvgAsset,
|
|
685
|
-
propsToReturn: PROPS_TO_RETURN,
|
|
686
472
|
});
|
|
687
473
|
continue;
|
|
688
474
|
}
|
|
@@ -742,238 +528,205 @@ function processFastPathPages(
|
|
|
742
528
|
});
|
|
743
529
|
}
|
|
744
530
|
}
|
|
745
|
-
|
|
746
|
-
if (debug && console)
|
|
747
|
-
console.log(
|
|
748
|
-
`[subfont timing] Fast-path text extraction (${fastPathPages.length} pages, ${fastPathFallbacks} fell back to full trace): ${subTimings['Fast-path extraction']}ms`
|
|
749
|
-
);
|
|
531
|
+
return fastPathFallbacks;
|
|
750
532
|
}
|
|
751
533
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
chromeArgs = [],
|
|
762
|
-
} = {}
|
|
763
|
-
) {
|
|
764
|
-
const htmlOrSvgAssetTextsWithProps = [];
|
|
765
|
-
|
|
766
|
-
const memoizedGetCssRulesByProperty = memoizeSync(getCssRulesByProperty);
|
|
767
|
-
|
|
768
|
-
const fontFaceDeclarationsByHtmlOrSvgAsset = new Map();
|
|
769
|
-
|
|
770
|
-
// Cache stylesheet-dependent results for pages with identical CSS
|
|
771
|
-
// configurations.
|
|
772
|
-
const stylesheetResultCache = new Map();
|
|
534
|
+
// Pre-build an index of stylesheet-related relations by source asset
|
|
535
|
+
// to avoid repeated assetGraph.findRelations scans (O(allRelations) each).
|
|
536
|
+
const STYLESHEET_REL_TYPES = [
|
|
537
|
+
'HtmlStyle',
|
|
538
|
+
'SvgStyle',
|
|
539
|
+
'CssImport',
|
|
540
|
+
'HtmlConditionalComment',
|
|
541
|
+
'HtmlNoscript',
|
|
542
|
+
];
|
|
773
543
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const stylesheetRelTypes = [
|
|
777
|
-
'HtmlStyle',
|
|
778
|
-
'SvgStyle',
|
|
779
|
-
'CssImport',
|
|
780
|
-
'HtmlConditionalComment',
|
|
781
|
-
'HtmlNoscript',
|
|
782
|
-
];
|
|
783
|
-
const stylesheetRelsByFromAsset = new Map();
|
|
544
|
+
function indexStylesheetRelations(assetGraph) {
|
|
545
|
+
const byFromAsset = new Map();
|
|
784
546
|
for (const relation of assetGraph.findRelations({
|
|
785
|
-
type: {
|
|
786
|
-
$in: stylesheetRelTypes,
|
|
787
|
-
},
|
|
547
|
+
type: { $in: STYLESHEET_REL_TYPES },
|
|
788
548
|
})) {
|
|
789
|
-
let arr =
|
|
549
|
+
let arr = byFromAsset.get(relation.from);
|
|
790
550
|
if (!arr) {
|
|
791
551
|
arr = [];
|
|
792
|
-
|
|
552
|
+
byFromAsset.set(relation.from, arr);
|
|
793
553
|
}
|
|
794
554
|
arr.push(relation);
|
|
795
555
|
}
|
|
556
|
+
return byFromAsset;
|
|
557
|
+
}
|
|
796
558
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
559
|
+
// Build a cache key by traversing stylesheet relations, capturing
|
|
560
|
+
// both asset identity and relation context (media, conditionalComment,
|
|
561
|
+
// noscript) that affect gatherStylesheetsWithPredicates output.
|
|
562
|
+
function buildStylesheetKey(
|
|
563
|
+
htmlOrSvgAsset,
|
|
564
|
+
skipNonFontInlineCss,
|
|
565
|
+
stylesheetRelsByFromAsset
|
|
566
|
+
) {
|
|
567
|
+
const keyParts = [];
|
|
568
|
+
const visited = new Set();
|
|
569
|
+
(function traverse(asset, isNoscript) {
|
|
570
|
+
if (visited.has(asset)) return;
|
|
571
|
+
if (!asset.isLoaded) return;
|
|
572
|
+
visited.add(asset);
|
|
573
|
+
for (const relation of stylesheetRelsByFromAsset.get(asset) || []) {
|
|
574
|
+
if (relation.type === 'HtmlNoscript') {
|
|
575
|
+
traverse(relation.to, true);
|
|
576
|
+
} else if (relation.type === 'HtmlConditionalComment') {
|
|
577
|
+
keyParts.push(`cc:${relation.condition}`);
|
|
578
|
+
traverse(relation.to, isNoscript);
|
|
579
|
+
} else {
|
|
580
|
+
const target = relation.to;
|
|
581
|
+
if (
|
|
582
|
+
skipNonFontInlineCss &&
|
|
583
|
+
target.isInline &&
|
|
584
|
+
target.type === 'Css' &&
|
|
585
|
+
!fontRelevantCssRegex.test(target.text || '')
|
|
586
|
+
) {
|
|
587
|
+
continue;
|
|
826
588
|
}
|
|
589
|
+
const media = relation.media || '';
|
|
590
|
+
keyParts.push(`${target.id}:${media}:${isNoscript ? 'ns' : ''}`);
|
|
591
|
+
traverse(target, isNoscript);
|
|
827
592
|
}
|
|
828
|
-
})(htmlOrSvgAsset, false);
|
|
829
|
-
return keyParts.join('\x1d');
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function getOrComputeStylesheetResults(htmlOrSvgAsset) {
|
|
833
|
-
const key = buildStylesheetKey(htmlOrSvgAsset);
|
|
834
|
-
if (stylesheetResultCache.has(key)) {
|
|
835
|
-
return stylesheetResultCache.get(key);
|
|
836
593
|
}
|
|
594
|
+
})(htmlOrSvgAsset, false);
|
|
595
|
+
return keyParts.join('\x1d');
|
|
596
|
+
}
|
|
837
597
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
598
|
+
// Walk the stylesheet graph rooted at htmlOrSvgAsset and collect every
|
|
599
|
+
// @font-face declaration into a flat list, preserving the CSS relation node
|
|
600
|
+
// so callers can correlate declarations back to their source rules.
|
|
601
|
+
function collectFontFaceDeclarations(
|
|
602
|
+
htmlOrSvgAsset,
|
|
603
|
+
stylesheetRelsByFromAsset
|
|
604
|
+
) {
|
|
605
|
+
const accumulatedFontFaceDeclarations = [];
|
|
606
|
+
const visitedAssets = new Set();
|
|
607
|
+
(function traverseForFontFace(asset) {
|
|
608
|
+
if (visitedAssets.has(asset)) return;
|
|
609
|
+
visitedAssets.add(asset);
|
|
610
|
+
|
|
611
|
+
if (asset.type === 'Css' && asset.isLoaded) {
|
|
612
|
+
const seenNodes = new Set();
|
|
613
|
+
const fontRelations = asset.outgoingRelations.filter(
|
|
614
|
+
(relation) => relation.type === 'CssFontFaceSrc'
|
|
615
|
+
);
|
|
843
616
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
(function traverseForFontFace(asset) {
|
|
849
|
-
if (visitedAssets.has(asset)) return;
|
|
850
|
-
visitedAssets.add(asset);
|
|
851
|
-
|
|
852
|
-
if (asset.type === 'Css' && asset.isLoaded) {
|
|
853
|
-
const seenNodes = new Set();
|
|
854
|
-
const fontRelations = asset.outgoingRelations.filter(
|
|
855
|
-
(relation) => relation.type === 'CssFontFaceSrc'
|
|
856
|
-
);
|
|
617
|
+
for (const fontRelation of fontRelations) {
|
|
618
|
+
const node = fontRelation.node;
|
|
619
|
+
if (seenNodes.has(node)) continue;
|
|
620
|
+
seenNodes.add(node);
|
|
857
621
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const fontFaceDeclaration = {
|
|
864
|
-
relations: fontRelations.filter((r) => r.node === node),
|
|
865
|
-
...initialValueByProp,
|
|
866
|
-
};
|
|
867
|
-
|
|
868
|
-
node.walkDecls((declaration) => {
|
|
869
|
-
const propName = declaration.prop.toLowerCase();
|
|
870
|
-
fontFaceDeclaration[propName] =
|
|
871
|
-
propName === 'font-family'
|
|
872
|
-
? cssFontParser.parseFontFamily(declaration.value)[0]
|
|
873
|
-
: declaration.value;
|
|
874
|
-
});
|
|
875
|
-
// Disregard incomplete @font-face declarations (must contain font-family and src per spec):
|
|
876
|
-
if (fontFaceDeclaration['font-family'] && fontFaceDeclaration.src) {
|
|
877
|
-
accumulatedFontFaceDeclarations.push(fontFaceDeclaration);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
622
|
+
const fontFaceDeclaration = {
|
|
623
|
+
relations: fontRelations.filter((r) => r.node === node),
|
|
624
|
+
...initialValueByProp,
|
|
625
|
+
};
|
|
881
626
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
627
|
+
node.walkDecls((declaration) => {
|
|
628
|
+
const propName = declaration.prop.toLowerCase();
|
|
629
|
+
fontFaceDeclaration[propName] =
|
|
630
|
+
propName === 'font-family'
|
|
631
|
+
? cssFontParser.parseFontFamily(declaration.value)[0]
|
|
632
|
+
: declaration.value;
|
|
633
|
+
});
|
|
634
|
+
// Disregard incomplete @font-face declarations (must contain font-family and src per spec):
|
|
635
|
+
if (fontFaceDeclaration['font-family'] && fontFaceDeclaration.src) {
|
|
636
|
+
accumulatedFontFaceDeclarations.push(fontFaceDeclaration);
|
|
891
637
|
}
|
|
892
|
-
}
|
|
638
|
+
}
|
|
893
639
|
}
|
|
894
640
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
comboGroups.get(comboKey).push(fontFace);
|
|
903
|
-
}
|
|
904
|
-
for (const [comboKey, group] of comboGroups) {
|
|
905
|
-
if (group.length <= 1) continue;
|
|
906
|
-
const withoutRange = group.filter((d) => !d['unicode-range']);
|
|
907
|
-
if (withoutRange.length > 0) {
|
|
908
|
-
throw new Error(
|
|
909
|
-
`Multiple @font-face with the same font-family/font-style/font-weight combo but missing unicode-range on ${withoutRange.length} of ${group.length} declarations: ${comboKey}`
|
|
910
|
-
);
|
|
641
|
+
const rels = stylesheetRelsByFromAsset.get(asset) || [];
|
|
642
|
+
for (const rel of rels) {
|
|
643
|
+
if (
|
|
644
|
+
fontFaceTraversalTypes.has(rel.type) ||
|
|
645
|
+
(rel.to && rel.to.type === 'Html' && rel.to.isInline)
|
|
646
|
+
) {
|
|
647
|
+
traverseForFontFace(rel.to);
|
|
911
648
|
}
|
|
912
649
|
}
|
|
650
|
+
})(htmlOrSvgAsset);
|
|
651
|
+
return accumulatedFontFaceDeclarations;
|
|
652
|
+
}
|
|
913
653
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
654
|
+
// Validate that @font-face declarations sharing family/style/weight carry
|
|
655
|
+
// disjoint unicode-range values; throws on incomplete coverage.
|
|
656
|
+
function validateFontFaceComboCoverage(accumulatedFontFaceDeclarations) {
|
|
657
|
+
const comboGroups = new Map();
|
|
658
|
+
for (const fontFace of accumulatedFontFaceDeclarations) {
|
|
659
|
+
const comboKey = `${fontFace['font-family']}/${fontFace['font-style']}/${fontFace['font-weight']}`;
|
|
660
|
+
if (!comboGroups.has(comboKey)) comboGroups.set(comboKey, []);
|
|
661
|
+
comboGroups.get(comboKey).push(fontFace);
|
|
662
|
+
}
|
|
663
|
+
for (const [comboKey, group] of comboGroups) {
|
|
664
|
+
if (group.length <= 1) continue;
|
|
665
|
+
const withoutRange = group.filter((d) => !d['unicode-range']);
|
|
666
|
+
if (withoutRange.length > 0) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
`Multiple @font-face with the same font-family/font-style/font-weight combo but missing unicode-range on ${withoutRange.length} of ${group.length} declarations: ${comboKey}`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
929
671
|
}
|
|
672
|
+
}
|
|
930
673
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
674
|
+
function computeStylesheetResults(htmlOrSvgAsset, stylesheetRelsByFromAsset) {
|
|
675
|
+
const stylesheetsWithPredicates = gatherStylesheetsWithPredicates(
|
|
676
|
+
htmlOrSvgAsset.assetGraph,
|
|
677
|
+
htmlOrSvgAsset,
|
|
678
|
+
stylesheetRelsByFromAsset
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const accumulatedFontFaceDeclarations = collectFontFaceDeclarations(
|
|
682
|
+
htmlOrSvgAsset,
|
|
683
|
+
stylesheetRelsByFromAsset
|
|
684
|
+
);
|
|
685
|
+
validateFontFaceComboCoverage(accumulatedFontFaceDeclarations);
|
|
935
686
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
687
|
+
const featureTagsByFamily = new Map();
|
|
688
|
+
const fontFamiliesWithFeatureSettings = findFontFamiliesWithFeatureSettings(
|
|
689
|
+
stylesheetsWithPredicates,
|
|
690
|
+
featureTagsByFamily
|
|
691
|
+
);
|
|
939
692
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
stylesheetsWithPredicates,
|
|
947
|
-
fontFamiliesWithFeatureSettings,
|
|
948
|
-
featureTagsByFamily,
|
|
949
|
-
fastPathKey,
|
|
950
|
-
} = getOrComputeStylesheetResults(htmlOrSvgAsset);
|
|
951
|
-
fontFaceDeclarationsByHtmlOrSvgAsset.set(
|
|
693
|
+
return {
|
|
694
|
+
accumulatedFontFaceDeclarations,
|
|
695
|
+
stylesheetsWithPredicates,
|
|
696
|
+
fontFamiliesWithFeatureSettings,
|
|
697
|
+
featureTagsByFamily,
|
|
698
|
+
fastPathKey: buildStylesheetKey(
|
|
952
699
|
htmlOrSvgAsset,
|
|
953
|
-
|
|
954
|
-
|
|
700
|
+
true,
|
|
701
|
+
stylesheetRelsByFromAsset
|
|
702
|
+
),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
955
705
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
706
|
+
// Strip `-subfont-text` nodes from CSS @font-face declarations once the
|
|
707
|
+
// subset planning is done, so they don't leak to the rendered output.
|
|
708
|
+
function stripSubfontTextNodes(fontFaceDeclarationsByHtmlOrSvgAsset) {
|
|
709
|
+
for (const fontFaceDeclarations of fontFaceDeclarationsByHtmlOrSvgAsset.values()) {
|
|
710
|
+
for (const fontFaceDeclaration of fontFaceDeclarations) {
|
|
711
|
+
const firstRelation = fontFaceDeclaration.relations[0];
|
|
712
|
+
const subfontTextNode = firstRelation.node.nodes.find(
|
|
713
|
+
(childNode) =>
|
|
714
|
+
childNode.type === 'decl' &&
|
|
715
|
+
childNode.prop.toLowerCase() === '-subfont-text'
|
|
716
|
+
);
|
|
959
717
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
featureTagsByFamily,
|
|
966
|
-
stylesheetCacheKey: fastPathKey,
|
|
967
|
-
});
|
|
718
|
+
if (subfontTextNode) {
|
|
719
|
+
subfontTextNode.remove();
|
|
720
|
+
firstRelation.from.markDirty();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
968
723
|
}
|
|
724
|
+
}
|
|
969
725
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
// Group pages by stylesheet cache key — pages sharing the same CSS
|
|
976
|
-
// configuration produce identical font-tracer props, only text differs.
|
|
726
|
+
// Split trace work: with a headless browser every page needs a full trace
|
|
727
|
+
// (dynamic content); otherwise one representative per stylesheet group is
|
|
728
|
+
// traced and the rest use fast-path text extraction.
|
|
729
|
+
function planTracing(pageData, hasHeadlessBrowser) {
|
|
977
730
|
const pagesByStylesheetKey = new Map();
|
|
978
731
|
for (const pd of pageData) {
|
|
979
732
|
let group = pagesByStylesheetKey.get(pd.stylesheetCacheKey);
|
|
@@ -986,7 +739,7 @@ async function collectTextsByPage(
|
|
|
986
739
|
|
|
987
740
|
const pagesNeedingFullTrace = [];
|
|
988
741
|
const fastPathPages = [];
|
|
989
|
-
if (
|
|
742
|
+
if (hasHeadlessBrowser) {
|
|
990
743
|
for (const pd of pageData) {
|
|
991
744
|
pagesNeedingFullTrace.push(pd);
|
|
992
745
|
}
|
|
@@ -1000,91 +753,37 @@ async function collectTextsByPage(
|
|
|
1000
753
|
}
|
|
1001
754
|
}
|
|
1002
755
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
try {
|
|
1010
|
-
await tracePages(pagesNeedingFullTrace, {
|
|
1011
|
-
headlessBrowser,
|
|
1012
|
-
concurrency,
|
|
1013
|
-
console,
|
|
1014
|
-
memoizedGetCssRulesByProperty,
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
subTimings['Full tracing'] = Date.now() - tracingStart;
|
|
1018
|
-
if (debug && console)
|
|
1019
|
-
console.log(
|
|
1020
|
-
`[subfont timing] Full tracing (${pagesNeedingFullTrace.length} pages): ${subTimings['Full tracing']}ms`
|
|
1021
|
-
);
|
|
1022
|
-
|
|
1023
|
-
processFastPathPages(fastPathPages, {
|
|
1024
|
-
memoizedGetCssRulesByProperty,
|
|
1025
|
-
console,
|
|
1026
|
-
debug,
|
|
1027
|
-
subTimings,
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
const assembleStart = Date.now();
|
|
1031
|
-
for (const pd of pageData) {
|
|
1032
|
-
for (const textByPropsEntry of pd.textByProps) {
|
|
1033
|
-
textByPropsEntry.htmlOrSvgAsset = pd.htmlOrSvgAsset;
|
|
1034
|
-
}
|
|
1035
|
-
// Use a loop instead of push(...spread) to avoid stack overflow on large sites
|
|
1036
|
-
for (const entry of pd.textByProps) {
|
|
1037
|
-
globalTextByProps.push(entry);
|
|
1038
|
-
}
|
|
1039
|
-
htmlOrSvgAssetTextsWithProps.push({
|
|
1040
|
-
htmlOrSvgAsset: pd.htmlOrSvgAsset,
|
|
1041
|
-
textByProps: pd.textByProps,
|
|
1042
|
-
accumulatedFontFaceDeclarations: pd.accumulatedFontFaceDeclarations,
|
|
1043
|
-
fontFamiliesWithFeatureSettings: pd.fontFamiliesWithFeatureSettings,
|
|
1044
|
-
featureTagsByFamily: pd.featureTagsByFamily,
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
subTimings['Result assembly'] = Date.now() - assembleStart;
|
|
1049
|
-
if (debug && console) {
|
|
1050
|
-
console.log(
|
|
1051
|
-
`[subfont timing] Result assembly: ${subTimings['Result assembly']}ms`
|
|
1052
|
-
);
|
|
1053
|
-
console.log(
|
|
1054
|
-
`[subfont timing] Total tracing+extraction+assembly: ${
|
|
1055
|
-
Date.now() - tracingStart
|
|
1056
|
-
}ms`
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
} finally {
|
|
1060
|
-
if (headlessBrowser) {
|
|
1061
|
-
await headlessBrowser.close();
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
const postProcessStart = Date.now();
|
|
756
|
+
return {
|
|
757
|
+
pagesNeedingFullTrace,
|
|
758
|
+
fastPathPages,
|
|
759
|
+
uniqueGroupCount: pagesByStylesheetKey.size,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
1066
762
|
|
|
1067
|
-
|
|
763
|
+
// Iterate every traced page, snap its text against the @font-face set, and
|
|
764
|
+
// emit fully-formed per-page fontUsages (one entry per font URL + props).
|
|
765
|
+
// Caching is per declarations-key (declCache) and per raw pageText
|
|
766
|
+
// (uniqueCharsCache) so sites with many similar pages stay linear.
|
|
767
|
+
function buildPerPageFontUsages(
|
|
768
|
+
htmlOrSvgAssetTextsWithProps,
|
|
769
|
+
globalTextByProps,
|
|
770
|
+
text
|
|
771
|
+
) {
|
|
1068
772
|
const declCache = new Map();
|
|
1069
|
-
|
|
1070
|
-
const perPageLoopStart = Date.now();
|
|
773
|
+
const uniqueCharsCache = new Map();
|
|
1071
774
|
let snappingTime = 0;
|
|
1072
775
|
let globalUsageTime = 0;
|
|
1073
|
-
|
|
1074
|
-
// Cache uniqueChars results by raw pageText string to avoid recomputing
|
|
1075
|
-
const uniqueCharsCache = new Map();
|
|
1076
776
|
let cloningTime = 0;
|
|
1077
777
|
|
|
1078
|
-
for (const
|
|
778
|
+
for (const entry of htmlOrSvgAssetTextsWithProps) {
|
|
1079
779
|
const {
|
|
1080
780
|
htmlOrSvgAsset,
|
|
1081
781
|
textByProps,
|
|
1082
782
|
accumulatedFontFaceDeclarations,
|
|
1083
783
|
fontFamiliesWithFeatureSettings,
|
|
1084
784
|
featureTagsByFamily,
|
|
1085
|
-
} =
|
|
785
|
+
} = entry;
|
|
1086
786
|
|
|
1087
|
-
// Get or compute the snapped global entries for this declarations set
|
|
1088
787
|
const declKey = getDeclarationsKey(accumulatedFontFaceDeclarations);
|
|
1089
788
|
if (!declCache.has(declKey)) {
|
|
1090
789
|
const snapStart = Date.now();
|
|
@@ -1100,108 +799,260 @@ async function collectTextsByPage(
|
|
|
1100
799
|
snappingTime += Date.now() - snapStart;
|
|
1101
800
|
}
|
|
1102
801
|
|
|
1103
|
-
// Precompute global font usage templates and indices once per declarations key
|
|
1104
802
|
const declCacheEntry = declCache.get(declKey);
|
|
1105
803
|
const globalUsageStart = Date.now();
|
|
1106
|
-
|
|
804
|
+
populateGlobalFontUsages(
|
|
1107
805
|
declCacheEntry,
|
|
1108
806
|
accumulatedFontFaceDeclarations,
|
|
1109
807
|
text
|
|
1110
808
|
);
|
|
1111
809
|
globalUsageTime += Date.now() - globalUsageStart;
|
|
1112
810
|
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
811
|
+
const {
|
|
812
|
+
fontUsageTemplates,
|
|
813
|
+
pageTextIndex,
|
|
814
|
+
preloadIndex: textAndPropsToFontUrl,
|
|
815
|
+
} = declCacheEntry;
|
|
1116
816
|
|
|
1117
|
-
// Compute preload per fontUrl using inverted index
|
|
1118
817
|
const preloadFontUrls = new Set();
|
|
1119
|
-
for (const
|
|
1120
|
-
const fontUrl = textAndPropsToFontUrl.get(
|
|
818
|
+
for (const textByPropsEntry of textByProps) {
|
|
819
|
+
const fontUrl = textAndPropsToFontUrl.get(textByPropsEntry);
|
|
1121
820
|
if (fontUrl) {
|
|
1122
821
|
preloadFontUrls.add(fontUrl);
|
|
1123
822
|
}
|
|
1124
823
|
}
|
|
1125
824
|
|
|
1126
|
-
// Build per-page fontUsages from precomputed templates
|
|
1127
825
|
const cloneStart = Date.now();
|
|
1128
826
|
const assetTexts = pageTextIndex.get(htmlOrSvgAsset);
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
827
|
+
entry.fontUsages = fontUsageTemplates.map((template) => {
|
|
828
|
+
const pageTexts = assetTexts
|
|
829
|
+
? assetTexts.get(template.fontUrl)
|
|
830
|
+
: undefined;
|
|
831
|
+
let pageTextStr = pageTexts ? pageTexts.join('') : '';
|
|
832
|
+
if (template.extraTextsStr) {
|
|
833
|
+
pageTextStr += template.extraTextsStr;
|
|
834
|
+
}
|
|
1138
835
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
836
|
+
let pageTextUnique = uniqueCharsCache.get(pageTextStr);
|
|
837
|
+
if (pageTextUnique === undefined) {
|
|
838
|
+
pageTextUnique = uniqueChars(pageTextStr);
|
|
839
|
+
uniqueCharsCache.set(pageTextStr, pageTextUnique);
|
|
840
|
+
}
|
|
1144
841
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
842
|
+
const { hasFontFeatureSettings, fontFeatureTags } =
|
|
843
|
+
resolveFeatureSettings(
|
|
844
|
+
template.fontFamilies,
|
|
845
|
+
fontFamiliesWithFeatureSettings,
|
|
846
|
+
featureTagsByFamily
|
|
847
|
+
);
|
|
1151
848
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
);
|
|
849
|
+
return {
|
|
850
|
+
smallestOriginalSize: template.smallestOriginalSize,
|
|
851
|
+
smallestOriginalFormat: template.smallestOriginalFormat,
|
|
852
|
+
texts: template.texts,
|
|
853
|
+
pageText: pageTextUnique,
|
|
854
|
+
text: template.text,
|
|
855
|
+
props: { ...template.props },
|
|
856
|
+
fontUrl: template.fontUrl,
|
|
857
|
+
fontFamilies: template.fontFamilies,
|
|
858
|
+
fontStyles: template.fontStyles,
|
|
859
|
+
fontStretches: template.fontStretches,
|
|
860
|
+
fontWeights: template.fontWeights,
|
|
861
|
+
fontVariationSettings: template.fontVariationSettings,
|
|
862
|
+
preload: preloadFontUrls.has(template.fontUrl),
|
|
863
|
+
hasFontFeatureSettings,
|
|
864
|
+
fontFeatureTags,
|
|
865
|
+
};
|
|
866
|
+
});
|
|
1172
867
|
cloningTime += Date.now() - cloneStart;
|
|
1173
868
|
}
|
|
1174
869
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
870
|
+
return { snappingTime, globalUsageTime, cloningTime };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Run computeStylesheetResults once per page, memoizing the result across
|
|
874
|
+
// pages that resolve to the same set of stylesheets. Pages without any
|
|
875
|
+
// @font-face declarations are recorded in the declarations map but skipped
|
|
876
|
+
// from pageData (nothing to trace or subset for them).
|
|
877
|
+
function precomputeStylesheetsForPages(
|
|
878
|
+
htmlOrSvgAssets,
|
|
879
|
+
stylesheetRelsByFromAsset,
|
|
880
|
+
fontFaceDeclarationsByHtmlOrSvgAsset
|
|
881
|
+
) {
|
|
882
|
+
const stylesheetResultCache = new Map();
|
|
883
|
+
const pageData = [];
|
|
884
|
+
|
|
885
|
+
for (const htmlOrSvgAsset of htmlOrSvgAssets) {
|
|
886
|
+
const key = buildStylesheetKey(
|
|
887
|
+
htmlOrSvgAsset,
|
|
888
|
+
false,
|
|
889
|
+
stylesheetRelsByFromAsset
|
|
1180
890
|
);
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
891
|
+
let result = stylesheetResultCache.get(key);
|
|
892
|
+
if (!result) {
|
|
893
|
+
result = computeStylesheetResults(
|
|
894
|
+
htmlOrSvgAsset,
|
|
895
|
+
stylesheetRelsByFromAsset
|
|
896
|
+
);
|
|
897
|
+
stylesheetResultCache.set(key, result);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
fontFaceDeclarationsByHtmlOrSvgAsset.set(
|
|
901
|
+
htmlOrSvgAsset,
|
|
902
|
+
result.accumulatedFontFaceDeclarations
|
|
1184
903
|
);
|
|
1185
|
-
|
|
904
|
+
|
|
905
|
+
if (result.accumulatedFontFaceDeclarations.length === 0) {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
pageData.push({
|
|
910
|
+
htmlOrSvgAsset,
|
|
911
|
+
accumulatedFontFaceDeclarations: result.accumulatedFontFaceDeclarations,
|
|
912
|
+
stylesheetsWithPredicates: result.stylesheetsWithPredicates,
|
|
913
|
+
fontFamiliesWithFeatureSettings: result.fontFamiliesWithFeatureSettings,
|
|
914
|
+
featureTagsByFamily: result.featureTagsByFamily,
|
|
915
|
+
stylesheetCacheKey: result.fastPathKey,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return pageData;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Flatten traced per-page textByProps into a single globalTextByProps array,
|
|
923
|
+
// tagging each entry with its owning asset so downstream code can map text
|
|
924
|
+
// back to the page that rendered it.
|
|
925
|
+
function flattenTracedPagesIntoGlobal(
|
|
926
|
+
pageData,
|
|
927
|
+
htmlOrSvgAssetTextsWithProps,
|
|
928
|
+
globalTextByProps
|
|
929
|
+
) {
|
|
930
|
+
for (const pd of pageData) {
|
|
931
|
+
for (const textByPropsEntry of pd.textByProps) {
|
|
932
|
+
textByPropsEntry.htmlOrSvgAsset = pd.htmlOrSvgAsset;
|
|
933
|
+
}
|
|
934
|
+
// Use a loop instead of push(...spread) to avoid stack overflow on large sites
|
|
935
|
+
for (const entry of pd.textByProps) {
|
|
936
|
+
globalTextByProps.push(entry);
|
|
937
|
+
}
|
|
938
|
+
htmlOrSvgAssetTextsWithProps.push({
|
|
939
|
+
htmlOrSvgAsset: pd.htmlOrSvgAsset,
|
|
940
|
+
textByProps: pd.textByProps,
|
|
941
|
+
accumulatedFontFaceDeclarations: pd.accumulatedFontFaceDeclarations,
|
|
942
|
+
fontFamiliesWithFeatureSettings: pd.fontFamiliesWithFeatureSettings,
|
|
943
|
+
featureTagsByFamily: pd.featureTagsByFamily,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function collectTextsByPage(
|
|
949
|
+
assetGraph,
|
|
950
|
+
htmlOrSvgAssets,
|
|
951
|
+
{
|
|
952
|
+
text,
|
|
953
|
+
console,
|
|
954
|
+
dynamic = false,
|
|
955
|
+
debug = false,
|
|
956
|
+
concurrency,
|
|
957
|
+
chromeArgs = [],
|
|
958
|
+
} = {}
|
|
959
|
+
) {
|
|
960
|
+
const htmlOrSvgAssetTextsWithProps = [];
|
|
961
|
+
const memoizedGetCssRulesByProperty = memoizeSync(getCssRulesByProperty);
|
|
962
|
+
const fontFaceDeclarationsByHtmlOrSvgAsset = new Map();
|
|
963
|
+
const stylesheetRelsByFromAsset = indexStylesheetRelations(assetGraph);
|
|
964
|
+
|
|
965
|
+
const headlessBrowser =
|
|
966
|
+
dynamic && new HeadlessBrowser({ console, chromeArgs });
|
|
967
|
+
const globalTextByProps = [];
|
|
968
|
+
const subTimings = {};
|
|
969
|
+
|
|
970
|
+
const trackPhase = makePhaseTracker(console, debug);
|
|
971
|
+
const overallPhase = trackPhase('collectTextsByPage');
|
|
972
|
+
|
|
973
|
+
const stylesheetPrecompute = trackPhase('Stylesheet precompute');
|
|
974
|
+
const pageData = precomputeStylesheetsForPages(
|
|
975
|
+
htmlOrSvgAssets,
|
|
976
|
+
stylesheetRelsByFromAsset,
|
|
977
|
+
fontFaceDeclarationsByHtmlOrSvgAsset
|
|
978
|
+
);
|
|
979
|
+
subTimings['Stylesheet precompute'] = stylesheetPrecompute.end(
|
|
980
|
+
`${pageData.length} pages with fonts`
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
// Pages sharing the same CSS configuration produce identical font-tracer
|
|
984
|
+
// props, only text differs — so we trace one representative and fast-path
|
|
985
|
+
// the rest. With --dynamic every page is traced individually.
|
|
986
|
+
const { pagesNeedingFullTrace, fastPathPages, uniqueGroupCount } =
|
|
987
|
+
planTracing(pageData, Boolean(headlessBrowser));
|
|
988
|
+
|
|
989
|
+
// Always surface the per-page work breakdown so users can tell at a
|
|
990
|
+
// glance how much of the run is actual tracing vs cheap CSS-group
|
|
991
|
+
// reuse. The threshold matches createPageProgress's minTotal so it
|
|
992
|
+
// only appears on non-trivial runs.
|
|
993
|
+
if (console && pageData.length >= 5) {
|
|
1186
994
|
console.log(
|
|
1187
|
-
`
|
|
995
|
+
` ${pageData.length} pages with fonts: ${pagesNeedingFullTrace.length} to trace, ${fastPathPages.length} via cached CSS group (${uniqueGroupCount} unique groups)`
|
|
1188
996
|
);
|
|
997
|
+
}
|
|
1189
998
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
999
|
+
const tracingStart = Date.now();
|
|
1000
|
+
const fullTracing = trackPhase(
|
|
1001
|
+
`Full tracing (${pagesNeedingFullTrace.length} pages)`
|
|
1002
|
+
);
|
|
1003
|
+
try {
|
|
1004
|
+
await tracePages(pagesNeedingFullTrace, {
|
|
1005
|
+
headlessBrowser,
|
|
1006
|
+
concurrency,
|
|
1007
|
+
console,
|
|
1008
|
+
memoizedGetCssRulesByProperty,
|
|
1009
|
+
debug,
|
|
1010
|
+
});
|
|
1198
1011
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1012
|
+
subTimings['Full tracing'] = fullTracing.end();
|
|
1013
|
+
|
|
1014
|
+
const fastPathPhase = trackPhase('Fast-path extraction');
|
|
1015
|
+
const fastPathFallbacks = processFastPathPages(fastPathPages, {
|
|
1016
|
+
memoizedGetCssRulesByProperty,
|
|
1017
|
+
});
|
|
1018
|
+
subTimings['Fast-path extraction'] = fastPathPhase.end(
|
|
1019
|
+
`${fastPathPages.length} pages, ${fastPathFallbacks} fell back to full trace`
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
const assemblePhase = trackPhase('Result assembly');
|
|
1023
|
+
flattenTracedPagesIntoGlobal(
|
|
1024
|
+
pageData,
|
|
1025
|
+
htmlOrSvgAssetTextsWithProps,
|
|
1026
|
+
globalTextByProps
|
|
1027
|
+
);
|
|
1028
|
+
subTimings['Result assembly'] = assemblePhase.end();
|
|
1029
|
+
if (debug && console) {
|
|
1030
|
+
console.log(
|
|
1031
|
+
`[subfont timing] Total tracing+extraction+assembly: ${
|
|
1032
|
+
Date.now() - tracingStart
|
|
1033
|
+
}ms`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
} finally {
|
|
1037
|
+
if (headlessBrowser) {
|
|
1038
|
+
await headlessBrowser.close();
|
|
1203
1039
|
}
|
|
1204
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
const postProcessPhase = trackPhase('Post-processing total');
|
|
1043
|
+
const perPageLoopPhase = trackPhase('Per-page loop');
|
|
1044
|
+
const { snappingTime, globalUsageTime, cloningTime } = buildPerPageFontUsages(
|
|
1045
|
+
htmlOrSvgAssetTextsWithProps,
|
|
1046
|
+
globalTextByProps,
|
|
1047
|
+
text
|
|
1048
|
+
);
|
|
1049
|
+
subTimings['Per-page loop'] = perPageLoopPhase.end(
|
|
1050
|
+
`snapping: ${snappingTime}ms, globalUsage: ${globalUsageTime}ms, cloning: ${cloningTime}ms`
|
|
1051
|
+
);
|
|
1052
|
+
subTimings['Post-processing total'] = postProcessPhase.end();
|
|
1053
|
+
overallPhase.end();
|
|
1054
|
+
|
|
1055
|
+
stripSubfontTextNodes(fontFaceDeclarationsByHtmlOrSvgAsset);
|
|
1205
1056
|
return {
|
|
1206
1057
|
htmlOrSvgAssetTextsWithProps,
|
|
1207
1058
|
fontFaceDeclarationsByHtmlOrSvgAsset,
|
|
@@ -1210,9 +1061,3 @@ async function collectTextsByPage(
|
|
|
1210
1061
|
}
|
|
1211
1062
|
|
|
1212
1063
|
module.exports = collectTextsByPage;
|
|
1213
|
-
|
|
1214
|
-
// Exported for testing only
|
|
1215
|
-
module.exports._extractFeatureTagsFromDecl = extractFeatureTagsFromDecl;
|
|
1216
|
-
module.exports._resolveFeatureSettings = resolveFeatureSettings;
|
|
1217
|
-
module.exports._findFontFamiliesWithFeatureSettings =
|
|
1218
|
-
findFontFamiliesWithFeatureSettings;
|