subfont 6.9.0 → 6.12.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.12.0 (2022-09-07)
2
+
3
+ - [Refactor repeated code into helper function](https://github.com/Munter/subfont/commit/5335d931957cadc150fed1c5d548ee6b6e245636) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
4
+ - [Clamp the used variation axis values to the available interval](https://github.com/Munter/subfont/commit/1429c0f35720ee8dadb691138eb4cfa3c1c86150) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
5
+ - [Detech unused slnt variation axis ranges](https://github.com/Munter/subfont/commit/dc4be1598ba1ba0e65d8269d0fa2d25aa528357d) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
6
+ - [Report variation axes that only use the default value as unused rather than underutilized](https://github.com/Munter/subfont/commit/12082590898407f558d9540347778272ddb9d197) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
7
+ - [Detect unused wdth variation axis ranges](https://github.com/Munter/subfont/commit/d7758eb3c9a45a49768aac12c36ed897a0c4b467) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
8
+
9
+ ### v6.11.0 (2022-09-04)
10
+
11
+ - [Treat wdth as a standard axis \(but unsupported for now\)](https://github.com/Munter/subfont/commit/20d539750448134cca0a82f2cc98f85d9a6be068) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
12
+ - [Detect unused ital variation axis ranges](https://github.com/Munter/subfont/commit/b848b21b66cdbcd0ff313690ba49a4bc9b9f90a4) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
13
+ - [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))
14
+ - [Detect unused wght variation axis ranges](https://github.com/Munter/subfont/commit/6a964d609f21bf2c9dad96da1d16d9c32c28dbfd) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
15
+
16
+ ### v6.10.0 (2022-08-29)
17
+
18
+ - [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))
19
+ - [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))
20
+ - [Update font-tracer to ^3.6.0](https://github.com/Munter/subfont/commit/2a47adc03122aad50ba2a01f116a2d1fe0f2b29e) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
21
+ - [Remove accidentally committed commented out code](https://github.com/Munter/subfont/commit/e0c677f9b2c7a9af123045cf29bc78a4f9a56550) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
22
+ - [Warn about unused variation axis ranges, not just fully unused axes](https://github.com/Munter/subfont/commit/32fc472327b1c8fc87090d4c1e5f1085533ad8fa) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
23
+ - [+13 more](https://github.com/Munter/subfont/compare/v6.9.0...v6.10.0)
24
+
1
25
  ### v6.9.0 (2022-08-07)
2
26
 
3
27
  - [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))
@@ -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 = [];
@@ -1,6 +1,7 @@
1
1
  const cssFontWeightNames = require('css-font-weight-names');
2
2
  const initialValueByProp = require('./initialValueByProp');
3
3
  const unquote = require('./unquote');
4
+ const normalizeFontStretch = require('font-snapper/lib/normalizeFontStretch');
4
5
 
5
6
  function normalizeFontPropertyValue(propName, value) {
6
7
  const propNameLowerCase = propName.toLowerCase();
@@ -22,6 +23,8 @@ function normalizeFontPropertyValue(propName, value) {
22
23
  } else {
23
24
  return value;
24
25
  }
26
+ } else if (propNameLowerCase === 'font-stretch') {
27
+ return normalizeFontStretch(value);
25
28
  } else if (typeof value === 'string' && propNameLowerCase !== 'src') {
26
29
  return value.toLowerCase();
27
30
  }
@@ -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,31 @@ 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
+ fontStretch: normalizeFontPropertyValue(
217
+ 'font-stretch',
218
+ textAndProps.props['font-stretch']
219
+ ),
220
+ animationTimingFunction:
221
+ textAndProps.props['animation-timing-function'],
176
222
  props,
177
223
  fontRelations: relations,
178
224
  fontUrl,
@@ -190,6 +236,23 @@ function groupTextsByFontFamilyProps(
190
236
  const fontFamilies = new Set(
191
237
  textsPropsArray.map((obj) => obj.props['font-family'])
192
238
  );
239
+ const fontStyles = new Set(textsPropsArray.map((obj) => obj.fontStyle));
240
+ const fontWeights = new Set(textsPropsArray.map((obj) => obj.fontWeight));
241
+ const fontStretches = new Set(
242
+ textsPropsArray.map((obj) => obj.fontStretch)
243
+ );
244
+ const fontVariationSettings = new Set(
245
+ textsPropsArray
246
+ .map((obj) => obj.fontVariationSettings)
247
+ .filter(
248
+ (fontVariationSettings) =>
249
+ fontVariationSettings &&
250
+ fontVariationSettings.toLowerCase() !== 'normal'
251
+ )
252
+ );
253
+ const hasOutOfBoundsAnimationTimingFunction = textsPropsArray.some((obj) =>
254
+ isOutOfBoundsAnimationTimingFunction(obj.animationTimingFunction)
255
+ );
193
256
 
194
257
  let smallestOriginalSize;
195
258
  let smallestOriginalFormat;
@@ -217,6 +280,11 @@ function groupTextsByFontFamilyProps(
217
280
  props: { ...textsPropsArray[0].props },
218
281
  fontUrl,
219
282
  fontFamilies,
283
+ fontStyles,
284
+ fontStretches,
285
+ fontWeights,
286
+ fontVariationSettings,
287
+ hasOutOfBoundsAnimationTimingFunction,
220
288
  preload,
221
289
  };
222
290
  });
@@ -497,9 +565,9 @@ async function createSelfHostedGoogleFontsCssAsset(
497
565
  assetGraph,
498
566
  googleFontsCssAsset,
499
567
  formats,
500
- hrefType
568
+ hrefType,
569
+ subsetUrl
501
570
  ) {
502
- const baseUrl = assetGraph.resolveUrl(assetGraph.root, '/subfont/');
503
571
  const lines = [];
504
572
  for (const cssFontFaceSrc of assetGraph.findRelations({
505
573
  from: googleFontsCssAsset,
@@ -516,9 +584,12 @@ async function createSelfHostedGoogleFontsCssAsset(
516
584
  const srcFragments = [];
517
585
  for (const format of formats) {
518
586
  const rawSrc = await fontverter.convert(cssFontFaceSrc.to.rawSrc, format);
519
- const url = `${assetGraph.root}subfont/${
520
- cssFontFaceSrc.to.baseName
521
- }-${md5HexPrefix(rawSrc)}${extensionByFormat[format]}`;
587
+ const url = assetGraph.resolveUrl(
588
+ subsetUrl,
589
+ `${cssFontFaceSrc.to.baseName}-${md5HexPrefix(rawSrc)}${
590
+ extensionByFormat[format]
591
+ }`
592
+ );
522
593
  const fontAsset =
523
594
  assetGraph.findAssets({ url })[0] ||
524
595
  (await assetGraph.addAsset({
@@ -526,7 +597,7 @@ async function createSelfHostedGoogleFontsCssAsset(
526
597
  rawSrc,
527
598
  }));
528
599
  srcFragments.push(
529
- `url(${assetGraph.buildHref(fontAsset.url, baseUrl, {
600
+ `url(${assetGraph.buildHref(fontAsset.url, subsetUrl, {
530
601
  hrefType,
531
602
  })}) format('${format}')`
532
603
  );
@@ -542,7 +613,7 @@ async function createSelfHostedGoogleFontsCssAsset(
542
613
  const text = lines.join('\n');
543
614
  const fallbackAsset = assetGraph.addAsset({
544
615
  type: 'Css',
545
- url: `/subfont/fallback-${md5HexPrefix(text)}.css`,
616
+ url: assetGraph.resolveUrl(subsetUrl, `fallback-${md5HexPrefix(text)}.css`),
546
617
  text,
547
618
  });
548
619
  return fallbackAsset;
@@ -574,6 +645,46 @@ function cssAssetIsEmpty(cssAsset) {
574
645
  );
575
646
  }
576
647
 
648
+ function parseFontWeightRange(str) {
649
+ if (typeof str === 'undefined' || str === 'auto') {
650
+ return [-Infinity, Infinity];
651
+ }
652
+ let minFontWeight = 400;
653
+ let maxFontWeight = 400;
654
+ const fontWeightTokens = str.split(/\s+/).map((str) => parseFloat(str));
655
+ if (
656
+ [1, 2].includes(fontWeightTokens.length) &&
657
+ !fontWeightTokens.some(isNaN)
658
+ ) {
659
+ minFontWeight = maxFontWeight = fontWeightTokens[0];
660
+ if (fontWeightTokens.length === 2) {
661
+ maxFontWeight = fontWeightTokens[1];
662
+ }
663
+ }
664
+ return [minFontWeight, maxFontWeight];
665
+ }
666
+
667
+ function parseFontStretchRange(str) {
668
+ if (typeof str === 'undefined' || str.toLowerCase() === 'auto') {
669
+ return [-Infinity, Infinity];
670
+ }
671
+ let minFontStretch = 100;
672
+ let maxFontStretch = 100;
673
+ const fontStretchTokens = str
674
+ .split(/\s+/)
675
+ .map((str) => normalizeFontPropertyValue('font-stretch', str));
676
+ if (
677
+ [1, 2].includes(fontStretchTokens.length) &&
678
+ !fontStretchTokens.some(isNaN)
679
+ ) {
680
+ minFontStretch = maxFontStretch = fontStretchTokens[0];
681
+ if (fontStretchTokens.length === 2) {
682
+ maxFontStretch = fontStretchTokens[1];
683
+ }
684
+ }
685
+ return [minFontStretch, maxFontStretch];
686
+ }
687
+
577
688
  function warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph) {
578
689
  const missingGlyphsErrors = [];
579
690
 
@@ -664,6 +775,183 @@ These glyphs are used on your site, but they don't exist in the font you applied
664
775
  }
665
776
  }
666
777
 
778
+ const standardVariationAxes = new Set(['wght', 'wdth', 'ital', 'slnt', 'opsz']);
779
+ // It would be very hard to trace statically which values of opsz (font-optical-sizing)
780
+ // are going to be used, so we ignore that one:
781
+ const ignoredVariationAxes = new Set(['opsz']);
782
+
783
+ function renderNumberRange(min, max) {
784
+ if (min === max) {
785
+ return String(min);
786
+ } else {
787
+ return `${min}-${max}`;
788
+ }
789
+ }
790
+
791
+ function warnAboutUnusedVariationAxes(
792
+ htmlOrSvgAssetTextsWithProps,
793
+ assetGraph
794
+ ) {
795
+ const seenAxisValuesByFontUrlAndAxisName = new Map();
796
+ const outOfBoundsAxesByFontUrl = new Map();
797
+
798
+ function noteUsedValue(fontUrl, axisName, axisValue) {
799
+ let seenAxes = seenAxisValuesByFontUrlAndAxisName.get(fontUrl);
800
+ if (!seenAxes) {
801
+ seenAxes = new Map();
802
+ seenAxisValuesByFontUrlAndAxisName.set(fontUrl, seenAxes);
803
+ }
804
+ if (seenAxes.has(axisName)) {
805
+ seenAxes.get(axisName).push(axisValue);
806
+ } else {
807
+ seenAxes.set(axisName, [axisValue]);
808
+ }
809
+ }
810
+
811
+ for (const { fontUsages } of htmlOrSvgAssetTextsWithProps) {
812
+ for (const {
813
+ fontUrl,
814
+ fontStyles,
815
+ fontWeights,
816
+ fontStretches,
817
+ fontVariationSettings,
818
+ hasOutOfBoundsAnimationTimingFunction,
819
+ props,
820
+ } of fontUsages) {
821
+ if (fontStyles.has('italic')) {
822
+ noteUsedValue(fontUrl, 'ital', 1);
823
+ }
824
+ // If any font-style value except italic is seen (including normal or oblique)
825
+ // we're also utilizing value 0:
826
+ if (fontStyles.size > fontStyles.has('italic') ? 1 : 0) {
827
+ noteUsedValue(fontUrl, 'ital', 0);
828
+ }
829
+ if (fontStyles.has('oblique')) {
830
+ // https://www.w3.org/TR/css-fonts-4/#font-style-prop
831
+ // oblique <angle>?
832
+ // [...] The lack of an <angle> represents 14deg.
833
+ noteUsedValue(fontUrl, 'slnt', 14);
834
+ }
835
+ // If any font-style value except oblique is seen (including normal or italic)
836
+ // we're also utilizing value 0:
837
+ if (fontStyles.size > fontStyles.has('oblique') ? 1 : 0) {
838
+ noteUsedValue(fontUrl, 'slnt', 0);
839
+ }
840
+
841
+ const minMaxFontWeight = parseFontWeightRange(props['font-weight']);
842
+ for (const fontWeight of fontWeights) {
843
+ noteUsedValue(
844
+ fontUrl,
845
+ 'wght',
846
+ _.clamp(fontWeight, ...minMaxFontWeight)
847
+ );
848
+ }
849
+
850
+ const minMaxFontStretch = parseFontStretchRange(props['font-stretch']);
851
+ for (const fontStrech of fontStretches) {
852
+ noteUsedValue(
853
+ fontUrl,
854
+ 'wdth',
855
+ _.clamp(fontStrech, ...minMaxFontStretch)
856
+ );
857
+ }
858
+
859
+ for (const fontVariationSettingsValue of fontVariationSettings) {
860
+ for (const [axisName, axisValue] of parseFontVariationSettings(
861
+ fontVariationSettingsValue
862
+ )) {
863
+ noteUsedValue(fontUrl, axisName, axisValue);
864
+ if (hasOutOfBoundsAnimationTimingFunction) {
865
+ let outOfBoundsAxes = outOfBoundsAxesByFontUrl.get(fontUrl);
866
+ if (!outOfBoundsAxes) {
867
+ outOfBoundsAxes = new Set();
868
+ outOfBoundsAxesByFontUrl.set(fontUrl, outOfBoundsAxes);
869
+ }
870
+ outOfBoundsAxes.add(axisName);
871
+ }
872
+ }
873
+ }
874
+ }
875
+ }
876
+
877
+ const warnings = [];
878
+ for (const [
879
+ fontUrl,
880
+ seenAxisValuesByAxisName,
881
+ ] of seenAxisValuesByFontUrlAndAxisName.entries()) {
882
+ const outOfBoundsAxes = outOfBoundsAxesByFontUrl.get(fontUrl) || new Set();
883
+ let font;
884
+ try {
885
+ font = fontkit.create(assetGraph.findAssets({ url: fontUrl })[0].rawSrc);
886
+ } catch (err) {
887
+ // Don't break if we encounter an invalid font or one that's unsupported by fontkit
888
+ continue;
889
+ }
890
+ const unusedAxes = [];
891
+ const underutilizedAxes = [];
892
+ for (const [name, { min, max, default: defaultValue }] of Object.entries(
893
+ font.variationAxes
894
+ )) {
895
+ if (ignoredVariationAxes.has(name)) {
896
+ continue;
897
+ }
898
+ let usedValues = [];
899
+ if (seenAxisValuesByAxisName.has(name) && !outOfBoundsAxes.has(name)) {
900
+ usedValues = [...seenAxisValuesByAxisName.get(name)].map((usedValue) =>
901
+ _.clamp(usedValue, min, max)
902
+ );
903
+ }
904
+ if (!usedValues.every((value) => value === defaultValue)) {
905
+ if (!standardVariationAxes.has(name)) {
906
+ usedValues.push(defaultValue);
907
+ }
908
+ const minUsed = Math.min(...usedValues);
909
+ const maxUsed = Math.max(...usedValues);
910
+ if (minUsed > min || maxUsed < max) {
911
+ underutilizedAxes.push({
912
+ name,
913
+ minUsed,
914
+ maxUsed,
915
+ min,
916
+ max,
917
+ });
918
+ }
919
+ } else {
920
+ unusedAxes.push(name);
921
+ }
922
+ }
923
+
924
+ if (unusedAxes.length > 0 || underutilizedAxes.length > 0) {
925
+ let message = `${fontUrl}:\n`;
926
+ if (unusedAxes.length > 0) {
927
+ message += ` Unused axes: ${unusedAxes.join(', ')}\n`;
928
+ }
929
+ if (underutilizedAxes.length > 0) {
930
+ message += ` Underutilized axes:\n${underutilizedAxes
931
+ .map(
932
+ ({ name, min, max, minUsed, maxUsed }) =>
933
+ ` ${name}: ${renderNumberRange(
934
+ minUsed,
935
+ maxUsed
936
+ )} used (${min}-${max} available)`
937
+ )
938
+ .join('\n')}\n`;
939
+ }
940
+ warnings.push(message);
941
+ }
942
+ }
943
+
944
+ if (warnings.length > 0) {
945
+ assetGraph.info(
946
+ new Error(`🪓 Unused variation axes detected in your variable fonts.
947
+ The below variable fonts contain custom axes that do not appear to be fully used on any of your pages.
948
+ This bloats your fonts and also the subset fonts that subfont creates.
949
+ Consider removing the unused axis ranges using a tool like Slice <https://slice-gui.netlify.app/>
950
+ ${warnings.join('\n')}`)
951
+ );
952
+ }
953
+ }
954
+
667
955
  async function collectTextsByPage(
668
956
  assetGraph,
669
957
  htmlOrSvgAssets,
@@ -722,9 +1010,8 @@ async function collectTextsByPage(
722
1010
  node.walkDecls((declaration) => {
723
1011
  const propName = declaration.prop.toLowerCase();
724
1012
  if (propName === 'font-family') {
725
- fontFaceDeclaration[propName] = fontFamily.parse(
726
- declaration.value
727
- )[0];
1013
+ fontFaceDeclaration[propName] =
1014
+ cssFontParser.parseFontFamily(declaration.value)[0];
728
1015
  } else {
729
1016
  fontFaceDeclaration[propName] = declaration.value;
730
1017
  }
@@ -951,6 +1238,7 @@ async function subsetFonts(
951
1238
  );
952
1239
 
953
1240
  warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
1241
+ warnAboutUnusedVariationAxes(htmlOrSvgAssetTextsWithProps, assetGraph);
954
1242
 
955
1243
  // Insert subsets:
956
1244
 
@@ -1119,7 +1407,7 @@ async function subsetFonts(
1119
1407
 
1120
1408
  if (
1121
1409
  fontAsset.contentType === 'font/woff2' &&
1122
- fontRelation.to.path.startsWith('/subfont/')
1410
+ fontRelation.to.url.startsWith(subsetUrl)
1123
1411
  ) {
1124
1412
  const fontFaceDeclaration = fontRelation.node;
1125
1413
  const originalFontFamily = unquote(
@@ -1323,7 +1611,8 @@ async function subsetFonts(
1323
1611
  assetGraph,
1324
1612
  googleFontStylesheetRelation.to,
1325
1613
  formats,
1326
- hrefType
1614
+ hrefType,
1615
+ subsetUrl
1327
1616
  );
1328
1617
  await selfHostedGoogleFontsCssAsset.minify();
1329
1618
  selfHostedGoogleCssByUrl.set(
@@ -1388,7 +1677,9 @@ async function subsetFonts(
1388
1677
  );
1389
1678
  for (let i = 0; i < fontFamilies.length; i += 1) {
1390
1679
  const subsetFontFamily =
1391
- webfontNameMap[fontFamily.parse(fontFamilies[i])[0].toLowerCase()];
1680
+ webfontNameMap[
1681
+ cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1682
+ ];
1392
1683
  if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1393
1684
  fontFamilies.splice(
1394
1685
  i,
@@ -1448,7 +1739,7 @@ async function subsetFonts(
1448
1739
  for (let i = 0; i < fontFamilies.length; i += 1) {
1449
1740
  const subsetFontFamily =
1450
1741
  webfontNameMap[
1451
- fontFamily.parse(fontFamilies[i])[0].toLowerCase()
1742
+ cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1452
1743
  ];
1453
1744
  if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1454
1745
  fontFamilies.splice(
@@ -1462,7 +1753,7 @@ async function subsetFonts(
1462
1753
  }
1463
1754
  }
1464
1755
  } else if (propName === 'font') {
1465
- const fontProperties = cssFontParser(cssRule.value);
1756
+ const fontProperties = cssFontParser.parseFont(cssRule.value);
1466
1757
  const fontFamilies =
1467
1758
  fontProperties && fontProperties['font-family'].map(unquote);
1468
1759
  if (fontFamilies) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subfont",
3
- "version": "6.9.0",
3
+ "version": "6.12.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.3.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",