@turntrout/subfont 1.0.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.
@@ -0,0 +1,343 @@
1
+ const specificity = require('specificity');
2
+ const postcss = require('postcss');
3
+ const postcssValueParser = require('postcss-value-parser');
4
+ const unquote = require('./unquote');
5
+ const parseAnimationShorthand = require('@hookun/parse-animation-shorthand');
6
+
7
+ const counterRendererNames = new Set([
8
+ 'none',
9
+ 'disc',
10
+ 'circle',
11
+ 'square',
12
+ 'decimal',
13
+ 'decimal-leading-zero',
14
+ 'lower-roman',
15
+ 'upper-roman',
16
+ 'lower-greek',
17
+ 'lower-latin',
18
+ 'lower-alpha',
19
+ 'upper-latin',
20
+ 'upper-alpha',
21
+ 'armenian',
22
+ 'georgian',
23
+ 'hebrew',
24
+ ]);
25
+
26
+ function unwrapNamespace(str) {
27
+ if (/^"/.test(str)) {
28
+ return unquote(str);
29
+ } else if (/^url\(.*\)$/i.test(str)) {
30
+ return unquote(str.replace(/^url\((.*)\)$/, '$1'));
31
+ } else {
32
+ throw new Error(`Cannot parse CSS namespace: ${str}`);
33
+ }
34
+ }
35
+
36
+ function getCssRulesByProperty(properties, cssSource, existingPredicates) {
37
+ if (!Array.isArray(properties)) {
38
+ throw new Error('properties argument must be an array');
39
+ }
40
+ if (typeof cssSource !== 'string') {
41
+ throw new Error('cssSource argument must be a string containing valid CSS');
42
+ }
43
+ existingPredicates = existingPredicates || {};
44
+
45
+ const parseTree = postcss.parse(cssSource);
46
+ let defaultNamespaceURI;
47
+ parseTree.walkAtRules('namespace', (rule) => {
48
+ const fragments = rule.params.split(/\s+/);
49
+ if (fragments.length === 1) {
50
+ defaultNamespaceURI = unwrapNamespace(rule.params);
51
+ }
52
+ // FIXME: Support registering namespace prefixes (fragments.length === 2):
53
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/@namespace
54
+ });
55
+ const rulesByProperty = {
56
+ counterStyles: [],
57
+ keyframes: [],
58
+ };
59
+
60
+ for (const property of properties) {
61
+ rulesByProperty[property] = [];
62
+ }
63
+
64
+ const specificityCache = new Map();
65
+ function getSpecificity(selector) {
66
+ let cached = specificityCache.get(selector);
67
+ if (!cached) {
68
+ cached = specificity.calculate(selector);
69
+ specificityCache.set(selector, cached);
70
+ }
71
+ return cached;
72
+ }
73
+
74
+ const activeCssQueryPredicates = [];
75
+ function getCurrentPredicates() {
76
+ if (activeCssQueryPredicates.length > 0) {
77
+ const predicates = { ...existingPredicates };
78
+ for (const predicate of activeCssQueryPredicates) {
79
+ predicates[predicate] = true;
80
+ }
81
+ return predicates;
82
+ } else {
83
+ return existingPredicates;
84
+ }
85
+ }
86
+
87
+ (function visit(node) {
88
+ // Check for selector. We might be in an at-rule like @font-face
89
+ if (node.type === 'decl' && node.parent.selector) {
90
+ const isCustomProperty = /^--/.test(node.prop);
91
+ const propName = isCustomProperty ? node.prop : node.prop.toLowerCase(); // Custom properties ARE case sensitive
92
+ 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
+ });
111
+ } else if (
112
+ propName === 'list-style' &&
113
+ properties.includes('list-style-type')
114
+ ) {
115
+ // Shorthand — use postcss-value-parser to properly handle quoted strings
116
+ let listStyleType;
117
+ for (const valueNode of postcssValueParser(node.value).nodes) {
118
+ if (valueNode.type === 'string') {
119
+ listStyleType = valueNode.value;
120
+ } else if (
121
+ valueNode.type === 'word' &&
122
+ counterRendererNames.has(valueNode.value)
123
+ ) {
124
+ listStyleType = valueNode.value;
125
+ }
126
+ }
127
+
128
+ 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
+ });
148
+ }
149
+ } else if (propName === 'animation') {
150
+ // Shorthand
151
+ const parsedAnimation = parseAnimationShorthand.parseSingle(
152
+ node.value
153
+ ).value;
154
+
155
+ 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
+ });
175
+ }
176
+ 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
+ });
199
+ }
200
+ } 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.)
203
+ const transitionProperties = [];
204
+ const transitionDurations = [];
205
+ const parsed = postcssValueParser(node.value);
206
+ // Split top-level nodes by dividers (commas)
207
+ let currentItem = [];
208
+ for (const valueNode of parsed.nodes) {
209
+ if (valueNode.type === 'div' && valueNode.value === ',') {
210
+ if (currentItem.length > 0) {
211
+ transitionProperties.push(currentItem[0]);
212
+ }
213
+ if (currentItem.length > 1) {
214
+ transitionDurations.push(currentItem[1]);
215
+ }
216
+ currentItem = [];
217
+ } else if (valueNode.type !== 'space') {
218
+ currentItem.push(postcssValueParser.stringify(valueNode));
219
+ }
220
+ }
221
+ // Handle last item
222
+ if (currentItem.length > 0) {
223
+ transitionProperties.push(currentItem[0]);
224
+ }
225
+ if (currentItem.length > 1) {
226
+ transitionDurations.push(currentItem[1]);
227
+ }
228
+
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({
250
+ predicates: getCurrentPredicates(),
251
+ namespaceURI: defaultNamespaceURI,
252
+ selector: isStyleAttribute
253
+ ? undefined
254
+ : specificityObject.selector.trim(),
255
+ specificityArray: isStyleAttribute
256
+ ? [1, 0, 0, 0]
257
+ : specificityObject.specificityArray,
258
+ prop: 'transition-duration',
259
+ value: transitionDurations.join(', '),
260
+ 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);
290
+ }
291
+ }
292
+ });
293
+ }
294
+ } else if (
295
+ node.type === 'atrule' &&
296
+ node.name.toLowerCase() === 'counter-style'
297
+ ) {
298
+ const props = {};
299
+ for (const childNode of node.nodes) {
300
+ props[childNode.prop] = childNode.value;
301
+ }
302
+ rulesByProperty.counterStyles.push({
303
+ name: node.params,
304
+ predicates: getCurrentPredicates(),
305
+ props,
306
+ });
307
+ } else if (
308
+ node.type === 'atrule' &&
309
+ node.name.toLowerCase() === 'keyframes'
310
+ ) {
311
+ rulesByProperty.keyframes.push({
312
+ name: node.params,
313
+ namespaceURI: defaultNamespaceURI,
314
+ predicates: getCurrentPredicates(),
315
+ node,
316
+ });
317
+ return;
318
+ }
319
+
320
+ if (node.nodes) {
321
+ let popAfter = false;
322
+ if (node.type === 'atrule') {
323
+ const name = node.name.toLowerCase();
324
+ if (name === 'media' || name === 'supports') {
325
+ activeCssQueryPredicates.push(`${name}Query:${node.params}`);
326
+ popAfter = true;
327
+ }
328
+ }
329
+ for (const childNode of node.nodes) {
330
+ visit(childNode);
331
+ }
332
+ if (popAfter) {
333
+ activeCssQueryPredicates.pop();
334
+ }
335
+ }
336
+ })(parseTree);
337
+
338
+ // TODO: Collapse into a single object for duplicate values?
339
+
340
+ return rulesByProperty;
341
+ }
342
+
343
+ module.exports = getCssRulesByProperty;
@@ -0,0 +1,36 @@
1
+ const fontverter = require('fontverter');
2
+
3
+ async function getFontInfoFromBuffer(buffer) {
4
+ const harfbuzzJs = await require('harfbuzzjs');
5
+
6
+ const blob = harfbuzzJs.createBlob(await fontverter.convert(buffer, 'sfnt')); // Load the font data into something Harfbuzz can use
7
+ const face = harfbuzzJs.createFace(blob, 0); // Select the first font in the file (there's normally only one!)
8
+
9
+ const fontInfo = {
10
+ characterSet: Array.from(face.collectUnicodes()),
11
+ variationAxes: face.getAxisInfos(),
12
+ };
13
+
14
+ face.destroy();
15
+ blob.destroy();
16
+
17
+ return fontInfo;
18
+ }
19
+
20
+ const fontInfoPromiseByBuffer = new WeakMap();
21
+
22
+ const enqueueWasm = require('./wasmQueue');
23
+
24
+ module.exports = function getFontInfo(buffer) {
25
+ if (!fontInfoPromiseByBuffer.has(buffer)) {
26
+ const promise = enqueueWasm(() => getFontInfoFromBuffer(buffer)).catch(
27
+ (err) => {
28
+ // Evict rejected promises so retries with the same buffer aren't stuck
29
+ fontInfoPromiseByBuffer.delete(buffer);
30
+ throw err;
31
+ }
32
+ );
33
+ fontInfoPromiseByBuffer.set(buffer, promise);
34
+ }
35
+ return fontInfoPromiseByBuffer.get(buffer);
36
+ };
@@ -0,0 +1,18 @@
1
+ module.exports = {
2
+ // 'font-family': 'serif'
3
+ 'font-weight': 'normal',
4
+ 'font-style': 'normal',
5
+ 'font-stretch': 'normal',
6
+ content: 'normal',
7
+ quotes: '"«" "»" "‹" "›" "‘" "’" "\'" "\'" "\\"" "\\""', // Wide default set to account for browser differences
8
+ 'list-style-type': 'none',
9
+ display: 'inline',
10
+ 'animation-name': 'none',
11
+ 'text-transform': 'none',
12
+ 'transition-property': 'all',
13
+ 'transition-duration': '0s',
14
+ 'counter-increment': 'none',
15
+ 'counter-reset': 'none',
16
+ 'counter-set': 'none',
17
+ 'white-space': 'normal',
18
+ };
@@ -0,0 +1,65 @@
1
+ const postcssValueParser = require('postcss-value-parser');
2
+
3
+ function injectSubsetDefinitions(cssValue, webfontNameMap, replaceOriginal) {
4
+ const subsetFontNames = new Set(
5
+ Object.values(webfontNameMap).map((name) => name.toLowerCase())
6
+ );
7
+ const rootNode = postcssValueParser(cssValue);
8
+ let isPreceededByWords = false;
9
+ for (const [i, node] of rootNode.nodes.entries()) {
10
+ let possibleFontFamily;
11
+ let lastFontFamilyTokenIndex = i;
12
+ if (node.type === 'string') {
13
+ possibleFontFamily = node.value;
14
+ } else if (node.type === 'word' || node.type === 'space') {
15
+ if (!isPreceededByWords) {
16
+ const wordSequence = [];
17
+ for (let j = i; j < rootNode.nodes.length; j += 1) {
18
+ if (rootNode.nodes[j].type === 'word') {
19
+ wordSequence.push(rootNode.nodes[j].value);
20
+ lastFontFamilyTokenIndex = j;
21
+ } else if (rootNode.nodes[j].type !== 'space') {
22
+ break;
23
+ }
24
+ }
25
+ possibleFontFamily = wordSequence.join(' ');
26
+ }
27
+ isPreceededByWords = true;
28
+ } else {
29
+ isPreceededByWords = false;
30
+ }
31
+ if (possibleFontFamily) {
32
+ const possibleFontFamilyLowerCase = possibleFontFamily.toLowerCase();
33
+ if (subsetFontNames.has(possibleFontFamilyLowerCase)) {
34
+ // Bail out, a subset font is already listed
35
+ return cssValue;
36
+ } else if (webfontNameMap[possibleFontFamilyLowerCase]) {
37
+ const newToken = {
38
+ type: 'string',
39
+ value: webfontNameMap[possibleFontFamilyLowerCase].replace(
40
+ /'/g,
41
+ "\\'"
42
+ ),
43
+ quote: "'",
44
+ };
45
+ if (replaceOriginal) {
46
+ rootNode.nodes.splice(
47
+ rootNode.nodes.indexOf(node),
48
+ lastFontFamilyTokenIndex - i + 1,
49
+ newToken
50
+ );
51
+ } else {
52
+ rootNode.nodes.splice(rootNode.nodes.indexOf(node), 0, newToken, {
53
+ type: 'div',
54
+ value: ',',
55
+ after: ' ',
56
+ });
57
+ }
58
+ return postcssValueParser.stringify(rootNode);
59
+ }
60
+ }
61
+ }
62
+ return cssValue;
63
+ }
64
+
65
+ module.exports = injectSubsetDefinitions;
@@ -0,0 +1,34 @@
1
+ const cssFontWeightNames = require('css-font-weight-names');
2
+ const initialValueByProp = require('./initialValueByProp');
3
+ const unquote = require('./unquote');
4
+ const normalizeFontStretch = require('font-snapper/lib/normalizeFontStretch');
5
+
6
+ function normalizeFontPropertyValue(propName, value) {
7
+ const propNameLowerCase = propName.toLowerCase();
8
+ if (value === undefined) {
9
+ return initialValueByProp[propName];
10
+ }
11
+ if (propNameLowerCase === 'font-family') {
12
+ return unquote(value);
13
+ } else if (propNameLowerCase === 'font-weight') {
14
+ let parsedValue = value;
15
+ if (typeof parsedValue === 'string') {
16
+ // FIXME: Stripping the +bolder... suffix here will not always yield the correct result
17
+ // when expanding animations and transitions
18
+ parsedValue = parsedValue.replace(/\+.*$/, '').toLowerCase();
19
+ }
20
+ parsedValue = parseFloat(cssFontWeightNames[parsedValue] || parsedValue);
21
+ if (parsedValue >= 1 && parsedValue <= 1000) {
22
+ return parsedValue;
23
+ } else {
24
+ return value;
25
+ }
26
+ } else if (propNameLowerCase === 'font-stretch') {
27
+ return normalizeFontStretch(value);
28
+ } else if (typeof value === 'string' && propNameLowerCase !== 'src') {
29
+ return value.toLowerCase();
30
+ }
31
+ return value;
32
+ }
33
+
34
+ module.exports = normalizeFontPropertyValue;
@@ -0,0 +1,131 @@
1
+ module.exports = function parseCommandLineOptions(argv) {
2
+ let yargs = require('yargs');
3
+ if (argv) {
4
+ yargs = yargs(argv);
5
+ }
6
+ yargs
7
+ .usage(
8
+ 'Create optimal font subsets from your actual font usage.\n$0 [options] <htmlFile(s) | url(s)>'
9
+ )
10
+ .options('root', {
11
+ describe:
12
+ 'Path to your web root (will be deduced from your input files if not specified)',
13
+ type: 'string',
14
+ demand: false,
15
+ })
16
+ .options('canonical-root', {
17
+ describe:
18
+ 'URI root where the site will be deployed. Must be either an absolute, a protocol-relative, or a root-relative url',
19
+ type: 'string',
20
+ demand: false,
21
+ })
22
+ .options('output', {
23
+ alias: 'o',
24
+ describe: 'Directory where results should be written to',
25
+ type: 'string',
26
+ demand: false,
27
+ })
28
+ .options('text', {
29
+ describe:
30
+ 'Additional characters to include in the subset for every @font-face found on the page',
31
+ type: 'string',
32
+ })
33
+ .options('fallbacks', {
34
+ describe:
35
+ 'Async-load the full original font as a fallback for dynamic content. Disable with --no-fallbacks',
36
+ type: 'boolean',
37
+ default: true,
38
+ })
39
+ .options('dynamic', {
40
+ describe:
41
+ 'Also trace the usage of fonts in a headless browser with JavaScript enabled',
42
+ type: 'boolean',
43
+ default: false,
44
+ })
45
+ .options('in-place', {
46
+ alias: 'i',
47
+ describe: 'Modify HTML-files in-place. Only use on build artifacts',
48
+ type: 'boolean',
49
+ default: false,
50
+ })
51
+ .options('inline-css', {
52
+ describe: 'Inline CSS that declares the @font-face for the subset fonts',
53
+ type: 'boolean',
54
+ default: false,
55
+ })
56
+ .options('font-display', {
57
+ describe: 'Injects a font-display value into the @font-face CSS',
58
+ type: 'string',
59
+ default: 'swap',
60
+ choices: ['auto', 'block', 'swap', 'fallback', 'optional'],
61
+ })
62
+ .options('recursive', {
63
+ alias: 'r',
64
+ describe:
65
+ 'Crawl all HTML-pages linked with relative and root relative links. This stays inside your domain',
66
+ type: 'boolean',
67
+ default: false,
68
+ })
69
+ .options('relative-urls', {
70
+ describe: 'Issue relative urls instead of root-relative ones',
71
+ type: 'boolean',
72
+ default: false,
73
+ })
74
+ .options('silent', {
75
+ alias: 's',
76
+ describe: `Do not write anything to stdout`,
77
+ type: 'boolean',
78
+ default: false,
79
+ })
80
+ .options('debug', {
81
+ alias: 'd',
82
+ describe: 'Verbose insights into font glyph detection',
83
+ type: 'boolean',
84
+ default: false,
85
+ })
86
+ .options('dry-run', {
87
+ describe: `Don't write anything to disk. Shows a preview of files, sizes, and CSS changes that would be made`,
88
+ type: 'boolean',
89
+ default: false,
90
+ })
91
+ .options('cache', {
92
+ describe:
93
+ 'Enable disk caching of subset font results between runs. Pass a directory path or use without a value for the default .subfont-cache directory',
94
+ type: 'string',
95
+ default: false,
96
+ coerce(val) {
97
+ if (val === '' || val === true) return true;
98
+ return val;
99
+ },
100
+ })
101
+ .options('chrome-flags', {
102
+ alias: ['chromeFlags'],
103
+ describe:
104
+ 'Custom flags to pass to the Chrome/Chromium browser for dynamic tracing (comma-separated)',
105
+ type: 'string',
106
+ coerce(flags) {
107
+ if (!flags) return [];
108
+ return flags.split(',').map((f) => f.trim());
109
+ },
110
+ })
111
+ .options('concurrency', {
112
+ describe:
113
+ 'Maximum number of worker threads for parallel font tracing. Defaults to the number of CPU cores (max 8)',
114
+ type: 'number',
115
+ demand: false,
116
+ })
117
+ .options('source-maps', {
118
+ describe: 'Preserve CSS source maps through subfont processing',
119
+ type: 'boolean',
120
+ default: false,
121
+ })
122
+ .wrap(require('yargs').terminalWidth());
123
+
124
+ const { _: inputFiles, ...rest } = yargs.argv;
125
+
126
+ return {
127
+ yargs,
128
+ inputFiles,
129
+ ...rest,
130
+ };
131
+ };
@@ -0,0 +1,39 @@
1
+ const postcssValueParser = require('postcss-value-parser');
2
+
3
+ module.exports = function* parseFontVariationSettings(value) {
4
+ let state = 'BEFORE_AXIS_NAME';
5
+ let axisName;
6
+ for (const token of postcssValueParser(value).nodes) {
7
+ if (token.type === 'space') {
8
+ continue;
9
+ }
10
+ switch (state) {
11
+ case 'BEFORE_AXIS_NAME': {
12
+ if (token.type !== 'string') {
13
+ return;
14
+ }
15
+ axisName = token.value;
16
+ state = 'AFTER_AXIS_NAME';
17
+ break;
18
+ }
19
+ case 'AFTER_AXIS_NAME': {
20
+ if (token.type === 'word') {
21
+ const axisValue = parseFloat(token.value);
22
+ if (!isNaN(axisValue)) {
23
+ yield [axisName, axisValue];
24
+ }
25
+ }
26
+ state = 'AFTER_AXIS_VALUE';
27
+ break;
28
+ }
29
+ case 'AFTER_AXIS_VALUE': {
30
+ if (token.type !== 'div' || token.value !== ',') {
31
+ return;
32
+ }
33
+ axisName = undefined;
34
+ state = 'BEFORE_AXIS_NAME';
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ };
@@ -0,0 +1,29 @@
1
+ const fontverter = require('fontverter');
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
+ const sfntPromiseByBuffer = new WeakMap();
7
+
8
+ function toSfnt(buffer) {
9
+ if (sfntPromiseByBuffer.has(buffer)) {
10
+ return sfntPromiseByBuffer.get(buffer);
11
+ }
12
+ let promise;
13
+ try {
14
+ const format = fontverter.detectFormat(buffer);
15
+ promise =
16
+ format === 'sfnt'
17
+ ? Promise.resolve(buffer)
18
+ : fontverter.convert(buffer, 'sfnt');
19
+ } catch (err) {
20
+ // Unrecognized format — don't cache so retries work
21
+ return fontverter.convert(buffer, 'sfnt');
22
+ }
23
+ // Evict on rejection so retries with the same buffer aren't stuck
24
+ promise.catch(() => sfntPromiseByBuffer.delete(buffer));
25
+ sfntPromiseByBuffer.set(buffer, promise);
26
+ return promise;
27
+ }
28
+
29
+ module.exports = { toSfnt };