@turntrout/subfont 1.7.1 → 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.
Files changed (147) hide show
  1. package/CLAUDE.md +39 -13
  2. package/README.md +24 -14
  3. package/lib/FontTracerPool.d.ts +1 -0
  4. package/lib/FontTracerPool.d.ts.map +1 -1
  5. package/lib/FontTracerPool.js +46 -24
  6. package/lib/FontTracerPool.js.map +1 -1
  7. package/lib/HeadlessBrowser.d.ts +18 -0
  8. package/lib/HeadlessBrowser.d.ts.map +1 -0
  9. package/lib/HeadlessBrowser.js +216 -210
  10. package/lib/HeadlessBrowser.js.map +1 -0
  11. package/lib/codepointMaps.d.ts +4 -0
  12. package/lib/codepointMaps.d.ts.map +1 -0
  13. package/lib/codepointMaps.js +99 -0
  14. package/lib/codepointMaps.js.map +1 -0
  15. package/lib/collectFeatureGlyphIds.d.ts +3 -0
  16. package/lib/collectFeatureGlyphIds.d.ts.map +1 -0
  17. package/lib/collectFeatureGlyphIds.js +124 -138
  18. package/lib/collectFeatureGlyphIds.js.map +1 -0
  19. package/lib/collectTextsByPage.d.ts +41 -0
  20. package/lib/collectTextsByPage.d.ts.map +1 -0
  21. package/lib/collectTextsByPage.js +726 -965
  22. package/lib/collectTextsByPage.js.map +1 -0
  23. package/lib/concurrencyLimit.d.ts +3 -0
  24. package/lib/concurrencyLimit.d.ts.map +1 -0
  25. package/lib/concurrencyLimit.js +12 -11
  26. package/lib/concurrencyLimit.js.map +1 -0
  27. package/lib/escapeJsStringLiteral.d.ts +3 -0
  28. package/lib/escapeJsStringLiteral.d.ts.map +1 -0
  29. package/lib/escapeJsStringLiteral.js +7 -6
  30. package/lib/escapeJsStringLiteral.js.map +1 -0
  31. package/lib/extractReferencedCustomPropertyNames.d.ts +3 -0
  32. package/lib/extractReferencedCustomPropertyNames.d.ts.map +1 -0
  33. package/lib/extractReferencedCustomPropertyNames.js +15 -16
  34. package/lib/extractReferencedCustomPropertyNames.js.map +1 -0
  35. package/lib/extractVisibleText.d.ts +7 -0
  36. package/lib/extractVisibleText.d.ts.map +1 -0
  37. package/lib/extractVisibleText.js +110 -119
  38. package/lib/extractVisibleText.js.map +1 -0
  39. package/lib/findCustomPropertyDefinitions.d.ts +8 -0
  40. package/lib/findCustomPropertyDefinitions.d.ts.map +1 -0
  41. package/lib/findCustomPropertyDefinitions.js +41 -48
  42. package/lib/findCustomPropertyDefinitions.js.map +1 -0
  43. package/lib/fontConverter.d.ts +2 -0
  44. package/lib/fontConverter.d.ts.map +1 -0
  45. package/lib/fontConverter.js +40 -21
  46. package/lib/fontConverter.js.map +1 -0
  47. package/lib/fontConverterWorker.d.ts +2 -0
  48. package/lib/fontConverterWorker.d.ts.map +1 -0
  49. package/lib/fontConverterWorker.js +52 -15
  50. package/lib/fontConverterWorker.js.map +1 -0
  51. package/lib/fontFaceHelpers.d.ts +64 -0
  52. package/lib/fontFaceHelpers.d.ts.map +1 -0
  53. package/lib/fontFaceHelpers.js +237 -249
  54. package/lib/fontFaceHelpers.js.map +1 -0
  55. package/lib/fontFeatureHelpers.d.ts +30 -0
  56. package/lib/fontFeatureHelpers.d.ts.map +1 -0
  57. package/lib/fontFeatureHelpers.js +277 -212
  58. package/lib/fontFeatureHelpers.js.map +1 -0
  59. package/lib/fontTracerWorker.d.ts +11 -0
  60. package/lib/fontTracerWorker.d.ts.map +1 -0
  61. package/lib/fontTracerWorker.js +94 -60
  62. package/lib/fontTracerWorker.js.map +1 -0
  63. package/lib/gatherStylesheetsWithPredicates.d.ts +26 -0
  64. package/lib/gatherStylesheetsWithPredicates.d.ts.map +1 -0
  65. package/lib/gatherStylesheetsWithPredicates.js +75 -84
  66. package/lib/gatherStylesheetsWithPredicates.js.map +1 -0
  67. package/lib/getCssRulesByProperty.d.ts +29 -0
  68. package/lib/getCssRulesByProperty.d.ts.map +1 -0
  69. package/lib/getCssRulesByProperty.js +316 -316
  70. package/lib/getCssRulesByProperty.js.map +1 -0
  71. package/lib/getFontInfo.d.ts +11 -0
  72. package/lib/getFontInfo.d.ts.map +1 -0
  73. package/lib/getFontInfo.js +31 -33
  74. package/lib/getFontInfo.js.map +1 -0
  75. package/lib/initialValueByProp.d.ts +3 -0
  76. package/lib/initialValueByProp.d.ts.map +1 -0
  77. package/lib/initialValueByProp.js +20 -17
  78. package/lib/initialValueByProp.js.map +1 -0
  79. package/lib/injectSubsetDefinitions.d.ts +3 -0
  80. package/lib/injectSubsetDefinitions.d.ts.map +1 -0
  81. package/lib/injectSubsetDefinitions.js +55 -59
  82. package/lib/injectSubsetDefinitions.js.map +1 -0
  83. package/lib/normalizeFontPropertyValue.d.ts +3 -0
  84. package/lib/normalizeFontPropertyValue.d.ts.map +1 -0
  85. package/lib/normalizeFontPropertyValue.js +59 -54
  86. package/lib/normalizeFontPropertyValue.js.map +1 -0
  87. package/lib/parseCommandLineOptions.d.ts +9 -0
  88. package/lib/parseCommandLineOptions.d.ts.map +1 -0
  89. package/lib/parseCommandLineOptions.js +145 -149
  90. package/lib/parseCommandLineOptions.js.map +1 -0
  91. package/lib/parseFontVariationSettings.d.ts +3 -0
  92. package/lib/parseFontVariationSettings.d.ts.map +1 -0
  93. package/lib/parseFontVariationSettings.js +38 -36
  94. package/lib/parseFontVariationSettings.js.map +1 -0
  95. package/lib/progress.d.ts +27 -0
  96. package/lib/progress.d.ts.map +1 -0
  97. package/lib/progress.js +51 -54
  98. package/lib/progress.js.map +1 -0
  99. package/lib/sfntCache.d.ts +4 -0
  100. package/lib/sfntCache.d.ts.map +1 -0
  101. package/lib/sfntCache.js +67 -25
  102. package/lib/sfntCache.js.map +1 -0
  103. package/lib/stripLocalTokens.d.ts +3 -0
  104. package/lib/stripLocalTokens.d.ts.map +1 -0
  105. package/lib/stripLocalTokens.js +23 -21
  106. package/lib/stripLocalTokens.js.map +1 -0
  107. package/lib/subfont.d.ts +22 -1
  108. package/lib/subfont.d.ts.map +1 -1
  109. package/lib/subfont.js +7 -6
  110. package/lib/subfont.js.map +1 -1
  111. package/lib/subsetFontWithGlyphs.d.ts +4 -0
  112. package/lib/subsetFontWithGlyphs.d.ts.map +1 -1
  113. package/lib/subsetFontWithGlyphs.js +73 -14
  114. package/lib/subsetFontWithGlyphs.js.map +1 -1
  115. package/lib/subsetFonts.d.ts +1 -5
  116. package/lib/subsetFonts.d.ts.map +1 -1
  117. package/lib/subsetFonts.js +18 -17
  118. package/lib/subsetFonts.js.map +1 -1
  119. package/lib/subsetGeneration.d.ts +3 -6
  120. package/lib/subsetGeneration.d.ts.map +1 -1
  121. package/lib/subsetGeneration.js +42 -9
  122. package/lib/subsetGeneration.js.map +1 -1
  123. package/lib/types/shared.d.ts +11 -0
  124. package/lib/types/shared.d.ts.map +1 -0
  125. package/lib/types/shared.js +3 -0
  126. package/lib/types/shared.js.map +1 -0
  127. package/lib/unicodeRange.d.ts +3 -0
  128. package/lib/unicodeRange.d.ts.map +1 -0
  129. package/lib/unicodeRange.js +17 -30
  130. package/lib/unicodeRange.js.map +1 -0
  131. package/lib/unquote.d.ts +3 -0
  132. package/lib/unquote.d.ts.map +1 -0
  133. package/lib/unquote.js +18 -25
  134. package/lib/unquote.js.map +1 -0
  135. package/lib/variationAxes.d.ts +33 -0
  136. package/lib/variationAxes.d.ts.map +1 -0
  137. package/lib/variationAxes.js +127 -157
  138. package/lib/variationAxes.js.map +1 -0
  139. package/lib/warnAboutMissingGlyphs.d.ts +43 -0
  140. package/lib/warnAboutMissingGlyphs.d.ts.map +1 -0
  141. package/lib/warnAboutMissingGlyphs.js +139 -147
  142. package/lib/warnAboutMissingGlyphs.js.map +1 -0
  143. package/lib/wasmQueue.d.ts +3 -0
  144. package/lib/wasmQueue.d.ts.map +1 -0
  145. package/lib/wasmQueue.js +13 -10
  146. package/lib/wasmQueue.js.map +1 -0
  147. package/package.json +3 -2
@@ -1,1063 +1,824 @@
1
- const memoizeSync = require('memoizesync');
2
- const os = require('os');
3
-
4
- const fontTracer = require('font-tracer');
5
- const fontSnapper = require('font-snapper');
6
-
7
- const HeadlessBrowser = require('./HeadlessBrowser');
8
- const FontTracerPool = require('./FontTracerPool');
9
- const gatherStylesheetsWithPredicates = require('./gatherStylesheetsWithPredicates');
10
- const cssFontParser = require('css-font-parser');
11
- const unquote = require('./unquote');
12
- const normalizeFontPropertyValue = require('./normalizeFontPropertyValue');
13
- const getCssRulesByProperty = require('./getCssRulesByProperty');
14
- const extractVisibleText = require('./extractVisibleText');
15
- const {
16
- stringifyFontFamily,
17
- getPreferredFontUrl,
18
- uniqueChars,
19
- uniqueCharsFromArray,
20
- } = require('./fontFaceHelpers');
21
- const {
22
- createPageProgress,
23
- logTracedPage,
24
- makePhaseTracker,
25
- } = require('./progress');
26
-
27
- const fontRelevantCssRegex =
28
- /font-family|font-weight|font-style|font-stretch|font-display|@font-face|font-variation|font-feature/i;
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
- return inlineFontStyleRegex.test(html);
55
+ return inlineFontStyleRegex.test(html);
35
56
  }
36
-
37
- const fontFaceTraversalTypes = new Set(['HtmlStyle', 'SvgStyle', 'CssImport']);
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
- 'font-style': allInitialValues['font-style'],
51
- 'font-weight': allInitialValues['font-weight'],
52
- 'font-stretch': allInitialValues['font-stretch'],
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
- return `${family}\0${weight}\0${style}\0${stretch}`;
72
+ return `${family}\0${weight}\0${style}\0${stretch}`;
58
73
  }
59
-
60
74
  const declKeyCache = new WeakMap();
61
75
  function getDeclarationsKey(declarations) {
62
- if (declKeyCache.has(declarations)) {
63
- return declKeyCache.get(declarations);
64
- }
65
- const key = JSON.stringify(
66
- declarations.map((d) => [
67
- d['font-family'],
68
- d['font-style'],
69
- d['font-weight'],
70
- d['font-stretch'],
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
- const entries = [];
81
- // Cache snapping results per unique props key within this declarations
82
- // set. Many globalTextByProps entries share the same font properties
83
- // (only text differs), so we avoid redundant fontSnapper + family
84
- // parsing calls.
85
- const snappingResultCache = new Map();
86
-
87
- for (const textAndProps of globalTextByProps) {
88
- const family = textAndProps.props['font-family'];
89
- if (family === undefined) {
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
- const {
123
- relations,
124
- '-subfont-text': _,
125
- ...props
126
- } = activeFontFaceDeclaration;
127
- const fontUrl = getPreferredFontUrl(relations);
128
- if (!fontUrl) {
129
- continue;
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
- let fontWeight = normalizeFontPropertyValue(
133
- 'font-weight',
134
- textAndProps.props['font-weight']
135
- );
136
- if (fontWeight === 'normal') {
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
- cached,
174
- accumulatedFontFaceDeclarations,
175
- text
176
- ) {
177
- if (cached.fontUsageTemplates) {
178
- return;
179
- }
180
-
181
- const snappedGlobalEntries = cached.snappedEntries;
182
-
183
- const pageTextIndex = new Map();
184
- const entriesByFontUrl = new Map();
185
- const textAndPropsToFontUrl = new Map();
186
-
187
- for (const entry of snappedGlobalEntries) {
188
- if (!entry.fontUrl) continue;
189
-
190
- const asset = entry.textAndProps.htmlOrSvgAsset;
191
- let assetMap = pageTextIndex.get(asset);
192
- if (!assetMap) {
193
- assetMap = new Map();
194
- pageTextIndex.set(asset, assetMap);
195
- }
196
- let texts = assetMap.get(entry.fontUrl);
197
- if (!texts) {
198
- texts = [];
199
- assetMap.set(entry.fontUrl, texts);
200
- }
201
- texts.push(entry.textAndProps.text);
202
-
203
- let arr = entriesByFontUrl.get(entry.fontUrl);
204
- if (!arr) {
205
- arr = [];
206
- entriesByFontUrl.set(entry.fontUrl, arr);
207
- }
208
- arr.push(entry);
209
-
210
- textAndPropsToFontUrl.set(entry.textAndProps, entry.fontUrl);
211
- }
212
- const extraTextsByFontUrl = new Map();
213
- for (const fontFaceDeclaration of accumulatedFontFaceDeclarations) {
214
- const {
215
- relations,
216
- '-subfont-text': subfontText,
217
- ...props
218
- } = fontFaceDeclaration;
219
- const fontUrl = getPreferredFontUrl(relations);
220
- if (!fontUrl) continue;
221
-
222
- const extras = [];
223
- if (subfontText !== undefined) {
224
- extras.push(unquote(subfontText));
225
- }
226
- if (text !== undefined) {
227
- extras.push(text);
228
- }
229
- if (extras.length > 0) {
230
- let arr = extraTextsByFontUrl.get(fontUrl);
231
- if (!arr) {
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 fontFamilies = new Set(
260
- fontEntries.map((e) => e.props['font-family'])
261
- );
262
- const fontStyles = new Set(fontEntries.map((e) => e.fontStyle));
263
- const fontWeights = new Set(fontEntries.map((e) => e.fontWeight));
264
- const fontStretches = new Set(fontEntries.map((e) => e.fontStretch));
265
- const fontVariationSettings = new Set(
266
- fontEntries
267
- .map((e) => e.fontVariationSettings)
268
- .filter((fvs) => fvs && fvs.toLowerCase() !== 'normal')
269
- );
270
- // Use first entry's relations for size computation, or extra's if no entries
271
- const fontRelations =
272
- fontEntries.length > 0
273
- ? fontEntries[0].fontRelations
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
- const props =
289
- fontEntries.length > 0 ? { ...fontEntries[0].props } : { ...extra.props };
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
- pagesNeedingFullTrace,
318
- {
319
- headlessBrowser,
320
- concurrency,
321
- console,
322
- memoizedGetCssRulesByProperty,
323
- debug = false,
324
- }
325
- ) {
326
- const totalPages = pagesNeedingFullTrace.length;
327
- if (totalPages === 0) return;
328
-
329
- const useWorkerPool =
330
- !headlessBrowser && totalPages >= MIN_PAGES_FOR_WORKER_POOL;
331
-
332
- const progress = createPageProgress({
333
- total: totalPages,
334
- console,
335
- label: 'Tracing fonts',
336
- });
337
-
338
- if (useWorkerPool) {
339
- const maxWorkers =
340
- concurrency > 0 ? concurrency : Math.min(os.cpus().length, 8);
341
- const numWorkers = Math.min(maxWorkers, totalPages);
342
- const pool = new FontTracerPool(numWorkers);
343
- await pool.init();
344
-
345
- try {
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
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
- }
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
- stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
365
- getCssRulesByProperty: memoizedGetCssRulesByProperty,
366
- asset: pd.htmlOrSvgAsset,
341
+ stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
342
+ getCssRulesByProperty: memoizedGetCssRulesByProperty,
343
+ asset: pd.htmlOrSvgAsset,
367
344
  });
368
- }
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();
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
- fastPathPages,
421
- { memoizedGetCssRulesByProperty }
422
- ) {
423
- if (fastPathPages.length === 0) return 0;
424
-
425
- const repDataCache = new Map();
426
- function getRepData(representativePd) {
427
- if (repDataCache.has(representativePd)) {
428
- return repDataCache.get(representativePd);
429
- }
430
- const repTextByProps = representativePd.textByProps;
431
-
432
- const uniquePropsMap = new Map();
433
- const textPerPropsKey = new Map();
434
- const seenVariantKeys = new Set();
435
- for (const entry of repTextByProps) {
436
- const family = entry.props['font-family'] || '';
437
- const propsKey = fontPropsKey(
438
- family,
439
- entry.props['font-weight'] || '',
440
- entry.props['font-style'] || '',
441
- entry.props['font-stretch'] || ''
442
- );
443
- if (!uniquePropsMap.has(propsKey)) {
444
- uniquePropsMap.set(propsKey, entry.props);
445
- textPerPropsKey.set(propsKey, []);
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
- const data = { uniquePropsMap, textPerPropsKey, seenVariantKeys };
460
- repDataCache.set(representativePd, data);
461
- return data;
462
- }
463
-
464
- let fastPathFallbacks = 0;
465
- for (const pd of fastPathPages) {
466
- if (hasInlineFontStyles(pd.htmlOrSvgAsset.text || '')) {
467
- fastPathFallbacks++;
468
- pd.textByProps = fontTracer(pd.htmlOrSvgAsset.parseTree, {
469
- stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
470
- getCssRulesByProperty: memoizedGetCssRulesByProperty,
471
- asset: pd.htmlOrSvgAsset,
472
- });
473
- continue;
474
- }
475
-
476
- const { uniquePropsMap, textPerPropsKey, seenVariantKeys } = getRepData(
477
- pd.representativePd
478
- );
479
-
480
- // Check if any @font-face variants are unseen by the representative.
481
- // Only copy Maps when extensions are actually needed.
482
- let effectivePropsMap = uniquePropsMap;
483
- let effectiveTextPerPropsKey = textPerPropsKey;
484
- for (const decl of pd.accumulatedFontFaceDeclarations) {
485
- const family = decl['font-family'];
486
- if (!family) continue;
487
- const weight = decl['font-weight'] || 'normal';
488
- const style = decl['font-style'] || 'normal';
489
- const stretch = decl['font-stretch'] || 'normal';
490
- const variantKey = fontPropsKey(
491
- family.toLowerCase(),
492
- weight,
493
- style,
494
- stretch
495
- );
496
- if (!seenVariantKeys.has(variantKey)) {
497
- // Lazy-copy on first unseen variant
498
- if (effectivePropsMap === uniquePropsMap) {
499
- effectivePropsMap = new Map(uniquePropsMap);
500
- effectiveTextPerPropsKey = new Map(textPerPropsKey);
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 propsKey = fontPropsKey(
503
- stringifyFontFamily(family),
504
- weight,
505
- style,
506
- stretch
507
- );
508
- if (!effectivePropsMap.has(propsKey)) {
509
- effectivePropsMap.set(propsKey, {
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
- 'HtmlStyle',
538
- 'SvgStyle',
539
- 'CssImport',
540
- 'HtmlConditionalComment',
541
- 'HtmlNoscript',
453
+ 'HtmlStyle',
454
+ 'SvgStyle',
455
+ 'CssImport',
456
+ 'HtmlConditionalComment',
457
+ 'HtmlNoscript',
542
458
  ];
543
-
544
459
  function indexStylesheetRelations(assetGraph) {
545
- const byFromAsset = new Map();
546
- for (const relation of assetGraph.findRelations({
547
- type: { $in: STYLESHEET_REL_TYPES },
548
- })) {
549
- let arr = byFromAsset.get(relation.from);
550
- if (!arr) {
551
- arr = [];
552
- byFromAsset.set(relation.from, arr);
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
- arr.push(relation);
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
- 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;
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
- const media = relation.media || '';
590
- keyParts.push(`${target.id}:${media}:${isNoscript ? 'ns' : ''}`);
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
- 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
- );
616
-
617
- for (const fontRelation of fontRelations) {
618
- const node = fontRelation.node;
619
- if (seenNodes.has(node)) continue;
620
- seenNodes.add(node);
621
-
622
- const fontFaceDeclaration = {
623
- relations: fontRelations.filter((r) => r.node === node),
624
- ...initialValueByProp,
625
- };
626
-
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);
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
- 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);
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
- 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
- );
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
- const stylesheetsWithPredicates = gatherStylesheetsWithPredicates(
676
- htmlOrSvgAsset.assetGraph,
677
- htmlOrSvgAsset,
678
- stylesheetRelsByFromAsset
679
- );
680
-
681
- const accumulatedFontFaceDeclarations = collectFontFaceDeclarations(
682
- htmlOrSvgAsset,
683
- stylesheetRelsByFromAsset
684
- );
685
- validateFontFaceComboCoverage(accumulatedFontFaceDeclarations);
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
- 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
- );
717
-
718
- if (subfontTextNode) {
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
- const pagesByStylesheetKey = new Map();
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
- pagesNeedingFullTrace.push(pd);
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
- } else {
747
- for (const group of pagesByStylesheetKey.values()) {
748
- pagesNeedingFullTrace.push(group[0]);
749
- for (let i = 1; i < group.length; i++) {
750
- group[i].representativePd = group[0];
751
- fastPathPages.push(group[i]);
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
- return {
757
- pagesNeedingFullTrace,
758
- fastPathPages,
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
- // 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
- ) {
772
- const declCache = new Map();
773
- const uniqueCharsCache = new Map();
774
- let snappingTime = 0;
775
- let globalUsageTime = 0;
776
- let cloningTime = 0;
777
-
778
- for (const entry of htmlOrSvgAssetTextsWithProps) {
779
- const {
780
- htmlOrSvgAsset,
781
- textByProps,
782
- accumulatedFontFaceDeclarations,
783
- fontFamiliesWithFeatureSettings,
784
- featureTagsByFamily,
785
- } = entry;
786
-
787
- const declKey = getDeclarationsKey(accumulatedFontFaceDeclarations);
788
- if (!declCache.has(declKey)) {
789
- const snapStart = Date.now();
790
- declCache.set(declKey, {
791
- snappedEntries: computeSnappedGlobalEntries(
792
- accumulatedFontFaceDeclarations,
793
- globalTextByProps
794
- ),
795
- fontUsageTemplates: null,
796
- pageTextIndex: null,
797
- preloadIndex: null,
798
- });
799
- snappingTime += Date.now() - snapStart;
800
- }
801
-
802
- const declCacheEntry = declCache.get(declKey);
803
- const globalUsageStart = Date.now();
804
- populateGlobalFontUsages(
805
- declCacheEntry,
806
- accumulatedFontFaceDeclarations,
807
- text
808
- );
809
- globalUsageTime += Date.now() - globalUsageStart;
810
-
811
- const {
812
- fontUsageTemplates,
813
- pageTextIndex,
814
- preloadIndex: textAndPropsToFontUrl,
815
- } = declCacheEntry;
816
-
817
- const preloadFontUrls = new Set();
818
- for (const textByPropsEntry of textByProps) {
819
- const fontUrl = textAndPropsToFontUrl.get(textByPropsEntry);
820
- if (fontUrl) {
821
- preloadFontUrls.add(fontUrl);
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. 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
890
- );
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
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
- 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);
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
- 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) {
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
- } finally {
1037
- if (headlessBrowser) {
1038
- await headlessBrowser.close();
805
+ finally {
806
+ if (headlessBrowser) {
807
+ await headlessBrowser.close();
808
+ }
1039
809
  }
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);
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