blume 2.1.3 → 2.2.1

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