mirror of
https://github.com/withastro/astro.git
synced 2025-03-10 23:01:26 -05:00
feat(fonts): css vars (#13362)
* feat(fonts): css vars * feat: tests and fix * chore: add todos
This commit is contained in:
parent
f62a7b73f6
commit
75b1f5b877
7 changed files with 64 additions and 15 deletions
|
@ -6,6 +6,7 @@ function dedupe<T>(arr: Array<T>): Array<T> {
|
|||
|
||||
export const resolveFontOptionsSchema = z.object({
|
||||
weights: z
|
||||
// TODO: support numbers
|
||||
.array(z.string())
|
||||
.nonempty()
|
||||
.transform((arr) => dedupe(arr)),
|
||||
|
|
|
@ -20,6 +20,7 @@ interface Options
|
|||
resolvedMap: Map<string, { preloadData: PreloadData; css: string }>;
|
||||
resolveMod: ResolveMod;
|
||||
log: (message: string) => void;
|
||||
generateCSSVariableName: (name: string) => string;
|
||||
}
|
||||
|
||||
export async function loadFonts({
|
||||
|
@ -35,6 +36,7 @@ export async function loadFonts({
|
|||
generateFontFace: generateFallbackFontFace,
|
||||
getMetricsForFamily,
|
||||
log,
|
||||
generateCSSVariableName,
|
||||
}: Options): Promise<void> {
|
||||
const resolved = await resolveProviders({
|
||||
root,
|
||||
|
@ -137,11 +139,17 @@ export async function loadFonts({
|
|||
generateFontFace: generateFallbackFontFace,
|
||||
});
|
||||
|
||||
// TODO: support family.as
|
||||
const cssVarValues = [family.name];
|
||||
|
||||
if (fallbackData) {
|
||||
css += fallbackData.css;
|
||||
// TODO: generate css var
|
||||
cssVarValues.push(...fallbackData.fallbacks);
|
||||
}
|
||||
|
||||
// TODO: support family.as
|
||||
css += `:root { --astro-font-${generateCSSVariableName(family.name)}: ${cssVarValues.join(', ')}; }`;
|
||||
|
||||
resolvedMap.set(family.name, { preloadData, css });
|
||||
}
|
||||
log('Fonts initialized');
|
||||
|
|
|
@ -169,15 +169,17 @@ export async function generateFallbacksCSS({
|
|||
return { css, fallbacks };
|
||||
}
|
||||
|
||||
// We prepend the fallbacks with the local fonts and we dedupe in case a local font is already provided
|
||||
fallbacks = [...new Set([...localFonts, ...fallbacks])];
|
||||
const localFontsMappings = localFonts.map((font) => ({
|
||||
font,
|
||||
// TODO: support family.as
|
||||
name: `"${family} fallback: ${font}"`,
|
||||
}));
|
||||
|
||||
for (const fallback of localFonts) {
|
||||
css += generateFontFace(metrics, {
|
||||
font: fallback,
|
||||
// TODO: support family.as
|
||||
name: `${family} fallback: ${fallback}`,
|
||||
});
|
||||
// We prepend the fallbacks with the local fonts and we dedupe in case a local font is already provided
|
||||
fallbacks = [...new Set([...localFontsMappings.map((m) => m.name), ...fallbacks])];
|
||||
|
||||
for (const { font, name } of localFontsMappings) {
|
||||
css += generateFontFace(metrics, { font, name });
|
||||
}
|
||||
|
||||
return { css, fallbacks };
|
||||
|
@ -233,3 +235,15 @@ export function createLogManager(logger: Logger) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
const CAMEL_CASE_REGEX = /([a-z])([A-Z])/g;
|
||||
const NON_ALPHANUMERIC_REGEX = /[^a-zA-Z0-9]+/g;
|
||||
const TRIM_DASHES_REGEX = /^-+|-+$/g;
|
||||
|
||||
export function kebab(value: string) {
|
||||
return value
|
||||
.replace(CAMEL_CASE_REGEX, '$1-$2') // Handle camelCase
|
||||
.replace(NON_ALPHANUMERIC_REGEX, '-') // Replace non-alphanumeric characters with dashes
|
||||
.replace(TRIM_DASHES_REGEX, '') // Trim leading/trailing dashes
|
||||
.toLowerCase();
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import xxhash from 'xxhash-wasm';
|
|||
import { isAbsolute } from 'node:path';
|
||||
import { getClientOutputDirectory } from '../../prerender/utils.js';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { cache, createLogManager, extractFontType } from './utils.js';
|
||||
import { cache, createLogManager, extractFontType, kebab } from './utils.js';
|
||||
import {
|
||||
VIRTUAL_MODULE_ID,
|
||||
RESOLVED_VIRTUAL_MODULE_ID,
|
||||
|
@ -113,6 +113,7 @@ export function fontsPlugin({ settings, sync, logger }: Options): Plugin {
|
|||
},
|
||||
generateFontFace: fontaine.generateFontFace,
|
||||
log: (message) => logger.info('assets', message),
|
||||
generateCSSVariableName: (name) => kebab(name),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -662,6 +662,7 @@ export const AstroConfigSchema = z.object({
|
|||
// We dedupe families
|
||||
.transform((families) => [
|
||||
// TODO: support family.as
|
||||
// TODO: warn if some families are being overriden and how to resolve the issue
|
||||
...new Map(families.map((family) => [family.name, family])).values(),
|
||||
]),
|
||||
})
|
||||
|
|
|
@ -54,8 +54,9 @@ it('loadFonts()', async () => {
|
|||
families: [
|
||||
{
|
||||
name: 'Roboto',
|
||||
// @ts-expect-error we do weird typings internally for "reasons" (provider is typed as "local" | "custom") but this is valid
|
||||
provider: 'google',
|
||||
// we do weird typings internally for "reasons" (provider is typed as "local" | "custom") but this is valid
|
||||
provider: /** @type {any} */ ('google'),
|
||||
fallbacks: ['sans-serif'],
|
||||
},
|
||||
],
|
||||
storage,
|
||||
|
@ -68,11 +69,18 @@ it('loadFonts()', async () => {
|
|||
return await import(id);
|
||||
},
|
||||
hashString: (v) => Buffer.from(v).toString('base64'),
|
||||
getMetricsForFamily: async () => null,
|
||||
getMetricsForFamily: async () => ({
|
||||
ascent: 0,
|
||||
descent: 0,
|
||||
lineGap: 0,
|
||||
unitsPerEm: 0,
|
||||
xWidthAvg: 0,
|
||||
}),
|
||||
generateFontFace: () => '',
|
||||
log: (message) => {
|
||||
logs.push(message);
|
||||
},
|
||||
generateCSSVariableName: (name) => name,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
|
@ -82,4 +90,10 @@ it('loadFonts()', async () => {
|
|||
assert.equal(Array.from(hashToUrlMap.keys()).length > 0, true);
|
||||
assert.deepStrictEqual(Array.from(resolvedMap.keys()), ['Roboto']);
|
||||
assert.deepStrictEqual(logs, ['Fonts initialized']);
|
||||
assert.equal(
|
||||
resolvedMap
|
||||
.get('Roboto')
|
||||
.css.includes(':root { --astro-font-Roboto: Roboto, "Roboto fallback: Arial", sans-serif; }'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
proxyURL,
|
||||
isGenericFontFamily,
|
||||
generateFallbacksCSS,
|
||||
kebab,
|
||||
} from '../../../../dist/assets/fonts/utils.js';
|
||||
|
||||
function createSpyCache() {
|
||||
|
@ -302,10 +303,19 @@ describe('fonts utils', () => {
|
|||
generateFontFace: (_metrics, fallback) => `[${fallback.font},${fallback.name}]`,
|
||||
}),
|
||||
{
|
||||
css: `[Arial,Roboto fallback: Arial]`,
|
||||
fallbacks: ['Arial', 'foo', 'sans-serif'],
|
||||
css: `[Arial,"Roboto fallback: Arial"]`,
|
||||
fallbacks: ['"Roboto fallback: Arial"', 'foo', 'sans-serif'],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('kebab()', () => {
|
||||
assert.equal(kebab('valid'), 'valid')
|
||||
assert.equal(kebab('camelCase'), 'camel-case')
|
||||
assert.equal(kebab('PascalCase'), 'pascal-case')
|
||||
assert.equal(kebab('snake_case'), 'snake-case')
|
||||
assert.equal(kebab(' trim- '), 'trim')
|
||||
assert.equal(kebab('de--dupe'), 'de-dupe')
|
||||
})
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue