subfont 6.9.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,12 @@
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
+
1
10
  ### v6.9.0 (2022-08-07)
2
11
 
3
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))
@@ -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;
@@ -664,6 +708,124 @@ These glyphs are used on your site, but they don't exist in the font you applied
664
708
  }
665
709
  }
666
710
 
711
+ const standardVariationAxes = new Set(['WGHT', 'ITAL', 'SLNT', 'OPSZ']);
712
+
713
+ function warnAboutUnusedCustomVariationAxes(
714
+ htmlOrSvgAssetTextsWithProps,
715
+ assetGraph
716
+ ) {
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
+ }
753
+ }
754
+
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
+ }
800
+
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
+ }
817
+
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
+ }
828
+
667
829
  async function collectTextsByPage(
668
830
  assetGraph,
669
831
  htmlOrSvgAssets,
@@ -722,9 +884,8 @@ async function collectTextsByPage(
722
884
  node.walkDecls((declaration) => {
723
885
  const propName = declaration.prop.toLowerCase();
724
886
  if (propName === 'font-family') {
725
- fontFaceDeclaration[propName] = fontFamily.parse(
726
- declaration.value
727
- )[0];
887
+ fontFaceDeclaration[propName] =
888
+ cssFontParser.parseFontFamily(declaration.value)[0];
728
889
  } else {
729
890
  fontFaceDeclaration[propName] = declaration.value;
730
891
  }
@@ -951,6 +1112,7 @@ async function subsetFonts(
951
1112
  );
952
1113
 
953
1114
  warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
1115
+ warnAboutUnusedCustomVariationAxes(htmlOrSvgAssetTextsWithProps, assetGraph);
954
1116
 
955
1117
  // Insert subsets:
956
1118
 
@@ -1119,7 +1281,7 @@ async function subsetFonts(
1119
1281
 
1120
1282
  if (
1121
1283
  fontAsset.contentType === 'font/woff2' &&
1122
- fontRelation.to.path.startsWith('/subfont/')
1284
+ fontRelation.to.url.startsWith(subsetUrl)
1123
1285
  ) {
1124
1286
  const fontFaceDeclaration = fontRelation.node;
1125
1287
  const originalFontFamily = unquote(
@@ -1323,7 +1485,8 @@ async function subsetFonts(
1323
1485
  assetGraph,
1324
1486
  googleFontStylesheetRelation.to,
1325
1487
  formats,
1326
- hrefType
1488
+ hrefType,
1489
+ subsetUrl
1327
1490
  );
1328
1491
  await selfHostedGoogleFontsCssAsset.minify();
1329
1492
  selfHostedGoogleCssByUrl.set(
@@ -1388,7 +1551,9 @@ async function subsetFonts(
1388
1551
  );
1389
1552
  for (let i = 0; i < fontFamilies.length; i += 1) {
1390
1553
  const subsetFontFamily =
1391
- webfontNameMap[fontFamily.parse(fontFamilies[i])[0].toLowerCase()];
1554
+ webfontNameMap[
1555
+ cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1556
+ ];
1392
1557
  if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1393
1558
  fontFamilies.splice(
1394
1559
  i,
@@ -1448,7 +1613,7 @@ async function subsetFonts(
1448
1613
  for (let i = 0; i < fontFamilies.length; i += 1) {
1449
1614
  const subsetFontFamily =
1450
1615
  webfontNameMap[
1451
- fontFamily.parse(fontFamilies[i])[0].toLowerCase()
1616
+ cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1452
1617
  ];
1453
1618
  if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1454
1619
  fontFamilies.splice(
@@ -1462,7 +1627,7 @@ async function subsetFonts(
1462
1627
  }
1463
1628
  }
1464
1629
  } else if (propName === 'font') {
1465
- const fontProperties = cssFontParser(cssRule.value);
1630
+ const fontProperties = cssFontParser.parseFont(cssRule.value);
1466
1631
  const fontFamilies =
1467
1632
  fontProperties && fontProperties['font-family'].map(unquote);
1468
1633
  if (fontFamilies) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subfont",
3
- "version": "6.9.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
+ "@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",