subfont 6.7.0 → 6.10.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,25 @@
1
+ ### v6.10.0 (2022-08-29)
2
+
3
+ - [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))
4
+ - [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))
5
+ - [Update font-tracer to ^3.6.0](https://github.com/Munter/subfont/commit/2a47adc03122aad50ba2a01f116a2d1fe0f2b29e) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
6
+ - [Remove accidentally committed commented out code](https://github.com/Munter/subfont/commit/e0c677f9b2c7a9af123045cf29bc78a4f9a56550) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
7
+ - [Warn about unused variation axis ranges, not just fully unused axes](https://github.com/Munter/subfont/commit/32fc472327b1c8fc87090d4c1e5f1085533ad8fa) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
8
+ - [+13 more](https://github.com/Munter/subfont/compare/v6.9.0...v6.10.0)
9
+
10
+ ### v6.9.0 (2022-08-07)
11
+
12
+ - [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))
13
+ - [Move some code into a collectTextsByPage function](https://github.com/Munter/subfont/commit/1dff3819fb80793f23a3cc37f9ff58bafffa4efc) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
14
+ - [Move the missing glyph detection to a function](https://github.com/Munter/subfont/commit/38bf36eba4370ecdbc777cb2457696b9bc7c7d1c) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
15
+ - [Fix typo causing regexp to not be matched correctly](https://github.com/Munter/subfont/commit/05137708b5c48af305f3119deb836b1cd9fed683) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
16
+ - [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))
17
+
18
+ ### v6.8.0 (2022-07-28)
19
+
20
+ - [Update assetgraph to ^7.8.1](https://github.com/Munter/subfont/commit/888a97912f98bd937a53b7bec0f39d50ddc96023) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
21
+ - [Don't leave stylesheets behind that only contain comments after removing @font-face rules from them https:\/\/github.com\/Munter\/subfont\/issues\/160\#issuecomment-1194672752](https://github.com/Munter/subfont/commit/3dfd9dfbbe5071b94eb52ac216796bc7c0554078) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com))
22
+
1
23
  ### v6.7.0 (2022-07-24)
2
24
 
3
25
  - [Update subset-font to ^1.5.0](https://github.com/Munter/subfont/commit/ba79ef6db81cedfd0e52672b530ca3afddd68a94) ([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.toUpperCase();
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
 
@@ -173,6 +197,9 @@ function groupTextsByFontFamilyProps(
173
197
  return {
174
198
  htmlOrSvgAsset: textAndProps.htmlOrSvgAsset,
175
199
  text: textAndProps.text,
200
+ fontVariationSettings: textAndProps.props['font-variation-settings'],
201
+ animationTimingFunction:
202
+ textAndProps.props['animation-timing-function'],
176
203
  props,
177
204
  fontRelations: relations,
178
205
  fontUrl,
@@ -190,6 +217,18 @@ function groupTextsByFontFamilyProps(
190
217
  const fontFamilies = new Set(
191
218
  textsPropsArray.map((obj) => obj.props['font-family'])
192
219
  );
220
+ const fontVariationSettings = new Set(
221
+ textsPropsArray
222
+ .map((obj) => obj.fontVariationSettings)
223
+ .filter(
224
+ (fontVariationSettings) =>
225
+ fontVariationSettings &&
226
+ fontVariationSettings.toLowerCase() !== 'normal'
227
+ )
228
+ );
229
+ const hasOutOfBoundsAnimationTimingFunction = textsPropsArray.some((obj) =>
230
+ isOutOfBoundsAnimationTimingFunction(obj.animationTimingFunction)
231
+ );
193
232
 
194
233
  let smallestOriginalSize;
195
234
  let smallestOriginalFormat;
@@ -217,6 +256,8 @@ function groupTextsByFontFamilyProps(
217
256
  props: { ...textsPropsArray[0].props },
218
257
  fontUrl,
219
258
  fontFamilies,
259
+ fontVariationSettings,
260
+ hasOutOfBoundsAnimationTimingFunction,
220
261
  preload,
221
262
  };
222
263
  });
@@ -497,9 +538,9 @@ async function createSelfHostedGoogleFontsCssAsset(
497
538
  assetGraph,
498
539
  googleFontsCssAsset,
499
540
  formats,
500
- hrefType
541
+ hrefType,
542
+ subsetUrl
501
543
  ) {
502
- const baseUrl = assetGraph.resolveUrl(assetGraph.root, '/subfont/');
503
544
  const lines = [];
504
545
  for (const cssFontFaceSrc of assetGraph.findRelations({
505
546
  from: googleFontsCssAsset,
@@ -516,9 +557,12 @@ async function createSelfHostedGoogleFontsCssAsset(
516
557
  const srcFragments = [];
517
558
  for (const format of formats) {
518
559
  const rawSrc = await fontverter.convert(cssFontFaceSrc.to.rawSrc, format);
519
- const url = `${assetGraph.root}subfont/${
520
- cssFontFaceSrc.to.baseName
521
- }-${md5HexPrefix(rawSrc)}${extensionByFormat[format]}`;
560
+ const url = assetGraph.resolveUrl(
561
+ subsetUrl,
562
+ `${cssFontFaceSrc.to.baseName}-${md5HexPrefix(rawSrc)}${
563
+ extensionByFormat[format]
564
+ }`
565
+ );
522
566
  const fontAsset =
523
567
  assetGraph.findAssets({ url })[0] ||
524
568
  (await assetGraph.addAsset({
@@ -526,7 +570,7 @@ async function createSelfHostedGoogleFontsCssAsset(
526
570
  rawSrc,
527
571
  }));
528
572
  srcFragments.push(
529
- `url(${assetGraph.buildHref(fontAsset.url, baseUrl, {
573
+ `url(${assetGraph.buildHref(fontAsset.url, subsetUrl, {
530
574
  hrefType,
531
575
  })}) format('${format}')`
532
576
  );
@@ -542,7 +586,7 @@ async function createSelfHostedGoogleFontsCssAsset(
542
586
  const text = lines.join('\n');
543
587
  const fallbackAsset = assetGraph.addAsset({
544
588
  type: 'Css',
545
- url: `/subfont/fallback-${md5HexPrefix(text)}.css`,
589
+ url: assetGraph.resolveUrl(subsetUrl, `fallback-${md5HexPrefix(text)}.css`),
546
590
  text,
547
591
  });
548
592
  return fallbackAsset;
@@ -568,62 +612,228 @@ function getCodepoints(text) {
568
612
  return codepoints;
569
613
  }
570
614
 
571
- async function subsetFonts(
572
- assetGraph,
573
- {
574
- formats = ['woff2', 'woff'],
575
- subsetPath = 'subfont/',
576
- omitFallbacks = false,
577
- inlineCss,
578
- fontDisplay,
579
- hrefType = 'rootRelative',
580
- onlyInfo,
581
- dynamic,
582
- console = global.console,
583
- text,
584
- } = {}
615
+ function cssAssetIsEmpty(cssAsset) {
616
+ return cssAsset.parseTree.nodes.every(
617
+ (node) => node.type === 'comment' && !node.text.startsWith('!')
618
+ );
619
+ }
620
+
621
+ function warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph) {
622
+ const missingGlyphsErrors = [];
623
+
624
+ for (const {
625
+ htmlOrSvgAsset,
626
+ fontUsages,
627
+ accumulatedFontFaceDeclarations,
628
+ } of htmlOrSvgAssetTextsWithProps) {
629
+ for (const fontUsage of fontUsages) {
630
+ if (fontUsage.subsets) {
631
+ const characterSet = fontkit.create(
632
+ Object.values(fontUsage.subsets)[0]
633
+ ).characterSet;
634
+
635
+ let missedAny = false;
636
+ for (const char of [...fontUsage.pageText]) {
637
+ // Turns out that browsers don't mind that these are missing:
638
+ if (char === '\t' || char === '\n') {
639
+ continue;
640
+ }
641
+
642
+ const codePoint = char.codePointAt(0);
643
+
644
+ const isMissing = !characterSet.includes(codePoint);
645
+
646
+ if (isMissing) {
647
+ let location;
648
+ const charIdx = htmlOrSvgAsset.text.indexOf(char);
649
+
650
+ if (charIdx === -1) {
651
+ location = `${htmlOrSvgAsset.urlOrDescription} (generated content)`;
652
+ } else {
653
+ const position = new LinesAndColumns(
654
+ htmlOrSvgAsset.text
655
+ ).locationForIndex(charIdx);
656
+ location = `${htmlOrSvgAsset.urlOrDescription}:${
657
+ position.line + 1
658
+ }:${position.column + 1}`;
659
+ }
660
+
661
+ missingGlyphsErrors.push({
662
+ codePoint,
663
+ char,
664
+ htmlOrSvgAsset,
665
+ fontUsage,
666
+ location,
667
+ });
668
+ missedAny = true;
669
+ }
670
+ }
671
+ if (missedAny) {
672
+ const fontFaces = accumulatedFontFaceDeclarations.filter((fontFace) =>
673
+ fontUsage.fontFamilies.has(fontFace['font-family'])
674
+ );
675
+ for (const fontFace of fontFaces) {
676
+ const cssFontFaceSrc = fontFace.relations[0];
677
+ const fontFaceDeclaration = cssFontFaceSrc.node;
678
+ if (
679
+ !fontFaceDeclaration.some((node) => node.prop === 'unicode-range')
680
+ ) {
681
+ fontFaceDeclaration.append({
682
+ prop: 'unicode-range',
683
+ value: unicodeRange(fontUsage.codepoints.original),
684
+ });
685
+ cssFontFaceSrc.from.markDirty();
686
+ }
687
+ }
688
+ }
689
+ }
690
+ }
691
+ }
692
+
693
+ if (missingGlyphsErrors.length) {
694
+ const errorLog = missingGlyphsErrors.map(
695
+ ({ char, fontUsage, location }) =>
696
+ `- \\u{${char.codePointAt(0).toString(16)}} (${char}) in font-family '${
697
+ fontUsage.props['font-family']
698
+ }' (${fontUsage.props['font-weight']}/${
699
+ fontUsage.props['font-style']
700
+ }) at ${location}`
701
+ );
702
+
703
+ const message = `Missing glyph fallback detected.
704
+ 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.
705
+ These glyphs are used on your site, but they don't exist in the font you applied to them:`;
706
+
707
+ assetGraph.info(new Error(`${message}\n${errorLog.join('\n')}`));
708
+ }
709
+ }
710
+
711
+ const standardVariationAxes = new Set(['WGHT', 'ITAL', 'SLNT', 'OPSZ']);
712
+
713
+ function warnAboutUnusedCustomVariationAxes(
714
+ htmlOrSvgAssetTextsWithProps,
715
+ assetGraph
585
716
  ) {
586
- if (!validFontDisplayValues.includes(fontDisplay)) {
587
- fontDisplay = undefined;
717
+ const seenAxisValuesByFontUrlAndAxisName = new Map();
718
+ const outOfBoundsAxesByFontUrl = new Map();
719
+
720
+ for (const { fontUsages } of htmlOrSvgAssetTextsWithProps) {
721
+ for (const {
722
+ fontUrl,
723
+ fontVariationSettings,
724
+ hasOutOfBoundsAnimationTimingFunction,
725
+ } of fontUsages) {
726
+ let seenAxes = seenAxisValuesByFontUrlAndAxisName.get(fontUrl);
727
+ if (!seenAxes) {
728
+ seenAxes = new Map();
729
+ seenAxisValuesByFontUrlAndAxisName.set(fontUrl, seenAxes);
730
+ }
731
+
732
+ for (const fontVariationSettingsValue of fontVariationSettings) {
733
+ for (const [axisName, axisValue] of parseFontVariationSettings(
734
+ fontVariationSettingsValue
735
+ )) {
736
+ const seenAxisValues = seenAxes.get(axisName);
737
+ if (seenAxisValues) {
738
+ seenAxisValues.push(axisValue);
739
+ } else {
740
+ seenAxes.set(axisName, [axisValue]);
741
+ }
742
+ if (hasOutOfBoundsAnimationTimingFunction) {
743
+ let outOfBoundsAxes = outOfBoundsAxesByFontUrl.get(fontUrl);
744
+ if (!outOfBoundsAxes) {
745
+ outOfBoundsAxes = new Set();
746
+ outOfBoundsAxesByFontUrl.set(fontUrl, outOfBoundsAxes);
747
+ }
748
+ outOfBoundsAxes.add(axisName);
749
+ }
750
+ }
751
+ }
752
+ }
588
753
  }
589
754
 
590
- const htmlOrSvgAssetTextsWithProps = [];
591
- const subsetUrl = urltools.ensureTrailingSlash(assetGraph.root + subsetPath);
755
+ const warnings = [];
756
+ for (const [
757
+ fontUrl,
758
+ seenAxisValuesByAxisName,
759
+ ] of seenAxisValuesByFontUrlAndAxisName.entries()) {
760
+ const outOfBoundsAxes = outOfBoundsAxesByFontUrl.get(fontUrl) || new Set();
761
+ let font;
762
+ try {
763
+ font = fontkit.create(assetGraph.findAssets({ url: fontUrl })[0].rawSrc);
764
+ } catch (err) {
765
+ // Don't break if we encounter an invalid font or one that's unsupported by fontkit
766
+ continue;
767
+ }
768
+ const unusedAxes = [];
769
+ const underutilizedAxes = [];
770
+ for (const { name, min, max, default: defaultValue } of Object.values(
771
+ font.variationAxes
772
+ )) {
773
+ const axisName = name.toUpperCase();
774
+ if (standardVariationAxes.has(axisName)) {
775
+ continue;
776
+ }
777
+ if (
778
+ seenAxisValuesByAxisName.has(axisName) &&
779
+ !outOfBoundsAxes.has(axisName)
780
+ ) {
781
+ const usedValues = [
782
+ defaultValue,
783
+ ...seenAxisValuesByAxisName.get(axisName),
784
+ ];
785
+ const minUsed = Math.min(...usedValues);
786
+ const maxUsed = Math.max(...usedValues);
787
+ if (minUsed > min || maxUsed < max) {
788
+ underutilizedAxes.push({
789
+ name: axisName,
790
+ minUsed,
791
+ maxUsed,
792
+ min,
793
+ max,
794
+ });
795
+ }
796
+ } else {
797
+ unusedAxes.push(axisName);
798
+ }
799
+ }
592
800
 
593
- await assetGraph.applySourceMaps({ type: 'Css' });
801
+ if (unusedAxes.length > 0 || underutilizedAxes.length > 0) {
802
+ let message = `${fontUrl}:\n`;
803
+ if (unusedAxes.length > 0) {
804
+ message += ` Unused axes: ${unusedAxes.join(', ')}\n`;
805
+ }
806
+ if (underutilizedAxes.length > 0) {
807
+ message += ` Underutilized axes:\n${underutilizedAxes
808
+ .map(
809
+ ({ name, min, max, minUsed, maxUsed }) =>
810
+ ` ${name}: ${minUsed}-${maxUsed} used (${min}-${max} available)`
811
+ )
812
+ .join('\n')}\n`;
813
+ }
814
+ warnings.push(message);
815
+ }
816
+ }
594
817
 
595
- await assetGraph.populate({
596
- followRelations: {
597
- $or: [
598
- {
599
- to: {
600
- url: googleFontsCssUrlRegex,
601
- },
602
- },
603
- {
604
- type: 'CssFontFaceSrc',
605
- from: {
606
- url: googleFontsCssUrlRegex,
607
- },
608
- },
609
- ],
610
- },
611
- });
818
+ if (warnings.length > 0) {
819
+ assetGraph.info(
820
+ new Error(`🪓 Unused custom variation axes detected in your variable fonts.
821
+ The below variable fonts contain custom axes that do not appear to be fully used on any of your pages.
822
+ This bloats your fonts and also the subset fonts that subfont creates.
823
+ Consider removing the unused axis ranges using a tool like Slice <https://slice-gui.netlify.app/>
824
+ ${warnings.join('\n')}`)
825
+ );
826
+ }
827
+ }
612
828
 
613
- // Collect texts by page
829
+ async function collectTextsByPage(
830
+ assetGraph,
831
+ htmlOrSvgAssets,
832
+ { text, console, dynamic = false } = {}
833
+ ) {
834
+ const htmlOrSvgAssetTextsWithProps = [];
614
835
 
615
836
  const memoizedGetCssRulesByProperty = memoizeSync(getCssRulesByProperty);
616
- const htmlOrSvgAssets = assetGraph.findAssets({
617
- $or: [
618
- {
619
- type: 'Html',
620
- isInline: false,
621
- },
622
- {
623
- type: 'Svg',
624
- },
625
- ],
626
- });
627
837
  const traversalRelationQuery = {
628
838
  $or: [
629
839
  {
@@ -638,12 +848,7 @@ async function subsetFonts(
638
848
  ],
639
849
  };
640
850
 
641
- // Keep track of the injected CSS assets that should eventually be minified
642
- // Minifying them along the way currently doesn't work because some of the
643
- // manipulation is sensitive to the exact text contents. We should fix that.
644
- const subsetFontsToBeMinified = new Set();
645
851
  const fontFaceDeclarationsByHtmlOrSvgAsset = new Map();
646
- const potentiallyOrphanedAssets = new Set();
647
852
 
648
853
  const headlessBrowser = dynamic && new HeadlessBrowser({ console });
649
854
  const globalTextByProps = [];
@@ -679,9 +884,8 @@ async function subsetFonts(
679
884
  node.walkDecls((declaration) => {
680
885
  const propName = declaration.prop.toLowerCase();
681
886
  if (propName === 'font-family') {
682
- fontFaceDeclaration[propName] = fontFamily.parse(
683
- declaration.value
684
- )[0];
887
+ fontFaceDeclaration[propName] =
888
+ cssFontParser.parseFontFamily(declaration.value)[0];
685
889
  } else {
686
890
  fontFaceDeclaration[propName] = declaration.value;
687
891
  }
@@ -768,7 +972,72 @@ async function subsetFonts(
768
972
  }
769
973
  }
770
974
  }
975
+ return { htmlOrSvgAssetTextsWithProps, fontFaceDeclarationsByHtmlOrSvgAsset };
976
+ }
771
977
 
978
+ async function subsetFonts(
979
+ assetGraph,
980
+ {
981
+ formats = ['woff2', 'woff'],
982
+ subsetPath = 'subfont/',
983
+ omitFallbacks = false,
984
+ inlineCss,
985
+ fontDisplay,
986
+ hrefType = 'rootRelative',
987
+ onlyInfo,
988
+ dynamic,
989
+ console = global.console,
990
+ text,
991
+ } = {}
992
+ ) {
993
+ if (!validFontDisplayValues.includes(fontDisplay)) {
994
+ fontDisplay = undefined;
995
+ }
996
+
997
+ const subsetUrl = urltools.ensureTrailingSlash(assetGraph.root + subsetPath);
998
+
999
+ await assetGraph.applySourceMaps({ type: 'Css' });
1000
+
1001
+ await assetGraph.populate({
1002
+ followRelations: {
1003
+ $or: [
1004
+ {
1005
+ to: {
1006
+ url: { $regex: googleFontsCssUrlRegex },
1007
+ },
1008
+ },
1009
+ {
1010
+ type: 'CssFontFaceSrc',
1011
+ from: {
1012
+ url: { $regex: googleFontsCssUrlRegex },
1013
+ },
1014
+ },
1015
+ ],
1016
+ },
1017
+ });
1018
+
1019
+ const htmlOrSvgAssets = assetGraph.findAssets({
1020
+ $or: [
1021
+ {
1022
+ type: 'Html',
1023
+ isInline: false,
1024
+ },
1025
+ {
1026
+ type: 'Svg',
1027
+ },
1028
+ ],
1029
+ });
1030
+
1031
+ // Collect texts by page
1032
+
1033
+ const { htmlOrSvgAssetTextsWithProps, fontFaceDeclarationsByHtmlOrSvgAsset } =
1034
+ await collectTextsByPage(assetGraph, htmlOrSvgAssets, {
1035
+ text,
1036
+ console,
1037
+ dynamic,
1038
+ });
1039
+
1040
+ const potentiallyOrphanedAssets = new Set();
772
1041
  if (omitFallbacks) {
773
1042
  for (const htmlOrSvgAsset of htmlOrSvgAssets) {
774
1043
  const accumulatedFontFaceDeclarations =
@@ -842,94 +1111,8 @@ async function subsetFonts(
842
1111
  formats
843
1112
  );
844
1113
 
845
- // Warn about missing glyphs
846
- const missingGlyphsErrors = [];
847
-
848
- for (const {
849
- htmlOrSvgAsset,
850
- fontUsages,
851
- accumulatedFontFaceDeclarations,
852
- } of htmlOrSvgAssetTextsWithProps) {
853
- for (const fontUsage of fontUsages) {
854
- if (fontUsage.subsets) {
855
- const characterSet = fontkit.create(
856
- Object.values(fontUsage.subsets)[0]
857
- ).characterSet;
858
-
859
- let missedAny = false;
860
- for (const char of [...fontUsage.pageText]) {
861
- // Turns out that browsers don't mind that these are missing:
862
- if (char === '\t' || char === '\n') {
863
- continue;
864
- }
865
-
866
- const codePoint = char.codePointAt(0);
867
-
868
- const isMissing = !characterSet.includes(codePoint);
869
-
870
- if (isMissing) {
871
- let location;
872
- const charIdx = htmlOrSvgAsset.text.indexOf(char);
873
-
874
- if (charIdx === -1) {
875
- location = `${htmlOrSvgAsset.urlOrDescription} (generated content)`;
876
- } else {
877
- const position = new LinesAndColumns(
878
- htmlOrSvgAsset.text
879
- ).locationForIndex(charIdx);
880
- location = `${htmlOrSvgAsset.urlOrDescription}:${
881
- position.line + 1
882
- }:${position.column + 1}`;
883
- }
884
-
885
- missingGlyphsErrors.push({
886
- codePoint,
887
- char,
888
- htmlOrSvgAsset,
889
- fontUsage,
890
- location,
891
- });
892
- missedAny = true;
893
- }
894
- }
895
- if (missedAny) {
896
- const fontFaces = accumulatedFontFaceDeclarations.filter((fontFace) =>
897
- fontUsage.fontFamilies.has(fontFace['font-family'])
898
- );
899
- for (const fontFace of fontFaces) {
900
- const cssFontFaceSrc = fontFace.relations[0];
901
- const fontFaceDeclaration = cssFontFaceSrc.node;
902
- if (
903
- !fontFaceDeclaration.some((node) => node.prop === 'unicode-range')
904
- ) {
905
- fontFaceDeclaration.append({
906
- prop: 'unicode-range',
907
- value: unicodeRange(fontUsage.codepoints.original),
908
- });
909
- cssFontFaceSrc.from.markDirty();
910
- }
911
- }
912
- }
913
- }
914
- }
915
- }
916
-
917
- if (missingGlyphsErrors.length) {
918
- const errorLog = missingGlyphsErrors.map(
919
- ({ char, fontUsage, location }) =>
920
- `- \\u{${char.codePointAt(0).toString(16)}} (${char}) in font-family '${
921
- fontUsage.props['font-family']
922
- }' (${fontUsage.props['font-weight']}/${
923
- fontUsage.props['font-style']
924
- }) at ${location}`
925
- );
926
-
927
- const message = `Missing glyph fallback detected.
928
- 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.
929
- These glyphs are used on your site, but they don't exist in the font you applied to them:`;
930
-
931
- assetGraph.info(new Error(`${message}\n${errorLog.join('\n')}`));
932
- }
1114
+ warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
1115
+ warnAboutUnusedCustomVariationAxes(htmlOrSvgAssetTextsWithProps, assetGraph);
933
1116
 
934
1117
  // Insert subsets:
935
1118
 
@@ -1015,7 +1198,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1015
1198
  text: subsetCssText,
1016
1199
  });
1017
1200
 
1018
- subsetFontsToBeMinified.add(cssAsset);
1201
+ await cssAsset.minify();
1019
1202
 
1020
1203
  for (const [i, fontRelation] of cssAsset.outgoingRelations.entries()) {
1021
1204
  const fontAsset = fontRelation.to;
@@ -1085,7 +1268,6 @@ These glyphs are used on your site, but they don't exist in the font you applied
1085
1268
  const existingCssAsset = assetGraph.findAssets({ url: cssAssetUrl })[0];
1086
1269
  if (existingCssAsset) {
1087
1270
  assetGraph.removeAsset(cssAsset);
1088
- subsetFontsToBeMinified.delete(cssAsset);
1089
1271
  cssAsset = existingCssAsset;
1090
1272
  } else {
1091
1273
  cssAsset.url = cssAssetUrl;
@@ -1099,7 +1281,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1099
1281
 
1100
1282
  if (
1101
1283
  fontAsset.contentType === 'font/woff2' &&
1102
- fontRelation.to.path.startsWith('/subfont/')
1284
+ fontRelation.to.url.startsWith(subsetUrl)
1103
1285
  ) {
1104
1286
  const fontFaceDeclaration = fontRelation.node;
1105
1287
  const originalFontFamily = unquote(
@@ -1220,7 +1402,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1220
1402
  assetGraph.removeAsset(cssAsset);
1221
1403
  cssAsset = existingCssAsset;
1222
1404
  } else {
1223
- subsetFontsToBeMinified.add(cssAsset);
1405
+ await cssAsset.minify();
1224
1406
  cssAsset.url = cssAssetUrl;
1225
1407
  }
1226
1408
 
@@ -1252,8 +1434,9 @@ These glyphs are used on your site, but they don't exist in the font you applied
1252
1434
  cssAsset.markDirty();
1253
1435
  maybeEmptyCssAssets.add(cssAsset);
1254
1436
  }
1437
+
1255
1438
  for (const cssAsset of maybeEmptyCssAssets) {
1256
- if (cssAsset.isEmpty) {
1439
+ if (cssAssetIsEmpty(cssAsset)) {
1257
1440
  for (const incomingRelation of cssAsset.incomingRelations) {
1258
1441
  incomingRelation.detach();
1259
1442
  }
@@ -1302,9 +1485,10 @@ These glyphs are used on your site, but they don't exist in the font you applied
1302
1485
  assetGraph,
1303
1486
  googleFontStylesheetRelation.to,
1304
1487
  formats,
1305
- hrefType
1488
+ hrefType,
1489
+ subsetUrl
1306
1490
  );
1307
- subsetFontsToBeMinified.add(selfHostedGoogleFontsCssAsset);
1491
+ await selfHostedGoogleFontsCssAsset.minify();
1308
1492
  selfHostedGoogleCssByUrl.set(
1309
1493
  googleFontStylesheetRelation.to.url,
1310
1494
  selfHostedGoogleFontsCssAsset
@@ -1367,7 +1551,9 @@ These glyphs are used on your site, but they don't exist in the font you applied
1367
1551
  );
1368
1552
  for (let i = 0; i < fontFamilies.length; i += 1) {
1369
1553
  const subsetFontFamily =
1370
- webfontNameMap[fontFamily.parse(fontFamilies[i])[0].toLowerCase()];
1554
+ webfontNameMap[
1555
+ cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1556
+ ];
1371
1557
  if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1372
1558
  fontFamilies.splice(
1373
1559
  i,
@@ -1427,7 +1613,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1427
1613
  for (let i = 0; i < fontFamilies.length; i += 1) {
1428
1614
  const subsetFontFamily =
1429
1615
  webfontNameMap[
1430
- fontFamily.parse(fontFamilies[i])[0].toLowerCase()
1616
+ cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1431
1617
  ];
1432
1618
  if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1433
1619
  fontFamilies.splice(
@@ -1441,7 +1627,7 @@ These glyphs are used on your site, but they don't exist in the font you applied
1441
1627
  }
1442
1628
  }
1443
1629
  } else if (propName === 'font') {
1444
- const fontProperties = cssFontParser(cssRule.value);
1630
+ const fontProperties = cssFontParser.parseFont(cssRule.value);
1445
1631
  const fontFamilies =
1446
1632
  fontProperties && fontProperties['font-family'].map(unquote);
1447
1633
  if (fontFamilies) {
@@ -1485,11 +1671,6 @@ These glyphs are used on your site, but they don't exist in the font you applied
1485
1671
  }
1486
1672
  }
1487
1673
 
1488
- // This is a bit awkward now, but if it's done sooner, it breaks the CSS source regexping:
1489
- for (const cssAsset of subsetFontsToBeMinified) {
1490
- await cssAsset.minify();
1491
- }
1492
-
1493
1674
  await assetGraph.serializeSourceMaps(undefined, {
1494
1675
  type: 'Css',
1495
1676
  outgoingRelations: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subfont",
3
- "version": "6.7.0",
3
+ "version": "6.10.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
- "assetgraph": "^7.2.0",
50
+ "@hookun/parse-animation-shorthand": "^0.1.4",
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",