fontaine 0.3.1 → 0.4.1

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/README.md CHANGED
@@ -54,6 +54,7 @@ const options = {
54
54
  resolvePath: (id) => 'file:///path/to/public/dir' + id,
55
55
  // overrideName: (originalName) => `${name} override`
56
56
  // sourcemap: false
57
+ // skipFontFaceGeneration: (fallbackName) => fallbackName === 'Roboto override'
57
58
  }
58
59
 
59
60
  // Vite
package/dist/index.cjs CHANGED
@@ -9,6 +9,10 @@ const metrics = require('@capsizecss/metrics');
9
9
  const ufo = require('ufo');
10
10
  const pathe = require('pathe');
11
11
 
12
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
13
+
14
+ const MagicString__default = /*#__PURE__*/_interopDefaultCompat(MagicString);
15
+
12
16
  function* parseFontFace(css) {
13
17
  const fontFamily = css.match(FAMILY_RE)?.groups.fontFamily;
14
18
  const family = withoutQuotes(fontFamily?.split(",")[0] || "");
@@ -30,22 +34,27 @@ const generateFallbackName = (name) => {
30
34
  return `${firstFamily} fallback`;
31
35
  };
32
36
  const withoutQuotes = (str) => str.trim().replace(QUOTES_RE, "");
33
- const generateFontFace = (metrics, options) => {
34
- const { name, fallbacks, ...properties } = options;
35
- const sizeAdjust = 1;
37
+ const generateFontFace = (metrics, fallback) => {
38
+ const {
39
+ name: fallbackName,
40
+ font: fallbackFontName,
41
+ metrics: fallbackMetrics,
42
+ ...properties
43
+ } = fallback;
44
+ const preferredFontXAvgRatio = metrics.xWidthAvg / metrics.unitsPerEm;
45
+ const fallbackFontXAvgRatio = fallbackMetrics ? fallbackMetrics.xWidthAvg / fallbackMetrics.unitsPerEm : 1;
46
+ const sizeAdjust = fallbackMetrics && preferredFontXAvgRatio && fallbackFontXAvgRatio ? preferredFontXAvgRatio / fallbackFontXAvgRatio : 1;
47
+ const adjustedEmSquare = metrics.unitsPerEm * sizeAdjust;
48
+ const ascentOverride = metrics.ascent / adjustedEmSquare;
49
+ const descentOverride = Math.abs(metrics.descent) / adjustedEmSquare;
50
+ const lineGapOverride = metrics.lineGap / adjustedEmSquare;
36
51
  const declaration = {
37
- "font-family": JSON.stringify(name),
38
- src: fallbacks.map((f) => `local(${JSON.stringify(f)})`),
39
- // 'size-adjust': toPercentage(sizeAdjust),
40
- "ascent-override": toPercentage(
41
- metrics.ascent / (metrics.unitsPerEm * sizeAdjust)
42
- ),
43
- "descent-override": toPercentage(
44
- Math.abs(metrics.descent / (metrics.unitsPerEm * sizeAdjust))
45
- ),
46
- "line-gap-override": toPercentage(
47
- metrics.lineGap / (metrics.unitsPerEm * sizeAdjust)
48
- ),
52
+ "font-family": JSON.stringify(fallbackName),
53
+ src: `local(${JSON.stringify(fallbackFontName)})`,
54
+ "size-adjust": toPercentage(sizeAdjust),
55
+ "ascent-override": toPercentage(ascentOverride),
56
+ "descent-override": toPercentage(descentOverride),
57
+ "line-gap-override": toPercentage(lineGapOverride),
49
58
  ...properties
50
59
  };
51
60
  return `@font-face {
@@ -53,9 +62,9 @@ ${toCSS(declaration)}
53
62
  }
54
63
  `;
55
64
  };
56
- const toPercentage = (value, fractionDigits = 8) => {
65
+ const toPercentage = (value, fractionDigits = 4) => {
57
66
  const percentage = value * 100;
58
- return (percentage % 1 ? percentage.toFixed(fractionDigits).replace(/0+$/, "") : percentage) + "%";
67
+ return +percentage.toFixed(fractionDigits) + "%";
59
68
  };
60
69
  const toCSS = (properties, indent = 2) => Object.entries(properties).map(([key, value]) => " ".repeat(indent) + `${key}: ${value};`).join("\n");
61
70
  const QUOTES_RE = magicRegexp.createRegExp(
@@ -79,12 +88,14 @@ const filterRequiredMetrics = ({
79
88
  ascent,
80
89
  descent,
81
90
  lineGap,
82
- unitsPerEm
91
+ unitsPerEm,
92
+ xWidthAvg
83
93
  }) => ({
84
94
  ascent,
85
95
  descent,
86
96
  lineGap,
87
- unitsPerEm
97
+ unitsPerEm,
98
+ xWidthAvg
88
99
  });
89
100
  async function getMetricsForFamily(family) {
90
101
  family = withoutQuotes(family);
@@ -123,6 +134,7 @@ const FontaineTransform = unplugin.createUnplugin(
123
134
  cssContext.value = "";
124
135
  const resolvePath = options.resolvePath || ((id) => id);
125
136
  const fallbackName = options.fallbackName || options.overrideName || generateFallbackName;
137
+ const skipFontFaceGeneration = options.skipFontFaceGeneration || (() => false);
126
138
  function readMetricsFromId(path, importer) {
127
139
  const resolvedPath = pathe.isAbsolute(importer) && path.startsWith(".") ? pathe.join(importer, path) : resolvePath(path);
128
140
  return readMetrics(resolvedPath);
@@ -135,7 +147,7 @@ const FontaineTransform = unplugin.createUnplugin(
135
147
  return CSS_RE.test(pathname) || CSS_RE.test(id);
136
148
  },
137
149
  async transform(code, id) {
138
- const s = new MagicString(code);
150
+ const s = new MagicString__default(code);
139
151
  const faceRanges = [];
140
152
  for (const match of code.matchAll(FONT_FACE_RE)) {
141
153
  const matchContent = match[0];
@@ -147,11 +159,22 @@ const FontaineTransform = unplugin.createUnplugin(
147
159
  continue;
148
160
  if (!supportedExtensions.some((e) => source?.endsWith(e)))
149
161
  continue;
162
+ if (skipFontFaceGeneration(fallbackName(family)))
163
+ continue;
150
164
  const metrics = await getMetricsForFamily(family) || source && await readMetricsFromId(source, id).catch(() => null);
151
- if (metrics) {
165
+ if (!metrics) {
166
+ continue;
167
+ }
168
+ for (let i = options.fallbacks.length - 1; i >= 0; i--) {
169
+ const fallback = options.fallbacks[i];
170
+ const fallbackMetrics = await getMetricsForFamily(fallback);
171
+ if (!fallbackMetrics) {
172
+ continue;
173
+ }
152
174
  const fontFace = generateFontFace(metrics, {
153
175
  name: fallbackName(family),
154
- fallbacks: options.fallbacks
176
+ font: fallback,
177
+ metrics: fallbackMetrics
155
178
  });
156
179
  cssContext.value += fontFace;
157
180
  s.appendLeft(match.index, fontFace);
@@ -0,0 +1,32 @@
1
+ import * as unplugin from 'unplugin';
2
+ import { Font } from '@capsizecss/unpack';
3
+
4
+ interface FontaineTransformOptions {
5
+ css?: {
6
+ value?: string;
7
+ };
8
+ fallbacks: string[];
9
+ resolvePath?: (path: string) => string | URL;
10
+ skipFontFaceGeneration?: (fallbackName: string) => boolean;
11
+ /** this should produce an unquoted font family name */
12
+ fallbackName?: (name: string) => string;
13
+ /** @deprecated use fallbackName */
14
+ overrideName?: (name: string) => string;
15
+ sourcemap?: boolean;
16
+ }
17
+ declare const FontaineTransform: unplugin.UnpluginInstance<FontaineTransformOptions, boolean>;
18
+
19
+ declare const generateFallbackName: (name: string) => string;
20
+ interface FallbackOptions {
21
+ name: string;
22
+ font: string;
23
+ metrics?: FontFaceMetrics;
24
+ [key: string]: any;
25
+ }
26
+ type FontFaceMetrics = Pick<Font, 'ascent' | 'descent' | 'lineGap' | 'unitsPerEm' | 'xWidthAvg'>;
27
+ declare const generateFontFace: (metrics: FontFaceMetrics, fallback: FallbackOptions) => string;
28
+
29
+ declare function getMetricsForFamily(family: string): Promise<FontFaceMetrics | null>;
30
+ declare function readMetrics(_source: URL | string): Promise<FontFaceMetrics | null>;
31
+
32
+ export { FontaineTransform, type FontaineTransformOptions, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics };
@@ -0,0 +1,32 @@
1
+ import * as unplugin from 'unplugin';
2
+ import { Font } from '@capsizecss/unpack';
3
+
4
+ interface FontaineTransformOptions {
5
+ css?: {
6
+ value?: string;
7
+ };
8
+ fallbacks: string[];
9
+ resolvePath?: (path: string) => string | URL;
10
+ skipFontFaceGeneration?: (fallbackName: string) => boolean;
11
+ /** this should produce an unquoted font family name */
12
+ fallbackName?: (name: string) => string;
13
+ /** @deprecated use fallbackName */
14
+ overrideName?: (name: string) => string;
15
+ sourcemap?: boolean;
16
+ }
17
+ declare const FontaineTransform: unplugin.UnpluginInstance<FontaineTransformOptions, boolean>;
18
+
19
+ declare const generateFallbackName: (name: string) => string;
20
+ interface FallbackOptions {
21
+ name: string;
22
+ font: string;
23
+ metrics?: FontFaceMetrics;
24
+ [key: string]: any;
25
+ }
26
+ type FontFaceMetrics = Pick<Font, 'ascent' | 'descent' | 'lineGap' | 'unitsPerEm' | 'xWidthAvg'>;
27
+ declare const generateFontFace: (metrics: FontFaceMetrics, fallback: FallbackOptions) => string;
28
+
29
+ declare function getMetricsForFamily(family: string): Promise<FontFaceMetrics | null>;
30
+ declare function readMetrics(_source: URL | string): Promise<FontFaceMetrics | null>;
31
+
32
+ export { FontaineTransform, type FontaineTransformOptions, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ interface FontaineTransformOptions {
7
7
  };
8
8
  fallbacks: string[];
9
9
  resolvePath?: (path: string) => string | URL;
10
+ skipFontFaceGeneration?: (fallbackName: string) => boolean;
10
11
  /** this should produce an unquoted font family name */
11
12
  fallbackName?: (name: string) => string;
12
13
  /** @deprecated use fallbackName */
@@ -16,15 +17,16 @@ interface FontaineTransformOptions {
16
17
  declare const FontaineTransform: unplugin.UnpluginInstance<FontaineTransformOptions, boolean>;
17
18
 
18
19
  declare const generateFallbackName: (name: string) => string;
19
- interface GenerateOptions {
20
+ interface FallbackOptions {
20
21
  name: string;
21
- fallbacks: string[];
22
+ font: string;
23
+ metrics?: FontFaceMetrics;
22
24
  [key: string]: any;
23
25
  }
24
- type FontFaceMetrics = Pick<Font, 'ascent' | 'descent' | 'lineGap' | 'unitsPerEm'>;
25
- declare const generateFontFace: (metrics: FontFaceMetrics, options: GenerateOptions) => string;
26
+ type FontFaceMetrics = Pick<Font, 'ascent' | 'descent' | 'lineGap' | 'unitsPerEm' | 'xWidthAvg'>;
27
+ declare const generateFontFace: (metrics: FontFaceMetrics, fallback: FallbackOptions) => string;
26
28
 
27
29
  declare function getMetricsForFamily(family: string): Promise<FontFaceMetrics | null>;
28
30
  declare function readMetrics(_source: URL | string): Promise<FontFaceMetrics | null>;
29
31
 
30
- export { FontaineTransform, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics };
32
+ export { FontaineTransform, type FontaineTransformOptions, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics };
package/dist/index.mjs CHANGED
@@ -28,22 +28,27 @@ const generateFallbackName = (name) => {
28
28
  return `${firstFamily} fallback`;
29
29
  };
30
30
  const withoutQuotes = (str) => str.trim().replace(QUOTES_RE, "");
31
- const generateFontFace = (metrics, options) => {
32
- const { name, fallbacks, ...properties } = options;
33
- const sizeAdjust = 1;
31
+ const generateFontFace = (metrics, fallback) => {
32
+ const {
33
+ name: fallbackName,
34
+ font: fallbackFontName,
35
+ metrics: fallbackMetrics,
36
+ ...properties
37
+ } = fallback;
38
+ const preferredFontXAvgRatio = metrics.xWidthAvg / metrics.unitsPerEm;
39
+ const fallbackFontXAvgRatio = fallbackMetrics ? fallbackMetrics.xWidthAvg / fallbackMetrics.unitsPerEm : 1;
40
+ const sizeAdjust = fallbackMetrics && preferredFontXAvgRatio && fallbackFontXAvgRatio ? preferredFontXAvgRatio / fallbackFontXAvgRatio : 1;
41
+ const adjustedEmSquare = metrics.unitsPerEm * sizeAdjust;
42
+ const ascentOverride = metrics.ascent / adjustedEmSquare;
43
+ const descentOverride = Math.abs(metrics.descent) / adjustedEmSquare;
44
+ const lineGapOverride = metrics.lineGap / adjustedEmSquare;
34
45
  const declaration = {
35
- "font-family": JSON.stringify(name),
36
- src: fallbacks.map((f) => `local(${JSON.stringify(f)})`),
37
- // 'size-adjust': toPercentage(sizeAdjust),
38
- "ascent-override": toPercentage(
39
- metrics.ascent / (metrics.unitsPerEm * sizeAdjust)
40
- ),
41
- "descent-override": toPercentage(
42
- Math.abs(metrics.descent / (metrics.unitsPerEm * sizeAdjust))
43
- ),
44
- "line-gap-override": toPercentage(
45
- metrics.lineGap / (metrics.unitsPerEm * sizeAdjust)
46
- ),
46
+ "font-family": JSON.stringify(fallbackName),
47
+ src: `local(${JSON.stringify(fallbackFontName)})`,
48
+ "size-adjust": toPercentage(sizeAdjust),
49
+ "ascent-override": toPercentage(ascentOverride),
50
+ "descent-override": toPercentage(descentOverride),
51
+ "line-gap-override": toPercentage(lineGapOverride),
47
52
  ...properties
48
53
  };
49
54
  return `@font-face {
@@ -51,9 +56,9 @@ ${toCSS(declaration)}
51
56
  }
52
57
  `;
53
58
  };
54
- const toPercentage = (value, fractionDigits = 8) => {
59
+ const toPercentage = (value, fractionDigits = 4) => {
55
60
  const percentage = value * 100;
56
- return (percentage % 1 ? percentage.toFixed(fractionDigits).replace(/0+$/, "") : percentage) + "%";
61
+ return +percentage.toFixed(fractionDigits) + "%";
57
62
  };
58
63
  const toCSS = (properties, indent = 2) => Object.entries(properties).map(([key, value]) => " ".repeat(indent) + `${key}: ${value};`).join("\n");
59
64
  const QUOTES_RE = createRegExp(
@@ -77,12 +82,14 @@ const filterRequiredMetrics = ({
77
82
  ascent,
78
83
  descent,
79
84
  lineGap,
80
- unitsPerEm
85
+ unitsPerEm,
86
+ xWidthAvg
81
87
  }) => ({
82
88
  ascent,
83
89
  descent,
84
90
  lineGap,
85
- unitsPerEm
91
+ unitsPerEm,
92
+ xWidthAvg
86
93
  });
87
94
  async function getMetricsForFamily(family) {
88
95
  family = withoutQuotes(family);
@@ -121,6 +128,7 @@ const FontaineTransform = createUnplugin(
121
128
  cssContext.value = "";
122
129
  const resolvePath = options.resolvePath || ((id) => id);
123
130
  const fallbackName = options.fallbackName || options.overrideName || generateFallbackName;
131
+ const skipFontFaceGeneration = options.skipFontFaceGeneration || (() => false);
124
132
  function readMetricsFromId(path, importer) {
125
133
  const resolvedPath = isAbsolute(importer) && path.startsWith(".") ? join(importer, path) : resolvePath(path);
126
134
  return readMetrics(resolvedPath);
@@ -145,11 +153,22 @@ const FontaineTransform = createUnplugin(
145
153
  continue;
146
154
  if (!supportedExtensions.some((e) => source?.endsWith(e)))
147
155
  continue;
156
+ if (skipFontFaceGeneration(fallbackName(family)))
157
+ continue;
148
158
  const metrics = await getMetricsForFamily(family) || source && await readMetricsFromId(source, id).catch(() => null);
149
- if (metrics) {
159
+ if (!metrics) {
160
+ continue;
161
+ }
162
+ for (let i = options.fallbacks.length - 1; i >= 0; i--) {
163
+ const fallback = options.fallbacks[i];
164
+ const fallbackMetrics = await getMetricsForFamily(fallback);
165
+ if (!fallbackMetrics) {
166
+ continue;
167
+ }
150
168
  const fontFace = generateFontFace(metrics, {
151
169
  name: fallbackName(family),
152
- fallbacks: options.fallbacks
170
+ font: fallback,
171
+ metrics: fallbackMetrics
153
172
  });
154
173
  cssContext.value += fontFace;
155
174
  s.appendLeft(match.index, fontFace);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontaine",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Automatic font fallback based on font metrics",
5
5
  "repository": "unjs/fontaine",
6
6
  "keywords": [
@@ -39,16 +39,14 @@
39
39
  "lint:eslint": "eslint --fix",
40
40
  "lint:prettier": "prettier --write --loglevel warn",
41
41
  "prepare": "husky install && pnpm build",
42
- "prepublishOnly": "pnpm lint && pnpm test && pinst --disable",
42
+ "prepublishOnly": "pnpm lint && pnpm test",
43
43
  "release": "pnpm test && bumpp && npm publish",
44
- "test": "vitest run",
45
- "_postinstall": "husky install",
46
- "postpublish": "pinst --enable"
44
+ "test": "vitest run"
47
45
  },
48
46
  "dependencies": {
49
47
  "@capsizecss/metrics": "^1.1.1",
50
48
  "@capsizecss/unpack": "^1.0.0",
51
- "magic-regexp": "^0.6.3",
49
+ "magic-regexp": "^0.7.0",
52
50
  "magic-string": "^0.30.0",
53
51
  "pathe": "^1.1.0",
54
52
  "ufo": "^1.1.1",
@@ -56,29 +54,28 @@
56
54
  },
57
55
  "devDependencies": {
58
56
  "@nuxtjs/eslint-config-typescript": "latest",
59
- "@types/node": "18.15.3",
57
+ "@types/node": "18.17.15",
60
58
  "@types/serve-handler": "6.1.1",
61
- "@typescript-eslint/eslint-plugin": "5.55.0",
62
- "@typescript-eslint/parser": "5.55.0",
63
- "@vitest/coverage-c8": "0.29.3",
64
- "bumpp": "9.0.0",
65
- "eslint": "8.36.0",
66
- "eslint-config-prettier": "^8.7.0",
59
+ "@typescript-eslint/eslint-plugin": "6.4.1",
60
+ "@typescript-eslint/parser": "6.4.1",
61
+ "@vitest/coverage-v8": "0.34.2",
62
+ "bumpp": "9.1.1",
63
+ "eslint": "8.48.0",
64
+ "eslint-config-prettier": "9.0.0",
67
65
  "eslint-plugin-prettier": "latest",
68
- "execa": "7.1.1",
69
- "get-port-please": "3.0.1",
66
+ "execa": "8.0.1",
67
+ "get-port-please": "3.1.1",
70
68
  "husky": "latest",
71
- "lint-staged": "^13.2.0",
72
- "pinst": "latest",
69
+ "lint-staged": "14.0.1",
73
70
  "prettier": "latest",
74
71
  "serve-handler": "6.1.5",
75
- "typescript": "^5.0.2",
72
+ "typescript": "5.1.6",
76
73
  "unbuild": "latest",
77
- "vite": "4.2.0",
78
- "vitest": "0.29.3"
74
+ "vite": "4.4.9",
75
+ "vitest": "0.34.2"
79
76
  },
80
77
  "resolutions": {
81
78
  "fontaine": "link:."
82
79
  },
83
- "packageManager": "pnpm@7.29.1"
80
+ "packageManager": "pnpm@8.6.12"
84
81
  }