@turntrout/subfont 1.7.0 → 1.8.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/CLAUDE.md +39 -13
- package/README.md +24 -14
- package/lib/FontTracerPool.d.ts +38 -0
- package/lib/FontTracerPool.d.ts.map +1 -0
- package/lib/FontTracerPool.js +230 -217
- package/lib/FontTracerPool.js.map +1 -0
- package/lib/HeadlessBrowser.d.ts +18 -0
- package/lib/HeadlessBrowser.d.ts.map +1 -0
- package/lib/HeadlessBrowser.js +216 -210
- package/lib/HeadlessBrowser.js.map +1 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +15 -12
- package/lib/cli.js.map +1 -0
- package/lib/codepointMaps.d.ts +4 -0
- package/lib/codepointMaps.d.ts.map +1 -0
- package/lib/codepointMaps.js +99 -0
- package/lib/codepointMaps.js.map +1 -0
- package/lib/collectFeatureGlyphIds.d.ts +3 -0
- package/lib/collectFeatureGlyphIds.d.ts.map +1 -0
- package/lib/collectFeatureGlyphIds.js +124 -138
- package/lib/collectFeatureGlyphIds.js.map +1 -0
- package/lib/collectTextsByPage.d.ts +41 -0
- package/lib/collectTextsByPage.d.ts.map +1 -0
- package/lib/collectTextsByPage.js +726 -965
- package/lib/collectTextsByPage.js.map +1 -0
- package/lib/concurrencyLimit.d.ts +3 -0
- package/lib/concurrencyLimit.d.ts.map +1 -0
- package/lib/concurrencyLimit.js +12 -11
- package/lib/concurrencyLimit.js.map +1 -0
- package/lib/escapeJsStringLiteral.d.ts +3 -0
- package/lib/escapeJsStringLiteral.d.ts.map +1 -0
- package/lib/escapeJsStringLiteral.js +7 -6
- package/lib/escapeJsStringLiteral.js.map +1 -0
- package/lib/extractReferencedCustomPropertyNames.d.ts +3 -0
- package/lib/extractReferencedCustomPropertyNames.d.ts.map +1 -0
- package/lib/extractReferencedCustomPropertyNames.js +15 -16
- package/lib/extractReferencedCustomPropertyNames.js.map +1 -0
- package/lib/extractVisibleText.d.ts +7 -0
- package/lib/extractVisibleText.d.ts.map +1 -0
- package/lib/extractVisibleText.js +110 -119
- package/lib/extractVisibleText.js.map +1 -0
- package/lib/findCustomPropertyDefinitions.d.ts +8 -0
- package/lib/findCustomPropertyDefinitions.d.ts.map +1 -0
- package/lib/findCustomPropertyDefinitions.js +41 -48
- package/lib/findCustomPropertyDefinitions.js.map +1 -0
- package/lib/fontConverter.d.ts +2 -0
- package/lib/fontConverter.d.ts.map +1 -0
- package/lib/fontConverter.js +40 -21
- package/lib/fontConverter.js.map +1 -0
- package/lib/fontConverterWorker.d.ts +2 -0
- package/lib/fontConverterWorker.d.ts.map +1 -0
- package/lib/fontConverterWorker.js +52 -15
- package/lib/fontConverterWorker.js.map +1 -0
- package/lib/fontFaceHelpers.d.ts +64 -0
- package/lib/fontFaceHelpers.d.ts.map +1 -0
- package/lib/fontFaceHelpers.js +237 -249
- package/lib/fontFaceHelpers.js.map +1 -0
- package/lib/fontFeatureHelpers.d.ts +30 -0
- package/lib/fontFeatureHelpers.d.ts.map +1 -0
- package/lib/fontFeatureHelpers.js +277 -212
- package/lib/fontFeatureHelpers.js.map +1 -0
- package/lib/fontTracerWorker.d.ts +11 -0
- package/lib/fontTracerWorker.d.ts.map +1 -0
- package/lib/fontTracerWorker.js +94 -60
- package/lib/fontTracerWorker.js.map +1 -0
- package/lib/gatherStylesheetsWithPredicates.d.ts +26 -0
- package/lib/gatherStylesheetsWithPredicates.d.ts.map +1 -0
- package/lib/gatherStylesheetsWithPredicates.js +75 -84
- package/lib/gatherStylesheetsWithPredicates.js.map +1 -0
- package/lib/getCssRulesByProperty.d.ts +29 -0
- package/lib/getCssRulesByProperty.d.ts.map +1 -0
- package/lib/getCssRulesByProperty.js +316 -316
- package/lib/getCssRulesByProperty.js.map +1 -0
- package/lib/getFontInfo.d.ts +11 -0
- package/lib/getFontInfo.d.ts.map +1 -0
- package/lib/getFontInfo.js +31 -33
- package/lib/getFontInfo.js.map +1 -0
- package/lib/initialValueByProp.d.ts +3 -0
- package/lib/initialValueByProp.d.ts.map +1 -0
- package/lib/initialValueByProp.js +20 -17
- package/lib/initialValueByProp.js.map +1 -0
- package/lib/injectSubsetDefinitions.d.ts +3 -0
- package/lib/injectSubsetDefinitions.d.ts.map +1 -0
- package/lib/injectSubsetDefinitions.js +55 -59
- package/lib/injectSubsetDefinitions.js.map +1 -0
- package/lib/normalizeFontPropertyValue.d.ts +3 -0
- package/lib/normalizeFontPropertyValue.d.ts.map +1 -0
- package/lib/normalizeFontPropertyValue.js +59 -54
- package/lib/normalizeFontPropertyValue.js.map +1 -0
- package/lib/parseCommandLineOptions.d.ts +9 -0
- package/lib/parseCommandLineOptions.d.ts.map +1 -0
- package/lib/parseCommandLineOptions.js +145 -149
- package/lib/parseCommandLineOptions.js.map +1 -0
- package/lib/parseFontVariationSettings.d.ts +3 -0
- package/lib/parseFontVariationSettings.d.ts.map +1 -0
- package/lib/parseFontVariationSettings.js +38 -36
- package/lib/parseFontVariationSettings.js.map +1 -0
- package/lib/progress.d.ts +27 -0
- package/lib/progress.d.ts.map +1 -0
- package/lib/progress.js +51 -54
- package/lib/progress.js.map +1 -0
- package/lib/sfntCache.d.ts +4 -0
- package/lib/sfntCache.d.ts.map +1 -0
- package/lib/sfntCache.js +67 -25
- package/lib/sfntCache.js.map +1 -0
- package/lib/stripLocalTokens.d.ts +3 -0
- package/lib/stripLocalTokens.d.ts.map +1 -0
- package/lib/stripLocalTokens.js +23 -21
- package/lib/stripLocalTokens.js.map +1 -0
- package/lib/subfont.d.ts +54 -0
- package/lib/subfont.d.ts.map +1 -0
- package/lib/subfont.js +531 -629
- package/lib/subfont.js.map +1 -0
- package/lib/subsetFontWithGlyphs.d.ts +21 -0
- package/lib/subsetFontWithGlyphs.d.ts.map +1 -0
- package/lib/subsetFontWithGlyphs.js +285 -259
- package/lib/subsetFontWithGlyphs.js.map +1 -0
- package/lib/subsetFonts.d.ts +55 -0
- package/lib/subsetFonts.d.ts.map +1 -0
- package/lib/subsetFonts.js +899 -1200
- package/lib/subsetFonts.js.map +1 -0
- package/lib/subsetGeneration.d.ts +36 -0
- package/lib/subsetGeneration.d.ts.map +1 -0
- package/lib/subsetGeneration.js +328 -325
- package/lib/subsetGeneration.js.map +1 -0
- package/lib/types/shared.d.ts +11 -0
- package/lib/types/shared.d.ts.map +1 -0
- package/lib/types/shared.js +3 -0
- package/lib/types/shared.js.map +1 -0
- package/lib/unicodeRange.d.ts +3 -0
- package/lib/unicodeRange.d.ts.map +1 -0
- package/lib/unicodeRange.js +17 -30
- package/lib/unicodeRange.js.map +1 -0
- package/lib/unquote.d.ts +3 -0
- package/lib/unquote.d.ts.map +1 -0
- package/lib/unquote.js +18 -25
- package/lib/unquote.js.map +1 -0
- package/lib/variationAxes.d.ts +33 -0
- package/lib/variationAxes.d.ts.map +1 -0
- package/lib/variationAxes.js +127 -157
- package/lib/variationAxes.js.map +1 -0
- package/lib/warnAboutMissingGlyphs.d.ts +43 -0
- package/lib/warnAboutMissingGlyphs.d.ts.map +1 -0
- package/lib/warnAboutMissingGlyphs.js +139 -147
- package/lib/warnAboutMissingGlyphs.js.map +1 -0
- package/lib/wasmQueue.d.ts +3 -0
- package/lib/wasmQueue.d.ts.map +1 -0
- package/lib/wasmQueue.js +13 -10
- package/lib/wasmQueue.js.map +1 -0
- package/package.json +12 -2
|
@@ -1,1063 +1,824 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
const memoizeSync = require("memoizesync");
|
|
36
|
+
const os = require("os");
|
|
37
|
+
const fontTracer = require("font-tracer");
|
|
38
|
+
const fontSnapper = require("font-snapper");
|
|
39
|
+
const HeadlessBrowser = require("./HeadlessBrowser");
|
|
40
|
+
const FontTracerPool = require("./FontTracerPool");
|
|
41
|
+
const gatherStylesheetsWithPredicates = require("./gatherStylesheetsWithPredicates");
|
|
42
|
+
const cssFontParser = __importStar(require("css-font-parser"));
|
|
43
|
+
const unquote = require("./unquote");
|
|
44
|
+
const normalizeFontPropertyValue = require("./normalizeFontPropertyValue");
|
|
45
|
+
const getCssRulesByProperty = require("./getCssRulesByProperty");
|
|
46
|
+
const extractVisibleText = require("./extractVisibleText");
|
|
47
|
+
const fontFaceHelpers_1 = require("./fontFaceHelpers");
|
|
48
|
+
const progress_1 = require("./progress");
|
|
49
|
+
const fontFeatureHelpers_1 = require("./fontFeatureHelpers");
|
|
50
|
+
const allInitialValues = require("./initialValueByProp");
|
|
51
|
+
const fontRelevantCssRegex = /font-family|font-weight|font-style|font-stretch|font-display|@font-face|font-variation|font-feature/i;
|
|
30
52
|
// The \s before style ensures we don't match data-style or similar.
|
|
31
|
-
const inlineFontStyleRegex =
|
|
32
|
-
/(?:^|\s)style\s*=\s*["'][^"']*\b(?:font-family|font-weight|font-style|font-stretch|font\s*:)/i;
|
|
53
|
+
const inlineFontStyleRegex = /(?:^|\s)style\s*=\s*["'][^"']*\b(?:font-family|font-weight|font-style|font-stretch|font\s*:)/i;
|
|
33
54
|
function hasInlineFontStyles(html) {
|
|
34
|
-
|
|
55
|
+
return inlineFontStyleRegex.test(html);
|
|
35
56
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
const fontFaceTraversalTypes = new Set([
|
|
58
|
+
'HtmlStyle',
|
|
59
|
+
'SvgStyle',
|
|
60
|
+
'CssImport',
|
|
61
|
+
]);
|
|
39
62
|
// Minimum number of pages that justifies spawning a worker pool (below this
|
|
40
63
|
// the overhead of worker thread startup exceeds the parallelism benefit).
|
|
41
64
|
const MIN_PAGES_FOR_WORKER_POOL = 4;
|
|
42
|
-
|
|
43
|
-
const {
|
|
44
|
-
findFontFamiliesWithFeatureSettings,
|
|
45
|
-
resolveFeatureSettings,
|
|
46
|
-
} = require('./fontFeatureHelpers');
|
|
47
|
-
|
|
48
|
-
const allInitialValues = require('./initialValueByProp');
|
|
49
65
|
const initialValueByProp = {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
'font-style': allInitialValues['font-style'],
|
|
67
|
+
'font-weight': allInitialValues['font-weight'],
|
|
68
|
+
'font-stretch': allInitialValues['font-stretch'],
|
|
53
69
|
};
|
|
54
|
-
|
|
55
70
|
// Null byte delimiter is collision-safe — CSS property values cannot contain \0.
|
|
56
71
|
function fontPropsKey(family, weight, style, stretch) {
|
|
57
|
-
|
|
72
|
+
return `${family}\0${weight}\0${style}\0${stretch}`;
|
|
58
73
|
}
|
|
59
|
-
|
|
60
74
|
const declKeyCache = new WeakMap();
|
|
61
75
|
function getDeclarationsKey(declarations) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
declKeyCache.set(declarations, key);
|
|
74
|
-
return key;
|
|
76
|
+
const cached = declKeyCache.get(declarations);
|
|
77
|
+
if (cached !== undefined)
|
|
78
|
+
return cached;
|
|
79
|
+
const key = JSON.stringify(declarations.map((d) => [
|
|
80
|
+
d['font-family'],
|
|
81
|
+
d['font-style'],
|
|
82
|
+
d['font-weight'],
|
|
83
|
+
d['font-stretch'],
|
|
84
|
+
]));
|
|
85
|
+
declKeyCache.set(declarations, key);
|
|
86
|
+
return key;
|
|
75
87
|
}
|
|
76
|
-
|
|
77
88
|
// Snap each globalTextByProps entry against font-face declarations
|
|
78
89
|
// to determine which font URL and properties each text segment maps to.
|
|
79
90
|
function computeSnappedGlobalEntries(declarations, globalTextByProps) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const propsKey = fontPropsKey(
|
|
94
|
-
family,
|
|
95
|
-
textAndProps.props['font-weight'] || '',
|
|
96
|
-
textAndProps.props['font-style'] || '',
|
|
97
|
-
textAndProps.props['font-stretch'] || ''
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
let snappedResults = snappingResultCache.get(propsKey);
|
|
101
|
-
if (!snappedResults) {
|
|
102
|
-
snappedResults = [];
|
|
103
|
-
const families = cssFontParser
|
|
104
|
-
.parseFontFamily(family)
|
|
105
|
-
.filter((fam) =>
|
|
106
|
-
declarations.some(
|
|
107
|
-
(fontFace) =>
|
|
108
|
-
fontFace['font-family'].toLowerCase() === fam.toLowerCase()
|
|
109
|
-
)
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
for (const fam of families) {
|
|
113
|
-
const activeFontFaceDeclaration = fontSnapper(declarations, {
|
|
114
|
-
...textAndProps.props,
|
|
115
|
-
'font-family': stringifyFontFamily(fam),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
if (!activeFontFaceDeclaration) {
|
|
119
|
-
continue;
|
|
91
|
+
const entries = [];
|
|
92
|
+
// Cache snapping results per unique props key within this declarations
|
|
93
|
+
// set. Many globalTextByProps entries share the same font properties
|
|
94
|
+
// (only text differs), so we avoid redundant fontSnapper + family
|
|
95
|
+
// parsing calls.
|
|
96
|
+
const snappingResultCache = new Map();
|
|
97
|
+
for (const textAndProps of globalTextByProps) {
|
|
98
|
+
const family = textAndProps.props['font-family'];
|
|
99
|
+
if (family === undefined) {
|
|
100
|
+
continue;
|
|
120
101
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
102
|
+
const propsKey = fontPropsKey(family, textAndProps.props['font-weight'] || '', textAndProps.props['font-style'] || '', textAndProps.props['font-stretch'] || '');
|
|
103
|
+
let snappedResults = snappingResultCache.get(propsKey);
|
|
104
|
+
if (!snappedResults) {
|
|
105
|
+
snappedResults = [];
|
|
106
|
+
const families = cssFontParser.parseFontFamily(family).filter((fam) => declarations.some((fontFace) => {
|
|
107
|
+
// collectFontFaceDeclarations only retains rows with a non-
|
|
108
|
+
// empty font-family, but the field is optional in the type.
|
|
109
|
+
const ffName = fontFace['font-family'];
|
|
110
|
+
return (typeof ffName === 'string' &&
|
|
111
|
+
ffName.toLowerCase() === fam.toLowerCase());
|
|
112
|
+
}));
|
|
113
|
+
for (const fam of families) {
|
|
114
|
+
const activeFontFaceDeclaration = fontSnapper(declarations, {
|
|
115
|
+
...textAndProps.props,
|
|
116
|
+
'font-family': (0, fontFaceHelpers_1.stringifyFontFamily)(fam),
|
|
117
|
+
});
|
|
118
|
+
if (!activeFontFaceDeclaration) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Drop relations + the CSS-injected -subfont-text descriptor before
|
|
122
|
+
// forwarding the rest of the props downstream. The leading-underscore
|
|
123
|
+
// name signals "intentionally unused" to eslint.
|
|
124
|
+
const { relations, '-subfont-text': _subfontText, ...props } = activeFontFaceDeclaration;
|
|
125
|
+
const fontUrl = (0, fontFaceHelpers_1.getPreferredFontUrl)(relations);
|
|
126
|
+
if (!fontUrl) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
let fontWeight = normalizeFontPropertyValue('font-weight', textAndProps.props['font-weight']);
|
|
130
|
+
if (fontWeight === 'normal') {
|
|
131
|
+
fontWeight = 400;
|
|
132
|
+
}
|
|
133
|
+
snappedResults.push({
|
|
134
|
+
fontUrl,
|
|
135
|
+
props: props,
|
|
136
|
+
fontRelations: relations,
|
|
137
|
+
fontStyle: normalizeFontPropertyValue('font-style', textAndProps.props['font-style']),
|
|
138
|
+
fontWeight,
|
|
139
|
+
fontStretch: normalizeFontPropertyValue('font-stretch', textAndProps.props['font-stretch']),
|
|
140
|
+
textAndProps,
|
|
141
|
+
fontVariationSettings: textAndProps.props['font-variation-settings'],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
snappingResultCache.set(propsKey, snappedResults);
|
|
130
145
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
fontWeight = 400;
|
|
146
|
+
for (const snapped of snappedResults) {
|
|
147
|
+
entries.push({
|
|
148
|
+
...snapped,
|
|
149
|
+
textAndProps,
|
|
150
|
+
fontVariationSettings: textAndProps.props['font-variation-settings'],
|
|
151
|
+
});
|
|
138
152
|
}
|
|
139
|
-
|
|
140
|
-
snappedResults.push({
|
|
141
|
-
fontUrl,
|
|
142
|
-
props,
|
|
143
|
-
fontRelations: relations,
|
|
144
|
-
fontStyle: normalizeFontPropertyValue(
|
|
145
|
-
'font-style',
|
|
146
|
-
textAndProps.props['font-style']
|
|
147
|
-
),
|
|
148
|
-
fontWeight,
|
|
149
|
-
fontStretch: normalizeFontPropertyValue(
|
|
150
|
-
'font-stretch',
|
|
151
|
-
textAndProps.props['font-stretch']
|
|
152
|
-
),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
snappingResultCache.set(propsKey, snappedResults);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (const snapped of snappedResults) {
|
|
159
|
-
entries.push({
|
|
160
|
-
textAndProps,
|
|
161
|
-
...snapped,
|
|
162
|
-
fontVariationSettings: textAndProps.props['font-variation-settings'],
|
|
163
|
-
});
|
|
164
153
|
}
|
|
165
|
-
|
|
166
|
-
return entries;
|
|
154
|
+
return entries;
|
|
167
155
|
}
|
|
168
|
-
|
|
169
156
|
// Fill in fontUsageTemplates/pageTextIndex/preloadIndex on the cached
|
|
170
157
|
// declarations entry. No-op on repeat calls — results are shared across
|
|
171
158
|
// pages that resolve to the same @font-face set.
|
|
172
|
-
function populateGlobalFontUsages(
|
|
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
|
-
arr = { texts: [], props, fontRelations: relations };
|
|
233
|
-
extraTextsByFontUrl.set(fontUrl, arr);
|
|
234
|
-
}
|
|
235
|
-
arr.texts.push(...extras);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Build the global fontUsage template for each fontUrl
|
|
240
|
-
const fontUsageTemplates = [];
|
|
241
|
-
const allFontUrls = new Set([
|
|
242
|
-
...entriesByFontUrl.keys(),
|
|
243
|
-
...extraTextsByFontUrl.keys(),
|
|
244
|
-
]);
|
|
245
|
-
|
|
246
|
-
for (const fontUrl of allFontUrls) {
|
|
247
|
-
const fontEntries = entriesByFontUrl.get(fontUrl) || [];
|
|
248
|
-
const extra = extraTextsByFontUrl.get(fontUrl);
|
|
249
|
-
|
|
250
|
-
// Collect all texts (extras first, then global entries)
|
|
251
|
-
const allTexts = [];
|
|
252
|
-
if (extra) {
|
|
253
|
-
allTexts.push(...extra.texts);
|
|
254
|
-
}
|
|
255
|
-
for (const e of fontEntries) {
|
|
256
|
-
allTexts.push(e.textAndProps.text);
|
|
159
|
+
function populateGlobalFontUsages(cached, accumulatedFontFaceDeclarations, text) {
|
|
160
|
+
if (cached.fontUsageTemplates) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const snappedGlobalEntries = cached.snappedEntries;
|
|
164
|
+
const pageTextIndex = new Map();
|
|
165
|
+
const entriesByFontUrl = new Map();
|
|
166
|
+
const textAndPropsToFontUrl = new Map();
|
|
167
|
+
for (const entry of snappedGlobalEntries) {
|
|
168
|
+
if (!entry.fontUrl)
|
|
169
|
+
continue;
|
|
170
|
+
// flattenTracedPagesIntoGlobal stamps htmlOrSvgAsset onto every
|
|
171
|
+
// textByProps entry before this runs, so the field is guaranteed.
|
|
172
|
+
const asset = entry.textAndProps.htmlOrSvgAsset;
|
|
173
|
+
let assetMap = pageTextIndex.get(asset);
|
|
174
|
+
if (!assetMap) {
|
|
175
|
+
assetMap = new Map();
|
|
176
|
+
pageTextIndex.set(asset, assetMap);
|
|
177
|
+
}
|
|
178
|
+
let texts = assetMap.get(entry.fontUrl);
|
|
179
|
+
if (!texts) {
|
|
180
|
+
texts = [];
|
|
181
|
+
assetMap.set(entry.fontUrl, texts);
|
|
182
|
+
}
|
|
183
|
+
texts.push(entry.textAndProps.text);
|
|
184
|
+
let arr = entriesByFontUrl.get(entry.fontUrl);
|
|
185
|
+
if (!arr) {
|
|
186
|
+
arr = [];
|
|
187
|
+
entriesByFontUrl.set(entry.fontUrl, arr);
|
|
188
|
+
}
|
|
189
|
+
arr.push(entry);
|
|
190
|
+
textAndPropsToFontUrl.set(entry.textAndProps, entry.fontUrl);
|
|
191
|
+
}
|
|
192
|
+
const extraTextsByFontUrl = new Map();
|
|
193
|
+
for (const fontFaceDeclaration of accumulatedFontFaceDeclarations) {
|
|
194
|
+
const { relations, '-subfont-text': subfontText, ...props } = fontFaceDeclaration;
|
|
195
|
+
const fontUrl = (0, fontFaceHelpers_1.getPreferredFontUrl)(relations);
|
|
196
|
+
if (!fontUrl)
|
|
197
|
+
continue;
|
|
198
|
+
const extras = [];
|
|
199
|
+
if (subfontText !== undefined) {
|
|
200
|
+
extras.push(unquote(subfontText));
|
|
201
|
+
}
|
|
202
|
+
if (text !== undefined) {
|
|
203
|
+
extras.push(text);
|
|
204
|
+
}
|
|
205
|
+
if (extras.length > 0) {
|
|
206
|
+
let arr = extraTextsByFontUrl.get(fontUrl);
|
|
207
|
+
if (!arr) {
|
|
208
|
+
// After destructuring out `relations` and `-subfont-text`, the
|
|
209
|
+
// remaining spread props are CSS descriptor strings.
|
|
210
|
+
arr = {
|
|
211
|
+
texts: [],
|
|
212
|
+
props: props,
|
|
213
|
+
fontRelations: relations,
|
|
214
|
+
};
|
|
215
|
+
extraTextsByFontUrl.set(fontUrl, arr);
|
|
216
|
+
}
|
|
217
|
+
arr.texts.push(...extras);
|
|
218
|
+
}
|
|
257
219
|
}
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
: extra.fontRelations;
|
|
275
|
-
let smallestOriginalSize = 0;
|
|
276
|
-
// undefined is fine here — only used for display/logging, never in arithmetic
|
|
277
|
-
let smallestOriginalFormat;
|
|
278
|
-
for (const relation of fontRelations) {
|
|
279
|
-
if (relation.to.isLoaded) {
|
|
280
|
-
const size = relation.to.rawSrc.length;
|
|
281
|
-
if (smallestOriginalSize === 0 || size < smallestOriginalSize) {
|
|
282
|
-
smallestOriginalSize = size;
|
|
283
|
-
smallestOriginalFormat = relation.to.type.toLowerCase();
|
|
220
|
+
// Build the global fontUsage template for each fontUrl
|
|
221
|
+
const fontUsageTemplates = [];
|
|
222
|
+
const allFontUrls = new Set([
|
|
223
|
+
...entriesByFontUrl.keys(),
|
|
224
|
+
...extraTextsByFontUrl.keys(),
|
|
225
|
+
]);
|
|
226
|
+
for (const fontUrl of allFontUrls) {
|
|
227
|
+
const fontEntries = entriesByFontUrl.get(fontUrl) || [];
|
|
228
|
+
const extra = extraTextsByFontUrl.get(fontUrl);
|
|
229
|
+
// Collect all texts (extras first, then global entries)
|
|
230
|
+
const allTexts = [];
|
|
231
|
+
if (extra) {
|
|
232
|
+
allTexts.push(...extra.texts);
|
|
233
|
+
}
|
|
234
|
+
for (const e of fontEntries) {
|
|
235
|
+
allTexts.push(e.textAndProps.text);
|
|
284
236
|
}
|
|
285
|
-
|
|
237
|
+
const fontFamilies = new Set(fontEntries.map((e) => e.props['font-family']));
|
|
238
|
+
const fontStyles = new Set(fontEntries.map((e) => e.fontStyle));
|
|
239
|
+
const fontWeights = new Set(fontEntries.map((e) => e.fontWeight));
|
|
240
|
+
const fontStretches = new Set(fontEntries.map((e) => e.fontStretch));
|
|
241
|
+
const fontVariationSettings = new Set(fontEntries
|
|
242
|
+
.map((e) => e.fontVariationSettings)
|
|
243
|
+
.filter((fvs) => typeof fvs === 'string' && fvs.toLowerCase() !== 'normal'));
|
|
244
|
+
// Use first entry's relations for size computation, or extra's if no entries
|
|
245
|
+
const fontRelations = fontEntries.length > 0
|
|
246
|
+
? fontEntries[0].fontRelations
|
|
247
|
+
: extra.fontRelations;
|
|
248
|
+
let smallestOriginalSize = 0;
|
|
249
|
+
let smallestOriginalFormat;
|
|
250
|
+
for (const relation of fontRelations) {
|
|
251
|
+
if (relation.to.isLoaded) {
|
|
252
|
+
const size = relation.to.rawSrc.length;
|
|
253
|
+
if (smallestOriginalSize === 0 || size < smallestOriginalSize) {
|
|
254
|
+
smallestOriginalSize = size;
|
|
255
|
+
smallestOriginalFormat = relation.to.type?.toLowerCase();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const props = fontEntries.length > 0
|
|
260
|
+
? { ...fontEntries[0].props }
|
|
261
|
+
: { ...extra.props };
|
|
262
|
+
const extraTextsStr = extra ? extra.texts.join('') : '';
|
|
263
|
+
fontUsageTemplates.push({
|
|
264
|
+
smallestOriginalSize,
|
|
265
|
+
smallestOriginalFormat,
|
|
266
|
+
texts: allTexts,
|
|
267
|
+
text: (0, fontFaceHelpers_1.uniqueCharsFromArray)(allTexts),
|
|
268
|
+
extraTextsStr,
|
|
269
|
+
props,
|
|
270
|
+
fontUrl,
|
|
271
|
+
fontFamilies,
|
|
272
|
+
fontStyles,
|
|
273
|
+
fontStretches,
|
|
274
|
+
fontWeights,
|
|
275
|
+
fontVariationSettings,
|
|
276
|
+
});
|
|
286
277
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const extraTextsStr = extra ? extra.texts.join('') : '';
|
|
291
|
-
|
|
292
|
-
fontUsageTemplates.push({
|
|
293
|
-
smallestOriginalSize,
|
|
294
|
-
smallestOriginalFormat,
|
|
295
|
-
texts: allTexts,
|
|
296
|
-
text: uniqueCharsFromArray(allTexts),
|
|
297
|
-
extraTextsStr,
|
|
298
|
-
props,
|
|
299
|
-
fontUrl,
|
|
300
|
-
fontFamilies,
|
|
301
|
-
fontStyles,
|
|
302
|
-
fontStretches,
|
|
303
|
-
fontWeights,
|
|
304
|
-
fontVariationSettings,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
cached.fontUsageTemplates = fontUsageTemplates;
|
|
309
|
-
cached.pageTextIndex = pageTextIndex;
|
|
310
|
-
cached.preloadIndex = textAndPropsToFontUrl;
|
|
278
|
+
cached.fontUsageTemplates = fontUsageTemplates;
|
|
279
|
+
cached.pageTextIndex = pageTextIndex;
|
|
280
|
+
cached.preloadIndex = textAndPropsToFontUrl;
|
|
311
281
|
}
|
|
312
|
-
|
|
313
282
|
// Trace fonts across the given pages. Uses a worker pool when the workload
|
|
314
283
|
// justifies the thread-startup overhead; otherwise falls back to sequential
|
|
315
284
|
// in-process tracing (required when a HeadlessBrowser is driving things).
|
|
316
|
-
async function tracePages(
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
285
|
+
async function tracePages(pagesNeedingFullTrace, { headlessBrowser, concurrency, console, memoizedGetCssRulesByProperty, debug = false, }) {
|
|
286
|
+
const totalPages = pagesNeedingFullTrace.length;
|
|
287
|
+
if (totalPages === 0)
|
|
288
|
+
return;
|
|
289
|
+
const useWorkerPool = !headlessBrowser && totalPages >= MIN_PAGES_FOR_WORKER_POOL;
|
|
290
|
+
const progress = (0, progress_1.createPageProgress)({
|
|
291
|
+
total: totalPages,
|
|
292
|
+
console,
|
|
293
|
+
label: 'Tracing fonts',
|
|
294
|
+
});
|
|
295
|
+
if (useWorkerPool) {
|
|
296
|
+
const maxWorkers = concurrency && concurrency > 0
|
|
297
|
+
? concurrency
|
|
298
|
+
: Math.min(os.cpus().length, 8);
|
|
299
|
+
const numWorkers = Math.min(maxWorkers, totalPages);
|
|
300
|
+
const pool = new FontTracerPool(numWorkers);
|
|
301
|
+
await pool.init();
|
|
302
|
+
try {
|
|
303
|
+
progress.banner(` Tracing fonts across ${totalPages} pages using ${numWorkers} worker${numWorkers === 1 ? '' : 's'}...`);
|
|
304
|
+
await Promise.all(pagesNeedingFullTrace.map(async (pd) => {
|
|
305
|
+
const pageStart = debug ? Date.now() : 0;
|
|
306
|
+
try {
|
|
307
|
+
pd.textByProps = (await pool.trace(pd.htmlOrSvgAsset.text || '', pd.stylesheetsWithPredicates));
|
|
308
|
+
}
|
|
309
|
+
catch (rawErr) {
|
|
310
|
+
const err = rawErr;
|
|
311
|
+
if (console) {
|
|
312
|
+
console.warn(`Worker fontTracer failed for ${pd.htmlOrSvgAsset.url}, falling back to main thread: ${err.message}`);
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
pd.textByProps = fontTracer(pd.htmlOrSvgAsset.parseTree, {
|
|
316
|
+
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
317
|
+
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
318
|
+
asset: pd.htmlOrSvgAsset,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
catch (fallbackErr) {
|
|
322
|
+
const fbErr = fallbackErr;
|
|
323
|
+
throw new Error(`fontTracer failed for ${pd.htmlOrSvgAsset.url} in both worker and main thread: ${fbErr.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const idx = progress.tick();
|
|
327
|
+
(0, progress_1.logTracedPage)(console, debug, idx, totalPages, pd.htmlOrSvgAsset, pageStart);
|
|
328
|
+
}));
|
|
329
|
+
progress.done();
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
await pool.destroy();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
progress.banner(` Tracing fonts across ${totalPages} page${totalPages === 1 ? '' : 's'} (single-threaded${headlessBrowser ? ' + headless browser' : ''})...`);
|
|
337
|
+
for (let pi = 0; pi < totalPages; pi++) {
|
|
338
|
+
const pd = pagesNeedingFullTrace[pi];
|
|
339
|
+
const pageStart = debug ? Date.now() : 0;
|
|
363
340
|
pd.textByProps = fontTracer(pd.htmlOrSvgAsset.parseTree, {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
341
|
+
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
342
|
+
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
343
|
+
asset: pd.htmlOrSvgAsset,
|
|
367
344
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
progress.done();
|
|
381
|
-
} finally {
|
|
382
|
-
await pool.destroy();
|
|
383
|
-
}
|
|
384
|
-
} else {
|
|
385
|
-
progress.banner(
|
|
386
|
-
` Tracing fonts across ${totalPages} page${totalPages === 1 ? '' : 's'} (single-threaded${headlessBrowser ? ' + headless browser' : ''})...`
|
|
387
|
-
);
|
|
388
|
-
for (let pi = 0; pi < totalPages; pi++) {
|
|
389
|
-
const pd = pagesNeedingFullTrace[pi];
|
|
390
|
-
const pageStart = debug ? Date.now() : 0;
|
|
391
|
-
pd.textByProps = fontTracer(pd.htmlOrSvgAsset.parseTree, {
|
|
392
|
-
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
393
|
-
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
394
|
-
asset: pd.htmlOrSvgAsset,
|
|
395
|
-
});
|
|
396
|
-
if (headlessBrowser) {
|
|
397
|
-
pd.textByProps.push(
|
|
398
|
-
...(await headlessBrowser.tracePage(pd.htmlOrSvgAsset))
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
const idx = progress.tick();
|
|
402
|
-
logTracedPage(
|
|
403
|
-
console,
|
|
404
|
-
debug,
|
|
405
|
-
idx,
|
|
406
|
-
totalPages,
|
|
407
|
-
pd.htmlOrSvgAsset,
|
|
408
|
-
pageStart
|
|
409
|
-
);
|
|
345
|
+
if (headlessBrowser) {
|
|
346
|
+
// HeadlessBrowser returns puppeteer-shaped trace results that
|
|
347
|
+
// share text + props with TextByPropsEntry but are typed as
|
|
348
|
+
// Record<string, unknown> at the seam.
|
|
349
|
+
const dynamicResults = await headlessBrowser.tracePage(pd.htmlOrSvgAsset);
|
|
350
|
+
pd.textByProps.push(...dynamicResults // eslint-disable-line no-restricted-syntax
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const idx = progress.tick();
|
|
354
|
+
(0, progress_1.logTracedPage)(console, debug, idx, totalPages, pd.htmlOrSvgAsset, pageStart);
|
|
355
|
+
}
|
|
356
|
+
progress.done();
|
|
410
357
|
}
|
|
411
|
-
progress.done();
|
|
412
|
-
}
|
|
413
358
|
}
|
|
414
|
-
|
|
415
359
|
// For each page that shares a representative's CSS configuration, copy the
|
|
416
360
|
// representative's font-variant props and overlay this page's visible text.
|
|
417
361
|
// Returns the number of pages that had to fall back to a full trace
|
|
418
362
|
// (because inline style attributes made the fast path unsafe).
|
|
419
|
-
function processFastPathPages(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
textPerPropsKey.get(propsKey).push(entry.text);
|
|
448
|
-
if (family) {
|
|
449
|
-
const weight = entry.props['font-weight'] || 'normal';
|
|
450
|
-
const style = entry.props['font-style'] || 'normal';
|
|
451
|
-
const stretch = entry.props['font-stretch'] || 'normal';
|
|
452
|
-
for (const fam of cssFontParser.parseFontFamily(family)) {
|
|
453
|
-
seenVariantKeys.add(
|
|
454
|
-
fontPropsKey(fam.toLowerCase(), weight, style, stretch)
|
|
455
|
-
);
|
|
363
|
+
function processFastPathPages(fastPathPages, { memoizedGetCssRulesByProperty }) {
|
|
364
|
+
if (fastPathPages.length === 0)
|
|
365
|
+
return 0;
|
|
366
|
+
const repDataCache = new Map();
|
|
367
|
+
function getRepData(representativePd) {
|
|
368
|
+
const cachedRep = repDataCache.get(representativePd);
|
|
369
|
+
if (cachedRep)
|
|
370
|
+
return cachedRep;
|
|
371
|
+
const repTextByProps = representativePd.textByProps ?? [];
|
|
372
|
+
const uniquePropsMap = new Map();
|
|
373
|
+
const textPerPropsKey = new Map();
|
|
374
|
+
const seenVariantKeys = new Set();
|
|
375
|
+
for (const entry of repTextByProps) {
|
|
376
|
+
const family = entry.props['font-family'] || '';
|
|
377
|
+
const propsKey = fontPropsKey(family, entry.props['font-weight'] || '', entry.props['font-style'] || '', entry.props['font-stretch'] || '');
|
|
378
|
+
if (!uniquePropsMap.has(propsKey)) {
|
|
379
|
+
uniquePropsMap.set(propsKey, entry.props);
|
|
380
|
+
textPerPropsKey.set(propsKey, []);
|
|
381
|
+
}
|
|
382
|
+
textPerPropsKey.get(propsKey).push(entry.text);
|
|
383
|
+
if (family) {
|
|
384
|
+
const weight = entry.props['font-weight'] || 'normal';
|
|
385
|
+
const style = entry.props['font-style'] || 'normal';
|
|
386
|
+
const stretch = entry.props['font-stretch'] || 'normal';
|
|
387
|
+
for (const fam of cssFontParser.parseFontFamily(family)) {
|
|
388
|
+
seenVariantKeys.add(fontPropsKey(fam.toLowerCase(), weight, style, stretch));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
456
391
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
392
|
+
const data = { uniquePropsMap, textPerPropsKey, seenVariantKeys };
|
|
393
|
+
repDataCache.set(representativePd, data);
|
|
394
|
+
return data;
|
|
395
|
+
}
|
|
396
|
+
let fastPathFallbacks = 0;
|
|
397
|
+
for (const pd of fastPathPages) {
|
|
398
|
+
if (hasInlineFontStyles(pd.htmlOrSvgAsset.text || '')) {
|
|
399
|
+
fastPathFallbacks++;
|
|
400
|
+
pd.textByProps = fontTracer(pd.htmlOrSvgAsset.parseTree, {
|
|
401
|
+
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
402
|
+
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
403
|
+
asset: pd.htmlOrSvgAsset,
|
|
404
|
+
});
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const { uniquePropsMap, textPerPropsKey, seenVariantKeys } = getRepData(pd.representativePd);
|
|
408
|
+
// Check if any @font-face variants are unseen by the representative.
|
|
409
|
+
// Only copy Maps when extensions are actually needed.
|
|
410
|
+
let effectivePropsMap = uniquePropsMap;
|
|
411
|
+
let effectiveTextPerPropsKey = textPerPropsKey;
|
|
412
|
+
for (const decl of pd.accumulatedFontFaceDeclarations) {
|
|
413
|
+
const family = decl['font-family'];
|
|
414
|
+
if (!family)
|
|
415
|
+
continue;
|
|
416
|
+
const weight = decl['font-weight'] || 'normal';
|
|
417
|
+
const style = decl['font-style'] || 'normal';
|
|
418
|
+
const stretch = decl['font-stretch'] || 'normal';
|
|
419
|
+
const variantKey = fontPropsKey(family.toLowerCase(), weight, style, stretch);
|
|
420
|
+
if (!seenVariantKeys.has(variantKey)) {
|
|
421
|
+
// Lazy-copy on first unseen variant
|
|
422
|
+
if (effectivePropsMap === uniquePropsMap) {
|
|
423
|
+
effectivePropsMap = new Map(uniquePropsMap);
|
|
424
|
+
effectiveTextPerPropsKey = new Map(textPerPropsKey);
|
|
425
|
+
}
|
|
426
|
+
const propsKey = fontPropsKey((0, fontFaceHelpers_1.stringifyFontFamily)(family), weight, style, stretch);
|
|
427
|
+
if (!effectivePropsMap.has(propsKey)) {
|
|
428
|
+
effectivePropsMap.set(propsKey, {
|
|
429
|
+
'font-family': (0, fontFaceHelpers_1.stringifyFontFamily)(family),
|
|
430
|
+
'font-weight': weight,
|
|
431
|
+
'font-style': style,
|
|
432
|
+
'font-stretch': stretch,
|
|
433
|
+
});
|
|
434
|
+
effectiveTextPerPropsKey.set(propsKey, []);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
501
437
|
}
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
'font-family': stringifyFontFamily(family),
|
|
511
|
-
'font-weight': weight,
|
|
512
|
-
'font-style': style,
|
|
513
|
-
'font-stretch': stretch,
|
|
514
|
-
});
|
|
515
|
-
effectiveTextPerPropsKey.set(propsKey, []);
|
|
438
|
+
const pageText = extractVisibleText(pd.htmlOrSvgAsset.text || '');
|
|
439
|
+
pd.textByProps = [];
|
|
440
|
+
for (const [propsKey, props] of effectivePropsMap) {
|
|
441
|
+
const repTexts = effectiveTextPerPropsKey.get(propsKey) || [];
|
|
442
|
+
pd.textByProps.push({
|
|
443
|
+
text: pageText + repTexts.join(''),
|
|
444
|
+
props: { ...props },
|
|
445
|
+
});
|
|
516
446
|
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const pageText = extractVisibleText(pd.htmlOrSvgAsset.text || '');
|
|
521
|
-
|
|
522
|
-
pd.textByProps = [];
|
|
523
|
-
for (const [propsKey, props] of effectivePropsMap) {
|
|
524
|
-
const repTexts = effectiveTextPerPropsKey.get(propsKey) || [];
|
|
525
|
-
pd.textByProps.push({
|
|
526
|
-
text: pageText + repTexts.join(''),
|
|
527
|
-
props: { ...props },
|
|
528
|
-
});
|
|
529
447
|
}
|
|
530
|
-
|
|
531
|
-
return fastPathFallbacks;
|
|
448
|
+
return fastPathFallbacks;
|
|
532
449
|
}
|
|
533
|
-
|
|
534
450
|
// Pre-build an index of stylesheet-related relations by source asset
|
|
535
451
|
// to avoid repeated assetGraph.findRelations scans (O(allRelations) each).
|
|
536
452
|
const STYLESHEET_REL_TYPES = [
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
453
|
+
'HtmlStyle',
|
|
454
|
+
'SvgStyle',
|
|
455
|
+
'CssImport',
|
|
456
|
+
'HtmlConditionalComment',
|
|
457
|
+
'HtmlNoscript',
|
|
542
458
|
];
|
|
543
|
-
|
|
544
459
|
function indexStylesheetRelations(assetGraph) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
460
|
+
const byFromAsset = new Map();
|
|
461
|
+
for (const relation of assetGraph.findRelations({
|
|
462
|
+
type: { $in: STYLESHEET_REL_TYPES },
|
|
463
|
+
})) {
|
|
464
|
+
let arr = byFromAsset.get(relation.from);
|
|
465
|
+
if (!arr) {
|
|
466
|
+
arr = [];
|
|
467
|
+
byFromAsset.set(relation.from, arr);
|
|
468
|
+
}
|
|
469
|
+
arr.push(relation);
|
|
553
470
|
}
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
return byFromAsset;
|
|
471
|
+
return byFromAsset;
|
|
557
472
|
}
|
|
558
|
-
|
|
559
473
|
// Build a cache key by traversing stylesheet relations, capturing
|
|
560
474
|
// both asset identity and relation context (media, conditionalComment,
|
|
561
475
|
// noscript) that affect gatherStylesheetsWithPredicates output.
|
|
562
|
-
function buildStylesheetKey(
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
)
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
476
|
+
function buildStylesheetKey(htmlOrSvgAsset, skipNonFontInlineCss, stylesheetRelsByFromAsset) {
|
|
477
|
+
const keyParts = [];
|
|
478
|
+
const visited = new Set();
|
|
479
|
+
(function traverse(asset, isNoscript) {
|
|
480
|
+
if (visited.has(asset))
|
|
481
|
+
return;
|
|
482
|
+
if (!asset.isLoaded)
|
|
483
|
+
return;
|
|
484
|
+
visited.add(asset);
|
|
485
|
+
for (const relation of stylesheetRelsByFromAsset.get(asset) || []) {
|
|
486
|
+
if (relation.type === 'HtmlNoscript') {
|
|
487
|
+
traverse(relation.to, true);
|
|
488
|
+
}
|
|
489
|
+
else if (relation.type === 'HtmlConditionalComment') {
|
|
490
|
+
keyParts.push(`cc:${relation.condition}`);
|
|
491
|
+
traverse(relation.to, isNoscript);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
const target = relation.to;
|
|
495
|
+
if (skipNonFontInlineCss &&
|
|
496
|
+
target.isInline &&
|
|
497
|
+
target.type === 'Css' &&
|
|
498
|
+
!fontRelevantCssRegex.test(target.text || '')) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const media = relation.media || '';
|
|
502
|
+
keyParts.push(`${target.id}:${media}:${isNoscript ? 'ns' : ''}`);
|
|
503
|
+
traverse(target, isNoscript);
|
|
504
|
+
}
|
|
588
505
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
traverse(target, isNoscript);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
})(htmlOrSvgAsset, false);
|
|
595
|
-
return keyParts.join('\x1d');
|
|
506
|
+
})(htmlOrSvgAsset, false);
|
|
507
|
+
return keyParts.join('\x1d');
|
|
596
508
|
}
|
|
597
|
-
|
|
598
509
|
// Walk the stylesheet graph rooted at htmlOrSvgAsset and collect every
|
|
599
510
|
// @font-face declaration into a flat list, preserving the CSS relation node
|
|
600
511
|
// so callers can correlate declarations back to their source rules.
|
|
601
|
-
function collectFontFaceDeclarations(
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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);
|
|
512
|
+
function collectFontFaceDeclarations(htmlOrSvgAsset, stylesheetRelsByFromAsset) {
|
|
513
|
+
const accumulatedFontFaceDeclarations = [];
|
|
514
|
+
const visitedAssets = new Set();
|
|
515
|
+
(function traverseForFontFace(asset) {
|
|
516
|
+
if (visitedAssets.has(asset))
|
|
517
|
+
return;
|
|
518
|
+
visitedAssets.add(asset);
|
|
519
|
+
if (asset.type === 'Css' && asset.isLoaded) {
|
|
520
|
+
const seenNodes = new Set();
|
|
521
|
+
const fontRelations = asset.outgoingRelations.filter((relation) => relation.type === 'CssFontFaceSrc');
|
|
522
|
+
for (const fontRelation of fontRelations) {
|
|
523
|
+
const node = fontRelation.node;
|
|
524
|
+
if (seenNodes.has(node))
|
|
525
|
+
continue;
|
|
526
|
+
seenNodes.add(node);
|
|
527
|
+
const fontFaceDeclaration = {
|
|
528
|
+
relations: fontRelations.filter((r) => r.node === node),
|
|
529
|
+
...initialValueByProp,
|
|
530
|
+
};
|
|
531
|
+
node.walkDecls((declaration) => {
|
|
532
|
+
const propName = declaration.prop.toLowerCase();
|
|
533
|
+
fontFaceDeclaration[propName] =
|
|
534
|
+
propName === 'font-family'
|
|
535
|
+
? cssFontParser.parseFontFamily(declaration.value)[0]
|
|
536
|
+
: declaration.value;
|
|
537
|
+
});
|
|
538
|
+
// Disregard incomplete @font-face declarations (must contain font-family and src per spec):
|
|
539
|
+
if (fontFaceDeclaration['font-family'] && fontFaceDeclaration.src) {
|
|
540
|
+
accumulatedFontFaceDeclarations.push(fontFaceDeclaration);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
637
543
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
traverseForFontFace(rel.to);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
})(htmlOrSvgAsset);
|
|
651
|
-
return accumulatedFontFaceDeclarations;
|
|
544
|
+
const rels = stylesheetRelsByFromAsset.get(asset) || [];
|
|
545
|
+
for (const rel of rels) {
|
|
546
|
+
if (fontFaceTraversalTypes.has(rel.type) ||
|
|
547
|
+
(rel.to && rel.to.type === 'Html' && rel.to.isInline)) {
|
|
548
|
+
traverseForFontFace(rel.to);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
})(htmlOrSvgAsset);
|
|
552
|
+
return accumulatedFontFaceDeclarations;
|
|
652
553
|
}
|
|
653
|
-
|
|
654
554
|
// Validate that @font-face declarations sharing family/style/weight carry
|
|
655
555
|
// disjoint unicode-range values; throws on incomplete coverage.
|
|
656
556
|
function validateFontFaceComboCoverage(accumulatedFontFaceDeclarations) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
557
|
+
const comboGroups = new Map();
|
|
558
|
+
for (const fontFace of accumulatedFontFaceDeclarations) {
|
|
559
|
+
const comboKey = `${fontFace['font-family']}/${fontFace['font-style']}/${fontFace['font-weight']}`;
|
|
560
|
+
if (!comboGroups.has(comboKey))
|
|
561
|
+
comboGroups.set(comboKey, []);
|
|
562
|
+
comboGroups.get(comboKey).push(fontFace);
|
|
563
|
+
}
|
|
564
|
+
for (const [comboKey, group] of comboGroups) {
|
|
565
|
+
if (group.length <= 1)
|
|
566
|
+
continue;
|
|
567
|
+
const withoutRange = group.filter((d) => !d['unicode-range']);
|
|
568
|
+
if (withoutRange.length > 0) {
|
|
569
|
+
throw new Error(`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}`);
|
|
570
|
+
}
|
|
670
571
|
}
|
|
671
|
-
}
|
|
672
572
|
}
|
|
673
|
-
|
|
674
573
|
function computeStylesheetResults(htmlOrSvgAsset, stylesheetRelsByFromAsset) {
|
|
675
|
-
|
|
676
|
-
htmlOrSvgAsset
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const featureTagsByFamily = new Map();
|
|
688
|
-
const fontFamiliesWithFeatureSettings = findFontFamiliesWithFeatureSettings(
|
|
689
|
-
stylesheetsWithPredicates,
|
|
690
|
-
featureTagsByFamily
|
|
691
|
-
);
|
|
692
|
-
|
|
693
|
-
return {
|
|
694
|
-
accumulatedFontFaceDeclarations,
|
|
695
|
-
stylesheetsWithPredicates,
|
|
696
|
-
fontFamiliesWithFeatureSettings,
|
|
697
|
-
featureTagsByFamily,
|
|
698
|
-
fastPathKey: buildStylesheetKey(
|
|
699
|
-
htmlOrSvgAsset,
|
|
700
|
-
true,
|
|
701
|
-
stylesheetRelsByFromAsset
|
|
702
|
-
),
|
|
703
|
-
};
|
|
574
|
+
const stylesheetsWithPredicates = gatherStylesheetsWithPredicates(htmlOrSvgAsset.assetGraph, htmlOrSvgAsset, stylesheetRelsByFromAsset);
|
|
575
|
+
const accumulatedFontFaceDeclarations = collectFontFaceDeclarations(htmlOrSvgAsset, stylesheetRelsByFromAsset);
|
|
576
|
+
validateFontFaceComboCoverage(accumulatedFontFaceDeclarations);
|
|
577
|
+
const featureTagsByFamily = new Map();
|
|
578
|
+
const fontFamiliesWithFeatureSettings = (0, fontFeatureHelpers_1.findFontFamiliesWithFeatureSettings)(stylesheetsWithPredicates, featureTagsByFamily);
|
|
579
|
+
return {
|
|
580
|
+
accumulatedFontFaceDeclarations,
|
|
581
|
+
stylesheetsWithPredicates,
|
|
582
|
+
fontFamiliesWithFeatureSettings,
|
|
583
|
+
featureTagsByFamily,
|
|
584
|
+
fastPathKey: buildStylesheetKey(htmlOrSvgAsset, true, stylesheetRelsByFromAsset),
|
|
585
|
+
};
|
|
704
586
|
}
|
|
705
|
-
|
|
706
587
|
// Strip `-subfont-text` nodes from CSS @font-face declarations once the
|
|
707
588
|
// subset planning is done, so they don't leak to the rendered output.
|
|
708
589
|
function stripSubfontTextNodes(fontFaceDeclarationsByHtmlOrSvgAsset) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
subfontTextNode.remove();
|
|
720
|
-
firstRelation.from.markDirty();
|
|
721
|
-
}
|
|
590
|
+
for (const fontFaceDeclarations of fontFaceDeclarationsByHtmlOrSvgAsset.values()) {
|
|
591
|
+
for (const fontFaceDeclaration of fontFaceDeclarations) {
|
|
592
|
+
const firstRelation = fontFaceDeclaration.relations[0];
|
|
593
|
+
const subfontTextNode = firstRelation.node.nodes?.find((childNode) => childNode.type === 'decl' &&
|
|
594
|
+
childNode.prop?.toLowerCase() === '-subfont-text');
|
|
595
|
+
if (subfontTextNode) {
|
|
596
|
+
subfontTextNode.remove();
|
|
597
|
+
firstRelation.from.markDirty();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
722
600
|
}
|
|
723
|
-
}
|
|
724
601
|
}
|
|
725
|
-
|
|
726
602
|
// Split trace work: with a headless browser every page needs a full trace
|
|
727
603
|
// (dynamic content); otherwise one representative per stylesheet group is
|
|
728
604
|
// traced and the rest use fast-path text extraction.
|
|
729
605
|
function planTracing(pageData, hasHeadlessBrowser) {
|
|
730
|
-
|
|
731
|
-
for (const pd of pageData) {
|
|
732
|
-
let group = pagesByStylesheetKey.get(pd.stylesheetCacheKey);
|
|
733
|
-
if (!group) {
|
|
734
|
-
group = [];
|
|
735
|
-
pagesByStylesheetKey.set(pd.stylesheetCacheKey, group);
|
|
736
|
-
}
|
|
737
|
-
group.push(pd);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const pagesNeedingFullTrace = [];
|
|
741
|
-
const fastPathPages = [];
|
|
742
|
-
if (hasHeadlessBrowser) {
|
|
606
|
+
const pagesByStylesheetKey = new Map();
|
|
743
607
|
for (const pd of pageData) {
|
|
744
|
-
|
|
608
|
+
let group = pagesByStylesheetKey.get(pd.stylesheetCacheKey);
|
|
609
|
+
if (!group) {
|
|
610
|
+
group = [];
|
|
611
|
+
pagesByStylesheetKey.set(pd.stylesheetCacheKey, group);
|
|
612
|
+
}
|
|
613
|
+
group.push(pd);
|
|
614
|
+
}
|
|
615
|
+
const pagesNeedingFullTrace = [];
|
|
616
|
+
const fastPathPages = [];
|
|
617
|
+
if (hasHeadlessBrowser) {
|
|
618
|
+
for (const pd of pageData) {
|
|
619
|
+
pagesNeedingFullTrace.push(pd);
|
|
620
|
+
}
|
|
745
621
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
622
|
+
else {
|
|
623
|
+
for (const group of pagesByStylesheetKey.values()) {
|
|
624
|
+
pagesNeedingFullTrace.push(group[0]);
|
|
625
|
+
for (let i = 1; i < group.length; i++) {
|
|
626
|
+
group[i].representativePd = group[0];
|
|
627
|
+
fastPathPages.push(group[i]);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
753
630
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
uniqueGroupCount: pagesByStylesheetKey.size,
|
|
760
|
-
};
|
|
631
|
+
return {
|
|
632
|
+
pagesNeedingFullTrace,
|
|
633
|
+
fastPathPages,
|
|
634
|
+
uniqueGroupCount: pagesByStylesheetKey.size,
|
|
635
|
+
};
|
|
761
636
|
}
|
|
762
|
-
|
|
763
637
|
// Iterate every traced page, snap its text against the @font-face set, and
|
|
764
638
|
// emit fully-formed per-page fontUsages (one entry per font URL + props).
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
639
|
+
function buildPerPageFontUsages(htmlOrSvgAssetTextsWithProps, globalTextByProps, text) {
|
|
640
|
+
const declCache = new Map();
|
|
641
|
+
const uniqueCharsCache = new Map();
|
|
642
|
+
let snappingTime = 0;
|
|
643
|
+
let globalUsageTime = 0;
|
|
644
|
+
let cloningTime = 0;
|
|
645
|
+
for (const entry of htmlOrSvgAssetTextsWithProps) {
|
|
646
|
+
const { htmlOrSvgAsset, textByProps, accumulatedFontFaceDeclarations, fontFamiliesWithFeatureSettings, featureTagsByFamily, } = entry;
|
|
647
|
+
const declKey = getDeclarationsKey(accumulatedFontFaceDeclarations);
|
|
648
|
+
if (!declCache.has(declKey)) {
|
|
649
|
+
const snapStart = Date.now();
|
|
650
|
+
declCache.set(declKey, {
|
|
651
|
+
snappedEntries: computeSnappedGlobalEntries(accumulatedFontFaceDeclarations, globalTextByProps),
|
|
652
|
+
fontUsageTemplates: null,
|
|
653
|
+
pageTextIndex: null,
|
|
654
|
+
preloadIndex: null,
|
|
655
|
+
});
|
|
656
|
+
snappingTime += Date.now() - snapStart;
|
|
657
|
+
}
|
|
658
|
+
const declCacheEntry = declCache.get(declKey);
|
|
659
|
+
const globalUsageStart = Date.now();
|
|
660
|
+
populateGlobalFontUsages(declCacheEntry, accumulatedFontFaceDeclarations, text);
|
|
661
|
+
globalUsageTime += Date.now() - globalUsageStart;
|
|
662
|
+
const fontUsageTemplates = declCacheEntry.fontUsageTemplates;
|
|
663
|
+
const pageTextIndex = declCacheEntry.pageTextIndex;
|
|
664
|
+
const textAndPropsToFontUrl = declCacheEntry.preloadIndex;
|
|
665
|
+
const preloadFontUrls = new Set();
|
|
666
|
+
for (const textByPropsEntry of textByProps) {
|
|
667
|
+
const fontUrl = textAndPropsToFontUrl.get(textByPropsEntry);
|
|
668
|
+
if (fontUrl) {
|
|
669
|
+
preloadFontUrls.add(fontUrl);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const cloneStart = Date.now();
|
|
673
|
+
const assetTexts = pageTextIndex.get(htmlOrSvgAsset);
|
|
674
|
+
entry.fontUsages = fontUsageTemplates.map((template) => {
|
|
675
|
+
const pageTexts = assetTexts
|
|
676
|
+
? assetTexts.get(template.fontUrl)
|
|
677
|
+
: undefined;
|
|
678
|
+
let pageTextStr = pageTexts ? pageTexts.join('') : '';
|
|
679
|
+
if (template.extraTextsStr) {
|
|
680
|
+
pageTextStr += template.extraTextsStr;
|
|
681
|
+
}
|
|
682
|
+
let pageTextUnique = uniqueCharsCache.get(pageTextStr);
|
|
683
|
+
if (pageTextUnique === undefined) {
|
|
684
|
+
pageTextUnique = (0, fontFaceHelpers_1.uniqueChars)(pageTextStr);
|
|
685
|
+
uniqueCharsCache.set(pageTextStr, pageTextUnique);
|
|
686
|
+
}
|
|
687
|
+
const { hasFontFeatureSettings, fontFeatureTags } = (0, fontFeatureHelpers_1.resolveFeatureSettings)(template.fontFamilies, fontFamiliesWithFeatureSettings, featureTagsByFamily);
|
|
688
|
+
return {
|
|
689
|
+
smallestOriginalSize: template.smallestOriginalSize,
|
|
690
|
+
smallestOriginalFormat: template.smallestOriginalFormat,
|
|
691
|
+
texts: template.texts,
|
|
692
|
+
pageText: pageTextUnique,
|
|
693
|
+
text: template.text,
|
|
694
|
+
props: { ...template.props },
|
|
695
|
+
fontUrl: template.fontUrl,
|
|
696
|
+
fontFamilies: template.fontFamilies,
|
|
697
|
+
fontStyles: template.fontStyles,
|
|
698
|
+
fontStretches: template.fontStretches,
|
|
699
|
+
fontWeights: template.fontWeights,
|
|
700
|
+
fontVariationSettings: template.fontVariationSettings,
|
|
701
|
+
preload: preloadFontUrls.has(template.fontUrl),
|
|
702
|
+
hasFontFeatureSettings,
|
|
703
|
+
fontFeatureTags,
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
cloningTime += Date.now() - cloneStart;
|
|
823
707
|
}
|
|
824
|
-
|
|
825
|
-
const cloneStart = Date.now();
|
|
826
|
-
const assetTexts = pageTextIndex.get(htmlOrSvgAsset);
|
|
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
|
-
}
|
|
835
|
-
|
|
836
|
-
let pageTextUnique = uniqueCharsCache.get(pageTextStr);
|
|
837
|
-
if (pageTextUnique === undefined) {
|
|
838
|
-
pageTextUnique = uniqueChars(pageTextStr);
|
|
839
|
-
uniqueCharsCache.set(pageTextStr, pageTextUnique);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
const { hasFontFeatureSettings, fontFeatureTags } =
|
|
843
|
-
resolveFeatureSettings(
|
|
844
|
-
template.fontFamilies,
|
|
845
|
-
fontFamiliesWithFeatureSettings,
|
|
846
|
-
featureTagsByFamily
|
|
847
|
-
);
|
|
848
|
-
|
|
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
|
-
});
|
|
867
|
-
cloningTime += Date.now() - cloneStart;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
return { snappingTime, globalUsageTime, cloningTime };
|
|
708
|
+
return { snappingTime, globalUsageTime, cloningTime };
|
|
871
709
|
}
|
|
872
|
-
|
|
873
710
|
// Run computeStylesheetResults once per page, memoizing the result across
|
|
874
|
-
// pages that resolve to the same set of stylesheets.
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
) {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
stylesheetResultCache.set(key, result);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
fontFaceDeclarationsByHtmlOrSvgAsset.set(
|
|
901
|
-
htmlOrSvgAsset,
|
|
902
|
-
result.accumulatedFontFaceDeclarations
|
|
903
|
-
);
|
|
904
|
-
|
|
905
|
-
if (result.accumulatedFontFaceDeclarations.length === 0) {
|
|
906
|
-
continue;
|
|
711
|
+
// pages that resolve to the same set of stylesheets.
|
|
712
|
+
function precomputeStylesheetsForPages(htmlOrSvgAssets, stylesheetRelsByFromAsset, fontFaceDeclarationsByHtmlOrSvgAsset) {
|
|
713
|
+
const stylesheetResultCache = new Map();
|
|
714
|
+
const pageData = [];
|
|
715
|
+
for (const htmlOrSvgAsset of htmlOrSvgAssets) {
|
|
716
|
+
const key = buildStylesheetKey(htmlOrSvgAsset, false, stylesheetRelsByFromAsset);
|
|
717
|
+
let result = stylesheetResultCache.get(key);
|
|
718
|
+
if (!result) {
|
|
719
|
+
result = computeStylesheetResults(htmlOrSvgAsset, stylesheetRelsByFromAsset);
|
|
720
|
+
stylesheetResultCache.set(key, result);
|
|
721
|
+
}
|
|
722
|
+
fontFaceDeclarationsByHtmlOrSvgAsset.set(htmlOrSvgAsset, result.accumulatedFontFaceDeclarations);
|
|
723
|
+
if (result.accumulatedFontFaceDeclarations.length === 0) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
pageData.push({
|
|
727
|
+
htmlOrSvgAsset,
|
|
728
|
+
accumulatedFontFaceDeclarations: result.accumulatedFontFaceDeclarations,
|
|
729
|
+
stylesheetsWithPredicates: result.stylesheetsWithPredicates,
|
|
730
|
+
fontFamiliesWithFeatureSettings: result.fontFamiliesWithFeatureSettings,
|
|
731
|
+
featureTagsByFamily: result.featureTagsByFamily,
|
|
732
|
+
stylesheetCacheKey: result.fastPathKey,
|
|
733
|
+
});
|
|
907
734
|
}
|
|
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;
|
|
735
|
+
return pageData;
|
|
920
736
|
}
|
|
921
|
-
|
|
922
737
|
// Flatten traced per-page textByProps into a single globalTextByProps array,
|
|
923
738
|
// tagging each entry with its owning asset so downstream code can map text
|
|
924
739
|
// back to the page that rendered it.
|
|
925
|
-
function flattenTracedPagesIntoGlobal(
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
740
|
+
function flattenTracedPagesIntoGlobal(pageData, htmlOrSvgAssetTextsWithProps, globalTextByProps) {
|
|
741
|
+
for (const pd of pageData) {
|
|
742
|
+
const textByProps = pd.textByProps ?? [];
|
|
743
|
+
for (const textByPropsEntry of textByProps) {
|
|
744
|
+
textByPropsEntry.htmlOrSvgAsset = pd.htmlOrSvgAsset;
|
|
745
|
+
}
|
|
746
|
+
// Use a loop instead of push(...spread) to avoid stack overflow on large sites
|
|
747
|
+
for (const entry of textByProps) {
|
|
748
|
+
globalTextByProps.push(entry);
|
|
749
|
+
}
|
|
750
|
+
htmlOrSvgAssetTextsWithProps.push({
|
|
751
|
+
htmlOrSvgAsset: pd.htmlOrSvgAsset,
|
|
752
|
+
textByProps,
|
|
753
|
+
accumulatedFontFaceDeclarations: pd.accumulatedFontFaceDeclarations,
|
|
754
|
+
fontFamiliesWithFeatureSettings: pd.fontFamiliesWithFeatureSettings,
|
|
755
|
+
featureTagsByFamily: pd.featureTagsByFamily,
|
|
756
|
+
fontUsages: [], // populated by buildPerPageFontUsages
|
|
757
|
+
});
|
|
937
758
|
}
|
|
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
759
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
)
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
// reuse. The threshold matches createPageProgress's minTotal so it
|
|
992
|
-
// only appears on non-trivial runs.
|
|
993
|
-
if (console && pageData.length >= 5) {
|
|
994
|
-
console.log(
|
|
995
|
-
` ${pageData.length} pages with fonts: ${pagesNeedingFullTrace.length} to trace, ${fastPathPages.length} via cached CSS group (${uniqueGroupCount} unique groups)`
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
|
|
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
|
-
});
|
|
1011
|
-
|
|
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
|
-
);
|
|
760
|
+
async function collectTextsByPage(assetGraph, htmlOrSvgAssets, { text, console, dynamic = false, debug = false, concurrency, chromeArgs = [], } = {}) {
|
|
761
|
+
const htmlOrSvgAssetTextsWithProps = [];
|
|
762
|
+
const memoizedGetCssRulesByProperty = memoizeSync(getCssRulesByProperty);
|
|
763
|
+
const fontFaceDeclarationsByHtmlOrSvgAsset = new Map();
|
|
764
|
+
const stylesheetRelsByFromAsset = indexStylesheetRelations(assetGraph);
|
|
765
|
+
const headlessBrowser = dynamic
|
|
766
|
+
? new HeadlessBrowser({ console: console, chromeArgs })
|
|
767
|
+
: null;
|
|
768
|
+
const globalTextByProps = [];
|
|
769
|
+
const subTimings = {};
|
|
770
|
+
const trackPhase = (0, progress_1.makePhaseTracker)(console, debug);
|
|
771
|
+
const overallPhase = trackPhase('collectTextsByPage');
|
|
772
|
+
const stylesheetPrecompute = trackPhase('Stylesheet precompute');
|
|
773
|
+
const pageData = precomputeStylesheetsForPages(htmlOrSvgAssets, stylesheetRelsByFromAsset, fontFaceDeclarationsByHtmlOrSvgAsset);
|
|
774
|
+
subTimings['Stylesheet precompute'] = stylesheetPrecompute.end(`${pageData.length} pages with fonts`);
|
|
775
|
+
// Pages sharing the same CSS configuration produce identical font-tracer
|
|
776
|
+
// props, only text differs — so we trace one representative and fast-path
|
|
777
|
+
// the rest. With --dynamic every page is traced individually.
|
|
778
|
+
const { pagesNeedingFullTrace, fastPathPages, uniqueGroupCount } = planTracing(pageData, Boolean(headlessBrowser));
|
|
779
|
+
if (console && pageData.length >= 5) {
|
|
780
|
+
console.log(` ${pageData.length} pages with fonts: ${pagesNeedingFullTrace.length} to trace, ${fastPathPages.length} via cached CSS group (${uniqueGroupCount} unique groups)`);
|
|
781
|
+
}
|
|
782
|
+
const tracingStart = Date.now();
|
|
783
|
+
const fullTracing = trackPhase(`Full tracing (${pagesNeedingFullTrace.length} pages)`);
|
|
784
|
+
try {
|
|
785
|
+
await tracePages(pagesNeedingFullTrace, {
|
|
786
|
+
headlessBrowser,
|
|
787
|
+
concurrency,
|
|
788
|
+
console,
|
|
789
|
+
memoizedGetCssRulesByProperty,
|
|
790
|
+
debug,
|
|
791
|
+
});
|
|
792
|
+
subTimings['Full tracing'] = fullTracing.end();
|
|
793
|
+
const fastPathPhase = trackPhase('Fast-path extraction');
|
|
794
|
+
const fastPathFallbacks = processFastPathPages(fastPathPages, {
|
|
795
|
+
memoizedGetCssRulesByProperty,
|
|
796
|
+
});
|
|
797
|
+
subTimings['Fast-path extraction'] = fastPathPhase.end(`${fastPathPages.length} pages, ${fastPathFallbacks} fell back to full trace`);
|
|
798
|
+
const assemblePhase = trackPhase('Result assembly');
|
|
799
|
+
flattenTracedPagesIntoGlobal(pageData, htmlOrSvgAssetTextsWithProps, globalTextByProps);
|
|
800
|
+
subTimings['Result assembly'] = assemblePhase.end();
|
|
801
|
+
if (debug && console) {
|
|
802
|
+
console.log(`[subfont timing] Total tracing+extraction+assembly: ${Date.now() - tracingStart}ms`);
|
|
803
|
+
}
|
|
1035
804
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
805
|
+
finally {
|
|
806
|
+
if (headlessBrowser) {
|
|
807
|
+
await headlessBrowser.close();
|
|
808
|
+
}
|
|
1039
809
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
subTimings['Post-processing total'] = postProcessPhase.end();
|
|
1053
|
-
overallPhase.end();
|
|
1054
|
-
|
|
1055
|
-
stripSubfontTextNodes(fontFaceDeclarationsByHtmlOrSvgAsset);
|
|
1056
|
-
return {
|
|
1057
|
-
htmlOrSvgAssetTextsWithProps,
|
|
1058
|
-
fontFaceDeclarationsByHtmlOrSvgAsset,
|
|
1059
|
-
subTimings,
|
|
1060
|
-
};
|
|
810
|
+
const postProcessPhase = trackPhase('Post-processing total');
|
|
811
|
+
const perPageLoopPhase = trackPhase('Per-page loop');
|
|
812
|
+
const { snappingTime, globalUsageTime, cloningTime } = buildPerPageFontUsages(htmlOrSvgAssetTextsWithProps, globalTextByProps, text);
|
|
813
|
+
subTimings['Per-page loop'] = perPageLoopPhase.end(`snapping: ${snappingTime}ms, globalUsage: ${globalUsageTime}ms, cloning: ${cloningTime}ms`);
|
|
814
|
+
subTimings['Post-processing total'] = postProcessPhase.end();
|
|
815
|
+
overallPhase.end();
|
|
816
|
+
stripSubfontTextNodes(fontFaceDeclarationsByHtmlOrSvgAsset);
|
|
817
|
+
return {
|
|
818
|
+
htmlOrSvgAssetTextsWithProps,
|
|
819
|
+
fontFaceDeclarationsByHtmlOrSvgAsset,
|
|
820
|
+
subTimings,
|
|
821
|
+
};
|
|
1061
822
|
}
|
|
1062
|
-
|
|
1063
823
|
module.exports = collectTextsByPage;
|
|
824
|
+
//# sourceMappingURL=collectTextsByPage.js.map
|