@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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/dist/client/appearance.d.ts +12 -0
  4. package/dist/client/appearance.d.ts.map +1 -0
  5. package/dist/client/appearance.js +66 -0
  6. package/dist/client/appearance.js.map +1 -0
  7. package/dist/client/code-copy.d.ts +1 -0
  8. package/dist/client/code-copy.js +63 -0
  9. package/dist/client/code-copy.js.map +1 -0
  10. package/dist/client/heading-links.d.ts +6 -0
  11. package/dist/client/heading-links.d.ts.map +1 -0
  12. package/dist/client/heading-links.js +87 -0
  13. package/dist/client/heading-links.js.map +1 -0
  14. package/dist/client/mobile-toc.d.ts +1 -0
  15. package/dist/client/mobile-toc.js +28 -0
  16. package/dist/client/mobile-toc.js.map +1 -0
  17. package/dist/client/navigation.d.ts +1 -0
  18. package/dist/client/navigation.js +112 -0
  19. package/dist/client/navigation.js.map +1 -0
  20. package/dist/client/page-copy.d.ts +1 -0
  21. package/dist/client/page-copy.js +34 -0
  22. package/dist/client/page-copy.js.map +1 -0
  23. package/dist/client/search.d.ts +13 -0
  24. package/dist/client/search.d.ts.map +1 -0
  25. package/dist/client/search.js +87 -0
  26. package/dist/client/search.js.map +1 -0
  27. package/dist/client/sidebar.d.ts +1 -0
  28. package/dist/client/sidebar.js +93 -0
  29. package/dist/client/sidebar.js.map +1 -0
  30. package/dist/client/tabs.d.ts +6 -0
  31. package/dist/client/tabs.d.ts.map +1 -0
  32. package/dist/client/tabs.js +64 -0
  33. package/dist/client/tabs.js.map +1 -0
  34. package/dist/components/Callout.astro +15 -0
  35. package/dist/components/Card.astro +13 -0
  36. package/dist/components/CodeGroup.astro +47 -0
  37. package/dist/components/ContrastIcon.astro +29 -0
  38. package/dist/components/EditLink.astro +11 -0
  39. package/dist/components/GlobalStyles.js +1675 -0
  40. package/dist/components/HeaderLinks.astro +84 -0
  41. package/dist/components/Icon.astro +39 -0
  42. package/dist/components/LastUpdated.astro +17 -0
  43. package/dist/components/LayoutComponents.js +866 -0
  44. package/dist/components/Logo.astro +37 -0
  45. package/dist/components/MobileNavigationTabs.astro +79 -0
  46. package/dist/components/MobileTocList.astro +22 -0
  47. package/dist/components/PackageVersion.astro +25 -0
  48. package/dist/components/PageActions.astro +29 -0
  49. package/dist/components/Pagination.astro +29 -0
  50. package/dist/components/Preview.astro +34 -0
  51. package/dist/components/SidebarList.astro +122 -0
  52. package/dist/components/SiteLogo.astro +40 -0
  53. package/dist/components/SiteTitle.astro +7 -0
  54. package/dist/components/Steps.astro +6 -0
  55. package/dist/components/Tab.astro +10 -0
  56. package/dist/components/TableOfContents.astro +23 -0
  57. package/dist/components/Tabs.astro +16 -0
  58. package/dist/components/TastyComponents.js +255 -0
  59. package/dist/components/VersionSwitcher.astro +44 -0
  60. package/dist/components/appearance-preferences.test.ts +91 -0
  61. package/dist/components/appearance-preferences.ts +57 -0
  62. package/dist/components/component-styles.test.ts +129 -0
  63. package/dist/components/component-styles.ts +65 -0
  64. package/dist/components/customize-component.js +8 -0
  65. package/dist/components/props.d.ts +38 -0
  66. package/dist/components/props.d.ts.map +1 -0
  67. package/dist/components/props.js +1 -0
  68. package/dist/components/props.ts +30 -0
  69. package/dist/components/svg-icon.test.ts +11 -0
  70. package/dist/components/svg-icon.ts +11 -0
  71. package/dist/components/tasty-states.d.ts +2 -0
  72. package/dist/components/tasty-states.js +42 -0
  73. package/dist/components-public.d.ts +21 -0
  74. package/dist/components.d.ts +10 -0
  75. package/dist/components.js +9 -0
  76. package/dist/content.d.ts +11 -0
  77. package/dist/content.d.ts.map +1 -0
  78. package/dist/content.js +16 -0
  79. package/dist/content.js.map +1 -0
  80. package/dist/eslint-plugin.d.ts +32 -0
  81. package/dist/eslint-plugin.d.ts.map +1 -0
  82. package/dist/eslint-plugin.js +155 -0
  83. package/dist/eslint-plugin.js.map +1 -0
  84. package/dist/icons/arrow-left.svg +1 -0
  85. package/dist/icons/arrow-right.svg +1 -0
  86. package/dist/icons/check.svg +1 -0
  87. package/dist/icons/chevron-down.svg +1 -0
  88. package/dist/icons/chevron-right.svg +1 -0
  89. package/dist/icons/close.svg +1 -0
  90. package/dist/icons/copy.svg +1 -0
  91. package/dist/icons/device-desktop.svg +1 -0
  92. package/dist/icons/favicon.svg +11 -0
  93. package/dist/icons/menu.svg +1 -0
  94. package/dist/icons/moon.svg +1 -0
  95. package/dist/icons/palette.svg +1 -0
  96. package/dist/icons/search.svg +1 -0
  97. package/dist/icons/sun.svg +1 -0
  98. package/dist/index-BuX_Sb5c.d.ts +10 -0
  99. package/dist/index-BuX_Sb5c.d.ts.map +1 -0
  100. package/dist/index.d.ts +23 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +1648 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/localization.d.ts +69 -0
  105. package/dist/localization.d.ts.map +1 -0
  106. package/dist/localization.js +195 -0
  107. package/dist/localization.js.map +1 -0
  108. package/dist/markdown/rendered-content.d.ts +6 -0
  109. package/dist/markdown/rendered-content.d.ts.map +1 -0
  110. package/dist/markdown/rendered-content.js +34 -0
  111. package/dist/markdown/rendered-content.js.map +1 -0
  112. package/dist/messages-B_3xUzUb.js +14 -0
  113. package/dist/messages-B_3xUzUb.js.map +1 -0
  114. package/dist/navigation.d.ts +31 -0
  115. package/dist/navigation.d.ts.map +1 -0
  116. package/dist/navigation.js +165 -0
  117. package/dist/navigation.js.map +1 -0
  118. package/dist/overrides/DraftContentNotice.astro +8 -0
  119. package/dist/overrides/EmptyFooter.astro +3 -0
  120. package/dist/overrides/Footer.astro +23 -0
  121. package/dist/overrides/Head.astro +59 -0
  122. package/dist/overrides/Header.astro +139 -0
  123. package/dist/overrides/Hero.astro +73 -0
  124. package/dist/overrides/LanguageSelect.astro +63 -0
  125. package/dist/overrides/MarkdownContent.astro +14 -0
  126. package/dist/overrides/MobileMenuFooter.astro +15 -0
  127. package/dist/overrides/MobileMenuToggle.astro +36 -0
  128. package/dist/overrides/MobileTableOfContents.astro +37 -0
  129. package/dist/overrides/PageFrame.astro +35 -0
  130. package/dist/overrides/Search.astro +64 -0
  131. package/dist/overrides/Sidebar.astro +89 -0
  132. package/dist/overrides/SocialIcons.astro +27 -0
  133. package/dist/overrides/ThemeSelect.astro +148 -0
  134. package/dist/page-metadata.d.ts +13 -0
  135. package/dist/page-metadata.d.ts.map +1 -0
  136. package/dist/page-metadata.js +109 -0
  137. package/dist/page-metadata.js.map +1 -0
  138. package/dist/route-context.d.ts +85 -0
  139. package/dist/route-context.d.ts.map +1 -0
  140. package/dist/route-context.js +116 -0
  141. package/dist/route-context.js.map +1 -0
  142. package/dist/routes/DocsPage.astro +91 -0
  143. package/dist/routes/NotFound.astro +12 -0
  144. package/dist/routes/Page.astro +120 -0
  145. package/dist/styling.d.ts +17 -0
  146. package/dist/styling.d.ts.map +1 -0
  147. package/dist/styling.js +17 -0
  148. package/dist/styling.js.map +1 -0
  149. package/dist/tasty-states-BDKIbfw2.js +65 -0
  150. package/dist/tasty-states-BDKIbfw2.js.map +1 -0
  151. package/dist/theme/defaults.d.ts +21 -0
  152. package/dist/theme/defaults.d.ts.map +1 -0
  153. package/dist/theme/defaults.js +93 -0
  154. package/dist/theme/defaults.js.map +1 -0
  155. package/dist/theme/fonts.ts +64 -0
  156. package/dist/theme/shiki-theme.d.ts +13 -0
  157. package/dist/theme/shiki-theme.d.ts.map +1 -0
  158. package/dist/theme/shiki-theme.js +328 -0
  159. package/dist/theme/shiki-theme.js.map +1 -0
  160. package/dist/theme/tasty-config.d.ts +13 -0
  161. package/dist/theme/tasty-config.d.ts.map +1 -0
  162. package/dist/theme/tasty-config.js +16 -0
  163. package/dist/theme/tasty-config.js.map +1 -0
  164. package/dist/ui.d.ts +6 -0
  165. package/dist/ui.d.ts.map +1 -0
  166. package/dist/ui.js +53 -0
  167. package/dist/ui.js.map +1 -0
  168. package/dist/versioning.d.ts +11 -0
  169. package/dist/versioning.d.ts.map +1 -0
  170. package/dist/versioning.js +38 -0
  171. package/dist/versioning.js.map +1 -0
  172. package/package.json +92 -0
  173. 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("\"", "&quot;").replaceAll("'", "&#39;");
264
+ }
265
+ function escapeText(value) {
266
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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