@turntrout/subfont 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -0
- package/README.md +1 -10
- package/lib/FontTracerPool.js +10 -0
- package/lib/HeadlessBrowser.js +19 -31
- package/lib/collectFeatureGlyphIds.js +1 -23
- package/lib/collectTextsByPage.js +8 -28
- package/lib/getCssRulesByProperty.js +64 -138
- package/lib/injectSubsetDefinitions.js +4 -4
- package/lib/parseCommandLineOptions.js +0 -4
- package/lib/sfntCache.js +0 -3
- package/lib/subfont.js +30 -24
- package/lib/subsetFontWithGlyphs.js +4 -28
- package/lib/subsetFonts.js +8 -8
- package/lib/subsetGeneration.js +132 -180
- package/lib/variationAxes.js +13 -3
- package/lib/warnAboutMissingGlyphs.js +0 -3
- package/package.json +11 -24
package/LICENSE.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Copyright 2017 Peter Brandt Müller
|
|
2
|
+
Copyright 2024-2026 Alexander Turner
|
|
2
3
|
|
|
3
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
5
|
|
package/README.md
CHANGED
|
@@ -2,16 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/alexander-turner/subfont/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
A faster fork of [subfont](https://github.com/Munter/subfont) that subsets web fonts to only the characters used on your pages. Adds parallel tracing, disk caching, woff2-only output, and always-on variable font instancing.
|
|
6
|
-
|
|
7
|
-
## Performance
|
|
8
|
-
|
|
9
|
-
On [TurnTrout.com](https://github.com/alexander-turner/TurnTrout.com) (382 pages, 20+ font variants), switching to this fork cut font subsetting from **111 minutes to 28 minutes**:
|
|
10
|
-
|
|
11
|
-
| | Version | Duration |
|
|
12
|
-
| -----------------------------------------------------------------------------------: | :------------: | :------- |
|
|
13
|
-
| [Before](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23470135763) | Munter/subfont | 111 min |
|
|
14
|
-
| [After](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23518006824) | This fork | 28 min |
|
|
5
|
+
A faster fork of [subfont](https://github.com/Munter/subfont) that subsets web fonts to only the characters used on your pages. Adds parallel tracing, disk caching, woff2-only output, and always-on variable font instancing. On [`turntrout.com`](https://github.com/alexander-turner/TurnTrout.com) (382 pages, 20+ font variants), switching to this fork cut font subsetting from [111 minutes](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23470135763) to [28 minutes](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23518006824).
|
|
15
6
|
|
|
16
7
|
## Install
|
|
17
8
|
|
package/lib/FontTracerPool.js
CHANGED
|
@@ -149,6 +149,16 @@ class FontTracerPool {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
async destroy() {
|
|
152
|
+
// Reject any tasks still waiting in the queue
|
|
153
|
+
for (const task of this._pendingTasks) {
|
|
154
|
+
const cb = this._taskCallbacks.get(task.message.taskId);
|
|
155
|
+
if (cb) {
|
|
156
|
+
this._taskCallbacks.delete(task.message.taskId);
|
|
157
|
+
cb.reject(new Error('Worker pool destroyed'));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
this._pendingTasks = [];
|
|
161
|
+
|
|
152
162
|
await Promise.all(this._workers.map((w) => w.terminate()));
|
|
153
163
|
this._workers = [];
|
|
154
164
|
this._idle = [];
|
package/lib/HeadlessBrowser.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const urlTools = require('urltools');
|
|
2
2
|
const puppeteer = require('puppeteer-core');
|
|
3
3
|
const pathModule = require('path');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const {
|
|
5
6
|
install,
|
|
6
|
-
uninstall,
|
|
7
7
|
Browser,
|
|
8
8
|
detectBrowserPlatform,
|
|
9
9
|
Cache,
|
|
@@ -19,7 +19,10 @@ async function transferResults(jsHandle) {
|
|
|
19
19
|
return results;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
async function downloadOrLocatePreferredBrowserRevision(
|
|
22
|
+
async function downloadOrLocatePreferredBrowserRevision(
|
|
23
|
+
extraArgs = [],
|
|
24
|
+
log = console
|
|
25
|
+
) {
|
|
23
26
|
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
|
|
24
27
|
return puppeteer.launch({
|
|
25
28
|
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
|
|
@@ -37,7 +40,7 @@ async function downloadOrLocatePreferredBrowserRevision(extraArgs = []) {
|
|
|
37
40
|
} else {
|
|
38
41
|
// Check the default puppeteer cache (~/.cache/puppeteer) before downloading
|
|
39
42
|
const defaultCacheDir = pathModule.join(
|
|
40
|
-
|
|
43
|
+
os.homedir(),
|
|
41
44
|
'.cache',
|
|
42
45
|
'puppeteer'
|
|
43
46
|
);
|
|
@@ -49,7 +52,7 @@ async function downloadOrLocatePreferredBrowserRevision(extraArgs = []) {
|
|
|
49
52
|
if (defaultChromeEntry) {
|
|
50
53
|
executablePath = defaultChromeEntry.executablePath;
|
|
51
54
|
} else {
|
|
52
|
-
|
|
55
|
+
log.log('Downloading Chrome');
|
|
53
56
|
const result = await install({
|
|
54
57
|
browser: Browser.CHROME,
|
|
55
58
|
buildId: 'stable',
|
|
@@ -57,27 +60,6 @@ async function downloadOrLocatePreferredBrowserRevision(extraArgs = []) {
|
|
|
57
60
|
platform,
|
|
58
61
|
});
|
|
59
62
|
executablePath = result.executablePath;
|
|
60
|
-
|
|
61
|
-
// Clean up older Chrome versions that may have accumulated from
|
|
62
|
-
// previous runs with different stable buildIds.
|
|
63
|
-
const allInstalled = cache.getInstalledBrowsers();
|
|
64
|
-
for (const entry of allInstalled) {
|
|
65
|
-
if (
|
|
66
|
-
entry.browser === Browser.CHROME &&
|
|
67
|
-
entry.executablePath !== executablePath
|
|
68
|
-
) {
|
|
69
|
-
try {
|
|
70
|
-
await uninstall({
|
|
71
|
-
browser: entry.browser,
|
|
72
|
-
buildId: entry.buildId,
|
|
73
|
-
cacheDir,
|
|
74
|
-
});
|
|
75
|
-
console.log(`Removed old Chrome ${entry.buildId}`);
|
|
76
|
-
} catch {
|
|
77
|
-
// Ignore cleanup errors — the old version may be in use or locked
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
63
|
}
|
|
82
64
|
}
|
|
83
65
|
return puppeteer.launch({
|
|
@@ -92,13 +74,19 @@ class HeadlessBrowser {
|
|
|
92
74
|
this._chromeArgs = chromeArgs;
|
|
93
75
|
}
|
|
94
76
|
|
|
95
|
-
_ensureBrowserDownloaded() {}
|
|
96
|
-
|
|
97
77
|
_launchBrowserMemoized() {
|
|
98
|
-
// Make sure we only download and launch one browser per HeadlessBrowser instance
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
downloadOrLocatePreferredBrowserRevision(
|
|
78
|
+
// Make sure we only download and launch one browser per HeadlessBrowser instance.
|
|
79
|
+
// Clear the cached promise on failure so a subsequent call can retry.
|
|
80
|
+
if (!this._launchPromise) {
|
|
81
|
+
this._launchPromise = downloadOrLocatePreferredBrowserRevision(
|
|
82
|
+
this._chromeArgs,
|
|
83
|
+
this.console
|
|
84
|
+
).catch((err) => {
|
|
85
|
+
this._launchPromise = undefined;
|
|
86
|
+
throw err;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return this._launchPromise;
|
|
102
90
|
}
|
|
103
91
|
|
|
104
92
|
async tracePage(htmlAsset) {
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
const { toSfnt } = require('./sfntCache');
|
|
2
2
|
|
|
3
|
-
// Standard OpenType GSUB feature tags that can substitute glyphs.
|
|
4
|
-
// We intersect with the font's actual GSUB features to avoid
|
|
5
|
-
// unnecessary shaping calls.
|
|
6
3
|
const GSUB_FEATURE_TAGS = new Set([
|
|
7
4
|
'aalt',
|
|
8
5
|
'c2sc',
|
|
@@ -47,19 +44,6 @@ const GSUB_FEATURE_TAGS = new Set([
|
|
|
47
44
|
'zero',
|
|
48
45
|
]);
|
|
49
46
|
|
|
50
|
-
/**
|
|
51
|
-
* Collect glyph IDs produced by GSUB features for the given text.
|
|
52
|
-
*
|
|
53
|
-
* Uses harfbuzzjs face.getTableFeatureTags('GSUB') to enumerate the
|
|
54
|
-
* font's actual GSUB features, then only tests those that are in our
|
|
55
|
-
* known substitution set. Collects ALL output glyph IDs from each
|
|
56
|
-
* shaping result (not just the first), to handle ligatures and
|
|
57
|
-
* multi-glyph substitutions.
|
|
58
|
-
*
|
|
59
|
-
* @param {Buffer} fontBuffer - The original font data
|
|
60
|
-
* @param {string} text - The text whose characters to check
|
|
61
|
-
* @returns {Promise<number[]>} Array of alternate glyph IDs
|
|
62
|
-
*/
|
|
63
47
|
const enqueueWasm = require('./wasmQueue');
|
|
64
48
|
|
|
65
49
|
async function collectFeatureGlyphIdsImpl(fontBuffer, text) {
|
|
@@ -71,8 +55,6 @@ async function collectFeatureGlyphIdsImpl(fontBuffer, text) {
|
|
|
71
55
|
const font = harfbuzzJs.createFont(face);
|
|
72
56
|
|
|
73
57
|
try {
|
|
74
|
-
// Use harfbuzzjs to enumerate GSUB features directly from the font,
|
|
75
|
-
// then intersect with our known substitution tags
|
|
76
58
|
const fontFeatures = new Set(face.getTableFeatureTags('GSUB'));
|
|
77
59
|
const featuresToTest = [...fontFeatures].filter((tag) =>
|
|
78
60
|
GSUB_FEATURE_TAGS.has(tag)
|
|
@@ -80,14 +62,11 @@ async function collectFeatureGlyphIdsImpl(fontBuffer, text) {
|
|
|
80
62
|
|
|
81
63
|
if (featuresToTest.length === 0) return [];
|
|
82
64
|
|
|
83
|
-
//
|
|
84
|
-
// Shaping the full string once per feature is O(features) HarfBuzz
|
|
85
|
-
// calls instead of O(chars × features) with per-character shaping.
|
|
65
|
+
// Shape the full string once per feature: O(features) calls, not O(chars × features).
|
|
86
66
|
const uniqueChars = [...new Set(text)].filter((ch) => ch.trim() !== '');
|
|
87
67
|
if (uniqueChars.length === 0) return [];
|
|
88
68
|
const testText = uniqueChars.join('');
|
|
89
69
|
|
|
90
|
-
// Get base glyph IDs (no features)
|
|
91
70
|
const baseBuf = harfbuzzJs.createBuffer();
|
|
92
71
|
let baseGids;
|
|
93
72
|
try {
|
|
@@ -103,7 +82,6 @@ async function collectFeatureGlyphIdsImpl(fontBuffer, text) {
|
|
|
103
82
|
|
|
104
83
|
const altGlyphIds = new Set();
|
|
105
84
|
|
|
106
|
-
// Shape the full text with each feature and collect alternate glyph IDs
|
|
107
85
|
for (const feat of featuresToTest) {
|
|
108
86
|
const buf = harfbuzzJs.createBuffer();
|
|
109
87
|
try {
|
|
@@ -19,18 +19,9 @@ const {
|
|
|
19
19
|
uniqueCharsFromArray,
|
|
20
20
|
} = require('./fontFaceHelpers');
|
|
21
21
|
|
|
22
|
-
// Inline stylesheets matching this regex contain font-related CSS and
|
|
23
|
-
// must be included in the fast-path grouping key. Non-matching inline
|
|
24
|
-
// CSS (e.g., layout-only critical CSS) is excluded so pages that differ
|
|
25
|
-
// only in non-font inline styles still share a single fontTracer run.
|
|
26
22
|
const fontRelevantCssRegex =
|
|
27
23
|
/font-family|font-weight|font-style|font-stretch|font-display|@font-face|font-variation|font-feature/i;
|
|
28
24
|
|
|
29
|
-
// Detect inline style attributes with font-related properties.
|
|
30
|
-
// Used to exclude pages from fast-path when inline styles could affect
|
|
31
|
-
// font-tracer output (since the stylesheet cache key doesn't cover them).
|
|
32
|
-
// Matches style="..." containing font-family, font-weight, font-style,
|
|
33
|
-
// font-stretch, or the font shorthand (font:).
|
|
34
25
|
// The \s before style ensures we don't match data-style or similar.
|
|
35
26
|
const inlineFontStyleRegex =
|
|
36
27
|
/(?:^|\s)style\s*=\s*["'][^"']*\b(?:font-family|font-weight|font-style|font-stretch|font\s*:)/i;
|
|
@@ -38,10 +29,12 @@ function hasInlineFontStyles(html) {
|
|
|
38
29
|
return inlineFontStyleRegex.test(html);
|
|
39
30
|
}
|
|
40
31
|
|
|
41
|
-
// Relation types followed when traversing from HTML to CSS for @font-face gathering
|
|
42
32
|
const fontFaceTraversalTypes = new Set(['HtmlStyle', 'SvgStyle', 'CssImport']);
|
|
43
33
|
|
|
44
|
-
//
|
|
34
|
+
// Minimum number of pages that justifies spawning a worker pool (below this
|
|
35
|
+
// the overhead of worker thread startup exceeds the parallelism benefit).
|
|
36
|
+
const MIN_PAGES_FOR_WORKER_POOL = 4;
|
|
37
|
+
|
|
45
38
|
const featureSettingsProps = new Set([
|
|
46
39
|
'font-feature-settings',
|
|
47
40
|
'font-variant-alternates',
|
|
@@ -102,8 +95,7 @@ const initialValueByProp = {
|
|
|
102
95
|
'font-stretch': allInitialValues['font-stretch'],
|
|
103
96
|
};
|
|
104
97
|
|
|
105
|
-
// Null byte delimiter — CSS property values cannot contain \0
|
|
106
|
-
// so this is collision-safe and cheaper than JSON.stringify in hot loops.
|
|
98
|
+
// Null byte delimiter is collision-safe — CSS property values cannot contain \0.
|
|
107
99
|
function fontPropsKey(family, weight, style, stretch) {
|
|
108
100
|
return `${family}\0${weight}\0${style}\0${stretch}`;
|
|
109
101
|
}
|
|
@@ -230,10 +222,6 @@ function getOrComputeGlobalFontUsages(
|
|
|
230
222
|
|
|
231
223
|
const snappedGlobalEntries = cached.snappedEntries;
|
|
232
224
|
|
|
233
|
-
// Build all indices in a single pass over snappedGlobalEntries:
|
|
234
|
-
// - pageTextIndex: Map<htmlOrSvgAsset, Map<fontUrl, string[]>> for pageText
|
|
235
|
-
// - entriesByFontUrl: Map<fontUrl, entry[]> for building templates
|
|
236
|
-
// - textAndPropsToFontUrl: Map<textAndProps, fontUrl> for preload (inverted index)
|
|
237
225
|
const pageTextIndex = new Map();
|
|
238
226
|
const entriesByFontUrl = new Map();
|
|
239
227
|
const textAndPropsToFontUrl = new Map();
|
|
@@ -241,7 +229,6 @@ function getOrComputeGlobalFontUsages(
|
|
|
241
229
|
for (const entry of snappedGlobalEntries) {
|
|
242
230
|
if (!entry.fontUrl) continue;
|
|
243
231
|
|
|
244
|
-
// pageTextIndex: group texts by (asset, fontUrl)
|
|
245
232
|
const asset = entry.textAndProps.htmlOrSvgAsset;
|
|
246
233
|
let assetMap = pageTextIndex.get(asset);
|
|
247
234
|
if (!assetMap) {
|
|
@@ -255,7 +242,6 @@ function getOrComputeGlobalFontUsages(
|
|
|
255
242
|
}
|
|
256
243
|
texts.push(entry.textAndProps.text);
|
|
257
244
|
|
|
258
|
-
// entriesByFontUrl: group entries by fontUrl
|
|
259
245
|
let arr = entriesByFontUrl.get(entry.fontUrl);
|
|
260
246
|
if (!arr) {
|
|
261
247
|
arr = [];
|
|
@@ -263,14 +249,8 @@ function getOrComputeGlobalFontUsages(
|
|
|
263
249
|
}
|
|
264
250
|
arr.push(entry);
|
|
265
251
|
|
|
266
|
-
// Inverted preload index: textAndProps -> fontUrl
|
|
267
|
-
// In the per-page loop we iterate the page's small textByProps and
|
|
268
|
-
// look up which fontUrls they map to, making preload O(|pageTextByProps|).
|
|
269
252
|
textAndPropsToFontUrl.set(entry.textAndProps, entry.fontUrl);
|
|
270
253
|
}
|
|
271
|
-
|
|
272
|
-
// Also collect subfont-text / text param contributions per fontUrl
|
|
273
|
-
// These are the same for every page sharing this declarations key
|
|
274
254
|
const extraTextsByFontUrl = new Map();
|
|
275
255
|
for (const fontFaceDeclaration of accumulatedFontFaceDeclarations) {
|
|
276
256
|
const {
|
|
@@ -349,7 +329,6 @@ function getOrComputeGlobalFontUsages(
|
|
|
349
329
|
|
|
350
330
|
const props =
|
|
351
331
|
fontEntries.length > 0 ? { ...fontEntries[0].props } : { ...extra.props };
|
|
352
|
-
// Pre-join the extra texts (subfont-text / text param) for pageText computation
|
|
353
332
|
const extraTextsStr = extra ? extra.texts.join('') : '';
|
|
354
333
|
|
|
355
334
|
fontUsageTemplates.push({
|
|
@@ -450,7 +429,6 @@ async function collectTextsByPage(
|
|
|
450
429
|
}
|
|
451
430
|
}
|
|
452
431
|
})(htmlOrSvgAsset, false);
|
|
453
|
-
// Key parts are structured id:media:ns strings — simple join is safe
|
|
454
432
|
return keyParts.join('\x1d');
|
|
455
433
|
}
|
|
456
434
|
|
|
@@ -640,7 +618,9 @@ async function collectTextsByPage(
|
|
|
640
618
|
);
|
|
641
619
|
|
|
642
620
|
// Use worker pool for parallel fontTracer when there are enough pages
|
|
643
|
-
const useWorkerPool =
|
|
621
|
+
const useWorkerPool =
|
|
622
|
+
!headlessBrowser &&
|
|
623
|
+
pagesNeedingFullTrace.length >= MIN_PAGES_FOR_WORKER_POOL;
|
|
644
624
|
|
|
645
625
|
const tracingStart = Date.now();
|
|
646
626
|
try {
|
|
@@ -84,35 +84,36 @@ function getCssRulesByProperty(properties, cssSource, existingPredicates) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function pushRulePerSelector(node, prop, value) {
|
|
88
|
+
getSpecificity(node.parent.selector).forEach((specificityObject) => {
|
|
89
|
+
const isStyleAttribute = specificityObject.selector === 'bogusselector';
|
|
90
|
+
(rulesByProperty[prop] = rulesByProperty[prop] || []).push({
|
|
91
|
+
predicates: getCurrentPredicates(),
|
|
92
|
+
namespaceURI: defaultNamespaceURI,
|
|
93
|
+
selector: isStyleAttribute
|
|
94
|
+
? undefined
|
|
95
|
+
: specificityObject.selector.trim(),
|
|
96
|
+
specificityArray: isStyleAttribute
|
|
97
|
+
? [1, 0, 0, 0]
|
|
98
|
+
: specificityObject.specificityArray,
|
|
99
|
+
prop,
|
|
100
|
+
value,
|
|
101
|
+
important: !!node.important,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
(function visit(node) {
|
|
88
107
|
// Check for selector. We might be in an at-rule like @font-face
|
|
89
108
|
if (node.type === 'decl' && node.parent.selector) {
|
|
90
109
|
const isCustomProperty = /^--/.test(node.prop);
|
|
91
110
|
const propName = isCustomProperty ? node.prop : node.prop.toLowerCase(); // Custom properties ARE case sensitive
|
|
92
111
|
if (isCustomProperty || properties.includes(propName)) {
|
|
93
|
-
|
|
94
|
-
getSpecificity(node.parent.selector).forEach((specificityObject) => {
|
|
95
|
-
const isStyleAttribute =
|
|
96
|
-
specificityObject.selector === 'bogusselector';
|
|
97
|
-
(rulesByProperty[propName] = rulesByProperty[propName] || []).push({
|
|
98
|
-
predicates: getCurrentPredicates(),
|
|
99
|
-
namespaceURI: defaultNamespaceURI,
|
|
100
|
-
selector: isStyleAttribute
|
|
101
|
-
? undefined
|
|
102
|
-
: specificityObject.selector.trim(),
|
|
103
|
-
specificityArray: isStyleAttribute
|
|
104
|
-
? [1, 0, 0, 0]
|
|
105
|
-
: specificityObject.specificityArray,
|
|
106
|
-
prop: propName,
|
|
107
|
-
value: node.value,
|
|
108
|
-
important: !!node.important,
|
|
109
|
-
});
|
|
110
|
-
});
|
|
112
|
+
pushRulePerSelector(node, propName, node.value);
|
|
111
113
|
} else if (
|
|
112
114
|
propName === 'list-style' &&
|
|
113
115
|
properties.includes('list-style-type')
|
|
114
116
|
) {
|
|
115
|
-
// Shorthand — use postcss-value-parser to properly handle quoted strings
|
|
116
117
|
let listStyleType;
|
|
117
118
|
for (const valueNode of postcssValueParser(node.value).nodes) {
|
|
118
119
|
if (valueNode.type === 'string') {
|
|
@@ -126,84 +127,31 @@ function getCssRulesByProperty(properties, cssSource, existingPredicates) {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
if (typeof listStyleType !== 'undefined') {
|
|
129
|
-
|
|
130
|
-
getSpecificity(node.parent.selector).forEach((specificityObject) => {
|
|
131
|
-
const isStyleAttribute =
|
|
132
|
-
specificityObject.selector === 'bogusselector';
|
|
133
|
-
|
|
134
|
-
rulesByProperty['list-style-type'].push({
|
|
135
|
-
predicates: getCurrentPredicates(),
|
|
136
|
-
namespaceURI: defaultNamespaceURI,
|
|
137
|
-
selector: isStyleAttribute
|
|
138
|
-
? undefined
|
|
139
|
-
: specificityObject.selector.trim(),
|
|
140
|
-
specificityArray: isStyleAttribute
|
|
141
|
-
? [1, 0, 0, 0]
|
|
142
|
-
: specificityObject.specificityArray,
|
|
143
|
-
prop: 'list-style-type',
|
|
144
|
-
value: listStyleType,
|
|
145
|
-
important: !!node.important,
|
|
146
|
-
});
|
|
147
|
-
});
|
|
130
|
+
pushRulePerSelector(node, 'list-style-type', listStyleType);
|
|
148
131
|
}
|
|
149
132
|
} else if (propName === 'animation') {
|
|
150
|
-
// Shorthand
|
|
151
133
|
const parsedAnimation = parseAnimationShorthand.parseSingle(
|
|
152
134
|
node.value
|
|
153
135
|
).value;
|
|
154
136
|
|
|
155
137
|
if (properties.includes('animation-name')) {
|
|
156
|
-
|
|
157
|
-
getSpecificity(node.parent.selector).forEach((specificityObject) => {
|
|
158
|
-
const isStyleAttribute =
|
|
159
|
-
specificityObject.selector === 'bogusselector';
|
|
160
|
-
|
|
161
|
-
rulesByProperty['animation-name'].push({
|
|
162
|
-
predicates: getCurrentPredicates(),
|
|
163
|
-
namespaceURI: defaultNamespaceURI,
|
|
164
|
-
selector: isStyleAttribute
|
|
165
|
-
? undefined
|
|
166
|
-
: specificityObject.selector.trim(),
|
|
167
|
-
specificityArray: isStyleAttribute
|
|
168
|
-
? [1, 0, 0, 0]
|
|
169
|
-
: specificityObject.specificityArray,
|
|
170
|
-
prop: 'animation-name',
|
|
171
|
-
value: parsedAnimation.name,
|
|
172
|
-
important: !!node.important,
|
|
173
|
-
});
|
|
174
|
-
});
|
|
138
|
+
pushRulePerSelector(node, 'animation-name', parsedAnimation.name);
|
|
175
139
|
}
|
|
176
140
|
if (properties.includes('animation-timing-function')) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
selector: isStyleAttribute
|
|
186
|
-
? undefined
|
|
187
|
-
: specificityObject.selector.trim(),
|
|
188
|
-
specificityArray: isStyleAttribute
|
|
189
|
-
? [1, 0, 0, 0]
|
|
190
|
-
: specificityObject.specificityArray,
|
|
191
|
-
prop: 'animation-timing-function',
|
|
192
|
-
value: parseAnimationShorthand.serialize({
|
|
193
|
-
name: '',
|
|
194
|
-
timingFunction: parsedAnimation.timingFunction,
|
|
195
|
-
}),
|
|
196
|
-
important: !!node.important,
|
|
197
|
-
});
|
|
198
|
-
});
|
|
141
|
+
pushRulePerSelector(
|
|
142
|
+
node,
|
|
143
|
+
'animation-timing-function',
|
|
144
|
+
parseAnimationShorthand.serialize({
|
|
145
|
+
name: '',
|
|
146
|
+
timingFunction: parsedAnimation.timingFunction,
|
|
147
|
+
})
|
|
148
|
+
);
|
|
199
149
|
}
|
|
200
150
|
} else if (propName === 'transition') {
|
|
201
|
-
//
|
|
202
|
-
// (regex split breaks on commas inside cubic-bezier() etc.)
|
|
151
|
+
// Use postcss-value-parser — regex split breaks on commas inside cubic-bezier() etc.
|
|
203
152
|
const transitionProperties = [];
|
|
204
153
|
const transitionDurations = [];
|
|
205
154
|
const parsed = postcssValueParser(node.value);
|
|
206
|
-
// Split top-level nodes by dividers (commas)
|
|
207
155
|
let currentItem = [];
|
|
208
156
|
for (const valueNode of parsed.nodes) {
|
|
209
157
|
if (valueNode.type === 'div' && valueNode.value === ',') {
|
|
@@ -218,7 +166,6 @@ function getCssRulesByProperty(properties, cssSource, existingPredicates) {
|
|
|
218
166
|
currentItem.push(postcssValueParser.stringify(valueNode));
|
|
219
167
|
}
|
|
220
168
|
}
|
|
221
|
-
// Handle last item
|
|
222
169
|
if (currentItem.length > 0) {
|
|
223
170
|
transitionProperties.push(currentItem[0]);
|
|
224
171
|
}
|
|
@@ -226,27 +173,32 @@ function getCssRulesByProperty(properties, cssSource, existingPredicates) {
|
|
|
226
173
|
transitionDurations.push(currentItem[1]);
|
|
227
174
|
}
|
|
228
175
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
176
|
+
if (properties.includes('transition-property')) {
|
|
177
|
+
pushRulePerSelector(
|
|
178
|
+
node,
|
|
179
|
+
'transition-property',
|
|
180
|
+
transitionProperties.join(', ')
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (properties.includes('transition-duration')) {
|
|
184
|
+
pushRulePerSelector(
|
|
185
|
+
node,
|
|
186
|
+
'transition-duration',
|
|
187
|
+
transitionDurations.join(', ')
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
} else if (propName === 'font') {
|
|
191
|
+
const fontLonghands = [
|
|
192
|
+
'font-family',
|
|
193
|
+
'font-weight',
|
|
194
|
+
'font-size',
|
|
195
|
+
'font-style',
|
|
196
|
+
].filter((prop) => properties.includes(prop));
|
|
197
|
+
if (fontLonghands.length > 0) {
|
|
198
|
+
getSpecificity(node.parent.selector).forEach((specificityObject) => {
|
|
199
|
+
const isStyleAttribute =
|
|
200
|
+
specificityObject.selector === 'bogusselector';
|
|
201
|
+
const entry = {
|
|
250
202
|
predicates: getCurrentPredicates(),
|
|
251
203
|
namespaceURI: defaultNamespaceURI,
|
|
252
204
|
selector: isStyleAttribute
|
|
@@ -255,41 +207,15 @@ function getCssRulesByProperty(properties, cssSource, existingPredicates) {
|
|
|
255
207
|
specificityArray: isStyleAttribute
|
|
256
208
|
? [1, 0, 0, 0]
|
|
257
209
|
: specificityObject.specificityArray,
|
|
258
|
-
prop: '
|
|
259
|
-
value:
|
|
210
|
+
prop: 'font',
|
|
211
|
+
value: node.value,
|
|
260
212
|
important: !!node.important,
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
} else if (propName === 'font') {
|
|
265
|
-
getSpecificity(node.parent.selector).forEach((specificityObject) => {
|
|
266
|
-
const isStyleAttribute =
|
|
267
|
-
specificityObject.selector === 'bogusselector';
|
|
268
|
-
const value = {
|
|
269
|
-
predicates: getCurrentPredicates(),
|
|
270
|
-
namespaceURI: defaultNamespaceURI,
|
|
271
|
-
selector: isStyleAttribute
|
|
272
|
-
? undefined
|
|
273
|
-
: specificityObject.selector.trim(),
|
|
274
|
-
specificityArray: isStyleAttribute
|
|
275
|
-
? [1, 0, 0, 0]
|
|
276
|
-
: specificityObject.specificityArray,
|
|
277
|
-
prop: 'font',
|
|
278
|
-
value: node.value,
|
|
279
|
-
important: !!node.important,
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
for (const prop of [
|
|
283
|
-
'font-family',
|
|
284
|
-
'font-weight',
|
|
285
|
-
'font-size',
|
|
286
|
-
'font-style',
|
|
287
|
-
]) {
|
|
288
|
-
if (properties.includes(prop)) {
|
|
289
|
-
rulesByProperty[prop].push(value);
|
|
213
|
+
};
|
|
214
|
+
for (const prop of fontLonghands) {
|
|
215
|
+
rulesByProperty[prop].push(entry);
|
|
290
216
|
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
293
219
|
}
|
|
294
220
|
} else if (
|
|
295
221
|
node.type === 'atrule' &&
|
|
@@ -5,14 +5,14 @@ function injectSubsetDefinitions(cssValue, webfontNameMap, replaceOriginal) {
|
|
|
5
5
|
Object.values(webfontNameMap).map((name) => name.toLowerCase())
|
|
6
6
|
);
|
|
7
7
|
const rootNode = postcssValueParser(cssValue);
|
|
8
|
-
let
|
|
8
|
+
let isPrecededByWords = false;
|
|
9
9
|
for (const [i, node] of rootNode.nodes.entries()) {
|
|
10
10
|
let possibleFontFamily;
|
|
11
11
|
let lastFontFamilyTokenIndex = i;
|
|
12
12
|
if (node.type === 'string') {
|
|
13
13
|
possibleFontFamily = node.value;
|
|
14
14
|
} else if (node.type === 'word' || node.type === 'space') {
|
|
15
|
-
if (!
|
|
15
|
+
if (!isPrecededByWords) {
|
|
16
16
|
const wordSequence = [];
|
|
17
17
|
for (let j = i; j < rootNode.nodes.length; j += 1) {
|
|
18
18
|
if (rootNode.nodes[j].type === 'word') {
|
|
@@ -24,9 +24,9 @@ function injectSubsetDefinitions(cssValue, webfontNameMap, replaceOriginal) {
|
|
|
24
24
|
}
|
|
25
25
|
possibleFontFamily = wordSequence.join(' ');
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
isPrecededByWords = true;
|
|
28
28
|
} else {
|
|
29
|
-
|
|
29
|
+
isPrecededByWords = false;
|
|
30
30
|
}
|
|
31
31
|
if (possibleFontFamily) {
|
|
32
32
|
const possibleFontFamilyLowerCase = possibleFontFamily.toLowerCase();
|
|
@@ -11,19 +11,16 @@ module.exports = function parseCommandLineOptions(argv) {
|
|
|
11
11
|
describe:
|
|
12
12
|
'Path to your web root (will be deduced from your input files if not specified)',
|
|
13
13
|
type: 'string',
|
|
14
|
-
demand: false,
|
|
15
14
|
})
|
|
16
15
|
.options('canonical-root', {
|
|
17
16
|
describe:
|
|
18
17
|
'URI root where the site will be deployed. Must be either an absolute, a protocol-relative, or a root-relative url',
|
|
19
18
|
type: 'string',
|
|
20
|
-
demand: false,
|
|
21
19
|
})
|
|
22
20
|
.options('output', {
|
|
23
21
|
alias: 'o',
|
|
24
22
|
describe: 'Directory where results should be written to',
|
|
25
23
|
type: 'string',
|
|
26
|
-
demand: false,
|
|
27
24
|
})
|
|
28
25
|
.options('text', {
|
|
29
26
|
describe:
|
|
@@ -112,7 +109,6 @@ module.exports = function parseCommandLineOptions(argv) {
|
|
|
112
109
|
describe:
|
|
113
110
|
'Maximum number of worker threads for parallel font tracing. Defaults to the number of CPU cores (max 8)',
|
|
114
111
|
type: 'number',
|
|
115
|
-
demand: false,
|
|
116
112
|
})
|
|
117
113
|
.options('source-maps', {
|
|
118
114
|
describe: 'Preserve CSS source maps through subfont processing',
|
package/lib/sfntCache.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
const fontverter = require('fontverter');
|
|
2
2
|
|
|
3
|
-
// Cache sfnt conversions by source buffer to avoid redundant work
|
|
4
|
-
// when the same font is processed by collectFeatureGlyphIds and
|
|
5
|
-
// subsetFontWithGlyphs.
|
|
6
3
|
const sfntPromiseByBuffer = new WeakMap();
|
|
7
4
|
|
|
8
5
|
function toSfnt(buffer) {
|