subfont 6.8.0 → 6.11.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 CHANGED
@@ -1,3 +1,27 @@
1
+ ### v6.11.0 (2022-09-04)
2
+
3
+ - [Treat wdth as a standard axis \(but unsupported for now\)](https://github.com/Munter/subfont/commit/20d539750448134cca0a82f2cc98f85d9a6be068) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
4
+ - [Detect unused ital variation axis ranges](https://github.com/Munter/subfont/commit/b848b21b66cdbcd0ff313690ba49a4bc9b9f90a4) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
5
+ - [Don't assume that the display name of a variation axis equals its name](https://github.com/Munter/subfont/commit/4fcf7df801de01b2d3aef3d5f1ea3d697779ffda) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
6
+ - [Detect unused wght variation axis ranges](https://github.com/Munter/subfont/commit/6a964d609f21bf2c9dad96da1d16d9c32c28dbfd) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
7
+
8
+ ### v6.10.0 (2022-08-29)
9
+
10
+ - [Update @hookun\/parse-animation-shorthand to ^0.1.4 to make sure we get the fix for hookhookun\/parse-animation-shorthand\#16](https://github.com/Munter/subfont/commit/4db17826245e289e987c6dc02d3f67ebf5891e6f) ([Andreas Lind](mailto:andreas.lind@workday.com))
11
+ - [Disable notice about unused variation axis ranges when there's an out-of-bounds cubic-bezier animation timing function in play](https://github.com/Munter/subfont/commit/1cf4ff46bb6099f8df8602968c7dc0fefb1c1f21) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
12
+ - [Update font-tracer to ^3.6.0](https://github.com/Munter/subfont/commit/2a47adc03122aad50ba2a01f116a2d1fe0f2b29e) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
13
+ - [Remove accidentally committed commented out code](https://github.com/Munter/subfont/commit/e0c677f9b2c7a9af123045cf29bc78a4f9a56550) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
14
+ - [Warn about unused variation axis ranges, not just fully unused axes](https://github.com/Munter/subfont/commit/32fc472327b1c8fc87090d4c1e5f1085533ad8fa) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
15
+ - [+13 more](https://github.com/Munter/subfont/compare/v6.9.0...v6.10.0)
16
+
17
+ ### v6.9.0 (2022-08-07)
18
+
19
+ - [Update font-tracer to ^3.3.0 Adds support for tracing ::marker, fixes \#166](https://github.com/Munter/subfont/commit/e46c79bac9cb3e62c70deb357823b7963114863b) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
20
+ - [Move some code into a collectTextsByPage function](https://github.com/Munter/subfont/commit/1dff3819fb80793f23a3cc37f9ff58bafffa4efc) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
21
+ - [Move the missing glyph detection to a function](https://github.com/Munter/subfont/commit/38bf36eba4370ecdbc777cb2457696b9bc7c7d1c) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
22
+ - [Fix typo causing regexp to not be matched correctly](https://github.com/Munter/subfont/commit/05137708b5c48af305f3119deb836b1cd9fed683) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
23
+ - [Remove delayed minification of CSS, seems like it's no longer necessary](https://github.com/Munter/subfont/commit/b98976b24c3a859ffbdd2a43f9f05e5048175f5a) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
24
+
1
25
  ### v6.8.0 (2022-07-28)
2
26
 
3
27
  - [Update assetgraph to ^7.8.1](https://github.com/Munter/subfont/commit/888a97912f98bd937a53b7bec0f39d50ddc96023) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
@@ -1,6 +1,7 @@
1
1
  const specificity = require('specificity');
2
2
  const postcss = require('postcss');
3
3
  const unquote = require('./unquote');
4
+ const parseAnimationShorthand = require('@hookun/parse-animation-shorthand');
4
5
 
5
6
  const counterRendererNames = new Set([
6
7
  'none',
@@ -144,34 +145,59 @@ function getCssRulesByProperty(properties, cssSource, existingPredicates) {
144
145
  });
145
146
  });
146
147
  }
147
- } else if (
148
- propName === 'animation' &&
149
- properties.includes('animation-name')
150
- ) {
148
+ } else if (propName === 'animation') {
151
149
  // Shorthand
152
- const animationName = node.value.split(' ').pop();
150
+ const parsedAnimation = parseAnimationShorthand.parseSingle(node.value);
153
151
 
154
- // Split up combined selectors as they might have different specificity
155
- specificity
156
- .calculate(node.parent.selector)
157
- .forEach((specificityObject) => {
158
- const isStyleAttribute =
159
- specificityObject.selector === 'bogusselector';
152
+ if (properties.includes('animation-name')) {
153
+ // Split up combined selectors as they might have different specificity
154
+ specificity
155
+ .calculate(node.parent.selector)
156
+ .forEach((specificityObject) => {
157
+ const isStyleAttribute =
158
+ specificityObject.selector === 'bogusselector';
160
159
 
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: animationName,
172
- important: !!node.important,
160
+ rulesByProperty['animation-name'].push({
161
+ predicates: getCurrentPredicates(),
162
+ namespaceURI: defaultNamespaceURI,
163
+ selector: isStyleAttribute
164
+ ? undefined
165
+ : specificityObject.selector.trim(),
166
+ specificityArray: isStyleAttribute
167
+ ? [1, 0, 0, 0]
168
+ : specificityObject.specificityArray,
169
+ prop: 'animation-name',
170
+ value: parsedAnimation.name,
171
+ important: !!node.important,
172
+ });
173
173
  });
174
- });
174
+ }
175
+ if (properties.includes('animation-timing-function')) {
176
+ // Split up combined selectors as they might have different specificity
177
+ specificity
178
+ .calculate(node.parent.selector)
179
+ .forEach((specificityObject) => {
180
+ const isStyleAttribute =
181
+ specificityObject.selector === 'bogusselector';
182
+
183
+ rulesByProperty['animation-timing-function'].push({
184
+ predicates: getCurrentPredicates(),
185
+ namespaceURI: defaultNamespaceURI,
186
+ selector: isStyleAttribute
187
+ ? undefined
188
+ : specificityObject.selector.trim(),
189
+ specificityArray: isStyleAttribute
190
+ ? [1, 0, 0, 0]
191
+ : specificityObject.specificityArray,
192
+ prop: 'animation-timing-function',
193
+ value: parseAnimationShorthand.serialize({
194
+ name: '',
195
+ timingFunction: parsedAnimation.timingFunction,
196
+ }),
197
+ important: !!node.important,
198
+ });
199
+ });
200
+ }
175
201
  } else if (propName === 'transition') {
176
202
  // Shorthand
177
203
  const transitionProperties = [];
@@ -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
+ };
@@ -13,13 +13,14 @@ const HeadlessBrowser = require('./HeadlessBrowser');
13
13
  const gatherStylesheetsWithPredicates = require('./gatherStylesheetsWithPredicates');
14
14
  const findCustomPropertyDefinitions = require('./findCustomPropertyDefinitions');
15
15
  const extractReferencedCustomPropertyNames = require('./extractReferencedCustomPropertyNames');
16
+ const parseFontVariationSettings = require('./parseFontVariationSettings');
17
+ const parseAnimationShorthand = require('@hookun/parse-animation-shorthand');
16
18
  const stripLocalTokens = require('./stripLocalTokens');
17
19
  const injectSubsetDefinitions = require('./injectSubsetDefinitions');
18
20
  const cssFontParser = require('css-font-parser');
19
21
  const cssListHelpers = require('css-list-helpers');
20
22
  const LinesAndColumns = require('lines-and-columns').default;
21
23
  const fontkit = require('fontkit');
22
- const fontFamily = require('font-family-papandreou');
23
24
  const crypto = require('crypto');
24
25
 
25
26
  const unquote = require('./unquote');
@@ -41,6 +42,14 @@ const contentTypeByFontFormat = {
41
42
  truetype: 'font/ttf',
42
43
  };
43
44
 
45
+ function stringifyFontFamily(name) {
46
+ if (/[^a-z0-9_-]/i.test(name)) {
47
+ return name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
48
+ } else {
49
+ return name;
50
+ }
51
+ }
52
+
44
53
  function uniqueChars(text) {
45
54
  return [...new Set([...text])].sort().join('');
46
55
  }
@@ -79,6 +88,21 @@ function getPreferredFontUrl(cssFontFaceSrcRelations = []) {
79
88
  }
80
89
  }
81
90
 
91
+ function isOutOfBoundsAnimationTimingFunction(animationTimingFunctionStr) {
92
+ if (typeof animationTimingFunctionStr !== 'string') {
93
+ return false;
94
+ }
95
+ const { timingFunction } = parseAnimationShorthand.parseSingle(
96
+ `${animationTimingFunctionStr} ignored-name`
97
+ );
98
+
99
+ if (timingFunction.type === 'cubic-bezier') {
100
+ const [, y1, , y2] = timingFunction.value;
101
+ return y1 > 1 || y1 < 0 || y2 > 1 || y2 < 0;
102
+ }
103
+ return false;
104
+ }
105
+
82
106
  // Hack to extract '@font-face { ... }' with all absolute urls
83
107
  function getFontFaceDeclarationText(node, relations) {
84
108
  const originalHrefTypeByRelation = new Map();
@@ -145,8 +169,8 @@ function groupTextsByFontFamilyProps(
145
169
  return [];
146
170
  }
147
171
  // Find all the families in the traced font-family that we have @font-face declarations for:
148
- const families = fontFamily
149
- .parse(family)
172
+ const families = cssFontParser
173
+ .parseFontFamily(family)
150
174
  .filter((family) =>
151
175
  availableFontFaceDeclarations.some(
152
176
  (fontFace) =>
@@ -159,7 +183,7 @@ function groupTextsByFontFamilyProps(
159
183
  availableFontFaceDeclarations,
160
184
  {
161
185
  ...textAndProps.props,
162
- 'font-family': fontFamily.stringify([family]),
186
+ 'font-family': stringifyFontFamily(family),
163
187
  }
164
188
  );
165
189
 
@@ -170,9 +194,27 @@ function groupTextsByFontFamilyProps(
170
194
  const { relations, ...props } = activeFontFaceDeclaration;
171
195
  const fontUrl = getPreferredFontUrl(relations);
172
196
 
197
+ const fontStyle = normalizeFontPropertyValue(
198
+ 'font-style',
199
+ textAndProps.props['font-style']
200
+ );
201
+
202
+ let fontWeight = normalizeFontPropertyValue(
203
+ 'font-weight',
204
+ textAndProps.props['font-weight']
205
+ );
206
+ if (fontWeight === 'normal') {
207
+ fontWeight = 400;
208
+ }
209
+
173
210
  return {
174
211
  htmlOrSvgAsset: textAndProps.htmlOrSvgAsset,
175
212
  text: textAndProps.text,
213
+ fontVariationSettings: textAndProps.props['font-variation-settings'],
214
+ fontStyle,
215
+ fontWeight,
216
+ animationTimingFunction:
217
+ textAndProps.props['animation-timing-function'],
176
218
  props,
177
219
  fontRelations: relations,
178
220
  fontUrl,
@@ -190,6 +232,20 @@ function groupTextsByFontFamilyProps(
190
232
  const fontFamilies = new Set(
191
233
  textsPropsArray.map((obj) => obj.props['font-family'])
192
234
  );
235
+ const fontStyles = new Set(textsPropsArray.map((obj) => obj.fontStyle));
236
+ const fontWeights = new Set(textsPropsArray.map((obj) => obj.fontWeight));
237
+ const fontVariationSettings = new Set(
238
+ textsPropsArray
239
+ .map((obj) => obj.fontVariationSettings)
240
+ .filter(
241
+ (fontVariationSettings) =>
242
+ fontVariationSettings &&
243
+ fontVariationSettings.toLowerCase() !== 'normal'
244
+ )
245
+ );
246
+ const hasOutOfBoundsAnimationTimingFunction = textsPropsArray.some((obj) =>
247
+ isOutOfBoundsAnimationTimingFunction(obj.animationTimingFunction)
248
+ );
193
249
 
194
250
  let smallestOriginalSize;
195
251
  let smallestOriginalFormat;
@@ -217,6 +273,10 @@ function groupTextsByFontFamilyProps(
217
273
  props: { ...textsPropsArray[0].props },
218
274
  fontUrl,
219
275
  fontFamilies,
276
+ fontStyles,
277
+ fontWeights,
278
+ fontVariationSettings,
279
+ hasOutOfBoundsAnimationTimingFunction,
220
280
  preload,
221
281
  };
222
282
  });
@@ -497,9 +557,9 @@ async function createSelfHostedGoogleFontsCssAsset(
497
557
  assetGraph,
498
558
  googleFontsCssAsset,
499
559
  formats,
500
- hrefType
560
+ hrefType,
561
+ subsetUrl
501
562
  ) {
502
- const baseUrl = assetGraph.resolveUrl(assetGraph.root, '/subfont/');
503
563
  const lines = [];
504
564
  for (const cssFontFaceSrc of assetGraph.findRelations({
505
565
  from: googleFontsCssAsset,
@@ -516,9 +576,12 @@ async function createSelfHostedGoogleFontsCssAsset(
516
576
  const srcFragments = [];
517
577
  for (const format of formats) {
518
578
  const rawSrc = await fontverter.convert(cssFontFaceSrc.to.rawSrc, format);
519
- const url = `${assetGraph.root}subfont/${
520
- cssFontFaceSrc.to.baseName
521
- }-${md5HexPrefix(rawSrc)}${extensionByFormat[format]}`;
579
+ const url = assetGraph.resolveUrl(
580
+ subsetUrl,
581
+ `${cssFontFaceSrc.to.baseName}-${md5HexPrefix(rawSrc)}${
582
+ extensionByFormat[format]
583
+ }`
584
+ );
522
585
  const fontAsset =
523
586
  assetGraph.findAssets({ url })[0] ||
524
587
  (await assetGraph.addAsset({
@@ -526,7 +589,7 @@ async function createSelfHostedGoogleFontsCssAsset(
526
589
  rawSrc,
527
590
  }));
528
591
  srcFragments.push(
529
- `url(${assetGraph.buildHref(fontAsset.url, baseUrl, {
592
+ `url(${assetGraph.buildHref(fontAsset.url, subsetUrl, {
530
593
  hrefType,
531
594
  })}) format('${format}')`
532
595
  );
@@ -542,7 +605,7 @@ async function createSelfHostedGoogleFontsCssAsset(
542
605
  const text = lines.join('\n');
543
606
  const fallbackAsset = assetGraph.addAsset({
544
607
  type: 'Css',
545
- url: `/subfont/fallback-${md5HexPrefix(text)}.css`,
608
+ url: assetGraph.resolveUrl(subsetUrl, `fallback-${md5HexPrefix(text)}.css`),
546
609
  text,
547
610
  });
548
611
  return fallbackAsset;
@@ -574,62 +637,279 @@ function cssAssetIsEmpty(cssAsset) {
574
637
  );
575
638
  }
576
639
 
577
- async function subsetFonts(
578
- assetGraph,
579
- {
580
- formats = ['woff2', 'woff'],
581
- subsetPath = 'subfont/',
582
- omitFallbacks = false,
583
- inlineCss,
584
- fontDisplay,
585
- hrefType = 'rootRelative',
586
- onlyInfo,
587
- dynamic,
588
- console = global.console,
589
- text,
590
- } = {}
640
+ function parseFontWeightRange(str) {
641
+ let minFontWeight = 400;
642
+ let maxFontWeight = 400;
643
+ const fontWeightTokens = str.split(/\s+/).map((str) => parseFloat(str));
644
+ if (
645
+ [1, 2].includes(fontWeightTokens.length) &&
646
+ !fontWeightTokens.some(isNaN)
647
+ ) {
648
+ minFontWeight = maxFontWeight = fontWeightTokens[0];
649
+ if (fontWeightTokens.length === 2) {
650
+ maxFontWeight = fontWeightTokens[1];
651
+ }
652
+ }
653
+ return [minFontWeight, maxFontWeight];
654
+ }
655
+
656
+ function warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph) {
657
+ const missingGlyphsErrors = [];
658
+
659
+ for (const {
660
+ htmlOrSvgAsset,
661
+ fontUsages,
662
+ accumulatedFontFaceDeclarations,
663
+ } of htmlOrSvgAssetTextsWithProps) {
664
+ for (const fontUsage of fontUsages) {
665
+ if (fontUsage.subsets) {
666
+ const characterSet = fontkit.create(
667
+ Object.values(fontUsage.subsets)[0]
668
+ ).characterSet;
669
+
670
+ let missedAny = false;
671
+ for (const char of [...fontUsage.pageText]) {
672
+ // Turns out that browsers don't mind that these are missing:
673
+ if (char === '\t' || char === '\n') {
674
+ continue;
675
+ }
676
+
677
+ const codePoint = char.codePointAt(0);
678
+
679
+ const isMissing = !characterSet.includes(codePoint);
680
+
681
+ if (isMissing) {
682
+ let location;
683
+ const charIdx = htmlOrSvgAsset.text.indexOf(char);
684
+
685
+ if (charIdx === -1) {
686
+ location = `${htmlOrSvgAsset.urlOrDescription} (generated content)`;
687
+ } else {
688
+ const position = new LinesAndColumns(
689
+ htmlOrSvgAsset.text
690
+ ).locationForIndex(charIdx);
691
+ location = `${htmlOrSvgAsset.urlOrDescription}:${
692
+ position.line + 1
693
+ }:${position.column + 1}`;
694
+ }
695
+
696
+ missingGlyphsErrors.push({
697
+ codePoint,
698
+ char,
699
+ htmlOrSvgAsset,
700
+ fontUsage,
701
+ location,
702
+ });
703
+ missedAny = true;
704
+ }
705
+ }
706
+ if (missedAny) {
707
+ const fontFaces = accumulatedFontFaceDeclarations.filter((fontFace) =>
708
+ fontUsage.fontFamilies.has(fontFace['font-family'])
709
+ );
710
+ for (const fontFace of fontFaces) {
711
+ const cssFontFaceSrc = fontFace.relations[0];
712
+ const fontFaceDeclaration = cssFontFaceSrc.node;
713
+ if (
714
+ !fontFaceDeclaration.some((node) => node.prop === 'unicode-range')
715
+ ) {
716
+ fontFaceDeclaration.append({
717
+ prop: 'unicode-range',
718
+ value: unicodeRange(fontUsage.codepoints.original),
719
+ });
720
+ cssFontFaceSrc.from.markDirty();
721
+ }
722
+ }
723
+ }
724
+ }
725
+ }
726
+ }
727
+
728
+ if (missingGlyphsErrors.length) {
729
+ const errorLog = missingGlyphsErrors.map(
730
+ ({ char, fontUsage, location }) =>
731
+ `- \\u{${char.codePointAt(0).toString(16)}} (${char}) in font-family '${
732
+ fontUsage.props['font-family']
733
+ }' (${fontUsage.props['font-weight']}/${
734
+ fontUsage.props['font-style']
735
+ }) at ${location}`
736
+ );
737
+
738
+ const message = `Missing glyph fallback detected.
739
+ When your primary webfont doesn't contain the glyphs you use, browsers that don't support unicode-range will load your fallback fonts, which will be a potential waste of bandwidth.
740
+ These glyphs are used on your site, but they don't exist in the font you applied to them:`;
741
+
742
+ assetGraph.info(new Error(`${message}\n${errorLog.join('\n')}`));
743
+ }
744
+ }
745
+
746
+ const standardVariationAxes = new Set(['wght', 'wdth', 'ital', 'slnt', 'opsz']);
747
+ // Tracing the ranges of these standard axes require a bit more work, so just skip them for now:
748
+ const ignoredVariationAxes = new Set(['wdth', 'slnt', 'opsz']);
749
+
750
+ function renderNumberRange(min, max) {
751
+ if (min === max) {
752
+ return String(min);
753
+ } else {
754
+ return `${min}-${max}`;
755
+ }
756
+ }
757
+
758
+ function warnAboutUnusedVariationAxes(
759
+ htmlOrSvgAssetTextsWithProps,
760
+ assetGraph
591
761
  ) {
592
- if (!validFontDisplayValues.includes(fontDisplay)) {
593
- fontDisplay = undefined;
762
+ const seenAxisValuesByFontUrlAndAxisName = new Map();
763
+ const outOfBoundsAxesByFontUrl = new Map();
764
+
765
+ for (const { fontUsages } of htmlOrSvgAssetTextsWithProps) {
766
+ for (const {
767
+ fontUrl,
768
+ fontVariationSettings,
769
+ fontStyles,
770
+ fontWeights,
771
+ hasOutOfBoundsAnimationTimingFunction,
772
+ props,
773
+ } of fontUsages) {
774
+ let seenAxes = seenAxisValuesByFontUrlAndAxisName.get(fontUrl);
775
+ if (!seenAxes) {
776
+ seenAxes = new Map();
777
+ seenAxisValuesByFontUrlAndAxisName.set(fontUrl, seenAxes);
778
+ }
779
+ const seenItalValues = [];
780
+ if (fontStyles.has('italic')) {
781
+ seenItalValues.push(1);
782
+ }
783
+ // If any font-style value except italic is seen (including normal or oblique)
784
+ // we're also utilizing value 0:
785
+ if (fontStyles.size > fontStyles.has('italic') ? 1 : 0) {
786
+ seenItalValues.push(0);
787
+ }
788
+ if (seenItalValues.length > 0) {
789
+ if (seenAxes.has('ital')) {
790
+ seenAxes.get('ital').push(...seenItalValues);
791
+ } else {
792
+ seenAxes.set('ital', seenItalValues);
793
+ }
794
+ }
795
+
796
+ const minMaxFontWeight = parseFontWeightRange(props['font-weight']);
797
+ const seenFontWeightValues = [];
798
+ for (const fontWeight of fontWeights) {
799
+ seenFontWeightValues.push(_.clamp(fontWeight, ...minMaxFontWeight));
800
+ }
801
+ if (seenFontWeightValues.length > 0) {
802
+ if (seenAxes.has('wght')) {
803
+ seenAxes.get('wght').push(...seenFontWeightValues);
804
+ } else {
805
+ seenAxes.set('wght', seenFontWeightValues);
806
+ }
807
+ }
808
+
809
+ for (const fontVariationSettingsValue of fontVariationSettings) {
810
+ for (const [axisName, axisValue] of parseFontVariationSettings(
811
+ fontVariationSettingsValue
812
+ )) {
813
+ const seenAxisValues = seenAxes.get(axisName);
814
+ if (seenAxisValues) {
815
+ seenAxisValues.push(axisValue);
816
+ } else {
817
+ seenAxes.set(axisName, [axisValue]);
818
+ }
819
+ if (hasOutOfBoundsAnimationTimingFunction) {
820
+ let outOfBoundsAxes = outOfBoundsAxesByFontUrl.get(fontUrl);
821
+ if (!outOfBoundsAxes) {
822
+ outOfBoundsAxes = new Set();
823
+ outOfBoundsAxesByFontUrl.set(fontUrl, outOfBoundsAxes);
824
+ }
825
+ outOfBoundsAxes.add(axisName);
826
+ }
827
+ }
828
+ }
829
+ }
594
830
  }
595
831
 
596
- const htmlOrSvgAssetTextsWithProps = [];
597
- const subsetUrl = urltools.ensureTrailingSlash(assetGraph.root + subsetPath);
832
+ const warnings = [];
833
+ for (const [
834
+ fontUrl,
835
+ seenAxisValuesByAxisName,
836
+ ] of seenAxisValuesByFontUrlAndAxisName.entries()) {
837
+ const outOfBoundsAxes = outOfBoundsAxesByFontUrl.get(fontUrl) || new Set();
838
+ let font;
839
+ try {
840
+ font = fontkit.create(assetGraph.findAssets({ url: fontUrl })[0].rawSrc);
841
+ } catch (err) {
842
+ // Don't break if we encounter an invalid font or one that's unsupported by fontkit
843
+ continue;
844
+ }
845
+ const unusedAxes = [];
846
+ const underutilizedAxes = [];
847
+ for (const [name, { min, max, default: defaultValue }] of Object.entries(
848
+ font.variationAxes
849
+ )) {
850
+ if (ignoredVariationAxes.has(name)) {
851
+ continue;
852
+ }
853
+ if (seenAxisValuesByAxisName.has(name) && !outOfBoundsAxes.has(name)) {
854
+ const usedValues = [...seenAxisValuesByAxisName.get(name)];
855
+ if (!standardVariationAxes.has(name)) {
856
+ usedValues.push(defaultValue);
857
+ }
858
+ const minUsed = Math.min(...usedValues);
859
+ const maxUsed = Math.max(...usedValues);
860
+ if (minUsed > min || maxUsed < max) {
861
+ underutilizedAxes.push({
862
+ name,
863
+ minUsed,
864
+ maxUsed,
865
+ min,
866
+ max,
867
+ });
868
+ }
869
+ } else {
870
+ unusedAxes.push(name);
871
+ }
872
+ }
598
873
 
599
- await assetGraph.applySourceMaps({ type: 'Css' });
874
+ if (unusedAxes.length > 0 || underutilizedAxes.length > 0) {
875
+ let message = `${fontUrl}:\n`;
876
+ if (unusedAxes.length > 0) {
877
+ message += ` Unused axes: ${unusedAxes.join(', ')}\n`;
878
+ }
879
+ if (underutilizedAxes.length > 0) {
880
+ message += ` Underutilized axes:\n${underutilizedAxes
881
+ .map(
882
+ ({ name, min, max, minUsed, maxUsed }) =>
883
+ ` ${name}: ${renderNumberRange(
884
+ minUsed,
885
+ maxUsed
886
+ )} used (${min}-${max} available)`
887
+ )
888
+ .join('\n')}\n`;
889
+ }
890
+ warnings.push(message);
891
+ }
892
+ }
600
893
 
601
- await assetGraph.populate({
602
- followRelations: {
603
- $or: [
604
- {
605
- to: {
606
- url: googleFontsCssUrlRegex,
607
- },
608
- },
609
- {
610
- type: 'CssFontFaceSrc',
611
- from: {
612
- url: googleFontsCssUrlRegex,
613
- },
614
- },
615
- ],
616
- },
617
- });
894
+ if (warnings.length > 0) {
895
+ assetGraph.info(
896
+ new Error(`🪓 Unused variation axes detected in your variable fonts.
897
+ The below variable fonts contain custom axes that do not appear to be fully used on any of your pages.
898
+ This bloats your fonts and also the subset fonts that subfont creates.
899
+ Consider removing the unused axis ranges using a tool like Slice <https://slice-gui.netlify.app/>
900
+ ${warnings.join('\n')}`)
901
+ );
902
+ }
903
+ }
618
904
 
619
- // Collect texts by page
905
+ async function collectTextsByPage(
906
+ assetGraph,
907
+ htmlOrSvgAssets,
908
+ { text, console, dynamic = false } = {}
909
+ ) {
910
+ const htmlOrSvgAssetTextsWithProps = [];
620
911
 
621
912
  const memoizedGetCssRulesByProperty = memoizeSync(getCssRulesByProperty);
622
- const htmlOrSvgAssets = assetGraph.findAssets({
623
- $or: [
624
- {
625
- type: 'Html',
626
- isInline: false,
627
- },
628
- {
629
- type: 'Svg',
630
- },
631
- ],
632
- });
633
913
  const traversalRelationQuery = {
634
914
  $or: [
635
915
  {
@@ -644,12 +924,7 @@ async function subsetFonts(
644
924
  ],
645
925
  };
646
926
 
647
- // Keep track of the injected CSS assets that should eventually be minified
648
- // Minifying them along the way currently doesn't work because some of the
649
- // manipulation is sensitive to the exact text contents. We should fix that.
650
- const subsetFontsToBeMinified = new Set();
651
927
  const fontFaceDeclarationsByHtmlOrSvgAsset = new Map();
652
- const potentiallyOrphanedAssets = new Set();
653
928
 
654
929
  const headlessBrowser = dynamic && new HeadlessBrowser({ console });
655
930
  const globalTextByProps = [];
@@ -685,9 +960,8 @@ async function subsetFonts(
685
960
  node.walkDecls((declaration) => {
686
961
  const propName = declaration.prop.toLowerCase();
687
962
  if (propName === 'font-family') {
688
- fontFaceDeclaration[propName] = fontFamily.parse(
689
- declaration.value
690
- )[0];
963
+ fontFaceDeclaration[propName] =
964
+ cssFontParser.parseFontFamily(declaration.value)[0];
691
965
  } else {
692
966
  fontFaceDeclaration[propName] = declaration.value;
693
967
  }
@@ -774,7 +1048,72 @@ async function subsetFonts(
774
1048
  }
775
1049
  }
776
1050
  }
1051
+ return { htmlOrSvgAssetTextsWithProps, fontFaceDeclarationsByHtmlOrSvgAsset };
1052
+ }
1053
+
1054
+ async function subsetFonts(
1055
+ assetGraph,
1056
+ {
1057
+ formats = ['woff2', 'woff'],
1058
+ subsetPath = 'subfont/',
1059
+ omitFallbacks = false,
1060
+ inlineCss,
1061
+ fontDisplay,
1062
+ hrefType = 'rootRelative',
1063
+ onlyInfo,
1064
+ dynamic,
1065
+ console = global.console,
1066
+ text,
1067
+ } = {}
1068
+ ) {
1069
+ if (!validFontDisplayValues.includes(fontDisplay)) {
1070
+ fontDisplay = undefined;
1071
+ }
1072
+
1073
+ const subsetUrl = urltools.ensureTrailingSlash(assetGraph.root + subsetPath);
1074
+
1075
+ await assetGraph.applySourceMaps({ type: 'Css' });
777
1076
 
1077
+ await assetGraph.populate({
1078
+ followRelations: {
1079
+ $or: [
1080
+ {
1081
+ to: {
1082
+ url: { $regex: googleFontsCssUrlRegex },
1083
+ },
1084
+ },
1085
+ {
1086
+ type: 'CssFontFaceSrc',
1087
+ from: {
1088
+ url: { $regex: googleFontsCssUrlRegex },
1089
+ },
1090
+ },
1091
+ ],
1092
+ },
1093
+ });
1094
+
1095
+ const htmlOrSvgAssets = assetGraph.findAssets({
1096
+ $or: [
1097
+ {
1098
+ type: 'Html',
1099
+ isInline: false,
1100
+ },
1101
+ {
1102
+ type: 'Svg',
1103
+ },
1104
+ ],
1105
+ });
1106
+
1107
+ // Collect texts by page
1108
+
1109
+ const { htmlOrSvgAssetTextsWithProps, fontFaceDeclarationsByHtmlOrSvgAsset } =
1110
+ await collectTextsByPage(assetGraph, htmlOrSvgAssets, {
1111
+ text,
1112
+ console,
1113
+ dynamic,
1114
+ });
1115
+
1116
+ const potentiallyOrphanedAssets = new Set();
778
1117
  if (omitFallbacks) {
779
1118
  for (const htmlOrSvgAsset of htmlOrSvgAssets) {
780
1119
  const accumulatedFontFaceDeclarations =
@@ -848,94 +1187,8 @@ async function subsetFonts(
848
1187
  formats
849
1188
  );
850
1189
 
851
- // Warn about missing glyphs
852
- const missingGlyphsErrors = [];
853
-
854
- for (const {
855
- htmlOrSvgAsset,
856
- fontUsages,
857
- accumulatedFontFaceDeclarations,
858
- } of htmlOrSvgAssetTextsWithProps) {
859
- for (const fontUsage of fontUsages) {
860
- if (fontUsage.subsets) {
861
- const characterSet = fontkit.create(
862
- Object.values(fontUsage.subsets)[0]
863
- ).characterSet;
864
-
865
- let missedAny = false;
866
- for (const char of [...fontUsage.pageText]) {
867
- // Turns out that browsers don't mind that these are missing:
868
- if (char === '\t' || char === '\n') {
869
- continue;
870
- }
871
-
872
- const codePoint = char.codePointAt(0);
873
-
874
- const isMissing = !characterSet.includes(codePoint);
875
-
876
- if (isMissing) {
877
- let location;
878
- const charIdx = htmlOrSvgAsset.text.indexOf(char);
879
-
880
- if (charIdx === -1) {
881
- location = `${htmlOrSvgAsset.urlOrDescription} (generated content)`;
882
- } else {
883
- const position = new LinesAndColumns(
884
- htmlOrSvgAsset.text
885
- ).locationForIndex(charIdx);
886
- location = `${htmlOrSvgAsset.urlOrDescription}:${
887
- position.line + 1
888
- }:${position.column + 1}`;
889
- }
890
-
891
- missingGlyphsErrors.push({
892
- codePoint,
893
- char,
894
- htmlOrSvgAsset,
895
- fontUsage,
896
- location,
897
- });
898
- missedAny = true;
899
- }
900
- }
901
- if (missedAny) {
902
- const fontFaces = accumulatedFontFaceDeclarations.filter((fontFace) =>
903
- fontUsage.fontFamilies.has(fontFace['font-family'])
904
- );
905
- for (const fontFace of fontFaces) {
906
- const cssFontFaceSrc = fontFace.relations[0];
907
- const fontFaceDeclaration = cssFontFaceSrc.node;
908
- if (
909
- !fontFaceDeclaration.some((node) => node.prop === 'unicode-range')
910
- ) {
911
- fontFaceDeclaration.append({
912
- prop: 'unicode-range',
913
- value: unicodeRange(fontUsage.codepoints.original),
914
- });
915
- cssFontFaceSrc.from.markDirty();
916
- }
917
- }
918
- }
919
- }
920
- }
921
- }
922
-
923
- if (missingGlyphsErrors.length) {
924
- const errorLog = missingGlyphsErrors.map(
925
- ({ char, fontUsage, location }) =>
926
- `- \\u{${char.codePointAt(0).toString(16)}} (${char}) in font-family '${
927
- fontUsage.props['font-family']
928
- }' (${fontUsage.props['font-weight']}/${
929
- fontUsage.props['font-style']
930
- }) at ${location}`
931
- );
932
-
933
- const message = `Missing glyph fallback detected.
934
- When your primary webfont doesn't contain the glyphs you use, browsers that don't support unicode-range will load your fallback fonts, which will be a potential waste of bandwidth.
935
- These glyphs are used on your site, but they don't exist in the font you applied to them:`;
936
-
937
- assetGraph.info(new Error(`${message}\n${errorLog.join('\n')}`));
938
- }
1190
+ warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
1191
+ warnAboutUnusedVariationAxes(htmlOrSvgAssetTextsWithProps, assetGraph);
939
1192
 
940
1193
  // Insert subsets:
941
1194
 
@@ -1021,7 +1274,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1021
1274
  text: subsetCssText,
1022
1275
  });
1023
1276
 
1024
- subsetFontsToBeMinified.add(cssAsset);
1277
+ await cssAsset.minify();
1025
1278
 
1026
1279
  for (const [i, fontRelation] of cssAsset.outgoingRelations.entries()) {
1027
1280
  const fontAsset = fontRelation.to;
@@ -1091,7 +1344,6 @@ These glyphs are used on your site, but they don't exist in the font you applied
1091
1344
  const existingCssAsset = assetGraph.findAssets({ url: cssAssetUrl })[0];
1092
1345
  if (existingCssAsset) {
1093
1346
  assetGraph.removeAsset(cssAsset);
1094
- subsetFontsToBeMinified.delete(cssAsset);
1095
1347
  cssAsset = existingCssAsset;
1096
1348
  } else {
1097
1349
  cssAsset.url = cssAssetUrl;
@@ -1105,7 +1357,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1105
1357
 
1106
1358
  if (
1107
1359
  fontAsset.contentType === 'font/woff2' &&
1108
- fontRelation.to.path.startsWith('/subfont/')
1360
+ fontRelation.to.url.startsWith(subsetUrl)
1109
1361
  ) {
1110
1362
  const fontFaceDeclaration = fontRelation.node;
1111
1363
  const originalFontFamily = unquote(
@@ -1226,7 +1478,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1226
1478
  assetGraph.removeAsset(cssAsset);
1227
1479
  cssAsset = existingCssAsset;
1228
1480
  } else {
1229
- subsetFontsToBeMinified.add(cssAsset);
1481
+ await cssAsset.minify();
1230
1482
  cssAsset.url = cssAssetUrl;
1231
1483
  }
1232
1484
 
@@ -1309,9 +1561,10 @@ These glyphs are used on your site, but they don't exist in the font you applied
1309
1561
  assetGraph,
1310
1562
  googleFontStylesheetRelation.to,
1311
1563
  formats,
1312
- hrefType
1564
+ hrefType,
1565
+ subsetUrl
1313
1566
  );
1314
- subsetFontsToBeMinified.add(selfHostedGoogleFontsCssAsset);
1567
+ await selfHostedGoogleFontsCssAsset.minify();
1315
1568
  selfHostedGoogleCssByUrl.set(
1316
1569
  googleFontStylesheetRelation.to.url,
1317
1570
  selfHostedGoogleFontsCssAsset
@@ -1374,7 +1627,9 @@ These glyphs are used on your site, but they don't exist in the font you applied
1374
1627
  );
1375
1628
  for (let i = 0; i < fontFamilies.length; i += 1) {
1376
1629
  const subsetFontFamily =
1377
- webfontNameMap[fontFamily.parse(fontFamilies[i])[0].toLowerCase()];
1630
+ webfontNameMap[
1631
+ cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1632
+ ];
1378
1633
  if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1379
1634
  fontFamilies.splice(
1380
1635
  i,
@@ -1434,7 +1689,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1434
1689
  for (let i = 0; i < fontFamilies.length; i += 1) {
1435
1690
  const subsetFontFamily =
1436
1691
  webfontNameMap[
1437
- fontFamily.parse(fontFamilies[i])[0].toLowerCase()
1692
+ cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1438
1693
  ];
1439
1694
  if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1440
1695
  fontFamilies.splice(
@@ -1448,7 +1703,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1448
1703
  }
1449
1704
  }
1450
1705
  } else if (propName === 'font') {
1451
- const fontProperties = cssFontParser(cssRule.value);
1706
+ const fontProperties = cssFontParser.parseFont(cssRule.value);
1452
1707
  const fontFamilies =
1453
1708
  fontProperties && fontProperties['font-family'].map(unquote);
1454
1709
  if (fontFamilies) {
@@ -1492,11 +1747,6 @@ These glyphs are used on your site, but they don't exist in the font you applied
1492
1747
  }
1493
1748
  }
1494
1749
 
1495
- // This is a bit awkward now, but if it's done sooner, it breaks the CSS source regexping:
1496
- for (const cssAsset of subsetFontsToBeMinified) {
1497
- await cssAsset.minify();
1498
- }
1499
-
1500
1750
  await assetGraph.serializeSourceMaps(undefined, {
1501
1751
  type: 'Css',
1502
1752
  outgoingRelations: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subfont",
3
- "version": "6.8.0",
3
+ "version": "6.11.0",
4
4
  "description": "Speeds up your pages initial paint by automatically subsetting local or Google fonts and loading them optimally",
5
5
  "engines": {
6
6
  "node": ">=10.0.0"
@@ -47,14 +47,14 @@
47
47
  "homepage": "https://github.com/Munter/subfont#readme",
48
48
  "dependencies": {
49
49
  "@gustavnikolaj/async-main-wrap": "^3.0.1",
50
+ "@hookun/parse-animation-shorthand": "^0.1.4",
50
51
  "assetgraph": "^7.8.1",
51
52
  "browserslist": "^4.13.0",
52
- "css-font-parser": "^0.3.0",
53
+ "css-font-parser": "^2.0.0",
53
54
  "css-font-weight-names": "^0.2.1",
54
55
  "css-list-helpers": "^2.0.0",
55
- "font-family-papandreou": "^0.2.0-patch2",
56
56
  "font-snapper": "^1.2.0",
57
- "font-tracer": "^3.2.0",
57
+ "font-tracer": "^3.6.0",
58
58
  "fontkit": "^1.8.0",
59
59
  "fontverter": "^2.0.0",
60
60
  "gettemporaryfilepath": "^1.0.1",