blume 2.1.3 → 2.2.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 (377) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +126 -0
  3. package/README.md +4 -4
  4. package/dist/cli/{chunk-74wfhv79.js → chunk-05f21r8h.js} +1 -1
  5. package/dist/cli/{chunk-9qzgg6mj.js → chunk-1cm0h819.js} +3 -3
  6. package/dist/cli/{chunk-nbyvgnnm.js → chunk-1rkxzg3n.js} +104 -47
  7. package/dist/cli/chunk-1rkxzg3n.js.map +36 -0
  8. package/dist/cli/{chunk-desvm72z.js → chunk-2nr29y95.js} +22 -26
  9. package/dist/cli/chunk-2nr29y95.js.map +13 -0
  10. package/dist/cli/{chunk-32khwcb6.js → chunk-49r370mb.js} +42 -69
  11. package/dist/cli/{chunk-32khwcb6.js.map → chunk-49r370mb.js.map} +9 -10
  12. package/dist/cli/{chunk-ms9z66nz.js → chunk-4rftmttv.js} +2 -2
  13. package/dist/cli/{chunk-s0bdnc7v.js → chunk-79bzzkx4.js} +5 -4
  14. package/dist/cli/chunk-79bzzkx4.js.map +10 -0
  15. package/dist/cli/{chunk-hvcxng5f.js → chunk-7tekknev.js} +6 -6
  16. package/dist/cli/{chunk-dv2z0rzf.js → chunk-8b489v1b.js} +38 -13
  17. package/dist/cli/{chunk-dv2z0rzf.js.map → chunk-8b489v1b.js.map} +5 -4
  18. package/dist/cli/{chunk-0pmb4tr4.js → chunk-8f8xw4x2.js} +1195 -222
  19. package/dist/cli/chunk-8f8xw4x2.js.map +44 -0
  20. package/dist/cli/chunk-96snx8yy.js +329 -0
  21. package/dist/cli/chunk-96snx8yy.js.map +13 -0
  22. package/dist/cli/{chunk-ckrcm08h.js → chunk-a3ydrqny.js} +48 -62
  23. package/dist/cli/{chunk-ckrcm08h.js.map → chunk-a3ydrqny.js.map} +4 -5
  24. package/dist/cli/{chunk-bxwhtvw8.js → chunk-ahc5gc9h.js} +5 -5
  25. package/dist/cli/{chunk-dpy8qdjf.js → chunk-as09pafp.js} +73 -182
  26. package/dist/cli/chunk-as09pafp.js.map +18 -0
  27. package/dist/cli/{chunk-tfycddbt.js → chunk-bjk964fp.js} +2 -2
  28. package/dist/cli/{chunk-gfc5fqvf.js → chunk-br0n8n3e.js} +18 -18
  29. package/dist/cli/{chunk-gfc5fqvf.js.map → chunk-br0n8n3e.js.map} +1 -1
  30. package/dist/cli/{chunk-297ajdcv.js → chunk-c69tw29j.js} +6 -6
  31. package/dist/cli/{chunk-297ajdcv.js.map → chunk-c69tw29j.js.map} +3 -3
  32. package/dist/cli/chunk-cn3dy3hz.js +146 -0
  33. package/dist/cli/chunk-cn3dy3hz.js.map +11 -0
  34. package/dist/cli/{chunk-124fvmzq.js → chunk-dm4mg9bj.js} +11064 -8742
  35. package/dist/cli/{chunk-124fvmzq.js.map → chunk-dm4mg9bj.js.map} +78 -64
  36. package/dist/cli/{chunk-dqhg54mr.js → chunk-dmx6cezx.js} +1 -1
  37. package/dist/cli/{chunk-60hz265r.js → chunk-dpcahcrb.js} +24 -11
  38. package/dist/cli/chunk-dpcahcrb.js.map +10 -0
  39. package/dist/cli/{chunk-a57yh0h9.js → chunk-dyk3r7jw.js} +7 -7
  40. package/dist/cli/{chunk-a57yh0h9.js.map → chunk-dyk3r7jw.js.map} +1 -1
  41. package/dist/cli/{chunk-twy7psdz.js → chunk-enk76qd5.js} +14 -10
  42. package/dist/cli/chunk-enk76qd5.js.map +10 -0
  43. package/dist/cli/{chunk-k3gr1nm9.js → chunk-fqy4n3q2.js} +13 -16
  44. package/dist/cli/{chunk-k3gr1nm9.js.map → chunk-fqy4n3q2.js.map} +3 -3
  45. package/dist/cli/{chunk-sc93apfs.js → chunk-frk2wdq1.js} +17 -17
  46. package/dist/cli/{chunk-sc93apfs.js.map → chunk-frk2wdq1.js.map} +2 -2
  47. package/dist/cli/{chunk-kdpzw38a.js → chunk-gprwej7h.js} +95 -7
  48. package/dist/cli/chunk-gprwej7h.js.map +11 -0
  49. package/dist/cli/chunk-kkrteb15.js +157 -0
  50. package/dist/cli/chunk-kkrteb15.js.map +10 -0
  51. package/dist/cli/{chunk-ave1wcbr.js → chunk-m9bp2hc1.js} +1 -1
  52. package/dist/cli/{chunk-58bt3hag.js → chunk-mrvh4c88.js} +4 -4
  53. package/dist/cli/{chunk-5cmts6dv.js → chunk-p8zg3a2h.js} +7 -7
  54. package/dist/cli/{chunk-5cmts6dv.js.map → chunk-p8zg3a2h.js.map} +1 -1
  55. package/dist/cli/{chunk-0w6hs479.js → chunk-rkqae7kh.js} +30 -18
  56. package/dist/cli/chunk-rkqae7kh.js.map +10 -0
  57. package/dist/cli/{chunk-bqxaqn5p.js → chunk-t5xx74rd.js} +8 -8
  58. package/dist/cli/{chunk-bqxaqn5p.js.map → chunk-t5xx74rd.js.map} +1 -1
  59. package/dist/cli/{chunk-1f882900.js → chunk-th0pgaj5.js} +2 -2
  60. package/dist/cli/{chunk-dhwtqnnd.js → chunk-vnbvhgmd.js} +2 -2
  61. package/dist/cli/{chunk-gxtg6ed2.js → chunk-vzy9rh6y.js} +21 -21
  62. package/dist/cli/{chunk-gxtg6ed2.js.map → chunk-vzy9rh6y.js.map} +3 -3
  63. package/dist/cli/{chunk-j1x92mx9.js → chunk-w6e455pz.js} +1 -1
  64. package/dist/cli/chunk-wehv26ry.js +155 -0
  65. package/dist/cli/chunk-wehv26ry.js.map +11 -0
  66. package/dist/cli/{chunk-y4msrwm9.js → chunk-xgq4eytj.js} +15 -15
  67. package/dist/cli/{chunk-36bxs9c7.js → chunk-xt0qvz1t.js} +27 -23
  68. package/dist/cli/chunk-xt0qvz1t.js.map +10 -0
  69. package/dist/cli/index.js +78 -42
  70. package/dist/cli/index.js.map +7 -6
  71. package/dist/types/ai/changelog-components.d.ts +18 -0
  72. package/dist/types/ai/changelog-markdown.d.ts +12 -0
  73. package/dist/types/ai/openapi-components.d.ts +7 -7
  74. package/dist/types/ai/operation-details.d.ts +39 -0
  75. package/dist/types/ai/relative-links.d.ts +3 -1
  76. package/dist/types/ai/serializers.d.ts +6 -4
  77. package/dist/types/analytics/head.d.ts +4 -2
  78. package/dist/types/analytics/script.d.ts +8 -2
  79. package/dist/types/components/content/youtube.d.ts +20 -0
  80. package/dist/types/components/layout/nav-utils.d.ts +8 -0
  81. package/dist/types/components/openapi/example-panels.d.ts +33 -0
  82. package/dist/types/components/openapi/helpers.d.ts +53 -11
  83. package/dist/types/components/openapi/snippets.d.ts +7 -1
  84. package/dist/types/core/alert-diagnostics.d.ts +10 -0
  85. package/dist/types/core/builtin-tags.d.ts +16 -0
  86. package/dist/types/core/changelog-list.d.ts +48 -0
  87. package/dist/types/core/closest-match.d.ts +6 -0
  88. package/dist/types/core/code-fence-diagnostics.d.ts +18 -0
  89. package/dist/types/core/code-fences.d.ts +8 -1
  90. package/dist/types/core/config-input.d.ts +1 -1
  91. package/dist/types/core/content-assets.d.ts +16 -18
  92. package/dist/types/core/diagnostics.d.ts +14 -0
  93. package/dist/types/core/directive-diagnostics.d.ts +22 -8
  94. package/dist/types/core/former-routes.d.ts +14 -0
  95. package/dist/types/core/github.d.ts +5 -4
  96. package/dist/types/core/heading-markers.d.ts +65 -2
  97. package/dist/types/core/hidden-pages.d.ts +3 -0
  98. package/dist/types/core/includes.d.ts +23 -0
  99. package/dist/types/core/last-modified.d.ts +18 -6
  100. package/dist/types/core/links.d.ts +42 -2
  101. package/dist/types/core/load-module.d.ts +19 -4
  102. package/dist/types/core/manifest.d.ts +14 -1
  103. package/dist/types/core/markdown-mirrors.d.ts +22 -0
  104. package/dist/types/core/mdx-elements.d.ts +32 -0
  105. package/dist/types/core/mdx-syntax.d.ts +100 -0
  106. package/dist/types/core/meta-diagnostics.d.ts +7 -0
  107. package/dist/types/core/meta.d.ts +32 -5
  108. package/dist/types/core/nav-diagnostics.d.ts +9 -2
  109. package/dist/types/core/navigation.d.ts +13 -0
  110. package/dist/types/core/ordering-prefix.d.ts +6 -0
  111. package/dist/types/core/pinned-ids.d.ts +23 -0
  112. package/dist/types/core/project-graph.d.ts +9 -1
  113. package/dist/types/core/redirect-patterns.d.ts +18 -0
  114. package/dist/types/core/relative-files.d.ts +31 -0
  115. package/dist/types/core/schema.d.ts +267 -7
  116. package/dist/types/core/sources/normalize.d.ts +101 -9
  117. package/dist/types/core/sources/types.d.ts +16 -0
  118. package/dist/types/core/static-files.d.ts +10 -0
  119. package/dist/types/core/static-host.d.ts +42 -0
  120. package/dist/types/core/svg-images.d.ts +26 -0
  121. package/dist/types/core/syntax-diagnostics.d.ts +14 -0
  122. package/dist/types/core/types.d.ts +42 -1
  123. package/dist/types/deploy/generated-files.d.ts +41 -0
  124. package/dist/types/deploy/redirects.d.ts +21 -7
  125. package/dist/types/markdown/code-title.d.ts +9 -5
  126. package/dist/types/markdown/directives.d.ts +8 -0
  127. package/dist/types/markdown/fence-language.d.ts +51 -0
  128. package/dist/types/markdown/fence-meta.d.ts +5 -0
  129. package/dist/types/markdown/github-alerts.d.ts +35 -0
  130. package/dist/types/openapi/asyncapi.d.ts +6 -5
  131. package/dist/types/openapi/checks.d.ts +10 -5
  132. package/dist/types/openapi/model.d.ts +55 -8
  133. package/dist/types/openapi/parse.d.ts +4 -1
  134. package/dist/types/sources/filesystem.d.ts +14 -4
  135. package/dist/types/sources/mdx-remote.d.ts +6 -3
  136. package/dist/types/sources/registry.d.ts +3 -3
  137. package/dist/types/theme/color.d.ts +33 -0
  138. package/dist/types/theme/palette.d.ts +122 -0
  139. package/docs/01-quickstart.mdx +1 -1
  140. package/docs/02-deployment.mdx +23 -8
  141. package/docs/04-migrating.mdx +16 -4
  142. package/docs/advanced/changelog.mdx +27 -1
  143. package/docs/advanced/skills.mdx +1 -1
  144. package/docs/cli/audit.mdx +1 -1
  145. package/docs/cli/doctor.mdx +2 -2
  146. package/docs/cli/evals.mdx +1 -1
  147. package/docs/cli/index.mdx +4 -2
  148. package/docs/cli/validate.mdx +10 -7
  149. package/docs/configuration/analytics.mdx +2 -0
  150. package/docs/configuration/customization.mdx +3 -1
  151. package/docs/configuration/index.mdx +2 -2
  152. package/docs/configuration/theming.mdx +5 -1
  153. package/docs/content/components.mdx +26 -6
  154. package/docs/content/frontmatter.mdx +1 -1
  155. package/docs/content/includes.mdx +6 -2
  156. package/docs/content/index.mdx +11 -1
  157. package/docs/content/islands.mdx +2 -0
  158. package/docs/content/meta.mdx +9 -3
  159. package/docs/content/navigation.mdx +14 -6
  160. package/docs/content/sources/obsidian.mdx +1 -1
  161. package/docs/content/sources/remote-mdx.mdx +3 -1
  162. package/docs/content/syntax.mdx +76 -8
  163. package/docs/content/variables.mdx +3 -1
  164. package/docs/discoverability/agent-discovery.mdx +1 -1
  165. package/docs/discoverability/json-api.mdx +1 -1
  166. package/docs/discoverability/llms-txt.mdx +1 -1
  167. package/docs/discoverability/markdown.mdx +2 -2
  168. package/docs/discoverability/open-graph.mdx +1 -1
  169. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  170. package/docs/references/graphql.mdx +2 -0
  171. package/docs/references/openapi.mdx +45 -7
  172. package/package.json +4 -4
  173. package/skills/blume/SKILL.md +2 -2
  174. package/skills/blume-migrate/SKILL.md +49 -27
  175. package/skills/blume-migrate/references/docsify.md +262 -0
  176. package/skills/blume-migrate/references/docus.md +298 -0
  177. package/skills/blume-migrate/references/docusaurus.md +3 -3
  178. package/skills/blume-migrate/references/fern.md +327 -0
  179. package/skills/blume-migrate/references/fumadocs.md +1 -1
  180. package/skills/blume-migrate/references/gitbook.md +267 -0
  181. package/skills/blume-migrate/references/github-wiki.md +259 -0
  182. package/skills/blume-migrate/references/jekyll.md +253 -0
  183. package/skills/blume-migrate/references/mdbook.md +256 -0
  184. package/skills/blume-migrate/references/mintlify.md +5 -5
  185. package/skills/blume-migrate/references/mkdocs.md +236 -0
  186. package/skills/blume-migrate/references/monorepo.md +20 -8
  187. package/skills/blume-migrate/references/nextra.md +2 -2
  188. package/skills/blume-migrate/references/readme.md +257 -0
  189. package/skills/blume-migrate/references/redocly.md +198 -0
  190. package/skills/blume-migrate/references/starlight.md +3 -3
  191. package/skills/blume-migrate/references/vitepress.md +204 -0
  192. package/skills/blume-migrate/references/vuepress.md +271 -0
  193. package/skills/blume-migrate/scripts/docsify-codemod.mjs +2387 -0
  194. package/skills/blume-migrate/scripts/docus-codemod.mjs +2542 -0
  195. package/skills/blume-migrate/scripts/fern-codemod.mjs +2633 -0
  196. package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +2808 -0
  197. package/skills/blume-migrate/scripts/include-excerpts.mjs +649 -0
  198. package/skills/blume-migrate/scripts/jekyll-codemod.mjs +3649 -0
  199. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +17 -13
  200. package/skills/blume-migrate/scripts/mkdocs-codemod.mjs +2526 -0
  201. package/skills/blume-migrate/scripts/operation-routes.mjs +508 -0
  202. package/skills/blume-migrate/scripts/pin-heading-ids.mjs +1049 -0
  203. package/skills/blume-migrate/scripts/readme-codemod.mjs +4139 -0
  204. package/skills/blume-migrate/scripts/vitepress-codemod.mjs +1511 -0
  205. package/src/ai/changelog-components.ts +24 -0
  206. package/src/ai/changelog-markdown.ts +45 -41
  207. package/src/ai/component-markdown.ts +22 -9
  208. package/src/ai/llms.ts +36 -5
  209. package/src/ai/markdown.ts +2 -2
  210. package/src/ai/openapi-components.ts +25 -9
  211. package/src/ai/operation-details.ts +208 -0
  212. package/src/ai/relative-links.ts +48 -8
  213. package/src/ai/serializers.ts +7 -6
  214. package/src/ai/site-skill.ts +6 -2
  215. package/src/analytics/head.ts +11 -5
  216. package/src/analytics/script.ts +13 -3
  217. package/src/astro/generate.ts +32 -6
  218. package/src/astro/index.ts +1 -0
  219. package/src/astro/integration.ts +27 -1
  220. package/src/astro/mdx-source-errors.ts +46 -0
  221. package/src/astro/templates.ts +89 -185
  222. package/src/audit/checks/redirects.ts +5 -1
  223. package/src/audit/redirects.ts +36 -19
  224. package/src/audit/report.ts +22 -9
  225. package/src/audit/run.ts +12 -28
  226. package/src/audit/terms.ts +23 -0
  227. package/src/audit/url.ts +2 -2
  228. package/src/cli/build-failure.ts +256 -7
  229. package/src/cli/command-meta.ts +1 -1
  230. package/src/cli/commands/audit.ts +1 -1
  231. package/src/cli/commands/build.ts +12 -3
  232. package/src/cli/commands/check.ts +13 -2
  233. package/src/cli/commands/dev.ts +1 -1
  234. package/src/cli/commands/doctor.ts +4 -2
  235. package/src/cli/commands/init.ts +3 -3
  236. package/src/cli/commands/preview.ts +17 -1
  237. package/src/cli/commands/validate.ts +22 -10
  238. package/src/cli/prepare.ts +1 -1
  239. package/src/cli/preview-pages.ts +161 -0
  240. package/src/cli/unknown-flags.ts +1 -42
  241. package/src/components/code-copy.ts +51 -0
  242. package/src/components/code-language.ts +75 -0
  243. package/src/components/content/AccordionItem.astro +16 -1
  244. package/src/components/content/Callout.astro +11 -1
  245. package/src/components/content/Changelog.astro +189 -0
  246. package/src/components/content/CodeBlock.astro +8 -11
  247. package/src/components/content/Tabs.astro +16 -2
  248. package/src/components/content/YouTube.astro +6 -4
  249. package/src/components/content/code-block.ts +33 -0
  250. package/src/components/content/content-strings.ts +5 -1
  251. package/src/components/content/inline-markdown.ts +35 -8
  252. package/src/components/content/youtube.ts +49 -7
  253. package/src/components/layout/Analytics.astro +5 -1
  254. package/src/components/layout/NavTree.astro +8 -3
  255. package/src/components/layout/RootLayout.astro +18 -31
  256. package/src/components/layout/nav-utils.ts +14 -0
  257. package/src/components/layout/search/pagefind.ts +2 -1
  258. package/src/components/openapi/Operation.astro +4 -0
  259. package/src/components/openapi/PanelTabs.astro +9 -3
  260. package/src/components/openapi/RequestBody.astro +42 -8
  261. package/src/components/openapi/RequestPanel.astro +34 -53
  262. package/src/components/openapi/example-panels.ts +110 -0
  263. package/src/components/openapi/helpers.ts +195 -20
  264. package/src/components/openapi/operation-model.ts +63 -26
  265. package/src/components/openapi/request.ts +13 -0
  266. package/src/components/openapi/snippets.ts +14 -2
  267. package/src/core/alert-diagnostics.ts +89 -0
  268. package/src/core/builtin-tags.ts +76 -0
  269. package/src/core/changelog-list.ts +87 -0
  270. package/src/core/closest-match.ts +41 -0
  271. package/src/core/code-fence-diagnostics.ts +267 -0
  272. package/src/core/code-fences.ts +43 -7
  273. package/src/core/component-diagnostics.ts +65 -12
  274. package/src/core/config-input.ts +1 -1
  275. package/src/core/config.ts +46 -1
  276. package/src/core/content-assets.ts +64 -144
  277. package/src/core/diagnostics.ts +36 -3
  278. package/src/core/directive-diagnostics.ts +265 -47
  279. package/src/core/former-routes.ts +46 -0
  280. package/src/core/git-ignored.ts +53 -0
  281. package/src/core/github.ts +5 -4
  282. package/src/core/heading-markers.ts +156 -2
  283. package/src/core/hidden-pages.ts +64 -0
  284. package/src/core/includes.ts +181 -41
  285. package/src/core/last-modified.ts +47 -10
  286. package/src/core/links.ts +438 -83
  287. package/src/core/load-module.ts +98 -9
  288. package/src/core/manifest.ts +12 -9
  289. package/src/core/markdown-mirrors.ts +52 -0
  290. package/src/core/mdx-elements.ts +153 -0
  291. package/src/core/mdx-syntax.ts +377 -0
  292. package/src/core/meta-diagnostics.ts +134 -0
  293. package/src/core/meta.ts +154 -55
  294. package/src/core/nav-diagnostics.ts +16 -4
  295. package/src/core/navigation.ts +70 -10
  296. package/src/core/ordering-prefix.ts +22 -2
  297. package/src/core/pinned-ids.ts +141 -0
  298. package/src/core/project-graph.ts +221 -29
  299. package/src/core/redirect-patterns.ts +41 -0
  300. package/src/core/relative-files.ts +145 -0
  301. package/src/core/schema.ts +17 -3
  302. package/src/core/sources/mdx-remote.ts +75 -14
  303. package/src/core/sources/normalize.ts +885 -179
  304. package/src/core/sources/notion.ts +5 -4
  305. package/src/core/sources/obsidian.ts +2 -5
  306. package/src/core/sources/types.ts +17 -0
  307. package/src/core/static-files.ts +40 -0
  308. package/src/core/static-host.ts +125 -0
  309. package/src/core/svg-images.ts +98 -0
  310. package/src/core/syntax-diagnostics.ts +491 -0
  311. package/src/core/types.ts +43 -1
  312. package/src/deploy/artifacts.ts +2 -28
  313. package/src/deploy/generated-files.ts +128 -0
  314. package/src/deploy/platforms/vercel.ts +25 -1
  315. package/src/deploy/redirects.ts +43 -16
  316. package/src/deploy/sitemap.ts +2 -1
  317. package/src/markdown/base-links.ts +87 -6
  318. package/src/markdown/code-title.ts +29 -13
  319. package/src/markdown/content-assets.ts +210 -0
  320. package/src/markdown/directives.ts +69 -5
  321. package/src/markdown/fence-language.ts +106 -0
  322. package/src/markdown/fence-meta.ts +42 -0
  323. package/src/markdown/github-alerts.ts +172 -0
  324. package/src/markdown/heading-anchors.ts +265 -56
  325. package/src/markdown/include.ts +30 -3
  326. package/src/markdown/index.ts +145 -40
  327. package/src/markdown/prompt-text.ts +95 -0
  328. package/src/markdown/relative-links.ts +37 -2
  329. package/src/markdown/route-snapshot.ts +22 -12
  330. package/src/markdown/svg-fallback.ts +51 -0
  331. package/src/markdown/title-heading.ts +134 -0
  332. package/src/migrate/migrate.ts +127 -2
  333. package/src/og/card.ts +14 -4
  334. package/src/openapi/asyncapi.ts +14 -6
  335. package/src/openapi/checks.ts +160 -15
  336. package/src/openapi/model.ts +172 -33
  337. package/src/openapi/parse.ts +13 -4
  338. package/src/openapi/render-mdx.ts +55 -9
  339. package/src/openapi/source.ts +87 -6
  340. package/src/registry/eject.ts +0 -1
  341. package/src/search/documents.ts +10 -2
  342. package/src/sources/filesystem.ts +28 -3
  343. package/src/sources/mdx-remote.ts +7 -15
  344. package/src/theme/color.ts +14 -0
  345. package/src/theme/entry.ts +22 -1
  346. package/src/theme/palette.ts +74 -1
  347. package/dist/cli/chunk-0pmb4tr4.js.map +0 -35
  348. package/dist/cli/chunk-0w6hs479.js.map +0 -10
  349. package/dist/cli/chunk-150jk7km.js +0 -52
  350. package/dist/cli/chunk-150jk7km.js.map +0 -10
  351. package/dist/cli/chunk-36bxs9c7.js.map +0 -10
  352. package/dist/cli/chunk-53jqw8z0.js +0 -89
  353. package/dist/cli/chunk-53jqw8z0.js.map +0 -10
  354. package/dist/cli/chunk-60hz265r.js.map +0 -10
  355. package/dist/cli/chunk-desvm72z.js.map +0 -14
  356. package/dist/cli/chunk-dpy8qdjf.js.map +0 -17
  357. package/dist/cli/chunk-es8h0v3j.js +0 -441
  358. package/dist/cli/chunk-es8h0v3j.js.map +0 -11
  359. package/dist/cli/chunk-kdpzw38a.js.map +0 -11
  360. package/dist/cli/chunk-nbyvgnnm.js.map +0 -36
  361. package/dist/cli/chunk-s0bdnc7v.js.map +0 -10
  362. package/dist/cli/chunk-twy7psdz.js.map +0 -10
  363. package/dist/cli/chunk-ydp3065p.js +0 -64
  364. package/dist/cli/chunk-ydp3065p.js.map +0 -10
  365. /package/dist/cli/{chunk-74wfhv79.js.map → chunk-05f21r8h.js.map} +0 -0
  366. /package/dist/cli/{chunk-9qzgg6mj.js.map → chunk-1cm0h819.js.map} +0 -0
  367. /package/dist/cli/{chunk-ms9z66nz.js.map → chunk-4rftmttv.js.map} +0 -0
  368. /package/dist/cli/{chunk-hvcxng5f.js.map → chunk-7tekknev.js.map} +0 -0
  369. /package/dist/cli/{chunk-bxwhtvw8.js.map → chunk-ahc5gc9h.js.map} +0 -0
  370. /package/dist/cli/{chunk-tfycddbt.js.map → chunk-bjk964fp.js.map} +0 -0
  371. /package/dist/cli/{chunk-dqhg54mr.js.map → chunk-dmx6cezx.js.map} +0 -0
  372. /package/dist/cli/{chunk-ave1wcbr.js.map → chunk-m9bp2hc1.js.map} +0 -0
  373. /package/dist/cli/{chunk-58bt3hag.js.map → chunk-mrvh4c88.js.map} +0 -0
  374. /package/dist/cli/{chunk-1f882900.js.map → chunk-th0pgaj5.js.map} +0 -0
  375. /package/dist/cli/{chunk-dhwtqnnd.js.map → chunk-vnbvhgmd.js.map} +0 -0
  376. /package/dist/cli/{chunk-j1x92mx9.js.map → chunk-w6e455pz.js.map} +0 -0
  377. /package/dist/cli/{chunk-y4msrwm9.js.map → chunk-xgq4eytj.js.map} +0 -0
@@ -4,6 +4,7 @@ import {
4
4
  localizeRoute,
5
5
  localizeInternalPath,
6
6
  i18nDiagnostics,
7
+ routeSetFor,
7
8
  blumeReferences,
8
9
  frontmatter_default,
9
10
  resolveLocalizable,
@@ -13,11 +14,16 @@ import {
13
14
  discoverPages,
14
15
  customStaticRoutes,
15
16
  nextFenceState,
17
+ resolveRelativeImage,
16
18
  hashText,
17
19
  pollingWatch,
18
20
  snapshotCache,
19
21
  loadWithCache,
22
+ MARKDOWN_BODY_FEATURES,
20
23
  MDX_BODY_FEATURES,
24
+ isExpression,
25
+ pageKey,
26
+ folderChildKeys,
21
27
  normalizeRef,
22
28
  isPageRef,
23
29
  pagesByRef,
@@ -28,7 +34,7 @@ import {
28
34
  hasVariables,
29
35
  substituteVariables,
30
36
  undefinedVariables,
31
- pageMetaSchema,
37
+ BUILT_IN_PAGE_META_KEYS,
32
38
  folderMetaSchema,
33
39
  versionizeRoute,
34
40
  versionsDiagnostics,
@@ -36,6 +42,10 @@ import {
36
42
  slugifyPath,
37
43
  unloadablePathDiagnostic,
38
44
  frontmatterYamlDiagnostic,
45
+ ATTRIBUTE,
46
+ trailingCurlyMarker,
47
+ HTML_COMMENT,
48
+ INLINE_CODE,
39
49
  extractHeadings,
40
50
  strippedLineOffset,
41
51
  isStringValue,
@@ -45,7 +55,8 @@ import {
45
55
  normalizeEntry,
46
56
  hasIncludeStatements,
47
57
  expandIncludes,
48
- parseYouTubeId,
58
+ youtubeEmbedSrc,
59
+ buildManifest,
49
60
  nodeRequire,
50
61
  escapeMarkdownText,
51
62
  guardBlockStart,
@@ -74,21 +85,24 @@ import {
74
85
  asObject,
75
86
  asNumber,
76
87
  objectsIn,
88
+ mirroredPage,
77
89
  createModuleLoader,
78
90
  resolveProjectContext,
79
91
  loadConfig
80
- } from "./chunk-124fvmzq.js";
92
+ } from "./chunk-dm4mg9bj.js";
81
93
  import {
82
- getBlumeVersion,
83
94
  BlumeError,
84
- diagnosticsFromZod
95
+ locatePath,
96
+ diagnosticsFromZod,
97
+ closestMatch
85
98
  } from "./index.js";
86
99
  import {
87
100
  trimChar,
88
101
  normalizePath,
89
102
  isInternalPath,
90
103
  withBasePath,
91
- mountBasePath
104
+ mountBasePath,
105
+ stripBasePath
92
106
  } from "./chunk-zccbjcj0.js";
93
107
  import {
94
108
  BLUME_IGNORE_DIRS,
@@ -195,9 +209,12 @@ var validateSearchPopularIcons = (popular) => {
195
209
  return unknownIconDiagnostics(icons, ICON_FORMAT_HINT);
196
210
  };
197
211
  var resolvesToPages = (routes, path) => routes.has(path) || [...routes].some((route) => route.startsWith(`${path}/`));
198
- var validateNavTargets = (navigation, routes) => {
212
+ var validateNavTargets = (navigation, routes, servesFile = () => false) => {
199
213
  const targets = [
200
- ...navigation.tabs.map((tab) => ({ label: tab.label, path: tab.path })),
214
+ ...navigation.tabs.map((tab) => ({
215
+ label: tab.label,
216
+ path: tab.href ?? tab.path
217
+ })),
201
218
  ...navigation.selectors.flatMap((selector) => selector.items.map((item) => ({ label: item.label, path: item.path }))),
202
219
  ...navigation.featured.map((link) => ({
203
220
  label: link.label,
@@ -215,7 +232,8 @@ var validateNavTargets = (navigation, routes) => {
215
232
  if (!isInternalPath(path) || path.startsWith("/#") || seen.has(path)) {
216
233
  continue;
217
234
  }
218
- if (!resolvesToPages(routes, path.split(/[?#]/u)[0] ?? path)) {
235
+ const bare = path.split(/[?#]/u)[0] ?? path;
236
+ if (!(resolvesToPages(routes, bare) || servesFile(bare))) {
219
237
  seen.add(path);
220
238
  diagnostics.push({
221
239
  code: "BLUME_NAV_MISSING_PAGE",
@@ -324,133 +342,12 @@ var validateNavStructure = (navigation, pages) => [
324
342
  ...hiddenInSidebarDiagnostics(navigation, pages)
325
343
  ];
326
344
 
327
- // src/core/manifest.ts
328
- var versionAlternateKey = (versionKey, locale) => `${versionKey}\x00${locale}`;
329
- var versionAlternatesFor = (byKey, versionKey, locale) => {
330
- const key = versionAlternateKey(versionKey, locale);
331
- const existing = byKey.get(key);
332
- if (existing) {
333
- return existing;
334
- }
335
- const list = [];
336
- byKey.set(key, list);
337
- return list;
338
- };
339
- var MANIFEST_VERSION = 1;
340
- var contentIndexable = (page, config) => !page.meta.search.exclude && (!page.meta.sidebar.hidden || config.search.indexing.includeHiddenPages);
341
- var routeDescription = (page) => page.meta.seo.description ?? page.description;
342
- var buildFallbackRoutes = (graph, i18n, alternatesByKey, basePath, versionAlternatesByKey) => {
343
- const fallback = resolveFallbackLocale(i18n);
344
- if (!fallback) {
345
- return [];
346
- }
347
- const fallbackPages = new Map(graph.pages.flatMap((page) => page.locale === fallback && !page.monolingual ? [[page.translationKey, page]] : []));
348
- const routes = [];
349
- for (const { code } of i18n.locales) {
350
- if (code === fallback) {
351
- continue;
352
- }
353
- const present = new Set(graph.pages.flatMap((page) => page.locale === code ? [page.translationKey] : []));
354
- for (const [key, source] of fallbackPages) {
355
- if (present.has(key)) {
356
- continue;
357
- }
358
- const path = mountBasePath(basePath, localizeRoute(key, code, i18n));
359
- const versionAlternates = versionAlternatesByKey ? versionAlternatesFor(versionAlternatesByKey, source.versionKey, code) : [];
360
- if (versionAlternatesByKey) {
361
- versionAlternates.push({ path, version: source.version });
362
- }
363
- routes.push({
364
- alternates: alternatesByKey.get(key) ?? [],
365
- collection: source.collection ?? "docs",
366
- contentType: source.contentType,
367
- description: routeDescription(source),
368
- draft: source.meta.draft,
369
- editUrl: source.editUrl,
370
- entryId: source.entryId ?? source.source.ref,
371
- fallback: true,
372
- hidden: source.meta.sidebar.hidden,
373
- id: source.id,
374
- indexable: false,
375
- lastModified: source.lastModified,
376
- locale: code,
377
- monolingual: source.monolingual,
378
- path,
379
- source: source.source,
380
- sourcePath: source.sourcePath,
381
- title: source.title,
382
- version: source.version,
383
- versionAlternates
384
- });
385
- }
386
- }
387
- return routes;
388
- };
389
- var buildManifest = (options) => {
390
- const { context, config, graph } = options;
391
- const searchEnabled = config.search.provider.mode !== "none";
392
- const { i18n } = config;
393
- const alternatesByKey = new Map;
394
- if (i18n) {
395
- for (const page of graph.pages) {
396
- const list = alternatesByKey.get(page.translationKey) ?? [];
397
- list.push({ locale: page.locale, path: page.route });
398
- alternatesByKey.set(page.translationKey, list);
399
- }
400
- }
401
- const versionAlternatesByKey = config.versions ? new Map : undefined;
402
- if (versionAlternatesByKey) {
403
- for (const page of graph.pages) {
404
- versionAlternatesFor(versionAlternatesByKey, page.versionKey, page.locale).push({ path: page.route, version: page.version });
405
- }
406
- }
407
- const routes = graph.pages.map((page) => ({
408
- aiExclude: page.meta.ai.exclude,
409
- alternates: alternatesByKey.get(page.translationKey) ?? [],
410
- collection: page.collection ?? "docs",
411
- contentType: page.contentType,
412
- description: routeDescription(page),
413
- draft: page.meta.draft,
414
- editUrl: page.editUrl,
415
- entryId: page.entryId ?? page.source.ref,
416
- hidden: page.meta.sidebar.hidden,
417
- id: page.id,
418
- indexable: searchEnabled && contentIndexable(page, config),
419
- lastModified: page.lastModified,
420
- locale: page.locale,
421
- monolingual: page.monolingual,
422
- path: page.route,
423
- source: page.source,
424
- sourcePath: page.sourcePath,
425
- title: page.title,
426
- version: page.version,
427
- versionAlternates: versionAlternatesByKey ? versionAlternatesFor(versionAlternatesByKey, page.versionKey, page.locale) : []
428
- }));
429
- if (i18n) {
430
- routes.push(...buildFallbackRoutes(graph, i18n, alternatesByKey, config.basePath, versionAlternatesByKey));
431
- }
432
- if (versionAlternatesByKey && config.versions) {
433
- const rank = new Map(config.versions.archived.map((version, index) => [version.id, index + 1]));
434
- for (const list of versionAlternatesByKey.values()) {
435
- list.sort((a, b) => (rank.get(a.version) ?? 0) - (rank.get(b.version) ?? 0));
436
- }
437
- }
438
- routes.sort((a, b) => a.path.localeCompare(b.path));
439
- return {
440
- blumeVersion: getBlumeVersion(),
441
- contentRoot: context.contentRoot,
442
- output: config.deployment.options.output,
443
- projectRoot: context.root,
444
- routes,
445
- version: MANIFEST_VERSION
446
- };
447
- };
448
-
449
345
  // src/core/project-graph.ts
450
- import { isAbsolute as isAbsolute3, relative as relative5 } from "pathe";
346
+ import { existsSync as existsSync3 } from "node:fs";
347
+ import { isAbsolute as isAbsolute3, join as join8, relative as relative5 } from "pathe";
451
348
 
452
- // src/core/directive-diagnostics.ts
453
- import { mdxToMdast } from "satteri";
349
+ // src/core/alert-diagnostics.ts
350
+ import { markdownToMdast } from "satteri";
454
351
 
455
352
  // src/markdown/directives.ts
456
353
  import { toString as mdastToString } from "mdast-util-to-string";
@@ -498,25 +395,366 @@ var directiveSource = (node, marker, source) => {
498
395
  return `${opening}${labelSource(node.children ?? [])}${attributeSource(node)}`;
499
396
  };
500
397
 
398
+ // src/markdown/github-alerts.ts
399
+ var ALERT_CALLOUTS = new Map([
400
+ ["caution", "danger"],
401
+ ["important", "note"],
402
+ ["note", "note"],
403
+ ["tip", "tip"],
404
+ ["warning", "warning"]
405
+ ]);
406
+ var ALERT_MARKER = /^\[!(?<kind>[a-z]+)\]/iu;
407
+ var alertMarkerOf = (text) => {
408
+ const match = ALERT_MARKER.exec(text);
409
+ const callout = ALERT_CALLOUTS.get(match?.groups?.kind?.toLowerCase() ?? "");
410
+ return match && callout ? { callout, marker: match[0] } : undefined;
411
+ };
412
+
413
+ // src/core/alert-diagnostics.ts
414
+ var ALERT_LINE = /^[\t >]*>[\t ]*\[![a-z]+\]/imu;
415
+ var alertFindings = (text) => {
416
+ if (!ALERT_LINE.test(text)) {
417
+ return [];
418
+ }
419
+ const tree = markdownToMdast(text, {
420
+ features: MARKDOWN_BODY_FEATURES
421
+ });
422
+ const found = [];
423
+ const walk = (node) => {
424
+ if (node.type === "blockquote") {
425
+ const [paragraph] = node.children;
426
+ const first = paragraph?.type === "paragraph" ? paragraph.children[0] : undefined;
427
+ const escaped = text[first?.position?.start.offset ?? 0] === "\\";
428
+ const alert = first?.type === "text" && !escaped ? alertMarkerOf(first.value) : undefined;
429
+ if (alert !== undefined) {
430
+ found.push({ ...alert, line: node.position?.start.line ?? 1 });
431
+ return;
432
+ }
433
+ }
434
+ if ("children" in node) {
435
+ for (const child of node.children) {
436
+ walk(child);
437
+ }
438
+ }
439
+ };
440
+ walk(tree);
441
+ return found;
442
+ };
443
+ var alertDiagnostics = (entry, sourceName) => {
444
+ if (entry.body.format !== "md") {
445
+ return [];
446
+ }
447
+ const page = entry.sourcePath ?? `${sourceName}:${entry.ref}`;
448
+ const offset = entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
449
+ return alertFindings(entry.expanded?.text ?? entry.body.text).map(({ callout, line, marker }) => {
450
+ const origin = entry.expanded?.origins[line - 1];
451
+ return {
452
+ code: "BLUME_MD_GITHUB_ALERT",
453
+ file: origin?.file ?? page,
454
+ line: origin?.line ?? line + offset,
455
+ message: `\`${marker}\` makes a quote a callout only in .mdx; in this .md page it renders as a quote that shows \`${marker}\` as text.`,
456
+ severity: "warning",
457
+ suggestion: `Rename the page to .mdx to render it as a \`${callout}\` callout, or remove \`${marker}\` to keep a plain quote.`
458
+ };
459
+ });
460
+ };
461
+
462
+ // src/core/code-fence-diagnostics.ts
463
+ import { markdownToMdast as markdownToMdast2, mdxToMdast } from "satteri";
464
+
465
+ // src/markdown/fence-language.ts
466
+ import { bundledLanguages } from "shiki/langs";
467
+ var PLAIN_LANGUAGES = new Set([
468
+ "ansi",
469
+ "plain",
470
+ "plaintext",
471
+ "text",
472
+ "txt"
473
+ ]);
474
+ var isKnownLanguage = (lang) => PLAIN_LANGUAGES.has(lang) || Object.hasOwn(bundledLanguages, lang);
475
+ var GLUED_RANGE = /^(?<lang>[^{]+)(?<range>\{[^}]*\})$/u;
476
+ var GLUED_TITLE_RANGE = /(?<=^|\s)(?<title>\[[^\]]*\])(?<range>\{[^}]*\})(?=\s|$)/u;
477
+ var normalizeFence = (lang, meta) => {
478
+ if (!lang) {
479
+ return null;
480
+ }
481
+ const glued = GLUED_RANGE.exec(lang)?.groups;
482
+ const id = glued?.lang ?? lang;
483
+ const fenceMeta = (glued?.range ? [glued.range, meta].filter(Boolean).join(" ") : meta)?.replace(GLUED_TITLE_RANGE, "$<title> $<range>") ?? null;
484
+ const lower = id.toLowerCase();
485
+ const language = !isKnownLanguage(id) && isKnownLanguage(lower) ? lower : id;
486
+ return language === lang && fenceMeta === (meta ?? null) ? null : { lang: language, meta: fenceMeta };
487
+ };
488
+
489
+ // src/markdown/fence-meta.ts
490
+ var QUOTED_ATTR = /[\w-]+=(?:"[^"]*"|'[^']*')/gu;
491
+ var LINE_RANGE = /\{[^}]*\}/u;
492
+ var META_TOKEN = new RegExp(`${QUOTED_ATTR.source}|${LINE_RANGE.source}|\\S+`, "gu");
493
+ var metaTokens = (meta) => meta?.match(META_TOKEN) ?? [];
494
+ var RESERVED_META_KEYWORDS = new Set([
495
+ "expandable",
496
+ "lineNumbers",
497
+ "ts2js",
498
+ "twoslash",
499
+ "wrap"
500
+ ]);
501
+ var FOREIGN_META_KEYWORDS = new Set([
502
+ "lines",
503
+ "wordWrap"
504
+ ]);
505
+ var SHOW_LINE_NUMBERS = /^showLineNumbers(?:\{\d*\})?$/u;
506
+ var RUST_META_KEYWORDS = new Set([
507
+ "compile_fail",
508
+ "edition2015",
509
+ "edition2018",
510
+ "edition2021",
511
+ "edition2024",
512
+ "editable",
513
+ "ignore",
514
+ "no_run",
515
+ "noplayground",
516
+ "should_panic"
517
+ ]);
518
+ var RUST_LANGUAGES = new Set(["rs", "rust"]);
519
+ var isForeignKeyword = (token, lang) => FOREIGN_META_KEYWORDS.has(token) || SHOW_LINE_NUMBERS.test(token) || RUST_LANGUAGES.has(lang ?? "") && RUST_META_KEYWORDS.has(token);
520
+ var isLineRange = (token) => token.startsWith("{");
521
+
522
+ // src/core/code-fence-diagnostics.ts
523
+ var BLUME_LANGUAGES = new Set([
524
+ "math",
525
+ "package-install"
526
+ ]);
527
+ var lineRange = (value) => `{${value.trim().split(/[\s,]+/u).join(",")}}`;
528
+ var FOREIGN_OPTIONS = [
529
+ {
530
+ effect: "the block highlights no lines",
531
+ instead: (value) => `Put the lines in braces after the language instead: \`${lineRange(value)}\`.`,
532
+ pattern: /^hl_lines=["'](?<value>[^"']*)["']$/u
533
+ },
534
+ {
535
+ effect: "the block shows no line numbers",
536
+ instead: (value) => `Write \`lineNumbers\` after the language instead.${value.trim() === "1" ? "" : " Blume numbers every block from 1."}`,
537
+ pattern: /^linenums=["'](?<value>[^"']*)["']$/u
538
+ },
539
+ {
540
+ effect: "the block shows no line numbers",
541
+ instead: () => "Write `lineNumbers` after the language instead.",
542
+ pattern: /^showLineNumbers(?:\{\d*\})?$/u
543
+ },
544
+ {
545
+ effect: "the block shows no line numbers",
546
+ instead: () => 'Write `lineNumbers` after the language instead. To keep the word in the block\'s title, set the title with `title="…"`.',
547
+ pattern: /^lines$/u
548
+ },
549
+ {
550
+ effect: "the block doesn't wrap its long lines",
551
+ instead: () => "Write `wrap` after the language instead.",
552
+ pattern: /^wordWrap$/u
553
+ },
554
+ {
555
+ effect: "the block has no title",
556
+ instead: (value) => `Write \`title="${value}"\` instead.`,
557
+ pattern: /^filename=["'](?<value>[^"']*)["']$/u
558
+ },
559
+ {
560
+ effect: "it does nothing: Blume shows a code block as static text, and never compiles, tests, or runs it",
561
+ instead: () => "Remove it.",
562
+ languages: RUST_LANGUAGES,
563
+ pattern: new RegExp(`^(?:${[...RUST_META_KEYWORDS].join("|")})$`, "u")
564
+ }
565
+ ];
566
+ var foreignOption = (token, lang) => {
567
+ for (const option of FOREIGN_OPTIONS) {
568
+ const match = option.pattern.exec(token);
569
+ if (match && (option.languages?.has(lang) ?? true)) {
570
+ return { option, value: match.groups?.value ?? "" };
571
+ }
572
+ }
573
+ return null;
574
+ };
575
+ var rendersLanguage = (lang) => isKnownLanguage(lang) || BLUME_LANGUAGES.has(lang);
576
+ var fenceParts = (lang, meta) => {
577
+ const fence = normalizeFence(lang, meta) ?? {
578
+ lang: lang ?? "",
579
+ meta: meta ?? null
580
+ };
581
+ return { lang: fence.lang, meta: fence.meta, tokens: metaTokens(fence.meta) };
582
+ };
583
+ var unknownLanguage = (node, line) => {
584
+ const { lang } = fenceParts(node.lang, node.meta);
585
+ if (!lang || rendersLanguage(lang)) {
586
+ return [];
587
+ }
588
+ return [
589
+ {
590
+ code: "BLUME_UNKNOWN_CODE_LANGUAGE",
591
+ line,
592
+ message: `\`${lang}\` isn't a language Blume can highlight, so the code block renders as plain text.`,
593
+ suggestion: 'Use a Shiki language id or alias (https://shiki.style/languages), or `text` for plain text. Options like a title go after the language, as in `text title="notes.txt"`.'
594
+ }
595
+ ];
596
+ };
597
+ var foreignOptions = (node, line) => {
598
+ const { lang, tokens } = fenceParts(node.lang, node.meta);
599
+ return [node.lang ?? "", ...tokens].flatMap((token) => {
600
+ const found = foreignOption(token, lang);
601
+ if (!found) {
602
+ return [];
603
+ }
604
+ return [
605
+ {
606
+ code: "BLUME_CODE_FENCE_OPTION",
607
+ line,
608
+ message: `\`${token}\` isn't a Blume code block option, so ${found.option.effect}.`,
609
+ suggestion: found.option.instead(found.value)
610
+ }
611
+ ];
612
+ });
613
+ };
614
+ var FENCE_INFO = /(?:`{3,}|~{3,})[\t ]*(?<info>[^`\n]*)/gu;
615
+ var suspectInfo = (info) => {
616
+ const [lang, ...rest] = info.trim().split(/\s+/u);
617
+ if (!lang) {
618
+ return false;
619
+ }
620
+ const parts = fenceParts(lang, rest.join(" ") || null);
621
+ return !rendersLanguage(parts.lang) || [lang, ...parts.tokens].some((token) => foreignOption(token, parts.lang) !== null);
622
+ };
623
+ var parse = (text, format) => {
624
+ try {
625
+ return format === "mdx" ? mdxToMdast(text, { features: MDX_BODY_FEATURES }) : markdownToMdast2(text, { features: MARKDOWN_BODY_FEATURES });
626
+ } catch {
627
+ return null;
628
+ }
629
+ };
630
+ var fenceFindings = (text, format) => {
631
+ const suspect = [...text.matchAll(FENCE_INFO)].some((match) => suspectInfo(match.groups?.info ?? ""));
632
+ const tree = suspect ? parse(text, format) : null;
633
+ if (!tree) {
634
+ return [];
635
+ }
636
+ const found = [];
637
+ const walk = (node) => {
638
+ if (node.type === "code") {
639
+ const line = node.position?.start.line ?? 1;
640
+ found.push(...unknownLanguage(node, line), ...foreignOptions(node, line));
641
+ }
642
+ if ("children" in node) {
643
+ for (const child of node.children) {
644
+ walk(child);
645
+ }
646
+ }
647
+ };
648
+ walk(tree);
649
+ return found;
650
+ };
651
+ var codeFenceDiagnostics = (entry, sourceName) => {
652
+ const page = entry.sourcePath ?? `${sourceName}:${entry.ref}`;
653
+ const offset = entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
654
+ return fenceFindings(entry.expanded?.text ?? entry.body.text, entry.body.format).map(({ line, ...finding }) => {
655
+ const origin = entry.expanded?.origins[line - 1];
656
+ return {
657
+ ...finding,
658
+ file: origin?.file ?? page,
659
+ line: origin?.line ?? line + offset,
660
+ severity: "warning"
661
+ };
662
+ });
663
+ };
664
+
501
665
  // src/core/directive-diagnostics.ts
666
+ import { markdownToMdast as markdownToMdast3, mdxToMdast as mdxToMdast2 } from "satteri";
502
667
  var CONTAINER_OPENING = /^[\t >]*:{3,}(?<name>[a-z][\w-]*)/gimu;
668
+ var FENCE_WITH_TEXT = /^[\t >]*:{3,}(?:[\t ]+\S|[^\s:a-z])/imu;
669
+ var CLOSING_LINE = /^[\t >]*(?<written>(?<fence>:{3,})[\t ]*(?<text>.*?))\s*$/u;
670
+ var OPENING_TEXT = /^[\t >]*(?<written>(?<fence>:{3,})(?<name>[a-z][\w-]*)(?<label>\[[^\]\n]*\])?(?<attributes>\{[^}\n]*\})?[\t ]+(?<text>\S.*?))\s*$/iu;
671
+ var OPENING_TEXT_LINE = /^[\t >]*:{3,}[a-z][\w-]*(?:\[[^\]\n]*\])?(?:\{[^}\n]*\})?[\t ]+\S/imu;
672
+ var SPACED_OPENING = /^[\t >]*(?<written>(?<fence>:{3,})[\t ]+(?<name>[a-z][\w-]*)(?<title>.*?))\s*$/iu;
673
+ var MD_OPENING = /^[\t >]*(?<written>:{3,}(?<name>[a-z][\w-]*))/iu;
674
+ var MD_OPENING_LINE = /^[\t >]*:{3,}[a-z]/imu;
503
675
  var codeList = (names) => [...names].map((name) => `\`${name}\``).join(", ");
504
676
  var CALLOUT_NAMES = `${codeList(CALLOUT_TYPES)} (or the aliases ${codeList(Object.keys(CALLOUT_ALIASES))})`;
505
- var unknownContainers = (text) => {
506
- const named = [...text.matchAll(CONTAINER_OPENING)].some((match) => calloutTypeFor(match.groups?.name ?? "") === null);
507
- if (!named) {
677
+ var unknownContainer = (node) => calloutTypeFor(node.name) === null ? [
678
+ {
679
+ code: "BLUME_UNKNOWN_DIRECTIVE",
680
+ line: node.position?.start.line ?? 1,
681
+ message: `\`:::${node.name}\` isn't a callout type, so the page shows its \`:::\` lines as written around its content.`,
682
+ suggestion: `Use a callout type — ${CALLOUT_NAMES} — or remove the \`:::\` lines to keep the content as plain prose.`
683
+ }
684
+ ] : [];
685
+ var closingText = (node, lines) => {
686
+ const start = node.position?.start.line ?? 1;
687
+ const end = node.position?.end.line ?? start;
688
+ const content = node.children.at(-1)?.position?.end.line ?? start;
689
+ const groups = end > content ? CLOSING_LINE.exec(lines[end - 1] ?? "")?.groups : undefined;
690
+ if (!(groups?.text && groups.fence && groups.written)) {
691
+ return [];
692
+ }
693
+ const longer = ":".repeat(groups.fence.length + 1);
694
+ return [
695
+ {
696
+ code: "BLUME_DIRECTIVE_CLOSING_TEXT",
697
+ line: end,
698
+ message: `\`${groups.written}\` closes the \`:::${node.name}\` container above it, so \`${groups.text}\` never reaches the page.`,
699
+ suggestion: `Give the container a longer fence than this line's — \`${longer}${node.name}\`, closed by \`${longer}\` — to keep the line inside it as text, or remove \`${groups.text}\` from the line.`
700
+ }
701
+ ];
702
+ };
703
+ var openingText = (node, lines) => {
704
+ const line = node.position?.start.line ?? 1;
705
+ const groups = OPENING_TEXT.exec(lines[line - 1] ?? "")?.groups;
706
+ if (!(groups?.fence && groups.name && groups.text && groups.written) || calloutTypeFor(node.name) === null) {
707
+ return [];
708
+ }
709
+ const opener = `${groups.fence}${groups.name}`;
710
+ return [
711
+ {
712
+ code: "BLUME_DIRECTIVE_OPENING_TEXT",
713
+ line,
714
+ message: `\`${groups.written}\` opens a \`${groups.name}\` callout, but the text after its name, \`${groups.text}\`, never reaches the page.`,
715
+ suggestion: groups.label || groups.attributes ? `Move \`${groups.text}\` into the callout's title, or onto the next line as its text.` : `Put the title in brackets: \`${opener}[${groups.text}]\`.`
716
+ }
717
+ ];
718
+ };
719
+ var spacedOpenings = (node, lines) => {
720
+ const start = node.position?.start.line ?? 1;
721
+ const end = node.position?.end.line ?? start;
722
+ return lines.slice(start - 1, end).flatMap((text, index) => {
723
+ const groups = SPACED_OPENING.exec(text)?.groups;
724
+ if (!(groups?.fence && groups.name && groups.written) || calloutTypeFor(groups.name) === null) {
725
+ return [];
726
+ }
727
+ const opener = `${groups.fence}${groups.name}`;
728
+ const title = groups.title?.trim();
729
+ return [
730
+ {
731
+ code: "BLUME_DIRECTIVE_SPACED_NAME",
732
+ line: start + index,
733
+ message: `\`${groups.written}\` has a space between its colons and its name, so it isn't a callout and the page shows it as text.`,
734
+ suggestion: title ? `Remove the space and put the title in brackets: \`${opener}[${title}]\`.` : `Remove the space: \`${opener}\`.`
735
+ }
736
+ ];
737
+ });
738
+ };
739
+ var directiveFindings = (text) => {
740
+ const suspect = FENCE_WITH_TEXT.test(text) || OPENING_TEXT_LINE.test(text) || [...text.matchAll(CONTAINER_OPENING)].some((match) => calloutTypeFor(match.groups?.name ?? "") === null);
741
+ if (!suspect) {
508
742
  return [];
509
743
  }
510
744
  let tree;
511
745
  try {
512
- tree = mdxToMdast(text, { features: MDX_BODY_FEATURES });
746
+ tree = mdxToMdast2(text, { features: MDX_BODY_FEATURES });
513
747
  } catch {
514
748
  return [];
515
749
  }
750
+ const lines = text.split(`
751
+ `);
516
752
  const found = [];
517
753
  const walk = (node) => {
518
- if (node.type === "containerDirective" && calloutTypeFor(node.name) === null) {
519
- found.push({ line: node.position?.start.line ?? 1, name: node.name });
754
+ if (node.type === "containerDirective") {
755
+ found.push(...unknownContainer(node), ...openingText(node, lines), ...closingText(node, lines));
756
+ } else if (node.type === "paragraph") {
757
+ found.push(...spacedOpenings(node, lines));
520
758
  }
521
759
  if ("children" in node) {
522
760
  for (const child of node.children) {
@@ -525,27 +763,83 @@ var unknownContainers = (text) => {
525
763
  }
526
764
  };
527
765
  walk(tree);
528
- return found;
766
+ return found.toSorted((a, b) => a.line - b.line);
767
+ };
768
+ var mdOpenings = (node, lines) => {
769
+ const start = node.position?.start.line ?? 1;
770
+ const end = node.position?.end.line ?? start;
771
+ return lines.slice(start - 1, end).flatMap((text, index) => {
772
+ const groups = MD_OPENING.exec(text)?.groups;
773
+ if (!(groups?.name && groups.written)) {
774
+ return [];
775
+ }
776
+ const callout = calloutTypeFor(groups.name);
777
+ return [
778
+ {
779
+ code: "BLUME_MD_DIRECTIVE",
780
+ line: start + index,
781
+ message: `\`${groups.written}\` opens a directive, which Blume renders only in .mdx, so this .md page shows it and its closing \`:::\` as text.`,
782
+ suggestion: callout ? `Rename the page to .mdx to render it as a \`${callout}\` callout.` : `Rename the page to .mdx and use a callout type — ${CALLOUT_NAMES} — or remove the \`:::\` lines to keep the content as plain prose.`
783
+ }
784
+ ];
785
+ });
529
786
  };
530
- var unknownDirectiveDiagnostics = (entry, sourceName) => {
531
- if (entry.body.format !== "mdx") {
787
+ var mdDirectiveFindings = (text) => {
788
+ if (!MD_OPENING_LINE.test(text)) {
532
789
  return [];
533
790
  }
791
+ const tree = markdownToMdast3(text, {
792
+ features: MARKDOWN_BODY_FEATURES
793
+ });
794
+ const lines = text.split(`
795
+ `);
796
+ const found = [];
797
+ const walk = (node) => {
798
+ if (node.type === "paragraph") {
799
+ found.push(...mdOpenings(node, lines));
800
+ } else if ("children" in node) {
801
+ for (const child of node.children) {
802
+ walk(child);
803
+ }
804
+ }
805
+ };
806
+ walk(tree);
807
+ return found;
808
+ };
809
+ var directiveDiagnostics = (entry, sourceName) => {
534
810
  const page = entry.sourcePath ?? `${sourceName}:${entry.ref}`;
535
811
  const offset = entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
536
- return unknownContainers(entry.expanded?.text ?? entry.body.text).map(({ line, name }) => {
812
+ const text = entry.expanded?.text ?? entry.body.text;
813
+ const findings = entry.body.format === "mdx" ? directiveFindings(text) : mdDirectiveFindings(text);
814
+ return findings.map(({ line, ...finding }) => {
537
815
  const origin = entry.expanded?.origins[line - 1];
538
816
  return {
539
- code: "BLUME_UNKNOWN_DIRECTIVE",
817
+ ...finding,
540
818
  file: origin?.file ?? page,
541
819
  line: origin?.line ?? line + offset,
542
- message: `\`:::${name}\` isn't a callout type, so the page shows its \`:::\` lines as written around its content.`,
543
- severity: "warning",
544
- suggestion: `Use a callout type — ${CALLOUT_NAMES} — or remove the \`:::\` lines to keep the content as plain prose.`
820
+ severity: "warning"
545
821
  };
546
822
  });
547
823
  };
548
824
 
825
+ // src/core/former-routes.ts
826
+ var formerRouteRedirects = (pages, served, config) => {
827
+ const targets = new Map;
828
+ for (const page of pages) {
829
+ if (page.formerRoute !== undefined) {
830
+ targets.set(page.formerRoute, targets.has(page.formerRoute) ? undefined : page.route);
831
+ }
832
+ }
833
+ const authored = new Set(config.redirects.map((redirect) => normalizePath(withBasePath(config.basePath, redirect.from))));
834
+ return [...targets].flatMap(([from, to]) => to === undefined || served.has(from) || authored.has(from) ? [] : [
835
+ {
836
+ from: stripBasePath(config.basePath, from),
837
+ status: 301,
838
+ to: stripBasePath(config.basePath, to)
839
+ }
840
+ ]);
841
+ };
842
+
549
843
  // src/core/graph.ts
550
844
  var collectRoutes = (pages) => {
551
845
  const routes = new Map;
@@ -810,13 +1104,33 @@ var gitEnv = () => Object.fromEntries(Object.entries(process.env).filter(([key])
810
1104
  var resolveLastModifiedConfig = (value) => value === false ? { enabled: false, source: "git" } : { enabled: true, source: value };
811
1105
  var parseGitLog = (output) => {
812
1106
  const times = new Map;
1107
+ const currentNames = new Map;
1108
+ const renamedAt = new Map;
813
1109
  let current = null;
814
1110
  for (const line of output.split(`
815
1111
  `)) {
816
1112
  if (line.startsWith("\x00")) {
817
1113
  current = line.slice(1);
818
- } else if (line && current && !times.has(line)) {
819
- times.set(line, current);
1114
+ continue;
1115
+ }
1116
+ const [status = "", from = "", to] = line.split("\t");
1117
+ const path = to ?? from;
1118
+ if (!(current && path)) {
1119
+ continue;
1120
+ }
1121
+ const name = currentNames.get(path) ?? path;
1122
+ if (status.startsWith("R")) {
1123
+ currentNames.set(from, name);
1124
+ if (!renamedAt.has(name)) {
1125
+ renamedAt.set(name, current);
1126
+ }
1127
+ } else if (!times.has(name)) {
1128
+ times.set(name, current);
1129
+ }
1130
+ }
1131
+ for (const [name, date] of renamedAt) {
1132
+ if (!times.has(name)) {
1133
+ times.set(name, date);
820
1134
  }
821
1135
  }
822
1136
  return times;
@@ -852,7 +1166,8 @@ var gitLastModifiedTimes = (root, contentRoots, sourcePaths, repositoryRoot) =>
852
1166
  "core.quotePath=false",
853
1167
  "log",
854
1168
  "--format=%x00%cI",
855
- "--name-only",
1169
+ "--name-status",
1170
+ "-M100%",
856
1171
  "--",
857
1172
  ...contentRoots.map(realPath)
858
1173
  ], { encoding: "utf-8", env: gitEnv(), maxBuffer: 256 * 1024 * 1024 });
@@ -894,6 +1209,245 @@ var lastModifiedShallowWarning = (root, undatedCount) => {
894
1209
  ];
895
1210
  };
896
1211
 
1212
+ // src/core/mdx-syntax.ts
1213
+ import { readFile } from "node:fs/promises";
1214
+ import { createMdxMdastHandle, dropHandle, mdxToMdast as mdxToMdast3 } from "satteri";
1215
+ var POSITIONED = /^(?<line>\d+):(?<column>\d+): (?<reason>[\s\S]*?)(?: \([\w-]+:[\w-]+\))?$/u;
1216
+ var INNER_POSITION = / \((?<line>\d+):(?<column>\d+)\)/gu;
1217
+ var mdxSyntaxError = (text) => {
1218
+ try {
1219
+ dropHandle(createMdxMdastHandle(text, MDX_BODY_FEATURES, false));
1220
+ return null;
1221
+ } catch (error) {
1222
+ const message = error instanceof Error ? error.message : String(error);
1223
+ const groups = POSITIONED.exec(message)?.groups;
1224
+ return {
1225
+ column: Number(groups?.column ?? 1),
1226
+ line: Number(groups?.line ?? 1),
1227
+ reason: groups?.reason ?? message
1228
+ };
1229
+ }
1230
+ };
1231
+ var sitesIn = (file, lineOffset, origins) => (line, column) => {
1232
+ const origin = origins?.[line - 1];
1233
+ return origin ? { column, file: origin.file, line: origin.line } : { column, file, line: line + lineOffset };
1234
+ };
1235
+ var placedReason = (reason, at, siteOf) => reason.replaceAll(INNER_POSITION, (written, line, column) => {
1236
+ const inner = siteOf(Number(line), Number(column));
1237
+ return inner.file === at.file ? ` (${inner.line}:${column})` : "";
1238
+ });
1239
+ var EXPRESSION_REASON = /could not parse expression/iu;
1240
+ var COMMENT_REASON = /to create a comment in MDX/u;
1241
+ var LINK_REASON = /to create a link in MDX/u;
1242
+ var syntaxSuggestion = (reason) => {
1243
+ if (EXPRESSION_REASON.test(reason)) {
1244
+ return "In .mdx, `{` starts a JavaScript expression. To show a brace as text, escape it (`\\{`) or put the text in inline code.";
1245
+ }
1246
+ if (COMMENT_REASON.test(reason)) {
1247
+ return "Write the comment as `{/* … */}`.";
1248
+ }
1249
+ if (LINK_REASON.test(reason)) {
1250
+ return "Write the link as `[text](https://…)`.";
1251
+ }
1252
+ return "MDX reads HTML as JSX: close every element (`<br />`, `<img … />`) and match each closing tag. A page that needs no components can be a .md file instead.";
1253
+ };
1254
+ var mdxSyntaxDiagnostic = (error, siteOf, options) => {
1255
+ const at = siteOf(error.line, error.column);
1256
+ const reason = placedReason(error.reason, at, siteOf);
1257
+ return {
1258
+ code: "BLUME_MDX_SYNTAX",
1259
+ column: at.column,
1260
+ file: at.file,
1261
+ line: at.line,
1262
+ message: options.includedIn ? `MDX can't parse this file once ${options.includedIn} includes it: ${reason}.` : `MDX can't parse this page: ${reason}.`,
1263
+ severity: options.severity,
1264
+ suggestion: syntaxSuggestion(reason)
1265
+ };
1266
+ };
1267
+ var mdxSyntaxCheck = (entry, sourceName, variables) => {
1268
+ if (entry.body.format !== "mdx") {
1269
+ return { diagnostics: [], unparsable: false };
1270
+ }
1271
+ const siteOf = (origins) => sitesIn(entry.sourcePath ?? `${sourceName}:${entry.ref}`, entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text), origins);
1272
+ const { expanded } = entry;
1273
+ const body = expanded ? substituteVariables(entry.body.text, variables) : entry.body.text;
1274
+ const own = mdxSyntaxError(body);
1275
+ if (own) {
1276
+ return {
1277
+ diagnostics: [mdxSyntaxDiagnostic(own, siteOf(), { severity: "error" })],
1278
+ unparsable: true
1279
+ };
1280
+ }
1281
+ const spliced = expanded ? mdxSyntaxError(expanded.text) : null;
1282
+ if (!(expanded && spliced)) {
1283
+ return { diagnostics: [], unparsable: false };
1284
+ }
1285
+ return {
1286
+ diagnostics: [
1287
+ mdxSyntaxDiagnostic(spliced, siteOf(expanded.origins), {
1288
+ includedIn: entry.ref,
1289
+ severity: "warning"
1290
+ })
1291
+ ],
1292
+ unparsable: false
1293
+ };
1294
+ };
1295
+ var compiledText = async (page) => {
1296
+ const raw = await readFile(page.sourcePath, "utf-8");
1297
+ const body = frontmatter_default(raw).content;
1298
+ const offset = strippedLineOffset(raw, body);
1299
+ const expansion = page.contentRoot && hasIncludeStatements(body) ? await expandIncludes(body, {
1300
+ contentRoot: page.contentRoot,
1301
+ lineOffset: offset,
1302
+ sourcePath: page.sourcePath
1303
+ }) : null;
1304
+ const written = expansion?.text ?? body;
1305
+ return {
1306
+ siteOf: sitesIn(page.sourcePath, offset, expansion?.origins),
1307
+ text: substituteVariables(written, page.variables),
1308
+ written
1309
+ };
1310
+ };
1311
+ var readsName = (expression, name) => new RegExp(String.raw`(?<![\w$.])${name.replaceAll("$", String.raw`\$`)}(?![\w$])`, "u").test(expression.replaceAll("...", " "));
1312
+ var expressionsOf = (node) => {
1313
+ if (node.type === "mdxFlowExpression" || node.type === "mdxTextExpression") {
1314
+ return [node.value];
1315
+ }
1316
+ if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
1317
+ return node.attributes.flatMap((attribute) => {
1318
+ if (attribute.type === "mdxJsxExpressionAttribute") {
1319
+ return [attribute.value];
1320
+ }
1321
+ return isExpression(attribute.value) ? [attribute.value.value] : [];
1322
+ });
1323
+ }
1324
+ return [];
1325
+ };
1326
+ var expressionReading = (text, name) => {
1327
+ let tree;
1328
+ try {
1329
+ tree = mdxToMdast3(text, { features: MDX_BODY_FEATURES });
1330
+ } catch {
1331
+ return null;
1332
+ }
1333
+ const visit = (node) => {
1334
+ const expression = expressionsOf(node).find((source) => readsName(source, name));
1335
+ if (expression !== undefined) {
1336
+ return { expression: `{${expression.trim()}}`, node };
1337
+ }
1338
+ if ("children" in node) {
1339
+ for (const child of node.children) {
1340
+ const found = visit(child);
1341
+ if (found) {
1342
+ return found;
1343
+ }
1344
+ }
1345
+ }
1346
+ return null;
1347
+ };
1348
+ return visit(tree);
1349
+ };
1350
+ var locateMdxFailure = async (page, failure) => {
1351
+ let compiled;
1352
+ try {
1353
+ compiled = await compiledText(page);
1354
+ } catch {
1355
+ return null;
1356
+ }
1357
+ const { siteOf, text, written } = compiled;
1358
+ if (failure.kind === "undefined-name") {
1359
+ const found = expressionReading(text, failure.name);
1360
+ const start = found?.node.position?.start;
1361
+ if (!(found && start)) {
1362
+ return null;
1363
+ }
1364
+ const at = written.split(`
1365
+ `)[start.line - 1]?.indexOf(found.expression) ?? -1;
1366
+ return {
1367
+ ...siteOf(start.line, at === -1 ? start.column : at + 1),
1368
+ expression: found.expression
1369
+ };
1370
+ }
1371
+ const error = mdxSyntaxError(text);
1372
+ if (!error) {
1373
+ return null;
1374
+ }
1375
+ const diagnostic = mdxSyntaxDiagnostic(error, siteOf, {
1376
+ includedIn: siteOf(error.line).file === page.sourcePath ? undefined : page.name,
1377
+ severity: "error"
1378
+ });
1379
+ return {
1380
+ column: diagnostic.column,
1381
+ diagnostic,
1382
+ file: diagnostic.file ?? page.sourcePath,
1383
+ line: diagnostic.line ?? 1
1384
+ };
1385
+ };
1386
+
1387
+ // src/core/meta-diagnostics.ts
1388
+ import { readFile as readFile2 } from "node:fs/promises";
1389
+ var groupPathOf = (key, versionDirs, localeDirs) => {
1390
+ const parts = key.split("/");
1391
+ if (versionDirs.has(parts[0] ?? "")) {
1392
+ parts.shift();
1393
+ }
1394
+ if (localeDirs.has(parts[0] ?? "")) {
1395
+ parts.shift();
1396
+ }
1397
+ return parts.join("/");
1398
+ };
1399
+ var likelyChild = (entry, children) => {
1400
+ const key = pageKey(entry);
1401
+ return key !== entry && children.has(key) ? key : closestMatch(entry, [...children]);
1402
+ };
1403
+ var SLUG_HINT = "List each child by its slug: the file or folder name without its extension, numeric prefix, or parentheses.";
1404
+ var unknownPageDiagnostics = async (file, pages, group, children) => {
1405
+ const unknown = pages.flatMap((entry, index) => children.has(entry) ? [] : [{ entry, index }]);
1406
+ if (unknown.length === 0) {
1407
+ return [];
1408
+ }
1409
+ const source = await readFile2(file, "utf-8");
1410
+ const where = group ? `"${group}"` : "the content root";
1411
+ return unknown.map(({ entry, index }) => {
1412
+ const position = locatePath(source, ["pages", index]);
1413
+ const match = likelyChild(entry, children);
1414
+ return {
1415
+ code: "BLUME_META_UNKNOWN_PAGE",
1416
+ column: position?.column,
1417
+ file,
1418
+ line: position?.line,
1419
+ message: `pages lists "${entry}", but no page or folder in ${where} has that slug, so the entry orders nothing.`,
1420
+ severity: "warning",
1421
+ suggestion: match ? `Did you mean "${match}"? ${SLUG_HINT}` : SLUG_HINT
1422
+ };
1423
+ });
1424
+ };
1425
+ var unreadDiagnostic = ({ dir, file }, group) => ({
1426
+ code: "BLUME_META_OUTSIDE_INCLUDE",
1427
+ file,
1428
+ message: `This meta file is in the "${group}" sidebar group's folder, but no include glob of its content source reaches that folder, so Blume doesn't read it.`,
1429
+ severity: "warning",
1430
+ suggestion: `Add an include glob that reaches the folder, such as "${dir}/**/*.{md,mdx}".`
1431
+ });
1432
+ var folderMetaDiagnostics = async (discovered, pages, options = {}) => {
1433
+ const children = folderChildKeys(pages.map((page) => page.navPath));
1434
+ const versionDirs = new Set(options.versionDirs);
1435
+ const localeDirs = new Set(options.localeDirs);
1436
+ const groupOf = (key) => groupPathOf(key, versionDirs, localeDirs);
1437
+ const listed = await Promise.all([...discovered.meta, ...discovered.shared].flatMap(([key, meta]) => {
1438
+ const file = discovered.files.get(meta);
1439
+ const group = groupOf(key);
1440
+ return file && meta.pages ? [
1441
+ unknownPageDiagnostics(file, meta.pages, group, children.get(group) ?? new Set)
1442
+ ] : [];
1443
+ }));
1444
+ const unread = discovered.unread.flatMap((entry) => {
1445
+ const group = groupOf(entry.key);
1446
+ return children.has(group) ? [unreadDiagnostic(entry, group)] : [];
1447
+ });
1448
+ return [...listed.flat(), ...unread];
1449
+ };
1450
+
897
1451
  // src/core/meta.ts
898
1452
  import { basename, dirname, relative as relative2 } from "pathe";
899
1453
  import { glob, isDynamicPattern } from "tinyglobby";
@@ -921,19 +1475,21 @@ var globBase = (pattern) => {
921
1475
  };
922
1476
  var onPathOf = (dir, base) => dir === base || base === "" || dir === "" || dir.startsWith(`${base}/`) || base.startsWith(`${dir}/`);
923
1477
  var withinInclude = (dir, include) => include === undefined || include.some((pattern) => !pattern.startsWith("!") && onPathOf(dir, globBase(pattern)));
1478
+ var globFolderMetaFiles = (source, patterns) => glob([...patterns], {
1479
+ absolute: true,
1480
+ cwd: source.root,
1481
+ ignore: [
1482
+ ...source.exclude ?? [],
1483
+ "**/node_modules/**",
1484
+ "**/.blume/**",
1485
+ "**/dist/**"
1486
+ ],
1487
+ onlyFiles: true
1488
+ });
1489
+ var isReadMetaFile = (source, file) => withinInclude(relative2(source.root, dirname(file)), source.include);
924
1490
  var findFolderMetaFiles = async (source, patterns = META_FILES) => {
925
- const found = await glob([...patterns], {
926
- absolute: true,
927
- cwd: source.root,
928
- ignore: [
929
- ...source.exclude ?? [],
930
- "**/node_modules/**",
931
- "**/.blume/**",
932
- "**/dist/**"
933
- ],
934
- onlyFiles: true
935
- });
936
- return found.filter((file) => withinInclude(relative2(source.root, dirname(file)), source.include));
1491
+ const found = await globFolderMetaFiles(source, patterns);
1492
+ return found.filter((file) => isReadMetaFile(source, file));
937
1493
  };
938
1494
  var metaKeyFor = (prefix, dir) => {
939
1495
  const clean = prefix ? trimChar(prefix, "/") : "";
@@ -949,10 +1505,31 @@ var discoverFolderMeta = async (sources, options = {}) => {
949
1505
  const load = createModuleLoader();
950
1506
  const meta = new Map;
951
1507
  const shared = new Map;
1508
+ const files = new Map;
1509
+ const unread = [];
952
1510
  const diagnostics = [];
1511
+ const keyOf = (source, file) => {
1512
+ const dir = relative2(source.root, dirname(file));
1513
+ const [head, ...tail] = dir.split("/");
1514
+ const version = head && versionDirs.has(head) ? head : "";
1515
+ const afterVersion = version ? tail : [head ?? "", ...tail];
1516
+ const [localeHead, ...localeTail] = afterVersion;
1517
+ const locale = localeHead ? localeDirs.get(localeHead.toLowerCase()) ?? "" : "";
1518
+ const rest = (locale ? localeTail : afterVersion).filter(Boolean).join("/");
1519
+ return [version, locale, metaKeyFor(source.prefix, rest)].filter(Boolean).join("/");
1520
+ };
953
1521
  const perSource = await Promise.all(list.map(async (source) => {
954
- const files = await findFolderMetaFiles(source);
955
- const loaded = await Promise.all(files.map(async (file) => {
1522
+ const found = await globFolderMetaFiles(source, META_FILES);
1523
+ for (const file of found) {
1524
+ if (!isReadMetaFile(source, file)) {
1525
+ unread.push({
1526
+ dir: relative2(source.root, dirname(file)),
1527
+ file,
1528
+ key: keyOf(source, file)
1529
+ });
1530
+ }
1531
+ }
1532
+ const loaded = await Promise.all(found.filter((file) => isReadMetaFile(source, file)).map(async (file) => {
956
1533
  try {
957
1534
  return {
958
1535
  file,
@@ -967,14 +1544,7 @@ var discoverFolderMeta = async (sources, options = {}) => {
967
1544
  }));
968
1545
  for (const { loaded, source } of perSource) {
969
1546
  for (const entry of loaded) {
970
- const dir = relative2(source.root, dirname(entry.file));
971
- const [head, ...tail] = dir.split("/");
972
- const version = head && versionDirs.has(head) ? head : "";
973
- const afterVersion = version ? tail : [head ?? "", ...tail];
974
- const [localeHead, ...localeTail] = afterVersion;
975
- const locale = localeHead ? localeDirs.get(localeHead.toLowerCase()) ?? "" : "";
976
- const rest = (locale ? localeTail : afterVersion).filter(Boolean).join("/");
977
- const key = [version, locale, metaKeyFor(source.prefix, rest)].filter(Boolean).join("/");
1547
+ const key = keyOf(source, entry.file);
978
1548
  if (!entry.ok) {
979
1549
  diagnostics.push({
980
1550
  code: "BLUME_META_LOAD_FAILED",
@@ -988,6 +1558,7 @@ var discoverFolderMeta = async (sources, options = {}) => {
988
1558
  if (result.success) {
989
1559
  const target = basename(entry.file).startsWith("meta.$.") ? shared : meta;
990
1560
  target.set(key, result.data);
1561
+ files.set(result.data, entry.file);
991
1562
  } else {
992
1563
  diagnostics.push(...diagnosticsFromZod(result.error, {
993
1564
  code: "BLUME_META_INVALID",
@@ -996,7 +1567,32 @@ var discoverFolderMeta = async (sources, options = {}) => {
996
1567
  }
997
1568
  }
998
1569
  }
999
- return { diagnostics, meta, shared };
1570
+ const read = new Set(perSource.flatMap(({ loaded }) => loaded.map((entry) => entry.file)));
1571
+ return {
1572
+ diagnostics,
1573
+ files,
1574
+ meta,
1575
+ shared,
1576
+ unread: unread.filter((entry) => !read.has(entry.file))
1577
+ };
1578
+ };
1579
+ var withGeneratedFolderMeta = (user, generated, localeDirs = []) => {
1580
+ const locales = new Set(localeDirs);
1581
+ const beneath = (key, meta) => {
1582
+ const [head, ...rest] = key.split("/");
1583
+ const generatedMeta = generated.get(head !== undefined && locales.has(head) ? rest.join("/") : key);
1584
+ return generatedMeta ? { ...generatedMeta, ...meta } : meta;
1585
+ };
1586
+ return {
1587
+ meta: new Map([...user.meta].map(([key, meta]) => [key, beneath(key, meta)])),
1588
+ shared: new Map([
1589
+ ...generated,
1590
+ ...[...user.shared].map(([key, meta]) => [
1591
+ key,
1592
+ { ...generated.get(key), ...meta }
1593
+ ])
1594
+ ])
1595
+ };
1000
1596
  };
1001
1597
 
1002
1598
  // src/core/sources/resolve.ts
@@ -1390,7 +1986,7 @@ var contentfulSource = (options, ctx) => {
1390
1986
 
1391
1987
  // src/core/sources/filesystem.ts
1392
1988
  import { existsSync, watch as fsWatch } from "node:fs";
1393
- import { readFile } from "node:fs/promises";
1989
+ import { readFile as readFile3 } from "node:fs/promises";
1394
1990
  import { YAMLException } from "js-yaml";
1395
1991
  import { extname, isAbsolute, join as join2, relative as relative3, resolve } from "pathe";
1396
1992
  import { glob as glob2 } from "tinyglobby";
@@ -1415,7 +2011,7 @@ var filesystemSource = (options) => {
1415
2011
  }
1416
2012
  }
1417
2013
  const read = await Promise.all(loadable.map(async (file) => {
1418
- const source = await readFile(file, "utf-8");
2014
+ const source = await readFile3(file, "utf-8");
1419
2015
  const ext = extname(file).toLowerCase();
1420
2016
  const format = ext === ".mdx" ? "mdx" : "md";
1421
2017
  let parsed;
@@ -1474,7 +2070,7 @@ var filesystemSource = (options) => {
1474
2070
  load,
1475
2071
  name: options.name,
1476
2072
  prefix: options.prefix,
1477
- read: (ref) => readFile(join2(contentRoot, ref), "utf-8"),
2073
+ read: (ref) => readFile3(join2(contentRoot, ref), "utf-8"),
1478
2074
  staged: false,
1479
2075
  validate,
1480
2076
  watch
@@ -1735,19 +2331,13 @@ var includeMatcher = (include) => {
1735
2331
  return (ref) => included(ref) && !excluded(ref);
1736
2332
  };
1737
2333
  var GITHUB_HOSTS = new Set(["api.github.com", "raw.githubusercontent.com"]);
2334
+ var isGithubUrl = (url) => URL.canParse(url) && GITHUB_HOSTS.has(new URL(url).hostname);
1738
2335
  var githubHeaders2 = (url) => {
1739
2336
  const token = process.env.GITHUB_TOKEN;
1740
- if (!token) {
1741
- return {};
1742
- }
1743
- let host = "";
1744
- try {
1745
- host = new URL(url).hostname;
1746
- } catch {
1747
- return {};
1748
- }
1749
- return GITHUB_HOSTS.has(host) ? { authorization: `Bearer ${token}` } : {};
2337
+ return token && isGithubUrl(url) ? { authorization: `Bearer ${token}` } : {};
1750
2338
  };
2339
+ var TOKEN_STATUSES = new Set([401, 403, 404]);
2340
+ var statusReason = (url, status) => TOKEN_STATUSES.has(status) && !process.env.GITHUB_TOKEN && isGithubUrl(url) ? `${url} -> ${status}; GITHUB_TOKEN is not set, which a private repository needs` : `${url} -> ${status}`;
1751
2341
  var fetchText = async (url, doFetch) => {
1752
2342
  try {
1753
2343
  const res = await doFetch(url, {
@@ -1755,7 +2345,7 @@ var fetchText = async (url, doFetch) => {
1755
2345
  signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS)
1756
2346
  });
1757
2347
  if (!res.ok) {
1758
- throw new Error(`${url} -> ${res.status}`);
2348
+ throw new Error(statusReason(url, res.status));
1759
2349
  }
1760
2350
  return await res.text();
1761
2351
  } catch (error) {
@@ -1797,6 +2387,24 @@ var enumerateGithub = async (github, include, doFetch) => {
1797
2387
  truncated
1798
2388
  };
1799
2389
  };
2390
+ var ATX_HEADING = /^ {0,3}#/u;
2391
+ var withoutTitleHeading = (page) => {
2392
+ const lines = page.body.split(`
2393
+ `);
2394
+ const start = lines.findIndex((line) => line.trim() !== "");
2395
+ const span = ATX_HEADING.test(lines[start] ?? "") ? 1 : 2;
2396
+ const [heading] = start === -1 ? [] : extractHeadings(lines.slice(start, start + span).join(`
2397
+ `));
2398
+ if (heading?.depth !== 1 || heading.text === "" || page.data.title !== undefined && page.data.title !== heading.text) {
2399
+ return page;
2400
+ }
2401
+ lines.fill("", start, start + span);
2402
+ return {
2403
+ body: lines.join(`
2404
+ `),
2405
+ data: { ...page.data, title: heading.text }
2406
+ };
2407
+ };
1800
2408
  var mdxRemoteSource = (options, ctx) => {
1801
2409
  const doFetch = options.fetchImpl ?? globalThis.fetch;
1802
2410
  const cache = snapshotCache(ctx.cacheDir);
@@ -1824,12 +2432,16 @@ var mdxRemoteSource = (options, ctx) => {
1824
2432
  const text = await fetchText(item.fetchUrl, doFetch);
1825
2433
  const parsed = frontmatter_default(text);
1826
2434
  const format = item.ref.toLowerCase().endsWith(".mdx") ? "mdx" : "md";
2435
+ const page = withoutTitleHeading({
2436
+ body: parsed.content,
2437
+ data: parsed.data
2438
+ });
1827
2439
  return {
1828
- body: { format, text: parsed.content },
1829
- data: parsed.data,
2440
+ body: { format, text: page.body },
2441
+ data: page.data,
1830
2442
  editUrl: item.editUrl,
1831
2443
  hash: hashText(text),
1832
- raw: text,
2444
+ raw: text.slice(0, text.length - parsed.content.length) + page.body,
1833
2445
  ref: item.ref
1834
2446
  };
1835
2447
  };
@@ -2140,7 +2752,7 @@ var FRAME_ESCAPED = /\\(?=[<>])/gu;
2140
2752
  var YOUTUBE_HOST = /(?:^|\.)(?:youtube(?:-nocookie)?\.com|youtu\.be)$/u;
2141
2753
  var isYouTubeUrl = (url) => {
2142
2754
  const host = URL.parse(url)?.hostname ?? "";
2143
- return YOUTUBE_HOST.test(host) && parseYouTubeId(url) !== null;
2755
+ return YOUTUBE_HOST.test(host) && youtubeEmbedSrc(url) !== null;
2144
2756
  };
2145
2757
  var renderVideo = (data) => {
2146
2758
  const url = data.external?.url ?? data.file?.url;
@@ -2425,7 +3037,7 @@ ${nested}`;
2425
3037
 
2426
3038
  // src/core/sources/obsidian.ts
2427
3039
  import { existsSync as existsSync2, watch as fsWatch2, statSync } from "node:fs";
2428
- import { readdir as readdir2, readFile as readFile2, stat } from "node:fs/promises";
3040
+ import { readdir as readdir2, readFile as readFile4, stat } from "node:fs/promises";
2429
3041
  import { YAMLException as YAMLException2 } from "js-yaml";
2430
3042
  import { basename as basename2, isAbsolute as isAbsolute2, join as join5, relative as relative4, resolve as resolve2 } from "pathe";
2431
3043
  var MARKDOWN_FILE = /\.md$/iu;
@@ -2443,7 +3055,6 @@ var OBSIDIAN_NATIVE_KEYS = new Set([
2443
3055
  "tag",
2444
3056
  "tags"
2445
3057
  ]);
2446
- var PAGE_META_KEYS = new Set(pageMetaSchema.keyof().options);
2447
3058
  var entrySlugFor = (navPath) => slugifyPath(navPath.replace(MARKDOWN_FILE, ""));
2448
3059
  var noTargets = () => ({
2449
3060
  ambiguous: [],
@@ -2726,7 +3337,7 @@ var noteToEntry = (pair, index, options, unresolved) => {
2726
3337
  const { note } = pair;
2727
3338
  const title = titleFor(note.rel, note.data.title);
2728
3339
  const keep = keptKeysFor(note.data, options);
2729
- const data = Object.fromEntries(Object.entries(note.data).filter(([key]) => !OBSIDIAN_NATIVE_KEYS.has(key) && (PAGE_META_KEYS.has(key) || keep.has(key))));
3340
+ const data = Object.fromEntries(Object.entries(note.data).filter(([key]) => !OBSIDIAN_NATIVE_KEYS.has(key) && (BUILT_IN_PAGE_META_KEYS.has(key) || keep.has(key))));
2730
3341
  const merged = title === undefined ? data : { ...data, title };
2731
3342
  const body = note.content.trim();
2732
3343
  const text = transformBody(body, index, pair.self, unresolved);
@@ -2748,7 +3359,7 @@ var noteToEntry = (pair, index, options, unresolved) => {
2748
3359
  var readNote = async (vaultDir, rel, options) => {
2749
3360
  const absPath = join5(vaultDir, rel);
2750
3361
  try {
2751
- const file = await readFile2(absPath, "utf-8");
3362
+ const file = await readFile4(absPath, "utf-8");
2752
3363
  const { content, data } = frontmatter_default(file);
2753
3364
  return {
2754
3365
  absPath,
@@ -2877,7 +3488,7 @@ var obsidianSource = (options, ctx) => {
2877
3488
  suggestion: "Reference notes by their vault-relative path."
2878
3489
  });
2879
3490
  }
2880
- return await readFile2(absPath, "utf-8");
3491
+ return await readFile4(absPath, "utf-8");
2881
3492
  };
2882
3493
  const validate = () => {
2883
3494
  const stats = statSync(vaultDir, { throwIfNoEntry: false });
@@ -3593,6 +4204,284 @@ var resolveSources = (config, context, runtime) => {
3593
4204
  return sources;
3594
4205
  };
3595
4206
 
4207
+ // src/core/svg-images.ts
4208
+ import { readFile as readFile5 } from "node:fs/promises";
4209
+ import { imageMetadata } from "astro/assets/utils";
4210
+ import { dirname as dirname2, extname as extname3 } from "pathe";
4211
+ var isUnmeasurableSvg = async (path) => {
4212
+ if (extname3(path).toLowerCase() !== ".svg") {
4213
+ return false;
4214
+ }
4215
+ let data;
4216
+ try {
4217
+ data = await readFile5(path);
4218
+ } catch {
4219
+ return false;
4220
+ }
4221
+ try {
4222
+ await imageMetadata(data, path);
4223
+ return false;
4224
+ } catch {
4225
+ return true;
4226
+ }
4227
+ };
4228
+ var unmeasurableSvgDiagnostics = async (pages) => {
4229
+ const seen = new Set;
4230
+ const embeds = pages.flatMap((page) => {
4231
+ const { sourcePath } = page;
4232
+ if (!sourcePath) {
4233
+ return [];
4234
+ }
4235
+ return page.links.flatMap((link) => {
4236
+ const file = link.file ?? sourcePath;
4237
+ const key = `${file}:${link.line}:${link.column}`;
4238
+ if (!link.image || seen.has(key)) {
4239
+ return [];
4240
+ }
4241
+ seen.add(key);
4242
+ const abs = resolveRelativeImage(dirname2(sourcePath), link.target);
4243
+ return abs ? [{ abs, file, link }] : [];
4244
+ });
4245
+ });
4246
+ const found = await Promise.all(embeds.map(async (embed) => await isUnmeasurableSvg(embed.abs) ? [embed] : []));
4247
+ return found.flat().map(({ file, link }) => ({
4248
+ code: "BLUME_SVG_UNOPTIMIZED",
4249
+ column: link.column,
4250
+ file,
4251
+ line: link.line,
4252
+ message: `\`${link.target}\` is served as it is, not optimized: Astro can't read its size, which takes the whole \`<svg>\` tag within the file's first 1,000 bytes, with a width and height or a viewBox.`,
4253
+ severity: "warning",
4254
+ suggestion: "The image still shows. To have it optimized, put the `<svg>` tag's width and height (or viewBox) near the start of the file. In a draw.io export, the `content` attribute pushes them out: export without a copy of the diagram, or remove that attribute."
4255
+ }));
4256
+ };
4257
+
4258
+ // src/core/syntax-diagnostics.ts
4259
+ import { slug } from "github-slugger";
4260
+ import { markdownToMdast as markdownToMdast4 } from "satteri";
4261
+ var UNESCAPED = String.raw`(?<=(?:^|[^\\])(?:\\\\)*)`;
4262
+ var TEMPLATE_TAG = new RegExp(String.raw`${UNESCAPED}\{%[^\n]*?%\}`, "gu");
4263
+ var TEMPLATE_OUTPUT = new RegExp(String.raw`${UNESCAPED}\{\{(?<expression>[^\n{}]*)\}\}`, "gu");
4264
+ var VARIABLE_REFERENCE = /^\s*[\w-]+\s*$/u;
4265
+ var WIKILINK2 = /(?<![!\\[])\[\[(?<inner>[^[\]\n]+)\]\](?![([:])/gu;
4266
+ var MDC_BLOCK = /^[\t ]*(?<written>::(?<name>[a-z][\w-]*)(?:\[[^\]\n]*\])?(?:\{[^}\n]*\})?)[\t ]*$/gimu;
4267
+ var MDC_INLINE = /(?<![\w:\\])(?<written>:{1,2}(?<name>[a-z][\w-]*)(?:\[[^\]\n]*\](?:\{[^}\n]*\})?|\{[^}\n]*\}))/giu;
4268
+ var ATTRIBUTE_LIST = new RegExp(String.raw`(?<![=\\])\{:?[\t ]*${ATTRIBUTE}(?:[\t ]+${ATTRIBUTE})*[\t ]*\}`, "gu");
4269
+ var DIRECTIVE_ATTRIBUTES = /:+[a-z][\w-]*(?:\[[^\]\n]*\])?\{[^}\n]*\}/giu;
4270
+ var ATX_HEADING2 = /^ {0,3}#{1,6}[\t ]/u;
4271
+ var SETEXT_UNDERLINE = /^ {0,3}(?:=+|-+)[\t ]*$/u;
4272
+ var VOID_ELEMENT = new RegExp(String.raw`${UNESCAPED}<(?<tag>area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)(?=[\s/>])(?:[^<>"'/]|"[^"]*"|'[^']*'|\/(?!>))*>`, "gu");
4273
+ var SUSPECT_WIKILINK = /\[\[/u;
4274
+ var SUSPECT_MDC = /(?:^|[^\w:]):{1,2}[a-z][\w-]*[[{]|^[\t ]*::[a-z]/imu;
4275
+ var MDX_COMMENT = /\{\s*\/\*[\s\S]*?\*\/\s*\}/gu;
4276
+ var ANGLE_DESTINATION = /(?<=\]\([\t ]*|^ {0,3}\[[^\]\n]+\]:[\t ]*)<[^<>\n]*>/gmu;
4277
+ var blank = (text) => text.replaceAll(/[^\n]/gu, " ");
4278
+ var maskMarkdown = (text) => {
4279
+ const ranges = [];
4280
+ const walk = (node) => {
4281
+ const start = node.position?.start.offset ?? 0;
4282
+ const end = node.position?.end.offset ?? start;
4283
+ if (node.type === "code" || node.type === "inlineCode" || node.type === "html" && node.value.startsWith("<!--")) {
4284
+ ranges.push([start, end]);
4285
+ } else if ("children" in node) {
4286
+ for (const child of node.children) {
4287
+ walk(child);
4288
+ }
4289
+ }
4290
+ };
4291
+ walk(markdownToMdast4(text, {
4292
+ features: { ...MARKDOWN_BODY_FEATURES, gfm: true }
4293
+ }));
4294
+ let masked = "";
4295
+ let cursor = 0;
4296
+ for (const [start, end] of ranges) {
4297
+ masked += text.slice(cursor, start) + blank(text.slice(start, end));
4298
+ cursor = end;
4299
+ }
4300
+ return masked + text.slice(cursor);
4301
+ };
4302
+ var maskMdx = (text) => {
4303
+ let fence = null;
4304
+ return text.split(`
4305
+ `).map((line) => {
4306
+ const next = nextFenceState(line, fence);
4307
+ const inFence = fence !== null || next !== null;
4308
+ fence = next;
4309
+ return inFence ? blank(line) : line.replaceAll(INLINE_CODE, blank);
4310
+ }).join(`
4311
+ `).replaceAll(MDX_COMMENT, blank).replaceAll(HTML_COMMENT, blank).replaceAll(ANGLE_DESTINATION, blank);
4312
+ };
4313
+ var lineStartsOf = (text) => {
4314
+ const starts = [0];
4315
+ for (let index = text.indexOf(`
4316
+ `);index !== -1; index = text.indexOf(`
4317
+ `, index + 1)) {
4318
+ starts.push(index + 1);
4319
+ }
4320
+ return starts;
4321
+ };
4322
+ var siteOf = (starts, at) => {
4323
+ const line = starts.findLastIndex((start) => start <= at);
4324
+ return { column: at - (starts[line] ?? 0) + 1, line: line + 1 };
4325
+ };
4326
+ var templateFindings = ({ starts, text }) => [
4327
+ ...[...text.matchAll(TEMPLATE_TAG)].map((match) => ({
4328
+ ...siteOf(starts, match.index),
4329
+ code: "BLUME_TEMPLATE_TAG",
4330
+ message: `\`${match[0]}\` is a Liquid or Markdoc tag, which Blume doesn't run, so the page shows it as written.`,
4331
+ suggestion: "Rewrite what it produced as Markdown or a Blume component, or remove it. To show it as text, put it in inline code."
4332
+ })),
4333
+ ...[...text.matchAll(TEMPLATE_OUTPUT)].filter((match) => !VARIABLE_REFERENCE.test(match.groups?.expression ?? "")).map((match) => ({
4334
+ ...siteOf(starts, match.index),
4335
+ code: "BLUME_TEMPLATE_TAG",
4336
+ message: `\`${match[0]}\` is a Liquid output tag, which Blume doesn't fill in, so the page shows it as written.`,
4337
+ suggestion: "Write the value in its place, or define it under `variables` in blume.config.ts and write `{{name}}`. To show it as text, put it in inline code."
4338
+ }))
4339
+ ];
4340
+ var wikiKey = (name) => name.trim().toLowerCase().replaceAll(/[\s_-]+/gu, "-");
4341
+ var indexPageNames = (pages) => {
4342
+ const index = new Map;
4343
+ for (const page of pages) {
4344
+ const stem = (page.source.ref.split("/").at(-1) ?? "").replace(/\.mdx?$/iu, "");
4345
+ for (const key of new Set([wikiKey(stem), wikiKey(page.title)])) {
4346
+ const named = index.get(key);
4347
+ if (named) {
4348
+ named.push(page);
4349
+ } else {
4350
+ index.set(key, [page]);
4351
+ }
4352
+ }
4353
+ }
4354
+ return index;
4355
+ };
4356
+ var partRoute = (text, pageRoute) => {
4357
+ const [name = "", heading] = text.split("#", 2);
4358
+ const route = pageRoute(name);
4359
+ return route && heading ? `${route}#${slug(heading)}` : route;
4360
+ };
4361
+ var wikilinkFindings = ({ pageRoute, starts, text }) => [...text.matchAll(WIKILINK2)].flatMap((match) => {
4362
+ const parts = (match.groups?.inner ?? "").split("|").map((part) => ({
4363
+ route: partRoute(part, pageRoute),
4364
+ text: part.trim()
4365
+ }));
4366
+ const target = parts.find((part) => part.route !== undefined);
4367
+ if (!target && parts.length === 1) {
4368
+ return [];
4369
+ }
4370
+ const label = parts.find((part) => part !== target) ?? target;
4371
+ return [
4372
+ {
4373
+ ...siteOf(starts, match.index),
4374
+ code: "BLUME_WIKILINK_UNSUPPORTED",
4375
+ message: `\`${match[0]}\` is a wiki link, which Blume reads only in an Obsidian vault source, so the page shows it as written${target ? ` instead of linking to ${target.route}` : ""}.`,
4376
+ suggestion: target ? `Rewrite it as a Markdown link: \`[${label?.text}](${target.route})\`.` : "Rewrite it as a Markdown link to the page, like `[Text](./page.md)`."
4377
+ }
4378
+ ];
4379
+ });
4380
+ var mdcFindings = ({ starts, text }) => {
4381
+ const blocks = [...text.matchAll(MDC_BLOCK)].map((match) => ({
4382
+ at: match.index + match[0].indexOf("::"),
4383
+ written: match.groups?.written
4384
+ }));
4385
+ const opened = new Set(blocks.map(({ at }) => at));
4386
+ return [
4387
+ ...blocks.map(({ at, written }) => ({
4388
+ ...siteOf(starts, at),
4389
+ code: "BLUME_MDC_SYNTAX",
4390
+ message: `\`${written}\` opens a Nuxt Content (MDC) block component, which Blume doesn't render, so the page shows its \`::\` lines as text.`,
4391
+ suggestion: "Rewrite the block as the matching Blume component (`<Callout>`, `<Card>`, `<Tabs>`, …) or a `:::` callout."
4392
+ })),
4393
+ ...[...text.matchAll(MDC_INLINE)].filter((match) => !opened.has(match.index)).map((match) => ({
4394
+ ...siteOf(starts, match.index),
4395
+ code: "BLUME_MDC_SYNTAX",
4396
+ message: `\`${match.groups?.written}\` is an inline component in Nuxt Content (MDC) syntax, which Blume doesn't render, so the page shows it as written.`,
4397
+ suggestion: "Rewrite it as the matching Blume component (`<Badge>`, `<Icon>`, …) or as plain text."
4398
+ }))
4399
+ ];
4400
+ };
4401
+ var isHeadingMarker = (lines, site, written, format) => {
4402
+ const line = lines[site.line - 1] ?? "";
4403
+ const heading = ATX_HEADING2.test(line) || SETEXT_UNDERLINE.test(lines[site.line] ?? "");
4404
+ const marker = trailingCurlyMarker(line);
4405
+ return heading && marker?.marker === written && marker.index === site.column - 1 && (format === "md" || !marker.attributes);
4406
+ };
4407
+ var ATTRIBUTE_LIST_PROBLEMS = {
4408
+ md: {
4409
+ code: "BLUME_MD_ATTRIBUTE_LIST",
4410
+ effect: "which Blume doesn't read, so the page shows it as written",
4411
+ suggestion: 'Remove it. To keep the attributes, write the element as HTML (`<img src="…" width="300">`), or pin a heading\'s anchor with `[#id]`. To show the braces as text, put them in inline code.'
4412
+ },
4413
+ mdx: {
4414
+ code: "BLUME_MDX_ATTRIBUTE_LIST",
4415
+ effect: "which MDX reads as a JavaScript expression, so the page fails to build",
4416
+ suggestion: 'Remove it. To keep the attributes, write the element as JSX (`<img src="…" width="300" />`), or pin a heading\'s anchor with `[#id]`. To show the braces as text, escape them: `\\{…\\}`.'
4417
+ }
4418
+ };
4419
+ var attributeListFindings = (format) => ({ starts, text }) => {
4420
+ const { code, effect, suggestion } = ATTRIBUTE_LIST_PROBLEMS[format];
4421
+ const scanned = text.replaceAll(DIRECTIVE_ATTRIBUTES, blank).replaceAll(TEMPLATE_OUTPUT, blank);
4422
+ const lines = text.split(`
4423
+ `);
4424
+ return [...scanned.matchAll(ATTRIBUTE_LIST)].map((match) => ({
4425
+ site: siteOf(starts, match.index),
4426
+ written: match[0]
4427
+ })).filter(({ site, written }) => !isHeadingMarker(lines, site, written, format)).map(({ site, written }) => ({
4428
+ ...site,
4429
+ code,
4430
+ message: `\`${written}\` is an attribute list, ${effect}.`,
4431
+ suggestion
4432
+ }));
4433
+ };
4434
+ var SUSPECT_ATTRIBUTE_LIST = /\{:?[\t ]*(?:[#.][A-Za-z_-]|[\w-]+=)/u;
4435
+ var voidElementFindings = ({ starts, text }) => [...text.matchAll(VOID_ELEMENT)].filter((match) => !text.slice(match.index + match[0].length).trimStart().startsWith(`</${match.groups?.tag}>`)).map((match) => ({
4436
+ ...siteOf(starts, match.index),
4437
+ code: "BLUME_MDX_UNCLOSED_ELEMENT",
4438
+ message: `\`<${match.groups?.tag}>\` isn't closed. MDX reads HTML as JSX, where it needs \`/>\`: unclosed, it fails the page, or in an included file it takes in everything after it.`,
4439
+ suggestion: `Close it: \`<${match.groups?.tag} … />\`.`
4440
+ }));
4441
+ var CHECKS = {
4442
+ md: [
4443
+ { find: templateFindings, suspect: /\{[%{]/u },
4444
+ { find: wikilinkFindings, suspect: SUSPECT_WIKILINK },
4445
+ { find: mdcFindings, suspect: SUSPECT_MDC },
4446
+ { find: attributeListFindings("md"), suspect: SUSPECT_ATTRIBUTE_LIST }
4447
+ ],
4448
+ mdx: [
4449
+ { find: wikilinkFindings, suspect: SUSPECT_WIKILINK },
4450
+ { find: mdcFindings, suspect: SUSPECT_MDC },
4451
+ { find: attributeListFindings("mdx"), suspect: SUSPECT_ATTRIBUTE_LIST },
4452
+ {
4453
+ find: voidElementFindings,
4454
+ suspect: /<(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)[\s/>]/u
4455
+ }
4456
+ ]
4457
+ };
4458
+ var syntaxFindings = (text, format, pageRoute) => {
4459
+ const checks = CHECKS[format].filter((check) => check.suspect.test(text));
4460
+ if (checks.length === 0) {
4461
+ return [];
4462
+ }
4463
+ const masked = format === "mdx" ? maskMdx(text) : maskMarkdown(text);
4464
+ const scan = { pageRoute, starts: lineStartsOf(masked), text: masked };
4465
+ return checks.flatMap((check) => check.find(scan)).toSorted((a, b) => a.line - b.line || a.column - b.column);
4466
+ };
4467
+ var syntaxDiagnostics = (entry, sourceName, pages = new Map, locale = "") => {
4468
+ const page = entry.sourcePath ?? `${sourceName}:${entry.ref}`;
4469
+ const offset = entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
4470
+ const pageRoute = (name) => {
4471
+ const named = pages.get(wikiKey(name));
4472
+ return (named?.find((other) => other.locale === locale) ?? named?.[0])?.route;
4473
+ };
4474
+ return syntaxFindings(entry.expanded?.text ?? entry.body.text, entry.body.format, pageRoute).map(({ line, ...finding }) => {
4475
+ const origin = entry.expanded?.origins[line - 1];
4476
+ return {
4477
+ ...finding,
4478
+ file: origin?.file ?? page,
4479
+ line: origin?.line ?? line + offset,
4480
+ severity: "warning"
4481
+ };
4482
+ });
4483
+ };
4484
+
3596
4485
  // src/core/project-graph.ts
3597
4486
  var isWithin = (dir, path) => {
3598
4487
  const rel = relative5(dir, path);
@@ -3631,16 +4520,25 @@ var entryIdDiagnostics = (pages, collectionBase) => {
3631
4520
  }
3632
4521
  return diagnostics;
3633
4522
  };
3634
- var normalizeLoadedEntries = (loaded, config) => {
4523
+ var normalizeLoadedEntries = (loaded, config, dropUnparsable) => {
3635
4524
  const frontmatterExtend = Object.keys(config.frontmatter.extend).length > 0 ? config.frontmatter.extend : undefined;
3636
4525
  const declaredTypes = Object.entries(config.content.types).filter(([, type]) => Object.keys(type.frontmatter).length > 0);
3637
4526
  const typeFrontmatter = declaredTypes.length > 0 ? Object.fromEntries(declaredTypes.map(([name, type]) => [name, type.frontmatter])) : undefined;
3638
4527
  const pages = [];
3639
4528
  const allDiagnostics = [];
4529
+ const syntaxErrors = [];
4530
+ const unparsable = [];
4531
+ const published = [];
3640
4532
  let droppedPages = 0;
3641
4533
  for (const { source, entries, diagnostics } of loaded) {
3642
4534
  allDiagnostics.push(...diagnostics);
3643
4535
  for (const entry of entries) {
4536
+ const syntax = mdxSyntaxCheck(entry, source.name, config.variables);
4537
+ syntaxErrors.push(...syntax.diagnostics);
4538
+ const drop = syntax.unparsable && dropUnparsable;
4539
+ if (drop && entry.sourcePath && !source.staged) {
4540
+ unparsable.push(entry.sourcePath);
4541
+ }
3644
4542
  const normalized = normalizeEntry(entry, {
3645
4543
  basePath: config.basePath,
3646
4544
  defaultType: config.content.defaultType,
@@ -3656,17 +4554,27 @@ var normalizeLoadedEntries = (loaded, config) => {
3656
4554
  typeFrontmatter,
3657
4555
  versions: config.versions
3658
4556
  });
3659
- if (normalized.pages.length === 0 && normalized.diagnostics.length > 0) {
4557
+ const [page] = normalized.pages;
4558
+ if (page ? drop : normalized.diagnostics.length > 0) {
3660
4559
  droppedPages += 1;
3661
4560
  }
3662
- pages.push(...normalized.pages);
4561
+ if (!drop) {
4562
+ pages.push(...normalized.pages);
4563
+ }
3663
4564
  allDiagnostics.push(...normalized.diagnostics);
3664
- if (normalized.pages.length > 0) {
3665
- allDiagnostics.push(...unknownDirectiveDiagnostics(entry, source.name));
4565
+ if (page) {
4566
+ allDiagnostics.push(...directiveDiagnostics(entry, source.name), ...codeFenceDiagnostics(entry, source.name), ...alertDiagnostics(entry, source.name));
4567
+ published.push({ entry, locale: page.locale, source: source.name });
3666
4568
  }
3667
4569
  }
3668
4570
  }
3669
- return { diagnostics: allDiagnostics, droppedPages, pages };
4571
+ const names = indexPageNames(pages);
4572
+ for (const { entry, locale, source } of published) {
4573
+ allDiagnostics.push(...syntaxDiagnostics(entry, source, names, locale));
4574
+ }
4575
+ const covered = (diagnostic) => allDiagnostics.some((other) => other.file === diagnostic.file && other.line === diagnostic.line && (other.severity === "error" || other.severity === "warning" && diagnostic.severity === "warning"));
4576
+ allDiagnostics.push(...syntaxErrors.filter((error) => !covered(error)));
4577
+ return { diagnostics: allDiagnostics, droppedPages, pages, unparsable };
3670
4578
  };
3671
4579
  var gitContentRoots = (sources, gitRoot) => {
3672
4580
  if (gitRoot === null) {
@@ -3701,19 +4609,31 @@ var substituteEntryVariables = (entry, variables) => {
3701
4609
  return diagnostics;
3702
4610
  };
3703
4611
  var substituteLoadedVariables = (loaded, variables) => hasVariables(variables) ? loaded.flatMap(({ entries }) => entries.flatMap((entry) => substituteEntryVariables(entry, variables))) : [];
3704
- var redirectPageDiagnostics = (config, pages, configFile) => config.redirects.flatMap((redirect) => {
4612
+ var redirectPageDiagnostics = (config, pages, mirrored, configFile) => config.redirects.flatMap((redirect) => {
3705
4613
  const from = withBasePath(config.basePath, redirect.from);
3706
4614
  if (!isPatternPath(redirect.from)) {
3707
4615
  const page = normalizePath(from);
3708
- return pages.includes(page) ? [
4616
+ if (pages.includes(page)) {
4617
+ return [
4618
+ {
4619
+ code: "BLUME_REDIRECT_MATCHES_PAGE",
4620
+ file: configFile ?? undefined,
4621
+ message: `The redirect from ${redirect.from} is also the page ${page}, which never publishes: its URL redirects to ${redirect.to} instead.`,
4622
+ severity: "warning",
4623
+ suggestion: "If the page moved, delete it or give it a new slug; otherwise remove the redirect."
4624
+ }
4625
+ ];
4626
+ }
4627
+ const owner = mirroredPage(mirrored, page);
4628
+ return owner === undefined ? [] : [
3709
4629
  {
3710
4630
  code: "BLUME_REDIRECT_MATCHES_PAGE",
3711
4631
  file: configFile ?? undefined,
3712
- message: `The redirect from ${redirect.from} is also the page ${page}, which never publishes: its URL redirects to ${redirect.to} instead.`,
4632
+ message: `The redirect from ${redirect.from} is also the Markdown copy of the page ${owner}, so that copy never publishes: its URL redirects to ${redirect.to} instead.`,
3713
4633
  severity: "warning",
3714
- suggestion: "If the page moved, delete it or give it a new slug; otherwise remove the redirect."
4634
+ suggestion: "Remove the redirect: that URL serves the page's Markdown to agents."
3715
4635
  }
3716
- ] : [];
4636
+ ];
3717
4637
  }
3718
4638
  const matched = pathsUnderPattern(from, pages);
3719
4639
  const [first] = matched;
@@ -3731,6 +4651,43 @@ var redirectPageDiagnostics = (config, pages, configFile) => config.redirects.fl
3731
4651
  }
3732
4652
  ];
3733
4653
  });
4654
+ var withSourceRedirects = (config, loaded, pages) => {
4655
+ const requested = loaded.flatMap(({ redirects }) => redirects ?? []);
4656
+ if (requested.length === 0) {
4657
+ return config;
4658
+ }
4659
+ const served = (route) => normalizePath(withBasePath(config.basePath, route));
4660
+ const prefixes = [
4661
+ "",
4662
+ ...config.i18n?.locales.map((locale) => `/${locale.code}`) ?? []
4663
+ ];
4664
+ const taken = new Set(config.redirects.map((redirect) => served(redirect.from)));
4665
+ const added = [];
4666
+ for (const redirect of requested) {
4667
+ for (const prefix of prefixes) {
4668
+ const from = `${prefix}${redirect.from}`;
4669
+ const to = `${prefix}${redirect.to}`;
4670
+ if (!pages.has(served(to)) || pages.has(served(from)) || taken.has(served(from))) {
4671
+ continue;
4672
+ }
4673
+ taken.add(served(from));
4674
+ added.push({ from, status: 301, to });
4675
+ }
4676
+ }
4677
+ return added.length > 0 ? { ...config, redirects: [...config.redirects, ...added] } : config;
4678
+ };
4679
+ var publicOpenApiDiagnostics = (config, root) => {
4680
+ const file = join8(root, "public", "openapi.json");
4681
+ return config.agents.api && existsSync3(file) ? [
4682
+ {
4683
+ code: "BLUME_PUBLIC_OPENAPI_JSON",
4684
+ file,
4685
+ message: "public/openapi.json is served at /openapi.json, where Blume publishes the OpenAPI description of the site's JSON docs API, so that description isn't generated, and /.well-known/api-catalog still lists /openapi.json as the docs API's description.",
4686
+ severity: "warning",
4687
+ suggestion: "Move the file to another path, like public/specs/openapi.json, and update what links to it. A spec an openapi() reference renders can live outside public/ altogether. Or set agents.api to false if the site shouldn't publish the JSON docs API."
4688
+ }
4689
+ ] : [];
4690
+ };
3734
4691
  var bannerLinkHref = (banner) => banner === undefined || isBannerShorthand(banner) ? undefined : banner.link?.href;
3735
4692
  var validatedSources = (config, context, runtime, beforeSources) => {
3736
4693
  beforeSources?.(config);
@@ -3786,15 +4743,17 @@ var scanProject = async (root, options = {}) => {
3786
4743
  });
3787
4744
  }));
3788
4745
  const variableDiagnostics = substituteLoadedVariables(loaded, config.variables);
3789
- const sharedFolderMeta = new Map([
3790
- ...loaded.flatMap(({ folderMeta: sourceMeta }) => Object.entries(sourceMeta ?? {})),
3791
- ...folderMeta.shared
3792
- ]);
4746
+ const mergedFolderMeta = withGeneratedFolderMeta(folderMeta, new Map(loaded.flatMap(({ folderMeta: sourceMeta }) => Object.entries(sourceMeta ?? {}))), localeDirs);
3793
4747
  const {
3794
4748
  diagnostics: contentDiagnostics,
3795
4749
  droppedPages,
3796
- pages: allPages
3797
- } = normalizeLoadedEntries(loaded, config);
4750
+ pages: allPages,
4751
+ unparsable
4752
+ } = normalizeLoadedEntries(loaded, config, mode === "build");
4753
+ const metaDiagnostics = await folderMetaDiagnostics(folderMeta, allPages, {
4754
+ localeDirs,
4755
+ versionDirs: config.versions?.archived.map((version) => version.id)
4756
+ });
3798
4757
  const pages = mode === "build" && !preview ? allPages.filter((page) => !page.meta.draft) : allPages;
3799
4758
  const lastModified = resolveLastModifiedConfig(config.lastModified);
3800
4759
  const lastModifiedWarnings = [];
@@ -3821,40 +4780,54 @@ var scanProject = async (root, options = {}) => {
3821
4780
  basePath: config.basePath,
3822
4781
  brandHref: logoHref(config.logo),
3823
4782
  extraRoutes,
3824
- folderMeta: folderMeta.meta,
4783
+ folderMeta: mergedFolderMeta.meta,
3825
4784
  i18n: config.i18n,
3826
4785
  navigation: config.navigation,
3827
- sharedFolderMeta,
4786
+ sharedFolderMeta: mergedFolderMeta.shared,
3828
4787
  versions: config.versions
3829
4788
  });
3830
4789
  const manifest = buildManifest({ config, context, graph });
3831
4790
  const i18nWarnings = config.i18n ? i18nDiagnostics(pages, config.i18n, config.versions) : [];
4791
+ const svgWarnings = await unmeasurableSvgDiagnostics(pages);
3832
4792
  const versionWarnings = config.versions ? versionsDiagnostics(pages, config.versions) : [];
4793
+ const servedPaths = [
4794
+ ...manifest.routes.map((route) => route.path),
4795
+ ...extraRoutes
4796
+ ];
4797
+ const redirects = [
4798
+ ...config.redirects,
4799
+ ...formerRouteRedirects(pages, new Set(servedPaths), config)
4800
+ ];
3833
4801
  return {
3834
- config,
4802
+ config: withSourceRedirects(redirects.length === config.redirects.length ? config : { ...config, redirects }, loaded, new Set([...routeSetFor(manifest.routes), ...extraRoutes])),
3835
4803
  context,
3836
4804
  diagnostics: [
4805
+ ...configResult.diagnostics,
3837
4806
  ...contentDiagnostics,
3838
4807
  ...includeDiagnostics,
3839
4808
  ...variableDiagnostics,
3840
4809
  ...folderMeta.diagnostics,
4810
+ ...metaDiagnostics,
3841
4811
  ...entryIdDiagnostics(pages, resolveDocsCollection(config, context.root).base),
3842
4812
  ...graph.diagnostics,
3843
4813
  ...i18nWarnings,
3844
4814
  ...versionWarnings,
3845
4815
  ...lastModifiedWarnings,
3846
- ...redirectPageDiagnostics(config, [...manifest.routes.map((route) => route.path), ...extraRoutes], context.configFile)
4816
+ ...svgWarnings,
4817
+ ...publicOpenApiDiagnostics(config, context.root),
4818
+ ...redirectPageDiagnostics(config, servedPaths, routeSetFor(manifest.routes), context.configFile)
3847
4819
  ],
3848
4820
  droppedPages,
3849
4821
  graph,
3850
4822
  manifest,
3851
4823
  mode,
3852
4824
  sources,
3853
- themeFontsConfigured: configResult.themeFontsConfigured
4825
+ themeFontsConfigured: configResult.themeFontsConfigured,
4826
+ unparsable
3854
4827
  };
3855
4828
  };
3856
4829
 
3857
- export { directiveSource, validateSearchPopularIcons, validateNavTargets, contentIndexable, buildManifest, findFolderMetaFiles, isWithin, scanProject };
4830
+ export { directiveSource, normalizeFence, QUOTED_ATTR, metaTokens, RESERVED_META_KEYWORDS, isForeignKeyword, isLineRange, validateSearchPopularIcons, validateNavTargets, gitEnv, realPath, gitRepositoryRoot, locateMdxFailure, findFolderMetaFiles, isWithin, scanProject };
3858
4831
 
3859
- //# debugId=D6EC9010B970FDF164756E2164756E21
3860
- //# sourceMappingURL=chunk-0pmb4tr4.js.map
4832
+ //# debugId=358E42903E85F0E564756E2164756E21
4833
+ //# sourceMappingURL=chunk-8f8xw4x2.js.map