fontaine 0.2.1 → 0.3.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/README.md CHANGED
@@ -95,6 +95,22 @@ exports.onCreateWebpackConfig = ({ stage, actions, getConfig }) => {
95
95
  config.plugins.push(FontaineTransform.webpack(options))
96
96
  actions.replaceWebpackConfig(config)
97
97
  }
98
+
99
+ // Astro config - astro.config.mjs
100
+ import { defineConfig } from 'astro/config'
101
+ import { FontaineTransform } from 'fontaine'
102
+
103
+ export default defineConfig({
104
+ integrations: [],
105
+ vite: {
106
+ plugins: [
107
+ FontaineTransform.vite({
108
+ fallbacks: ['Arial'],
109
+ resolvePath: (id) => new URL(`./public${id}`, import.meta.url), // id is the font src value in the CSS
110
+ }),
111
+ ],
112
+ },
113
+ })
98
114
  ```
99
115
 
100
116
  > **Note**
package/dist/index.cjs CHANGED
@@ -5,22 +5,29 @@ const magicRegexp = require('magic-regexp');
5
5
  const MagicString = require('magic-string');
6
6
  const node_url = require('node:url');
7
7
  const unpack = require('@capsizecss/unpack');
8
+ const metrics = require('@capsizecss/metrics');
8
9
  const ufo = require('ufo');
9
- const scule = require('scule');
10
10
  const pathe = require('pathe');
11
11
 
12
- const parseFontFace = (css) => {
12
+ function* parseFontFace(css) {
13
13
  const fontFamily = css.match(FAMILY_RE)?.groups.fontFamily;
14
- const src = css.match(SOURCE_RE)?.groups.src;
15
14
  const family = withoutQuotes(fontFamily?.split(",")[0] || "");
16
- const source = withoutQuotes(
17
- src?.split(",").map((source2) => source2.match(URL_RE)?.groups.url).filter(Boolean)[0] || ""
18
- );
19
- return { family, source };
20
- };
21
- const generateOverrideName = (name) => {
15
+ for (const match of css.matchAll(SOURCE_RE)) {
16
+ const sources = match.groups.src?.split(",");
17
+ for (const entry of sources || []) {
18
+ for (const url of entry.matchAll(URL_RE)) {
19
+ const source = withoutQuotes(url.groups?.url || "");
20
+ if (source) {
21
+ yield { family, source };
22
+ }
23
+ }
24
+ }
25
+ }
26
+ yield { family: "", source: "" };
27
+ }
28
+ const generateFallbackName = (name) => {
22
29
  const firstFamily = withoutQuotes(name.split(",").shift());
23
- return `${firstFamily} override`;
30
+ return `${firstFamily} fallback`;
24
31
  };
25
32
  const withoutQuotes = (str) => str.trim().replace(QUOTES_RE, "");
26
33
  const generateFontFace = (metrics, options) => {
@@ -29,6 +36,7 @@ const generateFontFace = (metrics, options) => {
29
36
  const declaration = {
30
37
  "font-family": JSON.stringify(name),
31
38
  src: fallbacks.map((f) => `local(${JSON.stringify(f)})`),
39
+ // 'size-adjust': toPercentage(sizeAdjust),
32
40
  "ascent-override": toPercentage(
33
41
  metrics.ascent / (metrics.unitsPerEm * sizeAdjust)
34
42
  ),
@@ -58,24 +66,37 @@ const FAMILY_RE = magicRegexp.createRegExp(
58
66
  magicRegexp.exactly("font-family:").and(magicRegexp.whitespace.optionally()).and(magicRegexp.charNotIn(";}").times.any().as("fontFamily"))
59
67
  );
60
68
  const SOURCE_RE = magicRegexp.createRegExp(
61
- magicRegexp.exactly("src:").and(magicRegexp.whitespace.optionally()).and(magicRegexp.charNotIn(";}").times.any().as("src"))
69
+ magicRegexp.exactly("src:").and(magicRegexp.whitespace.optionally()).and(magicRegexp.charNotIn(";}").times.any().as("src")),
70
+ ["g"]
62
71
  );
63
72
  const URL_RE = magicRegexp.createRegExp(
64
- magicRegexp.exactly("url(").and(magicRegexp.charNotIn(")").times.any().as("url")).and(")")
73
+ magicRegexp.exactly("url(").and(magicRegexp.charNotIn(")").times.any().as("url")).and(")"),
74
+ ["g"]
65
75
  );
66
76
 
67
77
  const metricCache = {};
78
+ const filterRequiredMetrics = ({
79
+ ascent,
80
+ descent,
81
+ lineGap,
82
+ unitsPerEm
83
+ }) => ({
84
+ ascent,
85
+ descent,
86
+ lineGap,
87
+ unitsPerEm
88
+ });
68
89
  async function getMetricsForFamily(family) {
69
90
  family = withoutQuotes(family);
70
91
  if (family in metricCache)
71
92
  return metricCache[family];
72
93
  try {
73
- const name = scule.camelCase(family).replace(/ /, "");
74
- const metrics = await import(`@capsizecss/metrics/${name}.js`).then(
75
- (r) => r.default || r
76
- );
77
- metricCache[family] = metrics;
78
- return metrics;
94
+ const name = metrics.fontFamilyToCamelCase(family);
95
+ const { entireMetricsCollection } = await import('@capsizecss/metrics/entireMetricsCollection');
96
+ const metrics$1 = entireMetricsCollection[name];
97
+ const filteredMetrics = filterRequiredMetrics(metrics$1);
98
+ metricCache[family] = filteredMetrics;
99
+ return filteredMetrics;
79
100
  } catch {
80
101
  metricCache[family] = null;
81
102
  return null;
@@ -90,16 +111,18 @@ async function readMetrics(_source) {
90
111
  if (!protocol)
91
112
  return null;
92
113
  const metrics = protocol === "file:" ? await unpack.fromFile(node_url.fileURLToPath(source)) : await unpack.fromUrl(source);
93
- metricCache[source] = metrics;
94
- return metrics;
114
+ const filteredMetrics = filterRequiredMetrics(metrics);
115
+ metricCache[source] = filteredMetrics;
116
+ return filteredMetrics;
95
117
  }
96
118
 
119
+ const supportedExtensions = ["woff2", "woff", "ttf"];
97
120
  const FontaineTransform = unplugin.createUnplugin(
98
121
  (options) => {
99
122
  const cssContext = options.css = options.css || {};
100
123
  cssContext.value = "";
101
124
  const resolvePath = options.resolvePath || ((id) => id);
102
- const overrideName = options.overrideName || generateOverrideName;
125
+ const fallbackName = options.fallbackName || options.overrideName || generateFallbackName;
103
126
  function readMetricsFromId(path, importer) {
104
127
  const resolvedPath = pathe.isAbsolute(importer) && path.startsWith(".") ? pathe.join(importer, path) : resolvePath(path);
105
128
  return readMetrics(resolvedPath);
@@ -119,17 +142,20 @@ const FontaineTransform = unplugin.createUnplugin(
119
142
  if (match.index === void 0 || !matchContent)
120
143
  continue;
121
144
  faceRanges.push([match.index, match.index + matchContent.length]);
122
- const { family, source } = parseFontFace(matchContent);
123
- if (!family)
124
- continue;
125
- const metrics = await getMetricsForFamily(family) || source && await readMetricsFromId(source, id).catch(() => null);
126
- if (metrics) {
127
- const fontFace = generateFontFace(metrics, {
128
- name: overrideName(family),
129
- fallbacks: options.fallbacks
130
- });
131
- cssContext.value += fontFace;
132
- s.appendLeft(match.index, fontFace);
145
+ for (const { family, source } of parseFontFace(matchContent)) {
146
+ if (!family)
147
+ continue;
148
+ if (!supportedExtensions.some((e) => source?.endsWith(e)))
149
+ continue;
150
+ const metrics = await getMetricsForFamily(family) || source && await readMetricsFromId(source, id).catch(() => null);
151
+ if (metrics) {
152
+ const fontFace = generateFontFace(metrics, {
153
+ name: fallbackName(family),
154
+ fallbacks: options.fallbacks
155
+ });
156
+ cssContext.value += fontFace;
157
+ s.appendLeft(match.index, fontFace);
158
+ }
133
159
  }
134
160
  }
135
161
  for (const match of code.matchAll(FONT_FAMILY_RE)) {
@@ -139,14 +165,14 @@ const FontaineTransform = unplugin.createUnplugin(
139
165
  if (faceRanges.some(([start, end]) => index > start && index < end))
140
166
  continue;
141
167
  const families = matchContent.split(",").map((f) => f.trim()).filter((f) => !f.startsWith("var("));
142
- if (!families.length)
168
+ if (!families.length || families[0] === "inherit")
143
169
  continue;
144
170
  s.overwrite(
145
171
  index,
146
172
  index + matchContent.length,
147
173
  " " + [
148
174
  families[0],
149
- `"${generateOverrideName(families[0])}"`,
175
+ `"${generateFallbackName(families[0])}"`,
150
176
  ...families.slice(1)
151
177
  ].join(", ")
152
178
  );
@@ -174,7 +200,7 @@ const CSS_RE = magicRegexp.createRegExp(
174
200
  );
175
201
 
176
202
  exports.FontaineTransform = FontaineTransform;
203
+ exports.generateFallbackName = generateFallbackName;
177
204
  exports.generateFontFace = generateFontFace;
178
- exports.generateOverrideName = generateOverrideName;
179
205
  exports.getMetricsForFamily = getMetricsForFamily;
180
206
  exports.readMetrics = readMetrics;
package/dist/index.d.ts CHANGED
@@ -8,42 +8,23 @@ interface FontaineTransformOptions {
8
8
  fallbacks: string[];
9
9
  resolvePath?: (path: string) => string | URL;
10
10
  /** this should produce an unquoted font family name */
11
+ fallbackName?: (name: string) => string;
12
+ /** @deprecated use fallbackName */
11
13
  overrideName?: (name: string) => string;
12
14
  sourcemap?: boolean;
13
15
  }
14
- declare const FontaineTransform: unplugin.UnpluginInstance<FontaineTransformOptions>;
16
+ declare const FontaineTransform: unplugin.UnpluginInstance<FontaineTransformOptions, boolean>;
15
17
 
16
- declare const generateOverrideName: (name: string) => string;
18
+ declare const generateFallbackName: (name: string) => string;
17
19
  interface GenerateOptions {
18
20
  name: string;
19
21
  fallbacks: string[];
20
22
  [key: string]: any;
21
23
  }
22
- declare const generateFontFace: (metrics: Font, options: GenerateOptions) => string;
24
+ type FontFaceMetrics = Pick<Font, 'ascent' | 'descent' | 'lineGap' | 'unitsPerEm'>;
25
+ declare const generateFontFace: (metrics: FontFaceMetrics, options: GenerateOptions) => string;
23
26
 
24
- declare function getMetricsForFamily(family: string): Promise<{
25
- familyName: string;
26
- fullName: string;
27
- postscriptName: string;
28
- subfamilyName: string;
29
- capHeight: number;
30
- ascent: number;
31
- descent: number;
32
- lineGap: number;
33
- unitsPerEm: number;
34
- xHeight: number;
35
- } | null>;
36
- declare function readMetrics(_source: URL | string): Promise<{
37
- familyName: string;
38
- fullName: string;
39
- postscriptName: string;
40
- subfamilyName: string;
41
- capHeight: number;
42
- ascent: number;
43
- descent: number;
44
- lineGap: number;
45
- unitsPerEm: number;
46
- xHeight: number;
47
- } | null>;
27
+ declare function getMetricsForFamily(family: string): Promise<FontFaceMetrics | null>;
28
+ declare function readMetrics(_source: URL | string): Promise<FontFaceMetrics | null>;
48
29
 
49
- export { FontaineTransform, generateFontFace, generateOverrideName, getMetricsForFamily, readMetrics };
30
+ export { FontaineTransform, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics };
package/dist/index.mjs CHANGED
@@ -3,22 +3,29 @@ import { createRegExp, charIn, exactly, whitespace, charNotIn, anyOf } from 'mag
3
3
  import MagicString from 'magic-string';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { fromFile, fromUrl } from '@capsizecss/unpack';
6
+ import { fontFamilyToCamelCase } from '@capsizecss/metrics';
6
7
  import { parseURL } from 'ufo';
7
- import { camelCase } from 'scule';
8
8
  import { isAbsolute, join } from 'pathe';
9
9
 
10
- const parseFontFace = (css) => {
10
+ function* parseFontFace(css) {
11
11
  const fontFamily = css.match(FAMILY_RE)?.groups.fontFamily;
12
- const src = css.match(SOURCE_RE)?.groups.src;
13
12
  const family = withoutQuotes(fontFamily?.split(",")[0] || "");
14
- const source = withoutQuotes(
15
- src?.split(",").map((source2) => source2.match(URL_RE)?.groups.url).filter(Boolean)[0] || ""
16
- );
17
- return { family, source };
18
- };
19
- const generateOverrideName = (name) => {
13
+ for (const match of css.matchAll(SOURCE_RE)) {
14
+ const sources = match.groups.src?.split(",");
15
+ for (const entry of sources || []) {
16
+ for (const url of entry.matchAll(URL_RE)) {
17
+ const source = withoutQuotes(url.groups?.url || "");
18
+ if (source) {
19
+ yield { family, source };
20
+ }
21
+ }
22
+ }
23
+ }
24
+ yield { family: "", source: "" };
25
+ }
26
+ const generateFallbackName = (name) => {
20
27
  const firstFamily = withoutQuotes(name.split(",").shift());
21
- return `${firstFamily} override`;
28
+ return `${firstFamily} fallback`;
22
29
  };
23
30
  const withoutQuotes = (str) => str.trim().replace(QUOTES_RE, "");
24
31
  const generateFontFace = (metrics, options) => {
@@ -27,6 +34,7 @@ const generateFontFace = (metrics, options) => {
27
34
  const declaration = {
28
35
  "font-family": JSON.stringify(name),
29
36
  src: fallbacks.map((f) => `local(${JSON.stringify(f)})`),
37
+ // 'size-adjust': toPercentage(sizeAdjust),
30
38
  "ascent-override": toPercentage(
31
39
  metrics.ascent / (metrics.unitsPerEm * sizeAdjust)
32
40
  ),
@@ -56,24 +64,37 @@ const FAMILY_RE = createRegExp(
56
64
  exactly("font-family:").and(whitespace.optionally()).and(charNotIn(";}").times.any().as("fontFamily"))
57
65
  );
58
66
  const SOURCE_RE = createRegExp(
59
- exactly("src:").and(whitespace.optionally()).and(charNotIn(";}").times.any().as("src"))
67
+ exactly("src:").and(whitespace.optionally()).and(charNotIn(";}").times.any().as("src")),
68
+ ["g"]
60
69
  );
61
70
  const URL_RE = createRegExp(
62
- exactly("url(").and(charNotIn(")").times.any().as("url")).and(")")
71
+ exactly("url(").and(charNotIn(")").times.any().as("url")).and(")"),
72
+ ["g"]
63
73
  );
64
74
 
65
75
  const metricCache = {};
76
+ const filterRequiredMetrics = ({
77
+ ascent,
78
+ descent,
79
+ lineGap,
80
+ unitsPerEm
81
+ }) => ({
82
+ ascent,
83
+ descent,
84
+ lineGap,
85
+ unitsPerEm
86
+ });
66
87
  async function getMetricsForFamily(family) {
67
88
  family = withoutQuotes(family);
68
89
  if (family in metricCache)
69
90
  return metricCache[family];
70
91
  try {
71
- const name = camelCase(family).replace(/ /, "");
72
- const metrics = await import(`@capsizecss/metrics/${name}.js`).then(
73
- (r) => r.default || r
74
- );
75
- metricCache[family] = metrics;
76
- return metrics;
92
+ const name = fontFamilyToCamelCase(family);
93
+ const { entireMetricsCollection } = await import('@capsizecss/metrics/entireMetricsCollection');
94
+ const metrics = entireMetricsCollection[name];
95
+ const filteredMetrics = filterRequiredMetrics(metrics);
96
+ metricCache[family] = filteredMetrics;
97
+ return filteredMetrics;
77
98
  } catch {
78
99
  metricCache[family] = null;
79
100
  return null;
@@ -88,16 +109,18 @@ async function readMetrics(_source) {
88
109
  if (!protocol)
89
110
  return null;
90
111
  const metrics = protocol === "file:" ? await fromFile(fileURLToPath(source)) : await fromUrl(source);
91
- metricCache[source] = metrics;
92
- return metrics;
112
+ const filteredMetrics = filterRequiredMetrics(metrics);
113
+ metricCache[source] = filteredMetrics;
114
+ return filteredMetrics;
93
115
  }
94
116
 
117
+ const supportedExtensions = ["woff2", "woff", "ttf"];
95
118
  const FontaineTransform = createUnplugin(
96
119
  (options) => {
97
120
  const cssContext = options.css = options.css || {};
98
121
  cssContext.value = "";
99
122
  const resolvePath = options.resolvePath || ((id) => id);
100
- const overrideName = options.overrideName || generateOverrideName;
123
+ const fallbackName = options.fallbackName || options.overrideName || generateFallbackName;
101
124
  function readMetricsFromId(path, importer) {
102
125
  const resolvedPath = isAbsolute(importer) && path.startsWith(".") ? join(importer, path) : resolvePath(path);
103
126
  return readMetrics(resolvedPath);
@@ -117,17 +140,20 @@ const FontaineTransform = createUnplugin(
117
140
  if (match.index === void 0 || !matchContent)
118
141
  continue;
119
142
  faceRanges.push([match.index, match.index + matchContent.length]);
120
- const { family, source } = parseFontFace(matchContent);
121
- if (!family)
122
- continue;
123
- const metrics = await getMetricsForFamily(family) || source && await readMetricsFromId(source, id).catch(() => null);
124
- if (metrics) {
125
- const fontFace = generateFontFace(metrics, {
126
- name: overrideName(family),
127
- fallbacks: options.fallbacks
128
- });
129
- cssContext.value += fontFace;
130
- s.appendLeft(match.index, fontFace);
143
+ for (const { family, source } of parseFontFace(matchContent)) {
144
+ if (!family)
145
+ continue;
146
+ if (!supportedExtensions.some((e) => source?.endsWith(e)))
147
+ continue;
148
+ const metrics = await getMetricsForFamily(family) || source && await readMetricsFromId(source, id).catch(() => null);
149
+ if (metrics) {
150
+ const fontFace = generateFontFace(metrics, {
151
+ name: fallbackName(family),
152
+ fallbacks: options.fallbacks
153
+ });
154
+ cssContext.value += fontFace;
155
+ s.appendLeft(match.index, fontFace);
156
+ }
131
157
  }
132
158
  }
133
159
  for (const match of code.matchAll(FONT_FAMILY_RE)) {
@@ -137,14 +163,14 @@ const FontaineTransform = createUnplugin(
137
163
  if (faceRanges.some(([start, end]) => index > start && index < end))
138
164
  continue;
139
165
  const families = matchContent.split(",").map((f) => f.trim()).filter((f) => !f.startsWith("var("));
140
- if (!families.length)
166
+ if (!families.length || families[0] === "inherit")
141
167
  continue;
142
168
  s.overwrite(
143
169
  index,
144
170
  index + matchContent.length,
145
171
  " " + [
146
172
  families[0],
147
- `"${generateOverrideName(families[0])}"`,
173
+ `"${generateFallbackName(families[0])}"`,
148
174
  ...families.slice(1)
149
175
  ].join(", ")
150
176
  );
@@ -171,4 +197,4 @@ const CSS_RE = createRegExp(
171
197
  exactly(".").and(anyOf("sass", "css", "scss")).at.lineEnd()
172
198
  );
173
199
 
174
- export { FontaineTransform, generateFontFace, generateOverrideName, getMetricsForFamily, readMetrics };
200
+ export { FontaineTransform, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontaine",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Automatic font fallback based on font metrics",
5
5
  "repository": "unjs/fontaine",
6
6
  "keywords": [
@@ -40,48 +40,45 @@
40
40
  "lint:prettier": "prettier --write --loglevel warn",
41
41
  "prepare": "husky install && pnpm build",
42
42
  "prepublishOnly": "pnpm lint && pnpm test && pinst --disable",
43
- "release": "release-it",
43
+ "release": "pnpm test && bumpp && npm publish",
44
44
  "test": "vitest run",
45
45
  "_postinstall": "husky install",
46
46
  "postpublish": "pinst --enable"
47
47
  },
48
48
  "dependencies": {
49
- "@capsizecss/metrics": "^0.3.0",
50
- "@capsizecss/unpack": "^0.1.0",
51
- "magic-regexp": "^0.5.0",
52
- "magic-string": "^0.26.7",
53
- "pathe": "^0.3.9",
54
- "scule": "^0.3.2",
55
- "ufo": "^0.8.6",
56
- "unplugin": "^0.10.0"
49
+ "@capsizecss/metrics": "^1.1.1",
50
+ "@capsizecss/unpack": "^1.0.0",
51
+ "magic-regexp": "^0.6.3",
52
+ "magic-string": "^0.30.0",
53
+ "pathe": "^1.1.0",
54
+ "ufo": "^1.1.1",
55
+ "unplugin": "^1.1.0"
57
56
  },
58
57
  "devDependencies": {
59
58
  "@nuxtjs/eslint-config-typescript": "latest",
60
- "@release-it/conventional-changelog": "latest",
61
- "@types/node": "latest",
62
- "@types/serve-handler": "^6.1.1",
63
- "@typescript-eslint/eslint-plugin": "^5.40.1",
64
- "@typescript-eslint/parser": "^5.40.1",
65
- "@vitest/coverage-c8": "^0.24.3",
66
- "conventional-changelog-conventionalcommits": "latest",
67
- "eslint": "latest",
59
+ "@types/node": "18.14.6",
60
+ "@types/serve-handler": "6.1.1",
61
+ "@typescript-eslint/eslint-plugin": "5.54.0",
62
+ "@typescript-eslint/parser": "5.54.0",
63
+ "@vitest/coverage-c8": "0.29.2",
64
+ "bumpp": "^9.0.0",
65
+ "eslint": "8.35.0",
68
66
  "eslint-config-prettier": "latest",
69
67
  "eslint-plugin-prettier": "latest",
70
- "execa": "^6.1.0",
71
- "get-port-please": "^2.6.1",
68
+ "execa": "7.0.0",
69
+ "get-port-please": "3.0.1",
72
70
  "husky": "latest",
73
71
  "lint-staged": "latest",
74
72
  "pinst": "latest",
75
73
  "prettier": "latest",
76
- "release-it": "latest",
77
- "serve-handler": "^6.1.3",
74
+ "serve-handler": "6.1.5",
78
75
  "typescript": "latest",
79
76
  "unbuild": "latest",
80
- "vite": "latest",
81
- "vitest": "latest"
77
+ "vite": "4.1.4",
78
+ "vitest": "0.29.2"
82
79
  },
83
80
  "resolutions": {
84
81
  "fontaine": "link:."
85
82
  },
86
- "packageManager": "pnpm@7.13.5"
83
+ "packageManager": "pnpm@7.28.0"
87
84
  }