blume 1.6.5 → 1.7.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 (179) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0qhq7b8q.js +111 -0
  4. package/dist/cli/chunk-0qhq7b8q.js.map +11 -0
  5. package/dist/cli/chunk-18tjv4f7.js +96 -0
  6. package/dist/cli/chunk-18tjv4f7.js.map +10 -0
  7. package/dist/cli/chunk-27gtm2ym.js +69 -0
  8. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  9. package/dist/cli/chunk-2aj8ddew.js +72 -0
  10. package/dist/cli/chunk-2aj8ddew.js.map +10 -0
  11. package/dist/cli/chunk-3r94j3tc.js +221 -0
  12. package/dist/cli/chunk-3r94j3tc.js.map +10 -0
  13. package/dist/cli/chunk-4trphnvy.js +102 -0
  14. package/dist/cli/chunk-4trphnvy.js.map +11 -0
  15. package/dist/cli/chunk-4xyggvgf.js +21 -0
  16. package/dist/cli/chunk-4xyggvgf.js.map +10 -0
  17. package/dist/cli/chunk-5d4q7121.js +4064 -0
  18. package/dist/cli/chunk-5d4q7121.js.map +40 -0
  19. package/dist/cli/chunk-5hs6gb7n.js +32 -0
  20. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  21. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  22. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  23. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  24. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  25. package/dist/cli/chunk-9qs6acpw.js +176 -0
  26. package/dist/cli/chunk-9qs6acpw.js.map +10 -0
  27. package/dist/cli/chunk-agy5rzxy.js +2453 -0
  28. package/dist/cli/chunk-agy5rzxy.js.map +15 -0
  29. package/dist/cli/chunk-bcy492zc.js +16 -0
  30. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  31. package/dist/cli/chunk-btfr9yvw.js +41 -0
  32. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  33. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  34. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  35. package/dist/cli/chunk-cfw6x4rm.js +1967 -0
  36. package/dist/cli/chunk-cfw6x4rm.js.map +34 -0
  37. package/dist/cli/chunk-ckh3a410.js +277 -0
  38. package/dist/cli/chunk-ckh3a410.js.map +11 -0
  39. package/dist/cli/chunk-drke6t0h.js +259 -0
  40. package/dist/cli/chunk-drke6t0h.js.map +11 -0
  41. package/dist/cli/chunk-ev67ycx0.js +15 -0
  42. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  43. package/dist/cli/chunk-ey89bjj1.js +209 -0
  44. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  45. package/dist/cli/chunk-j6pxe0dt.js +69 -0
  46. package/dist/cli/chunk-j6pxe0dt.js.map +11 -0
  47. package/dist/cli/chunk-jk1zwka1.js +387 -0
  48. package/dist/cli/chunk-jk1zwka1.js.map +12 -0
  49. package/dist/cli/chunk-jtb45atp.js +467 -0
  50. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  51. package/dist/cli/chunk-jxkxjsc1.js +76 -0
  52. package/dist/cli/chunk-jxkxjsc1.js.map +10 -0
  53. package/dist/cli/chunk-kwx90v78.js +81 -0
  54. package/dist/cli/chunk-kwx90v78.js.map +10 -0
  55. package/dist/cli/chunk-n0nyat6g.js +30 -0
  56. package/dist/cli/chunk-n0nyat6g.js.map +10 -0
  57. package/dist/cli/chunk-pxj10x8y.js +35 -0
  58. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  59. package/dist/cli/chunk-qq9nm3qd.js +1141 -0
  60. package/dist/cli/chunk-qq9nm3qd.js.map +19 -0
  61. package/dist/cli/chunk-s102bysw.js +5170 -0
  62. package/dist/cli/chunk-s102bysw.js.map +47 -0
  63. package/dist/cli/chunk-s5dsk8bj.js +769 -0
  64. package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
  65. package/dist/cli/chunk-s5e5jt53.js +227 -0
  66. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  67. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  68. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  69. package/dist/cli/chunk-tnskyrej.js +117 -0
  70. package/dist/cli/chunk-tnskyrej.js.map +10 -0
  71. package/dist/cli/chunk-v2ymm99c.js +1016 -0
  72. package/dist/cli/chunk-v2ymm99c.js.map +13 -0
  73. package/dist/cli/chunk-v5mm027v.js +185 -0
  74. package/dist/cli/chunk-v5mm027v.js.map +11 -0
  75. package/dist/cli/chunk-vt8fgygt.js +23 -0
  76. package/dist/cli/chunk-vt8fgygt.js.map +10 -0
  77. package/dist/cli/chunk-vxv4x1n8.js +17 -0
  78. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  79. package/dist/cli/chunk-wd27zjcz.js +60 -0
  80. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  81. package/dist/cli/chunk-x66c5yjn.js +23 -0
  82. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  83. package/dist/cli/chunk-xv91q4nm.js +5314 -0
  84. package/dist/cli/chunk-xv91q4nm.js.map +58 -0
  85. package/dist/cli/chunk-y3g15rvv.js +679 -0
  86. package/dist/cli/chunk-y3g15rvv.js.map +15 -0
  87. package/dist/cli/chunk-ye9zdkgv.js +136 -0
  88. package/dist/cli/chunk-ye9zdkgv.js.map +10 -0
  89. package/dist/cli/chunk-ynacq3ev.js +1062 -0
  90. package/dist/cli/chunk-ynacq3ev.js.map +25 -0
  91. package/dist/cli/chunk-zr3ygrq3.js +54 -0
  92. package/dist/cli/chunk-zr3ygrq3.js.map +10 -0
  93. package/dist/cli/index.js +55 -27597
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/components/layout/nav-utils.d.ts +33 -1
  97. package/dist/types/core/code-fences.d.ts +11 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +70 -0
  100. package/dist/types/theme/fonts.d.ts +22 -22
  101. package/docs/02-deployment.mdx +22 -1
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/navigation.mdx +2 -0
  105. package/docs/content/syntax.mdx +1 -1
  106. package/docs/discoverability/open-graph.mdx +4 -0
  107. package/docs/reference/cli.mdx +1 -1
  108. package/package.json +4 -2
  109. package/src/ai/api/handlers.ts +4 -7
  110. package/src/ai/api/paths.ts +8 -0
  111. package/src/ai/api/spec.ts +2 -1
  112. package/src/ai/ask-context.ts +378 -22
  113. package/src/astro/generate.ts +161 -28
  114. package/src/astro/include-hmr.ts +10 -13
  115. package/src/astro/include-refresh.ts +0 -0
  116. package/src/astro/index.ts +6 -1
  117. package/src/astro/integration.ts +280 -53
  118. package/src/astro/module-types.ts +83 -0
  119. package/src/astro/templates.ts +256 -108
  120. package/src/audit/image-size.ts +10 -8
  121. package/src/cli/command-meta.ts +77 -0
  122. package/src/cli/commands/add.ts +2 -4
  123. package/src/cli/commands/audit.ts +2 -4
  124. package/src/cli/commands/build.ts +70 -346
  125. package/src/cli/commands/check.ts +2 -4
  126. package/src/cli/commands/dev.ts +31 -42
  127. package/src/cli/commands/doctor.ts +2 -4
  128. package/src/cli/commands/eject.ts +3 -41
  129. package/src/cli/commands/eval.ts +2 -5
  130. package/src/cli/commands/init.ts +2 -4
  131. package/src/cli/commands/mcp-stdio.ts +2 -5
  132. package/src/cli/commands/preview.ts +3 -5
  133. package/src/cli/commands/sync.ts +2 -4
  134. package/src/cli/commands/translate.ts +2 -5
  135. package/src/cli/commands/validate.ts +2 -4
  136. package/src/cli/commands/version.ts +2 -4
  137. package/src/cli/eject-scripts.ts +0 -45
  138. package/src/cli/host-args.ts +16 -0
  139. package/src/cli/index.ts +84 -35
  140. package/src/cli/lazy-command.ts +47 -0
  141. package/src/components/Icon.astro +24 -0
  142. package/src/components/content/GithubInfo.astro +4 -1
  143. package/src/components/icon-sprite-middleware.ts +41 -0
  144. package/src/components/icon-sprite.ts +93 -0
  145. package/src/components/layout/IconSprite.astro +11 -0
  146. package/src/components/layout/NavTree.astro +156 -188
  147. package/src/components/layout/NavTreeCache.astro +45 -0
  148. package/src/components/layout/NavTreeScript.astro +256 -0
  149. package/src/components/layout/PageActions.astro +11 -5
  150. package/src/components/layout/PageLayout.astro +21 -3
  151. package/src/components/layout/ReferenceLayout.astro +21 -4
  152. package/src/components/layout/RootLayout.astro +44 -6
  153. package/src/components/layout/nav-cache.ts +49 -0
  154. package/src/components/layout/nav-utils.ts +69 -1
  155. package/src/components/layout/page-locale.ts +29 -0
  156. package/src/core/api-name.ts +18 -0
  157. package/src/core/code-fences.ts +48 -0
  158. package/src/core/content-assets.ts +3 -7
  159. package/src/core/includes.ts +3 -7
  160. package/src/core/package-root.ts +1 -1
  161. package/src/core/schema.ts +19 -0
  162. package/src/core/sources/normalize.ts +2 -37
  163. package/src/core/sources/obsidian.ts +3 -2
  164. package/src/core/svg-dimensions.ts +97 -0
  165. package/src/core/version-cut.ts +2 -2
  166. package/src/deploy/artifacts.ts +370 -0
  167. package/src/deploy/cloudflare-negotiation.ts +97 -32
  168. package/src/deploy/function-bundle.ts +66 -20
  169. package/src/deploy/sitemap.ts +6 -0
  170. package/src/deploy/vercel-negotiation.ts +8 -30
  171. package/src/markdown/language-icon.ts +64 -20
  172. package/src/markdown/mermaid.ts +11 -0
  173. package/src/og/cache.ts +236 -0
  174. package/src/og/card.ts +18 -16
  175. package/src/og/index.ts +8 -1
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +23 -10
  178. package/src/theme/entry.ts +41 -7
  179. package/src/theme/fonts.ts +30 -23
@@ -0,0 +1,4064 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildMcpData,
4
+ buildRawMarkdown,
5
+ buildRssFeeds,
6
+ buildSearchDocuments,
7
+ markdownRoutePaths,
8
+ renderRssFeed
9
+ } from "./chunk-ynacq3ev.js";
10
+ import {
11
+ MCP_TOOLS
12
+ } from "./chunk-pxj10x8y.js";
13
+ import {
14
+ customOgRoutes,
15
+ discoverPages,
16
+ hasGeneratedChangelog,
17
+ routeIsTaken
18
+ } from "./chunk-sbdqrjbb.js";
19
+ import {
20
+ collectContentAssets,
21
+ rewriteRelativeImages
22
+ } from "./chunk-v5mm027v.js";
23
+ import {
24
+ writeTextAtomic
25
+ } from "./chunk-ev67ycx0.js";
26
+ import {
27
+ registry
28
+ } from "./chunk-3r94j3tc.js";
29
+ import {
30
+ apiNavigationTemplate,
31
+ apiNotFoundTemplate,
32
+ apiPageTemplate,
33
+ apiPagesIndexTemplate,
34
+ apiSearchTemplate,
35
+ askBackendRuntimeDep,
36
+ askComponentTemplate,
37
+ askEndpointTemplate,
38
+ astroConfigTemplate,
39
+ buildHomeLinkHeader,
40
+ catchAllPageTemplate,
41
+ changelogIndexTemplate,
42
+ contentAssetsEndpointTemplate,
43
+ contentConfigTemplate,
44
+ exampleMapTemplate,
45
+ exampleSlug,
46
+ exampleWrapperTemplate,
47
+ examplesPageTemplate,
48
+ featuresTemplate,
49
+ islandMapTemplate,
50
+ islandWrapperTemplate,
51
+ mcpEndpointTemplate,
52
+ mcpPageFile,
53
+ mixedbreadSearchEndpointTemplate,
54
+ navFragmentTemplate,
55
+ notFoundJsonTemplate,
56
+ notFoundMarkdownTemplate,
57
+ notFoundPageTemplate,
58
+ ogEndpointTemplate,
59
+ playgroundProxyTemplate,
60
+ publishRuntimeModules,
61
+ rawMarkdownEndpointTemplate,
62
+ resolveAskBackend,
63
+ rssEndpointTemplate,
64
+ runtimeDependencies,
65
+ runtimePackageTemplate,
66
+ runtimeTsconfigTemplate,
67
+ scalarReferenceTemplate,
68
+ searchClientTemplate,
69
+ searchEndpointTemplate,
70
+ stagedContentDir,
71
+ staticJsonEndpointTemplate
72
+ } from "./chunk-agy5rzxy.js";
73
+ import {
74
+ API_NAVIGATION_PATH,
75
+ API_PAGES_PATH,
76
+ API_PAGE_PATH,
77
+ API_SEARCH_PATH,
78
+ OPENAPI_PATH
79
+ } from "./chunk-j6pxe0dt.js";
80
+ import {
81
+ absoluteUrl,
82
+ siteRoot
83
+ } from "./chunk-vxv4x1n8.js";
84
+ import {
85
+ searchProviderMeta,
86
+ servesStaticIndex
87
+ } from "./chunk-wd27zjcz.js";
88
+ import {
89
+ apiNamePhrase,
90
+ buildIncludeGraph,
91
+ isOpenApiSource,
92
+ resolveDocsCollection,
93
+ resolveFallbackLocale,
94
+ validateNavTargets,
95
+ validateSearchPopularIcons
96
+ } from "./chunk-s102bysw.js";
97
+ import {
98
+ EN_UI,
99
+ GOOGLE_FONTS,
100
+ apiUrl,
101
+ buildFontsCss,
102
+ builtinProxyKinds,
103
+ configuredFonts,
104
+ editBaseUrl,
105
+ fontLocaleCodes,
106
+ hasScalarReferences,
107
+ isFontSlug,
108
+ needsPlaygroundProxy,
109
+ referenceRoutes,
110
+ repoUrl,
111
+ resolveReferences,
112
+ resolveUIStrings
113
+ } from "./chunk-xv91q4nm.js";
114
+ import {
115
+ normalizeBasePath,
116
+ trimChar,
117
+ withBasePath
118
+ } from "./chunk-27gtm2ym.js";
119
+ import {
120
+ getBlumeVersion
121
+ } from "./chunk-4xyggvgf.js";
122
+ import {
123
+ packageRoot
124
+ } from "./chunk-6kzzpsx8.js";
125
+ import {
126
+ BlumeError,
127
+ enrichDiagnostic
128
+ } from "./chunk-ey89bjj1.js";
129
+
130
+ // src/ai/web-bot-auth.ts
131
+ var SIGNATURES_DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
132
+ var SIGNATURES_DIRECTORY_TYPE = "application/http-message-signatures-directory+json";
133
+
134
+ // src/astro/integration.ts
135
+ import { join as join3, relative, resolve as resolve2 } from "pathe";
136
+
137
+ // src/deploy/artifacts.ts
138
+ import { dirname, join as join2, resolve } from "pathe";
139
+
140
+ // src/search/build.ts
141
+ import { join } from "pathe";
142
+
143
+ // src/deploy/sitemap.ts
144
+ import { escape as escapeXml } from "html-escaper";
145
+ var ERROR_ROUTES = new Set(["/404", "/500"]);
146
+
147
+ // src/astro/integration.ts
148
+ var REGISTRY_KEY = Symbol.for("blume.integration");
149
+ var registry2 = () => {
150
+ const host = globalThis;
151
+ host[REGISTRY_KEY] ??= {
152
+ buildProject: null,
153
+ negotiation: null,
154
+ overlay: null,
155
+ refreshContent: null
156
+ };
157
+ return host[REGISTRY_KEY];
158
+ };
159
+ var publishBuildProject = (project) => {
160
+ registry2().buildProject = project;
161
+ };
162
+ var publishDevNegotiation = (negotiation) => {
163
+ registry2().negotiation = negotiation ? {
164
+ contentRoutes: new Set(negotiation.contentRoutes),
165
+ homeLinkHeader: negotiation.homeLinkHeader
166
+ } : null;
167
+ };
168
+ var refreshBlumeContent = async () => {
169
+ const { refreshContent } = registry2();
170
+ if (!refreshContent) {
171
+ return false;
172
+ }
173
+ await refreshContent({});
174
+ return true;
175
+ };
176
+ var overlayChannel = () => {
177
+ const { overlay } = registry2();
178
+ return overlay?.ws ?? overlay?.hot;
179
+ };
180
+ var showBlumeErrorOverlay = (diagnostics) => {
181
+ const errors = [];
182
+ for (const diagnostic of diagnostics) {
183
+ if (diagnostic.severity === "error") {
184
+ errors.push(enrichDiagnostic(diagnostic));
185
+ }
186
+ }
187
+ const channel = overlayChannel();
188
+ if (errors.length === 0 || !channel) {
189
+ return;
190
+ }
191
+ const body = errors.map((diagnostic) => {
192
+ const lineSuffix = diagnostic.line ? `:${diagnostic.line}` : "";
193
+ const where = diagnostic.file ? `
194
+ at ${diagnostic.file}${lineSuffix}` : "";
195
+ const fix = diagnostic.suggestion ? `
196
+ fix: ${diagnostic.suggestion}` : "";
197
+ const docs = diagnostic.docsUrl ? `
198
+ docs: ${diagnostic.docsUrl}` : "";
199
+ return `[${diagnostic.code}] ${diagnostic.message}${where}${fix}${docs}`;
200
+ }).join(`
201
+
202
+ `);
203
+ channel.send({
204
+ err: {
205
+ id: errors[0]?.file,
206
+ message: `Blume found ${errors.length} error(s):
207
+
208
+ ${body}`,
209
+ plugin: "blume",
210
+ stack: ""
211
+ },
212
+ type: "error"
213
+ });
214
+ };
215
+
216
+ // src/og/cache.ts
217
+ import { existsSync } from "node:fs";
218
+ import {
219
+ mkdir,
220
+ readdir,
221
+ readFile,
222
+ rename,
223
+ rm,
224
+ writeFile
225
+ } from "node:fs/promises";
226
+ import { dirname as dirname2, join as join4 } from "pathe";
227
+ var ogCacheDir = (context) => existsSync(join4(context.root, "node_modules")) ? join4(context.root, "node_modules", ".cache", "blume", "og") : join4(context.outDir, ".cache", "og");
228
+ var REGISTRY_KEY2 = Symbol.for("blume.og-cache");
229
+ var registry3 = () => {
230
+ const host = globalThis;
231
+ host[REGISTRY_KEY2] ??= { hits: 0, misses: 0, used: new Set };
232
+ return host[REGISTRY_KEY2];
233
+ };
234
+ var localFontDigests = new Map;
235
+ var inflight = new Map;
236
+ var pruneCardCache = async (dir) => {
237
+ const { used } = registry3();
238
+ let entries;
239
+ try {
240
+ entries = await readdir(dir);
241
+ } catch {
242
+ return 0;
243
+ }
244
+ const stale = entries.filter((name) => name.endsWith(".tmp") || name.endsWith(".png") && !used.has(name.slice(0, -".png".length)));
245
+ await Promise.all(stale.map((name) => rm(join4(dir, name), { force: true })));
246
+ return stale.length;
247
+ };
248
+ var cardCacheTally = () => {
249
+ const { hits, misses } = registry3();
250
+ return hits + misses === 0 ? null : { hits, misses };
251
+ };
252
+
253
+ // src/astro/generate.ts
254
+ import { createHash } from "node:crypto";
255
+ import { existsSync as existsSync6, readFileSync as readFileSync3, realpathSync } from "node:fs";
256
+ import {
257
+ lstat,
258
+ mkdir as mkdir2,
259
+ readFile as readFile5,
260
+ readlink,
261
+ realpath,
262
+ rm as rm2,
263
+ symlink
264
+ } from "node:fs/promises";
265
+ import { createRequire as createRequire2 } from "node:module";
266
+ import { pathToFileURL } from "node:url";
267
+ import pMap3 from "p-map";
268
+ import {
269
+ basename as basename2,
270
+ dirname as dirname6,
271
+ isAbsolute as isAbsolute6,
272
+ join as join11,
273
+ normalize,
274
+ relative as relative4,
275
+ resolve as resolve6
276
+ } from "pathe";
277
+ import { glob as glob3 } from "tinyglobby";
278
+
279
+ // src/ai/api/problem.ts
280
+ var PROBLEM_TYPE = "application/problem+json";
281
+
282
+ // src/ai/api/spec.ts
283
+ var JSON_TYPE = "application/json";
284
+ var MARKDOWN_TYPE = "text/markdown";
285
+ var TEXT_TYPE = "text/plain";
286
+ var ref = (name) => ({
287
+ $ref: `#/components/schemas/${name}`
288
+ });
289
+ var jsonResponse = (description, schema) => ({
290
+ content: { [JSON_TYPE]: { schema: ref(schema) } },
291
+ description
292
+ });
293
+ var problemResponse = (description) => ({
294
+ content: { [PROBLEM_TYPE]: { schema: ref("Problem") } },
295
+ description
296
+ });
297
+ var textResponse = (description, type) => ({
298
+ content: { [type]: { schema: { type: "string" } } },
299
+ description
300
+ });
301
+ var string = (description) => ({ description, type: "string" });
302
+ var facetsSchema = {
303
+ additionalProperties: { type: "string" },
304
+ description: "Facet values the site declares for the page's content type (`content.types.<type>.facets`), key → value.",
305
+ type: "object"
306
+ };
307
+ var versionProperty = {
308
+ description: 'Docs version the page belongs to on a versioned site: `""` for the current docs, else an archived version id. Absent on unversioned sites.',
309
+ type: "string"
310
+ };
311
+ var searchParameters = [
312
+ {
313
+ description: "The search query.",
314
+ in: "query",
315
+ name: "q",
316
+ required: true,
317
+ schema: { minLength: 1, type: "string" }
318
+ },
319
+ {
320
+ description: "Maximum hits to return (default 8, at most 20).",
321
+ in: "query",
322
+ name: "limit",
323
+ required: false,
324
+ schema: { default: 8, maximum: 20, minimum: 1, type: "integer" }
325
+ },
326
+ {
327
+ description: "Only include pages of these content types (frontmatter `type`, e.g. `doc`, `rfc`). Comma-separated or repeated. Omit for every type.",
328
+ explode: false,
329
+ in: "query",
330
+ name: "contentTypes",
331
+ required: false,
332
+ schema: { items: { type: "string" }, type: "array" },
333
+ style: "form"
334
+ },
335
+ {
336
+ description: "Only include pages in this locale (e.g. `fr`). Omit for every language.",
337
+ in: "query",
338
+ name: "locale",
339
+ required: false,
340
+ schema: { type: "string" }
341
+ },
342
+ {
343
+ description: "Docs version to scope to on a versioned site: `latest` (the default — current docs only), `all`, or an archived version id. Ignored when the site is unversioned.",
344
+ in: "query",
345
+ name: "version",
346
+ required: false,
347
+ schema: { type: "string" }
348
+ },
349
+ {
350
+ description: "Only include pages matching every facet, as `filters[key]=value` pairs (e.g. `filters[status]=enforced`). Facets are metadata the site declares per content type; the page index shows each page's values.",
351
+ explode: true,
352
+ in: "query",
353
+ name: "filters",
354
+ required: false,
355
+ schema: { additionalProperties: { type: "string" }, type: "object" },
356
+ style: "deepObject"
357
+ }
358
+ ];
359
+ var schemas = {
360
+ JsonRpcRequest: {
361
+ description: "A JSON-RPC 2.0 request, as the Model Context Protocol sends.",
362
+ properties: {
363
+ id: { description: "Request id (absent on notifications)." },
364
+ jsonrpc: { const: "2.0", type: "string" },
365
+ method: string("The MCP method, e.g. `initialize` or `tools/call`."),
366
+ params: { description: "Method parameters.", type: "object" }
367
+ },
368
+ required: ["jsonrpc", "method"],
369
+ type: "object"
370
+ },
371
+ JsonRpcResponse: {
372
+ description: "A JSON-RPC 2.0 response: a `result` or an `error`.",
373
+ properties: {
374
+ error: {
375
+ properties: {
376
+ code: { type: "integer" },
377
+ data: {},
378
+ message: { type: "string" }
379
+ },
380
+ required: ["code", "message"],
381
+ type: "object"
382
+ },
383
+ id: {},
384
+ jsonrpc: { const: "2.0", type: "string" },
385
+ result: { type: "object" }
386
+ },
387
+ required: ["jsonrpc"],
388
+ type: "object"
389
+ },
390
+ NavLink: {
391
+ properties: {
392
+ href: string("Link target — an internal route or an external URL."),
393
+ icon: string("Optional icon name."),
394
+ label: { type: "string" }
395
+ },
396
+ required: ["href", "label"],
397
+ type: "object"
398
+ },
399
+ NavNode: {
400
+ description: "A sidebar entry: a page (linking to its route) or a group holding further nodes.",
401
+ oneOf: [
402
+ {
403
+ properties: {
404
+ badge: { type: "string" },
405
+ deprecated: { type: "boolean" },
406
+ description: { type: "string" },
407
+ icon: { type: "string" },
408
+ kind: { const: "page", type: "string" },
409
+ label: { type: "string" },
410
+ pageId: string("The page's stable content id."),
411
+ route: string("The page's route.")
412
+ },
413
+ required: ["kind", "label", "pageId", "route"],
414
+ type: "object"
415
+ },
416
+ {
417
+ properties: {
418
+ badge: { type: "string" },
419
+ children: { items: ref("NavNode"), type: "array" },
420
+ collapsed: { type: "boolean" },
421
+ icon: { type: "string" },
422
+ kind: { const: "group", type: "string" },
423
+ label: { type: "string" },
424
+ path: string("The group's route prefix (not necessarily a page)."),
425
+ route: string("The group's index page route, when it has one.")
426
+ },
427
+ required: ["children", "kind", "label"],
428
+ type: "object"
429
+ }
430
+ ]
431
+ },
432
+ NavSelector: {
433
+ description: "A top-level partition selector (products, versions, languages).",
434
+ properties: {
435
+ items: {
436
+ items: {
437
+ properties: {
438
+ description: { type: "string" },
439
+ label: { type: "string" },
440
+ path: { type: "string" },
441
+ tag: { type: "string" }
442
+ },
443
+ required: ["label", "path"],
444
+ type: "object"
445
+ },
446
+ type: "array"
447
+ },
448
+ kind: { type: "string" },
449
+ label: { type: "string" }
450
+ },
451
+ required: ["items", "kind", "label"],
452
+ type: "object"
453
+ },
454
+ NavTab: {
455
+ properties: {
456
+ href: string("The clickable target when it differs from `path` (the section's first page)."),
457
+ icon: { type: "string" },
458
+ label: { type: "string" },
459
+ path: string("The tab's section prefix.")
460
+ },
461
+ required: ["label", "path"],
462
+ type: "object"
463
+ },
464
+ Navigation: {
465
+ description: "The docs navigation model: header tabs, the sidebar tree, partition selectors, and pinned links.",
466
+ properties: {
467
+ actions: { items: ref("NavLink"), type: "array" },
468
+ cta: { oneOf: [ref("NavLink"), { type: "null" }] },
469
+ featured: { items: ref("NavLink"), type: "array" },
470
+ repoUrl: { type: ["string", "null"] },
471
+ root: string("The tree root's route (`/`, or a locale/version prefix)."),
472
+ selectors: { items: ref("NavSelector"), type: "array" },
473
+ sidebar: { items: ref("NavNode"), type: "array" },
474
+ tabs: { items: ref("NavTab"), type: "array" }
475
+ },
476
+ required: ["featured", "selectors", "sidebar", "tabs"],
477
+ type: "object"
478
+ },
479
+ Page: {
480
+ allOf: [
481
+ ref("PageSummary"),
482
+ {
483
+ properties: {
484
+ markdown: string("The page as agent Markdown: frontmatter included, components downleveled to plain Markdown.")
485
+ },
486
+ required: ["markdown"],
487
+ type: "object"
488
+ }
489
+ ],
490
+ description: "A page's index entry plus its full Markdown body."
491
+ },
492
+ PageSummary: {
493
+ properties: {
494
+ contentType: string("The page's content type (frontmatter `type`)."),
495
+ description: { type: "string" },
496
+ facets: facetsSchema,
497
+ json: {
498
+ description: "This page's JSON representation (the `getPage` operation).",
499
+ format: "uri-reference",
500
+ type: "string"
501
+ },
502
+ lastModified: {
503
+ description: "ISO 8601 last-modified date, when known.",
504
+ type: ["string", "null"]
505
+ },
506
+ locale: string("The page's locale code."),
507
+ markdownUrl: {
508
+ description: "The page's raw-Markdown mirror (the `getPageMarkdown` operation).",
509
+ format: "uri-reference",
510
+ type: "string"
511
+ },
512
+ route: string("The page's route (`/guides/install`); the key every other operation takes."),
513
+ title: { type: "string" },
514
+ url: {
515
+ description: "Where the rendered page is served.",
516
+ format: "uri-reference",
517
+ type: "string"
518
+ },
519
+ version: versionProperty
520
+ },
521
+ required: [
522
+ "contentType",
523
+ "json",
524
+ "lastModified",
525
+ "locale",
526
+ "markdownUrl",
527
+ "route",
528
+ "title",
529
+ "url"
530
+ ],
531
+ type: "object"
532
+ },
533
+ PagesIndex: {
534
+ properties: {
535
+ count: { type: "integer" },
536
+ generator: string("The Blume version that built the site."),
537
+ pages: { items: ref("PageSummary"), type: "array" },
538
+ site: { type: ["string", "null"] }
539
+ },
540
+ required: ["count", "generator", "pages", "site"],
541
+ type: "object"
542
+ },
543
+ Problem: {
544
+ description: "RFC 9457 problem details, with a stable code and a resolution hint.",
545
+ properties: {
546
+ code: string("Stable error code (e.g. `PAGE_NOT_FOUND`)."),
547
+ detail: string("Human-readable explanation of this occurrence."),
548
+ instance: string("The request path the problem occurred on."),
549
+ links: {
550
+ description: "Recovery links, when there is somewhere useful to go.",
551
+ items: {
552
+ properties: { href: { type: "string" }, label: { type: "string" } },
553
+ required: ["href", "label"],
554
+ type: "object"
555
+ },
556
+ type: "array"
557
+ },
558
+ resolution: string("What to do next."),
559
+ status: { type: "integer" },
560
+ title: { type: "string" },
561
+ type: string("Problem type URI; `about:blank` by default.")
562
+ },
563
+ required: ["code", "detail", "resolution", "status", "title", "type"],
564
+ type: "object"
565
+ },
566
+ SearchHit: {
567
+ properties: {
568
+ contentType: { type: "string" },
569
+ excerpt: string("The page description, else the start of its content."),
570
+ facets: facetsSchema,
571
+ route: string("The page's route; pass it to `getPage`."),
572
+ title: { type: "string" },
573
+ url: { format: "uri-reference", type: "string" },
574
+ version: versionProperty
575
+ },
576
+ required: ["excerpt", "route", "title", "url"],
577
+ type: "object"
578
+ },
579
+ SearchResponse: {
580
+ properties: {
581
+ count: { type: "integer" },
582
+ query: { type: "string" },
583
+ results: { items: ref("SearchHit"), type: "array" }
584
+ },
585
+ required: ["count", "query", "results"],
586
+ type: "object"
587
+ }
588
+ };
589
+ var ROUTE_PARAM = {
590
+ description: "The page route without its leading slash (`guides/install`), or `index` for the home page. May contain slashes.",
591
+ in: "path",
592
+ name: "route",
593
+ required: true,
594
+ schema: { type: "string" }
595
+ };
596
+ var serverUrl = (input) => {
597
+ if (input.site) {
598
+ return input.base ? absoluteUrl(input.site, input.base) : siteRoot(input.site);
599
+ }
600
+ return input.base || "/";
601
+ };
602
+ var buildApiSpec = (input) => {
603
+ const pathEntries = [
604
+ [
605
+ API_PAGES_PATH,
606
+ {
607
+ get: {
608
+ description: "Every documentation page with its route, title, description, content type, locale, facets, and the URLs of its rendered, Markdown, and JSON forms. Unfiltered; on a versioned site every version is listed with its `version`. Use it to enumerate the docs or to find a page when search is too narrow.",
609
+ operationId: "listPages",
610
+ responses: {
611
+ "200": jsonResponse("The page index.", "PagesIndex"),
612
+ default: problemResponse("An error, as problem details.")
613
+ },
614
+ summary: "List every page",
615
+ tags: ["Pages"]
616
+ }
617
+ }
618
+ ],
619
+ [
620
+ API_PAGE_PATH,
621
+ {
622
+ get: {
623
+ description: "A single page as JSON: its index entry plus the page's agent Markdown (frontmatter included, components downleveled to plain Markdown). Take `route` from `listPages` or `searchDocs`.",
624
+ operationId: "getPage",
625
+ parameters: [ROUTE_PARAM],
626
+ responses: {
627
+ "200": jsonResponse("The page.", "Page"),
628
+ "404": problemResponse("No page has that route."),
629
+ default: problemResponse("An error, as problem details.")
630
+ },
631
+ summary: "Get a page as JSON",
632
+ tags: ["Pages"]
633
+ }
634
+ }
635
+ ],
636
+ [
637
+ API_NAVIGATION_PATH,
638
+ {
639
+ get: {
640
+ description: "The navigation tree (header tabs and the sidebar hierarchy) as readers see it, for the default locale and the current docs.",
641
+ operationId: "getNavigation",
642
+ responses: {
643
+ "200": jsonResponse("The navigation tree.", "Navigation"),
644
+ default: problemResponse("An error, as problem details.")
645
+ },
646
+ summary: "Get the navigation tree",
647
+ tags: ["Navigation"]
648
+ }
649
+ }
650
+ ]
651
+ ];
652
+ if (input.search) {
653
+ pathEntries.push([
654
+ API_SEARCH_PATH,
655
+ {
656
+ get: {
657
+ description: "Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; narrow by content type, locale, version, or facet. Use it first to discover relevant pages, then `getPage` to read one in full.",
658
+ operationId: "searchDocs",
659
+ parameters: searchParameters,
660
+ responses: {
661
+ "200": jsonResponse("The matching pages, best first.", "SearchResponse"),
662
+ "400": problemResponse("The query was missing or blank."),
663
+ default: problemResponse("An error, as problem details.")
664
+ },
665
+ summary: "Search the docs",
666
+ tags: ["Search"]
667
+ }
668
+ }
669
+ ]);
670
+ }
671
+ pathEntries.push([
672
+ "/{route}.md",
673
+ {
674
+ get: {
675
+ description: "A page's raw-Markdown mirror: append `.md` to any page URL. Components are downleveled to plain Markdown; `.mdx` serves the source as written. The same body the `getPage` operation carries in its `markdown` field.",
676
+ operationId: "getPageMarkdown",
677
+ parameters: [ROUTE_PARAM],
678
+ responses: {
679
+ "200": textResponse("The page as Markdown.", MARKDOWN_TYPE),
680
+ "404": textResponse("No page has that route; the body lists where to look next.", MARKDOWN_TYPE)
681
+ },
682
+ summary: "Get a page as Markdown",
683
+ tags: ["Markdown"]
684
+ }
685
+ }
686
+ ]);
687
+ if (input.llmsTxt) {
688
+ pathEntries.push([
689
+ "/llms.txt",
690
+ {
691
+ get: {
692
+ description: "The llms.txt index: the site's summary, when to use it, and every page with a one-line description, grouped by section.",
693
+ operationId: "getLlmsTxt",
694
+ responses: {
695
+ "200": textResponse("The index.", TEXT_TYPE)
696
+ },
697
+ summary: "Get llms.txt",
698
+ tags: ["Markdown"]
699
+ }
700
+ }
701
+ ], [
702
+ "/llms-full.txt",
703
+ {
704
+ get: {
705
+ description: "The full Markdown of every current-docs page in one file.",
706
+ operationId: "getLlmsFullTxt",
707
+ responses: {
708
+ "200": textResponse("Every page's Markdown.", TEXT_TYPE)
709
+ },
710
+ summary: "Get llms-full.txt",
711
+ tags: ["Markdown"]
712
+ }
713
+ }
714
+ ]);
715
+ }
716
+ if (input.agentReadability) {
717
+ pathEntries.push([
718
+ "/agent-readability.json",
719
+ {
720
+ get: {
721
+ description: "A manifest indexing every agent-facing artifact the site publishes — this API, the Markdown mirrors, llms.txt, the MCP server, feeds, and the sitemap.",
722
+ operationId: "getAgentReadability",
723
+ responses: {
724
+ "200": {
725
+ content: { [JSON_TYPE]: { schema: { type: "object" } } },
726
+ description: "The manifest."
727
+ }
728
+ },
729
+ summary: "Get the agent-readability manifest",
730
+ tags: ["Discovery"]
731
+ }
732
+ }
733
+ ]);
734
+ }
735
+ if (input.mcpRoute) {
736
+ pathEntries.push([
737
+ input.mcpRoute,
738
+ {
739
+ post: {
740
+ description: "The Model Context Protocol server (Streamable HTTP, stateless, JSON responses). Tools: `search_docs`, `get_page`, `list_pages`, `get_navigation` — the same operations this API exposes — plus every page as a `text/markdown` resource. Discovery document at `/.well-known/mcp.json`.",
741
+ operationId: "mcp",
742
+ requestBody: {
743
+ content: { [JSON_TYPE]: { schema: ref("JsonRpcRequest") } },
744
+ required: true
745
+ },
746
+ responses: {
747
+ "200": jsonResponse("The JSON-RPC response.", "JsonRpcResponse")
748
+ },
749
+ summary: "Call the MCP server",
750
+ tags: ["MCP"]
751
+ }
752
+ }
753
+ ]);
754
+ }
755
+ const tags = [
756
+ { description: "Enumerate and read documentation pages.", name: "Pages" },
757
+ ...input.search ? [{ description: "Full-text search over the docs.", name: "Search" }] : [],
758
+ { description: "How the docs are organized.", name: "Navigation" },
759
+ { description: "Plain-text and Markdown surfaces.", name: "Markdown" },
760
+ ...input.agentReadability ? [{ description: "Agent discovery documents.", name: "Discovery" }] : [],
761
+ ...input.mcpRoute ? [{ description: "The Model Context Protocol endpoint.", name: "MCP" }] : []
762
+ ];
763
+ const info = {
764
+ description: [
765
+ `Read-only JSON API over the ${input.name} documentation${input.description ? `: ${input.description}` : "."}`,
766
+ "Every operation is public and needs no authentication. Errors are RFC 9457 problem details (`application/problem+json`) with a stable `code`, a `detail`, and a `resolution` hint."
767
+ ].join(`
768
+
769
+ `),
770
+ title: apiNamePhrase(input.name),
771
+ version: input.version,
772
+ "x-generator": `blume@${input.version}`
773
+ };
774
+ const document = {
775
+ components: { schemas },
776
+ info,
777
+ openapi: "3.1.0",
778
+ paths: Object.fromEntries(pathEntries),
779
+ security: [],
780
+ servers: [{ description: input.name, url: serverUrl(input) }],
781
+ tags
782
+ };
783
+ if (input.site) {
784
+ document.externalDocs = {
785
+ description: `${input.name} documentation`,
786
+ url: absoluteUrl(input.site, withBasePath(input.base, "/"))
787
+ };
788
+ }
789
+ return document;
790
+ };
791
+
792
+ // src/ai/ask-data.ts
793
+ var buildAskData = async (project) => {
794
+ const documents = await buildSearchDocuments(project, {
795
+ audience: "agents",
796
+ content: "markdown",
797
+ includeWhenDisabled: true
798
+ });
799
+ return {
800
+ defaultLocale: project.config.i18n?.defaultLocale,
801
+ documents: documents.map((doc) => ({
802
+ content: doc.content,
803
+ description: doc.description,
804
+ locale: doc.locale,
805
+ route: doc.route,
806
+ title: doc.title
807
+ })),
808
+ site: project.config.deployment.site ?? null
809
+ };
810
+ };
811
+
812
+ // src/ai/mcp/discovery.ts
813
+ var serverUrl2 = (input) => {
814
+ const path = withBasePath(input.base, input.route);
815
+ return input.site ? absoluteUrl(input.site, path) : path;
816
+ };
817
+ var buildMcpDiscovery = (input) => ({
818
+ servers: [
819
+ {
820
+ name: input.name,
821
+ transport: "streamable-http",
822
+ url: serverUrl2(input)
823
+ }
824
+ ]
825
+ });
826
+ var SERVER_CARD_SCHEMA = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
827
+ var CARD_TEXT_MAX = 100;
828
+ var truncate = (text) => text.length > CARD_TEXT_MAX ? `${text.slice(0, CARD_TEXT_MAX - 1)}…` : text;
829
+ var NON_ASCII_SLUG = /[^a-z0-9]+/gu;
830
+ var COMBINING_MARKS = /\p{M}+/gu;
831
+ var asciiSlugify = (text) => trimChar(text.normalize("NFKD").replace(COMBINING_MARKS, "").toLowerCase().replace(NON_ASCII_SLUG, "-"), "-");
832
+ var reverseDnsName = (input) => {
833
+ let namespace = "localhost";
834
+ if (input.site) {
835
+ try {
836
+ namespace = new URL(input.site).hostname.split(".").toReversed().join(".");
837
+ } catch {}
838
+ }
839
+ return `${namespace}/${asciiSlugify(input.name) || "docs"}`;
840
+ };
841
+ var HTTP_URL = /^https?:\/\//u;
842
+ var buildMcpServerCard = (input) => {
843
+ const url = serverUrl2(input);
844
+ const card = {
845
+ $schema: SERVER_CARD_SCHEMA,
846
+ capabilities: {
847
+ resources: { listChanged: false, subscribe: false },
848
+ tools: { listChanged: false }
849
+ },
850
+ description: truncate(`Model Context Protocol server for the ${input.name} documentation.`),
851
+ name: reverseDnsName(input),
852
+ serverInfo: { name: input.name, version: input.version },
853
+ title: truncate(input.name),
854
+ tools: MCP_TOOLS.map((tool) => ({
855
+ annotations: tool.annotations,
856
+ description: tool.description,
857
+ name: tool.name,
858
+ title: tool.title
859
+ })),
860
+ transport: "streamable-http",
861
+ transports: [{ endpoint: url, type: "streamable-http" }],
862
+ url,
863
+ version: input.version
864
+ };
865
+ if (HTTP_URL.test(url)) {
866
+ card.remotes = [{ type: "streamable-http", url }];
867
+ }
868
+ if (input.site) {
869
+ card.websiteUrl = siteRoot(input.site);
870
+ }
871
+ return card;
872
+ };
873
+
874
+ // src/components/layout/nav-utils.ts
875
+ var hasDeferrableGroups = (sidebar) => sidebar.some((node) => node.kind === "group" && ((node.display ?? "flat") !== "flat" || hasDeferrableGroups(node.children)));
876
+ var navVariants = (data) => [
877
+ { locale: "default", navigation: data.navigation, version: "current" },
878
+ ...Object.entries(data.navigationByLocale).map(([locale, navigation]) => ({
879
+ locale,
880
+ navigation,
881
+ version: "current"
882
+ })),
883
+ ...Object.entries(data.navigationByVersion).flatMap(([version, byLocale]) => Object.entries(byLocale).map(([locale, navigation]) => ({
884
+ locale: locale || "default",
885
+ navigation,
886
+ version
887
+ })))
888
+ ];
889
+
890
+ // src/core/builtin-tags.ts
891
+ var BUILTIN_MDX_TAGS = new Set([
892
+ "Accordion",
893
+ "AccordionItem",
894
+ "ApiOverview",
895
+ "ApiTagOperations",
896
+ "AutoTypeTable",
897
+ "Badge",
898
+ "Callout",
899
+ "Card",
900
+ "CardGroup",
901
+ "CodeBlock",
902
+ "CodeGroup",
903
+ "Color",
904
+ "Column",
905
+ "Columns",
906
+ "Component",
907
+ "Diff",
908
+ "Expandable",
909
+ "FileTree",
910
+ "Frame",
911
+ "GithubInfo",
912
+ "Icon",
913
+ "Math",
914
+ "Operation",
915
+ "Panel",
916
+ "Prompt",
917
+ "Step",
918
+ "Steps",
919
+ "Tab",
920
+ "Tabs",
921
+ "Tile",
922
+ "Tooltip",
923
+ "Tree",
924
+ "TypeTable",
925
+ "Visibility",
926
+ "YouTube"
927
+ ]);
928
+
929
+ // src/core/component-diagnostics.ts
930
+ var toKebab = (tag) => tag.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").toLowerCase();
931
+ var validateUsedComponents = (pages, extraTags, registryNames) => {
932
+ const diagnostics = [];
933
+ const seen = new Set;
934
+ for (const page of pages) {
935
+ for (const tag of page.componentsUsed ?? []) {
936
+ if (BUILTIN_MDX_TAGS.has(tag) || extraTags.has(tag) || seen.has(tag)) {
937
+ continue;
938
+ }
939
+ seen.add(tag);
940
+ const name = toKebab(tag);
941
+ const suggestion = registryNames.has(name) ? `Run \`blume add ${name}\` to install it, or register <${tag}> in components.ts (mdx).` : `Register <${tag}> in components.ts (mdx), or add an islands/${tag}.tsx component.`;
942
+ diagnostics.push({
943
+ code: "BLUME_UNKNOWN_COMPONENT",
944
+ file: page.sourcePath ?? page.id,
945
+ message: `<${tag}> is used in ${page.route} but isn't a known component.`,
946
+ severity: "warning",
947
+ suggestion
948
+ });
949
+ }
950
+ }
951
+ return diagnostics;
952
+ };
953
+
954
+ // src/core/component-overrides.ts
955
+ import { existsSync as existsSync2 } from "node:fs";
956
+ import { dirname as dirname3, extname, isAbsolute, resolve as resolve3 } from "pathe";
957
+ import ts from "typescript";
958
+ var GROUPS = ["mdx", "layout", "islands"];
959
+ var GROUP_SET = new Set(GROUPS);
960
+ var isGroup = (name) => GROUP_SET.has(name);
961
+ var FRAMEWORK_BY_EXT = new Map([
962
+ ["jsx", "react"],
963
+ ["svelte", "svelte"],
964
+ ["tsx", "react"],
965
+ ["vue", "vue"]
966
+ ]);
967
+ var FRAMEWORK_LABEL = {
968
+ react: "React",
969
+ svelte: "Svelte",
970
+ vue: "Vue"
971
+ };
972
+ var COMPONENT_EXTS = [
973
+ "astro",
974
+ "tsx",
975
+ "ts",
976
+ "jsx",
977
+ "js",
978
+ "mjs",
979
+ "vue",
980
+ "svelte"
981
+ ];
982
+ var HYDRATION_MODES = new Set([
983
+ "idle",
984
+ "load",
985
+ "media",
986
+ "only",
987
+ "visible"
988
+ ]);
989
+ var isHydrationMode = (value) => HYDRATION_MODES.has(value);
990
+ var emptyAnalysis = () => ({
991
+ islands: [],
992
+ layout: [],
993
+ mdx: [],
994
+ warnings: []
995
+ });
996
+ var propName = (name) => ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
997
+ var addImportBindings = (map, statement) => {
998
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
999
+ return;
1000
+ }
1001
+ const specifier = statement.moduleSpecifier.text;
1002
+ const clause = statement.importClause;
1003
+ if (!clause) {
1004
+ return;
1005
+ }
1006
+ if (clause.name) {
1007
+ map.set(clause.name.text, { imported: "default", specifier });
1008
+ }
1009
+ const named = clause.namedBindings;
1010
+ if (named && ts.isNamedImports(named)) {
1011
+ for (const element of named.elements) {
1012
+ map.set(element.name.text, {
1013
+ imported: (element.propertyName ?? element.name).text,
1014
+ specifier
1015
+ });
1016
+ }
1017
+ }
1018
+ };
1019
+ var collectImports = (sourceFile) => {
1020
+ const map = new Map;
1021
+ for (const statement of sourceFile.statements) {
1022
+ addImportBindings(map, statement);
1023
+ }
1024
+ return map;
1025
+ };
1026
+ var unwrapObject = (expression) => {
1027
+ if (ts.isObjectLiteralExpression(expression)) {
1028
+ return expression;
1029
+ }
1030
+ if (ts.isCallExpression(expression)) {
1031
+ const [arg] = expression.arguments;
1032
+ return arg && ts.isObjectLiteralExpression(arg) ? arg : undefined;
1033
+ }
1034
+ if (ts.isAsExpression(expression) || ts.isParenthesizedExpression(expression)) {
1035
+ return unwrapObject(expression.expression);
1036
+ }
1037
+ return;
1038
+ };
1039
+ var findDefaultExportObject = (sourceFile) => {
1040
+ for (const statement of sourceFile.statements) {
1041
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
1042
+ return unwrapObject(statement.expression);
1043
+ }
1044
+ }
1045
+ return;
1046
+ };
1047
+ var probeExtension = (base) => {
1048
+ for (const extension of COMPONENT_EXTS) {
1049
+ const candidate = `${base}.${extension}`;
1050
+ if (existsSync2(candidate)) {
1051
+ return candidate;
1052
+ }
1053
+ }
1054
+ return null;
1055
+ };
1056
+ var toImport = (specifier, imported, dir) => {
1057
+ const relative2 = specifier.startsWith(".") || isAbsolute(specifier);
1058
+ let path = specifier;
1059
+ let extension = extname(specifier).slice(1).toLowerCase();
1060
+ if (relative2) {
1061
+ const absolute = isAbsolute(specifier) ? specifier : resolve3(dir, specifier);
1062
+ if (extension) {
1063
+ path = absolute;
1064
+ } else {
1065
+ const probed = probeExtension(absolute);
1066
+ path = probed ?? absolute;
1067
+ extension = probed ? extname(probed).slice(1).toLowerCase() : "";
1068
+ }
1069
+ }
1070
+ return {
1071
+ framework: FRAMEWORK_BY_EXT.get(extension) ?? null,
1072
+ name: imported,
1073
+ path
1074
+ };
1075
+ };
1076
+ var resolveIdentifier = (name, imports, dir) => {
1077
+ const binding = imports.get(name);
1078
+ return binding ? toImport(binding.specifier, binding.imported, dir) : null;
1079
+ };
1080
+ var applyDescriptorProperty = (descriptor, property, imports, dir) => {
1081
+ if (ts.isShorthandPropertyAssignment(property)) {
1082
+ if (property.name.text === "component") {
1083
+ descriptor.hadComponent = true;
1084
+ descriptor.source = resolveIdentifier(property.name.text, imports, dir);
1085
+ }
1086
+ return;
1087
+ }
1088
+ if (!ts.isPropertyAssignment(property)) {
1089
+ return;
1090
+ }
1091
+ const name = propName(property.name);
1092
+ const init = property.initializer;
1093
+ if (name === "component") {
1094
+ descriptor.hadComponent = true;
1095
+ if (ts.isStringLiteral(init)) {
1096
+ descriptor.source = toImport(init.text, "default", dir);
1097
+ } else if (ts.isIdentifier(init)) {
1098
+ descriptor.source = resolveIdentifier(init.text, imports, dir);
1099
+ }
1100
+ } else if (name === "client" && ts.isStringLiteral(init) && isHydrationMode(init.text)) {
1101
+ descriptor.client = init.text;
1102
+ } else if (name === "media" && ts.isStringLiteral(init)) {
1103
+ descriptor.media = init.text;
1104
+ }
1105
+ };
1106
+ var readDescriptor = (object, imports, dir) => {
1107
+ const descriptor = { hadComponent: false, source: null };
1108
+ for (const property of object.properties) {
1109
+ applyDescriptorProperty(descriptor, property, imports, dir);
1110
+ }
1111
+ return descriptor;
1112
+ };
1113
+ var finalize = (key, group, descriptor, label, identifier, warnings) => {
1114
+ const { client, media, source } = descriptor;
1115
+ if (group === "islands") {
1116
+ if (!source) {
1117
+ warnings.push(`Island override "${key}" couldn't be resolved to a file. Reference it by an imported component or a path string with an extension.`);
1118
+ return null;
1119
+ }
1120
+ if (!source.framework) {
1121
+ warnings.push(`Island override "${key}" (${label}) is not a React, Vue, or Svelte component; only framework components can be islands.`);
1122
+ return null;
1123
+ }
1124
+ }
1125
+ if (client === "media" && !media) {
1126
+ warnings.push(`Override "${key}" uses client: "media" but no \`media\` query was given; it will hydrate as if \`client: "load"\`.`);
1127
+ }
1128
+ if (client === "only" && source && !source.framework) {
1129
+ warnings.push(`Override "${key}" uses client: "only" but its framework couldn't be inferred; reference a .tsx/.jsx/.vue/.svelte file.`);
1130
+ }
1131
+ if (!client && source?.framework) {
1132
+ warnings.push(`Override "${key}" points to a ${FRAMEWORK_LABEL[source.framework]} component (${label}) but has no hydration mode, so it renders as static HTML with no interactivity. Add one, e.g. \`${key}: { component: ${JSON.stringify(label)}, client: "load" }\`.`);
1133
+ }
1134
+ const normalized = { identifier, key, source };
1135
+ if (client) {
1136
+ normalized.client = client;
1137
+ }
1138
+ if (media) {
1139
+ normalized.media = media;
1140
+ }
1141
+ return normalized;
1142
+ };
1143
+ var normalizeEntry = (entry, group, imports, dir, warnings) => {
1144
+ const defaultClient = group === "islands" ? "visible" : undefined;
1145
+ if (ts.isShorthandPropertyAssignment(entry)) {
1146
+ const name = entry.name.text;
1147
+ return finalize(name, group, {
1148
+ client: defaultClient,
1149
+ hadComponent: true,
1150
+ source: resolveIdentifier(name, imports, dir)
1151
+ }, name, true, warnings);
1152
+ }
1153
+ if (!ts.isPropertyAssignment(entry)) {
1154
+ return null;
1155
+ }
1156
+ const key = propName(entry.name);
1157
+ if (!key) {
1158
+ return null;
1159
+ }
1160
+ const value = entry.initializer;
1161
+ if (ts.isIdentifier(value)) {
1162
+ return finalize(key, group, {
1163
+ client: defaultClient,
1164
+ hadComponent: true,
1165
+ source: resolveIdentifier(value.text, imports, dir)
1166
+ }, value.text, true, warnings);
1167
+ }
1168
+ if (ts.isStringLiteral(value)) {
1169
+ return finalize(key, group, {
1170
+ client: defaultClient,
1171
+ hadComponent: true,
1172
+ source: toImport(value.text, "default", dir)
1173
+ }, value.text, false, warnings);
1174
+ }
1175
+ if (ts.isObjectLiteralExpression(value)) {
1176
+ const descriptor = readDescriptor(value, imports, dir);
1177
+ if (!descriptor.hadComponent) {
1178
+ warnings.push(`Override "${key}" is an object without a \`component\` field; expected \`{ component, client }\`.`);
1179
+ return null;
1180
+ }
1181
+ if (!descriptor.source) {
1182
+ warnings.push(`Override "${key}"'s \`component\` couldn't be resolved to a file. Reference an imported component or a path string with an extension.`);
1183
+ return null;
1184
+ }
1185
+ return finalize(key, group, { ...descriptor, client: descriptor.client ?? defaultClient }, key, false, warnings);
1186
+ }
1187
+ return { identifier: false, key, source: null };
1188
+ };
1189
+ var collectGroupOverrides = (property, imports, dir, result) => {
1190
+ if (!ts.isPropertyAssignment(property)) {
1191
+ return;
1192
+ }
1193
+ const name = propName(property.name);
1194
+ if (!(name && isGroup(name)) || !ts.isObjectLiteralExpression(property.initializer)) {
1195
+ return;
1196
+ }
1197
+ for (const entry of property.initializer.properties) {
1198
+ const normalized = normalizeEntry(entry, name, imports, dir, result.warnings);
1199
+ if (normalized) {
1200
+ result[name].push(normalized);
1201
+ }
1202
+ }
1203
+ };
1204
+ var analyzeComponentOverrides = (source, filePath) => {
1205
+ const result = emptyAnalysis();
1206
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, filePath.endsWith("tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
1207
+ const object = findDefaultExportObject(sourceFile);
1208
+ if (!object) {
1209
+ return result;
1210
+ }
1211
+ const imports = collectImports(sourceFile);
1212
+ const dir = dirname3(filePath);
1213
+ for (const property of object.properties) {
1214
+ collectGroupOverrides(property, imports, dir, result);
1215
+ }
1216
+ return result;
1217
+ };
1218
+
1219
+ // src/core/svg-dimensions.ts
1220
+ var ROOT = /<svg\s(?:[^>"']|"[^"]*"|'[^']*')*>/u;
1221
+ var WIDTH = /\swidth=(?<quote>['"])(?<value>[^%]+?)\k<quote>/u;
1222
+ var HEIGHT = /\sheight=(?<quote>['"])(?<value>[^%]+?)\k<quote>/u;
1223
+ var VIEWBOX = /\sviewBox=(?<quote>['"])(?<value>.+?)\k<quote>/iu;
1224
+ var INCH_CM = 2.54;
1225
+ var UNITS = new Map([
1226
+ ["cm", 96 / INCH_CM],
1227
+ ["em", 16],
1228
+ ["ex", 8],
1229
+ ["in", 96],
1230
+ ["m", 96 / INCH_CM * 100],
1231
+ ["mm", 96 / INCH_CM / 10],
1232
+ ["pc", 96 / 72 * 12],
1233
+ ["pt", 96 / 72],
1234
+ ["px", 1]
1235
+ ]);
1236
+ var LENGTH = /^(?<number>[0-9.]+(?:e\d+)?)(?<unit>in|cm|em|ex|m|mm|pc|pt|px)?$/u;
1237
+ var parseLength = (value) => {
1238
+ const groups = value === undefined ? undefined : LENGTH.exec(value)?.groups;
1239
+ if (!groups) {
1240
+ return;
1241
+ }
1242
+ const pixels = Math.round(Number(groups.number) * (UNITS.get(groups.unit ?? "px") ?? 1));
1243
+ return pixels > 0 ? pixels : undefined;
1244
+ };
1245
+ var attribute = (root, pattern) => pattern.exec(root)?.groups?.value;
1246
+ var viewBoxSize = (root) => {
1247
+ const raw = attribute(root, VIEWBOX);
1248
+ if (raw === undefined) {
1249
+ return null;
1250
+ }
1251
+ const bounds = raw.split(" ");
1252
+ const width = parseLength(bounds[2]);
1253
+ const height = parseLength(bounds[3]);
1254
+ return width && height ? { height, width } : null;
1255
+ };
1256
+ var svgDimensions = (svg) => {
1257
+ const root = ROOT.exec(svg)?.[0];
1258
+ if (root === undefined) {
1259
+ return null;
1260
+ }
1261
+ const width = parseLength(attribute(root, WIDTH));
1262
+ const height = parseLength(attribute(root, HEIGHT));
1263
+ if (width && height) {
1264
+ return { height, width };
1265
+ }
1266
+ const box = viewBoxSize(root);
1267
+ if (!box) {
1268
+ return null;
1269
+ }
1270
+ const ratio = box.width / box.height;
1271
+ if (width) {
1272
+ const scaled = Math.floor(width / ratio);
1273
+ return scaled > 0 ? { height: scaled, width } : null;
1274
+ }
1275
+ if (height) {
1276
+ const scaled = Math.floor(height * ratio);
1277
+ return scaled > 0 ? { height, width: scaled } : null;
1278
+ }
1279
+ return box;
1280
+ };
1281
+
1282
+ // src/core/tsconfig-aliases.ts
1283
+ import { existsSync as existsSync3 } from "node:fs";
1284
+ import { dirname as dirname4, join as join5, resolve as resolve4 } from "pathe";
1285
+ import ts2 from "typescript";
1286
+ var PARSE_HOST = {
1287
+ fileExists: ts2.sys.fileExists,
1288
+ readDirectory: () => [],
1289
+ readFile: ts2.sys.readFile,
1290
+ useCaseSensitiveFileNames: ts2.sys.useCaseSensitiveFileNames
1291
+ };
1292
+ var isPathTarget = (target) => typeof target === "string";
1293
+ var toAlias = (key, value, baseDir) => {
1294
+ const [first] = value;
1295
+ if (!isPathTarget(first)) {
1296
+ return null;
1297
+ }
1298
+ const find = key.endsWith("/*") ? key.slice(0, -2) : key;
1299
+ const target = first.endsWith("/*") ? first.slice(0, -2) : first;
1300
+ if (find === "" || find === "*") {
1301
+ return null;
1302
+ }
1303
+ return { find, replacement: resolve4(baseDir, target) };
1304
+ };
1305
+ var resolveTsconfigAliases = (root) => {
1306
+ const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join5(root, name)).find((file) => existsSync3(file));
1307
+ if (!entry) {
1308
+ return {};
1309
+ }
1310
+ const { config, error } = ts2.readConfigFile(entry, ts2.sys.readFile);
1311
+ if (error) {
1312
+ return {};
1313
+ }
1314
+ const configDir = dirname4(entry);
1315
+ const { options } = ts2.parseJsonConfigFileContent(config, PARSE_HOST, configDir, undefined, entry);
1316
+ const { paths } = options;
1317
+ if (!paths) {
1318
+ return {};
1319
+ }
1320
+ const { pathsBasePath } = options;
1321
+ const baseDir = options.baseUrl ?? pathsBasePath ?? configDir;
1322
+ const entries = [];
1323
+ for (const [key, value] of Object.entries(paths)) {
1324
+ const alias = toAlias(key, value, baseDir);
1325
+ if (alias) {
1326
+ entries.push([alias.find, alias.replacement]);
1327
+ }
1328
+ }
1329
+ return Object.fromEntries(entries);
1330
+ };
1331
+
1332
+ // src/markdown/language-icon.ts
1333
+ import {
1334
+ siAstro,
1335
+ siC,
1336
+ siCplusplus,
1337
+ siCss,
1338
+ siDart,
1339
+ siDocker,
1340
+ siGnubash,
1341
+ siGo,
1342
+ siGraphql,
1343
+ siHtml5,
1344
+ siJavascript,
1345
+ siJson,
1346
+ siKotlin,
1347
+ siLess,
1348
+ siLua,
1349
+ siMarkdown,
1350
+ siMdx,
1351
+ siMysql,
1352
+ siNextdotjs,
1353
+ siPhp,
1354
+ siPrisma,
1355
+ siPython,
1356
+ siReact,
1357
+ siRuby,
1358
+ siRust,
1359
+ siSass,
1360
+ siScala,
1361
+ siSvelte,
1362
+ siSvg,
1363
+ siSwift,
1364
+ siToml,
1365
+ siTypescript,
1366
+ siVuedotjs,
1367
+ siWebassembly,
1368
+ siYaml
1369
+ } from "simple-icons";
1370
+ var LANGUAGE_ICONS = {
1371
+ astro: siAstro,
1372
+ bash: siGnubash,
1373
+ c: siC,
1374
+ "c++": siCplusplus,
1375
+ cjs: siJavascript,
1376
+ cpp: siCplusplus,
1377
+ css: siCss,
1378
+ cts: siTypescript,
1379
+ dart: siDart,
1380
+ docker: siDocker,
1381
+ dockerfile: siDocker,
1382
+ go: siGo,
1383
+ gql: siGraphql,
1384
+ graphql: siGraphql,
1385
+ html: siHtml5,
1386
+ javascript: siJavascript,
1387
+ js: siJavascript,
1388
+ json: siJson,
1389
+ json5: siJson,
1390
+ jsonc: siJson,
1391
+ jsx: siReact,
1392
+ kotlin: siKotlin,
1393
+ kt: siKotlin,
1394
+ less: siLess,
1395
+ lua: siLua,
1396
+ markdown: siMarkdown,
1397
+ md: siMarkdown,
1398
+ mdx: siMdx,
1399
+ mjs: siJavascript,
1400
+ mts: siTypescript,
1401
+ nextjs: siNextdotjs,
1402
+ php: siPhp,
1403
+ prisma: siPrisma,
1404
+ py: siPython,
1405
+ python: siPython,
1406
+ rb: siRuby,
1407
+ react: siReact,
1408
+ rs: siRust,
1409
+ ruby: siRuby,
1410
+ rust: siRust,
1411
+ sass: siSass,
1412
+ scala: siScala,
1413
+ scss: siSass,
1414
+ sh: siGnubash,
1415
+ shell: siGnubash,
1416
+ sql: siMysql,
1417
+ svelte: siSvelte,
1418
+ svg: siSvg,
1419
+ swift: siSwift,
1420
+ toml: siToml,
1421
+ ts: siTypescript,
1422
+ tsx: siReact,
1423
+ typescript: siTypescript,
1424
+ vue: siVuedotjs,
1425
+ wasm: siWebassembly,
1426
+ yaml: siYaml,
1427
+ yml: siYaml,
1428
+ zsh: siGnubash
1429
+ };
1430
+ var languageIconSlug = (language) => LANGUAGE_ICONS[language.toLowerCase()]?.slug ?? null;
1431
+ var FENCE_LANGUAGE = /^[ \t]*(?:`{3,}|~{3,})[ \t]*(?<lang>[\w+#.-]+)/gmu;
1432
+ var PROP_LANGUAGE = /\blang(?:uage)?=["'](?<lang>[\w+#.-]+)["']/gu;
1433
+ var languageIconSlugsIn = (markdown) => {
1434
+ const slugs = new Set;
1435
+ for (const pattern of [FENCE_LANGUAGE, PROP_LANGUAGE]) {
1436
+ for (const match of markdown.matchAll(pattern)) {
1437
+ const slug = languageIconSlug(match.groups?.lang ?? "");
1438
+ if (slug) {
1439
+ slugs.add(slug);
1440
+ }
1441
+ }
1442
+ }
1443
+ return [...slugs].toSorted();
1444
+ };
1445
+ var iconBySlug = (slug) => Object.values(LANGUAGE_ICONS).find((icon) => icon.slug === slug);
1446
+ var maskUri = (path) => `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="${path}"/></svg>`)}")`;
1447
+ var languageIconCss = (slugs) => slugs.map((slug) => {
1448
+ const icon = iconBySlug(slug);
1449
+ if (!icon) {
1450
+ return "";
1451
+ }
1452
+ const mask = maskUri(icon.path);
1453
+ return `.prose > :where(pre[data-language][data-icon="${slug}"])::after {
1454
+ background-color: var(--blume-muted-foreground);
1455
+ -webkit-mask-image: ${mask};
1456
+ mask-image: ${mask};
1457
+ }`;
1458
+ }).filter((rule) => rule !== "").join(`
1459
+ `);
1460
+
1461
+ // src/markdown/mermaid.ts
1462
+ var MERMAID_FENCE = /^[ \t]*(?:`{3,}|~{3,})[ \t]*mermaid\b/mu;
1463
+ var hasMermaidFence = (text) => MERMAID_FENCE.test(text);
1464
+
1465
+ // src/og/derive.ts
1466
+ import { existsSync as existsSync4 } from "node:fs";
1467
+ import { isAbsolute as isAbsolute2, join as join6 } from "pathe";
1468
+ var CARD_WEIGHTS = [400, 600];
1469
+ var absoluteSrc = (root, src) => isAbsolute2(src) ? src : join6(root, src);
1470
+ var isNumericWeight = (weight) => typeof weight === "number";
1471
+ var isRangeWeight = (weight) => typeof weight === "string";
1472
+ var isFontName = (value) => typeof value === "string";
1473
+ var isLocalOgFont = (font) => typeof font !== "string" && ("src" in font);
1474
+ var googleWeights = (weights) => {
1475
+ const numbers = weights.filter(isNumericWeight);
1476
+ const used = numbers.filter((weight) => CARD_WEIGHTS.includes(weight));
1477
+ if (used.length > 0) {
1478
+ return used;
1479
+ }
1480
+ if (numbers.length > 0) {
1481
+ return numbers;
1482
+ }
1483
+ const [first] = weights;
1484
+ return weights.length === 1 && isRangeWeight(first) ? first : undefined;
1485
+ };
1486
+ var googleOgFont = (name, weights) => {
1487
+ const weight = googleWeights(weights);
1488
+ return weight === undefined ? { name } : { name, weight };
1489
+ };
1490
+ var localOgFonts = (font, root) => font.variants.map((variant) => {
1491
+ const entry = {
1492
+ name: font.name,
1493
+ src: absoluteSrc(root, variant.src)
1494
+ };
1495
+ const withWeight = isNumericWeight(variant.weight) ? { ...entry, weight: variant.weight } : entry;
1496
+ return variant.style === "normal" || variant.style === "italic" ? { ...withWeight, style: variant.style } : withWeight;
1497
+ });
1498
+ var roleFonts = (value, root) => {
1499
+ if (isFontName(value)) {
1500
+ if (!isFontSlug(value)) {
1501
+ return null;
1502
+ }
1503
+ const def = GOOGLE_FONTS[value];
1504
+ return [googleOgFont(def.family, def.weights)];
1505
+ }
1506
+ if ("variants" in value) {
1507
+ return localOgFonts(value, root);
1508
+ }
1509
+ if ((value.provider ?? "google") !== "google") {
1510
+ return null;
1511
+ }
1512
+ return [googleOgFont(value.name, value.weights ?? CARD_WEIGHTS)];
1513
+ };
1514
+ var roleFamily = (value) => {
1515
+ if (isFontName(value)) {
1516
+ return isFontSlug(value) ? GOOGLE_FONTS[value].family : null;
1517
+ }
1518
+ return value.name;
1519
+ };
1520
+ var deriveOgFonts = (fonts, root) => {
1521
+ const derived = [];
1522
+ const seen = new Set;
1523
+ const families = {};
1524
+ const roles = [
1525
+ ["title", fonts?.display],
1526
+ ["body", fonts?.body]
1527
+ ];
1528
+ for (const [role, value] of roles) {
1529
+ if (value === undefined) {
1530
+ continue;
1531
+ }
1532
+ const roleEntries = roleFonts(value, root);
1533
+ if (!roleEntries) {
1534
+ continue;
1535
+ }
1536
+ families[role] = roleFamily(value) ?? undefined;
1537
+ for (const entry of roleEntries) {
1538
+ const key = JSON.stringify(entry);
1539
+ if (!seen.has(key)) {
1540
+ seen.add(key);
1541
+ derived.push(entry);
1542
+ }
1543
+ }
1544
+ }
1545
+ const result = { fonts: derived };
1546
+ if (families.title || families.body) {
1547
+ result.families = families;
1548
+ }
1549
+ return result;
1550
+ };
1551
+ var resolveOgFontSources = (fonts, root) => fonts.map((font) => isLocalOgFont(font) ? { ...font, src: absoluteSrc(root, font.src) } : font);
1552
+ var resolveOgFonts = (options, root) => {
1553
+ if (options.ogFonts) {
1554
+ return { fonts: resolveOgFontSources(options.ogFonts, root) };
1555
+ }
1556
+ return options.themeFontsConfigured ? deriveOgFonts(options.themeFonts, root) : { fonts: [] };
1557
+ };
1558
+ var missingFontFiles = (options, root) => {
1559
+ const sources = [];
1560
+ for (const value of Object.values(options.themeFonts ?? {})) {
1561
+ if (!isFontName(value) && "variants" in value) {
1562
+ sources.push(...value.variants.map((variant) => absoluteSrc(root, variant.src)));
1563
+ }
1564
+ }
1565
+ for (const font of options.ogFonts) {
1566
+ if (isLocalOgFont(font)) {
1567
+ sources.push(absoluteSrc(root, font.src));
1568
+ }
1569
+ }
1570
+ return [...new Set(sources)].filter((path) => !existsSync4(path));
1571
+ };
1572
+
1573
+ // src/og/logo.ts
1574
+ import { existsSync as existsSync5, readFileSync } from "node:fs";
1575
+ import { join as join7 } from "pathe";
1576
+ var resolveOgLogo = (project, source) => {
1577
+ if (!source?.toLowerCase().endsWith(".svg")) {
1578
+ return;
1579
+ }
1580
+ const relative2 = source.replace(/^\//u, "");
1581
+ const file = [
1582
+ join7(project.context.root, "public", relative2),
1583
+ join7(project.context.root, relative2)
1584
+ ].find((path) => existsSync5(path));
1585
+ return file ? readFileSync(file, "utf-8") : undefined;
1586
+ };
1587
+
1588
+ // src/openapi/scalar.ts
1589
+ import { readFile as readFile2 } from "node:fs/promises";
1590
+ import { isAbsolute as isAbsolute3, join as join8 } from "pathe";
1591
+
1592
+ // src/theme/palette.ts
1593
+ var FALLBACK_ACCENT = "oklch(0.62 0.16 250)";
1594
+ var ACCENTS = {
1595
+ blue: FALLBACK_ACCENT,
1596
+ green: "oklch(0.6 0.16 150)",
1597
+ orange: "oklch(0.68 0.17 50)",
1598
+ pink: "oklch(0.65 0.2 350)",
1599
+ purple: "oklch(0.58 0.2 290)",
1600
+ red: "oklch(0.58 0.22 25)",
1601
+ teal: "oklch(0.6 0.12 195)"
1602
+ };
1603
+ var isAccentPreset = (value) => Object.hasOwn(ACCENTS, value);
1604
+ var CSS_COLOR = /^[\w\s#%.,()/+-]+$/u;
1605
+ var safeColor = (value, fallback) => CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
1606
+ var presetOrColor = (value) => isAccentPreset(value) ? ACCENTS[value] : safeColor(value, FALLBACK_ACCENT);
1607
+ var safeColorOrNull = (value) => value && CSS_COLOR.test(value.trim()) ? value.trim() : null;
1608
+ var RADII = {
1609
+ lg: "0.75rem",
1610
+ md: "0.5rem",
1611
+ none: "0",
1612
+ sm: "0.25rem"
1613
+ };
1614
+ var cssString = (value) => JSON.stringify(value);
1615
+ var backgroundImageCss = (image) => `url(${cssString(image)})`;
1616
+ var cssToken = (name, value) => value ? [` ${name}: ${value};`] : [];
1617
+ var themeRootCss = (theme, options) => [
1618
+ ` --blume-accent: ${options.accent};`,
1619
+ ...cssToken("--blume-action", options.action),
1620
+ ...cssToken("--blume-action-foreground", options.action ? "oklch(1 0 0)" : null),
1621
+ ...cssToken("--blume-background", safeColorOrNull(theme.background?.light)),
1622
+ ...cssToken("--blume-background-image", theme.backgroundImage?.light ? backgroundImageCss(theme.backgroundImage.light) : null),
1623
+ ` --blume-radius: ${options.radius};`
1624
+ ].filter(Boolean).join(`
1625
+ `);
1626
+ var themeDarkCss = (theme, options) => {
1627
+ const tokens = [
1628
+ ` --blume-accent: ${options.accent};`,
1629
+ " --blume-accent-foreground: oklch(1 0 0);",
1630
+ ...cssToken("--blume-action", options.action),
1631
+ ...cssToken("--blume-action-foreground", options.action ? "oklch(1 0 0)" : null),
1632
+ ...cssToken("--blume-background", safeColorOrNull(theme.background?.dark)),
1633
+ ...cssToken("--blume-background-image", theme.backgroundImage?.dark ? backgroundImageCss(theme.backgroundImage.dark) : null)
1634
+ ].filter(Boolean);
1635
+ return `:root[data-theme="dark"] {
1636
+ ${tokens.join(`
1637
+ `)}
1638
+ }
1639
+ `;
1640
+ };
1641
+ var resolveAccent = (theme) => ({
1642
+ dark: presetOrColor(theme.accent.dark),
1643
+ light: presetOrColor(theme.accent.light)
1644
+ });
1645
+ var resolveRadius = (theme) => RADII[theme.radius];
1646
+ var buildThemeCss = (theme) => {
1647
+ const accent = resolveAccent(theme);
1648
+ const action = theme.action ? presetOrColor(theme.action) : null;
1649
+ const radius = RADII[theme.radius];
1650
+ const root = themeRootCss(theme, {
1651
+ accent: accent.light,
1652
+ action,
1653
+ radius
1654
+ });
1655
+ const dark = themeDarkCss(theme, {
1656
+ accent: accent.dark,
1657
+ action
1658
+ });
1659
+ return `/* Generated by Blume from theme config. */
1660
+ :root {
1661
+ ${root}
1662
+ }
1663
+ ${dark}`;
1664
+ };
1665
+
1666
+ // src/openapi/scalar.ts
1667
+ var URL_SPEC = /^https?:\/\//u;
1668
+ var referencePagePath = (route) => {
1669
+ const segments = trimChar(route, "/");
1670
+ return `${segments === "" ? "index" : segments}.astro`;
1671
+ };
1672
+ var themeConfiguration = (config, override) => {
1673
+ if (override) {
1674
+ return { theme: override };
1675
+ }
1676
+ const accent = resolveAccent(config.theme);
1677
+ const radius = resolveRadius(config.theme);
1678
+ return {
1679
+ customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent.light};--scalar-radius:${radius};}.dark-mode{--scalar-color-accent:${accent.dark};}`
1680
+ };
1681
+ };
1682
+ var specConfiguration = async (spec, root) => {
1683
+ if (URL_SPEC.test(spec)) {
1684
+ return { config: { url: spec } };
1685
+ }
1686
+ const absolute = isAbsolute3(spec) ? spec : join8(root, spec);
1687
+ try {
1688
+ return { config: { content: await readFile2(absolute, "utf-8") } };
1689
+ } catch {
1690
+ return {
1691
+ config: { url: spec },
1692
+ warning: `API reference spec not found: "${spec}" (looked in ${absolute}).`
1693
+ };
1694
+ }
1695
+ };
1696
+ var acceptScalarReference = (ref2, seen, contentRoutes, warnings) => {
1697
+ if (ref2.renderer !== "scalar") {
1698
+ return null;
1699
+ }
1700
+ if (seen.has(ref2.route)) {
1701
+ warnings.push(`Two API reference sources resolve to ${ref2.route}; keeping the first.`);
1702
+ return null;
1703
+ }
1704
+ if (contentRoutes.has(ref2.route)) {
1705
+ warnings.push(`API reference route ${ref2.route} collides with a content page; skipping the reference there.`);
1706
+ return null;
1707
+ }
1708
+ seen.add(ref2.route);
1709
+ return ref2;
1710
+ };
1711
+ var buildReferenceFiles = async (options) => {
1712
+ const { config, root, contentRoutes } = options;
1713
+ const warnings = [];
1714
+ const seen = new Set;
1715
+ const accepted = [];
1716
+ for (const ref2 of resolveReferences(config)) {
1717
+ const next = acceptScalarReference(ref2, seen, contentRoutes, warnings);
1718
+ if (next) {
1719
+ accepted.push(next);
1720
+ }
1721
+ }
1722
+ const built = await Promise.all(accepted.map(async (ref2) => ({
1723
+ ref: ref2,
1724
+ spec: await specConfiguration(ref2.spec, root)
1725
+ })));
1726
+ const files = [];
1727
+ for (const { ref: ref2, spec } of built) {
1728
+ if (spec.warning) {
1729
+ warnings.push(spec.warning);
1730
+ }
1731
+ const pagePath = referencePagePath(ref2.route);
1732
+ files.push({
1733
+ content: scalarReferenceTemplate({
1734
+ configuration: {
1735
+ ...spec.config,
1736
+ ...themeConfiguration(config, ref2.theme),
1737
+ ...ref2.scalar
1738
+ },
1739
+ noindex: ref2.noindex,
1740
+ route: ref2.route,
1741
+ title: ref2.label
1742
+ }),
1743
+ pagePath
1744
+ });
1745
+ }
1746
+ return { files, warnings };
1747
+ };
1748
+
1749
+ // src/search/popular.ts
1750
+ var resolveSearchPopular = (popular, basePath) => popular.map(({ href, icon, label }) => {
1751
+ const page = {
1752
+ label,
1753
+ route: withBasePath(basePath, href)
1754
+ };
1755
+ if (icon) {
1756
+ page.icon = icon;
1757
+ }
1758
+ return page;
1759
+ });
1760
+
1761
+ // src/theme/code-block-padding.ts
1762
+ var CODE_PADDING_BLOCK_REM = 1;
1763
+
1764
+ // src/theme/entry.ts
1765
+ var CODE_SCROLL_INSET_REM = 0.375;
1766
+ var DARK_VARIANT = `/* Dark mode is driven by data-theme on the <html> element. */
1767
+ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));`;
1768
+ var COLOR_SCHEME_DEFAULTS = `:root {
1769
+ color-scheme: light;
1770
+ }
1771
+
1772
+ :root[data-theme="dark"] {
1773
+ color-scheme: dark;
1774
+ }`;
1775
+ var TOKEN_DEFAULTS = `:root {
1776
+ --blume-background: oklch(1 0 0);
1777
+ --blume-background-image: none;
1778
+ --blume-background-image-repeat: no-repeat;
1779
+ --blume-background-image-size: cover;
1780
+ --blume-foreground: oklch(0.145 0 0);
1781
+ --blume-muted: oklch(0.965 0 0);
1782
+ /* 5.28:1 on the page background. The headroom over 4.5:1 is the point: muted
1783
+ text is 14px body copy routinely set on a tinted surface (callouts, badges,
1784
+ panels), and each tint costs a few tenths. */
1785
+ --blume-muted-foreground: oklch(0.53 0 0);
1786
+ --blume-border: oklch(0.88 0.006 260 / 0.72);
1787
+ --blume-accent: oklch(0.145 0 0);
1788
+ --blume-accent-foreground: oklch(1 0 0);
1789
+ --blume-action: var(--blume-accent);
1790
+ --blume-action-foreground: var(--blume-accent-foreground);
1791
+ --blume-code-background: oklch(0.99 0 0);
1792
+ /* Shiki notation transformers: line/word highlight, diff add/remove. */
1793
+ --blume-code-highlight: oklch(0.55 0.16 255 / 0.1);
1794
+ --blume-code-highlight-border: oklch(0.55 0.16 255 / 0.55);
1795
+ --blume-code-add: oklch(0.72 0.16 150 / 0.16);
1796
+ --blume-code-add-border: oklch(0.52 0.15 150 / 0.7);
1797
+ --blume-code-remove: oklch(0.66 0.21 22 / 0.16);
1798
+ --blume-code-remove-border: oklch(0.55 0.2 22 / 0.7);
1799
+ --blume-code-word: oklch(0.55 0.16 255 / 0.16);
1800
+ --blume-code-word-border: oklch(0.55 0.16 255 / 0.5);
1801
+ --blume-radius: 0.75rem;
1802
+
1803
+ /* Max width of the prose content column (article, breadcrumb, TOC, feedback,
1804
+ pagination, last-updated). Override to re-measure the whole column at once. */
1805
+ --blume-content-width: 42rem;
1806
+
1807
+ /*
1808
+ * Font tokens resolve through optional src variables that Astro's Fonts API
1809
+ * populates (via theme.fonts). When unset they fall back to the system
1810
+ * stacks, so the default look is unchanged. The display font defaults to the
1811
+ * body font.
1812
+ */
1813
+ --blume-font-body: var(
1814
+ --blume-font-body-src,
1815
+ ui-sans-serif,
1816
+ system-ui,
1817
+ -apple-system,
1818
+ "Segoe UI",
1819
+ Roboto,
1820
+ Helvetica,
1821
+ Arial,
1822
+ sans-serif
1823
+ );
1824
+ --blume-font-mono: var(
1825
+ --blume-font-mono-src,
1826
+ ui-monospace,
1827
+ "SF Mono",
1828
+ "Cascadia Code",
1829
+ "Source Code Pro",
1830
+ Menlo,
1831
+ Consolas,
1832
+ monospace
1833
+ );
1834
+ --blume-font-display: var(--blume-font-display-src, var(--blume-font-body));
1835
+ }
1836
+
1837
+ :root[data-theme="dark"] {
1838
+ --blume-background: oklch(0.085 0 0);
1839
+ --blume-background-image: none;
1840
+ --blume-background-image-repeat: no-repeat;
1841
+ --blume-background-image-size: cover;
1842
+ --blume-foreground: oklch(0.96 0 0);
1843
+ --blume-muted: oklch(0.16 0 0);
1844
+ --blume-muted-foreground: oklch(0.68 0 0);
1845
+ --blume-border: oklch(0.24 0 0 / 0.8);
1846
+ --blume-accent: oklch(0.96 0 0);
1847
+ --blume-accent-foreground: oklch(0.085 0 0);
1848
+ --blume-action: var(--blume-accent);
1849
+ --blume-action-foreground: var(--blume-accent-foreground);
1850
+ --blume-code-background: oklch(0.12 0 0);
1851
+ /* Brighter tints read better over the dark code surface. */
1852
+ --blume-code-highlight: oklch(0.7 0.14 255 / 0.16);
1853
+ --blume-code-highlight-border: oklch(0.7 0.14 255 / 0.6);
1854
+ --blume-code-add: oklch(0.78 0.17 150 / 0.2);
1855
+ --blume-code-add-border: oklch(0.72 0.16 150 / 0.7);
1856
+ --blume-code-remove: oklch(0.72 0.21 22 / 0.22);
1857
+ --blume-code-remove-border: oklch(0.7 0.2 22 / 0.7);
1858
+ --blume-code-word: oklch(0.7 0.14 255 / 0.22);
1859
+ --blume-code-word-border: oklch(0.7 0.14 255 / 0.55);
1860
+ }`;
1861
+ var THEME_MAPPING = `@theme inline {
1862
+ --color-background: var(--blume-background);
1863
+ --color-foreground: var(--blume-foreground);
1864
+ --color-muted: var(--blume-muted);
1865
+ --color-muted-foreground: var(--blume-muted-foreground);
1866
+ --color-border: var(--blume-border);
1867
+ --color-accent: var(--blume-accent);
1868
+ --color-accent-foreground: var(--blume-accent-foreground);
1869
+ --color-action: var(--blume-action);
1870
+ --color-action-foreground: var(--blume-action-foreground);
1871
+ --color-code: var(--blume-code-background);
1872
+ --radius-blume: var(--blume-radius);
1873
+ --container-content: var(--blume-content-width);
1874
+ --font-sans: var(--blume-font-body);
1875
+ --font-mono: var(--blume-font-mono);
1876
+ --font-display: var(--blume-font-display);
1877
+ }`;
1878
+ var tailwindEntryTemplate = (options) => `/* Generated by Blume. Do not edit. */
1879
+ @import "tailwindcss";
1880
+ @plugin "@tailwindcss/typography";
1881
+
1882
+ /* Scan Blume's components and the user's project for utility classes. */
1883
+ ${options.sources.map((source) => `@source "${source}";`).join(`
1884
+ `)}
1885
+
1886
+ ${DARK_VARIANT}
1887
+
1888
+ ${COLOR_SCHEME_DEFAULTS}
1889
+
1890
+ ${TOKEN_DEFAULTS}
1891
+
1892
+ ${THEME_MAPPING}
1893
+
1894
+ @layer base {
1895
+ /* Nothing refuses to shrink below its intrinsic content width. This global
1896
+ min-width reset defuses the classic flex/grid overflow — a long or
1897
+ truncating child forcing its container (and the page) past the viewport
1898
+ edge — so components don't need per-element min-w-0 overrides. */
1899
+ * {
1900
+ min-width: 0;
1901
+ }
1902
+ /* Interactive controls get a pointer cursor unless disabled. */
1903
+ button:not(:disabled),
1904
+ [role="button"]:not(:disabled) {
1905
+ cursor: pointer;
1906
+ }
1907
+ html {
1908
+ scroll-behavior: smooth;
1909
+ scroll-padding-top: 4.5rem;
1910
+ text-rendering: optimizeLegibility;
1911
+ }
1912
+ /* Headings use the display font (defaults to the body font when unset).
1913
+ The tightened tracking is part of the theme, not the font: display-tuned
1914
+ families bake it into their metrics, but a text family promoted to
1915
+ headings (including the Inter default) reads loose without it. -0.05em
1916
+ was matched visually against Inter Tight, the previous display default. */
1917
+ h1,
1918
+ h2,
1919
+ h3,
1920
+ h4,
1921
+ h5,
1922
+ h6 {
1923
+ font-family: var(--font-display);
1924
+ letter-spacing: -0.05em;
1925
+ }
1926
+ :focus-visible {
1927
+ outline: 2px solid var(--blume-accent);
1928
+ outline-offset: 2px;
1929
+ border-radius: 2px;
1930
+ }
1931
+ body {
1932
+ background-attachment: fixed;
1933
+ background-image: var(--blume-background-image);
1934
+ background-position: center top;
1935
+ background-repeat: var(--blume-background-image-repeat);
1936
+ background-size: var(--blume-background-image-size);
1937
+ }
1938
+ @media (prefers-reduced-motion: reduce) {
1939
+ html {
1940
+ scroll-behavior: auto;
1941
+ }
1942
+ }
1943
+ }
1944
+
1945
+ /* Same-origin navigations ride Astro's client router (the layouts render
1946
+ ClientRouter), which owns the page-transition styling — including its own
1947
+ prefers-reduced-motion guard — so the theme declares nothing about it. The
1948
+ cross-document view-transition opt-in that briefly lived here is
1949
+ deliberately gone: mixing it with the client router double-animates the
1950
+ full-load navigations the router hands back to the browser. */
1951
+
1952
+ /* Code reads left-to-right regardless of page direction; only the surrounding
1953
+ chrome mirrors for RTL. Inline code is isolated so LTR identifiers don't
1954
+ disturb the bidi flow of right-to-left prose. */
1955
+ [dir="rtl"] pre,
1956
+ [dir="rtl"] .prose pre {
1957
+ direction: ltr;
1958
+ text-align: left;
1959
+ }
1960
+ [dir="rtl"] :not(pre) > code {
1961
+ unicode-bidi: isolate;
1962
+ }
1963
+
1964
+ /* The sidebar's rows. A docs site renders every sidebar row on every page,
1965
+ so a large site's HTML is mostly this markup — one utility per row kind
1966
+ instead of the dozen classes each expands to keeps every page (and the
1967
+ build's HTML output) small. Kept in sync with NavTree.astro: a page row,
1968
+ a page-mode group's drill-in button, a collapsible group's summary and
1969
+ the link inside it, and a flat group's heading and the link inside it. */
1970
+ @utility blume-nav-link {
1971
+ @apply block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground;
1972
+ }
1973
+ @utility blume-nav-drill {
1974
+ @apply flex w-full items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-left font-medium text-foreground text-sm transition-colors hover:bg-muted;
1975
+ }
1976
+ @utility blume-nav-summary {
1977
+ @apply flex cursor-pointer list-none items-center gap-1.5 rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden;
1978
+ }
1979
+ @utility blume-nav-summary-link {
1980
+ @apply -my-1 flex flex-1 items-center gap-1.5 rounded py-1 transition-colors aria-[current=page]:text-foreground aria-[current=page]:font-semibold;
1981
+ }
1982
+ @utility blume-nav-heading {
1983
+ @apply mb-2 flex items-center gap-1.5 px-2.5 font-medium text-foreground text-sm;
1984
+ }
1985
+ @utility blume-nav-heading-link {
1986
+ @apply -ml-1 flex flex-1 items-center gap-1.5 rounded-[0.65rem] px-1 py-0.5 text-foreground transition-colors hover:bg-muted aria-[current=page]:bg-muted;
1987
+ }
1988
+
1989
+ /* Theme Tailwind Typography (prose) with Blume tokens. */
1990
+ .prose {
1991
+ --tw-prose-body: var(--blume-foreground);
1992
+ --tw-prose-headings: var(--blume-foreground);
1993
+ --tw-prose-lead: var(--blume-muted-foreground);
1994
+ --tw-prose-links: var(--blume-foreground);
1995
+ --tw-prose-bold: var(--blume-foreground);
1996
+ --tw-prose-counters: var(--blume-muted-foreground);
1997
+ --tw-prose-bullets: var(--blume-muted-foreground);
1998
+ --tw-prose-hr: var(--blume-border);
1999
+ --tw-prose-quotes: var(--blume-muted-foreground);
2000
+ --tw-prose-quote-borders: var(--blume-border);
2001
+ --tw-prose-captions: var(--blume-muted-foreground);
2002
+ --tw-prose-code: var(--blume-foreground);
2003
+ --tw-prose-pre-code: var(--blume-foreground);
2004
+ --tw-prose-pre-bg: var(--blume-code-background);
2005
+ --tw-prose-th-borders: var(--blume-border);
2006
+ --tw-prose-td-borders: var(--blume-border);
2007
+ color: var(--blume-muted-foreground);
2008
+ font-size: 0.875rem;
2009
+ line-height: 1.7;
2010
+ /* An unbreakable run — an API permission, a broker list, a bare URL used as
2011
+ its own link text, an OpenAPI operation title of the form
2012
+ \`METHOD /very/long/{path}\`, a module-qualified name in a generated
2013
+ reference — paints past the content column, and nothing between the
2014
+ paragraph and the viewport clips it, so it lands in the document's scroll
2015
+ width and drags the page sideways on a phone. Inherited, so headings and
2016
+ inline code are covered too; the only re-declarations below are the
2017
+ opt-in code-wrap mode and Twoslash, which want different values.
2018
+ \`break-word\` and not \`anywhere\`: only \`anywhere\` reduces min-content,
2019
+ which is what sizes \`table-layout: auto\` columns, so it would resize
2020
+ every table on the site. The flip side is that a token in a table cell
2021
+ still never breaks — the column grows to fit it — so wide tables rely on
2022
+ the .blume-table-scroll wrapper, not on this. */
2023
+ overflow-wrap: break-word;
2024
+ }
2025
+
2026
+ /* No letter-spacing here: prose headings inherit the base h1-h6 rule's
2027
+ display tracking, same as headings outside the prose column. */
2028
+ .prose :where(h1, h2, h3, h4) {
2029
+ font-weight: 500;
2030
+ }
2031
+
2032
+ .prose :where(h1) {
2033
+ font-size: 3rem;
2034
+ line-height: 1.1;
2035
+ margin: 0 0 1rem;
2036
+ }
2037
+
2038
+ .prose :where(h2) {
2039
+ font-size: 1.875rem;
2040
+ line-height: 1.2;
2041
+ margin-top: 3rem;
2042
+ }
2043
+
2044
+ .prose :where(h3) {
2045
+ font-size: 1.25rem;
2046
+ line-height: 1.35;
2047
+ }
2048
+
2049
+ .prose :where(h4) {
2050
+ color: var(--blume-muted-foreground);
2051
+ font-size: 0.875rem;
2052
+ line-height: 1.4;
2053
+ }
2054
+
2055
+ .prose :where(h1, h2, h3, h4, h5, h6) a {
2056
+ color: inherit;
2057
+ font-weight: inherit;
2058
+ text-decoration: none;
2059
+ }
2060
+
2061
+ /* Auto-generated heading permalinks (markdown.headingAnchors): the whole
2062
+ heading links to its own id, with a muted “#” revealed on hover or keyboard
2063
+ focus so the link is discoverable without cluttering the heading at rest. */
2064
+ .prose :where(h2, h3, h4, h5, h6) a.blume-heading-anchor::after {
2065
+ content: "#";
2066
+ color: var(--blume-muted-foreground);
2067
+ margin-inline-start: 0.35em;
2068
+ opacity: 0;
2069
+ transition: opacity 0.15s ease;
2070
+ }
2071
+ .prose :where(h2, h3, h4, h5, h6) a.blume-heading-anchor:hover::after,
2072
+ .prose :where(h2, h3, h4, h5, h6) a.blume-heading-anchor:focus-visible::after {
2073
+ opacity: 1;
2074
+ }
2075
+
2076
+ /* A [toc]-marked heading exists only for the table of contents: it stays in
2077
+ the flow as a zero-height, invisible anchor target so its TOC entry (and any
2078
+ deep link) still has somewhere to scroll to, without rendering on the page. */
2079
+ .prose :where(.blume-toc-only) {
2080
+ border: 0;
2081
+ height: 0;
2082
+ margin: 0;
2083
+ overflow: hidden;
2084
+ padding: 0;
2085
+ visibility: hidden;
2086
+ }
2087
+
2088
+ .prose :where(h2:first-child) {
2089
+ border-top: 0;
2090
+ padding-top: 0;
2091
+ }
2092
+
2093
+ .prose :where(p, ul, ol) {
2094
+ margin-top: 1rem;
2095
+ margin-bottom: 1rem;
2096
+ }
2097
+
2098
+ .prose :where(strong) {
2099
+ font-weight: 600;
2100
+ }
2101
+
2102
+ .prose :where(a) {
2103
+ color: var(--blume-foreground);
2104
+ font-weight: 500;
2105
+ text-decoration-line: underline;
2106
+ text-decoration-style: dotted;
2107
+ text-decoration-thickness: 1px;
2108
+ text-underline-offset: 0.2em;
2109
+ }
2110
+
2111
+ .prose :where(a[data-blume-card]) {
2112
+ color: inherit;
2113
+ font-weight: inherit;
2114
+ text-decoration: none;
2115
+ }
2116
+
2117
+ .prose :where(hr) {
2118
+ margin: 2.5rem 0;
2119
+ }
2120
+
2121
+ .prose :where(pre) {
2122
+ background: transparent;
2123
+ border: 1px solid var(--blume-border);
2124
+ border-radius: var(--blume-radius);
2125
+ color: var(--blume-foreground);
2126
+ font-family: var(--font-mono);
2127
+ font-size: 0.8125rem;
2128
+ line-height: 1.55;
2129
+ margin: 1.5rem 0;
2130
+ overflow-x: auto;
2131
+ padding: ${CODE_PADDING_BLOCK_REM}rem 0;
2132
+ position: relative;
2133
+ }
2134
+
2135
+ .prose :where(pre.astro-code) {
2136
+ background: transparent !important;
2137
+ color: var(--shiki-light, var(--blume-foreground)) !important;
2138
+ }
2139
+
2140
+ :root[data-theme="dark"] .prose :where(pre.astro-code) {
2141
+ color: var(--shiki-dark, var(--blume-foreground)) !important;
2142
+ }
2143
+
2144
+ .prose :where(pre.astro-code span) {
2145
+ background: transparent !important;
2146
+ color: var(--shiki-light, inherit);
2147
+ font-style: var(--shiki-light-font-style, inherit);
2148
+ font-weight: var(--shiki-light-font-weight, inherit);
2149
+ text-decoration: var(--shiki-light-text-decoration, inherit);
2150
+ }
2151
+
2152
+ :root[data-theme="dark"] .prose :where(pre.astro-code span) {
2153
+ color: var(--shiki-dark, inherit);
2154
+ font-style: var(--shiki-dark-font-style, inherit);
2155
+ font-weight: var(--shiki-dark-font-weight, inherit);
2156
+ text-decoration: var(--shiki-dark-text-decoration, inherit);
2157
+ }
2158
+
2159
+ /* The Diff component (@pierre/diffs) renders into a declarative shadow root and
2160
+ themes itself with light-dark(). Drive that from Blume's data-theme instead
2161
+ of the OS, scoped to the diff host: the custom property pierces the shadow
2162
+ boundary, where the appended host color-scheme rule consumes it. */
2163
+ blume-diff {
2164
+ --blume-diff-color-scheme: light;
2165
+ display: block;
2166
+ }
2167
+
2168
+ :root[data-theme="dark"] blume-diff {
2169
+ --blume-diff-color-scheme: dark;
2170
+ }
2171
+
2172
+ .prose :where(pre[data-language]) {
2173
+ padding-top: 3.75rem;
2174
+ }
2175
+
2176
+ .prose :where(pre[data-language])::before {
2177
+ align-items: center;
2178
+ border-bottom: 1px solid var(--blume-border);
2179
+ color: var(--blume-muted-foreground);
2180
+ content: attr(data-language);
2181
+ display: flex;
2182
+ font-family: var(--font-sans);
2183
+ font-size: 0.75rem;
2184
+ font-weight: 500;
2185
+ height: 2.75rem;
2186
+ left: 0;
2187
+ padding: 0 3.25rem 0 1rem;
2188
+ position: absolute;
2189
+ right: 0;
2190
+ top: 0;
2191
+ }
2192
+
2193
+ .prose :where(pre[data-title])::before {
2194
+ content: attr(data-title);
2195
+ }
2196
+
2197
+ /* Language icon (simple-icons) sits at the header's left edge; the label shifts
2198
+ right to make room. Injected at build time by the language-icon transformer.
2199
+ The icon is gated on data-language (the header bar), not just data-icon: the
2200
+ transformer runs whenever icons are on, but a header-less standalone
2201
+ CodeBlock (no title) never gets data-language, so without this gate the
2202
+ absolutely-positioned icon would overlap the first code line. Fenced code
2203
+ and titled blocks always have a header, so the icon shows there. */
2204
+ .prose > :where(pre[data-language][data-icon])::after {
2205
+ content: "";
2206
+ height: 0.875rem;
2207
+ left: 1rem;
2208
+ -webkit-mask-position: center;
2209
+ -webkit-mask-repeat: no-repeat;
2210
+ -webkit-mask-size: contain;
2211
+ mask-position: center;
2212
+ mask-repeat: no-repeat;
2213
+ mask-size: contain;
2214
+ pointer-events: none;
2215
+ position: absolute;
2216
+ top: 0.875rem;
2217
+ width: 0.875rem;
2218
+ }
2219
+
2220
+ .prose > :where(pre[data-language][data-icon])::before {
2221
+ padding-left: 2.5rem;
2222
+ }
2223
+
2224
+ /* One mask rule per language the site's Markdown uses (see
2225
+ markdown/language-icon.ts); the ::after above only gets a color and a mask
2226
+ from these, so a block whose language has no rule paints nothing. */
2227
+ ${options.languageIcons ?? ""}
2228
+
2229
+ .prose :where(pre code) {
2230
+ background: transparent;
2231
+ border-radius: 0;
2232
+ color: inherit;
2233
+ font-size: inherit;
2234
+ font-weight: 400;
2235
+ padding: 0;
2236
+ }
2237
+
2238
+ /* Long lines scroll inside the code element, not the pre: the pre stays static
2239
+ so its absolute header bar (::before) and copy button don't drift with the
2240
+ scroll. The pre's horizontal padding lives here so content still scrolls
2241
+ edge-to-edge past it. Two contexts opt out: twoslash blocks (popups must
2242
+ escape any scroll container — see theme/twoslash.ts), and the API request
2243
+ panel, which owns its code layout and keeps the copy control in the panel
2244
+ header. Every other component that hosts a code block — Tabs, CodeGroup,
2245
+ Steps, Callout, Card, Accordion — is real prose content and keeps the inset,
2246
+ even though its chrome wrapper is not-prose. */
2247
+ .prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
2248
+ display: block;
2249
+ /* Tall blocks scroll vertically in place instead of taking the page. Both
2250
+ axes live on the code element for the same reason: the pre stays static
2251
+ so the header bar and copy button never drift with the scroll. */
2252
+ max-height: 24rem;
2253
+ overflow: auto;
2254
+ /* The small bottom inset keeps the horizontal thumb off the last line's
2255
+ descenders now that scrollbars are visible. The pre gives up the same
2256
+ amount below (next rule), so the inset adds no height to the block. */
2257
+ padding: 0 1.25rem ${CODE_SCROLL_INSET_REM}rem;
2258
+ /* Thin theme-colored scrollbars, matching the sidebar treatment, so a
2259
+ height-capped block reads as scrollable instead of simply ending.
2260
+ Safari before 18.2 supports neither property and falls back to the
2261
+ platform-default scrollbar — acceptable, since macOS overlays it. */
2262
+ scrollbar-color: var(--blume-border) transparent;
2263
+ scrollbar-width: thin;
2264
+ }
2265
+
2266
+ /* The scroller's bottom inset comes out of the pre's own block padding: the
2267
+ text still sits one full inset above the frame's bottom edge, the thumb sits
2268
+ in the gap, and a one-line command is no taller than it was before the
2269
+ scroller existed. */
2270
+ .prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *):has(> code)) {
2271
+ padding-bottom: calc(${CODE_PADDING_BLOCK_REM}rem - ${CODE_SCROLL_INSET_REM}rem);
2272
+ }
2273
+
2274
+ /* The dark border token is too close to the page background to read as a
2275
+ scrollbar thumb; derive a brighter one from the muted foreground instead. */
2276
+ :root[data-theme="dark"]
2277
+ .prose
2278
+ :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
2279
+ scrollbar-color: color-mix(in oklab, var(--blume-muted-foreground) 55%, transparent)
2280
+ transparent;
2281
+ }
2282
+
2283
+ /* Word wrap (markdown.code.wrap): long lines wrap instead of scrolling. The
2284
+ attribute is set on <body> from config; default code keeps \`white-space: pre\`. */
2285
+ [data-blume-code-wrap] pre,
2286
+ [data-blume-code-wrap] pre code {
2287
+ overflow-wrap: break-word;
2288
+ white-space: pre-wrap;
2289
+ }
2290
+
2291
+ .prose :where(table) {
2292
+ font-size: 0.8125rem;
2293
+ }
2294
+
2295
+ /* Scroll wide tables in place; the wrapper is added by the blume:table-wrap hast
2296
+ plugin. The border+radius frames the scroll viewport so a clipped wide table
2297
+ reads as scrollable rather than cut off, and carries the table's vertical
2298
+ rhythm (the inner table's own margin is zeroed so it fills the frame). */
2299
+ .blume-table-scroll {
2300
+ overflow-x: auto;
2301
+ -webkit-overflow-scrolling: touch;
2302
+ margin: 1.5rem 0;
2303
+ border: 1px solid var(--blume-border);
2304
+ border-radius: var(--blume-radius);
2305
+ }
2306
+ .blume-table-scroll > table {
2307
+ margin: 0;
2308
+ }
2309
+ /* Restore inner padding on every cell. Typography zeroes the first/last cell's
2310
+ inline padding so a borderless table aligns to the prose margin; inside the
2311
+ framed wrapper that leaves edge text touching the border. The :is() selector
2312
+ outweighs Typography's :where()-scoped rules so the outer columns get it too.
2313
+
2314
+ The block padding is 0.75rem against a table line-height near 1.7: at 0.5rem a
2315
+ cell whose content wrapped put MORE space between its own two lines than
2316
+ between itself and the next row, so a table of wrapping cells read as one
2317
+ block rather than as rows. The inline padding stays where it was, deliberately
2318
+ — widening it comes out of column width in a capped article, and on one corpus
2319
+ that pushed cells fitting on two lines onto three. */
2320
+ .blume-table-scroll :is(th, td) {
2321
+ padding: 0.75rem;
2322
+ }
2323
+ /* Keep column labels on one line so a two-word header does not wrap into a
2324
+ ragged stack; the table just scrolls a little wider instead. Body cells keep
2325
+ wrapping so a wide table stays a scroll away, not an endless horizontal run. */
2326
+ .blume-table-scroll th {
2327
+ white-space: nowrap;
2328
+ }
2329
+
2330
+ /* GFM renders cells as <td><code> directly, which the descendant form alone
2331
+ never matches (a cell is not its own descendant). */
2332
+ .prose :where(td, th) > code,
2333
+ .prose :where(td, th) :not(pre) > code {
2334
+ white-space: normal;
2335
+ }
2336
+
2337
+ blume-tabs pre,
2338
+ .not-prose > div > pre {
2339
+ background: var(--blume-code-background);
2340
+ }
2341
+
2342
+ /* The Component source pane fills its tab's fixed height (set inline to match the
2343
+ preview tab) and scrolls the inner code element, leaving the pre static so the
2344
+ copy button — absolute, pinned to the pre — doesn't drift while it scrolls. */
2345
+ pre.blume-source {
2346
+ display: flex;
2347
+ flex-direction: column;
2348
+ height: 100%;
2349
+ overflow: hidden;
2350
+ }
2351
+
2352
+ pre.blume-source > code {
2353
+ flex: 1;
2354
+ /* The pane's measured height is authoritative (it can exceed the prose
2355
+ 24rem cap), so undo the generic prose max-height: the code must fill
2356
+ whatever height the tab was given. */
2357
+ max-height: none;
2358
+ min-height: 0;
2359
+ overflow: auto;
2360
+ }
2361
+
2362
+ /* Code blocks inside tabs sit flush; the tab container owns the frame. These
2363
+ rules are unlayered like the base \`pre\` styles, so they win over Tailwind's
2364
+ layered utilities (which a class on the panel cannot). The second selector
2365
+ covers <CodeGroup>, where each code block IS the panel (a direct child of the
2366
+ tab content wrapper) rather than a <pre> nested inside a <Tab>. */
2367
+ blume-tabs [data-blume-tab-panel] > pre,
2368
+ blume-tabs [data-blume-tab-content] > pre {
2369
+ /* Important to beat the astro-code background rule above; the tab container
2370
+ owns the surface, so the code sits directly on it. */
2371
+ background: transparent !important;
2372
+ border: 0;
2373
+ border-radius: 0;
2374
+ margin: 0;
2375
+ }
2376
+
2377
+ /* These contexts drop the language bar (\`::before\` is cleared below), so the
2378
+ bar's padding goes with it and the block keeps the plain inset. Unscoped on
2379
+ purpose: <CodeBlock> wraps its pre in its own \`.prose\` below the component
2380
+ chrome, so without this a titled block on a page with no prose column
2381
+ (PageLayout) would keep the 3.75rem bar padding under a cleared bar. */
2382
+ blume-tabs pre[data-language],
2383
+ .not-prose pre[data-language] {
2384
+ padding-top: ${CODE_PADDING_BLOCK_REM}rem;
2385
+ }
2386
+
2387
+ /* Where no language bar holds the copy button — a flush block in tabs or a
2388
+ not-prose component, a bar-less block in prose — the docs layout still pins
2389
+ one over the block's top-right corner, so a first line long enough to reach
2390
+ it would end underneath. A code switcher (CodeGroup, ts2js) hosts the button
2391
+ in its tab strip instead and needs nothing here; for every block that keeps
2392
+ an overlay button, give the scrolling code element enough end padding that
2393
+ the line's tail clears the button at the end of its scroll. Keyed on the
2394
+ injected button itself (which the playground's client-created response pre
2395
+ never gets), and \`.prose\`-scoped to outrank the base \`:where(pre code)\`
2396
+ inset. Not a vertical inset — that reserved a strip above every one-line
2397
+ command and read as a rendering bug. */
2398
+ .prose :is(blume-tabs pre, .not-prose pre, pre:not([data-language])):not(.twoslash, .twoslash pre, blume-panel-tabs *):has(> [data-blume-copy]) > code {
2399
+ padding-inline-end: 3.5rem;
2400
+ }
2401
+
2402
+ blume-tabs pre[data-language]::before,
2403
+ .not-prose pre[data-language]::before {
2404
+ content: none;
2405
+ }
2406
+
2407
+ /* Pre-hydration layout-shift guard. The horizontal trigger row and
2408
+ single-panel visibility are built by the <blume-tabs> custom element's JS:
2409
+ until it upgrades, the trigger row is empty and every panel is in normal flow,
2410
+ so the browser paints all panels stacked vertically (e.g. all four
2411
+ package-manager commands at once) before JS hides them. While the element is
2412
+ :not(:defined), show only the first panel and reserve the trigger row's height
2413
+ so nothing jumps when JS takes over. The child selector covers both <Tab>
2414
+ panels and <CodeGroup> raw code blocks (which only gain
2415
+ [data-blume-tab-panel] from JS). */
2416
+ blume-tabs:not(:defined) [data-blume-tab-content] > *:nth-child(n + 2) {
2417
+ display: none;
2418
+ }
2419
+
2420
+ blume-tabs:not(:defined) [data-blume-tablist] {
2421
+ /* Matches a trigger's height: py-2.5 + text-xs line-height + border-b-2. */
2422
+ min-height: 2.375rem;
2423
+ }
2424
+
2425
+ blume-tabs:not(:defined)[data-dropdown="true"] [data-blume-tablist] {
2426
+ /* Dropdown variant: p-3 padding around a select (py-2 + text-sm + border). */
2427
+ min-height: 3.875rem;
2428
+ }
2429
+
2430
+ /* Code blocks inside request/response example panels sit flush; the panel owns
2431
+ the frame. Unlayered (like the tab rules) so they beat the base \`pre\` styles
2432
+ that a class on the panel cannot. */
2433
+ [data-blume-code-panel] pre {
2434
+ background: transparent !important;
2435
+ border: 0;
2436
+ border-radius: 0;
2437
+ margin: 0;
2438
+ }
2439
+
2440
+ /* Opt-in line numbers (\`\`\`ts file.ts lineNumbers): a counter-driven gutter that
2441
+ works in prose and flush (tab / API panel) code blocks alike. */
2442
+ pre[data-line-numbers] code {
2443
+ counter-reset: line;
2444
+ }
2445
+
2446
+ pre[data-line-numbers] .line::before {
2447
+ color: var(--blume-muted-foreground);
2448
+ content: counter(line);
2449
+ counter-increment: line;
2450
+ display: inline-block;
2451
+ margin-right: 1.25rem;
2452
+ text-align: right;
2453
+ user-select: none;
2454
+ width: 1.25rem;
2455
+ }
2456
+
2457
+ /* Shiki notation transformers (on by default). The notation comments are
2458
+ stripped from the output; these style the classes they leave behind. A styled
2459
+ line becomes a full-width inline-block so its background fills the row; plain
2460
+ lines are left untouched, so blocks without notations render as before. */
2461
+ .line.highlighted,
2462
+ .line.diff {
2463
+ display: inline-block;
2464
+ width: 100%;
2465
+ }
2466
+
2467
+ /* The backgrounds need !important to beat the \`pre.astro-code span\` rule above,
2468
+ which forces token spans transparent; these line/word spans are exceptions. */
2469
+ .line.highlighted {
2470
+ background-color: var(--blume-code-highlight) !important;
2471
+ box-shadow: inset 2px 0 0 0 var(--blume-code-highlight-border);
2472
+ }
2473
+
2474
+ .line.diff.add {
2475
+ background-color: var(--blume-code-add) !important;
2476
+ box-shadow: inset 2px 0 0 0 var(--blume-code-add-border);
2477
+ }
2478
+
2479
+ .line.diff.remove {
2480
+ background-color: var(--blume-code-remove) !important;
2481
+ box-shadow: inset 2px 0 0 0 var(--blume-code-remove-border);
2482
+ }
2483
+
2484
+ /* Word highlight (\`// [!code word:x]\`) wraps matches in an inline span. The
2485
+ element selector keeps it ahead of the transparent token-span rule. */
2486
+ span.highlighted-word {
2487
+ background-color: var(--blume-code-word) !important;
2488
+ border-radius: 0.25rem;
2489
+ box-shadow: 0 0 0 1px var(--blume-code-word-border);
2490
+ padding: 0.1em 0.2em;
2491
+ }
2492
+
2493
+ /* Focus (\`// [!code focus]\`): dim and blur the rest; reveal on hover. */
2494
+ pre:has(.line.focused) .line:not(.focused) {
2495
+ filter: blur(0.085rem);
2496
+ opacity: 0.5;
2497
+ transition:
2498
+ filter 0.2s ease,
2499
+ opacity 0.2s ease;
2500
+ }
2501
+
2502
+ pre:has(.line.focused):hover .line:not(.focused) {
2503
+ filter: none;
2504
+ opacity: 1;
2505
+ }
2506
+
2507
+ @media (prefers-reduced-motion: reduce) {
2508
+ pre:has(.line.focused) .line:not(.focused) {
2509
+ transition: none;
2510
+ }
2511
+ }
2512
+
2513
+ @media (max-width: 640px) {
2514
+ .prose :where(h1) {
2515
+ font-size: 2.25rem;
2516
+ }
2517
+
2518
+ .prose :where(h2) {
2519
+ font-size: 1.625rem;
2520
+ }
2521
+ }
2522
+
2523
+ .prose :not(pre) > code:not(:where([class~="not-prose"] *)) {
2524
+ background: var(--blume-code-background);
2525
+ padding: 0.15em 0.35em;
2526
+ border-radius: 0.3rem;
2527
+ font-family: var(--font-mono);
2528
+ font-weight: 500;
2529
+ }
2530
+
2531
+ .prose :not(pre) > code::before,
2532
+ .prose :not(pre) > code::after {
2533
+ content: none;
2534
+ }
2535
+
2536
+ /* Inline code highlighting: Shiki colors the tokens of a \`code\`{:lang} snippet
2537
+ via the same dual-theme CSS variables as fenced blocks, keeping the inline
2538
+ pill background. Always on — it only fires on the trailing {:lang} marker. */
2539
+ .prose code.blume-inline-code span {
2540
+ color: var(--shiki-light);
2541
+ }
2542
+
2543
+ :root[data-theme="dark"] .prose code.blume-inline-code span {
2544
+ color: var(--shiki-dark);
2545
+ }
2546
+
2547
+ /* Print / "Save as PDF" (the page-actions Export → PDF runs window.print()).
2548
+ Strip the surrounding chrome so the printout is just the article. */
2549
+ @media print {
2550
+ [data-blume-banner],
2551
+ header,
2552
+ aside,
2553
+ [data-blume-page-actions],
2554
+ #blume-content > nav,
2555
+ #blume-content > details {
2556
+ display: none !important;
2557
+ }
2558
+
2559
+ /* Paper can't scroll: uncap the code scroller so long blocks print in
2560
+ full, same as tab panels are force-expanded below. */
2561
+ .prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
2562
+ max-height: none;
2563
+ overflow: visible;
2564
+ }
2565
+
2566
+ #blume-content {
2567
+ padding: 0 !important;
2568
+ }
2569
+
2570
+ .prose {
2571
+ margin: 0 !important;
2572
+ max-width: none !important;
2573
+ }
2574
+
2575
+ /* Reveal collapsed tab panels so their content isn't dropped from the print. */
2576
+ blume-tabs [data-blume-tab-panel] {
2577
+ display: block !important;
2578
+ }
2579
+
2580
+ /* Keep code/callout backgrounds (subject to the dialog's "Background graphics"
2581
+ toggle). */
2582
+ pre,
2583
+ .prose :not(pre) > code {
2584
+ -webkit-print-color-adjust: exact;
2585
+ print-color-adjust: exact;
2586
+ }
2587
+ }
2588
+
2589
+ /* Twoslash rich-renderer styles (used by fences with the \`twoslash\` meta). */
2590
+ ${options.twoslashCss ?? ""}
2591
+
2592
+ /* Token overrides: config first, then the user's theme.css (highest priority). */
2593
+ ${options.configTokens}
2594
+ ${options.userTheme}
2595
+ `;
2596
+ var examplesEntryTemplate = (options) => `/* Generated by Blume. Do not edit. */
2597
+ @import "tailwindcss";
2598
+
2599
+ /* Scan the project (and an out-of-root examples directory) for utility classes. */
2600
+ ${options.sources.map((source) => `@source "${source}";`).join(`
2601
+ `)}
2602
+
2603
+ ${DARK_VARIANT}
2604
+
2605
+ ${COLOR_SCHEME_DEFAULTS}
2606
+
2607
+ ${TOKEN_DEFAULTS}
2608
+
2609
+ ${THEME_MAPPING}
2610
+
2611
+ /* Frame defaults: readable text in both modes, transparent so the docs pane's
2612
+ surface shows through. Base layer, so any user/example CSS wins. */
2613
+ @layer base {
2614
+ body {
2615
+ background: transparent;
2616
+ color: var(--blume-foreground);
2617
+ }
2618
+ }
2619
+
2620
+ /* Token overrides: config first, then the configured examples css (highest
2621
+ priority) — the place for shadcn variables and other component tokens. */
2622
+ ${options.configTokens}
2623
+ ${options.userCss}
2624
+ `;
2625
+
2626
+ // src/theme/sources.ts
2627
+ import { dirname as dirname5, isAbsolute as isAbsolute4, relative as relative2, resolve as resolve5 } from "pathe";
2628
+ var SOURCE_DIRECTIVE = /@source(?<not>\s+not)?\s+(?<quote>["'])(?<path>[^"']+)\k<quote>/gu;
2629
+ var rebaseSourceDirectives = (css, options) => {
2630
+ const base = dirname5(options.from);
2631
+ return css.replace(SOURCE_DIRECTIVE, (directive, not, quote, path) => {
2632
+ if (isAbsolute4(path)) {
2633
+ return directive;
2634
+ }
2635
+ const rebased = relative2(options.to, resolve5(base, path));
2636
+ return `@source${not ?? ""} ${quote}${rebased}${quote}`;
2637
+ });
2638
+ };
2639
+
2640
+ // src/theme/twoslash.ts
2641
+ import { readFileSync as readFileSync2 } from "node:fs";
2642
+ import { createRequire } from "node:module";
2643
+ var require2 = createRequire(import.meta.url);
2644
+ var OVERRIDES = `
2645
+ /* Twoslash: theme the rich renderer with Blume tokens. */
2646
+ :root {
2647
+ --twoslash-border-color: var(--blume-border);
2648
+ --twoslash-popup-bg: var(--blume-code-background);
2649
+ --twoslash-popup-color: var(--blume-foreground);
2650
+ --twoslash-popup-shadow: 0 6px 24px oklch(0 0 0 / 0.18);
2651
+ --twoslash-docs-color: var(--blume-muted-foreground);
2652
+ --twoslash-docs-font: var(--font-sans);
2653
+ --twoslash-code-font: var(--font-mono);
2654
+ --twoslash-matched-color: var(--blume-foreground);
2655
+ --twoslash-unmatched-color: var(--blume-muted-foreground);
2656
+ --twoslash-cursor-color: var(--blume-muted-foreground);
2657
+ }
2658
+
2659
+ /* Popups are absolutely positioned and must escape the pre's scroll container.
2660
+ The base prose pre rule (from the typography layer) wins the cascade here
2661
+ despite lower specificity, so !important is needed to force visibility.
2662
+ Regular code blocks scroll their inner code element and carry the horizontal
2663
+ padding there; twoslash code opts out of that scroller (popups again), so the
2664
+ padding is restored on the pre. */
2665
+ .prose pre.twoslash {
2666
+ overflow: visible !important;
2667
+ padding-left: 1.25rem;
2668
+ padding-right: 1.25rem;
2669
+ }
2670
+
2671
+ /* Twoslash blocks keep \`overflow: visible\` so popups can escape, which means
2672
+ long lines can't scroll — they'd push past the viewport on narrow screens.
2673
+ Wrap them on mobile instead; hover popups still position against the token. */
2674
+ @media (max-width: 640px) {
2675
+ .prose pre.twoslash code {
2676
+ white-space: pre-wrap;
2677
+ overflow-wrap: anywhere;
2678
+ }
2679
+ }
2680
+
2681
+ /* The rich renderer renders each popup's type signature as a nested Shiki
2682
+ pre. Strip the code-block chrome (border, radius, padding, background) so it
2683
+ sits flush inside the popup, which owns the frame. */
2684
+ .prose pre.twoslash .twoslash-popup-container pre {
2685
+ background: transparent !important;
2686
+ border: 0 !important;
2687
+ border-radius: 0 !important;
2688
+ margin: 0 !important;
2689
+ padding: 0 !important;
2690
+ }
2691
+
2692
+ /* Re-assert chrome backgrounds the transparent token-span rule would clear. */
2693
+ .twoslash .twoslash-popup-container,
2694
+ .twoslash .twoslash-popup-arrow,
2695
+ .twoslash .twoslash-completion-cursor .twoslash-completion-list {
2696
+ background: var(--twoslash-popup-bg) !important;
2697
+ }
2698
+ .twoslash .twoslash-popup-error {
2699
+ background: var(--twoslash-error-bg) !important;
2700
+ }
2701
+ .twoslash .twoslash-highlighted {
2702
+ background: var(--twoslash-highlighted-bg) !important;
2703
+ }
2704
+
2705
+ /* The error squiggle uses a background image the transparent rule clears, so
2706
+ draw it as a wavy underline instead. */
2707
+ .twoslash .twoslash-error {
2708
+ text-decoration: underline wavy var(--twoslash-error-color);
2709
+ text-underline-offset: 0.25em;
2710
+ }
2711
+
2712
+ .twoslash-popup-container {
2713
+ max-width: min(90vw, 36rem);
2714
+ }
2715
+ `;
2716
+ var twoslashCss = () => {
2717
+ const file = require2.resolve("@shikijs/twoslash/style-rich.css");
2718
+ return `${readFileSync2(file, "utf-8")}
2719
+ ${OVERRIDES}`;
2720
+ };
2721
+
2722
+ // src/astro/component-slots.ts
2723
+ var EMPTY_MODULE = `// Generated by Blume. Do not edit.
2724
+ import type { ComponentOverride } from "blume/core/define-components.ts";
2725
+ export const mdxComponents: Record<string, ComponentOverride> = {};
2726
+ export const layoutOverrides: Record<string, ComponentOverride> = {};
2727
+ `;
2728
+ var attributeValue = (value) => value.replaceAll(/["\n\r]/gu, " ").trim();
2729
+ var CLIENT_LOAD = "client:load";
2730
+ var directiveFor = (override) => {
2731
+ const framework = override.source?.framework;
2732
+ switch (override.client) {
2733
+ case "idle": {
2734
+ return "client:idle";
2735
+ }
2736
+ case "visible": {
2737
+ return "client:visible";
2738
+ }
2739
+ case "media": {
2740
+ return override.media ? `client:media="${attributeValue(override.media)}"` : CLIENT_LOAD;
2741
+ }
2742
+ case "only": {
2743
+ return framework ? `client:only="${attributeValue(framework)}"` : CLIENT_LOAD;
2744
+ }
2745
+ default: {
2746
+ return CLIENT_LOAD;
2747
+ }
2748
+ }
2749
+ };
2750
+ var importClause = (variable, name, path) => name === "default" ? `import ${variable} from ${JSON.stringify(path)};` : `import { ${name} as ${variable} } from ${JSON.stringify(path)};`;
2751
+ var wrapperContent = (override) => {
2752
+ const { name, path } = override.source;
2753
+ const clause = name === "default" ? `import Component from ${JSON.stringify(path)};` : `import { ${name} as Component } from ${JSON.stringify(path)};`;
2754
+ return `---
2755
+ // Generated by Blume. Do not edit.
2756
+ ${clause}
2757
+ ---
2758
+ <Component ${directiveFor(override)} {...Astro.props}><slot /></Component>
2759
+ `;
2760
+ };
2761
+ var sanitize = (value) => value.replaceAll(/[^A-Za-z0-9]/gu, (char) => `_${(char.codePointAt(0) ?? 0).toString(16)}_`);
2762
+ var planComponentSlots = (componentsFile, analysis) => {
2763
+ const frameworks = new Set;
2764
+ if (!componentsFile) {
2765
+ return { frameworks, module: EMPTY_MODULE, wrappers: [] };
2766
+ }
2767
+ if (!analysis) {
2768
+ return {
2769
+ frameworks,
2770
+ module: `// Generated by Blume. Do not edit.
2771
+ import overrides from ${JSON.stringify(componentsFile)};
2772
+ export const mdxComponents = overrides.mdx ?? {};
2773
+ export const layoutOverrides = overrides.layout ?? {};
2774
+ `,
2775
+ wrappers: []
2776
+ };
2777
+ }
2778
+ const wrappers = [];
2779
+ const importLines = [];
2780
+ const mdxEntries = [];
2781
+ const layoutEntries = [];
2782
+ let counter = 0;
2783
+ const mdxOverrides = [...analysis.mdx, ...analysis.islands];
2784
+ const plan = (override, group, entries) => {
2785
+ const { source } = override;
2786
+ if (source?.framework) {
2787
+ frameworks.add(source.framework);
2788
+ }
2789
+ if (override.identifier && !override.client) {
2790
+ return;
2791
+ }
2792
+ if (!source) {
2793
+ return;
2794
+ }
2795
+ const variable = `__blumeSlot${counter}`;
2796
+ counter += 1;
2797
+ if (override.client) {
2798
+ const name = `${group}-${sanitize(override.key)}`;
2799
+ wrappers.push({ content: wrapperContent(override), name });
2800
+ importLines.push(importClause(variable, "default", `./component-slots/${name}.astro`));
2801
+ } else {
2802
+ importLines.push(importClause(variable, source.name, source.path));
2803
+ }
2804
+ entries.push(`${JSON.stringify(override.key)}: ${variable}`);
2805
+ };
2806
+ for (const override of mdxOverrides) {
2807
+ plan(override, "mdx", mdxEntries);
2808
+ }
2809
+ for (const override of analysis.layout) {
2810
+ plan(override, "layout", layoutEntries);
2811
+ }
2812
+ const moduleSource = `// Generated by Blume. Do not edit.
2813
+ import overrides from ${JSON.stringify(componentsFile)};
2814
+ ${importLines.join(`
2815
+ `)}${importLines.length ? `
2816
+ ` : ""}export const mdxComponents = { ...(overrides.mdx ?? {})${mdxEntries.length ? `, ${mdxEntries.join(", ")}` : ""} };
2817
+ export const layoutOverrides = { ...(overrides.layout ?? {})${layoutEntries.length ? `, ${layoutEntries.join(", ")}` : ""} };
2818
+ `;
2819
+ return { frameworks, module: moduleSource, wrappers };
2820
+ };
2821
+
2822
+ // src/astro/examples.ts
2823
+ import { readFile as readFile4 } from "node:fs/promises";
2824
+ import pMap2 from "p-map";
2825
+ import { isAbsolute as isAbsolute5, join as join10, relative as relative3 } from "pathe";
2826
+ import { glob as glob2 } from "tinyglobby";
2827
+
2828
+ // src/astro/islands.ts
2829
+ import { readFile as readFile3 } from "node:fs/promises";
2830
+ import pMap from "p-map";
2831
+ import { basename, join as join9 } from "pathe";
2832
+ import { glob } from "tinyglobby";
2833
+ var DEFAULT_CLIENT = "visible";
2834
+ var READ_CONCURRENCY = 16;
2835
+ var VALID_MODES = new Set([
2836
+ "idle",
2837
+ "load",
2838
+ "only",
2839
+ "visible"
2840
+ ]);
2841
+ var isClientMode = (mode) => VALID_MODES.has(mode);
2842
+ var FRAMEWORK_BY_EXT2 = new Map([
2843
+ ["jsx", "react"],
2844
+ ["svelte", "svelte"],
2845
+ ["tsx", "react"],
2846
+ ["vue", "vue"]
2847
+ ]);
2848
+ var ISLAND_FILE = /\.(?<ext>jsx|svelte|tsx|vue)$/u;
2849
+ var CLIENT_EXPORT = /export\s+const\s+client\s*(?::[^=]+)?=\s*["'](?<mode>\w+)["']/u;
2850
+ var readClientMode = (source, file, warnings) => {
2851
+ const mode = source.match(CLIENT_EXPORT)?.groups?.mode;
2852
+ if (!mode) {
2853
+ return DEFAULT_CLIENT;
2854
+ }
2855
+ if (!isClientMode(mode)) {
2856
+ warnings.push(`Island "${file}" declares an unknown client mode "${mode}"; defaulting to "${DEFAULT_CLIENT}". Use "load", "idle", "visible", or "only".`);
2857
+ return DEFAULT_CLIENT;
2858
+ }
2859
+ return mode;
2860
+ };
2861
+ var discoverIslands = async (root) => {
2862
+ const dir = join9(root, "islands");
2863
+ const matches = await glob(["**/*"], {
2864
+ absolute: true,
2865
+ cwd: dir,
2866
+ onlyFiles: true
2867
+ });
2868
+ const files = matches.toSorted();
2869
+ const sources = await pMap(files, (file) => readFile3(file, "utf-8"), {
2870
+ concurrency: READ_CONCURRENCY
2871
+ });
2872
+ const islands = [];
2873
+ const warnings = [];
2874
+ const seen = new Map;
2875
+ const collectIsland = (file, source) => {
2876
+ const base = basename(file);
2877
+ const ext = base.match(ISLAND_FILE)?.groups?.ext;
2878
+ const framework = ext ? FRAMEWORK_BY_EXT2.get(ext) : undefined;
2879
+ if (!framework) {
2880
+ return;
2881
+ }
2882
+ const name = base.replace(ISLAND_FILE, "");
2883
+ if (!/^[A-Z][A-Za-z0-9_]*$/u.test(name)) {
2884
+ warnings.push(`Island "${file}" must have a PascalCase identifier filename to be used in MDX (letters, digits, and underscores only, e.g. Counter.tsx → <Counter />); skipping it.`);
2885
+ return;
2886
+ }
2887
+ const existing = seen.get(name);
2888
+ if (existing) {
2889
+ warnings.push(`Two islands both resolve to <${name}> ("${existing}" and "${file}"); ignoring the second. Give them distinct filenames.`);
2890
+ return;
2891
+ }
2892
+ seen.set(name, file);
2893
+ islands.push({
2894
+ client: readClientMode(source, file, warnings),
2895
+ file,
2896
+ framework,
2897
+ name
2898
+ });
2899
+ };
2900
+ for (const [index, file] of files.entries()) {
2901
+ collectIsland(file, sources[index] ?? "");
2902
+ }
2903
+ return { islands, warnings };
2904
+ };
2905
+
2906
+ // src/astro/examples.ts
2907
+ var FRAMEWORK_BY_EXT3 = {
2908
+ astro: "astro",
2909
+ jsx: "react",
2910
+ svelte: "svelte",
2911
+ tsx: "react",
2912
+ vue: "vue"
2913
+ };
2914
+ var EXAMPLE_FILE = /\.(?<ext>astro|jsx|svelte|tsx|vue)$/u;
2915
+ var DEFAULT_EXAMPLE_GLOB = "**/*.{astro,jsx,svelte,tsx,vue}";
2916
+ var READ_CONCURRENCY2 = 16;
2917
+ var EXAMPLE_SCAN_GLOB = "**/*.{astro,jsx,svelte,ts,tsx,vue}";
2918
+ var exampleScanRoots = (root, examplesDir) => {
2919
+ const path = relative3(root, examplesDir);
2920
+ const outside = path.startsWith("..") || isAbsolute5(path);
2921
+ return outside ? [root, examplesDir] : [root];
2922
+ };
2923
+ var GLOB_MAGIC = /[!*?[\]{}]/u;
2924
+ var splitGlobBase = (pattern) => {
2925
+ const segments = pattern.split("/");
2926
+ const firstMagic = segments.findIndex((segment) => GLOB_MAGIC.test(segment));
2927
+ return {
2928
+ base: segments.slice(0, firstMagic).join("/"),
2929
+ rest: segments.slice(firstMagic).join("/")
2930
+ };
2931
+ };
2932
+ var discoverExamples = async (root, pattern = "examples") => {
2933
+ const { base, rest } = GLOB_MAGIC.test(pattern) ? splitGlobBase(pattern) : { base: pattern, rest: DEFAULT_EXAMPLE_GLOB };
2934
+ const dir = join10(root, base);
2935
+ const matches = await glob2([rest], {
2936
+ absolute: true,
2937
+ cwd: dir,
2938
+ onlyFiles: true
2939
+ });
2940
+ const files = matches.toSorted();
2941
+ const sources = await pMap2(files, (file) => readFile4(file, "utf-8"), {
2942
+ concurrency: READ_CONCURRENCY2
2943
+ });
2944
+ const examples = [];
2945
+ const warnings = [];
2946
+ const seen = new Map;
2947
+ const collectExample = (file, source) => {
2948
+ const ext = file.match(EXAMPLE_FILE)?.groups?.ext;
2949
+ const framework = ext ? FRAMEWORK_BY_EXT3[ext] : undefined;
2950
+ if (!(ext && framework)) {
2951
+ return;
2952
+ }
2953
+ const path = relative3(dir, file).slice(0, -(ext.length + 1));
2954
+ const existing = seen.get(path);
2955
+ if (existing) {
2956
+ warnings.push(`Two examples both resolve to "${path}" ("${existing}" and "${file}"); ignoring the second. Give them distinct paths.`);
2957
+ return;
2958
+ }
2959
+ seen.set(path, file);
2960
+ examples.push({
2961
+ client: framework === "astro" ? undefined : readClientMode(source, file, warnings),
2962
+ file,
2963
+ framework,
2964
+ lang: ext,
2965
+ path,
2966
+ source
2967
+ });
2968
+ };
2969
+ for (const [index, file] of files.entries()) {
2970
+ collectExample(file, sources[index] ?? "");
2971
+ }
2972
+ return { dir, examples, warnings };
2973
+ };
2974
+ var exampleMarkdownLookup = (examples) => Object.fromEntries(examples.map((example) => [
2975
+ example.path,
2976
+ { lang: example.lang, source: example.source }
2977
+ ]));
2978
+
2979
+ // src/astro/generate.ts
2980
+ var BLUME_SRC = join11(packageRoot(), "src");
2981
+ var canResolveFrom = (fromDir, spec) => {
2982
+ try {
2983
+ createRequire2(pathToFileURL(join11(fromDir, "_.js")).href).resolve(spec);
2984
+ return true;
2985
+ } catch {
2986
+ return false;
2987
+ }
2988
+ };
2989
+ var resolveReactCompiler = (config, needsReact, pkgDir = packageRoot()) => {
2990
+ if (!(needsReact && config.react.compiler)) {
2991
+ return null;
2992
+ }
2993
+ try {
2994
+ return createRequire2(pathToFileURL(join11(pkgDir, "_.js")).href).resolve("babel-plugin-react-compiler");
2995
+ } catch {
2996
+ return null;
2997
+ }
2998
+ };
2999
+ var reactCompilerWarnings = (config, needsReact, compilerPath) => needsReact && config.react.compiler && !compilerPath ? [
3000
+ "React Compiler is enabled but `babel-plugin-react-compiler` could not be resolved; falling back to an uncompiled build. Reinstall Blume, or set `react: { compiler: false }` to silence this."
3001
+ ] : [];
3002
+ var resolveAstroPackageJson = (modulesDir) => {
3003
+ try {
3004
+ return realpathSync(join11(modulesDir, "astro", "package.json"));
3005
+ } catch {
3006
+ return null;
3007
+ }
3008
+ };
3009
+ var resolvedAstroHit = (fromDir) => {
3010
+ let dir = normalize(fromDir);
3011
+ while (true) {
3012
+ const modulesDir = join11(dir, "node_modules");
3013
+ const pkg = resolveAstroPackageJson(modulesDir);
3014
+ if (pkg) {
3015
+ return { modulesDir, pkg };
3016
+ }
3017
+ const parent = dirname6(dir);
3018
+ if (parent === dir) {
3019
+ return null;
3020
+ }
3021
+ dir = parent;
3022
+ }
3023
+ };
3024
+ var sameRealDir = (a, b) => {
3025
+ try {
3026
+ return realpathSync(a) === realpathSync(b);
3027
+ } catch {
3028
+ return false;
3029
+ }
3030
+ };
3031
+ var depsCandidates = (pkgDir) => [
3032
+ join11(pkgDir, "node_modules"),
3033
+ dirname6(pkgDir)
3034
+ ];
3035
+ var candidateHolding = (pkgDir, ...segments) => depsCandidates(pkgDir).find((dir) => existsSync6(join11(dir, ...segments))) ?? null;
3036
+ var symlinkDir = async (target, link) => {
3037
+ try {
3038
+ await symlink(target, link, "dir");
3039
+ } catch {
3040
+ await symlink(target, link, "junction");
3041
+ }
3042
+ };
3043
+ var linkDepsJunction = async (link, depsDir) => {
3044
+ let existing;
3045
+ try {
3046
+ existing = await lstat(link);
3047
+ } catch {
3048
+ existing = null;
3049
+ }
3050
+ if (existing) {
3051
+ if (!existing.isSymbolicLink()) {
3052
+ return;
3053
+ }
3054
+ try {
3055
+ if (resolve6(dirname6(link), await readlink(link)) === resolve6(depsDir)) {
3056
+ return;
3057
+ }
3058
+ } catch {}
3059
+ await rm2(link, { force: true });
3060
+ }
3061
+ await mkdir2(dirname6(link), { recursive: true });
3062
+ await symlinkDir(depsDir, link);
3063
+ };
3064
+ var readPkgVersion = (pkgJsonPath) => {
3065
+ if (!pkgJsonPath) {
3066
+ return null;
3067
+ }
3068
+ try {
3069
+ return JSON.parse(readFileSync3(pkgJsonPath, "utf-8")).version ?? null;
3070
+ } catch {
3071
+ return null;
3072
+ }
3073
+ };
3074
+ var astroConflictWarning = (blumeAstroPkg, shadowAstroPkg) => {
3075
+ const blume = readPkgVersion(blumeAstroPkg);
3076
+ const shadow = readPkgVersion(shadowAstroPkg);
3077
+ const versions = blume && shadow ? `astro@${shadow} shadowing Blume's astro@${blume}` : "a second copy of Astro shadowing Blume's";
3078
+ const pin = blume ?? "<Blume's astro version>";
3079
+ return `Astro version conflict: another dependency hoisted ${versions} to the project root, so @astrojs/mdx binds to the wrong copy and the build fails on a missing export (e.g. "chunkToString"). A single symlink can't reconcile a split install — pin Blume's Astro by adding a package.json "overrides" (npm/bun/pnpm) or "resolutions" (yarn) entry { "astro": "${pin}" }, then reinstall. Run \`npm ls astro\` to find the dependency pulling the older copy.`;
3080
+ };
3081
+ var dropStaleDepsLink = async (link, pkgDir) => {
3082
+ let existing;
3083
+ try {
3084
+ existing = await lstat(link);
3085
+ } catch {
3086
+ return;
3087
+ }
3088
+ if (!existing.isSymbolicLink()) {
3089
+ return;
3090
+ }
3091
+ let linkedBlume;
3092
+ let runningBlume;
3093
+ try {
3094
+ linkedBlume = await realpath(join11(link, "blume"));
3095
+ runningBlume = await realpath(pkgDir);
3096
+ } catch {
3097
+ return;
3098
+ }
3099
+ if (linkedBlume !== runningBlume) {
3100
+ await rm2(link, { force: true });
3101
+ }
3102
+ };
3103
+ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
3104
+ const astroDir = candidateHolding(pkgDir, "astro");
3105
+ if (!astroDir) {
3106
+ return null;
3107
+ }
3108
+ const mdxDir = candidateHolding(pkgDir, "@astrojs", "mdx");
3109
+ const blumeAstro = resolveAstroPackageJson(astroDir);
3110
+ await dropStaleDepsLink(join11(outDir, "node_modules"), pkgDir);
3111
+ const outDirHit = resolvedAstroHit(outDir);
3112
+ const astroCorrect = blumeAstro !== null && outDirHit?.pkg === blumeAstro;
3113
+ const walkLandsInDeps = astroCorrect && outDirHit !== null && sameRealDir(outDirHit.modulesDir, astroDir);
3114
+ if (walkLandsInDeps && mdxDir === astroDir) {
3115
+ return null;
3116
+ }
3117
+ if (mdxDir && (mdxDir === astroDir || astroCorrect)) {
3118
+ await linkDepsJunction(join11(outDir, "node_modules"), mdxDir);
3119
+ return null;
3120
+ }
3121
+ return astroConflictWarning(blumeAstro, outDirHit?.pkg ?? null);
3122
+ };
3123
+ var ISLAND_FRAMEWORK_DEPS = new Map([
3124
+ ["svelte", "@astrojs/svelte"],
3125
+ ["vue", "@astrojs/vue"]
3126
+ ]);
3127
+ var DEPLOYMENT_ADAPTER_DEPS = new Map([
3128
+ ["cloudflare", "@astrojs/cloudflare"],
3129
+ ["netlify", "@astrojs/netlify"]
3130
+ ]);
3131
+ var islandFrameworkWarnings = (frameworks, root) => {
3132
+ const warnings = [];
3133
+ for (const framework of frameworks) {
3134
+ const dep = ISLAND_FRAMEWORK_DEPS.get(framework);
3135
+ if (dep && !canResolveFrom(root, dep)) {
3136
+ warnings.push(`Islands use ${framework}, which needs "${dep}". Install it (e.g. \`npm install ${dep} ${framework}\`).`);
3137
+ }
3138
+ }
3139
+ return warnings;
3140
+ };
3141
+ var deploymentAdapterWarnings = (deployment, root, pkgDir = packageRoot()) => {
3142
+ const dep = deployment.output === "server" && deployment.adapter ? DEPLOYMENT_ADAPTER_DEPS.get(deployment.adapter) : undefined;
3143
+ if (dep && !(canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep))) {
3144
+ return [
3145
+ `Deployment adapter "${deployment.adapter}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
3146
+ ];
3147
+ }
3148
+ return [];
3149
+ };
3150
+ var searchProviderWarnings = (provider, root, pkgDir = packageRoot()) => {
3151
+ const warnings = [];
3152
+ for (const dep of searchProviderMeta(provider).runtimeDeps) {
3153
+ if (!(canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep))) {
3154
+ warnings.push(`Search provider "${provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
3155
+ }
3156
+ }
3157
+ return warnings;
3158
+ };
3159
+ var askProviderWarnings = (ask, root, pkgDir = packageRoot()) => {
3160
+ if (!ask?.enabled || ask.endpoint) {
3161
+ return [];
3162
+ }
3163
+ const dep = askBackendRuntimeDep(ask);
3164
+ if (!dep || canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep)) {
3165
+ return [];
3166
+ }
3167
+ return [
3168
+ `Ask AI provider "${ask.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
3169
+ ];
3170
+ };
3171
+ var examplesCssFile = (root, config) => config.examples.css ? join11(root, config.examples.css) : null;
3172
+ var writeExamplesPreview = async (options) => {
3173
+ const { config, hasExamples, root, srcDir, write } = options;
3174
+ if (hasExamples) {
3175
+ await write(join11(srcDir, "pages", ...config.basePath.split("/").filter(Boolean), "blume-examples", "[...path].astro"), examplesPageTemplate());
3176
+ }
3177
+ const cssFile = examplesCssFile(root, config);
3178
+ return cssFile && !existsSync6(cssFile) ? [
3179
+ `examples.css points at "${config.examples.css}", which doesn't exist; previews render without it.`
3180
+ ] : [];
3181
+ };
3182
+ var readOptional = async (path) => {
3183
+ if (!path) {
3184
+ return "";
3185
+ }
3186
+ try {
3187
+ return await readFile5(path, "utf-8");
3188
+ } catch {
3189
+ return "";
3190
+ }
3191
+ };
3192
+ var readUserCss = async (file, outputDir) => {
3193
+ const css = await readOptional(file);
3194
+ return file ? rebaseSourceDirectives(css, { from: file, to: outputDir }) : css;
3195
+ };
3196
+ var detectNeedsReact = async (root) => {
3197
+ const matches = await glob3(["**/*.{tsx,jsx}"], {
3198
+ cwd: root,
3199
+ ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
3200
+ onlyFiles: true
3201
+ });
3202
+ return matches.length > 0;
3203
+ };
3204
+ var containsMath = (content) => content.includes("$$") || content.includes("<Math");
3205
+ var READ_CONCURRENCY3 = 16;
3206
+ var detectUsesMath = async (root, staged = []) => {
3207
+ const files = await glob3(["**/*.{md,mdx}"], {
3208
+ cwd: root,
3209
+ ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
3210
+ onlyFiles: true
3211
+ });
3212
+ const contents = await pMap3(files, (file) => readOptional(join11(root, file)), {
3213
+ concurrency: READ_CONCURRENCY3
3214
+ });
3215
+ return [...contents, ...staged].some(containsMath);
3216
+ };
3217
+ var hashConfigSource = (source) => createHash("sha256").update(source).digest("hex");
3218
+ var loadIntegrationBridge = async (config, context) => {
3219
+ if (config.integrations.length === 0 || !context.configFile) {
3220
+ return;
3221
+ }
3222
+ return {
3223
+ configFile: relative4(context.outDir, context.configFile),
3224
+ sourceHash: hashConfigSource(await readOptional(context.configFile))
3225
+ };
3226
+ };
3227
+ var writeIfChanged = async (path, content) => {
3228
+ let existing = null;
3229
+ try {
3230
+ existing = await readFile5(path, "utf-8");
3231
+ } catch {
3232
+ existing = null;
3233
+ }
3234
+ if (existing === content) {
3235
+ return false;
3236
+ }
3237
+ await writeTextAtomic(path, content);
3238
+ return true;
3239
+ };
3240
+ var pruneOrphans = async (srcDir, written) => {
3241
+ const existing = await glob3("**/*", {
3242
+ absolute: true,
3243
+ cwd: srcDir,
3244
+ onlyFiles: true
3245
+ });
3246
+ const removals = [];
3247
+ for (const path of existing) {
3248
+ const normalized = normalize(path);
3249
+ if (!written.has(normalized)) {
3250
+ removals.push(rm2(normalized, { force: true }));
3251
+ }
3252
+ }
3253
+ await Promise.all(removals);
3254
+ };
3255
+ var collectStaged = (project) => {
3256
+ const staged = new Map;
3257
+ for (const page of project.graph.pages) {
3258
+ if (page.collection === "staged" && page.entryId && page.body) {
3259
+ const text = page.sourcePath ? rewriteRelativeImages({
3260
+ deployBase: project.config.deployment.base,
3261
+ projectRoot: project.context.root,
3262
+ source: page.body.text,
3263
+ sourcePath: page.sourcePath
3264
+ }) : page.body.text;
3265
+ staged.set(page.entryId, text);
3266
+ }
3267
+ }
3268
+ return staged;
3269
+ };
3270
+ var writeStagedContent = async (out, staged) => {
3271
+ const contentDir = stagedContentDir(out);
3272
+ const written = new Set;
3273
+ await Promise.all([...staged].map(async ([entryId, text]) => {
3274
+ const path = join11(contentDir, entryId);
3275
+ written.add(normalize(path));
3276
+ await writeIfChanged(path, text);
3277
+ }));
3278
+ if (existsSync6(contentDir)) {
3279
+ await pruneOrphans(contentDir, written);
3280
+ }
3281
+ };
3282
+ var logoDimensions = (svg) => svg ? svgDimensions(svg) ?? undefined : undefined;
3283
+ var readLogoSvg = (project, source) => {
3284
+ if (!source?.toLowerCase().endsWith(".svg")) {
3285
+ return;
3286
+ }
3287
+ const rel = source.replace(/^\//u, "");
3288
+ const file = [
3289
+ join11(project.context.root, "public", rel),
3290
+ join11(project.context.root, rel)
3291
+ ].find((path) => existsSync6(path));
3292
+ return file ? readFileSync3(file, "utf-8") : undefined;
3293
+ };
3294
+ var isStringShorthand = (value) => typeof value === "string";
3295
+ var headerRepoUrl = (repo, derived) => {
3296
+ if (isStringShorthand(repo)) {
3297
+ return repo;
3298
+ }
3299
+ return repo ? derived : null;
3300
+ };
3301
+ var resolveLogo = (project) => {
3302
+ const { logo } = project.config;
3303
+ if (!logo) {
3304
+ return null;
3305
+ }
3306
+ const config = isStringShorthand(logo) ? { image: logo } : logo;
3307
+ const { href, image: source, text } = config;
3308
+ const image = isStringShorthand(source) ? { light: source } : source;
3309
+ const light = image?.light ?? image?.dark;
3310
+ const dark = image?.dark ?? image?.light;
3311
+ const alt = image?.alt ?? "";
3312
+ const brandHref = href ?? "/";
3313
+ const lightSvg = readLogoSvg(project, light);
3314
+ const darkSvg = dark === light ? lightSvg : readLogoSvg(project, dark);
3315
+ if (light && light === dark && lightSvg) {
3316
+ return { alt, href: brandHref, svg: lightSvg, text };
3317
+ }
3318
+ const lightDimensions = logoDimensions(lightSvg);
3319
+ const darkDimensions = logoDimensions(darkSvg);
3320
+ const dimensions = lightDimensions || darkDimensions ? { dark: darkDimensions, light: lightDimensions } : undefined;
3321
+ return { alt, dark, dimensions, href: brandHref, light, text };
3322
+ };
3323
+ var FAVICON_CANDIDATES = [
3324
+ "icon.svg",
3325
+ "favicon.svg",
3326
+ "icon.png",
3327
+ "favicon.png",
3328
+ "favicon.ico",
3329
+ "icon.ico"
3330
+ ];
3331
+ var darkSibling = (name) => name.replace(/\.(?=[^.]+$)/u, "-dark.");
3332
+ var FAVICON_TYPES = new Map([
3333
+ ["ico", "image/x-icon"],
3334
+ ["jpeg", "image/jpeg"],
3335
+ ["jpg", "image/jpeg"],
3336
+ ["png", "image/png"],
3337
+ ["svg", "image/svg+xml"]
3338
+ ]);
3339
+ var faviconType = (name) => {
3340
+ const ext = name.split(".").pop()?.toLowerCase();
3341
+ return ext ? FAVICON_TYPES.get(ext) : undefined;
3342
+ };
3343
+ var inlineDataUri = (file, type) => `data:${type};base64,${readFileSync3(file).toString("base64")}`;
3344
+ var defaultFavicon = () => ({
3345
+ dark: {
3346
+ href: inlineDataUri(join11(BLUME_SRC, "assets", "icon-dark.png"), "image/png"),
3347
+ type: "image/png"
3348
+ },
3349
+ href: inlineDataUri(join11(BLUME_SRC, "assets", "icon.png"), "image/png"),
3350
+ type: "image/png"
3351
+ });
3352
+ var APPLE_ICON_CANDIDATES = [
3353
+ "apple-icon.png",
3354
+ "apple-icon.jpg",
3355
+ "apple-icon.jpeg",
3356
+ "apple-touch-icon.png"
3357
+ ];
3358
+ var resolveIconFile = (project, candidates) => {
3359
+ const { root } = project.context;
3360
+ for (const name of candidates) {
3361
+ if (existsSync6(join11(root, "public", name))) {
3362
+ return { href: `/${name}`, type: faviconType(name) };
3363
+ }
3364
+ }
3365
+ for (const name of candidates) {
3366
+ const file = join11(root, name);
3367
+ if (existsSync6(file)) {
3368
+ const type = faviconType(name);
3369
+ return { href: inlineDataUri(file, type ?? "image/x-icon"), type };
3370
+ }
3371
+ }
3372
+ return null;
3373
+ };
3374
+ var resolveFavicon = (project) => {
3375
+ const { root } = project.context;
3376
+ for (const name of FAVICON_CANDIDATES) {
3377
+ if (existsSync6(join11(root, "public", name))) {
3378
+ const type = faviconType(name);
3379
+ const sibling = darkSibling(name);
3380
+ return existsSync6(join11(root, "public", sibling)) ? { dark: { href: `/${sibling}`, type }, href: `/${name}`, type } : { href: `/${name}`, type };
3381
+ }
3382
+ }
3383
+ for (const name of FAVICON_CANDIDATES) {
3384
+ const file = join11(root, name);
3385
+ if (existsSync6(file)) {
3386
+ const type = faviconType(name);
3387
+ const mime = type ?? "image/x-icon";
3388
+ const siblingFile = join11(root, darkSibling(name));
3389
+ return existsSync6(siblingFile) ? {
3390
+ dark: { href: inlineDataUri(siblingFile, mime), type },
3391
+ href: inlineDataUri(file, mime),
3392
+ type
3393
+ } : { href: inlineDataUri(file, mime), type };
3394
+ }
3395
+ }
3396
+ return resolveIconFile(project, FAVICON_CANDIDATES.map(darkSibling)) ?? defaultFavicon();
3397
+ };
3398
+ var resolveAppleIcon = (project) => resolveIconFile(project, APPLE_ICON_CANDIDATES);
3399
+ var resolveBanner = (config) => {
3400
+ const { banner } = config;
3401
+ if (!banner) {
3402
+ return null;
3403
+ }
3404
+ if (isStringShorthand(banner)) {
3405
+ return { content: banner, dismissible: false, key: banner };
3406
+ }
3407
+ return {
3408
+ content: banner.content,
3409
+ dismissible: banner.dismissible,
3410
+ key: banner.id ?? banner.content,
3411
+ link: banner.link
3412
+ };
3413
+ };
3414
+ var resolveOgMark = (project, siteLogo) => {
3415
+ const configured = project.config.seo.og.logo;
3416
+ if (configured === false) {
3417
+ return false;
3418
+ }
3419
+ if (configured) {
3420
+ return resolveOgLogo(project, configured);
3421
+ }
3422
+ return siteLogo;
3423
+ };
3424
+ var resolveOgSite = (config) => {
3425
+ const configured = config.seo.og.site;
3426
+ if (configured === false) {
3427
+ return;
3428
+ }
3429
+ if (configured !== undefined) {
3430
+ return configured;
3431
+ }
3432
+ return config.deployment.site ? `${new URL(config.deployment.site).host}${normalizeBasePath(config.deployment.base)}` : undefined;
3433
+ };
3434
+ var resolveOgDescription = (config) => {
3435
+ const configured = config.seo.og.description;
3436
+ if (configured === false) {
3437
+ return;
3438
+ }
3439
+ return configured ?? config.description;
3440
+ };
3441
+ var resolveGithubData = (github) => github ? {
3442
+ api: apiUrl(github),
3443
+ host: github.host,
3444
+ owner: github.owner,
3445
+ repo: github.repo
3446
+ } : null;
3447
+ var buildRuntimeData = (project) => {
3448
+ const { config, context, graph, manifest } = project;
3449
+ const { github } = config;
3450
+ const repoUrl2 = github ? repoUrl(github) : null;
3451
+ const editBase = github ? editBaseUrl(github) : null;
3452
+ const logo = resolveLogo(project);
3453
+ const ogLogo = resolveOgMark(project, logo?.svg);
3454
+ const editUrlFor = (sourcePath) => {
3455
+ if (!(editBase && sourcePath)) {
3456
+ return null;
3457
+ }
3458
+ const rel = relative4(context.root, sourcePath).split("\\").join("/");
3459
+ const editDir = trimChar(github?.dir ?? "", "/");
3460
+ const editPath = normalize(editDir ? join11(editDir, rel) : rel);
3461
+ if (editPath.startsWith("..") || isAbsolute6(editPath)) {
3462
+ return null;
3463
+ }
3464
+ return `${editBase}/${editPath}`;
3465
+ };
3466
+ const { i18n } = config;
3467
+ const markUrl = headerRepoUrl(config.navigation.repo, repoUrl2);
3468
+ const withRepoUrl = (nav) => ({
3469
+ ...nav,
3470
+ repoUrl: markUrl
3471
+ });
3472
+ const uiByLocale = i18n ? Object.fromEntries(i18n.locales.map(({ code }) => [
3473
+ code,
3474
+ resolveUIStrings(code, {
3475
+ defaultLocale: i18n.defaultLocale,
3476
+ overrides: i18n.ui
3477
+ })
3478
+ ])) : {};
3479
+ const defaultUi = i18n ? resolveUIStrings(i18n.defaultLocale, {
3480
+ defaultLocale: i18n.defaultLocale,
3481
+ overrides: i18n.ui
3482
+ }) : EN_UI;
3483
+ const navigationByLocale = i18n ? Object.fromEntries(i18n.locales.map(({ code }) => [
3484
+ code,
3485
+ withRepoUrl(graph.navigationByLocale[code] ?? {
3486
+ actions: [],
3487
+ cta: null,
3488
+ featured: [],
3489
+ selectors: [],
3490
+ sidebar: [],
3491
+ tabs: []
3492
+ })
3493
+ ])) : {};
3494
+ const data = {
3495
+ config: {
3496
+ analytics: config.analytics ?? null,
3497
+ appleIcon: resolveAppleIcon(project),
3498
+ ask: config.ai.ask?.enabled ? {
3499
+ endpoint: config.ai.ask.endpoint ?? null,
3500
+ suggestions: config.ai.ask.suggestions
3501
+ } : null,
3502
+ banner: resolveBanner(config),
3503
+ basePath: config.basePath,
3504
+ codeThemes: config.markdown.codeBlocks.theme,
3505
+ codeWrap: config.markdown.code.wrap,
3506
+ dateFormat: config.dateFormat,
3507
+ description: config.description,
3508
+ discovery: {
3509
+ agentReadability: config.seo.agentReadability,
3510
+ api: config.ai.api,
3511
+ llmsTxt: config.ai.llmsTxt.enabled,
3512
+ sitemap: config.seo.sitemap && Boolean(config.deployment.site)
3513
+ },
3514
+ favicon: resolveFavicon(project),
3515
+ feedback: config.feedback,
3516
+ github: resolveGithubData(github),
3517
+ i18n: i18n ? {
3518
+ defaultLocale: i18n.defaultLocale,
3519
+ fallbackLocale: resolveFallbackLocale(i18n),
3520
+ hideDefaultLocalePrefix: i18n.hideDefaultLocalePrefix,
3521
+ locales: i18n.locales.map(({ code, dir, label }) => ({
3522
+ code,
3523
+ dir,
3524
+ label
3525
+ }))
3526
+ } : null,
3527
+ identity: config.seo.organization || config.seo.software ? {
3528
+ organization: config.seo.organization,
3529
+ software: config.seo.software
3530
+ } : null,
3531
+ imageZoom: config.markdown.imageZoom,
3532
+ logo,
3533
+ mcp: config.ai.mcp.enabled ? {
3534
+ name: config.ai.mcp.name ?? config.title,
3535
+ route: config.ai.mcp.route
3536
+ } : null,
3537
+ og: {
3538
+ description: resolveOgDescription(config),
3539
+ enabled: config.seo.og.enabled ?? false,
3540
+ logo: ogLogo,
3541
+ palette: config.seo.og.palette,
3542
+ site: resolveOgSite(config)
3543
+ },
3544
+ openInChat: config.ai.openInChat,
3545
+ repoUrl: repoUrl2,
3546
+ search: {
3547
+ enabled: config.search.provider !== "none",
3548
+ popular: resolveSearchPopular(config.search.popular, config.basePath),
3549
+ provider: config.search.provider
3550
+ },
3551
+ site: config.deployment.site ?? null,
3552
+ structuredData: config.seo.structuredData,
3553
+ theme: config.theme,
3554
+ title: config.title,
3555
+ toc: config.toc,
3556
+ versions: config.versions ?? null,
3557
+ webmcp: {
3558
+ enabled: config.ai.webmcp,
3559
+ llms: config.ai.llmsTxt.enabled
3560
+ },
3561
+ x: config.seo.x
3562
+ },
3563
+ feeds: buildRssFeeds(project).map((feed) => ({
3564
+ href: feed.path,
3565
+ title: feed.title
3566
+ })),
3567
+ fontCssVars: configuredFonts(config.theme.fonts, fontLocaleCodes(config.i18n)),
3568
+ navigation: withRepoUrl(graph.navigation),
3569
+ navigationByLocale,
3570
+ navigationByVersion: Object.fromEntries(Object.entries(graph.navigationByVersion).map(([id, byLocale]) => [
3571
+ id,
3572
+ Object.fromEntries(Object.entries(byLocale).map(([code, nav]) => [
3573
+ code,
3574
+ withRepoUrl(nav)
3575
+ ]))
3576
+ ])),
3577
+ routes: manifest.routes.map((route) => ({
3578
+ alternates: route.alternates,
3579
+ collection: route.collection,
3580
+ description: route.description ?? null,
3581
+ draft: route.draft,
3582
+ editUrl: route.editUrl ?? editUrlFor(route.sourcePath),
3583
+ entryId: route.entryId,
3584
+ fallback: route.fallback ?? false,
3585
+ hidden: route.hidden,
3586
+ id: route.id,
3587
+ indexable: route.indexable,
3588
+ lastModified: route.lastModified ?? null,
3589
+ locale: route.locale,
3590
+ path: route.path,
3591
+ title: route.title,
3592
+ version: route.version,
3593
+ versionAlternates: route.versionAlternates
3594
+ })),
3595
+ ui: defaultUi,
3596
+ uiByLocale
3597
+ };
3598
+ return `${JSON.stringify(data, null, 2)}
3599
+ `;
3600
+ };
3601
+ var planMcp = (project, srcDir, userPages) => {
3602
+ const { config } = project;
3603
+ const { route } = config.ai.mcp;
3604
+ const dir = join11(srcDir, "blume-mcp");
3605
+ const base = {
3606
+ dir,
3607
+ discoveryPages: [],
3608
+ enabled: false,
3609
+ route,
3610
+ srcDir,
3611
+ warnings: []
3612
+ };
3613
+ if (!config.ai.mcp.enabled) {
3614
+ return base;
3615
+ }
3616
+ if (routeIsTaken(userPages, project.graph.pages, route)) {
3617
+ return {
3618
+ ...base,
3619
+ warnings: [
3620
+ `MCP server route "${route}" is already used by a content or custom page; the MCP server was not generated. Set a different "ai.mcp.route" in blume.config.ts.`
3621
+ ]
3622
+ };
3623
+ }
3624
+ return {
3625
+ ...base,
3626
+ discoveryPages: [
3627
+ {
3628
+ entrypoint: join11(dir, "discovery.ts"),
3629
+ pattern: "/.well-known/mcp.json"
3630
+ },
3631
+ {
3632
+ entrypoint: join11(dir, "server-card.ts"),
3633
+ pattern: "/.well-known/mcp/server-card.json"
3634
+ }
3635
+ ],
3636
+ enabled: true
3637
+ };
3638
+ };
3639
+ var publishAgentData = async (project, plans, modules) => {
3640
+ if (!(plans.mcp.enabled || plans.api.enabled)) {
3641
+ return null;
3642
+ }
3643
+ const data = await buildMcpData(project);
3644
+ modules.set("blume:mcp-data", JSON.stringify(data));
3645
+ return data;
3646
+ };
3647
+ var writeMcpFiles = async (plan, write, data) => {
3648
+ if (!(plan.enabled && data)) {
3649
+ return;
3650
+ }
3651
+ const discoveryInput = {
3652
+ base: data.base,
3653
+ name: data.name,
3654
+ route: plan.route,
3655
+ site: data.site,
3656
+ version: data.version
3657
+ };
3658
+ await Promise.all([
3659
+ write(join11(plan.srcDir, "pages", mcpPageFile(plan.route)), mcpEndpointTemplate()),
3660
+ write(join11(plan.dir, "discovery.ts"), staticJsonEndpointTemplate(buildMcpDiscovery(discoveryInput))),
3661
+ write(join11(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
3662
+ ]);
3663
+ };
3664
+ var ownsApiRest = (page) => page.pattern.startsWith("/api/[");
3665
+ var contentUnderApi = (page) => page.route === "/api" || page.route.startsWith("/api/");
3666
+ var planApi = (project, srcDir, userPages) => {
3667
+ const { config, context } = project;
3668
+ const server = config.deployment.output === "server";
3669
+ return {
3670
+ catchAll: server && !userPages.some(ownsApiRest) && !project.graph.pages.some(contentUnderApi),
3671
+ enabled: config.ai.api,
3672
+ server,
3673
+ spec: !routeIsTaken(userPages, project.graph.pages, OPENAPI_PATH) && !existsSync6(join11(context.root, "public", "openapi.json")),
3674
+ srcDir
3675
+ };
3676
+ };
3677
+ var writeApiFiles = async (project, plan, write, data, mcp) => {
3678
+ if (!(plan.enabled && data)) {
3679
+ return;
3680
+ }
3681
+ const { config } = project;
3682
+ const mcpRoute = mcp.enabled ? mcp.route : null;
3683
+ const apiDir = join11(plan.srcDir, "pages", "api");
3684
+ const writes = [
3685
+ write(join11(apiDir, "docs", "pages.json.ts"), apiPagesIndexTemplate()),
3686
+ write(join11(apiDir, "docs", "pages", "[...route].json.ts"), apiPageTemplate()),
3687
+ write(join11(apiDir, "docs", "navigation.json.ts"), apiNavigationTemplate())
3688
+ ];
3689
+ if (plan.server) {
3690
+ writes.push(write(join11(apiDir, "docs", "search.ts"), apiSearchTemplate()));
3691
+ }
3692
+ if (plan.catchAll) {
3693
+ writes.push(write(join11(apiDir, "[...path].ts"), apiNotFoundTemplate({ base: data.base, site: data.site })));
3694
+ }
3695
+ if (plan.spec) {
3696
+ writes.push(write(join11(plan.srcDir, "pages", "openapi.json.ts"), staticJsonEndpointTemplate(buildApiSpec({
3697
+ agentReadability: config.seo.agentReadability,
3698
+ base: data.base,
3699
+ description: config.description,
3700
+ llmsTxt: config.ai.llmsTxt.enabled,
3701
+ mcpRoute,
3702
+ name: config.title,
3703
+ search: plan.server,
3704
+ site: data.site,
3705
+ version: data.version
3706
+ }))));
3707
+ }
3708
+ await Promise.all(writes);
3709
+ };
3710
+ var planPlaygroundProxy = (config, srcDir) => ({
3711
+ enabled: needsPlaygroundProxy(config),
3712
+ entrypoint: join11(srcDir, "blume-openapi", "api-proxy.ts"),
3713
+ pattern: "/_api-proxy"
3714
+ });
3715
+ var specOriginsOf = (spec) => {
3716
+ const origins = new Set;
3717
+ if (spec.endpoint !== undefined) {
3718
+ try {
3719
+ origins.add(new URL(spec.endpoint).origin);
3720
+ } catch {}
3721
+ }
3722
+ const { servers } = spec.document;
3723
+ for (const server of Array.isArray(servers) ? servers : []) {
3724
+ const url = server.url ?? "";
3725
+ if (url.includes("{")) {
3726
+ continue;
3727
+ }
3728
+ try {
3729
+ origins.add(new URL(url).origin);
3730
+ } catch {}
3731
+ }
3732
+ return [...origins];
3733
+ };
3734
+ var specOrigins = (data) => [...new Set(Object.values(data).flatMap(specOriginsOf))].toSorted();
3735
+ var proxyAllowlistWarnings = (config, data) => {
3736
+ const kinds = builtinProxyKinds(config);
3737
+ const warnings = [];
3738
+ for (const spec of Object.values(data)) {
3739
+ if (!kinds.includes(spec.kind) || specOriginsOf(spec).length > 0) {
3740
+ continue;
3741
+ }
3742
+ warnings.push(spec.kind === "graphql" ? `The "${spec.label}" GraphQL reference (${spec.route}) has playground.proxy: true, but no absolute endpoint is configured for it, so the built-in proxy has no origin to allow and will refuse every request its playground sends. Set the graphql block's (or the source's) \`endpoint\` to the live GraphQL URL, or point playground.proxy at an external proxy URL.` : `The "${spec.label}" reference (${spec.route}) has playground.proxy: true, but its spec declares no absolute servers[].url (relative and templated URLs carry no origin), so the built-in proxy has no origin to allow and will refuse every request its playground sends. Add an absolute server URL to the spec, or point playground.proxy at an external proxy URL.`);
3743
+ }
3744
+ return warnings;
3745
+ };
3746
+ var writeAskFiles = async (project, srcDir, write, modules) => {
3747
+ const { ask } = project.config.ai;
3748
+ if (!(ask?.enabled && !ask.endpoint)) {
3749
+ return;
3750
+ }
3751
+ const grounded = ask.provider !== "inkeep";
3752
+ if (grounded) {
3753
+ modules.set("blume:ask-data", JSON.stringify(await buildAskData(project)));
3754
+ }
3755
+ await write(join11(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(ask), grounded, {
3756
+ instructions: ask.instructions,
3757
+ retrieval: ask.retrieval
3758
+ }));
3759
+ };
3760
+ var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
3761
+ if (routeIsTaken(pages, contentPages, "/404")) {
3762
+ return;
3763
+ }
3764
+ await Promise.all([
3765
+ write(join11(srcDir, "pages", "404.astro"), notFoundPageTemplate()),
3766
+ write(join11(srcDir, "pages", "404.md.ts"), notFoundMarkdownTemplate()),
3767
+ write(join11(srcDir, "pages", "404.json.ts"), notFoundJsonTemplate())
3768
+ ]);
3769
+ };
3770
+ var diagnosticWarning = (diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message;
3771
+ var buildComponentSlots = async (componentsFile) => {
3772
+ const analysis = componentsFile ? analyzeComponentOverrides(await readFile5(componentsFile, "utf-8"), componentsFile) : null;
3773
+ return {
3774
+ plan: planComponentSlots(componentsFile, analysis),
3775
+ tags: analysis ? [...analysis.mdx, ...analysis.islands].map((entry) => entry.key) : [],
3776
+ warnings: analysis ? analysis.warnings : []
3777
+ };
3778
+ };
3779
+ var projectOgFonts = (project) => resolveOgFonts({
3780
+ ogFonts: project.config.seo.og.fonts,
3781
+ themeFonts: project.config.theme.fonts,
3782
+ themeFontsConfigured: project.themeFontsConfigured
3783
+ }, project.context.root);
3784
+ var assertFontFilesExist = (project) => {
3785
+ const { config, context } = project;
3786
+ const missing = missingFontFiles({ ogFonts: config.seo.og.fonts ?? [], themeFonts: config.theme.fonts }, context.root);
3787
+ if (missing.length > 0) {
3788
+ throw new BlumeError({
3789
+ code: "BLUME_FONT_FILE_MISSING",
3790
+ message: `Configured font file(s) not found: ${missing.join(", ")}. Font paths resolve relative to the project root.`,
3791
+ severity: "error"
3792
+ });
3793
+ }
3794
+ };
3795
+ var clientFeaturesFrom = (project, rawMarkdown) => ({
3796
+ epub: project.config.export.epub,
3797
+ mermaid: Object.values(rawMarkdown).some((entry) => hasMermaidFence(entry.mdx ?? entry.md ?? "")) || project.graph.pages.some((page) => page.body !== undefined && hasMermaidFence(page.body.text))
3798
+ });
3799
+ var languageIconCssFrom = (project, rawMarkdown) => languageIconCss(languageIconSlugsIn([
3800
+ ...Object.values(rawMarkdown).map((entry) => entry.mdx ?? entry.md ?? ""),
3801
+ ...project.graph.pages.map((page) => page.body?.text ?? "")
3802
+ ].join(`
3803
+ `)));
3804
+ var languageIconCssFor = async (project) => languageIconCssFrom(project, await buildRawMarkdown(project));
3805
+ var writeNavFragments = (write, srcDir, navFragments) => navFragments ? write(join11(srcDir, "pages", "blume-nav", "[version]", "[locale]", "[id].astro"), navFragmentTemplate()) : Promise.resolve(false);
3806
+ var clientFeaturesFor = async (project) => clientFeaturesFrom(project, await buildRawMarkdown(project));
3807
+ var generateRuntime = async (project) => {
3808
+ const { context, config } = project;
3809
+ assertFontFilesExist(project);
3810
+ const out = context.outDir;
3811
+ const srcDir = join11(out, "src");
3812
+ const generatedDir = join11(srcDir, "generated");
3813
+ const askPath = join11(srcDir, "generated", "Ask.astro");
3814
+ const themePath = join11(srcDir, "generated", "app.css");
3815
+ const searchClientPath = join11(srcDir, "generated", "search-client.ts");
3816
+ const featuresPath = join11(srcDir, "generated", "features.ts");
3817
+ const examplesPath = join11(srcDir, "generated", "examples.ts");
3818
+ const examplesThemePath = join11(srcDir, "generated", "examples.css");
3819
+ const written = new Set;
3820
+ const write = (path, content) => {
3821
+ written.add(normalize(path));
3822
+ return writeIfChanged(path, content);
3823
+ };
3824
+ const modules = new Map;
3825
+ const depsLinkWarning = await ensureDepsLink(out);
3826
+ const contentRoutes = markdownRoutePaths(project);
3827
+ const homeLinkHeader = buildHomeLinkHeader(config, contentRoutes) ?? undefined;
3828
+ const askEnabled = config.ai.ask?.enabled ?? false;
3829
+ const exportPdf = config.export.pdf;
3830
+ const exportEpub = config.export.epub;
3831
+ const rawMarkdown = await buildRawMarkdown(project);
3832
+ const clientFeatures = clientFeaturesFrom(project, rawMarkdown);
3833
+ const navFragments = navVariants(project.graph).some(({ navigation }) => hasDeferrableGroups(navigation.sidebar));
3834
+ const languageIcons = languageIconCssFrom(project, rawMarkdown);
3835
+ const staged = collectStaged(project);
3836
+ const [
3837
+ pages,
3838
+ detectedReact,
3839
+ usesMath,
3840
+ userTheme,
3841
+ userExamplesCss,
3842
+ integrationBridge,
3843
+ islandDiscovery,
3844
+ exampleDiscovery,
3845
+ componentSlots
3846
+ ] = await Promise.all([
3847
+ context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
3848
+ detectNeedsReact(context.root),
3849
+ detectUsesMath(context.root, staged.values()),
3850
+ readUserCss(context.themeFile, generatedDir),
3851
+ readUserCss(examplesCssFile(context.root, config), generatedDir),
3852
+ loadIntegrationBridge(config, context),
3853
+ discoverIslands(context.root),
3854
+ discoverExamples(context.root, config.examples.source),
3855
+ buildComponentSlots(context.componentsFile)
3856
+ ]);
3857
+ const {
3858
+ plan: slotPlan,
3859
+ tags: overrideTags,
3860
+ warnings: overrideWarnings
3861
+ } = componentSlots;
3862
+ project.examples = exampleMarkdownLookup(exampleDiscovery.examples);
3863
+ const frameworks = new Set([
3864
+ ...islandDiscovery.islands.map((island) => island.framework),
3865
+ ...exampleDiscovery.examples.map((example) => example.framework),
3866
+ ...slotPlan.frameworks
3867
+ ]);
3868
+ const needsReact = detectedReact || askEnabled || frameworks.has("react");
3869
+ const needsVue = frameworks.has("vue");
3870
+ const needsSvelte = frameworks.has("svelte");
3871
+ const reactCompilerPath = resolveReactCompiler(config, needsReact);
3872
+ const ogRoutes = customOgRoutes(pages, config.title, config.seo.og.titles);
3873
+ const changelogIndex = hasGeneratedChangelog(project, pages);
3874
+ const mcp = planMcp(project, srcDir, pages);
3875
+ pages.push(...mcp.discoveryPages);
3876
+ const api = planApi(project, srcDir, pages);
3877
+ const agentData = await publishAgentData(project, { api, mcp }, modules);
3878
+ const openApiSource = project.sources.find(isOpenApiSource);
3879
+ const openApiData = openApiSource ? openApiSource.openApiData() : {};
3880
+ const playgroundProxy = planPlaygroundProxy(config, srcDir);
3881
+ if (playgroundProxy.enabled) {
3882
+ pages.push({
3883
+ entrypoint: playgroundProxy.entrypoint,
3884
+ pattern: playgroundProxy.pattern
3885
+ });
3886
+ }
3887
+ const proxyOrigins = specOrigins(openApiData);
3888
+ const hasStaged = staged.size > 0;
3889
+ const hasFilesystemSource = project.sources.some((source) => !source.staged);
3890
+ const docsCollection = resolveDocsCollection(config, context);
3891
+ const [structural] = await Promise.all([
3892
+ Promise.all([
3893
+ write(join11(out, "astro.config.mjs"), astroConfigTemplate({
3894
+ aliases: resolveTsconfigAliases(context.root),
3895
+ askPath,
3896
+ config,
3897
+ contentRoot: docsCollection.base,
3898
+ contentRoutes,
3899
+ context,
3900
+ examplesPath,
3901
+ examplesThemePath,
3902
+ features: clientFeatures,
3903
+ featuresPath,
3904
+ integrationBridge,
3905
+ needsReact,
3906
+ needsSvelte,
3907
+ needsVue,
3908
+ pages,
3909
+ reactCompilerPath,
3910
+ searchClientPath,
3911
+ themePath
3912
+ })),
3913
+ write(join11(out, "package.json"), runtimePackageTemplate(runtimeDependencies({ config, needsReact, needsSvelte, needsVue }))),
3914
+ write(join11(out, "tsconfig.json"), runtimeTsconfigTemplate()),
3915
+ write(join11(srcDir, "content.config.ts"), contentConfigTemplate({
3916
+ collection: docsCollection,
3917
+ config,
3918
+ context,
3919
+ filesystem: hasFilesystemSource,
3920
+ staged: hasStaged
3921
+ })),
3922
+ write(join11(srcDir, "pages", "[...slug].astro"), catchAllPageTemplate({
3923
+ exportEpub,
3924
+ exportPdf,
3925
+ mathEnabled: usesMath,
3926
+ navFragments,
3927
+ needsReact
3928
+ })),
3929
+ writeNavFragments(write, srcDir, navFragments),
3930
+ write(askPath, askComponentTemplate(askEnabled)),
3931
+ write(join11(srcDir, "generated", "components.ts"), slotPlan.module),
3932
+ write(join11(srcDir, "generated", "islands.ts"), islandMapTemplate(islandDiscovery.islands)),
3933
+ write(join11(srcDir, "generated", "examples.ts"), exampleMapTemplate(exampleDiscovery.examples, config.basePath)),
3934
+ write(examplesThemePath, examplesEntryTemplate({
3935
+ configTokens: buildThemeCss(config.theme),
3936
+ sources: exampleScanRoots(context.root, exampleDiscovery.dir).map((dir) => `${dir}/${EXAMPLE_SCAN_GLOB}`),
3937
+ userCss: userExamplesCss
3938
+ })),
3939
+ write(themePath, tailwindEntryTemplate({
3940
+ configTokens: `${buildThemeCss(config.theme)}${buildFontsCss(config.theme.fonts)}`,
3941
+ languageIcons,
3942
+ sources: [
3943
+ `${BLUME_SRC}/**/*.{astro,ts,tsx}`,
3944
+ `${context.root}/**/*.{astro,mdx,ts,tsx}`
3945
+ ],
3946
+ twoslashCss: twoslashCss(),
3947
+ userTheme
3948
+ }))
3949
+ ]),
3950
+ Promise.all(islandDiscovery.islands.map((island) => write(join11(srcDir, "generated", "islands", `${island.name}.astro`), islandWrapperTemplate(island)))),
3951
+ Promise.all(slotPlan.wrappers.map((wrapper) => write(join11(srcDir, "generated", "component-slots", `${wrapper.name}.astro`), wrapper.content))),
3952
+ Promise.all(exampleDiscovery.examples.map((example) => write(join11(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example)))),
3953
+ writeAskFiles(project, srcDir, write, modules),
3954
+ writeMcpFiles(mcp, write, agentData),
3955
+ writeApiFiles(project, api, write, agentData, mcp),
3956
+ playgroundProxy.enabled ? write(playgroundProxy.entrypoint, playgroundProxyTemplate(proxyOrigins)) : Promise.resolve(false)
3957
+ ]);
3958
+ if (config.seo.og.enabled) {
3959
+ await write(join11(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes, {
3960
+ ...projectOgFonts(project),
3961
+ cache: {
3962
+ dir: ogCacheDir(project.context),
3963
+ version: getBlumeVersion()
3964
+ },
3965
+ pageDescriptions: config.seo.og.description !== false
3966
+ }, changelogIndex));
3967
+ }
3968
+ if (changelogIndex) {
3969
+ await write(join11(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
3970
+ exportEpub,
3971
+ exportPdf,
3972
+ needsReact,
3973
+ staged: hasStaged
3974
+ }));
3975
+ }
3976
+ const [examplesWarnings] = await Promise.all([
3977
+ writeExamplesPreview({
3978
+ config,
3979
+ hasExamples: exampleDiscovery.examples.length > 0,
3980
+ root: context.root,
3981
+ srcDir,
3982
+ write
3983
+ }),
3984
+ writeNotFoundPage(write, srcDir, pages, project.graph.pages),
3985
+ write(searchClientPath, searchClientTemplate(config)),
3986
+ write(featuresPath, featuresTemplate(clientFeatures))
3987
+ ]);
3988
+ if (servesStaticIndex(config.search.provider)) {
3989
+ const documents = await buildSearchDocuments(project);
3990
+ modules.set("blume:search-index", JSON.stringify(documents));
3991
+ await write(join11(srcDir, "pages", "blume-search.json.ts"), searchEndpointTemplate());
3992
+ }
3993
+ if (config.search.provider === "mixedbread") {
3994
+ await write(join11(srcDir, "pages", "api", "search.ts"), mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""));
3995
+ }
3996
+ await write(join11(srcDir, "generated", "includes.json"), `${JSON.stringify(buildIncludeGraph(project.graph.pages))}
3997
+ `);
3998
+ modules.set("blume:raw-markdown", JSON.stringify(rawMarkdown));
3999
+ const contentAssets = await collectContentAssets(project);
4000
+ modules.set("blume:content-assets", JSON.stringify(contentAssets));
4001
+ await Promise.all([
4002
+ write(join11(srcDir, "pages", "[...slug].md.ts"), rawMarkdownEndpointTemplate("md")),
4003
+ write(join11(srcDir, "pages", "[...slug].mdx.ts"), rawMarkdownEndpointTemplate("mdx")),
4004
+ write(join11(srcDir, "pages", "blume-assets", "[...asset].ts"), contentAssetsEndpointTemplate(join11(project.context.outDir, "public", "blume-assets")))
4005
+ ]);
4006
+ const feeds = buildRssFeeds(project);
4007
+ if (feeds.length > 0) {
4008
+ const feedXml = Object.fromEntries(feeds.map((feed) => [feed.type, renderRssFeed(feed)]));
4009
+ modules.set("blume:rss", JSON.stringify(feedXml));
4010
+ await write(join11(srcDir, "pages", "[section]", "rss.xml.ts"), rssEndpointTemplate());
4011
+ }
4012
+ const warnings = [
4013
+ ...depsLinkWarning ? [depsLinkWarning] : [],
4014
+ ...proxyAllowlistWarnings(config, openApiData),
4015
+ ...reactCompilerWarnings(config, needsReact, reactCompilerPath),
4016
+ ...mcp.warnings,
4017
+ ...islandDiscovery.warnings,
4018
+ ...exampleDiscovery.warnings,
4019
+ ...examplesWarnings,
4020
+ ...overrideWarnings
4021
+ ];
4022
+ const navTargetRoutes = new Set([
4023
+ ...project.graph.routes.keys(),
4024
+ ...pages.map((page) => page.pattern),
4025
+ ...referenceRoutes(config)
4026
+ ]);
4027
+ if (changelogIndex) {
4028
+ navTargetRoutes.add("/changelog");
4029
+ }
4030
+ warnings.push(...[
4031
+ ...validateNavTargets(project.graph.navigation, navTargetRoutes),
4032
+ ...validateSearchPopularIcons(config.search.popular)
4033
+ ].map(diagnosticWarning));
4034
+ const knownComponentTags = new Set([
4035
+ ...islandDiscovery.islands.map((island) => island.name),
4036
+ ...overrideTags
4037
+ ]);
4038
+ warnings.push(...validateUsedComponents(project.graph.pages, knownComponentTags, new Set(registry.map((item) => item.name))).map(diagnosticWarning), ...searchProviderWarnings(config.search.provider, context.root), ...askProviderWarnings(config.ai.ask, context.root), ...deploymentAdapterWarnings(config.deployment, context.root), ...islandFrameworkWarnings(frameworks, context.root));
4039
+ if (hasScalarReferences(config)) {
4040
+ const references = await buildReferenceFiles({
4041
+ config,
4042
+ contentRoutes: new Set(project.graph.pages.map((page) => page.route)),
4043
+ root: context.root
4044
+ });
4045
+ warnings.push(...references.warnings);
4046
+ await Promise.all(references.files.map((file) => write(join11(srcDir, "pages", file.pagePath), file.content)));
4047
+ }
4048
+ modules.set("blume:data", buildRuntimeData(project));
4049
+ modules.set("blume:openapi", JSON.stringify(openApiData));
4050
+ await Promise.all([
4051
+ write(join11(out, "blume.manifest.json"), `${JSON.stringify(project.manifest, null, 2)}
4052
+ `),
4053
+ writeStagedContent(out, staged)
4054
+ ]);
4055
+ await pruneOrphans(srcDir, written);
4056
+ publishDevNegotiation({ contentRoutes, homeLinkHeader });
4057
+ publishRuntimeModules(modules);
4058
+ return { structuralChange: structural.some(Boolean), warnings };
4059
+ };
4060
+
4061
+ export { SIGNATURES_DIRECTORY_PATH, SIGNATURES_DIRECTORY_TYPE, publishBuildProject, refreshBlumeContent, showBlumeErrorOverlay, ogCacheDir, pruneCardCache, cardCacheTally, buildAskData, buildMcpDiscovery, buildMcpServerCard, buildThemeCss, buildReferenceFiles, tailwindEntryTemplate, examplesEntryTemplate, rebaseSourceDirectives, twoslashCss, planComponentSlots, discoverIslands, EXAMPLE_SCAN_GLOB, exampleScanRoots, discoverExamples, exampleMarkdownLookup, detectNeedsReact, detectUsesMath, collectStaged, buildRuntimeData, languageIconCssFor, clientFeaturesFor, generateRuntime };
4062
+
4063
+ //# debugId=E13637FE8F0CF5E964756E2164756E21
4064
+ //# sourceMappingURL=chunk-5d4q7121.js.map