@tenphi/renderer 0.19.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/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/client/appearance.d.ts +12 -0
- package/dist/client/appearance.d.ts.map +1 -0
- package/dist/client/appearance.js +66 -0
- package/dist/client/appearance.js.map +1 -0
- package/dist/client/code-copy.d.ts +1 -0
- package/dist/client/code-copy.js +63 -0
- package/dist/client/code-copy.js.map +1 -0
- package/dist/client/heading-links.d.ts +6 -0
- package/dist/client/heading-links.d.ts.map +1 -0
- package/dist/client/heading-links.js +87 -0
- package/dist/client/heading-links.js.map +1 -0
- package/dist/client/mobile-toc.d.ts +1 -0
- package/dist/client/mobile-toc.js +28 -0
- package/dist/client/mobile-toc.js.map +1 -0
- package/dist/client/navigation.d.ts +1 -0
- package/dist/client/navigation.js +112 -0
- package/dist/client/navigation.js.map +1 -0
- package/dist/client/page-copy.d.ts +1 -0
- package/dist/client/page-copy.js +34 -0
- package/dist/client/page-copy.js.map +1 -0
- package/dist/client/search.d.ts +13 -0
- package/dist/client/search.d.ts.map +1 -0
- package/dist/client/search.js +87 -0
- package/dist/client/search.js.map +1 -0
- package/dist/client/sidebar.d.ts +1 -0
- package/dist/client/sidebar.js +93 -0
- package/dist/client/sidebar.js.map +1 -0
- package/dist/client/tabs.d.ts +6 -0
- package/dist/client/tabs.d.ts.map +1 -0
- package/dist/client/tabs.js +64 -0
- package/dist/client/tabs.js.map +1 -0
- package/dist/components/Callout.astro +15 -0
- package/dist/components/Card.astro +13 -0
- package/dist/components/CodeGroup.astro +47 -0
- package/dist/components/ContrastIcon.astro +29 -0
- package/dist/components/EditLink.astro +11 -0
- package/dist/components/GlobalStyles.js +1675 -0
- package/dist/components/HeaderLinks.astro +84 -0
- package/dist/components/Icon.astro +39 -0
- package/dist/components/LastUpdated.astro +17 -0
- package/dist/components/LayoutComponents.js +866 -0
- package/dist/components/Logo.astro +37 -0
- package/dist/components/MobileNavigationTabs.astro +79 -0
- package/dist/components/MobileTocList.astro +22 -0
- package/dist/components/PackageVersion.astro +25 -0
- package/dist/components/PageActions.astro +29 -0
- package/dist/components/Pagination.astro +29 -0
- package/dist/components/Preview.astro +34 -0
- package/dist/components/SidebarList.astro +122 -0
- package/dist/components/SiteLogo.astro +40 -0
- package/dist/components/SiteTitle.astro +7 -0
- package/dist/components/Steps.astro +6 -0
- package/dist/components/Tab.astro +10 -0
- package/dist/components/TableOfContents.astro +23 -0
- package/dist/components/Tabs.astro +16 -0
- package/dist/components/TastyComponents.js +255 -0
- package/dist/components/VersionSwitcher.astro +44 -0
- package/dist/components/appearance-preferences.test.ts +91 -0
- package/dist/components/appearance-preferences.ts +57 -0
- package/dist/components/component-styles.test.ts +129 -0
- package/dist/components/component-styles.ts +65 -0
- package/dist/components/customize-component.js +8 -0
- package/dist/components/props.d.ts +38 -0
- package/dist/components/props.d.ts.map +1 -0
- package/dist/components/props.js +1 -0
- package/dist/components/props.ts +30 -0
- package/dist/components/svg-icon.test.ts +11 -0
- package/dist/components/svg-icon.ts +11 -0
- package/dist/components/tasty-states.d.ts +2 -0
- package/dist/components/tasty-states.js +42 -0
- package/dist/components-public.d.ts +21 -0
- package/dist/components.d.ts +10 -0
- package/dist/components.js +9 -0
- package/dist/content.d.ts +11 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +16 -0
- package/dist/content.js.map +1 -0
- package/dist/eslint-plugin.d.ts +32 -0
- package/dist/eslint-plugin.d.ts.map +1 -0
- package/dist/eslint-plugin.js +155 -0
- package/dist/eslint-plugin.js.map +1 -0
- package/dist/icons/arrow-left.svg +1 -0
- package/dist/icons/arrow-right.svg +1 -0
- package/dist/icons/check.svg +1 -0
- package/dist/icons/chevron-down.svg +1 -0
- package/dist/icons/chevron-right.svg +1 -0
- package/dist/icons/close.svg +1 -0
- package/dist/icons/copy.svg +1 -0
- package/dist/icons/device-desktop.svg +1 -0
- package/dist/icons/favicon.svg +11 -0
- package/dist/icons/menu.svg +1 -0
- package/dist/icons/moon.svg +1 -0
- package/dist/icons/palette.svg +1 -0
- package/dist/icons/search.svg +1 -0
- package/dist/icons/sun.svg +1 -0
- package/dist/index-BuX_Sb5c.d.ts +10 -0
- package/dist/index-BuX_Sb5c.d.ts.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1648 -0
- package/dist/index.js.map +1 -0
- package/dist/localization.d.ts +69 -0
- package/dist/localization.d.ts.map +1 -0
- package/dist/localization.js +195 -0
- package/dist/localization.js.map +1 -0
- package/dist/markdown/rendered-content.d.ts +6 -0
- package/dist/markdown/rendered-content.d.ts.map +1 -0
- package/dist/markdown/rendered-content.js +34 -0
- package/dist/markdown/rendered-content.js.map +1 -0
- package/dist/messages-B_3xUzUb.js +14 -0
- package/dist/messages-B_3xUzUb.js.map +1 -0
- package/dist/navigation.d.ts +31 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +165 -0
- package/dist/navigation.js.map +1 -0
- package/dist/overrides/DraftContentNotice.astro +8 -0
- package/dist/overrides/EmptyFooter.astro +3 -0
- package/dist/overrides/Footer.astro +23 -0
- package/dist/overrides/Head.astro +59 -0
- package/dist/overrides/Header.astro +139 -0
- package/dist/overrides/Hero.astro +73 -0
- package/dist/overrides/LanguageSelect.astro +63 -0
- package/dist/overrides/MarkdownContent.astro +14 -0
- package/dist/overrides/MobileMenuFooter.astro +15 -0
- package/dist/overrides/MobileMenuToggle.astro +36 -0
- package/dist/overrides/MobileTableOfContents.astro +37 -0
- package/dist/overrides/PageFrame.astro +35 -0
- package/dist/overrides/Search.astro +64 -0
- package/dist/overrides/Sidebar.astro +89 -0
- package/dist/overrides/SocialIcons.astro +27 -0
- package/dist/overrides/ThemeSelect.astro +148 -0
- package/dist/page-metadata.d.ts +13 -0
- package/dist/page-metadata.d.ts.map +1 -0
- package/dist/page-metadata.js +109 -0
- package/dist/page-metadata.js.map +1 -0
- package/dist/route-context.d.ts +85 -0
- package/dist/route-context.d.ts.map +1 -0
- package/dist/route-context.js +116 -0
- package/dist/route-context.js.map +1 -0
- package/dist/routes/DocsPage.astro +91 -0
- package/dist/routes/NotFound.astro +12 -0
- package/dist/routes/Page.astro +120 -0
- package/dist/styling.d.ts +17 -0
- package/dist/styling.d.ts.map +1 -0
- package/dist/styling.js +17 -0
- package/dist/styling.js.map +1 -0
- package/dist/tasty-states-BDKIbfw2.js +65 -0
- package/dist/tasty-states-BDKIbfw2.js.map +1 -0
- package/dist/theme/defaults.d.ts +21 -0
- package/dist/theme/defaults.d.ts.map +1 -0
- package/dist/theme/defaults.js +93 -0
- package/dist/theme/defaults.js.map +1 -0
- package/dist/theme/fonts.ts +64 -0
- package/dist/theme/shiki-theme.d.ts +13 -0
- package/dist/theme/shiki-theme.d.ts.map +1 -0
- package/dist/theme/shiki-theme.js +328 -0
- package/dist/theme/shiki-theme.js.map +1 -0
- package/dist/theme/tasty-config.d.ts +13 -0
- package/dist/theme/tasty-config.d.ts.map +1 -0
- package/dist/theme/tasty-config.js +16 -0
- package/dist/theme/tasty-config.js.map +1 -0
- package/dist/ui.d.ts +6 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +53 -0
- package/dist/ui.js.map +1 -0
- package/dist/versioning.d.ts +11 -0
- package/dist/versioning.d.ts.map +1 -0
- package/dist/versioning.js +38 -0
- package/dist/versioning.js.map +1 -0
- package/package.json +92 -0
- package/tasty.config.mjs +1 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1648 @@
|
|
|
1
|
+
import { navigationPath, resolveNavigationLayout } from "./navigation.js";
|
|
2
|
+
import { localeAlternates } from "./localization.js";
|
|
3
|
+
import { DEFAULT_THEME_TOKENS, DEFAULT_TYPOGRAPHY_PRESETS, resolveThemeTokens, resolveTypographyPresets } from "./theme/defaults.js";
|
|
4
|
+
import { configureCodeHighlighting, cookbookShikiConfig } from "./theme/shiki-theme.js";
|
|
5
|
+
import { TASTY_UNITS, tastyTokens } from "./theme/tasty-config.js";
|
|
6
|
+
import { a as resolveLegacyAnatomyStyles, n as cookbookStates, o as unusedCustomStyleNames, r as configureComponentStyles } from "./tasty-states-BDKIbfw2.js";
|
|
7
|
+
import { agentPagePath } from "./page-metadata.js";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import { cp, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
11
|
+
import sharp from "sharp";
|
|
12
|
+
import { DOCS_FRONTMATTER_KEYS, assertValidDocs, createDocsGraph, renderAgentMarkdown, resolveColorTheme, resolveDocsProject } from "@tenphi/docs";
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
|
+
import { fromHtml } from "hast-util-from-html";
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
import mdx from "@astrojs/mdx";
|
|
18
|
+
import * as pagefind from "pagefind";
|
|
19
|
+
import sitemap from "@astrojs/sitemap";
|
|
20
|
+
import { configure } from "@tenphi/tasty/core";
|
|
21
|
+
import { tastyIntegration } from "@tenphi/tasty/ssr/astro";
|
|
22
|
+
import { renderMermaidSVG } from "beautiful-mermaid";
|
|
23
|
+
//#region src/hero-image.ts
|
|
24
|
+
const metadataCache = /* @__PURE__ */ new Map();
|
|
25
|
+
async function resolveHeroMetadata(entry) {
|
|
26
|
+
const hero = entry.frontmatter.hero;
|
|
27
|
+
const image = hero?.image;
|
|
28
|
+
if (!hero || !image || "html" in image) return hero;
|
|
29
|
+
const read = async (src) => {
|
|
30
|
+
const asset = entry.assets.find((asset) => asset.resolved === src || asset.publicPath === src);
|
|
31
|
+
let dimensions;
|
|
32
|
+
if (asset?.sourcePath) {
|
|
33
|
+
const file = await stat(asset.sourcePath);
|
|
34
|
+
const key = `${asset.sourcePath}:${file.mtimeMs}:${file.size}`;
|
|
35
|
+
if (!metadataCache.has(key)) metadataCache.set(key, sharp(asset.sourcePath).metadata().then((meta) => {
|
|
36
|
+
const { width, height } = meta.autoOrient ?? meta;
|
|
37
|
+
if (!width || !height || !meta.format) throw Error(`Cannot determine hero image dimensions for ${entry.sourcePath}.`);
|
|
38
|
+
return {
|
|
39
|
+
width,
|
|
40
|
+
height,
|
|
41
|
+
format: meta.format
|
|
42
|
+
};
|
|
43
|
+
}));
|
|
44
|
+
dimensions = await metadataCache.get(key);
|
|
45
|
+
} else if (image.width && image.height) dimensions = {
|
|
46
|
+
width: image.width,
|
|
47
|
+
height: image.height,
|
|
48
|
+
format: extname(src.split(/[?#]/, 1)[0] ?? src).slice(1).replace("jpg", "jpeg") || "png"
|
|
49
|
+
};
|
|
50
|
+
else throw Error(`Hero image ${src} in ${entry.sourcePath} needs width and height, or a relative local file whose dimensions can be read.`);
|
|
51
|
+
if (image.width && image.height && Math.abs(image.width / image.height - dimensions.width / dimensions.height) > .01) throw Error(`hero.image width/height in ${entry.sourcePath} must preserve the source aspect ratio.`);
|
|
52
|
+
return {
|
|
53
|
+
src,
|
|
54
|
+
...dimensions
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
if ("file" in image) return {
|
|
58
|
+
...hero,
|
|
59
|
+
image: {
|
|
60
|
+
...image,
|
|
61
|
+
file: await read(image.file)
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const [dark, light] = await Promise.all([read(image.dark), read(image.light)]);
|
|
65
|
+
if (Math.abs(dark.width / dark.height - light.width / light.height) > .01) throw Error(`Hero light/dark images in ${entry.sourcePath} must have the same aspect ratio.`);
|
|
66
|
+
return {
|
|
67
|
+
...hero,
|
|
68
|
+
image: {
|
|
69
|
+
...image,
|
|
70
|
+
dark,
|
|
71
|
+
light
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/plugin-contract.ts
|
|
77
|
+
async function validatePluginFrontmatter(entries, schema) {
|
|
78
|
+
if (!schema) return;
|
|
79
|
+
for (const entry of entries) try {
|
|
80
|
+
const parsed = "parseAsync" in schema ? await schema.parseAsync(structuredClone(entry.metadata)) : schema.parse(structuredClone(entry.metadata));
|
|
81
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("The schema must return a frontmatter object.");
|
|
82
|
+
for (const key of Object.keys(parsed)) if (DOCS_FRONTMATTER_KEYS.has(key)) throw new Error(`Custom schemas cannot set reserved field "${key}". Set it in source frontmatter instead.`);
|
|
83
|
+
assertJsonMetadata(parsed);
|
|
84
|
+
entry.metadata = {
|
|
85
|
+
...entry.metadata,
|
|
86
|
+
...parsed
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(`Invalid plugin frontmatter in ${entry.sourcePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function assertJsonMetadata(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
93
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
94
|
+
if (typeof value === "number" && Number.isFinite(value)) return;
|
|
95
|
+
if (typeof value !== "object" || value === null || ancestors.has(value) || !Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype) throw new Error("Custom metadata must contain only JSON values (no dates, functions, cycles, or undefined).");
|
|
96
|
+
ancestors.add(value);
|
|
97
|
+
for (const item of Object.values(value)) assertJsonMetadata(item, ancestors);
|
|
98
|
+
ancestors.delete(value);
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/site-logo.ts
|
|
102
|
+
async function resolveSiteLogo(root, base, site = {}) {
|
|
103
|
+
if (site.logo === void 0 || site.logo === false) return {
|
|
104
|
+
logo: site.logo,
|
|
105
|
+
sourcePaths: [],
|
|
106
|
+
assets: []
|
|
107
|
+
};
|
|
108
|
+
const config = typeof site.logo === "string" ? { src: site.logo } : site.logo;
|
|
109
|
+
const assets = [];
|
|
110
|
+
const sourcePaths = [];
|
|
111
|
+
const image = async (source, role) => {
|
|
112
|
+
const path = resolve(root, source);
|
|
113
|
+
const body = await readFile(path).catch(() => {
|
|
114
|
+
throw new Error(`site.logo.${role}: cannot read local image ${path}.`);
|
|
115
|
+
});
|
|
116
|
+
const metadata = await sharp(body, { animated: false }).metadata().catch(() => {
|
|
117
|
+
throw new Error(`site.logo.${role}: ${path} is not a supported image.`);
|
|
118
|
+
});
|
|
119
|
+
const format = metadata.format;
|
|
120
|
+
if (!format || ![
|
|
121
|
+
"svg",
|
|
122
|
+
"png",
|
|
123
|
+
"jpeg",
|
|
124
|
+
"webp",
|
|
125
|
+
"avif",
|
|
126
|
+
"gif"
|
|
127
|
+
].includes(format)) throw new Error(`site.logo.${role}: use an SVG, PNG, JPEG, WebP, AVIF, or GIF image.`);
|
|
128
|
+
const width = config.width ?? (config.height && metadata.width && metadata.height ? config.height * metadata.width / metadata.height : metadata.width);
|
|
129
|
+
const height = config.height ?? (config.width && metadata.width && metadata.height ? config.width * metadata.height / metadata.width : metadata.height);
|
|
130
|
+
if (!width || !height) throw new Error(`site.logo.${role}: provide intrinsic width and height for ${path}.`);
|
|
131
|
+
const outputPath = `_cookbook/logos/${createHash("sha256").update(body).digest("hex").slice(0, 20)}.${format === "jpeg" ? "jpg" : format}`;
|
|
132
|
+
const publicPath = `${base.replace(/\/$/, "")}/${outputPath}`;
|
|
133
|
+
sourcePaths.push(path);
|
|
134
|
+
if (!assets.some((asset) => asset.outputPath === outputPath)) assets.push({
|
|
135
|
+
body,
|
|
136
|
+
outputPath,
|
|
137
|
+
publicPath,
|
|
138
|
+
contentType: format === "svg" ? "image/svg+xml" : `image/${format}`
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
src: publicPath,
|
|
142
|
+
width,
|
|
143
|
+
height
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
const light = await image(config.src ?? config.light, config.src ? "src" : "light");
|
|
147
|
+
const dark = config.dark ? await image(config.dark, "dark") : void 0;
|
|
148
|
+
if (dark && Math.abs(light.width / light.height - dark.width / dark.height) > .01) throw new Error("site.logo: light and dark variants must have the same aspect ratio to prevent layout shifts. Set matching intrinsic dimensions or use matching artwork.");
|
|
149
|
+
return {
|
|
150
|
+
logo: {
|
|
151
|
+
light,
|
|
152
|
+
...dark ? { dark } : {},
|
|
153
|
+
alt: config.alt || site.title || "Documentation",
|
|
154
|
+
decorative: config.decorative ?? true,
|
|
155
|
+
...config.href ? { href: config.href } : {}
|
|
156
|
+
},
|
|
157
|
+
assets,
|
|
158
|
+
sourcePaths
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/output-styles.ts
|
|
163
|
+
/** Parse attributes rather than matching text in code, scripts or escaped srcdoc. */
|
|
164
|
+
function assertTastyOutput(html, path) {
|
|
165
|
+
const tree = fromHtml(html);
|
|
166
|
+
const visit = (node) => {
|
|
167
|
+
const props = node.properties ?? {};
|
|
168
|
+
const stylesheet = node.tagName === "link" && Array.isArray(props.rel) && props.rel.includes("stylesheet");
|
|
169
|
+
if (node.tagName === "style" || Object.hasOwn(props, "style") || stylesheet && !Object.hasOwn(props, "dataTastySsr")) throw new Error(`Non-Tasty CSS found in ${path}: <${node.tagName}${Object.hasOwn(props, "style") ? " style=…" : ""}>. Use theme.styles or Tasty components; isolate example CSS in a sandboxed Preview.`);
|
|
170
|
+
if (typeof props.srcDoc === "string" && (!Array.isArray(props.sandbox) || props.sandbox.includes("allow-same-origin"))) assertTastyOutput(props.srcDoc, `${path} (unsandboxed iframe)`);
|
|
171
|
+
node.children?.forEach((child) => visit(child));
|
|
172
|
+
if (node.content) visit(node.content);
|
|
173
|
+
};
|
|
174
|
+
visit(tree);
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/markdown/rehype-mermaid.ts
|
|
178
|
+
const sourceStyleDirective = /^\s*(?:classDef|style|linkStyle)\s+.*$/gim;
|
|
179
|
+
const accessibilityDirective = /^\s*acc(?:Title|Descr):\s*.*$/gim;
|
|
180
|
+
const svgStyleBlock = /\s*<style>[\s\S]*?<\/style>\s*/gi;
|
|
181
|
+
const supportedDiagram = /^(?:(?:flowchart|graph)(?:\s+(?:TB|TD|BT|RL|LR))?|stateDiagram(?:-v2)?|sequenceDiagram|classDiagram|erDiagram)\b/i;
|
|
182
|
+
/** Render supported Mermaid fences to theme-responsive SVG at build time. */
|
|
183
|
+
function rehypeMermaid() {
|
|
184
|
+
return (tree) => {
|
|
185
|
+
replaceMermaidCodeBlocks(tree);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Sätteri adapter for Astro's default Markdown processor. */
|
|
189
|
+
const satteriMermaid = {
|
|
190
|
+
name: "cookbook:mermaid",
|
|
191
|
+
element: {
|
|
192
|
+
filter: ["pre"],
|
|
193
|
+
visit(node, context) {
|
|
194
|
+
if (!isMermaidCodeBlock(node)) return;
|
|
195
|
+
try {
|
|
196
|
+
context.replaceNode(node, {
|
|
197
|
+
type: "raw",
|
|
198
|
+
value: renderMermaidElement(context.textContent(node))
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
context.setProperty(node, "data-mermaid-state", "error");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
function replaceMermaidCodeBlocks(parent) {
|
|
207
|
+
if (!parent.children) return;
|
|
208
|
+
for (const [index, child] of parent.children.entries()) {
|
|
209
|
+
if (isMermaidCodeBlock(child)) {
|
|
210
|
+
const source = textContent(child);
|
|
211
|
+
try {
|
|
212
|
+
parent.children[index] = {
|
|
213
|
+
type: "raw",
|
|
214
|
+
value: renderMermaidElement(source)
|
|
215
|
+
};
|
|
216
|
+
} catch {
|
|
217
|
+
child.properties = {
|
|
218
|
+
...child.properties,
|
|
219
|
+
"data-mermaid-state": "error"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
replaceMermaidCodeBlocks(child);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function renderMermaidElement(source) {
|
|
228
|
+
return `<div class="td-mermaid" data-mermaid-state="ready">${accessibleSvg(render(source), source)}</div>`;
|
|
229
|
+
}
|
|
230
|
+
function isMermaidCodeBlock(node) {
|
|
231
|
+
if (node.type !== "element" || node.tagName !== "pre") return false;
|
|
232
|
+
return node.properties?.dataLanguage === "mermaid" || node.properties?.["data-language"] === "mermaid";
|
|
233
|
+
}
|
|
234
|
+
function render(source) {
|
|
235
|
+
const header = source.split("\n").map((line) => line.trim()).find((line) => line && !line.startsWith("%%") && !/^acc(?:Title|Descr):/i.test(line));
|
|
236
|
+
if (!header || !supportedDiagram.test(header)) throw new Error("Unsupported Mermaid diagram type");
|
|
237
|
+
const safeSource = source.replace(sourceStyleDirective, "").replace(accessibilityDirective, "");
|
|
238
|
+
return renderMermaidSVG(safeSource, {
|
|
239
|
+
bg: "var(--surface-2-color)",
|
|
240
|
+
fg: "var(--text-color)",
|
|
241
|
+
line: "var(--text-soft-color)",
|
|
242
|
+
accent: "var(--accent-text-color)",
|
|
243
|
+
muted: "var(--text-soft-color)",
|
|
244
|
+
surface: "var(--surface-color)",
|
|
245
|
+
border: "var(--border-strong-color)",
|
|
246
|
+
font: "var(--body-font-family)",
|
|
247
|
+
transparent: true
|
|
248
|
+
}).replace(svgStyleBlock, "").replace(/(<svg\b[^>]*?)\sstyle="[^"]*"/, "$1");
|
|
249
|
+
}
|
|
250
|
+
function accessibleSvg(svg, source) {
|
|
251
|
+
const title = directive(source, "accTitle") ?? "Diagram";
|
|
252
|
+
const description = directive(source, "accDescr") ?? "Rendered from a Mermaid code block.";
|
|
253
|
+
return svg.replace("<svg ", `<svg role="img" aria-label="${escapeAttribute(title)}" `).replace(/(<svg\b[^>]*>)/, `$1<title>${escapeText(title)}</title><desc>${escapeText(description)}</desc>`);
|
|
254
|
+
}
|
|
255
|
+
function directive(source, name) {
|
|
256
|
+
return source.match(new RegExp(`^\\s*${name}:\\s*(.+)$`, "im"))?.[1]?.trim();
|
|
257
|
+
}
|
|
258
|
+
function textContent(node) {
|
|
259
|
+
if (node.type === "text") return node.value ?? "";
|
|
260
|
+
return node.children?.map(textContent).join("") ?? "";
|
|
261
|
+
}
|
|
262
|
+
function escapeAttribute(value) {
|
|
263
|
+
return escapeText(value).replaceAll("\"", """).replaceAll("'", "'");
|
|
264
|
+
}
|
|
265
|
+
function escapeText(value) {
|
|
266
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/markdown/rehype-alerts.ts
|
|
270
|
+
const alerts = {
|
|
271
|
+
NOTE: {
|
|
272
|
+
kind: "note",
|
|
273
|
+
title: "Note"
|
|
274
|
+
},
|
|
275
|
+
TIP: {
|
|
276
|
+
kind: "tip",
|
|
277
|
+
title: "Tip"
|
|
278
|
+
},
|
|
279
|
+
IMPORTANT: {
|
|
280
|
+
kind: "tip",
|
|
281
|
+
title: "Important"
|
|
282
|
+
},
|
|
283
|
+
WARNING: {
|
|
284
|
+
kind: "caution",
|
|
285
|
+
title: "Warning"
|
|
286
|
+
},
|
|
287
|
+
CAUTION: {
|
|
288
|
+
kind: "danger",
|
|
289
|
+
title: "Caution"
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
/** Keep GitHub alert syntax useful in repository Markdown and MDX. */
|
|
293
|
+
function rehypeAlerts() {
|
|
294
|
+
return (tree) => visit(tree);
|
|
295
|
+
}
|
|
296
|
+
const satteriAlerts = {
|
|
297
|
+
name: "cookbook:alerts",
|
|
298
|
+
element: {
|
|
299
|
+
filter: ["blockquote"],
|
|
300
|
+
visit(node, context) {
|
|
301
|
+
const replacement = alert(node);
|
|
302
|
+
if (replacement) context.replaceNode(node, replacement);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
function visit(parent) {
|
|
307
|
+
if (!parent.children) return;
|
|
308
|
+
for (const [index, child] of parent.children.entries()) {
|
|
309
|
+
const replacement = alert(child);
|
|
310
|
+
if (replacement) parent.children[index] = replacement;
|
|
311
|
+
else visit(child);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function alert(blockquote) {
|
|
315
|
+
if (blockquote.type !== "element" || blockquote.tagName !== "blockquote") return;
|
|
316
|
+
const first = blockquote.children?.find((child) => child.type === "element");
|
|
317
|
+
if (first?.tagName !== "p") return;
|
|
318
|
+
const marker = first.children?.find((child) => child.type === "text");
|
|
319
|
+
if (!marker?.value) return;
|
|
320
|
+
const match = /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:\s|$)/i.exec(marker.value);
|
|
321
|
+
if (!match) return;
|
|
322
|
+
const { kind, title } = alerts[match[1].toUpperCase()];
|
|
323
|
+
const updatedFirst = {
|
|
324
|
+
...first,
|
|
325
|
+
children: first.children.map((child) => child === marker ? {
|
|
326
|
+
...child,
|
|
327
|
+
value: child.value.slice(match[0].length).trimStart()
|
|
328
|
+
} : child)
|
|
329
|
+
};
|
|
330
|
+
const body = blockquote.children?.map((child) => child === first ? updatedFirst : child).filter((child) => child !== updatedFirst || updatedFirst.children?.some((part) => part.type !== "text" || Boolean(part.value?.trim()))) ?? [];
|
|
331
|
+
return {
|
|
332
|
+
type: "element",
|
|
333
|
+
tagName: "aside",
|
|
334
|
+
properties: {
|
|
335
|
+
className: ["cookbook-alert", `cookbook-alert--${kind}`],
|
|
336
|
+
ariaLabel: title
|
|
337
|
+
},
|
|
338
|
+
children: [{
|
|
339
|
+
type: "element",
|
|
340
|
+
tagName: "p",
|
|
341
|
+
properties: { className: ["cookbook-alert__title"] },
|
|
342
|
+
children: [{
|
|
343
|
+
type: "text",
|
|
344
|
+
value: title
|
|
345
|
+
}]
|
|
346
|
+
}, {
|
|
347
|
+
type: "element",
|
|
348
|
+
tagName: "div",
|
|
349
|
+
properties: { className: ["cookbook-alert__content"] },
|
|
350
|
+
children: body
|
|
351
|
+
}]
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/markdown/rehype-table-scroll.ts
|
|
356
|
+
const containerClass = "td-table-scroll";
|
|
357
|
+
/** Wrap Markdown tables in a dedicated horizontal scroll container. */
|
|
358
|
+
function rehypeTableScroll() {
|
|
359
|
+
return (tree) => {
|
|
360
|
+
wrapTables(tree);
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/** Sätteri adapter for Astro's default Markdown processor. */
|
|
364
|
+
const satteriTableScroll = {
|
|
365
|
+
name: "cookbook:table-scroll",
|
|
366
|
+
element: {
|
|
367
|
+
filter: ["table"],
|
|
368
|
+
visit(node, context) {
|
|
369
|
+
context.replaceNode(node, scrollContainer(node));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
function wrapTables(parent) {
|
|
374
|
+
if (!parent.children || isScrollContainer(parent)) return;
|
|
375
|
+
for (const [index, child] of parent.children.entries()) {
|
|
376
|
+
if (child.type === "element" && child.tagName === "table") {
|
|
377
|
+
parent.children[index] = scrollContainer(child);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
wrapTables(child);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function scrollContainer(table) {
|
|
384
|
+
return {
|
|
385
|
+
type: "element",
|
|
386
|
+
tagName: "div",
|
|
387
|
+
properties: { className: [containerClass] },
|
|
388
|
+
children: [table]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function isScrollContainer(node) {
|
|
392
|
+
const className = node.properties?.className;
|
|
393
|
+
return node.type === "element" && node.tagName === "div" && Array.isArray(className) && className.includes(containerClass);
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/markdown/rehype-page-affordances.ts
|
|
397
|
+
/** Add copy controls to rendered Markdown code blocks. */
|
|
398
|
+
function rehypePageAffordances() {
|
|
399
|
+
return (tree) => addPageAffordances(tree);
|
|
400
|
+
}
|
|
401
|
+
/** Sätteri adapter for Astro's default Markdown processor. */
|
|
402
|
+
const satteriPageAffordances = {
|
|
403
|
+
name: "cookbook:page-affordances",
|
|
404
|
+
element: {
|
|
405
|
+
filter: ["pre"],
|
|
406
|
+
visit(node, context) {
|
|
407
|
+
const replacement = pageAffordance(node);
|
|
408
|
+
if (replacement !== node) context.replaceNode(node, replacement);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
function addPageAffordances(parent) {
|
|
413
|
+
if (!parent.children) return;
|
|
414
|
+
for (const [index, child] of parent.children.entries()) {
|
|
415
|
+
const replacement = pageAffordance(child);
|
|
416
|
+
if (replacement !== child) {
|
|
417
|
+
parent.children[index] = replacement;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
addPageAffordances(child);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function pageAffordance(node) {
|
|
424
|
+
if (isOrdinaryCodeBlock(node)) return codeBlockWithCopy(node);
|
|
425
|
+
return node;
|
|
426
|
+
}
|
|
427
|
+
function isOrdinaryCodeBlock(node) {
|
|
428
|
+
if (node.type !== "element" || node.tagName !== "pre") return false;
|
|
429
|
+
return (node.properties?.dataLanguage ?? node.properties?.["data-language"]) !== "mermaid" && node.children?.[0]?.tagName === "code";
|
|
430
|
+
}
|
|
431
|
+
function codeBlockWithCopy(pre) {
|
|
432
|
+
return element("cookbook-code-block", {
|
|
433
|
+
className: ["td-code-block"],
|
|
434
|
+
dataTastyAnatomy: "MarkdownCodeBlock"
|
|
435
|
+
}, [pre, element("button", {
|
|
436
|
+
type: "button",
|
|
437
|
+
dataCopyCode: "",
|
|
438
|
+
ariaLabel: "Copy code",
|
|
439
|
+
title: "Copy code"
|
|
440
|
+
}, [element("span", {
|
|
441
|
+
dataCopyIcon: "",
|
|
442
|
+
ariaHidden: "true"
|
|
443
|
+
}, [])])]);
|
|
444
|
+
}
|
|
445
|
+
function element(tagName, properties, children) {
|
|
446
|
+
return {
|
|
447
|
+
type: "element",
|
|
448
|
+
tagName,
|
|
449
|
+
properties,
|
|
450
|
+
children
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/theme/fonts.ts
|
|
455
|
+
const sharedFonts = globalThis;
|
|
456
|
+
const fallback = {
|
|
457
|
+
body: "system-ui, sans-serif",
|
|
458
|
+
heading: "system-ui, sans-serif",
|
|
459
|
+
code: "ui-monospace, monospace"
|
|
460
|
+
};
|
|
461
|
+
function fontFamilies(fonts = {}) {
|
|
462
|
+
return Object.fromEntries(Object.entries(fonts).filter(([, font]) => font !== void 0).map(([role, font]) => [role, `'${typeof font === "string" ? font : "google" in font ? font.google : font.family}', ${fallback[role]}`]));
|
|
463
|
+
}
|
|
464
|
+
function configureFontFaces(faces, defaults) {
|
|
465
|
+
sharedFonts.__tenphiCookbookFontFaces = faces;
|
|
466
|
+
sharedFonts.__tenphiCookbookDefaultFontUsage = defaults;
|
|
467
|
+
}
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region src/theme/index.ts
|
|
470
|
+
function resolveDocsTheme(theme = {}) {
|
|
471
|
+
return {
|
|
472
|
+
...resolveColorTheme(theme),
|
|
473
|
+
tokens: resolveThemeTokens(theme.tokens),
|
|
474
|
+
presets: resolveTypographyPresets(theme.presets, fontFamilies(theme.fonts))
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/theme/font-cache.ts
|
|
479
|
+
const fontHash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
480
|
+
const userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
481
|
+
/** Content-verified, atomic cache entries. Cache keys include the fixed browser profile. */
|
|
482
|
+
function createFontFetcher(directory, mode = "reuse", fetchFont = fetch) {
|
|
483
|
+
const pending = /* @__PURE__ */ new Map();
|
|
484
|
+
return (url) => {
|
|
485
|
+
const load = async () => {
|
|
486
|
+
const key = fontHash(`${userAgent}\n${url}`);
|
|
487
|
+
const path = directory ? join(directory, `${key}.json`) : void 0;
|
|
488
|
+
if (path && mode !== "refresh") try {
|
|
489
|
+
const cached = JSON.parse(await readFile(path, "utf8"));
|
|
490
|
+
const body = Buffer.from(cached.body, "base64");
|
|
491
|
+
if (![200, 400].includes(cached.status) || cached.url !== url || fontHash(body) !== cached.sha256) throw new Error("Invalid font cache entry");
|
|
492
|
+
return {
|
|
493
|
+
status: cached.status,
|
|
494
|
+
body
|
|
495
|
+
};
|
|
496
|
+
} catch {}
|
|
497
|
+
if (mode === "offline") throw new Error(`Google font cache is missing or corrupt for ${url}. Run once with theme.fontLoading.cache: "reuse" and preserve the Astro cache directory.`);
|
|
498
|
+
let response;
|
|
499
|
+
try {
|
|
500
|
+
response = await fetchFont(url, {
|
|
501
|
+
headers: { "User-Agent": userAgent },
|
|
502
|
+
signal: AbortSignal.timeout(15e3)
|
|
503
|
+
});
|
|
504
|
+
} catch (error) {
|
|
505
|
+
throw new Error(`Could not download Google font resource ${url}: ${String(error)}. Preserve the Astro cache for offline builds, or use local font files.`);
|
|
506
|
+
}
|
|
507
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
508
|
+
if (![200, 400].includes(response.status)) throw new Error(`Google font request failed (${response.status}): ${url}.`);
|
|
509
|
+
if (path) {
|
|
510
|
+
await mkdir(directory, { recursive: true });
|
|
511
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
512
|
+
await writeFile(temporary, JSON.stringify({
|
|
513
|
+
url,
|
|
514
|
+
status: response.status,
|
|
515
|
+
sha256: fontHash(body),
|
|
516
|
+
body: body.toString("base64")
|
|
517
|
+
}));
|
|
518
|
+
await rename(temporary, path);
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
status: response.status,
|
|
522
|
+
body
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
if (!pending.has(url)) pending.set(url, load());
|
|
526
|
+
return pending.get(url);
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/theme/font-loading.ts
|
|
531
|
+
/** Merge overlapping/touching ranges, as required by the Google CSS2 API. */
|
|
532
|
+
function fontWeightSpans(weights) {
|
|
533
|
+
return weights.map((weight) => typeof weight === "number" ? [weight, weight] : weight.split(" ").map(Number)).sort((a, b) => a[0] - b[0]).reduce((result, span) => {
|
|
534
|
+
const previous = result.at(-1);
|
|
535
|
+
if (previous && span[0] <= previous[1]) previous[1] = Math.max(previous[1], span[1]);
|
|
536
|
+
else result.push([...span]);
|
|
537
|
+
return result;
|
|
538
|
+
}, []);
|
|
539
|
+
}
|
|
540
|
+
function presetWeights(family, presets) {
|
|
541
|
+
const resolveValue = (value, seen = /* @__PURE__ */ new Set()) => {
|
|
542
|
+
if (typeof value !== "string") return value;
|
|
543
|
+
const ref = /^var\(--(.+)-(font-family|bold-font-weight|font-weight)\)$/.exec(value);
|
|
544
|
+
if (!ref || seen.has(value)) return value;
|
|
545
|
+
seen.add(value);
|
|
546
|
+
const key = ref[2] === "font-family" ? "fontFamily" : ref[2] === "font-weight" ? "fontWeight" : "boldFontWeight";
|
|
547
|
+
return resolveValue(presets[ref[1]]?.[key], seen);
|
|
548
|
+
};
|
|
549
|
+
const values = Object.values(presets).flatMap((preset) => {
|
|
550
|
+
const resolvedFamily = resolveValue(preset.fontFamily);
|
|
551
|
+
if (typeof resolvedFamily !== "string" || resolvedFamily.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "") !== family) return [];
|
|
552
|
+
return [resolveValue(preset.fontWeight), resolveValue(preset.boldFontWeight)];
|
|
553
|
+
}).map((value) => value === "bold" ? 700 : value === "normal" ? 400 : Number(value)).filter((value) => Number.isInteger(value) && value >= 1 && value <= 1e3);
|
|
554
|
+
return [...new Set(values.length ? values : [400, 700])];
|
|
555
|
+
}
|
|
556
|
+
/** Resolve CSS and optional font assets before rendering; all faces are emitted by Tasty. */
|
|
557
|
+
async function resolveThemeFonts(fonts = {}, options = {}) {
|
|
558
|
+
const base = (options.base ?? "/").replace(/\/$/, "");
|
|
559
|
+
const loading = options.loading ?? {};
|
|
560
|
+
const download = createFontFetcher(options.cacheDir, loading.cache, options.fetch);
|
|
561
|
+
const presets = options.presets ?? resolveTypographyPresets({}, fontFamilies(fonts));
|
|
562
|
+
const faces = [];
|
|
563
|
+
const assets = /* @__PURE__ */ new Map();
|
|
564
|
+
const google = /* @__PURE__ */ new Map();
|
|
565
|
+
for (const font of Object.values(fonts).filter((font) => font !== void 0)) if (typeof font === "string" || "google" in font) {
|
|
566
|
+
const family = typeof font === "string" ? font : font.google;
|
|
567
|
+
const requested = google.get(family) ?? {
|
|
568
|
+
explicit: /* @__PURE__ */ new Map(),
|
|
569
|
+
inferred: /* @__PURE__ */ new Map()
|
|
570
|
+
};
|
|
571
|
+
const weights = typeof font === "string" ? void 0 : font.weights;
|
|
572
|
+
const styles = typeof font === "string" ? ["normal", "italic"] : font.styles ?? ["normal", "italic"];
|
|
573
|
+
for (const style of styles) if (weights) requested.explicit.set(style, [...requested.explicit.get(style) ?? [], ...weights]);
|
|
574
|
+
else requested.inferred.set(style, [...requested.inferred.get(style) ?? [], ...presetWeights(family, presets)]);
|
|
575
|
+
google.set(family, requested);
|
|
576
|
+
} else for (const file of font.files) {
|
|
577
|
+
const extension = file.src.split(".").at(-1);
|
|
578
|
+
const format = extension === "ttf" ? "truetype" : extension === "otf" ? "opentype" : extension;
|
|
579
|
+
faces.push({
|
|
580
|
+
family: font.family,
|
|
581
|
+
descriptors: {
|
|
582
|
+
src: `url("${base}${file.src}") format("${format}")`,
|
|
583
|
+
fontWeight: file.weight ?? 400,
|
|
584
|
+
fontStyle: file.style ?? "normal",
|
|
585
|
+
fontDisplay: loading.display ?? "swap"
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
for (const [family, request] of google) {
|
|
590
|
+
const fetchFaces = async (byStyle, strict) => {
|
|
591
|
+
if (!byStyle.size) return [];
|
|
592
|
+
const tuples = [...byStyle].sort(([a], [b]) => (a === "normal" ? 0 : 1) - (b === "normal" ? 0 : 1)).flatMap(([style, weights]) => fontWeightSpans(weights).map(([min, max]) => `${style === "italic" ? 1 : 0},${min === max ? min : `${min}..${max}`}`));
|
|
593
|
+
const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family).replaceAll("%20", "+")}:ital,wght@${tuples.join(";")}&display=${loading.display ?? "swap"}`;
|
|
594
|
+
const response = await download(url);
|
|
595
|
+
if (response.status === 400) {
|
|
596
|
+
if (!strict) return [];
|
|
597
|
+
throw new Error(`Google Fonts rejected "${family}" (400). Check theme.fonts weights/ranges/styles against the family's supported axes.`);
|
|
598
|
+
}
|
|
599
|
+
const parsed = parseGoogleFontCss(response.body.toString("utf8"), family, loading.display ?? "swap");
|
|
600
|
+
if (!parsed.length) throw new Error(`Google Fonts returned no usable font files for "${family}".`);
|
|
601
|
+
if (strict) {
|
|
602
|
+
for (const [style, weights] of byStyle) for (const [lo, hi] of fontWeightSpans(weights)) if (!fontWeightSpans(parsed.filter((face) => face.descriptors.fontStyle === style).map((face) => String(face.descriptors.fontWeight).includes(" ") ? String(face.descriptors.fontWeight) : Number(face.descriptors.fontWeight))).some(([min, max]) => min <= lo && max >= hi)) throw new Error(`Google Fonts did not provide ${family} ${style} weight ${lo === hi ? lo : `${lo} ${hi}`}. Choose supported theme.fonts weights/styles or use local font files.`);
|
|
603
|
+
}
|
|
604
|
+
return parsed;
|
|
605
|
+
};
|
|
606
|
+
const familyFaces = await fetchFaces(request.explicit, true);
|
|
607
|
+
if (request.inferred.size) {
|
|
608
|
+
let inferred = await fetchFaces(new Map([...request.inferred].map(([style, weights]) => [style, [Math.min(...weights) === Math.max(...weights) ? weights[0] : `${Math.min(...weights)} ${Math.max(...weights)}`]])), false);
|
|
609
|
+
if (!inferred.length) {
|
|
610
|
+
inferred = await fetchFaces(new Map([...request.inferred].map(([style, weights]) => [style, [...new Set(weights.flatMap((weight) => [
|
|
611
|
+
weight,
|
|
612
|
+
Math.max(100, Math.floor(weight / 100) * 100),
|
|
613
|
+
Math.min(900, Math.ceil(weight / 100) * 100)
|
|
614
|
+
]))]])), false);
|
|
615
|
+
if (!inferred.length) throw new Error(`Google Fonts rejected "${family}". Check the family name or provide explicit supported theme.fonts weights/styles.`);
|
|
616
|
+
}
|
|
617
|
+
for (const style of request.inferred.keys()) if (!inferred.some((face) => face.descriptors.fontStyle === style)) options.warn?.(`Google Font "${family}" has no ${style} face for these presets. Choose a family with that style, or set theme.fonts styles explicitly.`);
|
|
618
|
+
familyFaces.push(...inferred);
|
|
619
|
+
}
|
|
620
|
+
for (const face of familyFaces) {
|
|
621
|
+
if (loading.google !== "remote") {
|
|
622
|
+
const source = /url\("([^"]+)"\) format\("([^"]+)"\)/.exec(String(face.descriptors.src));
|
|
623
|
+
const response = await download(source[1]);
|
|
624
|
+
if (response.status !== 200 || response.body.length < 12 || ![
|
|
625
|
+
"wOF2",
|
|
626
|
+
"wOFF",
|
|
627
|
+
"OTTO",
|
|
628
|
+
"ttcf",
|
|
629
|
+
"\0\0\0"
|
|
630
|
+
].includes(response.body.subarray(0, 4).toString("binary"))) throw new Error(`Could not download Google Font "${family}" from ${source[1]}.`);
|
|
631
|
+
const extension = source[2] === "truetype" ? "ttf" : source[2] === "opentype" ? "otf" : source[2];
|
|
632
|
+
const outputPath = `_cookbook/fonts/${fontHash(response.body)}.${extension}`;
|
|
633
|
+
const publicPath = `${base}/${outputPath}`;
|
|
634
|
+
assets.set(outputPath, {
|
|
635
|
+
outputPath,
|
|
636
|
+
publicPath,
|
|
637
|
+
body: response.body,
|
|
638
|
+
contentType: `font/${extension}`
|
|
639
|
+
});
|
|
640
|
+
face.descriptors.src = `url("${publicPath}") format("${source[2]}")`;
|
|
641
|
+
}
|
|
642
|
+
faces.push(face);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
faces: [...new Map(faces.map((face) => [JSON.stringify(face), face])).values()],
|
|
647
|
+
assets: [...assets.values()]
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function parseGoogleFontCss(css, family, display) {
|
|
651
|
+
const faces = [];
|
|
652
|
+
for (const block of css.matchAll(/@font-face\s*\{([^}]*)\}/g)) {
|
|
653
|
+
const declarations = Object.fromEntries([...(block[1] ?? "").matchAll(/([\w-]+)\s*:\s*([^;]+);/g)].map((match) => [match[1], match[2].trim()]));
|
|
654
|
+
const source = declarations.src?.match(/url\(['"]?(https:\/\/fonts\.gstatic\.com\/[^'"()\s]+)['"]?\)\s*format\(['"]?(woff2?|truetype|opentype)['"]?\)/);
|
|
655
|
+
if (!source || declarations["font-family"]?.replaceAll(/["']/g, "").toLowerCase() !== family.toLowerCase()) continue;
|
|
656
|
+
faces.push({
|
|
657
|
+
family,
|
|
658
|
+
descriptors: {
|
|
659
|
+
src: `url("${source[1]}") format("${source[2]}")`,
|
|
660
|
+
fontWeight: declarations["font-weight"] ?? 400,
|
|
661
|
+
fontStyle: declarations["font-style"] ?? "normal",
|
|
662
|
+
fontDisplay: display,
|
|
663
|
+
...declarations["unicode-range"] ? { unicodeRange: declarations["unicode-range"] } : {}
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return faces;
|
|
668
|
+
}
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/component-overrides.ts
|
|
671
|
+
function resolveComponentOverrides(defaults, overrides, disabledFooterPath) {
|
|
672
|
+
const resolved = { ...defaults };
|
|
673
|
+
for (const [name, override] of Object.entries(overrides ?? {})) if (name === "Footer" && override === false) resolved.Footer = disabledFooterPath;
|
|
674
|
+
else if (typeof override === "string") resolved[name] = override;
|
|
675
|
+
return resolved;
|
|
676
|
+
}
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/site-icons.ts
|
|
679
|
+
const ICON_DIRECTORY = "_cookbook/icons";
|
|
680
|
+
const DEFAULT_ICON_BACKGROUND = "#4b6dc3";
|
|
681
|
+
const SUPPORTED_SOURCE_FORMATS = /* @__PURE__ */ new Set([
|
|
682
|
+
"avif",
|
|
683
|
+
"gif",
|
|
684
|
+
"heif",
|
|
685
|
+
"jpeg",
|
|
686
|
+
"jpg",
|
|
687
|
+
"png",
|
|
688
|
+
"svg",
|
|
689
|
+
"webp"
|
|
690
|
+
]);
|
|
691
|
+
async function createSiteIcons({ base, root, site = {}, themeColors }) {
|
|
692
|
+
const configured = site.favicon;
|
|
693
|
+
const source = typeof configured === "string" ? configured : configured?.source;
|
|
694
|
+
const sourcePath = source ? resolveSourcePath(source, root) : fileURLToPath(new URL("./icons/favicon.svg", import.meta.url));
|
|
695
|
+
const background = typeof configured === "object" && configured?.background ? configured.background : DEFAULT_ICON_BACKGROUND;
|
|
696
|
+
const input = await readFile(sourcePath).catch((error) => {
|
|
697
|
+
throw new Error(`Unable to read site.favicon source at ${sourcePath}: ${errorMessage$1(error)}`);
|
|
698
|
+
});
|
|
699
|
+
const format = (await sharp(input, { animated: false }).metadata()).format?.toLowerCase();
|
|
700
|
+
if (!format || !SUPPORTED_SOURCE_FORMATS.has(format)) throw new Error("site.favicon must point to an SVG, PNG, JPEG, WebP, AVIF, or GIF image.");
|
|
701
|
+
const definitions = [
|
|
702
|
+
await pngAsset(input, base, "favicon-32x32.png", 32),
|
|
703
|
+
await safePngAsset(input, base, "apple-touch-icon.png", 180, background),
|
|
704
|
+
await pngAsset(input, base, "icon-192x192.png", 192),
|
|
705
|
+
await pngAsset(input, base, "icon-512x512.png", 512),
|
|
706
|
+
await safePngAsset(input, base, "icon-192x192-maskable.png", 192, background),
|
|
707
|
+
await safePngAsset(input, base, "icon-512x512-maskable.png", 512, background)
|
|
708
|
+
];
|
|
709
|
+
const scalable = format === "svg" ? asset(base, "favicon.svg", input, "image/svg+xml") : void 0;
|
|
710
|
+
const faviconPath = `/${ICON_DIRECTORY}/${scalable ? "favicon.svg" : "favicon-32x32.png"}`;
|
|
711
|
+
const iconEntries = [
|
|
712
|
+
{
|
|
713
|
+
src: pathWithBase(base, `/${ICON_DIRECTORY}/icon-192x192.png`),
|
|
714
|
+
sizes: "192x192",
|
|
715
|
+
type: "image/png",
|
|
716
|
+
purpose: "any"
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
src: pathWithBase(base, `/${ICON_DIRECTORY}/icon-512x512.png`),
|
|
720
|
+
sizes: "512x512",
|
|
721
|
+
type: "image/png",
|
|
722
|
+
purpose: "any"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
src: pathWithBase(base, `/${ICON_DIRECTORY}/icon-192x192-maskable.png`),
|
|
726
|
+
sizes: "192x192",
|
|
727
|
+
type: "image/png",
|
|
728
|
+
purpose: "maskable"
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
src: pathWithBase(base, `/${ICON_DIRECTORY}/icon-512x512-maskable.png`),
|
|
732
|
+
sizes: "512x512",
|
|
733
|
+
type: "image/png",
|
|
734
|
+
purpose: "maskable"
|
|
735
|
+
},
|
|
736
|
+
...scalable ? [{
|
|
737
|
+
src: scalable.publicPath,
|
|
738
|
+
sizes: "any",
|
|
739
|
+
type: "image/svg+xml",
|
|
740
|
+
purpose: "any"
|
|
741
|
+
}] : []
|
|
742
|
+
];
|
|
743
|
+
const manifest = asset(base, "site.webmanifest", Buffer.from(`${JSON.stringify({
|
|
744
|
+
name: site.title ?? "Documentation",
|
|
745
|
+
short_name: site.title ?? "Documentation",
|
|
746
|
+
...site.description ? { description: site.description } : {},
|
|
747
|
+
id: normalizedBase(base),
|
|
748
|
+
start_url: normalizedBase(base),
|
|
749
|
+
scope: normalizedBase(base),
|
|
750
|
+
display: "standalone",
|
|
751
|
+
background_color: background,
|
|
752
|
+
theme_color: themeColors.light,
|
|
753
|
+
icons: iconEntries
|
|
754
|
+
}, null, 2)}\n`), "application/manifest+json");
|
|
755
|
+
const favicon32 = definitions[0];
|
|
756
|
+
const appleTouchIcon = definitions[1];
|
|
757
|
+
if (!favicon32 || !appleTouchIcon) throw new Error("Cookbook failed to generate the required site icons.");
|
|
758
|
+
return {
|
|
759
|
+
assets: [
|
|
760
|
+
...definitions,
|
|
761
|
+
...scalable ? [scalable] : [],
|
|
762
|
+
manifest
|
|
763
|
+
],
|
|
764
|
+
faviconPath,
|
|
765
|
+
head: [
|
|
766
|
+
{
|
|
767
|
+
tag: "link",
|
|
768
|
+
attrs: {
|
|
769
|
+
rel: "icon",
|
|
770
|
+
href: favicon32.publicPath,
|
|
771
|
+
sizes: "32x32",
|
|
772
|
+
type: favicon32.contentType
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
...scalable ? [{
|
|
776
|
+
tag: "link",
|
|
777
|
+
attrs: {
|
|
778
|
+
rel: "icon",
|
|
779
|
+
href: scalable.publicPath,
|
|
780
|
+
sizes: "any",
|
|
781
|
+
type: scalable.contentType
|
|
782
|
+
}
|
|
783
|
+
}] : [],
|
|
784
|
+
{
|
|
785
|
+
tag: "link",
|
|
786
|
+
attrs: {
|
|
787
|
+
rel: "apple-touch-icon",
|
|
788
|
+
href: appleTouchIcon.publicPath,
|
|
789
|
+
sizes: "180x180"
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
tag: "link",
|
|
794
|
+
attrs: {
|
|
795
|
+
rel: "manifest",
|
|
796
|
+
href: manifest.publicPath
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
tag: "meta",
|
|
801
|
+
attrs: {
|
|
802
|
+
name: "theme-color",
|
|
803
|
+
content: themeColors.light,
|
|
804
|
+
media: "(prefers-color-scheme: light)"
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
tag: "meta",
|
|
809
|
+
attrs: {
|
|
810
|
+
name: "theme-color",
|
|
811
|
+
content: themeColors.dark,
|
|
812
|
+
media: "(prefers-color-scheme: dark)"
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
],
|
|
816
|
+
sourcePath
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
async function pngAsset(input, base, name, size) {
|
|
820
|
+
return asset(base, name, await sharp(input, {
|
|
821
|
+
animated: false,
|
|
822
|
+
density: 512
|
|
823
|
+
}).resize(size, size, {
|
|
824
|
+
fit: "contain",
|
|
825
|
+
background: {
|
|
826
|
+
r: 0,
|
|
827
|
+
g: 0,
|
|
828
|
+
b: 0,
|
|
829
|
+
alpha: 0
|
|
830
|
+
}
|
|
831
|
+
}).png().toBuffer(), "image/png");
|
|
832
|
+
}
|
|
833
|
+
async function safePngAsset(input, base, name, size, background) {
|
|
834
|
+
const safeSize = Math.round(size * .8);
|
|
835
|
+
const foreground = await sharp(input, {
|
|
836
|
+
animated: false,
|
|
837
|
+
density: 512
|
|
838
|
+
}).resize(safeSize, safeSize, {
|
|
839
|
+
fit: "contain",
|
|
840
|
+
background: {
|
|
841
|
+
r: 0,
|
|
842
|
+
g: 0,
|
|
843
|
+
b: 0,
|
|
844
|
+
alpha: 0
|
|
845
|
+
}
|
|
846
|
+
}).png().toBuffer();
|
|
847
|
+
return asset(base, name, await sharp({ create: {
|
|
848
|
+
width: size,
|
|
849
|
+
height: size,
|
|
850
|
+
channels: 4,
|
|
851
|
+
background
|
|
852
|
+
} }).composite([{
|
|
853
|
+
input: foreground,
|
|
854
|
+
gravity: "center"
|
|
855
|
+
}]).png().toBuffer().catch((error) => {
|
|
856
|
+
throw new Error(`Unable to use site.favicon.background ${JSON.stringify(background)}: ${errorMessage$1(error)}`);
|
|
857
|
+
}), "image/png");
|
|
858
|
+
}
|
|
859
|
+
function asset(base, name, body, contentType) {
|
|
860
|
+
const outputPath = `${ICON_DIRECTORY}/${name}`;
|
|
861
|
+
return {
|
|
862
|
+
body,
|
|
863
|
+
contentType,
|
|
864
|
+
outputPath,
|
|
865
|
+
publicPath: pathWithBase(base, `/${outputPath}`)
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function resolveSourcePath(source, root) {
|
|
869
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(source)) throw new Error("site.favicon must reference a local image path.");
|
|
870
|
+
return isAbsolute(source) ? source : resolve(root, source);
|
|
871
|
+
}
|
|
872
|
+
function pathWithBase(base, pathname) {
|
|
873
|
+
const prefix = normalizedBase(base);
|
|
874
|
+
return `${prefix === "/" ? "" : prefix.replace(/\/$/, "")}${pathname}`;
|
|
875
|
+
}
|
|
876
|
+
function normalizedBase(base) {
|
|
877
|
+
const value = base.replace(/^\/+|\/+$/g, "");
|
|
878
|
+
return value ? `/${value}/` : "/";
|
|
879
|
+
}
|
|
880
|
+
function errorMessage$1(error) {
|
|
881
|
+
return error instanceof Error ? error.message : String(error);
|
|
882
|
+
}
|
|
883
|
+
//#endregion
|
|
884
|
+
//#region src/output-path.ts
|
|
885
|
+
function normalizedBasePath(base) {
|
|
886
|
+
const path = base.replace(/^\/+|\/+$/g, "");
|
|
887
|
+
return path ? `/${path}/` : "/";
|
|
888
|
+
}
|
|
889
|
+
function outputPathForPublicAsset(publicPath, base) {
|
|
890
|
+
const normalizedPublicPath = `/${publicPath.replace(/^\/+/, "")}`;
|
|
891
|
+
const prefix = normalizedBasePath(base);
|
|
892
|
+
if (!normalizedPublicPath.startsWith(prefix)) throw new Error(`Cookbook asset path ${publicPath} is outside the configured base ${base}.`);
|
|
893
|
+
const outputPath = normalizedPublicPath.slice(prefix.length);
|
|
894
|
+
if (!outputPath || outputPath.split("/").some((segment) => segment === "." || segment === "..")) throw new Error(`Cookbook asset path ${publicPath} is not safe to emit.`);
|
|
895
|
+
return outputPath;
|
|
896
|
+
}
|
|
897
|
+
//#endregion
|
|
898
|
+
//#region src/agent-discovery.ts
|
|
899
|
+
function sitePath(base, route) {
|
|
900
|
+
return `${base === "/" ? "" : `/${base.replace(/^\/+|\/+$/g, "")}`}${route === "/" ? "/" : `${route}/`}`;
|
|
901
|
+
}
|
|
902
|
+
function pageUrl(site, path) {
|
|
903
|
+
return site ? new URL(path, site).href : path;
|
|
904
|
+
}
|
|
905
|
+
function singleLine(value) {
|
|
906
|
+
return value.replace(/\s+/g, " ").trim();
|
|
907
|
+
}
|
|
908
|
+
function markdownLabel(value) {
|
|
909
|
+
return singleLine(value).replace(/[\\[\]]/g, "\\$&");
|
|
910
|
+
}
|
|
911
|
+
function renderLlmsTxt(graph) {
|
|
912
|
+
const { site, build } = graph.config;
|
|
913
|
+
const lines = [`# ${singleLine(site.title ?? "Documentation")}`, ""];
|
|
914
|
+
if (site.description) lines.push(`> ${singleLine(site.description)}`, "");
|
|
915
|
+
lines.push("## Documentation", "");
|
|
916
|
+
const discoverable = new Set(graph.routes.filter((route) => route.discoverable !== false && route.sitemap !== false).map((route) => route.route));
|
|
917
|
+
for (const entry of graph.entries) {
|
|
918
|
+
if (!discoverable.has(entry.route)) continue;
|
|
919
|
+
const url = pageUrl(site.url, sitePath(build.base, entry.route));
|
|
920
|
+
lines.push(`- [${markdownLabel(entry.title)}](${url})${entry.description ? `: ${singleLine(entry.description)}` : ""}${site.seo?.copyPage === false ? "" : ` ([Markdown](${pageUrl(site.url, agentPagePath(entry.route, build.base))}))`}`);
|
|
921
|
+
}
|
|
922
|
+
return `${lines.join("\n")}\n`;
|
|
923
|
+
}
|
|
924
|
+
function renderRobotsTxt(site, sitemap = true) {
|
|
925
|
+
return `User-agent: *\nAllow: /\n${sitemap ? `Sitemap: ${new URL("/sitemap-index.xml", site).href}\n` : ""}`;
|
|
926
|
+
}
|
|
927
|
+
async function writeUnlessPresent(path, body) {
|
|
928
|
+
try {
|
|
929
|
+
await writeFile(path, body, { flag: "wx" });
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") return;
|
|
932
|
+
throw error;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async function writeAgentDiscovery(output, graph) {
|
|
936
|
+
await writeUnlessPresent(join(output, "llms.txt"), renderLlmsTxt(graph));
|
|
937
|
+
await mkdir(join(output, "_cookbook/pages"), { recursive: true });
|
|
938
|
+
const pages = [];
|
|
939
|
+
for (const entry of graph.entries) {
|
|
940
|
+
if (entry.frontmatter.draft) continue;
|
|
941
|
+
const route = graph.routes.find((route) => route.route === entry.route);
|
|
942
|
+
const markdown = !entry.frontmatter.draft && graph.config.site.seo?.copyPage !== false ? agentPagePath(entry.route, graph.config.build.base) : void 0;
|
|
943
|
+
if (markdown) await writeFile(join(output, outputPathForPublicAsset(markdown, graph.config.build.base)), renderAgentMarkdown(entry, graph.config));
|
|
944
|
+
pages.push({
|
|
945
|
+
route: entry.route,
|
|
946
|
+
url: pageUrl(graph.config.site.url, sitePath(graph.config.build.base, entry.route)),
|
|
947
|
+
canonical: route.canonical,
|
|
948
|
+
index: route.indexable !== false,
|
|
949
|
+
discoverable: route.discoverable !== false,
|
|
950
|
+
sitemap: route.sitemap !== false,
|
|
951
|
+
...markdown ? { markdown } : {}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
await writeFile(join(output, "_cookbook/publishing.json"), JSON.stringify({
|
|
955
|
+
base: graph.config.build.base,
|
|
956
|
+
site: graph.config.site.url,
|
|
957
|
+
pages
|
|
958
|
+
}, null, 2) + "\n");
|
|
959
|
+
if (graph.config.build.base === "/" && graph.config.site.url) await writeUnlessPresent(join(output, "robots.txt"), renderRobotsTxt(graph.config.site.url, graph.routes.some((route) => route.sitemap !== false)));
|
|
960
|
+
}
|
|
961
|
+
//#endregion
|
|
962
|
+
//#region src/integration.ts
|
|
963
|
+
const stylingRuntimeError = "Cookbook styles are build/server-only. Do not import @tenphi/cookbook/styling, Tasty, or Glaze in browser scripts or client:* islands. Render styled markup on the server and attach a small client script for interactions.";
|
|
964
|
+
function isStylingRuntime(id) {
|
|
965
|
+
const path = id.replaceAll("\\", "/");
|
|
966
|
+
return /^@tenphi\/(?:tasty|glaze)(?:\/|$)/.test(path) || /^@tenphi\/(?:cookbook|renderer)\/styling$/.test(path) || /(?:^|\/)node_modules\/@tenphi\/(?:tasty|glaze)(?:\/|$)/.test(path);
|
|
967
|
+
}
|
|
968
|
+
const packageRequire = createRequire(import.meta.url);
|
|
969
|
+
const tastyStaticMiddleware = packageRequire.resolve("@tenphi/tasty/ssr/astro-middleware-static");
|
|
970
|
+
const tastyExtractStaticMiddleware = packageRequire.resolve("@tenphi/tasty/ssr/astro-middleware-extract-static");
|
|
971
|
+
const astroReactServer = packageRequire.resolve("@astrojs/react/server.js");
|
|
972
|
+
const astroReactClient = packageRequire.resolve("@astrojs/react/client.js");
|
|
973
|
+
const astroReactIntegration = packageRequire.resolve("@astrojs/react");
|
|
974
|
+
const importNative = new Function("specifier", "return import(specifier)");
|
|
975
|
+
function cookbook(options = {}) {
|
|
976
|
+
let integration;
|
|
977
|
+
return {
|
|
978
|
+
name: "cookbook",
|
|
979
|
+
hooks: {
|
|
980
|
+
"astro:config:setup": async (context) => {
|
|
981
|
+
const project = await resolveDocsProject({
|
|
982
|
+
...options,
|
|
983
|
+
root: options.root ?? fileURLToPath(context.config.root)
|
|
984
|
+
});
|
|
985
|
+
if (project.configFile) context.addWatchFile(project.configFile);
|
|
986
|
+
integration = configuredCookbook({
|
|
987
|
+
config: project.config,
|
|
988
|
+
root: project.root,
|
|
989
|
+
...options.frontmatterSchema ? { frontmatterSchema: options.frontmatterSchema } : {}
|
|
990
|
+
});
|
|
991
|
+
await callInner([integration], "astro:config:setup", context);
|
|
992
|
+
},
|
|
993
|
+
"astro:config:done": async (context) => {
|
|
994
|
+
if (integration) await callInner([integration], "astro:config:done", context);
|
|
995
|
+
},
|
|
996
|
+
"astro:server:setup": async (context) => {
|
|
997
|
+
if (integration) await callInner([integration], "astro:server:setup", context);
|
|
998
|
+
},
|
|
999
|
+
"astro:build:start": async (context) => {
|
|
1000
|
+
if (integration) await callInner([integration], "astro:build:start", context);
|
|
1001
|
+
},
|
|
1002
|
+
"astro:build:done": async (context) => {
|
|
1003
|
+
if (integration) await callInner([integration], "astro:build:done", context);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function configuredCookbook(options) {
|
|
1009
|
+
const docsTheme = resolveDocsTheme(options.config?.theme);
|
|
1010
|
+
if (docsTheme.diagnostics.some((diagnostic) => diagnostic.severity === "error")) throw new Error(docsTheme.diagnostics.map((diagnostic) => diagnostic.message).join("\n"));
|
|
1011
|
+
configureTastyTheme(options.config?.theme, docsTheme);
|
|
1012
|
+
configureComponentStyles({
|
|
1013
|
+
...options.config?.theme?.customStyles,
|
|
1014
|
+
...options.config?.theme?.styles
|
|
1015
|
+
});
|
|
1016
|
+
const headerPath = fileURLToPath(new URL("./overrides/Header.astro", import.meta.url));
|
|
1017
|
+
const footerPath = fileURLToPath(new URL("./overrides/Footer.astro", import.meta.url));
|
|
1018
|
+
const heroPath = fileURLToPath(new URL("./overrides/Hero.astro", import.meta.url));
|
|
1019
|
+
const emptyFooterPath = fileURLToPath(new URL("./overrides/EmptyFooter.astro", import.meta.url));
|
|
1020
|
+
const sidebarPath = fileURLToPath(new URL("./overrides/Sidebar.astro", import.meta.url));
|
|
1021
|
+
const mobileMenuFooterPath = fileURLToPath(new URL("./overrides/MobileMenuFooter.astro", import.meta.url));
|
|
1022
|
+
const mobileMenuTogglePath = fileURLToPath(new URL("./overrides/MobileMenuToggle.astro", import.meta.url));
|
|
1023
|
+
const markdownContentPath = fileURLToPath(new URL("./overrides/MarkdownContent.astro", import.meta.url));
|
|
1024
|
+
const themeSelectPath = fileURLToPath(new URL("./overrides/ThemeSelect.astro", import.meta.url));
|
|
1025
|
+
const components = resolveComponentOverrides({
|
|
1026
|
+
DraftContentNotice: fileURLToPath(new URL("./overrides/DraftContentNotice.astro", import.meta.url)),
|
|
1027
|
+
Search: fileURLToPath(new URL("./overrides/Search.astro", import.meta.url)),
|
|
1028
|
+
Head: fileURLToPath(new URL("./overrides/Head.astro", import.meta.url)),
|
|
1029
|
+
LanguageSelect: fileURLToPath(new URL("./overrides/LanguageSelect.astro", import.meta.url)),
|
|
1030
|
+
PageFrame: fileURLToPath(new URL("./overrides/PageFrame.astro", import.meta.url)),
|
|
1031
|
+
MobileTableOfContents: fileURLToPath(new URL("./overrides/MobileTableOfContents.astro", import.meta.url)),
|
|
1032
|
+
Footer: footerPath,
|
|
1033
|
+
Header: headerPath,
|
|
1034
|
+
Hero: heroPath,
|
|
1035
|
+
MarkdownContent: markdownContentPath,
|
|
1036
|
+
Sidebar: sidebarPath,
|
|
1037
|
+
MobileMenuFooter: mobileMenuFooterPath,
|
|
1038
|
+
MobileMenuToggle: mobileMenuTogglePath,
|
|
1039
|
+
ThemeSelect: themeSelectPath,
|
|
1040
|
+
SocialIcons: fileURLToPath(new URL("./overrides/SocialIcons.astro", import.meta.url)),
|
|
1041
|
+
SiteTitle: fileURLToPath(new URL("./components/SiteTitle.astro", import.meta.url)),
|
|
1042
|
+
EditLink: fileURLToPath(new URL("./components/EditLink.astro", import.meta.url)),
|
|
1043
|
+
LastUpdated: fileURLToPath(new URL("./components/LastUpdated.astro", import.meta.url)),
|
|
1044
|
+
Pagination: fileURLToPath(new URL("./components/Pagination.astro", import.meta.url)),
|
|
1045
|
+
TableOfContents: fileURLToPath(new URL("./components/TableOfContents.astro", import.meta.url))
|
|
1046
|
+
}, options.config?.components?.overrides, emptyFooterPath);
|
|
1047
|
+
const navigation = resolveNavigationLayout(options.config?.navigation);
|
|
1048
|
+
const tasty = tastyIntegration({
|
|
1049
|
+
islands: false,
|
|
1050
|
+
css: { mode: "extract" }
|
|
1051
|
+
});
|
|
1052
|
+
let inner = [tasty];
|
|
1053
|
+
let projectRoot = options.root;
|
|
1054
|
+
let graphConfig = options.config;
|
|
1055
|
+
let graphBase = "/";
|
|
1056
|
+
let graph;
|
|
1057
|
+
let siteIconBase = "/";
|
|
1058
|
+
let siteIcons;
|
|
1059
|
+
let fontAssets = [];
|
|
1060
|
+
let siteLogo;
|
|
1061
|
+
async function loadGraph(refresh = false) {
|
|
1062
|
+
if (!graph || refresh) {
|
|
1063
|
+
graph = await createDocsGraph({
|
|
1064
|
+
...projectRoot ? { root: projectRoot } : {},
|
|
1065
|
+
...graphConfig ? { config: graphConfig } : {},
|
|
1066
|
+
base: graphBase
|
|
1067
|
+
});
|
|
1068
|
+
assertValidDocs(graph);
|
|
1069
|
+
await validatePluginFrontmatter(graph.entries, options.frontmatterSchema);
|
|
1070
|
+
}
|
|
1071
|
+
return graph;
|
|
1072
|
+
}
|
|
1073
|
+
async function loadSiteIcons() {
|
|
1074
|
+
if (!projectRoot) throw new Error("Cookbook cannot generate site icons without a project root.");
|
|
1075
|
+
return createSiteIcons({
|
|
1076
|
+
base: siteIconBase,
|
|
1077
|
+
root: projectRoot,
|
|
1078
|
+
...options.config?.site ? { site: options.config.site } : {},
|
|
1079
|
+
themeColors: {
|
|
1080
|
+
light: docsTheme.colors.surface.light ?? "#ffffff",
|
|
1081
|
+
dark: docsTheme.colors.surface.dark ?? "#20232a"
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
name: "cookbook",
|
|
1087
|
+
hooks: {
|
|
1088
|
+
"astro:config:setup": async (context) => {
|
|
1089
|
+
const react = (await importNative(pathToFileURL(astroReactIntegration).href)).default();
|
|
1090
|
+
projectRoot ??= fileURLToPath(context.config.root);
|
|
1091
|
+
const base = context.config.base;
|
|
1092
|
+
for (const [role, font] of Object.entries(options.config?.theme?.fonts ?? {})) {
|
|
1093
|
+
if (!font || typeof font === "string" || "google" in font) continue;
|
|
1094
|
+
for (const file of font.files) {
|
|
1095
|
+
const path = join(fileURLToPath(context.config.publicDir), file.src.slice(1));
|
|
1096
|
+
let isFile = false;
|
|
1097
|
+
try {
|
|
1098
|
+
isFile = (await stat(path)).isFile();
|
|
1099
|
+
} catch {}
|
|
1100
|
+
if (!isFile) throw new Error(`theme.fonts.${role}: ${file.src} must be a readable file in the site's public directory.`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const resolvedFonts = await resolveThemeFonts(options.config?.theme?.fonts, {
|
|
1104
|
+
base,
|
|
1105
|
+
cacheDir: join(fileURLToPath(context.config.cacheDir), "cookbook-fonts"),
|
|
1106
|
+
loading: options.config?.theme?.fontLoading ?? {},
|
|
1107
|
+
presets: docsTheme.presets,
|
|
1108
|
+
warn: (message) => context.logger.warn(message)
|
|
1109
|
+
});
|
|
1110
|
+
const fontFaces = resolvedFonts.faces;
|
|
1111
|
+
fontAssets = resolvedFonts.assets;
|
|
1112
|
+
const usedFamilies = Object.values(docsTheme.presets).map((preset) => preset.fontFamily ?? "");
|
|
1113
|
+
configureFontFaces(fontFaces, {
|
|
1114
|
+
display: options.config?.theme?.fontLoading?.display ?? "swap",
|
|
1115
|
+
onest: usedFamilies.some((family) => family.includes("Onest Variable")),
|
|
1116
|
+
mono: usedFamilies.some((family) => family.includes("JetBrains Mono Variable"))
|
|
1117
|
+
});
|
|
1118
|
+
const configuredSite = options.config?.site?.url;
|
|
1119
|
+
const astroSite = context.config.site ? new URL(context.config.site).href : void 0;
|
|
1120
|
+
if (configuredSite && astroSite && astroSite !== new URL(configuredSite).href) throw new Error(`Cookbook site.url (${configuredSite}) conflicts with Astro site (${astroSite}).`);
|
|
1121
|
+
if (configuredSite || astroSite) graphConfig = {
|
|
1122
|
+
...graphConfig,
|
|
1123
|
+
site: {
|
|
1124
|
+
...graphConfig?.site,
|
|
1125
|
+
url: configuredSite ?? astroSite
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
siteIconBase = base;
|
|
1129
|
+
graphBase = base;
|
|
1130
|
+
context.config.integrations.push(sitemap({
|
|
1131
|
+
serialize: (item) => {
|
|
1132
|
+
if (!graph?.config.locales) return item;
|
|
1133
|
+
const route = navigationPath(decodeURI(new URL(item.url).pathname), graphBase);
|
|
1134
|
+
return {
|
|
1135
|
+
...item,
|
|
1136
|
+
links: localeAlternates(route, graph.routes.filter((route) => route.sitemap !== false), graph.config).map(({ lang, route }) => ({
|
|
1137
|
+
lang,
|
|
1138
|
+
url: new URL(`${graphBase.replace(/\/$/, "")}${route === "/" ? "/" : `${route}/`}`, item.url).href
|
|
1139
|
+
}))
|
|
1140
|
+
};
|
|
1141
|
+
},
|
|
1142
|
+
filter: (url) => {
|
|
1143
|
+
const pathname = decodeURI(new URL(url).pathname);
|
|
1144
|
+
const route = navigationPath(pathname, graphBase);
|
|
1145
|
+
return graph?.routes.some((entry) => entry.route === route && entry.discoverable !== false && entry.sitemap !== false) ?? false;
|
|
1146
|
+
}
|
|
1147
|
+
}));
|
|
1148
|
+
siteIcons = await loadSiteIcons();
|
|
1149
|
+
siteLogo = await resolveSiteLogo(projectRoot, base, options.config?.site);
|
|
1150
|
+
registerCookbookMarkdownPlugins(context.config.markdown.processor);
|
|
1151
|
+
inner = [
|
|
1152
|
+
react,
|
|
1153
|
+
tasty,
|
|
1154
|
+
mdx()
|
|
1155
|
+
];
|
|
1156
|
+
let markdownRuntime = {
|
|
1157
|
+
image: context.config.image,
|
|
1158
|
+
markdown: {
|
|
1159
|
+
...context.config.markdown,
|
|
1160
|
+
syntaxHighlight: "shiki",
|
|
1161
|
+
shikiConfig: cookbookShikiConfig(context.config.markdown.shikiConfig)
|
|
1162
|
+
},
|
|
1163
|
+
srcDir: context.config.srcDir
|
|
1164
|
+
};
|
|
1165
|
+
let markdownRenderer;
|
|
1166
|
+
context.updateConfig({
|
|
1167
|
+
...configuredSite && !context.config.site ? { site: configuredSite } : {},
|
|
1168
|
+
base,
|
|
1169
|
+
output: "static",
|
|
1170
|
+
markdown: {
|
|
1171
|
+
syntaxHighlight: "shiki",
|
|
1172
|
+
shikiConfig: cookbookShikiConfig(context.config.markdown.shikiConfig)
|
|
1173
|
+
},
|
|
1174
|
+
vite: {
|
|
1175
|
+
ssr: { external: [
|
|
1176
|
+
"@tenphi/docs",
|
|
1177
|
+
"react",
|
|
1178
|
+
"react-dom"
|
|
1179
|
+
] },
|
|
1180
|
+
plugins: [
|
|
1181
|
+
{
|
|
1182
|
+
name: "cookbook-server-only-styling",
|
|
1183
|
+
enforce: "pre",
|
|
1184
|
+
resolveId(id, _importer, settings) {
|
|
1185
|
+
if (settings?.ssr || this.environment.config.consumer === "server") return;
|
|
1186
|
+
if (isStylingRuntime(id)) this.error(stylingRuntimeError);
|
|
1187
|
+
},
|
|
1188
|
+
generateBundle() {
|
|
1189
|
+
if (this.environment.config.consumer === "server") return;
|
|
1190
|
+
for (const id of this.getModuleIds()) if (isStylingRuntime(id)) this.error(stylingRuntimeError);
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
name: "cookbook-react-runtime",
|
|
1195
|
+
enforce: "post",
|
|
1196
|
+
configResolved(config) {
|
|
1197
|
+
for (const environment of Object.values(config.environments)) {
|
|
1198
|
+
if (environment.consumer === "server") environment.resolve.dedupe = environment.resolve.dedupe.filter((id) => id !== "react" && id !== "react-dom");
|
|
1199
|
+
if (environment.consumer === "client") environment.optimizeDeps.include = environment.optimizeDeps.include?.map((id) => /^react(?:-dom)?(?:\/|$)/.test(id) ? packageRequire.resolve(id) : id) ?? [];
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
resolveId(id, _importer, settings) {
|
|
1203
|
+
if (!settings?.ssr && this.environment.config.consumer !== "server" && /^react(?:-dom)?(?:\/|$)/.test(id)) return packageRequire.resolve(id);
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
componentOverridesPlugin(components, fileURLToPath(context.config.root)),
|
|
1207
|
+
virtualDocsPlugin(async () => {
|
|
1208
|
+
const loaded = await loadGraph(true);
|
|
1209
|
+
return {
|
|
1210
|
+
entries: await Promise.all(loaded.entries.map(async (entry) => {
|
|
1211
|
+
const hero = await resolveHeroMetadata(entry);
|
|
1212
|
+
if (entry.trust === "mdx" && entry.sourcePath.toLowerCase().endsWith(".mdx")) return {
|
|
1213
|
+
...entry,
|
|
1214
|
+
hero,
|
|
1215
|
+
mdx: true
|
|
1216
|
+
};
|
|
1217
|
+
const { image, markdown, srcDir } = markdownRuntime;
|
|
1218
|
+
markdownRenderer ??= markdown.processor.createRenderer({
|
|
1219
|
+
image,
|
|
1220
|
+
syntaxHighlight: markdown.syntaxHighlight,
|
|
1221
|
+
shikiConfig: markdown.shikiConfig,
|
|
1222
|
+
gfm: markdown.gfm,
|
|
1223
|
+
smartypants: markdown.smartypants
|
|
1224
|
+
});
|
|
1225
|
+
const rendered = await (await markdownRenderer).render(entry.transformedBody, {
|
|
1226
|
+
frontmatter: entry.frontmatter,
|
|
1227
|
+
fileURL: docsContentUrl(entry.route, srcDir)
|
|
1228
|
+
});
|
|
1229
|
+
return {
|
|
1230
|
+
...entry,
|
|
1231
|
+
hero,
|
|
1232
|
+
rendered: {
|
|
1233
|
+
html: rendered.code,
|
|
1234
|
+
headings: rendered.metadata.headings
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
})),
|
|
1238
|
+
routes: loaded.routes,
|
|
1239
|
+
redirects: loaded.redirects,
|
|
1240
|
+
tableOfContents: loaded.config.tableOfContents,
|
|
1241
|
+
site: documentedSite(loaded),
|
|
1242
|
+
base: loaded.config.build.base,
|
|
1243
|
+
search: loaded.config.search.enabled || typeof options.config?.components?.overrides?.Search === "string",
|
|
1244
|
+
logo: siteLogo?.logo,
|
|
1245
|
+
locales: loaded.config.locales,
|
|
1246
|
+
defaultLocale: loaded.config.defaultLocale,
|
|
1247
|
+
translations: loaded.config.translations,
|
|
1248
|
+
head: [...siteIcons?.head ?? [], ...options.config?.head ?? []]
|
|
1249
|
+
};
|
|
1250
|
+
}, navigation, () => [
|
|
1251
|
+
projectRoot,
|
|
1252
|
+
...(graphConfig?.content?.sources ?? []).flatMap((source) => "package" in source ? [] : [resolve(projectRoot, source.root ?? ".")]),
|
|
1253
|
+
...graph?.entries.map((entry) => entry.sourceRoot) ?? []
|
|
1254
|
+
])
|
|
1255
|
+
],
|
|
1256
|
+
resolve: { alias: [
|
|
1257
|
+
{
|
|
1258
|
+
find: "@tenphi/tasty/ssr/astro-middleware-static",
|
|
1259
|
+
replacement: tastyStaticMiddleware
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
find: "@tenphi/tasty/ssr/astro-middleware-extract-static",
|
|
1263
|
+
replacement: tastyExtractStaticMiddleware
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
find: "@astrojs/react/server.js",
|
|
1267
|
+
replacement: astroReactServer
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
find: "@astrojs/react/client.js",
|
|
1271
|
+
replacement: astroReactClient
|
|
1272
|
+
}
|
|
1273
|
+
] }
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
await callInner(inner, "astro:config:setup", context);
|
|
1277
|
+
graph = await createDocsGraph({
|
|
1278
|
+
root: projectRoot,
|
|
1279
|
+
...graphConfig ? { config: graphConfig } : {},
|
|
1280
|
+
base: graphBase
|
|
1281
|
+
});
|
|
1282
|
+
assertValidDocs(graph);
|
|
1283
|
+
context.injectRoute({
|
|
1284
|
+
pattern: "[...route]",
|
|
1285
|
+
entrypoint: new URL("./routes/DocsPage.astro", import.meta.url),
|
|
1286
|
+
prerender: true
|
|
1287
|
+
});
|
|
1288
|
+
if (!graph.entryByRoute("/404")) context.injectRoute({
|
|
1289
|
+
pattern: "404",
|
|
1290
|
+
entrypoint: new URL("./routes/NotFound.astro", import.meta.url),
|
|
1291
|
+
prerender: true
|
|
1292
|
+
});
|
|
1293
|
+
context.config.integrations.push({
|
|
1294
|
+
name: "cookbook-markdown-renderer",
|
|
1295
|
+
hooks: { "astro:config:setup": ({ config }) => {
|
|
1296
|
+
markdownRuntime = {
|
|
1297
|
+
image: config.image,
|
|
1298
|
+
markdown: {
|
|
1299
|
+
...config.markdown,
|
|
1300
|
+
syntaxHighlight: "shiki",
|
|
1301
|
+
shikiConfig: cookbookShikiConfig(config.markdown.shikiConfig)
|
|
1302
|
+
},
|
|
1303
|
+
srcDir: config.srcDir
|
|
1304
|
+
};
|
|
1305
|
+
} }
|
|
1306
|
+
});
|
|
1307
|
+
},
|
|
1308
|
+
"astro:config:done": async (context) => {
|
|
1309
|
+
configureCodeHighlighting(cookbookShikiConfig(context.config.markdown.shikiConfig));
|
|
1310
|
+
await callInner(inner, "astro:config:done", context);
|
|
1311
|
+
},
|
|
1312
|
+
"astro:server:setup": async ({ server, logger }) => {
|
|
1313
|
+
let assets = docsAssetMap(await loadGraph());
|
|
1314
|
+
if (siteLogo?.sourcePaths.length) {
|
|
1315
|
+
server.watcher.add(siteLogo.sourcePaths);
|
|
1316
|
+
server.watcher.on("change", (path) => {
|
|
1317
|
+
if (siteLogo?.sourcePaths.includes(path)) server.restart();
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
if (options.config?.site?.favicon && siteIcons) {
|
|
1321
|
+
server.watcher.add(siteIcons.sourcePath);
|
|
1322
|
+
server.watcher.on("change", async (changedPath) => {
|
|
1323
|
+
if (changedPath !== siteIcons?.sourcePath) return;
|
|
1324
|
+
try {
|
|
1325
|
+
siteIcons = await loadSiteIcons();
|
|
1326
|
+
server.ws.send({ type: "full-reload" });
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
logger.error(errorMessage(error));
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
server.middlewares.use(async (request, response, next) => {
|
|
1333
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
1334
|
+
next();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const requestPathname = requestPath(request.url);
|
|
1338
|
+
const pathname = graphBase !== "/" && !requestPathname.startsWith(graphBase) ? `${graphBase.replace(/\/$/, "")}${requestPathname}` : requestPathname;
|
|
1339
|
+
if (pathname.includes("/_cookbook/pages/")) try {
|
|
1340
|
+
const current = await loadGraph(true);
|
|
1341
|
+
const entry = current.entries.find((entry) => !entry.frontmatter.draft && agentPagePath(entry.route, current.config.build.base) === pathname);
|
|
1342
|
+
if (entry && current.config.site.seo?.copyPage !== false) {
|
|
1343
|
+
response.statusCode = 200;
|
|
1344
|
+
response.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
1345
|
+
response.setHeader("X-Robots-Tag", "noindex");
|
|
1346
|
+
response.end(request.method === "HEAD" ? void 0 : renderAgentMarkdown(entry, current.config));
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
logger.error(errorMessage(error));
|
|
1351
|
+
}
|
|
1352
|
+
const fontAsset = [...fontAssets, ...siteLogo?.assets ?? []].find((asset) => asset.publicPath === pathname);
|
|
1353
|
+
if (fontAsset) {
|
|
1354
|
+
response.statusCode = 200;
|
|
1355
|
+
response.setHeader("Content-Type", fontAsset.contentType);
|
|
1356
|
+
response.setHeader("Content-Length", fontAsset.body.byteLength);
|
|
1357
|
+
response.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1358
|
+
response.end(request.method === "HEAD" ? void 0 : fontAsset.body);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const siteIcon = siteIcons?.assets.find((asset) => asset.publicPath === pathname);
|
|
1362
|
+
if (siteIcon) {
|
|
1363
|
+
response.statusCode = 200;
|
|
1364
|
+
response.setHeader("Content-Type", siteIcon.contentType);
|
|
1365
|
+
response.setHeader("Content-Length", siteIcon.body.byteLength);
|
|
1366
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
1367
|
+
response.end(request.method === "HEAD" ? void 0 : siteIcon.body);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (!pathname.includes("/_tasty-assets/")) {
|
|
1371
|
+
next();
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
let asset = assets.get(pathname);
|
|
1375
|
+
if (!asset) {
|
|
1376
|
+
try {
|
|
1377
|
+
assets = docsAssetMap(await loadGraph(true));
|
|
1378
|
+
} catch {
|
|
1379
|
+
next();
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
asset = assets.get(pathname);
|
|
1383
|
+
}
|
|
1384
|
+
if (!asset?.sourcePath) {
|
|
1385
|
+
next();
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
try {
|
|
1389
|
+
const body = await readFile(asset.sourcePath);
|
|
1390
|
+
response.statusCode = 200;
|
|
1391
|
+
response.setHeader("Content-Type", assetContentType(pathname));
|
|
1392
|
+
response.setHeader("Content-Length", body.byteLength);
|
|
1393
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
1394
|
+
response.end(request.method === "HEAD" ? void 0 : body);
|
|
1395
|
+
} catch {
|
|
1396
|
+
next();
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
},
|
|
1400
|
+
"astro:build:start": async (context) => {
|
|
1401
|
+
siteIcons = await loadSiteIcons();
|
|
1402
|
+
await loadGraph();
|
|
1403
|
+
await callInner(inner, "astro:build:start", context);
|
|
1404
|
+
},
|
|
1405
|
+
"astro:build:done": async (context) => {
|
|
1406
|
+
await callInner(inner, "astro:build:done", context);
|
|
1407
|
+
const output = fileURLToPath(context.dir);
|
|
1408
|
+
const anatomyNames = /* @__PURE__ */ new Set();
|
|
1409
|
+
for (const relativePath of await readdir(output, { recursive: true })) {
|
|
1410
|
+
if (extname(relativePath) !== ".html") continue;
|
|
1411
|
+
const path = join(output, relativePath);
|
|
1412
|
+
const html = await readFile(path, "utf8");
|
|
1413
|
+
for (const match of html.matchAll(/\bdata-tasty-anatomy="([^"]+)"/g)) anatomyNames.add(match[1]);
|
|
1414
|
+
const sanitized = html.replace(/\s*<link\b(?=[^>]*rel="stylesheet")(?=[^>]*href="data:text\/css,")[^>]*>/g, "").replace(/\s*<style>\s*<\/style>/g, "").replace(/<style>astro-island,astro-slot,astro-static-slot\{display:contents\}<\/style>/g, "");
|
|
1415
|
+
assertTastyOutput(sanitized, relativePath);
|
|
1416
|
+
if (sanitized !== html) await writeFile(path, sanitized);
|
|
1417
|
+
}
|
|
1418
|
+
for (const name of unusedCustomStyleNames(options.config?.theme?.customStyles, anatomyNames)) context.logger.warn(`theme.customStyles.${name} did not match a component style resolver or rendered data-tasty-anatomy attribute. Check that its name matches defineComponent() or resolveComponentStyles().`);
|
|
1419
|
+
if (graph?.config.search.enabled !== false) try {
|
|
1420
|
+
const { index, errors } = await pagefind.createIndex();
|
|
1421
|
+
if (errors.length || !index) throw new Error(errors.join("\n") || "Pagefind did not create an index.");
|
|
1422
|
+
const added = await index.addDirectory({ path: output });
|
|
1423
|
+
if (added.errors.length) throw new Error(added.errors.join("\n"));
|
|
1424
|
+
const written = await index.writeFiles({ outputPath: join(output, "pagefind") });
|
|
1425
|
+
if (written.errors.length) throw new Error(written.errors.join("\n"));
|
|
1426
|
+
} finally {
|
|
1427
|
+
await pagefind.close();
|
|
1428
|
+
}
|
|
1429
|
+
const pagefindOutput = join(output, "pagefind");
|
|
1430
|
+
if (existsSync(pagefindOutput)) {
|
|
1431
|
+
for (const name of await readdir(pagefindOutput)) if (extname(name) === ".css") await unlink(join(pagefindOutput, name));
|
|
1432
|
+
}
|
|
1433
|
+
for (const relativePath of await readdir(output, { recursive: true })) if (extname(relativePath) === ".css" && !/^tasty\.[^/]+\.css$/.test(relativePath.split(/[/\\]/).at(-1) ?? "")) throw new Error(`Non-Tasty stylesheet found in ${relativePath}. Use theme.styles or Tasty components for visual changes.`);
|
|
1434
|
+
for (const asset of [
|
|
1435
|
+
...siteIcons?.assets ?? [],
|
|
1436
|
+
...fontAssets,
|
|
1437
|
+
...siteLogo?.assets ?? []
|
|
1438
|
+
]) {
|
|
1439
|
+
const target = join(output, asset.outputPath);
|
|
1440
|
+
await mkdir(dirname(target), { recursive: true });
|
|
1441
|
+
await writeFile(target, asset.body);
|
|
1442
|
+
}
|
|
1443
|
+
if (!graph) return;
|
|
1444
|
+
for (const asset of graph.assets) {
|
|
1445
|
+
if (!asset.sourcePath || !asset.publicPath) continue;
|
|
1446
|
+
const target = join(output, outputPathForPublicAsset(asset.publicPath, graph.config.build.base));
|
|
1447
|
+
await mkdir(dirname(target), { recursive: true });
|
|
1448
|
+
await cp(asset.sourcePath, target);
|
|
1449
|
+
}
|
|
1450
|
+
await writeAgentDiscovery(output, graph);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function registerCookbookMarkdownPlugins(processor) {
|
|
1456
|
+
if (processor.name === "unified") {
|
|
1457
|
+
const options = processor.options;
|
|
1458
|
+
const plugins = Array.isArray(options.rehypePlugins) ? options.rehypePlugins : [];
|
|
1459
|
+
if (!plugins.includes(rehypeMermaid)) plugins.push(rehypeMermaid);
|
|
1460
|
+
if (!plugins.includes(rehypeAlerts)) plugins.push(rehypeAlerts);
|
|
1461
|
+
if (!plugins.includes(rehypeTableScroll)) plugins.push(rehypeTableScroll);
|
|
1462
|
+
if (!plugins.includes(rehypePageAffordances)) plugins.push(rehypePageAffordances);
|
|
1463
|
+
options.rehypePlugins = plugins;
|
|
1464
|
+
} else if (processor.name === "satteri") {
|
|
1465
|
+
const options = processor.options;
|
|
1466
|
+
const plugins = Array.isArray(options.hastPlugins) ? options.hastPlugins : [];
|
|
1467
|
+
if (!plugins.includes(satteriMermaid)) plugins.push(satteriMermaid);
|
|
1468
|
+
if (!plugins.includes(satteriAlerts)) plugins.push(satteriAlerts);
|
|
1469
|
+
if (!plugins.includes(satteriTableScroll)) plugins.push(satteriTableScroll);
|
|
1470
|
+
if (!plugins.includes(satteriPageAffordances)) plugins.push(satteriPageAffordances);
|
|
1471
|
+
options.hastPlugins = plugins;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
function docsContentUrl(route, srcDir) {
|
|
1475
|
+
const slug = route === "/" ? "index" : route.replace(/^\/+|\/+$/g, "");
|
|
1476
|
+
return new URL(`content/docs/${slug}.md`, srcDir);
|
|
1477
|
+
}
|
|
1478
|
+
function componentOverridesPlugin(components, astroRoot) {
|
|
1479
|
+
const prefix = "virtual:cookbook/components/";
|
|
1480
|
+
return {
|
|
1481
|
+
name: "cookbook-component-overrides",
|
|
1482
|
+
resolveId(id) {
|
|
1483
|
+
const name = id.startsWith(prefix) ? id.slice(28) : void 0;
|
|
1484
|
+
return name && components[name] ? `\0${id}` : void 0;
|
|
1485
|
+
},
|
|
1486
|
+
load(id) {
|
|
1487
|
+
const name = id.startsWith(`\0${prefix}`) ? id.slice(29) : void 0;
|
|
1488
|
+
if (!name || !components[name]) return void 0;
|
|
1489
|
+
const configured = components[name];
|
|
1490
|
+
const target = configured.startsWith(".") ? resolve(astroRoot, configured) : configured;
|
|
1491
|
+
return `export { default } from ${JSON.stringify(target)};`;
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
function docsAssetMap(graph) {
|
|
1496
|
+
return new Map(graph.assets.flatMap((asset) => asset.publicPath && asset.sourcePath ? [[asset.publicPath, asset]] : []));
|
|
1497
|
+
}
|
|
1498
|
+
function documentedSite(graph) {
|
|
1499
|
+
if (graph.config.site.version) return graph.config.site;
|
|
1500
|
+
const packages = new Set(graph.entries.flatMap((entry) => entry.package?.resolved ? [entry.package.resolved] : []));
|
|
1501
|
+
if (packages.size !== 1) return graph.config.site;
|
|
1502
|
+
const resolved = packages.values().next().value;
|
|
1503
|
+
if (!resolved) return graph.config.site;
|
|
1504
|
+
const separator = resolved.lastIndexOf("@");
|
|
1505
|
+
if (separator <= 0 || separator === resolved.length - 1) return graph.config.site;
|
|
1506
|
+
return {
|
|
1507
|
+
...graph.config.site,
|
|
1508
|
+
version: resolved.slice(separator + 1)
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function requestPath(url) {
|
|
1512
|
+
try {
|
|
1513
|
+
return decodeURIComponent(new URL(url ?? "/", "http://localhost").pathname);
|
|
1514
|
+
} catch {
|
|
1515
|
+
return "";
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
function assetContentType(pathname) {
|
|
1519
|
+
switch (extname(pathname).toLowerCase()) {
|
|
1520
|
+
case ".avif": return "image/avif";
|
|
1521
|
+
case ".gif": return "image/gif";
|
|
1522
|
+
case ".jpeg":
|
|
1523
|
+
case ".jpg": return "image/jpeg";
|
|
1524
|
+
case ".png": return "image/png";
|
|
1525
|
+
case ".svg": return "image/svg+xml";
|
|
1526
|
+
case ".webp": return "image/webp";
|
|
1527
|
+
default: return "application/octet-stream";
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
function errorMessage(error) {
|
|
1531
|
+
return error instanceof Error ? error.message : String(error);
|
|
1532
|
+
}
|
|
1533
|
+
function configureTastyTheme(theme, resolved) {
|
|
1534
|
+
const tokens = tastyTokens(resolved);
|
|
1535
|
+
const globalStyles = resolveLegacyAnatomyStyles(theme?.customStyles);
|
|
1536
|
+
globalThis.__tenphiCookbookTastyRuntime = {
|
|
1537
|
+
units: {
|
|
1538
|
+
...TASTY_UNITS,
|
|
1539
|
+
...theme?.units
|
|
1540
|
+
},
|
|
1541
|
+
recipes: theme?.recipes ?? {},
|
|
1542
|
+
states: theme?.states ?? {},
|
|
1543
|
+
presets: resolved.presets,
|
|
1544
|
+
tokens
|
|
1545
|
+
};
|
|
1546
|
+
configure({
|
|
1547
|
+
states: {
|
|
1548
|
+
...cookbookStates,
|
|
1549
|
+
...theme?.states
|
|
1550
|
+
},
|
|
1551
|
+
units: {
|
|
1552
|
+
...TASTY_UNITS,
|
|
1553
|
+
...theme?.units
|
|
1554
|
+
},
|
|
1555
|
+
recipes: theme?.recipes ?? {},
|
|
1556
|
+
tokens,
|
|
1557
|
+
presets: resolved.presets,
|
|
1558
|
+
...globalStyles ? { globalStyles } : {}
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
async function callInner(integrations, hook, context) {
|
|
1562
|
+
for (const integration of integrations) {
|
|
1563
|
+
const handler = integration.hooks[hook];
|
|
1564
|
+
if (typeof handler === "function") await handler(context);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function virtualDocsPlugin(getContent, layout, getWatchRoots) {
|
|
1568
|
+
const configId = "\0virtual:cookbook/config";
|
|
1569
|
+
const layoutId = "\0virtual:cookbook/layout";
|
|
1570
|
+
const mdxPrefix = "virtual:cookbook/mdx/";
|
|
1571
|
+
let contentPromise;
|
|
1572
|
+
let mdxRecords = [];
|
|
1573
|
+
const watchedPaths = /* @__PURE__ */ new Set();
|
|
1574
|
+
async function loadContent() {
|
|
1575
|
+
contentPromise ??= Promise.resolve(getContent());
|
|
1576
|
+
let content;
|
|
1577
|
+
try {
|
|
1578
|
+
content = await contentPromise;
|
|
1579
|
+
} catch (error) {
|
|
1580
|
+
contentPromise = void 0;
|
|
1581
|
+
throw error;
|
|
1582
|
+
}
|
|
1583
|
+
mdxRecords = content.entries.flatMap((entry, index) => entry.mdx ? [{
|
|
1584
|
+
entry,
|
|
1585
|
+
sourceId: entry.absolutePath.replace(/\.mdx$/i, `.cookbook-${index}.mdx`),
|
|
1586
|
+
virtualId: `${mdxPrefix}${index}`
|
|
1587
|
+
}] : []);
|
|
1588
|
+
return content;
|
|
1589
|
+
}
|
|
1590
|
+
return {
|
|
1591
|
+
name: "cookbook-data",
|
|
1592
|
+
enforce: "pre",
|
|
1593
|
+
configureServer(server) {
|
|
1594
|
+
const roots = [...new Set(getWatchRoots())];
|
|
1595
|
+
server.watcher.add(roots);
|
|
1596
|
+
const changed = (_event, path) => {
|
|
1597
|
+
const owned = roots.some((root) => path === root || path.startsWith(`${root}/`));
|
|
1598
|
+
if (!watchedPaths.has(path) && !(owned && (/\.mdx?$/i.test(path) || path.endsWith("/cookbook.lock.json")))) return;
|
|
1599
|
+
contentPromise = void 0;
|
|
1600
|
+
mdxRecords = [];
|
|
1601
|
+
for (const environment of Object.values(server.environments)) environment.moduleGraph.invalidateAll();
|
|
1602
|
+
server.ws.send({ type: "full-reload" });
|
|
1603
|
+
};
|
|
1604
|
+
server.watcher.on("all", changed);
|
|
1605
|
+
server.httpServer?.once("close", () => server.watcher.off("all", changed));
|
|
1606
|
+
},
|
|
1607
|
+
async resolveId(id, _importer, resolveOptions) {
|
|
1608
|
+
if (id === "virtual:cookbook/config") {
|
|
1609
|
+
if (resolveOptions.ssr === false) throw new Error("Cookbook content queries are build-time only. Pass selected public data to a client component as props instead.");
|
|
1610
|
+
return configId;
|
|
1611
|
+
}
|
|
1612
|
+
if (id === "virtual:cookbook/layout") return layoutId;
|
|
1613
|
+
if (id.startsWith(mdxPrefix)) {
|
|
1614
|
+
await loadContent();
|
|
1615
|
+
return mdxRecords.find((record) => record.virtualId === id)?.sourceId;
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
async load(id) {
|
|
1619
|
+
if (id === configId) {
|
|
1620
|
+
const content = await loadContent();
|
|
1621
|
+
watchedPaths.clear();
|
|
1622
|
+
for (const entry of content.entries) {
|
|
1623
|
+
watchedPaths.add(entry.absolutePath);
|
|
1624
|
+
this.addWatchFile(entry.absolutePath);
|
|
1625
|
+
for (const asset of entry.assets ?? []) {
|
|
1626
|
+
if (!asset.sourcePath) continue;
|
|
1627
|
+
watchedPaths.add(asset.sourcePath);
|
|
1628
|
+
this.addWatchFile(asset.sourcePath);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
const loaders = mdxRecords.map(({ entry, virtualId }) => `${JSON.stringify(entry.route)}: () => import(${JSON.stringify(virtualId)})`).join(",\n");
|
|
1632
|
+
return `export const content = ${JSON.stringify(content)};\nexport const mdxLoaders = {${loaders}};`;
|
|
1633
|
+
}
|
|
1634
|
+
if (id === layoutId) return `export const layout = ${JSON.stringify(layout)};`;
|
|
1635
|
+
const mdx = mdxRecords.find((record) => record.sourceId === id);
|
|
1636
|
+
if (mdx) return mdx.entry.transformedBody;
|
|
1637
|
+
},
|
|
1638
|
+
watchChange(id) {
|
|
1639
|
+
if (!watchedPaths.has(id)) return;
|
|
1640
|
+
contentPromise = void 0;
|
|
1641
|
+
mdxRecords = [];
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
//#endregion
|
|
1646
|
+
export { DEFAULT_THEME_TOKENS, DEFAULT_TYPOGRAPHY_PRESETS, cookbook as default, resolveDocsTheme };
|
|
1647
|
+
|
|
1648
|
+
//# sourceMappingURL=index.js.map
|