@turntrout/subfont 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -43
- package/lib/FontTracerPool.js +49 -1
- package/lib/HeadlessBrowser.js +11 -3
- package/lib/collectTextsByPage.js +425 -352
- package/lib/escapeJsStringLiteral.js +13 -0
- package/lib/extractVisibleText.js +6 -2
- package/lib/fontConverter.js +25 -0
- package/lib/fontConverterWorker.js +16 -0
- package/lib/fontFaceHelpers.js +16 -4
- package/lib/gatherStylesheetsWithPredicates.js +4 -5
- package/lib/normalizeFontPropertyValue.js +1 -1
- package/lib/sfntCache.js +10 -7
- package/lib/subfont.js +57 -16
- package/lib/subsetFontWithGlyphs.js +33 -22
- package/lib/subsetFonts.js +166 -123
- package/lib/unquote.js +9 -4
- package/lib/warnAboutMissingGlyphs.js +36 -25
- package/lib/wasmQueue.js +6 -2
- package/package.json +1 -2
package/lib/subsetFonts.js
CHANGED
|
@@ -16,6 +16,7 @@ const unicodeRange = require('./unicodeRange');
|
|
|
16
16
|
const getFontInfo = require('./getFontInfo');
|
|
17
17
|
const collectTextsByPage = require('./collectTextsByPage');
|
|
18
18
|
|
|
19
|
+
const escapeJsStringLiteral = require('./escapeJsStringLiteral');
|
|
19
20
|
const {
|
|
20
21
|
maybeCssQuote,
|
|
21
22
|
getFontFaceDeclarationText,
|
|
@@ -55,18 +56,6 @@ function getParents(asset, assetQuery) {
|
|
|
55
56
|
return parents;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
// Escape a value for safe inclusion in any JS string context (single-quoted,
|
|
59
|
-
// double-quoted, or template literal). Uses JSON.stringify for robust escaping
|
|
60
|
-
// of backslashes, quotes, newlines, U+2028, U+2029, etc.
|
|
61
|
-
// The < escape prevents </script> from closing an inline script tag.
|
|
62
|
-
function escapeJsStringLiteral(str) {
|
|
63
|
-
return JSON.stringify(str)
|
|
64
|
-
.slice(1, -1)
|
|
65
|
-
.replace(/'/g, "\\'")
|
|
66
|
-
.replace(/`/g, '\\x60')
|
|
67
|
-
.replace(/</g, '\\x3c');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
59
|
function countUniqueFontUrls(htmlOrSvgAssetTextsWithProps) {
|
|
71
60
|
const urls = new Set();
|
|
72
61
|
for (const item of htmlOrSvgAssetTextsWithProps) {
|
|
@@ -225,8 +214,8 @@ const validFontDisplayValues = [
|
|
|
225
214
|
|
|
226
215
|
const warnAboutMissingGlyphs = require('./warnAboutMissingGlyphs');
|
|
227
216
|
|
|
228
|
-
//
|
|
229
|
-
//
|
|
217
|
+
// Create (or retrieve from disk cache) the subset CSS asset for a set of
|
|
218
|
+
// fontUsages, relocating the font binary to its hashed URL under subsetUrl.
|
|
230
219
|
async function getOrCreateSubsetCssAsset({
|
|
231
220
|
assetGraph,
|
|
232
221
|
subsetCssText,
|
|
@@ -322,7 +311,8 @@ async function getOrCreateSubsetCssAsset({
|
|
|
322
311
|
return cssAsset;
|
|
323
312
|
}
|
|
324
313
|
|
|
325
|
-
//
|
|
314
|
+
// Insert <link rel="preload"> hints for every woff2 subset flagged as
|
|
315
|
+
// preload-worthy, so the browser starts fetching them during HTML parse.
|
|
326
316
|
function addSubsetFontPreloads({
|
|
327
317
|
cssAsset,
|
|
328
318
|
fontUsages,
|
|
@@ -373,6 +363,104 @@ function addSubsetFontPreloads({
|
|
|
373
363
|
return insertionPoint;
|
|
374
364
|
}
|
|
375
365
|
|
|
366
|
+
// Skip Google Fonts populate when no Google Fonts references exist —
|
|
367
|
+
// otherwise assetgraph spends ~30s network-walking for nothing on sites
|
|
368
|
+
// that only self-host. Returns whether the populate ran so callers can
|
|
369
|
+
// annotate their phase timing.
|
|
370
|
+
async function populateGoogleFontsIfPresent(assetGraph) {
|
|
371
|
+
const hasGoogleFonts =
|
|
372
|
+
assetGraph.findRelations({
|
|
373
|
+
to: { url: { $regex: googleFontsCssUrlRegex } },
|
|
374
|
+
}).length > 0;
|
|
375
|
+
if (!hasGoogleFonts) return false;
|
|
376
|
+
|
|
377
|
+
await assetGraph.populate({
|
|
378
|
+
followRelations: {
|
|
379
|
+
$or: [
|
|
380
|
+
{ to: { url: { $regex: googleFontsCssUrlRegex } } },
|
|
381
|
+
{
|
|
382
|
+
type: 'CssFontFaceSrc',
|
|
383
|
+
from: { url: { $regex: googleFontsCssUrlRegex } },
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Strip every original @font-face rule when --no-fallbacks is set. The
|
|
392
|
+
// severed assets are returned via the `potentiallyOrphanedAssets` set so
|
|
393
|
+
// the final orphan sweep can remove anything left dangling.
|
|
394
|
+
function removeOriginalFontFaceRules(
|
|
395
|
+
htmlOrSvgAssets,
|
|
396
|
+
fontFaceDeclarationsByHtmlOrSvgAsset,
|
|
397
|
+
potentiallyOrphanedAssets
|
|
398
|
+
) {
|
|
399
|
+
for (const htmlOrSvgAsset of htmlOrSvgAssets) {
|
|
400
|
+
const accumulatedFontFaceDeclarations =
|
|
401
|
+
fontFaceDeclarationsByHtmlOrSvgAsset.get(htmlOrSvgAsset);
|
|
402
|
+
for (const { relations } of accumulatedFontFaceDeclarations) {
|
|
403
|
+
for (const relation of relations) {
|
|
404
|
+
potentiallyOrphanedAssets.add(relation.to);
|
|
405
|
+
if (relation.node.parentNode) {
|
|
406
|
+
relation.node.parentNode.removeChild(relation.node);
|
|
407
|
+
}
|
|
408
|
+
relation.remove();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
htmlOrSvgAsset.markDirty();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Rewrite CSS source-map relations to the caller's chosen hrefType so they
|
|
416
|
+
// align with the rest of the emitted assets. Only invoked when sourceMaps is
|
|
417
|
+
// enabled — subsetFonts normally skips source-map serialization for speed.
|
|
418
|
+
async function rewriteCssSourceMaps(assetGraph, hrefType) {
|
|
419
|
+
await assetGraph.serializeSourceMaps(undefined, {
|
|
420
|
+
type: 'Css',
|
|
421
|
+
outgoingRelations: {
|
|
422
|
+
$where: (relations) =>
|
|
423
|
+
relations.some((relation) => relation.type === 'CssSourceMappingUrl'),
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
for (const relation of assetGraph.findRelations({
|
|
427
|
+
type: 'SourceMapSource',
|
|
428
|
+
})) {
|
|
429
|
+
relation.hrefType = hrefType;
|
|
430
|
+
}
|
|
431
|
+
for (const relation of assetGraph.findRelations({
|
|
432
|
+
type: 'CssSourceMappingUrl',
|
|
433
|
+
hrefType: { $in: ['relative', 'inline'] },
|
|
434
|
+
})) {
|
|
435
|
+
relation.hrefType = hrefType;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Remove assets whose last incoming relation was severed during subset
|
|
440
|
+
// injection (original @font-face rules, merged Google Fonts CSS, etc.) so
|
|
441
|
+
// the emitted site doesn't ship with dangling files.
|
|
442
|
+
function removeOrphanedAssets(assetGraph, potentiallyOrphanedAssets) {
|
|
443
|
+
for (const asset of potentiallyOrphanedAssets) {
|
|
444
|
+
if (asset.incomingRelations.length === 0) {
|
|
445
|
+
assetGraph.removeAsset(asset);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Shape the per-page fontUsages into the external fontInfo report: strip
|
|
451
|
+
// internal bookkeeping (subsets buffer, feature-tag scratch) and flatten
|
|
452
|
+
// each page to { assetFileName, fontUsages }.
|
|
453
|
+
function buildFontInfoReport(htmlOrSvgAssetTextsWithProps) {
|
|
454
|
+
return htmlOrSvgAssetTextsWithProps.map(({ fontUsages, htmlOrSvgAsset }) => ({
|
|
455
|
+
assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
|
|
456
|
+
fontUsages: fontUsages.map((fontUsage) =>
|
|
457
|
+
(({ subsets, hasFontFeatureSettings, fontFeatureTags, ...rest }) => rest)(
|
|
458
|
+
fontUsage
|
|
459
|
+
)
|
|
460
|
+
),
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
|
|
376
464
|
async function subsetFonts(
|
|
377
465
|
assetGraph,
|
|
378
466
|
{
|
|
@@ -415,34 +503,8 @@ async function subsetFonts(
|
|
|
415
503
|
}
|
|
416
504
|
timings.applySourceMaps = applySourceMapsPhase.end();
|
|
417
505
|
|
|
418
|
-
// Only run Google Fonts populate if there are actually Google Fonts
|
|
419
|
-
// references in the graph. This avoids ~30s of wasted work on sites
|
|
420
|
-
// that use only self-hosted fonts.
|
|
421
|
-
const hasGoogleFonts =
|
|
422
|
-
assetGraph.findRelations({
|
|
423
|
-
to: { url: { $regex: googleFontsCssUrlRegex } },
|
|
424
|
-
}).length > 0;
|
|
425
|
-
|
|
426
506
|
const googlePopulatePhase = trackPhase('populate (google fonts)');
|
|
427
|
-
|
|
428
|
-
await assetGraph.populate({
|
|
429
|
-
followRelations: {
|
|
430
|
-
$or: [
|
|
431
|
-
{
|
|
432
|
-
to: {
|
|
433
|
-
url: { $regex: googleFontsCssUrlRegex },
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
|
-
{
|
|
437
|
-
type: 'CssFontFaceSrc',
|
|
438
|
-
from: {
|
|
439
|
-
url: { $regex: googleFontsCssUrlRegex },
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
],
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
|
-
}
|
|
507
|
+
const hasGoogleFonts = await populateGoogleFontsIfPresent(assetGraph);
|
|
446
508
|
timings['populate (google fonts)'] = googlePopulatePhase.end(
|
|
447
509
|
hasGoogleFonts ? null : 'skipped, no Google Fonts found'
|
|
448
510
|
);
|
|
@@ -478,26 +540,14 @@ async function subsetFonts(
|
|
|
478
540
|
timings.collectTextsByPageDetails = subTimings;
|
|
479
541
|
|
|
480
542
|
const omitFallbacksPhase = trackPhase('omitFallbacks processing');
|
|
481
|
-
|
|
482
543
|
const potentiallyOrphanedAssets = new Set();
|
|
483
544
|
if (omitFallbacks) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
for (const relation of relations) {
|
|
490
|
-
potentiallyOrphanedAssets.add(relation.to);
|
|
491
|
-
if (relation.node.parentNode) {
|
|
492
|
-
relation.node.parentNode.removeChild(relation.node);
|
|
493
|
-
}
|
|
494
|
-
relation.remove();
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
htmlOrSvgAsset.markDirty();
|
|
498
|
-
}
|
|
545
|
+
removeOriginalFontFaceRules(
|
|
546
|
+
htmlOrSvgAssets,
|
|
547
|
+
fontFaceDeclarationsByHtmlOrSvgAsset,
|
|
548
|
+
potentiallyOrphanedAssets
|
|
549
|
+
);
|
|
499
550
|
}
|
|
500
|
-
|
|
501
551
|
timings['omitFallbacks processing'] = omitFallbacksPhase.end();
|
|
502
552
|
|
|
503
553
|
const codepointPhase = trackPhase('codepoint generation');
|
|
@@ -652,22 +702,26 @@ async function subsetFonts(
|
|
|
652
702
|
|
|
653
703
|
// Insert subsets:
|
|
654
704
|
|
|
655
|
-
// Pre-compute which fontUrls are used (with text) on every page
|
|
656
|
-
//
|
|
705
|
+
// Pre-compute which fontUrls are used (with text) on every page.
|
|
706
|
+
// Set intersection: O(pages × fonts_per_page) vs the old every+some approach.
|
|
657
707
|
const fontUrlsUsedOnEveryPage = new Set();
|
|
658
708
|
if (htmlOrSvgAssetTextsWithProps.length > 0) {
|
|
659
|
-
// Start with all fontUrls from the first page
|
|
660
709
|
const firstPageFontUrls = new Set();
|
|
661
710
|
for (const fu of htmlOrSvgAssetTextsWithProps[0].fontUsages) {
|
|
662
711
|
if (fu.pageText) firstPageFontUrls.add(fu.fontUrl);
|
|
663
712
|
}
|
|
664
713
|
for (const fontUrl of firstPageFontUrls) {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
) {
|
|
670
|
-
|
|
714
|
+
fontUrlsUsedOnEveryPage.add(fontUrl);
|
|
715
|
+
}
|
|
716
|
+
for (let i = 1; i < htmlOrSvgAssetTextsWithProps.length; i++) {
|
|
717
|
+
const pageFontUrls = new Set();
|
|
718
|
+
for (const fu of htmlOrSvgAssetTextsWithProps[i].fontUsages) {
|
|
719
|
+
if (fu.pageText) pageFontUrls.add(fu.fontUrl);
|
|
720
|
+
}
|
|
721
|
+
for (const fontUrl of fontUrlsUsedOnEveryPage) {
|
|
722
|
+
if (!pageFontUrls.has(fontUrl)) {
|
|
723
|
+
fontUrlsUsedOnEveryPage.delete(fontUrl);
|
|
724
|
+
}
|
|
671
725
|
}
|
|
672
726
|
}
|
|
673
727
|
}
|
|
@@ -676,6 +730,12 @@ async function subsetFonts(
|
|
|
676
730
|
// addAsset/minify/removeAsset cycles for pages sharing identical CSS.
|
|
677
731
|
const subsetCssAssetCache = new Map();
|
|
678
732
|
|
|
733
|
+
// Cache the heavy CSS-text assembly (including base64-encoded font data)
|
|
734
|
+
// keyed by the shared accumulatedFontFaceDeclarations array. Pages grouped
|
|
735
|
+
// under the same stylesheet config produce byte-identical output, so this
|
|
736
|
+
// collapses the per-page string build from O(pages) to O(unique configs).
|
|
737
|
+
const subsetCssTextCache = new WeakMap();
|
|
738
|
+
|
|
679
739
|
// Pre-index relations by source asset to avoid O(allRelations) scans
|
|
680
740
|
// in the per-page injection loop below. Build indices once, then use
|
|
681
741
|
// O(1) lookups per page instead of repeated assetGraph.findRelations.
|
|
@@ -773,11 +833,19 @@ async function subsetFonts(
|
|
|
773
833
|
}
|
|
774
834
|
numFontUsagesWithSubset += subsetFontUsages.length;
|
|
775
835
|
|
|
776
|
-
let
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
836
|
+
let cssTextParts = subsetCssTextCache.get(accumulatedFontFaceDeclarations);
|
|
837
|
+
if (!cssTextParts) {
|
|
838
|
+
cssTextParts = {
|
|
839
|
+
subset: getFontUsageStylesheet(subsetFontUsages),
|
|
840
|
+
unused: getUnusedVariantsStylesheet(
|
|
841
|
+
fontUsages,
|
|
842
|
+
accumulatedFontFaceDeclarations
|
|
843
|
+
),
|
|
844
|
+
};
|
|
845
|
+
subsetCssTextCache.set(accumulatedFontFaceDeclarations, cssTextParts);
|
|
846
|
+
}
|
|
847
|
+
let subsetCssText = cssTextParts.subset;
|
|
848
|
+
const unusedVariantsCss = cssTextParts.unused;
|
|
781
849
|
if (!inlineCss && !omitFallbacks) {
|
|
782
850
|
// This can go into the same stylesheet because we won't reload all __subset suffixed families in the JS preload fallback
|
|
783
851
|
subsetCssText += unusedVariantsCss;
|
|
@@ -971,11 +1039,16 @@ async function subsetFonts(
|
|
|
971
1039
|
|
|
972
1040
|
const googleCleanupPhase = trackPhase('Google Fonts + cleanup');
|
|
973
1041
|
|
|
974
|
-
// Async load Google Web Fonts CSS
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1042
|
+
// Async load Google Web Fonts CSS. Skip the regex findAssets scan and
|
|
1043
|
+
// the surrounding loop entirely when no Google Fonts were detected up
|
|
1044
|
+
// front — the final detach loop below still runs because other phases
|
|
1045
|
+
// (lazy fallback CSS) populate relationsToRemove.
|
|
1046
|
+
const googleFontStylesheets = hasGoogleFonts
|
|
1047
|
+
? assetGraph.findAssets({
|
|
1048
|
+
type: 'Css',
|
|
1049
|
+
url: { $regex: googleFontsCssUrlRegex },
|
|
1050
|
+
})
|
|
1051
|
+
: [];
|
|
979
1052
|
const selfHostedGoogleCssByUrl = new Map();
|
|
980
1053
|
for (const googleFontStylesheet of googleFontStylesheets) {
|
|
981
1054
|
const seenPages = new Set(); // Only do the work once for each font on each page
|
|
@@ -1053,7 +1126,7 @@ async function subsetFonts(
|
|
|
1053
1126
|
|
|
1054
1127
|
// Use subsets in font-family:
|
|
1055
1128
|
|
|
1056
|
-
const webfontNameMap =
|
|
1129
|
+
const webfontNameMap = Object.create(null);
|
|
1057
1130
|
|
|
1058
1131
|
for (const { fontUsages } of htmlOrSvgAssetTextsWithProps) {
|
|
1059
1132
|
for (const { subsets, fontFamilies, props } of fontUsages) {
|
|
@@ -1067,6 +1140,7 @@ async function subsetFonts(
|
|
|
1067
1140
|
}
|
|
1068
1141
|
|
|
1069
1142
|
let customPropertyDefinitions; // Avoid computing this unless necessary
|
|
1143
|
+
const cssAssetsDirtiedByCustomProps = new Set();
|
|
1070
1144
|
// Inject subset font name before original webfont in SVG font-family attributes
|
|
1071
1145
|
const svgAssets = assetGraph.findAssets({ type: 'Svg' });
|
|
1072
1146
|
for (const svgAsset of svgAssets) {
|
|
@@ -1105,7 +1179,10 @@ async function subsetFonts(
|
|
|
1105
1179
|
type: 'Css',
|
|
1106
1180
|
isLoaded: true,
|
|
1107
1181
|
});
|
|
1108
|
-
|
|
1182
|
+
const parseTreeToAsset = new Map();
|
|
1183
|
+
for (const cssAsset of cssAssets) {
|
|
1184
|
+
parseTreeToAsset.set(cssAsset.parseTree, cssAsset);
|
|
1185
|
+
}
|
|
1109
1186
|
for (const cssAsset of cssAssets) {
|
|
1110
1187
|
let changesMade = false;
|
|
1111
1188
|
cssAsset.eachRuleInParseTree((cssRule) => {
|
|
@@ -1133,7 +1210,10 @@ async function subsetFonts(
|
|
|
1133
1210
|
);
|
|
1134
1211
|
if (modifiedValue !== relatedCssRule.value) {
|
|
1135
1212
|
relatedCssRule.value = modifiedValue;
|
|
1136
|
-
|
|
1213
|
+
const ownerAsset = parseTreeToAsset.get(relatedCssRule.root());
|
|
1214
|
+
if (ownerAsset) {
|
|
1215
|
+
cssAssetsDirtiedByCustomProps.add(ownerAsset);
|
|
1216
|
+
}
|
|
1137
1217
|
}
|
|
1138
1218
|
}
|
|
1139
1219
|
}
|
|
@@ -1190,60 +1270,23 @@ async function subsetFonts(
|
|
|
1190
1270
|
}
|
|
1191
1271
|
}
|
|
1192
1272
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
for (const cssAsset of cssAssets) {
|
|
1196
|
-
cssAsset.markDirty();
|
|
1197
|
-
}
|
|
1273
|
+
for (const dirtiedAsset of cssAssetsDirtiedByCustomProps) {
|
|
1274
|
+
dirtiedAsset.markDirty();
|
|
1198
1275
|
}
|
|
1199
1276
|
|
|
1200
1277
|
timings['inject subset font-family'] = injectPhase.end();
|
|
1201
1278
|
|
|
1202
1279
|
const orphanCleanupPhase = trackPhase('source maps + orphan cleanup');
|
|
1203
1280
|
if (sourceMaps) {
|
|
1204
|
-
await assetGraph
|
|
1205
|
-
type: 'Css',
|
|
1206
|
-
outgoingRelations: {
|
|
1207
|
-
$where: (relations) =>
|
|
1208
|
-
relations.some((relation) => relation.type === 'CssSourceMappingUrl'),
|
|
1209
|
-
},
|
|
1210
|
-
});
|
|
1211
|
-
for (const relation of assetGraph.findRelations({
|
|
1212
|
-
type: 'SourceMapSource',
|
|
1213
|
-
})) {
|
|
1214
|
-
relation.hrefType = hrefType;
|
|
1215
|
-
}
|
|
1216
|
-
for (const relation of assetGraph.findRelations({
|
|
1217
|
-
type: 'CssSourceMappingUrl',
|
|
1218
|
-
hrefType: { $in: ['relative', 'inline'] },
|
|
1219
|
-
})) {
|
|
1220
|
-
relation.hrefType = hrefType;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
for (const asset of potentiallyOrphanedAssets) {
|
|
1225
|
-
if (asset.incomingRelations.length === 0) {
|
|
1226
|
-
assetGraph.removeAsset(asset);
|
|
1227
|
-
}
|
|
1281
|
+
await rewriteCssSourceMaps(assetGraph, hrefType);
|
|
1228
1282
|
}
|
|
1229
|
-
|
|
1283
|
+
removeOrphanedAssets(assetGraph, potentiallyOrphanedAssets);
|
|
1230
1284
|
timings['source maps + orphan cleanup'] = orphanCleanupPhase.end();
|
|
1231
1285
|
|
|
1232
|
-
// Hand out some useful info about the detected subsets:
|
|
1233
1286
|
return {
|
|
1234
|
-
fontInfo: htmlOrSvgAssetTextsWithProps
|
|
1235
|
-
({ fontUsages, htmlOrSvgAsset }) => ({
|
|
1236
|
-
assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
|
|
1237
|
-
fontUsages: fontUsages.map((fontUsage) =>
|
|
1238
|
-
(({ subsets, hasFontFeatureSettings, fontFeatureTags, ...rest }) =>
|
|
1239
|
-
rest)(fontUsage)
|
|
1240
|
-
),
|
|
1241
|
-
})
|
|
1242
|
-
),
|
|
1287
|
+
fontInfo: buildFontInfoReport(htmlOrSvgAssetTextsWithProps),
|
|
1243
1288
|
timings,
|
|
1244
1289
|
};
|
|
1245
1290
|
}
|
|
1246
1291
|
|
|
1247
1292
|
module.exports = subsetFonts;
|
|
1248
|
-
// Exported for testing
|
|
1249
|
-
module.exports._escapeJsStringLiteral = escapeJsStringLiteral;
|
package/lib/unquote.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
function unescapeCssString(str) {
|
|
2
2
|
return str.replace(
|
|
3
3
|
/\\([0-9a-f]{1,6})(\s?)/gi,
|
|
4
|
-
($0, hexChars, followingWhitespace) =>
|
|
5
|
-
|
|
6
|
-
hexChars
|
|
7
|
-
|
|
4
|
+
($0, hexChars, followingWhitespace) => {
|
|
5
|
+
try {
|
|
6
|
+
return `${String.fromCodePoint(parseInt(hexChars, 16))}${
|
|
7
|
+
hexChars.length === 6 ? followingWhitespace : ''
|
|
8
|
+
}`;
|
|
9
|
+
} catch {
|
|
10
|
+
return $0;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
8
13
|
);
|
|
9
14
|
}
|
|
10
15
|
|
|
@@ -42,6 +42,41 @@ async function warnAboutMissingGlyphs(
|
|
|
42
42
|
accumulatedFontFaceDeclarations,
|
|
43
43
|
} of htmlOrSvgAssetTextsWithProps) {
|
|
44
44
|
let linesAndColumns;
|
|
45
|
+
// Dedupe scans for the same missing char across different fontUsages on
|
|
46
|
+
// this page. On KaTeX-heavy pages the same symbol is often missing in
|
|
47
|
+
// several font-families, and each scan is an O(N) walk of the HTML text.
|
|
48
|
+
const charLookupCache = new Map();
|
|
49
|
+
const lookupChar = (char) => {
|
|
50
|
+
let cached = charLookupCache.get(char);
|
|
51
|
+
if (cached) return cached;
|
|
52
|
+
let firstLocation;
|
|
53
|
+
let occurrences = 0;
|
|
54
|
+
if (char.length > 0) {
|
|
55
|
+
const sourceText = htmlOrSvgAsset.text;
|
|
56
|
+
let searchIdx = 0;
|
|
57
|
+
while (true) {
|
|
58
|
+
const charIdx = sourceText.indexOf(char, searchIdx);
|
|
59
|
+
if (charIdx === -1) break;
|
|
60
|
+
occurrences++;
|
|
61
|
+
if (occurrences === 1) {
|
|
62
|
+
if (!linesAndColumns) {
|
|
63
|
+
linesAndColumns = new LinesAndColumns(sourceText);
|
|
64
|
+
}
|
|
65
|
+
const position = linesAndColumns.locationForIndex(charIdx);
|
|
66
|
+
firstLocation = `${htmlOrSvgAsset.urlOrDescription}:${
|
|
67
|
+
position.line + 1
|
|
68
|
+
}:${position.column + 1}`;
|
|
69
|
+
}
|
|
70
|
+
searchIdx = charIdx + char.length;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!firstLocation) {
|
|
74
|
+
firstLocation = `${htmlOrSvgAsset.urlOrDescription} (generated content)`;
|
|
75
|
+
}
|
|
76
|
+
cached = { firstLocation, occurrences };
|
|
77
|
+
charLookupCache.set(char, cached);
|
|
78
|
+
return cached;
|
|
79
|
+
};
|
|
45
80
|
for (const fontUsage of fontUsages) {
|
|
46
81
|
if (!fontUsage.subsets) continue;
|
|
47
82
|
const subsetBuffer = Object.values(fontUsage.subsets)[0];
|
|
@@ -63,31 +98,7 @@ async function warnAboutMissingGlyphs(
|
|
|
63
98
|
// Report only the first location plus a count of remaining
|
|
64
99
|
// occurrences. A character like U+200B can appear thousands of
|
|
65
100
|
// times on a page and per-occurrence lines drown the log.
|
|
66
|
-
|
|
67
|
-
let occurrences = 0;
|
|
68
|
-
if (char.length > 0) {
|
|
69
|
-
const sourceText = htmlOrSvgAsset.text;
|
|
70
|
-
let searchIdx = 0;
|
|
71
|
-
while (true) {
|
|
72
|
-
const charIdx = sourceText.indexOf(char, searchIdx);
|
|
73
|
-
if (charIdx === -1) break;
|
|
74
|
-
occurrences++;
|
|
75
|
-
if (occurrences === 1) {
|
|
76
|
-
if (!linesAndColumns) {
|
|
77
|
-
linesAndColumns = new LinesAndColumns(sourceText);
|
|
78
|
-
}
|
|
79
|
-
const position = linesAndColumns.locationForIndex(charIdx);
|
|
80
|
-
firstLocation = `${htmlOrSvgAsset.urlOrDescription}:${
|
|
81
|
-
position.line + 1
|
|
82
|
-
}:${position.column + 1}`;
|
|
83
|
-
}
|
|
84
|
-
searchIdx = charIdx + char.length;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!firstLocation) {
|
|
89
|
-
firstLocation = `${htmlOrSvgAsset.urlOrDescription} (generated content)`;
|
|
90
|
-
}
|
|
101
|
+
const { firstLocation, occurrences } = lookupChar(char);
|
|
91
102
|
|
|
92
103
|
missingGlyphsErrors.push({
|
|
93
104
|
codePoint,
|
package/lib/wasmQueue.js
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
let queue = Promise.resolve();
|
|
6
6
|
|
|
7
7
|
function enqueue(fn) {
|
|
8
|
-
|
|
8
|
+
// Chain fn after the previous task settles. Both handlers wrap fn() in
|
|
9
|
+
// an arrow to avoid leaking the previous result/error as an argument.
|
|
10
|
+
// The error handler ensures a prior rejection doesn't block the queue.
|
|
11
|
+
queue = queue.then(
|
|
9
12
|
() => fn(),
|
|
10
13
|
() => fn()
|
|
11
|
-
)
|
|
14
|
+
);
|
|
15
|
+
return queue;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
module.exports = enqueue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@turntrout/subfont",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Automatically subset web fonts to only the characters used on your pages. Fork of Munter/subfont with modern defaults.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -57,7 +57,6 @@
|
|
|
57
57
|
"jsdom": "^25.0.0",
|
|
58
58
|
"lines-and-columns": "^1.1.6",
|
|
59
59
|
"memoizesync": "^1.1.1",
|
|
60
|
-
"p-limit": "^3.0.0",
|
|
61
60
|
"parse5": "^7.0.0",
|
|
62
61
|
"postcss": "^8.3.11",
|
|
63
62
|
"postcss-value-parser": "^4.0.2",
|