mirror of
https://github.com/quartz-community/fonts.git
synced 2026-07-22 03:00:28 +00:00
feat: allow self-hosted fonts
This commit is contained in:
parent
71e090afd7
commit
fb6ece0478
15 changed files with 467 additions and 13 deletions
6
dist/emitter.d.ts
vendored
Normal file
6
dist/emitter.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { QuartzEmitterPlugin } from '@quartz-community/types';
|
||||
import { QuartzFontsOptions } from './types.js';
|
||||
|
||||
declare const QuartzFontsEmitter: QuartzEmitterPlugin<Partial<QuartzFontsOptions>>;
|
||||
|
||||
export { QuartzFontsEmitter };
|
||||
147
dist/emitter.js
vendored
Normal file
147
dist/emitter.js
vendored
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { createRequire } from 'module';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
createRequire(import.meta.url);
|
||||
|
||||
// src/defaults.ts
|
||||
var DEFAULT_WEIGHTS = {
|
||||
title: [400, 700],
|
||||
header: [400, 700],
|
||||
body: [400, 600],
|
||||
code: [400, 600]
|
||||
};
|
||||
var DEFAULT_ITALIC = {
|
||||
title: false,
|
||||
header: false,
|
||||
body: true,
|
||||
code: false
|
||||
};
|
||||
|
||||
// src/util/google-fonts.ts
|
||||
function normalizeFontSpec(spec) {
|
||||
return typeof spec === "string" ? { name: spec } : spec;
|
||||
}
|
||||
function mergeInto(map, role, spec) {
|
||||
const { name, weights, includeItalic } = normalizeFontSpec(spec);
|
||||
const resolvedWeights = weights ?? DEFAULT_WEIGHTS[role];
|
||||
const italic = includeItalic ?? DEFAULT_ITALIC[role];
|
||||
const existing = map.get(name);
|
||||
if (existing) {
|
||||
for (const w of resolvedWeights) existing.weights.add(w);
|
||||
if (italic) existing.italic = true;
|
||||
} else {
|
||||
map.set(name, { name, weights: new Set(resolvedWeights), italic });
|
||||
}
|
||||
}
|
||||
function formatMergedEntry(entry) {
|
||||
const sortedWeights = [...entry.weights].sort((a, b) => a - b);
|
||||
const features = [];
|
||||
if (entry.italic) {
|
||||
features.push("ital");
|
||||
}
|
||||
if (sortedWeights.length > 1) {
|
||||
const weightSpec = entry.italic ? sortedWeights.flatMap((w) => [`0,${w}`, `1,${w}`]).sort().join(";") : sortedWeights.join(";");
|
||||
features.push(`wght@${weightSpec}`);
|
||||
}
|
||||
if (features.length > 0) {
|
||||
return `${entry.name}:${features.join(",")}`;
|
||||
}
|
||||
return entry.name;
|
||||
}
|
||||
function googleFontHref(fonts) {
|
||||
const merged = /* @__PURE__ */ new Map();
|
||||
mergeInto(merged, "header", fonts.header);
|
||||
mergeInto(merged, "body", fonts.body);
|
||||
mergeInto(merged, "code", fonts.code);
|
||||
if (fonts.title) {
|
||||
mergeInto(merged, "title", fonts.title);
|
||||
}
|
||||
const families = [...merged.values()].map(formatMergedEntry);
|
||||
const params = families.map((f) => `family=${encodeURIComponent(f)}`).join("&");
|
||||
return `https://fonts.googleapis.com/css2?${params}&display=swap`;
|
||||
}
|
||||
|
||||
// src/util/process-fonts.ts
|
||||
var fontMimeMap = {
|
||||
truetype: "ttf",
|
||||
woff: "woff",
|
||||
woff2: "woff2",
|
||||
opentype: "otf"
|
||||
};
|
||||
var fontUrlPattern = /url\((https:\/\/fonts.gstatic.com\/.+(?:\/|(?:kit=))(.+?)[.&].+?)\)\sformat\('(\w+?)'\);/g;
|
||||
function processGoogleFonts(stylesheet, baseUrl) {
|
||||
const fontFiles = [];
|
||||
const processedStylesheet = stylesheet.replace(
|
||||
fontUrlPattern,
|
||||
(match, url, filename, format) => {
|
||||
const extension = fontMimeMap[format] ?? format;
|
||||
const rewrittenUrl = `https://${baseUrl}/static/fonts/${filename}.${extension}`;
|
||||
fontFiles.push({ url, filename, extension });
|
||||
return match.replace(url, rewrittenUrl);
|
||||
}
|
||||
);
|
||||
return { processedStylesheet, fontFiles };
|
||||
}
|
||||
|
||||
// src/emitter.ts
|
||||
var QUARTZ_DEFAULT_HEADER = "Schibsted Grotesk";
|
||||
var QUARTZ_DEFAULT_BODY = "Source Sans Pro";
|
||||
var QUARTZ_DEFAULT_CODE = "IBM Plex Mono";
|
||||
var defaultOptions = {
|
||||
useThemeFonts: true,
|
||||
fontOrigin: "googleFonts"
|
||||
};
|
||||
var QuartzFontsEmitter = (userOptions) => {
|
||||
const options = { ...defaultOptions, ...userOptions };
|
||||
return {
|
||||
name: "QuartzFontsEmitter",
|
||||
async *emit(ctx, _content, _resources) {
|
||||
if (options.fontOrigin !== "selfHosted") {
|
||||
return;
|
||||
}
|
||||
const baseUrl = ctx.cfg.configuration.baseUrl;
|
||||
if (!baseUrl) {
|
||||
throw new Error("[QuartzFontsEmitter] baseUrl is required for selfHosted fonts.");
|
||||
}
|
||||
const headerSpec = options.header ?? QUARTZ_DEFAULT_HEADER;
|
||||
const bodySpec = options.body ?? QUARTZ_DEFAULT_BODY;
|
||||
const codeSpec = options.code ?? QUARTZ_DEFAULT_CODE;
|
||||
const href = googleFontHref({
|
||||
title: options.title,
|
||||
header: headerSpec,
|
||||
body: bodySpec,
|
||||
code: codeSpec
|
||||
});
|
||||
const cssResponse = await fetch(href);
|
||||
if (!cssResponse.ok) {
|
||||
throw new Error(
|
||||
`[QuartzFontsEmitter] Failed to fetch Google Fonts CSS: ${cssResponse.status} ${cssResponse.statusText}`
|
||||
);
|
||||
}
|
||||
const cssText = await cssResponse.text();
|
||||
const { processedStylesheet, fontFiles } = processGoogleFonts(cssText, baseUrl);
|
||||
const fontsDir = path.join(ctx.argv.output, "static", "fonts");
|
||||
await fs.promises.mkdir(fontsDir, { recursive: true });
|
||||
for (const fontFile of fontFiles) {
|
||||
const fontResponse = await fetch(fontFile.url);
|
||||
if (!fontResponse.ok) {
|
||||
throw new Error(
|
||||
`[QuartzFontsEmitter] Failed to fetch font file: ${fontFile.url} (${fontResponse.status} ${fontResponse.statusText})`
|
||||
);
|
||||
}
|
||||
const buf = await fontResponse.arrayBuffer();
|
||||
const filePath = path.join(fontsDir, `${fontFile.filename}.${fontFile.extension}`);
|
||||
await fs.promises.writeFile(filePath, Buffer.from(buf));
|
||||
yield filePath;
|
||||
}
|
||||
const cssPath = path.join(fontsDir, "quartz-fonts.css");
|
||||
await fs.promises.writeFile(cssPath, processedStylesheet);
|
||||
yield cssPath;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export { QuartzFontsEmitter };
|
||||
//# sourceMappingURL=emitter.js.map
|
||||
//# sourceMappingURL=emitter.js.map
|
||||
1
dist/emitter.js.map
vendored
Normal file
1
dist/emitter.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5
dist/index.d.ts
vendored
5
dist/index.d.ts
vendored
|
|
@ -1,7 +1,8 @@
|
|||
import { QuartzTransformerPlugin } from '@quartz-community/types';
|
||||
export { QuartzTransformerPlugin } from '@quartz-community/types';
|
||||
export { QuartzEmitterPlugin, QuartzTransformerPlugin } from '@quartz-community/types';
|
||||
import { QuartzFontsOptions } from './types.js';
|
||||
export { FontFileEntry, FontSpecification, QuartzFontRegistry } from './types.js';
|
||||
export { FontFileEntry, FontSpecification, GoogleFontFile, ProcessedFontResult, QuartzFontRegistry } from './types.js';
|
||||
export { QuartzFontsEmitter } from './emitter.js';
|
||||
|
||||
declare const QuartzFonts: QuartzTransformerPlugin<Partial<QuartzFontsOptions>>;
|
||||
|
||||
|
|
|
|||
91
dist/index.js
vendored
91
dist/index.js
vendored
|
|
@ -1,4 +1,6 @@
|
|||
import { createRequire } from 'module';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const require$1 = createRequire(import.meta.url);
|
||||
var __require = /* @__PURE__ */ ((x2) => typeof require$1 !== "undefined" ? require$1 : typeof Proxy !== "undefined" ? new Proxy(x2, {
|
||||
|
|
@ -316,12 +318,97 @@ var QuartzFonts = (userOptions) => {
|
|||
{ content: buildLayeredCSS(fonts), inline: true },
|
||||
{ content: buildUnlayeredCSS(fonts), inline: true }
|
||||
];
|
||||
const additionalHead = options.fontOrigin === "googleFonts" ? buildGoogleFontsHead(options) : [];
|
||||
let additionalHead = [];
|
||||
if (options.fontOrigin === "googleFonts") {
|
||||
additionalHead = buildGoogleFontsHead(options);
|
||||
} else if (options.fontOrigin === "selfHosted") {
|
||||
additionalHead = [_("link", { rel: "stylesheet", href: "/static/fonts/quartz-fonts.css" })];
|
||||
}
|
||||
return { css, js: [], additionalHead };
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export { QuartzFonts };
|
||||
// src/util/process-fonts.ts
|
||||
var fontMimeMap = {
|
||||
truetype: "ttf",
|
||||
woff: "woff",
|
||||
woff2: "woff2",
|
||||
opentype: "otf"
|
||||
};
|
||||
var fontUrlPattern = /url\((https:\/\/fonts.gstatic.com\/.+(?:\/|(?:kit=))(.+?)[.&].+?)\)\sformat\('(\w+?)'\);/g;
|
||||
function processGoogleFonts(stylesheet, baseUrl) {
|
||||
const fontFiles = [];
|
||||
const processedStylesheet = stylesheet.replace(
|
||||
fontUrlPattern,
|
||||
(match, url, filename, format) => {
|
||||
const extension = fontMimeMap[format] ?? format;
|
||||
const rewrittenUrl = `https://${baseUrl}/static/fonts/${filename}.${extension}`;
|
||||
fontFiles.push({ url, filename, extension });
|
||||
return match.replace(url, rewrittenUrl);
|
||||
}
|
||||
);
|
||||
return { processedStylesheet, fontFiles };
|
||||
}
|
||||
|
||||
// src/emitter.ts
|
||||
var QUARTZ_DEFAULT_HEADER2 = "Schibsted Grotesk";
|
||||
var QUARTZ_DEFAULT_BODY2 = "Source Sans Pro";
|
||||
var QUARTZ_DEFAULT_CODE2 = "IBM Plex Mono";
|
||||
var defaultOptions2 = {
|
||||
useThemeFonts: true,
|
||||
fontOrigin: "googleFonts"
|
||||
};
|
||||
var QuartzFontsEmitter = (userOptions) => {
|
||||
const options = { ...defaultOptions2, ...userOptions };
|
||||
return {
|
||||
name: "QuartzFontsEmitter",
|
||||
async *emit(ctx, _content, _resources) {
|
||||
if (options.fontOrigin !== "selfHosted") {
|
||||
return;
|
||||
}
|
||||
const baseUrl = ctx.cfg.configuration.baseUrl;
|
||||
if (!baseUrl) {
|
||||
throw new Error("[QuartzFontsEmitter] baseUrl is required for selfHosted fonts.");
|
||||
}
|
||||
const headerSpec = options.header ?? QUARTZ_DEFAULT_HEADER2;
|
||||
const bodySpec = options.body ?? QUARTZ_DEFAULT_BODY2;
|
||||
const codeSpec = options.code ?? QUARTZ_DEFAULT_CODE2;
|
||||
const href = googleFontHref({
|
||||
title: options.title,
|
||||
header: headerSpec,
|
||||
body: bodySpec,
|
||||
code: codeSpec
|
||||
});
|
||||
const cssResponse = await fetch(href);
|
||||
if (!cssResponse.ok) {
|
||||
throw new Error(
|
||||
`[QuartzFontsEmitter] Failed to fetch Google Fonts CSS: ${cssResponse.status} ${cssResponse.statusText}`
|
||||
);
|
||||
}
|
||||
const cssText = await cssResponse.text();
|
||||
const { processedStylesheet, fontFiles } = processGoogleFonts(cssText, baseUrl);
|
||||
const fontsDir = path.join(ctx.argv.output, "static", "fonts");
|
||||
await fs.promises.mkdir(fontsDir, { recursive: true });
|
||||
for (const fontFile of fontFiles) {
|
||||
const fontResponse = await fetch(fontFile.url);
|
||||
if (!fontResponse.ok) {
|
||||
throw new Error(
|
||||
`[QuartzFontsEmitter] Failed to fetch font file: ${fontFile.url} (${fontResponse.status} ${fontResponse.statusText})`
|
||||
);
|
||||
}
|
||||
const buf = await fontResponse.arrayBuffer();
|
||||
const filePath = path.join(fontsDir, `${fontFile.filename}.${fontFile.extension}`);
|
||||
await fs.promises.writeFile(filePath, Buffer.from(buf));
|
||||
yield filePath;
|
||||
}
|
||||
const cssPath = path.join(fontsDir, "quartz-fonts.css");
|
||||
await fs.promises.writeFile(cssPath, processedStylesheet);
|
||||
yield cssPath;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export { QuartzFonts, QuartzFontsEmitter };
|
||||
//# sourceMappingURL=index.js.map
|
||||
//# sourceMappingURL=index.js.map
|
||||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
14
dist/types.d.ts
vendored
14
dist/types.d.ts
vendored
|
|
@ -26,9 +26,19 @@ interface QuartzFontsOptions {
|
|||
* Where to load fonts from.
|
||||
* - `"googleFonts"`: generate Google Fonts `<link>` tags.
|
||||
* - `"local"`: assume fonts are available locally (no loading).
|
||||
* - `"selfHosted"`: download Google Fonts and serve from the build output.
|
||||
* Defaults to `"local"` (no automatic font loading).
|
||||
*/
|
||||
fontOrigin?: "googleFonts" | "local";
|
||||
fontOrigin?: "googleFonts" | "local" | "selfHosted";
|
||||
}
|
||||
interface ProcessedFontResult {
|
||||
processedStylesheet: string;
|
||||
fontFiles: GoogleFontFile[];
|
||||
}
|
||||
interface GoogleFontFile {
|
||||
url: string;
|
||||
filename: string;
|
||||
extension: string;
|
||||
}
|
||||
interface QuartzFontRegistry {
|
||||
themeName: string;
|
||||
|
|
@ -45,4 +55,4 @@ interface FontFileEntry {
|
|||
unicodeRange?: string | null;
|
||||
}
|
||||
|
||||
export type { FontFileEntry, FontSpecification, QuartzFontRegistry, QuartzFontsOptions };
|
||||
export type { FontFileEntry, FontSpecification, GoogleFontFile, ProcessedFontResult, QuartzFontRegistry, QuartzFontsOptions };
|
||||
|
|
|
|||
|
|
@ -88,7 +88,10 @@
|
|||
"quartz": {
|
||||
"name": "quartz-fonts",
|
||||
"displayName": "Quartz Fonts",
|
||||
"category": "transformer",
|
||||
"category": [
|
||||
"transformer",
|
||||
"emitter"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"quartzVersion": ">=5.0.0",
|
||||
"dependencies": [],
|
||||
|
|
@ -403,7 +406,8 @@
|
|||
"type": "string",
|
||||
"enum": [
|
||||
"googleFonts",
|
||||
"local"
|
||||
"local",
|
||||
"selfHosted"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
76
src/emitter.ts
Normal file
76
src/emitter.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import type { QuartzEmitterPlugin, BuildCtx, FilePath } from "@quartz-community/types";
|
||||
import type { QuartzFontsOptions } from "./types";
|
||||
import { googleFontHref } from "./util/google-fonts";
|
||||
import { processGoogleFonts } from "./util/process-fonts";
|
||||
|
||||
const QUARTZ_DEFAULT_HEADER = "Schibsted Grotesk";
|
||||
const QUARTZ_DEFAULT_BODY = "Source Sans Pro";
|
||||
const QUARTZ_DEFAULT_CODE = "IBM Plex Mono";
|
||||
|
||||
const defaultOptions: QuartzFontsOptions = {
|
||||
useThemeFonts: true,
|
||||
fontOrigin: "googleFonts",
|
||||
};
|
||||
|
||||
export const QuartzFontsEmitter: QuartzEmitterPlugin<Partial<QuartzFontsOptions>> = (
|
||||
userOptions?: Partial<QuartzFontsOptions>,
|
||||
) => {
|
||||
const options: QuartzFontsOptions = { ...defaultOptions, ...userOptions };
|
||||
|
||||
return {
|
||||
name: "QuartzFontsEmitter",
|
||||
async *emit(ctx: BuildCtx, _content: unknown[], _resources: unknown): AsyncGenerator<FilePath> {
|
||||
if (options.fontOrigin !== "selfHosted") {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = ctx.cfg.configuration.baseUrl;
|
||||
if (!baseUrl) {
|
||||
throw new Error("[QuartzFontsEmitter] baseUrl is required for selfHosted fonts.");
|
||||
}
|
||||
|
||||
const headerSpec = options.header ?? QUARTZ_DEFAULT_HEADER;
|
||||
const bodySpec = options.body ?? QUARTZ_DEFAULT_BODY;
|
||||
const codeSpec = options.code ?? QUARTZ_DEFAULT_CODE;
|
||||
|
||||
const href = googleFontHref({
|
||||
title: options.title,
|
||||
header: headerSpec,
|
||||
body: bodySpec,
|
||||
code: codeSpec,
|
||||
});
|
||||
|
||||
const cssResponse = await fetch(href);
|
||||
if (!cssResponse.ok) {
|
||||
throw new Error(
|
||||
`[QuartzFontsEmitter] Failed to fetch Google Fonts CSS: ${cssResponse.status} ${cssResponse.statusText}`,
|
||||
);
|
||||
}
|
||||
const cssText = await cssResponse.text();
|
||||
|
||||
const { processedStylesheet, fontFiles } = processGoogleFonts(cssText, baseUrl);
|
||||
|
||||
const fontsDir = path.join(ctx.argv.output, "static", "fonts");
|
||||
await fs.promises.mkdir(fontsDir, { recursive: true });
|
||||
|
||||
for (const fontFile of fontFiles) {
|
||||
const fontResponse = await fetch(fontFile.url);
|
||||
if (!fontResponse.ok) {
|
||||
throw new Error(
|
||||
`[QuartzFontsEmitter] Failed to fetch font file: ${fontFile.url} (${fontResponse.status} ${fontResponse.statusText})`,
|
||||
);
|
||||
}
|
||||
const buf = await fontResponse.arrayBuffer();
|
||||
const filePath = path.join(fontsDir, `${fontFile.filename}.${fontFile.extension}`);
|
||||
await fs.promises.writeFile(filePath, Buffer.from(buf));
|
||||
yield filePath as FilePath;
|
||||
}
|
||||
|
||||
const cssPath = path.join(fontsDir, "quartz-fonts.css");
|
||||
await fs.promises.writeFile(cssPath, processedStylesheet);
|
||||
yield cssPath as FilePath;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
export { QuartzFonts } from "./transformer";
|
||||
export { QuartzFontsEmitter } from "./emitter";
|
||||
|
||||
export type {
|
||||
FontSpecification,
|
||||
QuartzFontsOptions,
|
||||
QuartzFontRegistry,
|
||||
FontFileEntry,
|
||||
ProcessedFontResult,
|
||||
GoogleFontFile,
|
||||
} from "./types";
|
||||
|
||||
export type { QuartzTransformerPlugin } from "@quartz-community/types";
|
||||
export type { QuartzTransformerPlugin, QuartzEmitterPlugin } from "@quartz-community/types";
|
||||
|
|
|
|||
|
|
@ -212,8 +212,12 @@ export const QuartzFonts: QuartzTransformerPlugin<Partial<QuartzFontsOptions>> =
|
|||
{ content: buildUnlayeredCSS(fonts), inline: true },
|
||||
];
|
||||
|
||||
const additionalHead: unknown[] =
|
||||
options.fontOrigin === "googleFonts" ? buildGoogleFontsHead(options) : [];
|
||||
let additionalHead: unknown[] = [];
|
||||
if (options.fontOrigin === "googleFonts") {
|
||||
additionalHead = buildGoogleFontsHead(options);
|
||||
} else if (options.fontOrigin === "selfHosted") {
|
||||
additionalHead = [h("link", { rel: "stylesheet", href: "/static/fonts/quartz-fonts.css" })];
|
||||
}
|
||||
|
||||
return { css, js: [], additionalHead };
|
||||
},
|
||||
|
|
|
|||
14
src/types.ts
14
src/types.ts
|
|
@ -36,9 +36,21 @@ export interface QuartzFontsOptions {
|
|||
* Where to load fonts from.
|
||||
* - `"googleFonts"`: generate Google Fonts `<link>` tags.
|
||||
* - `"local"`: assume fonts are available locally (no loading).
|
||||
* - `"selfHosted"`: download Google Fonts and serve from the build output.
|
||||
* Defaults to `"local"` (no automatic font loading).
|
||||
*/
|
||||
fontOrigin?: "googleFonts" | "local";
|
||||
fontOrigin?: "googleFonts" | "local" | "selfHosted";
|
||||
}
|
||||
|
||||
export interface ProcessedFontResult {
|
||||
processedStylesheet: string;
|
||||
fontFiles: GoogleFontFile[];
|
||||
}
|
||||
|
||||
export interface GoogleFontFile {
|
||||
url: string;
|
||||
filename: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
export interface QuartzFontRegistry {
|
||||
|
|
|
|||
27
src/util/process-fonts.ts
Normal file
27
src/util/process-fonts.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { ProcessedFontResult, GoogleFontFile } from "../types";
|
||||
|
||||
const fontMimeMap: Record<string, string> = {
|
||||
truetype: "ttf",
|
||||
woff: "woff",
|
||||
woff2: "woff2",
|
||||
opentype: "otf",
|
||||
};
|
||||
|
||||
const fontUrlPattern =
|
||||
/url\((https:\/\/fonts.gstatic.com\/.+(?:\/|(?:kit=))(.+?)[.&].+?)\)\sformat\('(\w+?)'\);/g;
|
||||
|
||||
export function processGoogleFonts(stylesheet: string, baseUrl: string): ProcessedFontResult {
|
||||
const fontFiles: GoogleFontFile[] = [];
|
||||
|
||||
const processedStylesheet = stylesheet.replace(
|
||||
fontUrlPattern,
|
||||
(match, url: string, filename: string, format: string) => {
|
||||
const extension = fontMimeMap[format] ?? format;
|
||||
const rewrittenUrl = `https://${baseUrl}/static/fonts/${filename}.${extension}`;
|
||||
fontFiles.push({ url, filename, extension });
|
||||
return match.replace(url, rewrittenUrl);
|
||||
},
|
||||
);
|
||||
|
||||
return { processedStylesheet, fontFiles };
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import type { BuildCtx } from "@quartz-community/types";
|
|||
import { QuartzFonts } from "../src/transformer";
|
||||
|
||||
import { formatFontSpecification, googleFontHref, getFontName } from "../src/util/google-fonts";
|
||||
import { processGoogleFonts } from "../src/util/process-fonts";
|
||||
import { validateFontSpec } from "../src/util/validate";
|
||||
|
||||
const REGISTRY_KEY = "__quartzFonts";
|
||||
|
|
@ -200,6 +201,39 @@ describe("QuartzFonts", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("selfHosted fontOrigin", () => {
|
||||
it("injects link to self-hosted CSS file instead of Google Fonts", () => {
|
||||
const head = getAdditionalHead(
|
||||
QuartzFonts({
|
||||
fontOrigin: "selfHosted",
|
||||
body: "Inter",
|
||||
header: "Playfair Display",
|
||||
code: "JetBrains Mono",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(head).toHaveLength(1);
|
||||
const link = head[0] as { props: Record<string, string> };
|
||||
expect(link.props.rel).toBe("stylesheet");
|
||||
expect(link.props.href).toBe("/static/fonts/quartz-fonts.css");
|
||||
});
|
||||
|
||||
it("still emits layered CSS with font variables", () => {
|
||||
const css = getCSSContent(
|
||||
QuartzFonts({
|
||||
fontOrigin: "selfHosted",
|
||||
body: "Inter",
|
||||
header: "Playfair Display",
|
||||
code: "JetBrains Mono",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(css).toContain("--bodyFont: Inter");
|
||||
expect(css).toContain("--headerFont: Playfair Display");
|
||||
expect(css).toContain("--codeFont: JetBrains Mono");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with QuartzTheme registry", () => {
|
||||
beforeEach(() => {
|
||||
setRegistry({
|
||||
|
|
@ -380,3 +414,44 @@ describe("validateFontSpec", () => {
|
|||
expect(Array.isArray(warnings)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("processGoogleFonts", () => {
|
||||
it("rewrites font URLs to self-hosted paths", () => {
|
||||
const css = `
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/abc123def456.woff2) format('woff2');
|
||||
}`;
|
||||
const { processedStylesheet, fontFiles } = processGoogleFonts(css, "example.com");
|
||||
|
||||
expect(processedStylesheet).toContain("https://example.com/static/fonts/abc123def456.woff2");
|
||||
expect(processedStylesheet).not.toContain("fonts.gstatic.com");
|
||||
expect(fontFiles).toHaveLength(1);
|
||||
const [first] = fontFiles;
|
||||
expect(first).toBeDefined();
|
||||
if (!first) {
|
||||
throw new Error("Expected at least one font file");
|
||||
}
|
||||
expect(first.filename).toBe("abc123def456");
|
||||
expect(first.extension).toBe("woff2");
|
||||
});
|
||||
|
||||
it("handles multiple font faces", () => {
|
||||
const css = `
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/abc123.woff2) format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/def456.woff) format('woff');
|
||||
}`;
|
||||
const { fontFiles } = processGoogleFonts(css, "example.com");
|
||||
expect(fontFiles).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("returns empty fontFiles for CSS without font URLs", () => {
|
||||
const { fontFiles } = processGoogleFonts("body { color: red; }", "example.com");
|
||||
expect(fontFiles).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export default defineConfig({
|
|||
entry: {
|
||||
index: "src/index.ts",
|
||||
types: "src/types.ts",
|
||||
emitter: "src/emitter.ts",
|
||||
},
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue