@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.
- package/CHANGELOG.md +35 -0
- package/CLAUDE.md +53 -0
- package/LICENSE.md +7 -0
- package/README.md +93 -0
- package/lib/FontTracerPool.js +158 -0
- package/lib/HeadlessBrowser.js +223 -0
- package/lib/cli.js +14 -0
- package/lib/collectFeatureGlyphIds.js +137 -0
- package/lib/collectTextsByPage.js +1017 -0
- package/lib/extractReferencedCustomPropertyNames.js +20 -0
- package/lib/extractVisibleText.js +64 -0
- package/lib/findCustomPropertyDefinitions.js +54 -0
- package/lib/fontFaceHelpers.js +292 -0
- package/lib/fontTracerWorker.js +76 -0
- package/lib/gatherStylesheetsWithPredicates.js +87 -0
- package/lib/getCssRulesByProperty.js +343 -0
- package/lib/getFontInfo.js +36 -0
- package/lib/initialValueByProp.js +18 -0
- package/lib/injectSubsetDefinitions.js +65 -0
- package/lib/normalizeFontPropertyValue.js +34 -0
- package/lib/parseCommandLineOptions.js +131 -0
- package/lib/parseFontVariationSettings.js +39 -0
- package/lib/sfntCache.js +29 -0
- package/lib/stripLocalTokens.js +23 -0
- package/lib/subfont.js +571 -0
- package/lib/subsetFontWithGlyphs.js +193 -0
- package/lib/subsetFonts.js +1218 -0
- package/lib/subsetGeneration.js +347 -0
- package/lib/unicodeRange.js +38 -0
- package/lib/unquote.js +23 -0
- package/lib/variationAxes.js +162 -0
- package/lib/warnAboutMissingGlyphs.js +145 -0
- package/lib/wasmQueue.js +11 -0
- package/package.json +113 -0
|
@@ -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
|
+
};
|
package/lib/sfntCache.js
ADDED
|
@@ -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 };
|