fontaine 0.4.0 → 0.5.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 +7 -8
- package/dist/index.cjs +58 -58
- package/dist/index.d.cts +32 -0
- package/dist/index.d.mts +32 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +58 -58
- package/package.json +41 -36
package/README.md
CHANGED
|
@@ -48,10 +48,13 @@ yarn add -D fontaine
|
|
|
48
48
|
```js
|
|
49
49
|
import { FontaineTransform } from 'fontaine'
|
|
50
50
|
|
|
51
|
+
// Astro config - astro.config.mjs
|
|
52
|
+
import { defineConfig } from 'astro/config'
|
|
53
|
+
|
|
51
54
|
const options = {
|
|
52
55
|
fallbacks: ['BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Arial', 'Noto Sans'],
|
|
53
56
|
// You may need to resolve assets like `/fonts/Roboto.woff2` to a particular directory
|
|
54
|
-
resolvePath:
|
|
57
|
+
resolvePath: id => `file:///path/to/public/dir${id}`,
|
|
55
58
|
// overrideName: (originalName) => `${name} override`
|
|
56
59
|
// sourcemap: false
|
|
57
60
|
// skipFontFaceGeneration: (fallbackName) => fallbackName === 'Roboto override'
|
|
@@ -83,9 +86,9 @@ function fontainePlugin(_context, _options) {
|
|
|
83
86
|
plugins: [
|
|
84
87
|
fontaine.FontaineTransform.webpack(options),
|
|
85
88
|
],
|
|
86
|
-
}
|
|
89
|
+
}
|
|
87
90
|
},
|
|
88
|
-
}
|
|
91
|
+
}
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
// Gatsby config - gatsby-node.js
|
|
@@ -97,17 +100,13 @@ exports.onCreateWebpackConfig = ({ stage, actions, getConfig }) => {
|
|
|
97
100
|
actions.replaceWebpackConfig(config)
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
// Astro config - astro.config.mjs
|
|
101
|
-
import { defineConfig } from 'astro/config'
|
|
102
|
-
import { FontaineTransform } from 'fontaine'
|
|
103
|
-
|
|
104
103
|
export default defineConfig({
|
|
105
104
|
integrations: [],
|
|
106
105
|
vite: {
|
|
107
106
|
plugins: [
|
|
108
107
|
FontaineTransform.vite({
|
|
109
108
|
fallbacks: ['Arial'],
|
|
110
|
-
resolvePath:
|
|
109
|
+
resolvePath: id => new URL(`./public${id}`, import.meta.url), // id is the font src value in the CSS
|
|
111
110
|
}),
|
|
112
111
|
],
|
|
113
112
|
},
|
package/dist/index.cjs
CHANGED
|
@@ -3,16 +3,39 @@
|
|
|
3
3
|
const unplugin = require('unplugin');
|
|
4
4
|
const magicRegexp = require('magic-regexp');
|
|
5
5
|
const MagicString = require('magic-string');
|
|
6
|
+
const ufo = require('ufo');
|
|
7
|
+
const pathe = require('pathe');
|
|
6
8
|
const node_url = require('node:url');
|
|
7
9
|
const unpack = require('@capsizecss/unpack');
|
|
8
10
|
const metrics = require('@capsizecss/metrics');
|
|
9
|
-
const ufo = require('ufo');
|
|
10
|
-
const pathe = require('pathe');
|
|
11
11
|
|
|
12
12
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
13
13
|
|
|
14
14
|
const MagicString__default = /*#__PURE__*/_interopDefaultCompat(MagicString);
|
|
15
15
|
|
|
16
|
+
function toPercentage(value, fractionDigits = 4) {
|
|
17
|
+
const percentage = value * 100;
|
|
18
|
+
return `${+percentage.toFixed(fractionDigits)}%`;
|
|
19
|
+
}
|
|
20
|
+
function toCSS(properties, indent = 2) {
|
|
21
|
+
return Object.entries(properties).map(([key, value]) => `${" ".repeat(indent)}${key}: ${value};`).join("\n");
|
|
22
|
+
}
|
|
23
|
+
const QUOTES_RE = magicRegexp.createRegExp(
|
|
24
|
+
magicRegexp.charIn(`"'`).at.lineStart().or(magicRegexp.charIn(`"'`).at.lineEnd()),
|
|
25
|
+
["g"]
|
|
26
|
+
);
|
|
27
|
+
const FAMILY_RE = magicRegexp.createRegExp(
|
|
28
|
+
magicRegexp.exactly("font-family:").and(magicRegexp.whitespace.optionally()).and(magicRegexp.charNotIn(";}").times.any().as("fontFamily"))
|
|
29
|
+
);
|
|
30
|
+
const SOURCE_RE = magicRegexp.createRegExp(
|
|
31
|
+
magicRegexp.exactly("src:").and(magicRegexp.whitespace.optionally()).and(magicRegexp.charNotIn(";}").times.any().as("src")),
|
|
32
|
+
["g"]
|
|
33
|
+
);
|
|
34
|
+
const URL_RE = magicRegexp.createRegExp(
|
|
35
|
+
magicRegexp.exactly("url(").and(magicRegexp.charNotIn(")").times.any().as("url")).and(")"),
|
|
36
|
+
["g"]
|
|
37
|
+
);
|
|
38
|
+
const withoutQuotes = (str) => str.trim().replace(QUOTES_RE, "");
|
|
16
39
|
function* parseFontFace(css) {
|
|
17
40
|
const fontFamily = css.match(FAMILY_RE)?.groups.fontFamily;
|
|
18
41
|
const family = withoutQuotes(fontFamily?.split(",")[0] || "");
|
|
@@ -21,20 +44,18 @@ function* parseFontFace(css) {
|
|
|
21
44
|
for (const entry of sources || []) {
|
|
22
45
|
for (const url of entry.matchAll(URL_RE)) {
|
|
23
46
|
const source = withoutQuotes(url.groups?.url || "");
|
|
24
|
-
if (source)
|
|
47
|
+
if (source)
|
|
25
48
|
yield { family, source };
|
|
26
|
-
}
|
|
27
49
|
}
|
|
28
50
|
}
|
|
29
51
|
}
|
|
30
52
|
yield { family: "", source: "" };
|
|
31
53
|
}
|
|
32
|
-
|
|
54
|
+
function generateFallbackName(name) {
|
|
33
55
|
const firstFamily = withoutQuotes(name.split(",").shift());
|
|
34
56
|
return `${firstFamily} fallback`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const generateFontFace = (metrics, fallback) => {
|
|
57
|
+
}
|
|
58
|
+
function generateFontFace(metrics, fallback) {
|
|
38
59
|
const {
|
|
39
60
|
name: fallbackName,
|
|
40
61
|
font: fallbackFontName,
|
|
@@ -50,7 +71,7 @@ const generateFontFace = (metrics, fallback) => {
|
|
|
50
71
|
const lineGapOverride = metrics.lineGap / adjustedEmSquare;
|
|
51
72
|
const declaration = {
|
|
52
73
|
"font-family": JSON.stringify(fallbackName),
|
|
53
|
-
src: `local(${JSON.stringify(fallbackFontName)})`,
|
|
74
|
+
"src": `local(${JSON.stringify(fallbackFontName)})`,
|
|
54
75
|
"size-adjust": toPercentage(sizeAdjust),
|
|
55
76
|
"ascent-override": toPercentage(ascentOverride),
|
|
56
77
|
"descent-override": toPercentage(descentOverride),
|
|
@@ -61,49 +82,31 @@ const generateFontFace = (metrics, fallback) => {
|
|
|
61
82
|
${toCSS(declaration)}
|
|
62
83
|
}
|
|
63
84
|
`;
|
|
64
|
-
}
|
|
65
|
-
const toPercentage = (value, fractionDigits = 4) => {
|
|
66
|
-
const percentage = value * 100;
|
|
67
|
-
return +percentage.toFixed(fractionDigits) + "%";
|
|
68
|
-
};
|
|
69
|
-
const toCSS = (properties, indent = 2) => Object.entries(properties).map(([key, value]) => " ".repeat(indent) + `${key}: ${value};`).join("\n");
|
|
70
|
-
const QUOTES_RE = magicRegexp.createRegExp(
|
|
71
|
-
magicRegexp.charIn(`"'`).at.lineStart().or(magicRegexp.charIn(`"'`).at.lineEnd()),
|
|
72
|
-
["g"]
|
|
73
|
-
);
|
|
74
|
-
const FAMILY_RE = magicRegexp.createRegExp(
|
|
75
|
-
magicRegexp.exactly("font-family:").and(magicRegexp.whitespace.optionally()).and(magicRegexp.charNotIn(";}").times.any().as("fontFamily"))
|
|
76
|
-
);
|
|
77
|
-
const SOURCE_RE = magicRegexp.createRegExp(
|
|
78
|
-
magicRegexp.exactly("src:").and(magicRegexp.whitespace.optionally()).and(magicRegexp.charNotIn(";}").times.any().as("src")),
|
|
79
|
-
["g"]
|
|
80
|
-
);
|
|
81
|
-
const URL_RE = magicRegexp.createRegExp(
|
|
82
|
-
magicRegexp.exactly("url(").and(magicRegexp.charNotIn(")").times.any().as("url")).and(")"),
|
|
83
|
-
["g"]
|
|
84
|
-
);
|
|
85
|
+
}
|
|
85
86
|
|
|
86
87
|
const metricCache = {};
|
|
87
|
-
|
|
88
|
+
function filterRequiredMetrics({
|
|
88
89
|
ascent,
|
|
89
90
|
descent,
|
|
90
91
|
lineGap,
|
|
91
92
|
unitsPerEm,
|
|
92
93
|
xWidthAvg
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
}) {
|
|
95
|
+
return {
|
|
96
|
+
ascent,
|
|
97
|
+
descent,
|
|
98
|
+
lineGap,
|
|
99
|
+
unitsPerEm,
|
|
100
|
+
xWidthAvg
|
|
101
|
+
};
|
|
102
|
+
}
|
|
100
103
|
async function getMetricsForFamily(family) {
|
|
101
104
|
family = withoutQuotes(family);
|
|
102
105
|
if (family in metricCache)
|
|
103
106
|
return metricCache[family];
|
|
104
107
|
try {
|
|
105
108
|
const name = metrics.fontFamilyToCamelCase(family);
|
|
106
|
-
const { entireMetricsCollection } = await import('@capsizecss/metrics/entireMetricsCollection
|
|
109
|
+
const { entireMetricsCollection } = await import('@capsizecss/metrics/entireMetricsCollection');
|
|
107
110
|
const metrics$1 = entireMetricsCollection[name];
|
|
108
111
|
const filteredMetrics = filterRequiredMetrics(metrics$1);
|
|
109
112
|
metricCache[family] = filteredMetrics;
|
|
@@ -115,9 +118,8 @@ async function getMetricsForFamily(family) {
|
|
|
115
118
|
}
|
|
116
119
|
async function readMetrics(_source) {
|
|
117
120
|
const source = typeof _source !== "string" && "href" in _source ? _source.href : _source;
|
|
118
|
-
if (source in metricCache)
|
|
121
|
+
if (source in metricCache)
|
|
119
122
|
return Promise.resolve(metricCache[source]);
|
|
120
|
-
}
|
|
121
123
|
const { protocol } = ufo.parseURL(source);
|
|
122
124
|
if (!protocol)
|
|
123
125
|
return null;
|
|
@@ -128,6 +130,17 @@ async function readMetrics(_source) {
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
const supportedExtensions = ["woff2", "woff", "ttf"];
|
|
133
|
+
const FONT_FACE_RE = magicRegexp.createRegExp(
|
|
134
|
+
magicRegexp.exactly("@font-face").and(magicRegexp.whitespace.times.any()).and("{").and(magicRegexp.charNotIn("}").times.any()).and("}"),
|
|
135
|
+
["g"]
|
|
136
|
+
);
|
|
137
|
+
const FONT_FAMILY_RE = magicRegexp.createRegExp(
|
|
138
|
+
magicRegexp.charNotIn(";}").times.any().as("family").after(magicRegexp.exactly("font-family:").and(magicRegexp.whitespace.times.any())).before(magicRegexp.charIn(";}").or(magicRegexp.exactly("").at.lineEnd())),
|
|
139
|
+
["g"]
|
|
140
|
+
);
|
|
141
|
+
const CSS_RE = magicRegexp.createRegExp(
|
|
142
|
+
magicRegexp.exactly(".").and(magicRegexp.anyOf("sass", "css", "scss")).at.lineEnd()
|
|
143
|
+
);
|
|
131
144
|
const FontaineTransform = unplugin.createUnplugin(
|
|
132
145
|
(options) => {
|
|
133
146
|
const cssContext = options.css = options.css || {};
|
|
@@ -162,15 +175,13 @@ const FontaineTransform = unplugin.createUnplugin(
|
|
|
162
175
|
if (skipFontFaceGeneration(fallbackName(family)))
|
|
163
176
|
continue;
|
|
164
177
|
const metrics = await getMetricsForFamily(family) || source && await readMetricsFromId(source, id).catch(() => null);
|
|
165
|
-
if (!metrics)
|
|
178
|
+
if (!metrics)
|
|
166
179
|
continue;
|
|
167
|
-
}
|
|
168
180
|
for (let i = options.fallbacks.length - 1; i >= 0; i--) {
|
|
169
181
|
const fallback = options.fallbacks[i];
|
|
170
182
|
const fallbackMetrics = await getMetricsForFamily(fallback);
|
|
171
|
-
if (!fallbackMetrics)
|
|
183
|
+
if (!fallbackMetrics)
|
|
172
184
|
continue;
|
|
173
|
-
}
|
|
174
185
|
const fontFace = generateFontFace(metrics, {
|
|
175
186
|
name: fallbackName(family),
|
|
176
187
|
font: fallback,
|
|
@@ -193,11 +204,11 @@ const FontaineTransform = unplugin.createUnplugin(
|
|
|
193
204
|
s.overwrite(
|
|
194
205
|
index,
|
|
195
206
|
index + matchContent.length,
|
|
196
|
-
|
|
207
|
+
` ${[
|
|
197
208
|
families[0],
|
|
198
209
|
`"${generateFallbackName(families[0])}"`,
|
|
199
210
|
...families.slice(1)
|
|
200
|
-
].join(", ")
|
|
211
|
+
].join(", ")}`
|
|
201
212
|
);
|
|
202
213
|
}
|
|
203
214
|
if (s.hasChanged()) {
|
|
@@ -210,17 +221,6 @@ const FontaineTransform = unplugin.createUnplugin(
|
|
|
210
221
|
};
|
|
211
222
|
}
|
|
212
223
|
);
|
|
213
|
-
const FONT_FACE_RE = magicRegexp.createRegExp(
|
|
214
|
-
magicRegexp.exactly("@font-face").and(magicRegexp.whitespace.times.any()).and("{").and(magicRegexp.charNotIn("}").times.any()).and("}"),
|
|
215
|
-
["g"]
|
|
216
|
-
);
|
|
217
|
-
const FONT_FAMILY_RE = magicRegexp.createRegExp(
|
|
218
|
-
magicRegexp.charNotIn(";}").times.any().as("family").after(magicRegexp.exactly("font-family:").and(magicRegexp.whitespace.times.any())).before(magicRegexp.charIn(";}").or(magicRegexp.exactly("").at.lineEnd())),
|
|
219
|
-
["g"]
|
|
220
|
-
);
|
|
221
|
-
const CSS_RE = magicRegexp.createRegExp(
|
|
222
|
-
magicRegexp.exactly(".").and(magicRegexp.anyOf("sass", "css", "scss")).at.lineEnd()
|
|
223
|
-
);
|
|
224
224
|
|
|
225
225
|
exports.FontaineTransform = FontaineTransform;
|
|
226
226
|
exports.generateFallbackName = generateFallbackName;
|
package/dist/index.d.cts
ADDED
|
@@ -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 function 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 function 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.mts
ADDED
|
@@ -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 function 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 function 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
|
@@ -16,7 +16,7 @@ interface FontaineTransformOptions {
|
|
|
16
16
|
}
|
|
17
17
|
declare const FontaineTransform: unplugin.UnpluginInstance<FontaineTransformOptions, boolean>;
|
|
18
18
|
|
|
19
|
-
declare
|
|
19
|
+
declare function generateFallbackName(name: string): string;
|
|
20
20
|
interface FallbackOptions {
|
|
21
21
|
name: string;
|
|
22
22
|
font: string;
|
|
@@ -24,9 +24,9 @@ interface FallbackOptions {
|
|
|
24
24
|
[key: string]: any;
|
|
25
25
|
}
|
|
26
26
|
type FontFaceMetrics = Pick<Font, 'ascent' | 'descent' | 'lineGap' | 'unitsPerEm' | 'xWidthAvg'>;
|
|
27
|
-
declare
|
|
27
|
+
declare function generateFontFace(metrics: FontFaceMetrics, fallback: FallbackOptions): string;
|
|
28
28
|
|
|
29
29
|
declare function getMetricsForFamily(family: string): Promise<FontFaceMetrics | null>;
|
|
30
30
|
declare function readMetrics(_source: URL | string): Promise<FontFaceMetrics | null>;
|
|
31
31
|
|
|
32
|
-
export { FontaineTransform, FontaineTransformOptions, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics };
|
|
32
|
+
export { FontaineTransform, type FontaineTransformOptions, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics };
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { createUnplugin } from 'unplugin';
|
|
2
2
|
import { createRegExp, charIn, exactly, whitespace, charNotIn, anyOf } from 'magic-regexp';
|
|
3
3
|
import MagicString from 'magic-string';
|
|
4
|
+
import { parseURL } from 'ufo';
|
|
5
|
+
import { isAbsolute, join } from 'pathe';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { fromFile, fromUrl } from '@capsizecss/unpack';
|
|
6
8
|
import { fontFamilyToCamelCase } from '@capsizecss/metrics';
|
|
7
|
-
import { parseURL } from 'ufo';
|
|
8
|
-
import { isAbsolute, join } from 'pathe';
|
|
9
9
|
|
|
10
|
+
function toPercentage(value, fractionDigits = 4) {
|
|
11
|
+
const percentage = value * 100;
|
|
12
|
+
return `${+percentage.toFixed(fractionDigits)}%`;
|
|
13
|
+
}
|
|
14
|
+
function toCSS(properties, indent = 2) {
|
|
15
|
+
return Object.entries(properties).map(([key, value]) => `${" ".repeat(indent)}${key}: ${value};`).join("\n");
|
|
16
|
+
}
|
|
17
|
+
const QUOTES_RE = createRegExp(
|
|
18
|
+
charIn(`"'`).at.lineStart().or(charIn(`"'`).at.lineEnd()),
|
|
19
|
+
["g"]
|
|
20
|
+
);
|
|
21
|
+
const FAMILY_RE = createRegExp(
|
|
22
|
+
exactly("font-family:").and(whitespace.optionally()).and(charNotIn(";}").times.any().as("fontFamily"))
|
|
23
|
+
);
|
|
24
|
+
const SOURCE_RE = createRegExp(
|
|
25
|
+
exactly("src:").and(whitespace.optionally()).and(charNotIn(";}").times.any().as("src")),
|
|
26
|
+
["g"]
|
|
27
|
+
);
|
|
28
|
+
const URL_RE = createRegExp(
|
|
29
|
+
exactly("url(").and(charNotIn(")").times.any().as("url")).and(")"),
|
|
30
|
+
["g"]
|
|
31
|
+
);
|
|
32
|
+
const withoutQuotes = (str) => str.trim().replace(QUOTES_RE, "");
|
|
10
33
|
function* parseFontFace(css) {
|
|
11
34
|
const fontFamily = css.match(FAMILY_RE)?.groups.fontFamily;
|
|
12
35
|
const family = withoutQuotes(fontFamily?.split(",")[0] || "");
|
|
@@ -15,20 +38,18 @@ function* parseFontFace(css) {
|
|
|
15
38
|
for (const entry of sources || []) {
|
|
16
39
|
for (const url of entry.matchAll(URL_RE)) {
|
|
17
40
|
const source = withoutQuotes(url.groups?.url || "");
|
|
18
|
-
if (source)
|
|
41
|
+
if (source)
|
|
19
42
|
yield { family, source };
|
|
20
|
-
}
|
|
21
43
|
}
|
|
22
44
|
}
|
|
23
45
|
}
|
|
24
46
|
yield { family: "", source: "" };
|
|
25
47
|
}
|
|
26
|
-
|
|
48
|
+
function generateFallbackName(name) {
|
|
27
49
|
const firstFamily = withoutQuotes(name.split(",").shift());
|
|
28
50
|
return `${firstFamily} fallback`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const generateFontFace = (metrics, fallback) => {
|
|
51
|
+
}
|
|
52
|
+
function generateFontFace(metrics, fallback) {
|
|
32
53
|
const {
|
|
33
54
|
name: fallbackName,
|
|
34
55
|
font: fallbackFontName,
|
|
@@ -44,7 +65,7 @@ const generateFontFace = (metrics, fallback) => {
|
|
|
44
65
|
const lineGapOverride = metrics.lineGap / adjustedEmSquare;
|
|
45
66
|
const declaration = {
|
|
46
67
|
"font-family": JSON.stringify(fallbackName),
|
|
47
|
-
src: `local(${JSON.stringify(fallbackFontName)})`,
|
|
68
|
+
"src": `local(${JSON.stringify(fallbackFontName)})`,
|
|
48
69
|
"size-adjust": toPercentage(sizeAdjust),
|
|
49
70
|
"ascent-override": toPercentage(ascentOverride),
|
|
50
71
|
"descent-override": toPercentage(descentOverride),
|
|
@@ -55,49 +76,31 @@ const generateFontFace = (metrics, fallback) => {
|
|
|
55
76
|
${toCSS(declaration)}
|
|
56
77
|
}
|
|
57
78
|
`;
|
|
58
|
-
}
|
|
59
|
-
const toPercentage = (value, fractionDigits = 4) => {
|
|
60
|
-
const percentage = value * 100;
|
|
61
|
-
return +percentage.toFixed(fractionDigits) + "%";
|
|
62
|
-
};
|
|
63
|
-
const toCSS = (properties, indent = 2) => Object.entries(properties).map(([key, value]) => " ".repeat(indent) + `${key}: ${value};`).join("\n");
|
|
64
|
-
const QUOTES_RE = createRegExp(
|
|
65
|
-
charIn(`"'`).at.lineStart().or(charIn(`"'`).at.lineEnd()),
|
|
66
|
-
["g"]
|
|
67
|
-
);
|
|
68
|
-
const FAMILY_RE = createRegExp(
|
|
69
|
-
exactly("font-family:").and(whitespace.optionally()).and(charNotIn(";}").times.any().as("fontFamily"))
|
|
70
|
-
);
|
|
71
|
-
const SOURCE_RE = createRegExp(
|
|
72
|
-
exactly("src:").and(whitespace.optionally()).and(charNotIn(";}").times.any().as("src")),
|
|
73
|
-
["g"]
|
|
74
|
-
);
|
|
75
|
-
const URL_RE = createRegExp(
|
|
76
|
-
exactly("url(").and(charNotIn(")").times.any().as("url")).and(")"),
|
|
77
|
-
["g"]
|
|
78
|
-
);
|
|
79
|
+
}
|
|
79
80
|
|
|
80
81
|
const metricCache = {};
|
|
81
|
-
|
|
82
|
+
function filterRequiredMetrics({
|
|
82
83
|
ascent,
|
|
83
84
|
descent,
|
|
84
85
|
lineGap,
|
|
85
86
|
unitsPerEm,
|
|
86
87
|
xWidthAvg
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
}) {
|
|
89
|
+
return {
|
|
90
|
+
ascent,
|
|
91
|
+
descent,
|
|
92
|
+
lineGap,
|
|
93
|
+
unitsPerEm,
|
|
94
|
+
xWidthAvg
|
|
95
|
+
};
|
|
96
|
+
}
|
|
94
97
|
async function getMetricsForFamily(family) {
|
|
95
98
|
family = withoutQuotes(family);
|
|
96
99
|
if (family in metricCache)
|
|
97
100
|
return metricCache[family];
|
|
98
101
|
try {
|
|
99
102
|
const name = fontFamilyToCamelCase(family);
|
|
100
|
-
const { entireMetricsCollection } = await import('@capsizecss/metrics/entireMetricsCollection
|
|
103
|
+
const { entireMetricsCollection } = await import('@capsizecss/metrics/entireMetricsCollection');
|
|
101
104
|
const metrics = entireMetricsCollection[name];
|
|
102
105
|
const filteredMetrics = filterRequiredMetrics(metrics);
|
|
103
106
|
metricCache[family] = filteredMetrics;
|
|
@@ -109,9 +112,8 @@ async function getMetricsForFamily(family) {
|
|
|
109
112
|
}
|
|
110
113
|
async function readMetrics(_source) {
|
|
111
114
|
const source = typeof _source !== "string" && "href" in _source ? _source.href : _source;
|
|
112
|
-
if (source in metricCache)
|
|
115
|
+
if (source in metricCache)
|
|
113
116
|
return Promise.resolve(metricCache[source]);
|
|
114
|
-
}
|
|
115
117
|
const { protocol } = parseURL(source);
|
|
116
118
|
if (!protocol)
|
|
117
119
|
return null;
|
|
@@ -122,6 +124,17 @@ async function readMetrics(_source) {
|
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
const supportedExtensions = ["woff2", "woff", "ttf"];
|
|
127
|
+
const FONT_FACE_RE = createRegExp(
|
|
128
|
+
exactly("@font-face").and(whitespace.times.any()).and("{").and(charNotIn("}").times.any()).and("}"),
|
|
129
|
+
["g"]
|
|
130
|
+
);
|
|
131
|
+
const FONT_FAMILY_RE = createRegExp(
|
|
132
|
+
charNotIn(";}").times.any().as("family").after(exactly("font-family:").and(whitespace.times.any())).before(charIn(";}").or(exactly("").at.lineEnd())),
|
|
133
|
+
["g"]
|
|
134
|
+
);
|
|
135
|
+
const CSS_RE = createRegExp(
|
|
136
|
+
exactly(".").and(anyOf("sass", "css", "scss")).at.lineEnd()
|
|
137
|
+
);
|
|
125
138
|
const FontaineTransform = createUnplugin(
|
|
126
139
|
(options) => {
|
|
127
140
|
const cssContext = options.css = options.css || {};
|
|
@@ -156,15 +169,13 @@ const FontaineTransform = createUnplugin(
|
|
|
156
169
|
if (skipFontFaceGeneration(fallbackName(family)))
|
|
157
170
|
continue;
|
|
158
171
|
const metrics = await getMetricsForFamily(family) || source && await readMetricsFromId(source, id).catch(() => null);
|
|
159
|
-
if (!metrics)
|
|
172
|
+
if (!metrics)
|
|
160
173
|
continue;
|
|
161
|
-
}
|
|
162
174
|
for (let i = options.fallbacks.length - 1; i >= 0; i--) {
|
|
163
175
|
const fallback = options.fallbacks[i];
|
|
164
176
|
const fallbackMetrics = await getMetricsForFamily(fallback);
|
|
165
|
-
if (!fallbackMetrics)
|
|
177
|
+
if (!fallbackMetrics)
|
|
166
178
|
continue;
|
|
167
|
-
}
|
|
168
179
|
const fontFace = generateFontFace(metrics, {
|
|
169
180
|
name: fallbackName(family),
|
|
170
181
|
font: fallback,
|
|
@@ -187,11 +198,11 @@ const FontaineTransform = createUnplugin(
|
|
|
187
198
|
s.overwrite(
|
|
188
199
|
index,
|
|
189
200
|
index + matchContent.length,
|
|
190
|
-
|
|
201
|
+
` ${[
|
|
191
202
|
families[0],
|
|
192
203
|
`"${generateFallbackName(families[0])}"`,
|
|
193
204
|
...families.slice(1)
|
|
194
|
-
].join(", ")
|
|
205
|
+
].join(", ")}`
|
|
195
206
|
);
|
|
196
207
|
}
|
|
197
208
|
if (s.hasChanged()) {
|
|
@@ -204,16 +215,5 @@ const FontaineTransform = createUnplugin(
|
|
|
204
215
|
};
|
|
205
216
|
}
|
|
206
217
|
);
|
|
207
|
-
const FONT_FACE_RE = createRegExp(
|
|
208
|
-
exactly("@font-face").and(whitespace.times.any()).and("{").and(charNotIn("}").times.any()).and("}"),
|
|
209
|
-
["g"]
|
|
210
|
-
);
|
|
211
|
-
const FONT_FAMILY_RE = createRegExp(
|
|
212
|
-
charNotIn(";}").times.any().as("family").after(exactly("font-family:").and(whitespace.times.any())).before(charIn(";}").or(exactly("").at.lineEnd())),
|
|
213
|
-
["g"]
|
|
214
|
-
);
|
|
215
|
-
const CSS_RE = createRegExp(
|
|
216
|
-
exactly(".").and(anyOf("sass", "css", "scss")).at.lineEnd()
|
|
217
|
-
);
|
|
218
218
|
|
|
219
219
|
export { FontaineTransform, generateFallbackName, generateFontFace, getMetricsForFamily, readMetrics };
|
package/package.json
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fontaine",
|
|
3
|
-
"
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.5.0",
|
|
5
|
+
"packageManager": "pnpm@8.15.4",
|
|
4
6
|
"description": "Automatic font fallback based on font metrics",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Daniel Roe <daniel@roe.dev>",
|
|
9
|
+
"url": "https://github.com/danielroe"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
5
12
|
"repository": "unjs/fontaine",
|
|
6
13
|
"keywords": [
|
|
7
14
|
"fonts",
|
|
@@ -9,13 +16,7 @@
|
|
|
9
16
|
"web-vitals",
|
|
10
17
|
"performance"
|
|
11
18
|
],
|
|
12
|
-
"author": {
|
|
13
|
-
"name": "Daniel Roe <daniel@roe.dev>",
|
|
14
|
-
"url": "https://github.com/danielroe"
|
|
15
|
-
},
|
|
16
|
-
"license": "MIT",
|
|
17
19
|
"sideEffects": false,
|
|
18
|
-
"type": "module",
|
|
19
20
|
"exports": {
|
|
20
21
|
".": {
|
|
21
22
|
"import": "./dist/index.mjs",
|
|
@@ -33,49 +34,53 @@
|
|
|
33
34
|
"dev": "vitest",
|
|
34
35
|
"demo": "vite dev playground",
|
|
35
36
|
"demo:dev": "pnpm demo --config test/vite.config.mjs",
|
|
36
|
-
"lint": "
|
|
37
|
-
"
|
|
38
|
-
"lint:all:prettier": "pnpm lint:prettier \"{src,test}/**/*.{js,json,ts}\"",
|
|
39
|
-
"lint:eslint": "eslint --fix",
|
|
40
|
-
"lint:prettier": "prettier --write --loglevel warn",
|
|
41
|
-
"prepare": "husky install && pnpm build",
|
|
37
|
+
"lint": "eslint --fix .",
|
|
38
|
+
"prepare": "simple-git-hooks install && pnpm build",
|
|
42
39
|
"prepublishOnly": "pnpm lint && pnpm test",
|
|
43
40
|
"release": "pnpm test && bumpp && npm publish",
|
|
44
41
|
"test": "vitest run"
|
|
45
42
|
},
|
|
46
43
|
"dependencies": {
|
|
47
|
-
"@capsizecss/metrics": "^
|
|
48
|
-
"@capsizecss/unpack": "^
|
|
49
|
-
"magic-regexp": "^0.
|
|
50
|
-
"magic-string": "^0.30.
|
|
51
|
-
"pathe": "^1.1.
|
|
52
|
-
"ufo": "^1.
|
|
53
|
-
"unplugin": "^1.3
|
|
44
|
+
"@capsizecss/metrics": "^2.1.1",
|
|
45
|
+
"@capsizecss/unpack": "^2.0.1",
|
|
46
|
+
"magic-regexp": "^0.8.0",
|
|
47
|
+
"magic-string": "^0.30.8",
|
|
48
|
+
"pathe": "^1.1.2",
|
|
49
|
+
"ufo": "^1.4.0",
|
|
50
|
+
"unplugin": "^1.8.3"
|
|
54
51
|
},
|
|
55
52
|
"devDependencies": {
|
|
53
|
+
"@antfu/eslint-config": "^2.8.0",
|
|
56
54
|
"@nuxtjs/eslint-config-typescript": "latest",
|
|
57
|
-
"@types/node": "
|
|
58
|
-
"@types/serve-handler": "6.1.
|
|
59
|
-
"@typescript-eslint/eslint-plugin": "
|
|
60
|
-
"@typescript-eslint/parser": "
|
|
61
|
-
"@vitest/coverage-v8": "
|
|
62
|
-
"bumpp": "9.
|
|
63
|
-
"eslint": "8.
|
|
64
|
-
"eslint-config-prettier": "
|
|
55
|
+
"@types/node": "20.11.25",
|
|
56
|
+
"@types/serve-handler": "6.1.4",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "7.1.1",
|
|
58
|
+
"@typescript-eslint/parser": "7.1.1",
|
|
59
|
+
"@vitest/coverage-v8": "1.3.1",
|
|
60
|
+
"bumpp": "9.4.0",
|
|
61
|
+
"eslint": "8.57.0",
|
|
62
|
+
"eslint-config-prettier": "9.1.0",
|
|
65
63
|
"eslint-plugin-prettier": "latest",
|
|
66
|
-
"execa": "
|
|
67
|
-
"get-port-please": "3.
|
|
68
|
-
"
|
|
69
|
-
"lint-staged": "13.2.2",
|
|
64
|
+
"execa": "8.0.1",
|
|
65
|
+
"get-port-please": "3.1.2",
|
|
66
|
+
"lint-staged": "15.2.2",
|
|
70
67
|
"prettier": "latest",
|
|
71
68
|
"serve-handler": "6.1.5",
|
|
72
|
-
"
|
|
69
|
+
"simple-git-hooks": "^2.10.0",
|
|
70
|
+
"typescript": "5.4.2",
|
|
73
71
|
"unbuild": "latest",
|
|
74
|
-
"vite": "
|
|
75
|
-
"vitest": "
|
|
72
|
+
"vite": "5.1.5",
|
|
73
|
+
"vitest": "1.3.1"
|
|
76
74
|
},
|
|
77
75
|
"resolutions": {
|
|
78
76
|
"fontaine": "link:."
|
|
79
77
|
},
|
|
80
|
-
"
|
|
78
|
+
"simple-git-hooks": {
|
|
79
|
+
"pre-commit": "npx lint-staged"
|
|
80
|
+
},
|
|
81
|
+
"lint-staged": {
|
|
82
|
+
"*.{js,ts,mjs,cjs,json,.*rc}": [
|
|
83
|
+
"pnpm eslint --fix"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
81
86
|
}
|