@quartz-community/quartz-fonts 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - Initial Quartz community plugin template.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quartz Community
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # @quartz-community/fonts
2
+
3
+ Fine-grained font control for Quartz sites. Supports per-heading fonts, automatic theme font discovery via [QuartzTheme](https://github.com/saberzero1/quartz-themes), and Obsidian-compatible defaults.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx quartz plugin add github:quartz-community/fonts
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```yaml title="quartz.config.yaml"
14
+ plugins:
15
+ - source: github:quartz-community/fonts
16
+ enabled: true
17
+ ```
18
+
19
+ With custom fonts:
20
+
21
+ ```yaml title="quartz.config.yaml"
22
+ plugins:
23
+ - source: github:quartz-community/fonts
24
+ enabled: true
25
+ options:
26
+ body: '"Inter", sans-serif'
27
+ header: '"Playfair Display", serif'
28
+ code: '"JetBrains Mono", monospace'
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ Font options accept either a CSS font-family string or an object with Google Fonts loading control:
34
+
35
+ ```yaml
36
+ # String form
37
+ body: '"Inter", sans-serif'
38
+
39
+ # Object form (for Google Fonts weight/italic control)
40
+ body:
41
+ name: Inter
42
+ weights: [400, 600, 700]
43
+ includeItalic: true
44
+ ```
45
+
46
+ | Option | Type | Default | Description |
47
+ | --------------- | ------------------- | ---------------- | ------------------------------------------------------------------------- |
48
+ | `title` | `FontSpecification` | `header` value | Font family for the site title. |
49
+ | `body` | `FontSpecification` | Obsidian default | Font family for body text. |
50
+ | `header` | `FontSpecification` | Obsidian default | Default font family for all headings (h1-h6). |
51
+ | `code` | `FontSpecification` | Obsidian default | Font family for code and monospace elements. |
52
+ | `interface` | `FontSpecification` | Obsidian default | Font family for UI elements. |
53
+ | `h1` | `FontSpecification` | `header` value | Font family for h1 headings. |
54
+ | `h2` | `FontSpecification` | `header` value | Font family for h2 headings. |
55
+ | `h3` | `FontSpecification` | `header` value | Font family for h3 headings. |
56
+ | `h4` | `FontSpecification` | `header` value | Font family for h4 headings. |
57
+ | `h5` | `FontSpecification` | `header` value | Font family for h5 headings. |
58
+ | `h6` | `FontSpecification` | `header` value | Font family for h6 headings. |
59
+ | `useThemeFonts` | `boolean` | `true` | Use fonts from QuartzTheme as defaults when it is installed. |
60
+ | `fontOrigin` | `string` | `"local"` | `"googleFonts"` to auto-load from Google Fonts, `"local"` for no loading. |
61
+
62
+ ### Default options
63
+
64
+ ```yaml title="quartz.config.yaml"
65
+ - source: github:quartz-community/fonts
66
+ enabled: true
67
+ options:
68
+ useThemeFonts: true
69
+ fontOrigin: local
70
+ ```
71
+
72
+ ## How it works
73
+
74
+ Fonts resolves fonts using a priority chain:
75
+
76
+ ```
77
+ User config (plugin options)
78
+ -> Theme fonts (from QuartzTheme, if installed)
79
+ -> Obsidian defaults (system font stacks)
80
+ ```
81
+
82
+ For individual headings, the resolution is:
83
+
84
+ ```
85
+ h1 option -> header option -> theme --h1-font -> theme font -> Obsidian default
86
+ ```
87
+
88
+ For the site title:
89
+
90
+ ```
91
+ title option -> header option -> theme font -> Obsidian default
92
+ ```
93
+
94
+ ### With QuartzTheme
95
+
96
+ When [QuartzTheme](https://github.com/saberzero1/quartz-themes) is installed and runs before Fonts, theme fonts are automatically discovered and used as defaults. Any options you set in Fonts will override the theme fonts.
97
+
98
+ Fonts must run after QuartzTheme. This is handled automatically by `defaultOrder` (QuartzTheme = 50, Fonts = 60).
99
+
100
+ ### Without QuartzTheme
101
+
102
+ Fonts works standalone. Without a theme, it falls back to Obsidian's default system font stacks.
103
+
104
+ ## Examples
105
+
106
+ ```yaml title="quartz.config.yaml"
107
+ # Use theme fonts automatically (default behavior)
108
+ - source: github:quartz-community/fonts
109
+ enabled: true
110
+
111
+ # Override just the heading font
112
+ - source: github:quartz-community/fonts
113
+ enabled: true
114
+ options:
115
+ header: '"Playfair Display", serif'
116
+
117
+ # Full control with per-heading fonts
118
+ - source: github:quartz-community/fonts
119
+ enabled: true
120
+ options:
121
+ body: '"Inter", sans-serif'
122
+ header: '"Playfair Display", serif'
123
+ code: '"JetBrains Mono", monospace'
124
+ h1: '"Playfair Display", serif'
125
+ h2: '"Lora", serif'
126
+
127
+ # Load from Google Fonts automatically
128
+ - source: github:quartz-community/fonts
129
+ enabled: true
130
+ options:
131
+ fontOrigin: googleFonts
132
+ body: Inter
133
+ header: Playfair Display
134
+ code: JetBrains Mono
135
+
136
+ # Google Fonts with weight/italic control
137
+ - source: github:quartz-community/fonts
138
+ enabled: true
139
+ options:
140
+ fontOrigin: googleFonts
141
+ body:
142
+ name: Inter
143
+ weights: [400, 600, 700]
144
+ includeItalic: true
145
+ header:
146
+ name: Playfair Display
147
+ weights: [400, 700]
148
+ code:
149
+ name: JetBrains Mono
150
+ weights: [400]
151
+
152
+ # Custom title font (separate from header)
153
+ - source: github:quartz-community/fonts
154
+ enabled: true
155
+ options:
156
+ fontOrigin: googleFonts
157
+ title: Abril Fatface
158
+ header: Playfair Display
159
+ body: Inter
160
+ code: JetBrains Mono
161
+
162
+ # Ignore theme fonts entirely
163
+ - source: github:quartz-community/fonts
164
+ enabled: true
165
+ options:
166
+ useThemeFonts: false
167
+ body: '"Inter", sans-serif'
168
+ ```
169
+
170
+ ## Google Fonts Validation
171
+
172
+ When `fontOrigin: googleFonts` is set and the optional [`google-font-metadata`](https://www.npmjs.com/package/google-font-metadata) package is installed, Fonts validates your font configuration at build time:
173
+
174
+ - Checks that font family names exist in Google Fonts.
175
+ - Warns if requested weights are not available for a font.
176
+ - Warns if italic is requested but the font doesn't support it.
177
+
178
+ Install it to enable validation:
179
+
180
+ ```bash
181
+ npm install google-font-metadata
182
+ ```
183
+
184
+ Validation warnings are logged to the console but do not block the build.
185
+
186
+ ## Documentation
187
+
188
+ See the [Quartz documentation](https://quartz.jzhao.xyz/plugins/Fonts) for more information.
189
+
190
+ ## License
191
+
192
+ MIT
@@ -0,0 +1,6 @@
1
+ import { QuartzEmitterPlugin } from '@quartz-community/types';
2
+ import { FontsOptions } from './types.js';
3
+
4
+ declare const FontsEmitter: QuartzEmitterPlugin<Partial<FontsOptions>>;
5
+
6
+ export { FontsEmitter };
@@ -0,0 +1,147 @@
1
+ import { createRequire } from 'module';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ createRequire(import.meta.url);
6
+
7
+ // src/defaults.ts
8
+ var DEFAULT_WEIGHTS = {
9
+ title: [400, 700],
10
+ header: [400, 700],
11
+ body: [400, 600],
12
+ code: [400, 600]
13
+ };
14
+ var DEFAULT_ITALIC = {
15
+ title: false,
16
+ header: false,
17
+ body: true,
18
+ code: false
19
+ };
20
+
21
+ // src/util/google-fonts.ts
22
+ function normalizeFontSpec(spec) {
23
+ return typeof spec === "string" ? { name: spec } : spec;
24
+ }
25
+ function mergeInto(map, role, spec) {
26
+ const { name, weights, includeItalic } = normalizeFontSpec(spec);
27
+ const resolvedWeights = weights ?? DEFAULT_WEIGHTS[role];
28
+ const italic = includeItalic ?? DEFAULT_ITALIC[role];
29
+ const existing = map.get(name);
30
+ if (existing) {
31
+ for (const w of resolvedWeights) existing.weights.add(w);
32
+ if (italic) existing.italic = true;
33
+ } else {
34
+ map.set(name, { name, weights: new Set(resolvedWeights), italic });
35
+ }
36
+ }
37
+ function formatMergedEntry(entry) {
38
+ const sortedWeights = [...entry.weights].sort((a, b) => a - b);
39
+ const features = [];
40
+ if (entry.italic) {
41
+ features.push("ital");
42
+ }
43
+ if (sortedWeights.length > 1) {
44
+ const weightSpec = entry.italic ? sortedWeights.flatMap((w) => [`0,${w}`, `1,${w}`]).sort().join(";") : sortedWeights.join(";");
45
+ features.push(`wght@${weightSpec}`);
46
+ }
47
+ if (features.length > 0) {
48
+ return `${entry.name}:${features.join(",")}`;
49
+ }
50
+ return entry.name;
51
+ }
52
+ function googleFontHref(fonts) {
53
+ const merged = /* @__PURE__ */ new Map();
54
+ mergeInto(merged, "header", fonts.header);
55
+ mergeInto(merged, "body", fonts.body);
56
+ mergeInto(merged, "code", fonts.code);
57
+ if (fonts.title) {
58
+ mergeInto(merged, "title", fonts.title);
59
+ }
60
+ const families = [...merged.values()].map(formatMergedEntry);
61
+ const params = families.map((f) => `family=${encodeURIComponent(f)}`).join("&");
62
+ return `https://fonts.googleapis.com/css2?${params}&display=swap`;
63
+ }
64
+
65
+ // src/util/process-fonts.ts
66
+ var fontMimeMap = {
67
+ truetype: "ttf",
68
+ woff: "woff",
69
+ woff2: "woff2",
70
+ opentype: "otf"
71
+ };
72
+ var fontUrlPattern = /url\((https:\/\/fonts.gstatic.com\/.+(?:\/|(?:kit=))(.+?)[.&].+?)\)\sformat\('(\w+?)'\);/g;
73
+ function processGoogleFonts(stylesheet, baseUrl) {
74
+ const fontFiles = [];
75
+ const processedStylesheet = stylesheet.replace(
76
+ fontUrlPattern,
77
+ (match, url, filename, format) => {
78
+ const extension = fontMimeMap[format] ?? format;
79
+ const rewrittenUrl = `https://${baseUrl}/static/fonts/${filename}.${extension}`;
80
+ fontFiles.push({ url, filename, extension });
81
+ return match.replace(url, rewrittenUrl);
82
+ }
83
+ );
84
+ return { processedStylesheet, fontFiles };
85
+ }
86
+
87
+ // src/emitter.ts
88
+ var QUARTZ_DEFAULT_HEADER = "Schibsted Grotesk";
89
+ var QUARTZ_DEFAULT_BODY = "Source Sans Pro";
90
+ var QUARTZ_DEFAULT_CODE = "IBM Plex Mono";
91
+ var defaultOptions = {
92
+ useThemeFonts: true,
93
+ fontOrigin: "googleFonts"
94
+ };
95
+ var FontsEmitter = (userOptions) => {
96
+ const options = { ...defaultOptions, ...userOptions };
97
+ return {
98
+ name: "FontsEmitter",
99
+ async *emit(ctx, _content, _resources) {
100
+ if (options.fontOrigin !== "selfHosted") {
101
+ return;
102
+ }
103
+ const baseUrl = ctx.cfg.configuration.baseUrl;
104
+ if (!baseUrl) {
105
+ throw new Error("[FontsEmitter] baseUrl is required for selfHosted fonts.");
106
+ }
107
+ const headerSpec = options.header ?? QUARTZ_DEFAULT_HEADER;
108
+ const bodySpec = options.body ?? QUARTZ_DEFAULT_BODY;
109
+ const codeSpec = options.code ?? QUARTZ_DEFAULT_CODE;
110
+ const href = googleFontHref({
111
+ title: options.title,
112
+ header: headerSpec,
113
+ body: bodySpec,
114
+ code: codeSpec
115
+ });
116
+ const cssResponse = await fetch(href);
117
+ if (!cssResponse.ok) {
118
+ throw new Error(
119
+ `[FontsEmitter] Failed to fetch Google Fonts CSS: ${cssResponse.status} ${cssResponse.statusText}`
120
+ );
121
+ }
122
+ const cssText = await cssResponse.text();
123
+ const { processedStylesheet, fontFiles } = processGoogleFonts(cssText, baseUrl);
124
+ const fontsDir = path.join(ctx.argv.output, "static", "fonts");
125
+ await fs.promises.mkdir(fontsDir, { recursive: true });
126
+ for (const fontFile of fontFiles) {
127
+ const fontResponse = await fetch(fontFile.url);
128
+ if (!fontResponse.ok) {
129
+ throw new Error(
130
+ `[FontsEmitter] Failed to fetch font file: ${fontFile.url} (${fontResponse.status} ${fontResponse.statusText})`
131
+ );
132
+ }
133
+ const buf = await fontResponse.arrayBuffer();
134
+ const filePath = path.join(fontsDir, `${fontFile.filename}.${fontFile.extension}`);
135
+ await fs.promises.writeFile(filePath, Buffer.from(buf));
136
+ yield filePath;
137
+ }
138
+ const cssPath = path.join(fontsDir, "quartz-fonts.css");
139
+ await fs.promises.writeFile(cssPath, processedStylesheet);
140
+ yield cssPath;
141
+ }
142
+ };
143
+ };
144
+
145
+ export { FontsEmitter };
146
+ //# sourceMappingURL=emitter.js.map
147
+ //# sourceMappingURL=emitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/defaults.ts","../src/util/google-fonts.ts","../src/util/process-fonts.ts","../src/emitter.ts"],"names":[],"mappings":"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","file":"emitter.js","sourcesContent":["export const OBSIDIAN_SANS_STACK =\n 'ui-sans-serif, -apple-system, BlinkMacSystemFont, system-ui, \"Segoe UI\", Roboto, \"Inter\", \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", sans-serif';\n\nexport const OBSIDIAN_MONO_STACK =\n 'ui-monospace, SFMono-Regular, \"Cascadia Mono\", \"Roboto Mono\", \"DejaVu Sans Mono\", \"Liberation Mono\", Menlo, Monaco, \"Consolas\", \"Source Code Pro\", monospace';\n\nexport type FontRole = \"title\" | \"header\" | \"body\" | \"code\";\n\nexport const DEFAULT_WEIGHTS: Record<FontRole, number[]> = {\n title: [400, 700],\n header: [400, 700],\n body: [400, 600],\n code: [400, 600],\n};\n\nexport const DEFAULT_ITALIC: Record<FontRole, boolean> = {\n title: false,\n header: false,\n body: true,\n code: false,\n};\n","import type { FontSpecification } from \"../types\";\nimport type { FontRole } from \"../defaults\";\nimport { DEFAULT_WEIGHTS, DEFAULT_ITALIC } from \"../defaults\";\n\nfunction normalizeFontSpec(spec: FontSpecification): {\n name: string;\n weights?: number[];\n includeItalic?: boolean;\n} {\n return typeof spec === \"string\" ? { name: spec } : spec;\n}\n\nexport function getFontName(spec: FontSpecification): string {\n return typeof spec === \"string\" ? spec : spec.name;\n}\n\nexport function formatFontSpecification(role: FontRole, spec: FontSpecification): string {\n const { name, weights, includeItalic } = normalizeFontSpec(spec);\n\n const resolvedWeights = weights ?? DEFAULT_WEIGHTS[role];\n const italic = includeItalic ?? DEFAULT_ITALIC[role];\n\n const features: string[] = [];\n if (italic) {\n features.push(\"ital\");\n }\n\n if (resolvedWeights.length > 1) {\n const weightSpec = italic\n ? resolvedWeights\n .flatMap((w) => [`0,${w}`, `1,${w}`])\n .sort()\n .join(\";\")\n : resolvedWeights.join(\";\");\n\n features.push(`wght@${weightSpec}`);\n }\n\n if (features.length > 0) {\n return `${name}:${features.join(\",\")}`;\n }\n\n return name;\n}\n\ninterface MergedFontEntry {\n name: string;\n weights: Set<number>;\n italic: boolean;\n}\n\nfunction mergeInto(\n map: Map<string, MergedFontEntry>,\n role: FontRole,\n spec: FontSpecification,\n): void {\n const { name, weights, includeItalic } = normalizeFontSpec(spec);\n const resolvedWeights = weights ?? DEFAULT_WEIGHTS[role];\n const italic = includeItalic ?? DEFAULT_ITALIC[role];\n\n const existing = map.get(name);\n if (existing) {\n for (const w of resolvedWeights) existing.weights.add(w);\n if (italic) existing.italic = true;\n } else {\n map.set(name, { name, weights: new Set(resolvedWeights), italic });\n }\n}\n\nfunction formatMergedEntry(entry: MergedFontEntry): string {\n const sortedWeights = [...entry.weights].sort((a, b) => a - b);\n const features: string[] = [];\n\n if (entry.italic) {\n features.push(\"ital\");\n }\n\n if (sortedWeights.length > 1) {\n const weightSpec = entry.italic\n ? sortedWeights\n .flatMap((w) => [`0,${w}`, `1,${w}`])\n .sort()\n .join(\";\")\n : sortedWeights.join(\";\");\n features.push(`wght@${weightSpec}`);\n }\n\n if (features.length > 0) {\n return `${entry.name}:${features.join(\",\")}`;\n }\n return entry.name;\n}\n\nexport function googleFontHref(fonts: {\n title?: FontSpecification;\n header: FontSpecification;\n body: FontSpecification;\n code: FontSpecification;\n}): string {\n const merged = new Map<string, MergedFontEntry>();\n\n mergeInto(merged, \"header\", fonts.header);\n mergeInto(merged, \"body\", fonts.body);\n mergeInto(merged, \"code\", fonts.code);\n\n if (fonts.title) {\n mergeInto(merged, \"title\", fonts.title);\n }\n\n const families = [...merged.values()].map(formatMergedEntry);\n const params = families.map((f) => `family=${encodeURIComponent(f)}`).join(\"&\");\n return `https://fonts.googleapis.com/css2?${params}&display=swap`;\n}\n","import type { ProcessedFontResult, GoogleFontFile } from \"../types\";\n\nconst fontMimeMap: Record<string, string> = {\n truetype: \"ttf\",\n woff: \"woff\",\n woff2: \"woff2\",\n opentype: \"otf\",\n};\n\nconst fontUrlPattern =\n /url\\((https:\\/\\/fonts.gstatic.com\\/.+(?:\\/|(?:kit=))(.+?)[.&].+?)\\)\\sformat\\('(\\w+?)'\\);/g;\n\nexport function processGoogleFonts(stylesheet: string, baseUrl: string): ProcessedFontResult {\n const fontFiles: GoogleFontFile[] = [];\n\n const processedStylesheet = stylesheet.replace(\n fontUrlPattern,\n (match, url: string, filename: string, format: string) => {\n const extension = fontMimeMap[format] ?? format;\n const rewrittenUrl = `https://${baseUrl}/static/fonts/${filename}.${extension}`;\n fontFiles.push({ url, filename, extension });\n return match.replace(url, rewrittenUrl);\n },\n );\n\n return { processedStylesheet, fontFiles };\n}\n","import fs from \"fs\";\nimport path from \"path\";\nimport type { QuartzEmitterPlugin, BuildCtx, FilePath } from \"@quartz-community/types\";\nimport type { FontsOptions } from \"./types\";\nimport { googleFontHref } from \"./util/google-fonts\";\nimport { processGoogleFonts } from \"./util/process-fonts\";\n\nconst QUARTZ_DEFAULT_HEADER = \"Schibsted Grotesk\";\nconst QUARTZ_DEFAULT_BODY = \"Source Sans Pro\";\nconst QUARTZ_DEFAULT_CODE = \"IBM Plex Mono\";\n\nconst defaultOptions: FontsOptions = {\n useThemeFonts: true,\n fontOrigin: \"googleFonts\",\n};\n\nexport const FontsEmitter: QuartzEmitterPlugin<Partial<FontsOptions>> = (\n userOptions?: Partial<FontsOptions>,\n) => {\n const options: FontsOptions = { ...defaultOptions, ...userOptions };\n\n return {\n name: \"FontsEmitter\",\n async *emit(ctx: BuildCtx, _content: unknown[], _resources: unknown): AsyncGenerator<FilePath> {\n if (options.fontOrigin !== \"selfHosted\") {\n return;\n }\n\n const baseUrl = ctx.cfg.configuration.baseUrl;\n if (!baseUrl) {\n throw new Error(\"[FontsEmitter] baseUrl is required for selfHosted fonts.\");\n }\n\n const headerSpec = options.header ?? QUARTZ_DEFAULT_HEADER;\n const bodySpec = options.body ?? QUARTZ_DEFAULT_BODY;\n const codeSpec = options.code ?? QUARTZ_DEFAULT_CODE;\n\n const href = googleFontHref({\n title: options.title,\n header: headerSpec,\n body: bodySpec,\n code: codeSpec,\n });\n\n const cssResponse = await fetch(href);\n if (!cssResponse.ok) {\n throw new Error(\n `[FontsEmitter] Failed to fetch Google Fonts CSS: ${cssResponse.status} ${cssResponse.statusText}`,\n );\n }\n const cssText = await cssResponse.text();\n\n const { processedStylesheet, fontFiles } = processGoogleFonts(cssText, baseUrl);\n\n const fontsDir = path.join(ctx.argv.output, \"static\", \"fonts\");\n await fs.promises.mkdir(fontsDir, { recursive: true });\n\n for (const fontFile of fontFiles) {\n const fontResponse = await fetch(fontFile.url);\n if (!fontResponse.ok) {\n throw new Error(\n `[FontsEmitter] Failed to fetch font file: ${fontFile.url} (${fontResponse.status} ${fontResponse.statusText})`,\n );\n }\n const buf = await fontResponse.arrayBuffer();\n const filePath = path.join(fontsDir, `${fontFile.filename}.${fontFile.extension}`);\n await fs.promises.writeFile(filePath, Buffer.from(buf));\n yield filePath as FilePath;\n }\n\n const cssPath = path.join(fontsDir, \"quartz-fonts.css\");\n await fs.promises.writeFile(cssPath, processedStylesheet);\n yield cssPath as FilePath;\n },\n };\n};\n"]}
@@ -0,0 +1,9 @@
1
+ import { QuartzTransformerPlugin } from '@quartz-community/types';
2
+ export { QuartzEmitterPlugin, QuartzTransformerPlugin } from '@quartz-community/types';
3
+ import { FontsOptions } from './types.js';
4
+ export { FontFileEntry, FontSpecification, GoogleFontFile, ProcessedFontResult, QuartzFontRegistry } from './types.js';
5
+ export { FontsEmitter } from './emitter.js';
6
+
7
+ declare const Fonts: QuartzTransformerPlugin<Partial<FontsOptions>>;
8
+
9
+ export { Fonts, FontsOptions };