@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 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
  [![Build Status](https://github.com/alexander-turner/subfont/actions/workflows/ci.yml/badge.svg)](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
 
@@ -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 = [];
@@ -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(extraArgs = []) {
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
- require('os').homedir(),
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
- console.log('Downloading Chrome');
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
- return (this._launchPromise =
100
- this._launchPromise ||
101
- downloadOrLocatePreferredBrowserRevision(this._chromeArgs));
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
- // Build a single string of unique non-whitespace characters.
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
- // CSS properties that trigger OpenType feature glyph collection
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 = !headlessBrowser && pagesNeedingFullTrace.length >= 4;
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
- // Split up combined selectors as they might have different specificity
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
- // Split up combined selectors as they might have different specificity
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
- // Split up combined selectors as they might have different specificity
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
- // Split up combined selectors as they might have different specificity
178
- getSpecificity(node.parent.selector).forEach((specificityObject) => {
179
- const isStyleAttribute =
180
- specificityObject.selector === 'bogusselector';
181
-
182
- rulesByProperty['animation-timing-function'].push({
183
- predicates: getCurrentPredicates(),
184
- namespaceURI: defaultNamespaceURI,
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
- // Shorthand — use postcss-value-parser to correctly split on commas
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
- // Split up combined selectors as they might have different specificity
230
- getSpecificity(node.parent.selector).forEach((specificityObject) => {
231
- const isStyleAttribute =
232
- specificityObject.selector === 'bogusselector';
233
- if (properties.includes('transition-property')) {
234
- rulesByProperty['transition-property'].push({
235
- predicates: getCurrentPredicates(),
236
- namespaceURI: defaultNamespaceURI,
237
- selector: isStyleAttribute
238
- ? undefined
239
- : specificityObject.selector.trim(),
240
- specificityArray: isStyleAttribute
241
- ? [1, 0, 0, 0]
242
- : specificityObject.specificityArray,
243
- prop: 'transition-property',
244
- value: transitionProperties.join(', '),
245
- important: !!node.important,
246
- });
247
- }
248
- if (properties.includes('transition-duration')) {
249
- rulesByProperty['transition-duration'].push({
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: 'transition-duration',
259
- value: transitionDurations.join(', '),
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 isPreceededByWords = false;
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 (!isPreceededByWords) {
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
- isPreceededByWords = true;
27
+ isPrecededByWords = true;
28
28
  } else {
29
- isPreceededByWords = false;
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) {