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
@@ -0,0 +1,1511 @@
1
+ #!/usr/bin/env node
2
+ // vitepress-codemod.mjs — the mechanical part of a VitePress → Blume content
3
+ // migration. Point it at the VitePress source directory (`srcDir`, which
4
+ // becomes Blume's `content.root`). It rewrites page bodies only where the
5
+ // mapping is exact, and reports everything it leaves for review:
6
+ //
7
+ // - `::: tip` / `::: tip Title` → `:::tip` / `:::tip[Title]`, paired the way
8
+ // markdown-it-container pairs them (a bare `:::` closes the outermost open
9
+ // container whose fence is no longer than it, and everything nested in
10
+ // it; an unclosed container runs to the end of its parent). Nested
11
+ // callouts get the longer outer fence Blume needs.
12
+ // - `::: code-group` → `<CodeGroup>` (a code group ends at its last code
13
+ // block when its closer is missing), `::: details` → `<Expandable>`,
14
+ // `::: raw` / `::: v-pre` wrappers removed.
15
+ // - GitHub alerts (`> [!NOTE]`) → directives, when the blockquote is
16
+ // unambiguous. (Blume renders GitHub's own five in `.mdx` as written, but
17
+ // not VitePress's `[!INFO]` and `[!DANGER]`, nor any alert in `.md`.)
18
+ // - Fence meta: `[label]` → a title, `js{1,3}` → `js {1,3}` (Blume reads
19
+ // a lone `[label]` and `js{1,3}` as written too; those two only normalize),
20
+ // `:line-numbers` → `lineNumbers`, and a title for every block in a code
21
+ // group (VitePress labels an unlabeled one with its language).
22
+ // - `<<< @/file [Label]` snippet imports and `<!--@include: ./part.md-->`
23
+ // → `<include>`, when there's no region or line range and the file is
24
+ // inside the source directory.
25
+ // - Links: `/x.html`, `/x.md`, `/x/index.html`, `/x/` → `/x`, and relative
26
+ // `.html` links → their route, when a page backs the route.
27
+ // - `<Badge type text />` → `<Badge variant>text</Badge>` (a badge in a
28
+ // heading also pins the heading's VitePress anchor), `{#id}` → `[#id]`,
29
+ // `[[toc]]` removed, and the body H1 → frontmatter `title`.
30
+ // - A page that now uses MDX-only syntax is renamed `.md` → `.mdx`, with
31
+ // its HTML comments and `<br>` made MDX-safe. What still can't compile as
32
+ // MDX (Vue syntax, `{{ }}`, unclosed tags, autolinks) is reported.
33
+ //
34
+ // Design constraints (shared with mintlify-codemod.mjs):
35
+ // - ZERO dependencies: runs with a bare `node`.
36
+ // - Deterministic: files are processed in sorted order.
37
+ // - Idempotent: a second run changes nothing.
38
+ // - Reports every change and every finding, so nothing is silent.
39
+ //
40
+ // Usage:
41
+ // node vitepress-codemod.mjs <srcDir> # dry run, report only
42
+ // node vitepress-codemod.mjs --write <srcDir> # apply in place
43
+ // node vitepress-codemod.mjs --json <srcDir> # machine-readable report
44
+ // node vitepress-codemod.mjs --redirects <distDir> # print `.html` redirects
45
+ //
46
+ // `--redirects` reads a VitePress build output (`.vitepress/dist`) and prints
47
+ // one `{ from: "/x.html", to: "/x" }` per built page (index pages and 404
48
+ // excluded), for `redirects` in blume.config.ts.
49
+
50
+ import {
51
+ existsSync,
52
+ readdirSync,
53
+ readFileSync,
54
+ renameSync,
55
+ statSync,
56
+ writeFileSync,
57
+ } from "node:fs";
58
+ import path from "node:path";
59
+
60
+ // --- Tables -----------------------------------------------------------------
61
+
62
+ // VitePress container (and alert) names → Blume callout types. Blume's own
63
+ // names map to themselves, so a second pass over converted output is a no-op.
64
+ const CALLOUTS = new Map([
65
+ ["caution", "danger"],
66
+ ["danger", "danger"],
67
+ ["error", "danger"],
68
+ ["important", "note"],
69
+ ["info", "info"],
70
+ ["note", "note"],
71
+ ["success", "success"],
72
+ ["tip", "tip"],
73
+ ["warn", "warning"],
74
+ ["warning", "warning"],
75
+ ]);
76
+
77
+ // Containers VitePress 1.x doesn't register (2.0 does): 1.x showed them as text.
78
+ const NEW_IN_2 = new Set(["caution", "important", "note"]);
79
+
80
+ const ALERTS = new Map([
81
+ ["caution", "danger"],
82
+ ["danger", "danger"],
83
+ ["important", "note"],
84
+ ["info", "info"],
85
+ ["note", "note"],
86
+ ["tip", "tip"],
87
+ ["warning", "warning"],
88
+ ]);
89
+
90
+ // The alerts GitHub defines, which Blume itself renders as callouts in `.mdx`
91
+ // (a `.md` page shows them as quotes, with a BLUME_MD_GITHUB_ALERT warning).
92
+ const GITHUB_ALERTS = new Set([
93
+ "caution",
94
+ "important",
95
+ "note",
96
+ "tip",
97
+ "warning",
98
+ ]);
99
+
100
+ // VitePress `<Badge type>` → Blume `variant` (`default` is Blume's default).
101
+ const BADGE_VARIANTS = new Map([
102
+ ["caution", "danger"],
103
+ ["danger", "danger"],
104
+ ["important", "accent"],
105
+ ["info", "default"],
106
+ ["note", "default"],
107
+ ["tip", "accent"],
108
+ ["warning", "warning"],
109
+ ]);
110
+
111
+ // Fence keywords Blume reads; never usable as a bare title.
112
+ const RESERVED_META = new Set([
113
+ "expandable",
114
+ "lineNumbers",
115
+ "ts2js",
116
+ "twoslash",
117
+ "wrap",
118
+ ]);
119
+
120
+ // Blume's page frontmatter keys (its schema is strict: any other key fails the
121
+ // build). Reported, not rewritten: mapping them takes judgment.
122
+ const BLUME_KEYS = new Set([
123
+ "ai",
124
+ "api",
125
+ "authMethod",
126
+ "authors",
127
+ "changelog",
128
+ "date",
129
+ "deprecated",
130
+ "description",
131
+ "draft",
132
+ "hidden",
133
+ "icon",
134
+ "lastModified",
135
+ "mode",
136
+ "narration",
137
+ "noindex",
138
+ "pagination",
139
+ "playground",
140
+ "related",
141
+ "search",
142
+ "seo",
143
+ "sidebar",
144
+ "slug",
145
+ "title",
146
+ "type",
147
+ ]);
148
+
149
+ // Components this codemod writes itself; every other capitalized tag is
150
+ // reported for review.
151
+ const PRODUCED = new Set(["Badge", "CodeGroup", "Expandable"]);
152
+
153
+ // --- Patterns -----------------------------------------------------------------
154
+
155
+ const FENCE_RE = /^(?<indent>\s*)(?<marker>`{3,}|~{3,})(?<info>.*)$/u;
156
+ const OPEN_RE =
157
+ /^(?<indent>\s*)(?<colons>:{3,})\s*(?<name>[A-Za-z][\w-]*)(?<rest>.*)$/u;
158
+ const CLOSE_RE = /^\s*(?<colons>:{3,})\s*$/u;
159
+ const SNIPPET_RE = /^(?<indent>\s*)<<<\s*(?<raw>\S.*?)\s*$/u;
160
+ // VitePress 1.6's snippet grammar (src/node/markdown/plugins/snippet.ts).
161
+ const RAW_PATH_RE =
162
+ /^(?<file>.+?(?:\.[a-z0-9]+)?)(?<region>#[\w-]+)?(?: ?\{(?<lines>\d+(?:[,-]\d+)*)? ?(?<lang>\S+)? ?(?<attrs>\S+)?\})? ?(?:\[(?<title>.+)\])?$/u;
163
+ const INCLUDE_LINE_RE =
164
+ /^(?<indent>\s*)<!--\s*@include:\s*(?<target>.*?)\s*-->\s*$/u;
165
+ const INCLUDE_ANY_RE = /<!--\s*@include:/u;
166
+ const BLUME_INCLUDE_RE =
167
+ /^\s*<include(?<attrs>[^>]*)>\s*(?<target>[^<\s]+)\s*<\/include>\s*$/u;
168
+ const ALERT_RE = /^>\s?\[!(?<type>[A-Za-z]+)\](?<title>[^\n]*)$/u;
169
+ const HEADING_RE = /^(?<hashes>#{1,6})\s+(?<text>.*?)\s*$/u;
170
+ const CURLY_ID_RE = /^(?<head>#{1,6}\s.*?)\s*\{#(?<id>[\w-]+)\}\s*$/u;
171
+ const PIN_RE = /\[#[\w-]+\]\s*$/u;
172
+ const LINE_NUMBERS_RE = /:(?<no>no-)?line-numbers(?:=(?<start>\d*))?/u;
173
+
174
+ // --- Small helpers ---------------------------------------------------------------
175
+
176
+ const isFenceClose = (text, fence) => {
177
+ const m = /^\s*(?<marker>`{3,}|~{3,})\s*$/u.exec(text);
178
+ return (
179
+ m !== null &&
180
+ m.groups.marker[0] === fence.char &&
181
+ m.groups.marker.length >= fence.length
182
+ );
183
+ };
184
+
185
+ // Apply `fn` to the text outside inline code spans.
186
+ const outsideCode = (text, fn) =>
187
+ text
188
+ .split(/(?<code>`+[^`]*`+)/u)
189
+ .map((part, i) => (i % 2 === 1 ? part : fn(part)))
190
+ .join("");
191
+
192
+ // The text with its inline code spans removed.
193
+ const stripCode = (text) => text.replaceAll(/`+[^`]*`+/gu, "");
194
+
195
+ // VitePress's heading slugify (@mdit-vue/shared), for pinning old anchors.
196
+ const vitepressSlug = (text) =>
197
+ text
198
+ .normalize("NFKD")
199
+ .replaceAll(/[\u0300-\u036F]/gu, "")
200
+ .replaceAll(/[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/gu, "-")
201
+ .replaceAll(/-{2,}/gu, "-")
202
+ .replaceAll(/^-+|-+$/gu, "")
203
+ .replace(/^(?<digit>\d)/u, "_$<digit>")
204
+ .toLowerCase();
205
+
206
+ /** `text` without `pattern`'s matches, removed again until none is left. */
207
+ const removeAll = (text, pattern) => {
208
+ let out = text;
209
+ let previous;
210
+ do {
211
+ previous = out;
212
+ out = out.replaceAll(pattern, "");
213
+ } while (out !== previous);
214
+ return out;
215
+ };
216
+
217
+ // The heading text markdown-it slugs: text and code, without HTML or markup.
218
+ const headingText = (text) =>
219
+ removeAll(text, /<[^>]*>/gu)
220
+ .replaceAll(/\[(?<label>[^\]]*)\]\([^)]*\)/gu, "$<label>")
221
+ .replaceAll(/`|\*\*|__/gu, "")
222
+ .trim();
223
+
224
+ // A fence or include title: a bare token when Blume reads one as a title,
225
+ // otherwise `title="…"`.
226
+ const titleToken = (label) => {
227
+ if (/^[^\s"'{}=[\]]+$/u.test(label) && !RESERVED_META.has(label)) {
228
+ return label;
229
+ }
230
+ if (!label.includes('"')) {
231
+ return `title="${label}"`;
232
+ }
233
+ return label.includes("'") ? null : `title='${label}'`;
234
+ };
235
+
236
+ // A JSX string attribute value.
237
+ const jsxString = (value) =>
238
+ value.includes('"') ? `{${JSON.stringify(value)}}` : `"${value}"`;
239
+
240
+ // A YAML scalar for a frontmatter title.
241
+ const yamlScalar = (value) =>
242
+ /^[A-Za-z0-9][\w\s.,'()/&+?!-]*$/u.test(value) &&
243
+ !/\s$/u.test(value) &&
244
+ !/^(?:true|false|null|yes|no|on|off|~)$/iu.test(value)
245
+ ? value
246
+ : JSON.stringify(value);
247
+
248
+ // VitePress's code-group tab label for an unlabeled fence: its language.
249
+ const languageTitle = (lang) => {
250
+ if (lang === "" || lang === "ansi") {
251
+ return "txt";
252
+ }
253
+ return lang === "vue-html" ? "template" : lang;
254
+ };
255
+
256
+ // --- Project model ----------------------------------------------------------------
257
+
258
+ const SKIP_DIRS = new Set(["node_modules", "public"]);
259
+
260
+ const collectFiles = (root) => {
261
+ const out = [];
262
+ const walk = (dir) => {
263
+ for (const name of readdirSync(dir).toSorted()) {
264
+ const abs = path.join(dir, name);
265
+ if (statSync(abs).isDirectory()) {
266
+ if (!(name.startsWith(".") || (dir === root && SKIP_DIRS.has(name)))) {
267
+ walk(abs);
268
+ }
269
+ } else if (/\.mdx?$/u.test(name)) {
270
+ out.push(path.relative(root, abs).split(path.sep).join("/"));
271
+ }
272
+ }
273
+ };
274
+ walk(root);
275
+ return out;
276
+ };
277
+
278
+ const routeOf = (rel) => {
279
+ const route = `/${rel.replace(/\.mdx?$/u, "")}`.replace(/\/index$/u, "");
280
+ return route === "" ? "/" : route;
281
+ };
282
+
283
+ // --- GitHub alerts → container items --------------------------------------------
284
+
285
+ // Replace each unambiguous alert blockquote with open/close marker items around
286
+ // its unquoted lines; the body pass turns them into a callout.
287
+ const expandAlerts = (items, review) => {
288
+ const out = [];
289
+ let fence = null;
290
+ const leftAsQuote = (line, type) =>
291
+ review(
292
+ line,
293
+ GITHUB_ALERTS.has(type.toLowerCase())
294
+ ? "GitHub alert left as a blockquote: Blume renders it as a callout only in .mdx (a .md page warns BLUME_MD_GITHUB_ALERT), so check it there or convert it by hand"
295
+ : "GitHub alert left as a blockquote: convert it by hand"
296
+ );
297
+ for (let i = 0; i < items.length; i += 1) {
298
+ const { line, text } = items[i];
299
+ if (fence) {
300
+ fence = isFenceClose(text, fence) ? null : fence;
301
+ out.push(items[i]);
302
+ continue;
303
+ }
304
+ const f = FENCE_RE.exec(text);
305
+ if (f) {
306
+ fence = { char: f.groups.marker[0], length: f.groups.marker.length };
307
+ out.push(items[i]);
308
+ continue;
309
+ }
310
+ const indented = /^\s+>\s?\[!(?<type>\w+)\]/u.exec(text);
311
+ const m = ALERT_RE.exec(text);
312
+ if (indented) {
313
+ leftAsQuote(line, indented.groups.type);
314
+ } else if (m && !ALERTS.has(m.groups.type.toLowerCase())) {
315
+ leftAsQuote(line, m.groups.type);
316
+ }
317
+ if (!m || !ALERTS.has(m.groups.type.toLowerCase())) {
318
+ out.push(items[i]);
319
+ continue;
320
+ }
321
+ let end = i + 1;
322
+ while (end < items.length && items[end].text.startsWith(">")) {
323
+ end += 1;
324
+ }
325
+ const block = items.slice(i + 1, end);
326
+ const lazy = end < items.length && items[end].text.trim() !== "";
327
+ if (lazy || block.some((item) => /^>\s?>/u.test(item.text))) {
328
+ leftAsQuote(line, m.groups.type);
329
+ out.push(items[i]);
330
+ continue;
331
+ }
332
+ out.push({
333
+ kind: "alert-open",
334
+ line,
335
+ title: m.groups.title.trim(),
336
+ type: ALERTS.get(m.groups.type.toLowerCase()),
337
+ });
338
+ for (const item of block) {
339
+ out.push({ line: item.line, text: item.text.replace(/^>\s?/u, "") });
340
+ }
341
+ out.push({ kind: "alert-close", line });
342
+ i = end - 1;
343
+ }
344
+ return out;
345
+ };
346
+
347
+ // Resolve a snippet or include path from page `from`: `@` is the source
348
+ // directory (Blume's content root, written `/`); a relative path stays relative.
349
+ // Null when the path leaves the source directory.
350
+ const resolveSource = (from, target) => {
351
+ if (target.startsWith("@")) {
352
+ const rel = path.posix.normalize(target.slice(target[1] === "/" ? 2 : 1));
353
+ return rel.startsWith("../") ? null : { rel, written: `/${rel}` };
354
+ }
355
+ const rel = path.posix.normalize(
356
+ path.posix.join(path.posix.dirname(from), target)
357
+ );
358
+ return rel.startsWith("../") ? null : { rel, written: target };
359
+ };
360
+
361
+ // --- Body pass ----------------------------------------------------------------------
362
+
363
+ const parseTitle = (rest) => {
364
+ const trimmed = rest.trim();
365
+ const bracket = /^\[(?<title>.*)\]$/u.exec(trimmed);
366
+ if (bracket) {
367
+ return { attrs: "", title: bracket.groups.title };
368
+ }
369
+ const attr = /^\{.*\btitle="(?<title>[^"]*)".*\}$/u.exec(trimmed);
370
+ if (attr) {
371
+ return { attrs: "", title: attr.groups.title };
372
+ }
373
+ const attrs = /\{[^}]*\}$/u.exec(trimmed);
374
+ return {
375
+ attrs: attrs ? attrs[0] : "",
376
+ title: (attrs ? trimmed.slice(0, attrs.index) : trimmed).trim(),
377
+ };
378
+ };
379
+
380
+ // One page body: containers, alerts, fences, snippets, includes, inline syntax.
381
+ // `items` are `{ line, text }` (plus alert markers); returns output lines with
382
+ // the source line each came from.
383
+ const convertBody = (items, file, project, report) => {
384
+ const out = [];
385
+ const origin = [];
386
+ const stack = [];
387
+ const callouts = [];
388
+ let fence = null;
389
+ const { edit, review } = report;
390
+
391
+ const emit = (text, line) => {
392
+ out.push(text);
393
+ origin.push(line);
394
+ };
395
+ // A blank line this pass adds around a block tag; a blank source line right
396
+ // after one is dropped, so blocks don't gain double gaps.
397
+ const padded = new Set();
398
+ const pad = (line) => {
399
+ emit("", line);
400
+ padded.add(out.length - 1);
401
+ };
402
+ const blank = (line) => {
403
+ if (out.length > 0 && out.at(-1).trim() !== "") {
404
+ emit("", line);
405
+ }
406
+ };
407
+ // The stack depth a code group that lost its closer was ended at, until the
408
+ // next container line.
409
+ let endedGroup = -1;
410
+ const inCodeGroup = () => stack.at(-1)?.kind === "codegroup";
411
+ const floor = () => stack.findLastIndex((entry) => entry.barrier) + 1;
412
+
413
+ const close = (entry, line, closer) => {
414
+ if (entry.kind === "codegroup" || entry.kind === "details") {
415
+ blank(line);
416
+ emit(
417
+ `${entry.indent}</${entry.kind === "codegroup" ? "CodeGroup" : "Expandable"}>`,
418
+ line
419
+ );
420
+ pad(line);
421
+ } else if (entry.kind === "callout") {
422
+ entry.closeIndex = out.length;
423
+ emit("", line);
424
+ callouts.push(entry);
425
+ let depth = 1;
426
+ for (let p = entry.parent; p; p = p.parent) {
427
+ p.depth = Math.max(p.depth, depth);
428
+ depth += 1;
429
+ }
430
+ } else if (entry.kind === "custom") {
431
+ if (closer !== undefined) {
432
+ emit(closer, line);
433
+ }
434
+ // Left as written, its `:::` lines still end a callout around it whose
435
+ // fence is no longer, so each enclosing callout needs a longer fence.
436
+ let depth = entry.colons - 2;
437
+ for (let p = entry.parent; p; p = p.parent) {
438
+ p.depth = Math.max(p.depth, depth);
439
+ depth += 1;
440
+ }
441
+ }
442
+ };
443
+
444
+ // Close the entries above `index` (they had no closer of their own), then
445
+ // the one at `index` with the closer line itself.
446
+ const closeTo = (index, line, closer) => {
447
+ while (stack.length > index + 1) {
448
+ const inner = stack.pop();
449
+ if (inner.kind !== "custom" && inner.kind !== "wrapper") {
450
+ review(
451
+ inner.line,
452
+ `container has no closer of its own; VitePress ended it on line ${line}, and so does the output`
453
+ );
454
+ }
455
+ close(inner, line);
456
+ }
457
+ close(stack.pop(), line, closer);
458
+ };
459
+
460
+ const href = (value, line) => {
461
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/iu.test(value)) {
462
+ return value;
463
+ }
464
+ const cut = value.search(/[?#]/u);
465
+ const target = cut === -1 ? value : value.slice(0, cut);
466
+ const suffix = cut === -1 ? "" : value.slice(cut);
467
+ const ext = /\.(?:html|md)$/u.exec(target);
468
+ const slash = target.length > 1 && target.endsWith("/");
469
+ if (!(ext || slash) || (!target.startsWith("/") && ext?.[0] === ".md")) {
470
+ return value;
471
+ }
472
+ if (target.startsWith("/") && project.publicFiles.has(target)) {
473
+ return value;
474
+ }
475
+ let route = target.startsWith("/")
476
+ ? target
477
+ : path.posix.join("/", path.posix.dirname(file), target);
478
+ route = route.slice(0, ext ? -ext[0].length : -1);
479
+ route = route.replace(/\/index$/u, "") || "/";
480
+ if (!project.routes.has(route)) {
481
+ review(line, `link ${target}: no page backs ${route}; left as written`);
482
+ return value;
483
+ }
484
+ edit("link → route");
485
+ return `${route}${suffix}`;
486
+ };
487
+
488
+ // Markdown links, `href`s, and link reference definitions.
489
+ const links = (text, line) => {
490
+ const result = outsideCode(text, (part) =>
491
+ part
492
+ .replaceAll(
493
+ /(?<!!)\]\((?<target>[^)\s]+)(?<title>\s+"[^"]*")?\)(?<attrs>\{[^}]*\})?/gu,
494
+ (_, target, title, attrs) => {
495
+ if (attrs) {
496
+ edit("link attributes {…} removed");
497
+ }
498
+ return `](${href(target, line)}${title ?? ""})`;
499
+ }
500
+ )
501
+ .replaceAll(
502
+ /\bhref="(?<target>[^"]+)"/gu,
503
+ (_, target) => `href="${href(target, line)}"`
504
+ )
505
+ );
506
+ const definition =
507
+ /^(?<head>\s*\[[^\]]+\]:\s*)(?<target>\S+)(?<tail>.*)$/u.exec(result);
508
+ if (!definition) {
509
+ return result;
510
+ }
511
+ const { head, tail } = definition.groups;
512
+ return `${head}${href(definition.groups.target, line)}${tail}`;
513
+ };
514
+
515
+ const open = (m, line) => {
516
+ endedGroup = -1;
517
+ const { colons, indent, name } = m.groups;
518
+ const lower = name.toLowerCase();
519
+ const { attrs, title } = parseTitle(m.groups.rest);
520
+ const base = { colons: colons.length, indent, line };
521
+ if (lower === "code-group") {
522
+ blank(line);
523
+ emit(`${indent}<CodeGroup>`, line);
524
+ pad(line);
525
+ stack.push({ ...base, kind: "codegroup" });
526
+ edit("::: code-group → <CodeGroup>");
527
+ return;
528
+ }
529
+ if (lower === "details") {
530
+ blank(line);
531
+ const opened = /\bopen\b/u.test(attrs) ? " defaultOpen" : "";
532
+ emit(
533
+ `${indent}<Expandable title=${jsxString(title || "Details")}${opened}>`,
534
+ line
535
+ );
536
+ pad(line);
537
+ stack.push({ ...base, kind: "details" });
538
+ edit("::: details → <Expandable>");
539
+ return;
540
+ }
541
+ if (lower === "raw" || lower === "v-pre") {
542
+ stack.push({ ...base, kind: "wrapper" });
543
+ edit(`::: ${lower} wrapper removed`);
544
+ if (lower === "v-pre") {
545
+ review(
546
+ line,
547
+ "`::: v-pre` removed: any `{{ }}` inside it is literal in .md but an expression in .mdx"
548
+ );
549
+ }
550
+ return;
551
+ }
552
+ const type = CALLOUTS.get(lower);
553
+ if (type) {
554
+ if (attrs) {
555
+ review(line, `container attributes ${attrs} dropped`);
556
+ }
557
+ if (/[[\]]/u.test(title)) {
558
+ review(line, "callout title contains brackets: check the `[…]` label");
559
+ }
560
+ stack.push({
561
+ ...base,
562
+ depth: 0,
563
+ kind: "callout",
564
+ name: lower,
565
+ openIndex: out.length,
566
+ parent: stack.findLast((entry) => entry.kind === "callout"),
567
+ source: m.input,
568
+ title: links(title, line),
569
+ type,
570
+ });
571
+ emit("", line);
572
+ return;
573
+ }
574
+ stack.push({
575
+ ...base,
576
+ kind: "custom",
577
+ parent: stack.findLast((entry) => entry.kind === "callout"),
578
+ });
579
+ emit(m.input, line);
580
+ review(
581
+ line,
582
+ `\`${name}\` isn't a VitePress built-in container: it was paired as one and left as written; convert it by hand`
583
+ );
584
+ };
585
+
586
+ const snippet = (m, line) => {
587
+ const p = RAW_PATH_RE.exec(m.groups.raw);
588
+ if (!p) {
589
+ review(line, "`<<<` import not understood: convert it by hand");
590
+ return false;
591
+ }
592
+ const { attrs, lines, region, title } = p.groups;
593
+ const file0 = p.groups.file;
594
+ if (region) {
595
+ review(
596
+ line,
597
+ `\`<<<\` region ${region}: <include> has no region selection; move the region into its own file`
598
+ );
599
+ return false;
600
+ }
601
+ const source = resolveSource(file, file0);
602
+ if (!source) {
603
+ review(
604
+ line,
605
+ `\`<<< ${file0}\` is outside the source directory: copy it in (e.g. under _snippets/) and include the copy`
606
+ );
607
+ return false;
608
+ }
609
+ if (!existsSync(path.join(project.root, source.rel))) {
610
+ review(line, `\`<<< ${file0}\`: file not found`);
611
+ return false;
612
+ }
613
+ let numbered = false;
614
+ // `{2 ts:line-numbers}` or `{2 ts :line-numbers}`: either slot.
615
+ const withoutLineNumbers = (value = "") => {
616
+ const ln = LINE_NUMBERS_RE.exec(value);
617
+ if (!ln) {
618
+ return value;
619
+ }
620
+ numbered = !ln.groups.no;
621
+ if (ln.groups.start) {
622
+ review(
623
+ line,
624
+ `line numbers started at ${ln.groups.start}: Blume starts at 1`
625
+ );
626
+ }
627
+ return `${value.slice(0, ln.index)}${value.slice(ln.index + ln[0].length)}`;
628
+ };
629
+ let lang = withoutLineNumbers(p.groups.lang);
630
+ const extra = withoutLineNumbers(attrs);
631
+ if (!lang && /\.mdx?$/u.test(source.rel)) {
632
+ lang = "md";
633
+ }
634
+ const label =
635
+ title ?? (inCodeGroup() ? path.posix.basename(file0) : undefined);
636
+ const titlePart = label === undefined ? undefined : titleToken(label);
637
+ if (titlePart === null) {
638
+ review(line, "label holds both quote kinds: set its title by hand");
639
+ }
640
+ const meta = [
641
+ titlePart,
642
+ lines ? `{${lines}}` : undefined,
643
+ numbered ? "lineNumbers" : undefined,
644
+ extra.trim(),
645
+ ]
646
+ .filter(Boolean)
647
+ .join(" ");
648
+ let metaAttr = "";
649
+ if (meta) {
650
+ metaAttr = meta.includes('"') ? ` meta='${meta}'` : ` meta="${meta}"`;
651
+ }
652
+ const langAttr = lang ? ` lang="${lang}"` : "";
653
+ blank(line);
654
+ emit(
655
+ `${m.groups.indent}<include${langAttr}${metaAttr}>${source.written}</include>`,
656
+ line
657
+ );
658
+ pad(line);
659
+ edit("<<< import → <include>");
660
+ return true;
661
+ };
662
+
663
+ const include = (m, line) => {
664
+ const { target } = m.groups;
665
+ if (/\{\d*,\d*\}$/u.test(target) || /#[\w-]+/u.test(target)) {
666
+ review(
667
+ line,
668
+ "`<!--@include:-->` with a region or line range: split the partial and include the piece"
669
+ );
670
+ return false;
671
+ }
672
+ const source = resolveSource(file, target);
673
+ if (!source) {
674
+ review(
675
+ line,
676
+ `\`<!--@include: ${target}-->\` is outside the source directory: move the partial in`
677
+ );
678
+ return false;
679
+ }
680
+ if (!/\.mdx?$/u.test(source.rel)) {
681
+ review(
682
+ line,
683
+ `\`<!--@include: ${target}-->\` splices a non-Markdown file; Blume shows one as a code block: convert it by hand`
684
+ );
685
+ return false;
686
+ }
687
+ if (!existsSync(path.join(project.root, source.rel))) {
688
+ review(
689
+ line,
690
+ `\`<!--@include: ${target}-->\`: file not found (VitePress skipped it silently)`
691
+ );
692
+ return false;
693
+ }
694
+ blank(line);
695
+ emit(`${m.groups.indent}<include>${source.written}</include>`, line);
696
+ pad(line);
697
+ project.includes.get(file).add(source.rel);
698
+ edit("<!--@include:--> → <include>");
699
+ return true;
700
+ };
701
+
702
+ const fenceInfo = (m, line) => {
703
+ const { indent, marker } = m.groups;
704
+ const raw = m.groups.info.trim();
705
+ const parts = /^(?<lang>[^\s{[:]*)(?<rest>.*)$/u.exec(raw).groups;
706
+ let { lang, rest } = parts;
707
+ const glued = rest.startsWith("{");
708
+ let changed = false;
709
+ let label;
710
+ const bracket = /\[(?<label>.*)\]/u.exec(rest);
711
+ if (bracket) {
712
+ label = bracket.groups.label.trim();
713
+ rest = rest.replace(bracket[0], " ");
714
+ changed = true;
715
+ edit("fence [label] → title");
716
+ }
717
+ // Anywhere outside the label: VitePress reads `ts{1}:line-numbers` too.
718
+ let numbered = false;
719
+ const ln = LINE_NUMBERS_RE.exec(rest);
720
+ if (ln) {
721
+ rest = `${rest.slice(0, ln.index)} ${rest.slice(ln.index + ln[0].length)}`;
722
+ numbered = !ln.groups.no;
723
+ changed = true;
724
+ edit("fence :line-numbers → lineNumbers");
725
+ if (ln.groups.start) {
726
+ review(
727
+ line,
728
+ `line numbers started at ${ln.groups.start}: Blume starts at 1`
729
+ );
730
+ }
731
+ }
732
+ if (lang.endsWith("-vue")) {
733
+ lang = lang.slice(0, -4);
734
+ changed = true;
735
+ review(
736
+ line,
737
+ "`-vue` fence became a plain fence: resolve any `{{ }}` in it by hand"
738
+ );
739
+ }
740
+ const group = inCodeGroup() && !label;
741
+ if (!(changed || glued || group)) {
742
+ return `${indent}${marker}${m.groups.info}`;
743
+ }
744
+ const ranges = [];
745
+ rest = rest.replaceAll(/\{(?<inside>[^}]*)\}/gu, (all, inside) => {
746
+ if (/^[\d,\s-]*$/u.test(inside)) {
747
+ ranges.push(`{${inside.replaceAll(/\s+/gu, "")}}`);
748
+ } else {
749
+ review(line, `fence attributes ${all} dropped`);
750
+ }
751
+ return " ";
752
+ });
753
+ if (glued) {
754
+ edit("fence lang{lines} → lang {lines}");
755
+ }
756
+ if (group) {
757
+ label = languageTitle(lang);
758
+ edit("code group block titled with its language");
759
+ }
760
+ let titlePart;
761
+ if (label) {
762
+ titlePart = titleToken(label);
763
+ if (titlePart === null) {
764
+ review(line, "label holds both quote kinds: set its title by hand");
765
+ }
766
+ }
767
+ const tokens = [
768
+ lang || "txt",
769
+ titlePart,
770
+ ...ranges,
771
+ numbered ? "lineNumbers" : undefined,
772
+ ...rest.trim().split(/\s+/u),
773
+ ].filter(Boolean);
774
+ return `${indent}${marker}${tokens.join(" ")}`;
775
+ };
776
+
777
+ const badge = (attrs, children, line) => {
778
+ const pairs = [
779
+ ...attrs.matchAll(/(?<key>[:@]?[\w-]+)(?:="(?<value>[^"]*)")?/gu),
780
+ ];
781
+ if (pairs.some((p) => !["text", "type"].includes(p.groups.key))) {
782
+ review(
783
+ line,
784
+ "`<Badge>` with bindings or other props: convert it by hand"
785
+ );
786
+ return null;
787
+ }
788
+ const props = Object.fromEntries(
789
+ pairs.map((p) => [p.groups.key, p.groups.value ?? ""])
790
+ );
791
+ const variant = BADGE_VARIANTS.get(props.type ?? "tip") ?? "accent";
792
+ const text = children ?? props.text ?? "";
793
+ edit("<Badge type text> → <Badge variant>");
794
+ const variantAttr = variant === "default" ? "" : ` variant="${variant}"`;
795
+ return `<Badge${variantAttr}>${text}</Badge>`;
796
+ };
797
+
798
+ const inline = (text, line) => {
799
+ let result = links(text, line);
800
+ let converted = false;
801
+ result = outsideCode(result, (part) =>
802
+ part
803
+ .replaceAll(/<Badge\b(?<attrs>[^>]*?)\/>/gu, (all, attrs) => {
804
+ const out2 = badge(attrs, undefined, line);
805
+ converted ||= out2 !== null;
806
+ return out2 ?? all;
807
+ })
808
+ .replaceAll(
809
+ /<Badge\b(?<attrs>(?:(?!variant=)[^>])*)>(?<children>.*?)<\/Badge>/gu,
810
+ (all, attrs, children) => {
811
+ if (!/\btype=/u.test(attrs)) {
812
+ return all;
813
+ }
814
+ const out2 = badge(attrs, children, line);
815
+ converted ||= out2 !== null;
816
+ return out2 ?? all;
817
+ }
818
+ )
819
+ );
820
+ const curly = CURLY_ID_RE.exec(result);
821
+ if (curly) {
822
+ result = `${curly.groups.head} [#${curly.groups.id}]`;
823
+ edit("heading {#id} → [#id]");
824
+ }
825
+ // VitePress slugged the source heading: a self-closing badge added no
826
+ // text, a badge with children added its children. Blume leaves a badge's
827
+ // text out of the id, so the pin only changes the id for the second kind;
828
+ // for the first it repeats Blume's own.
829
+ const heading = HEADING_RE.exec(text);
830
+ if (
831
+ converted &&
832
+ heading &&
833
+ heading.groups.hashes.length > 1 &&
834
+ !PIN_RE.test(result)
835
+ ) {
836
+ const slug = vitepressSlug(
837
+ headingText(heading.groups.text.replace(/\s*\{#[\w-]+\}$/u, ""))
838
+ );
839
+ if (slug) {
840
+ result = `${result} [#${slug}]`;
841
+ edit("badge heading pinned to its VitePress anchor");
842
+ }
843
+ }
844
+ return result;
845
+ };
846
+
847
+ const openAlert = (item) => {
848
+ stack.push({
849
+ barrier: true,
850
+ colons: Number.POSITIVE_INFINITY,
851
+ depth: 0,
852
+ indent: "",
853
+ kind: "callout",
854
+ line: item.line,
855
+ name: item.type,
856
+ openIndex: out.length,
857
+ parent: stack.findLast((entry) => entry.kind === "callout"),
858
+ source: "",
859
+ title: item.title,
860
+ type: item.type,
861
+ });
862
+ emit("", item.line);
863
+ edit("GitHub alert → directive");
864
+ };
865
+
866
+ // A line inside a code fence: VitePress's container scan doesn't skip
867
+ // fences, so a bare `:::` here would have closed an open container.
868
+ const fenced = (text, line) => {
869
+ if (isFenceClose(text, fence)) {
870
+ fence = null;
871
+ } else if (CLOSE_RE.test(text) && stack.length > floor()) {
872
+ review(
873
+ line,
874
+ "VitePress ends the open `:::` container on this line, inside a code fence: check the block"
875
+ );
876
+ } else if (INCLUDE_ANY_RE.test(text)) {
877
+ // VitePress splices includes into the raw source, fences included.
878
+ review(
879
+ line,
880
+ "`<!--@include:-->` inside a code fence: VitePress showed the file's text here, Blume shows the comment; use an <include> of the file instead"
881
+ );
882
+ }
883
+ emit(text, line);
884
+ };
885
+
886
+ // A code group only holds code: the first other content ends one whose
887
+ // closer is missing (VitePress let it swallow what followed).
888
+ const endOpenCodeGroup = (text, line) => {
889
+ const trimmed = text.trim();
890
+ if (
891
+ !inCodeGroup() ||
892
+ trimmed === "" ||
893
+ SNIPPET_RE.test(text) ||
894
+ /^<!--[\s\S]*-->$/u.test(trimmed)
895
+ ) {
896
+ return;
897
+ }
898
+ review(
899
+ stack.at(-1).line,
900
+ `code group has no closer before line ${line}; it now ends at its last code block (VitePress swallowed what followed)`
901
+ );
902
+ edit("unclosed code group ended at its last code block");
903
+ closeTo(stack.length - 1, line);
904
+ endedGroup = stack.length;
905
+ };
906
+
907
+ // A bare `:::` closes the outermost open container whose fence is no
908
+ // longer than it (and everything nested in it), as markdown-it-container
909
+ // pairs them; alerts are a boundary it can't close across.
910
+ const closeLine = (closer, text, line) => {
911
+ // The first closer after a code group ended early, with nothing opened
912
+ // since, was that group's closer in VitePress.
913
+ const groupCloser = endedGroup === stack.length;
914
+ endedGroup = -1;
915
+ const size = closer.groups.colons.length;
916
+ const start = floor();
917
+ const index = stack.findIndex(
918
+ (entry, k) => k >= start && entry.colons <= size
919
+ );
920
+ if (index === -1) {
921
+ review(
922
+ line,
923
+ groupCloser
924
+ ? "`:::` closed the code group above in VitePress, which now ends at its last code block: removed"
925
+ : "stray `:::` (VitePress showed it as text) removed"
926
+ );
927
+ edit("stray ::: removed");
928
+ return;
929
+ }
930
+ closeTo(index, line, text);
931
+ };
932
+
933
+ // Snippet imports, includes, and `[[toc]]`; true when the line was consumed.
934
+ const blockLine = (text, line) => {
935
+ const snip = SNIPPET_RE.exec(text);
936
+ if (snip && snippet(snip, line)) {
937
+ return true;
938
+ }
939
+ const inc = INCLUDE_LINE_RE.exec(text);
940
+ if (inc && include(inc, line)) {
941
+ return true;
942
+ }
943
+ if (!inc && INCLUDE_ANY_RE.test(text)) {
944
+ review(line, "`<!--@include:-->` inside a line: convert it by hand");
945
+ }
946
+ const existing = BLUME_INCLUDE_RE.exec(text);
947
+ if (existing && !/\blang=/u.test(existing.groups.attrs)) {
948
+ const source = resolveSource(file, existing.groups.target);
949
+ if (source) {
950
+ project.includes.get(file).add(source.rel);
951
+ }
952
+ }
953
+ if (text.trim() === "[[toc]]") {
954
+ edit("[[toc]] removed");
955
+ return true;
956
+ }
957
+ return text.trim() === "" && padded.has(out.length - 1);
958
+ };
959
+
960
+ const step = (item) => {
961
+ if (item.kind === "alert-open") {
962
+ openAlert(item);
963
+ return;
964
+ }
965
+ if (item.kind === "alert-close") {
966
+ closeTo(
967
+ stack.findLastIndex((entry) => entry.barrier),
968
+ item.line
969
+ );
970
+ return;
971
+ }
972
+ const { line, text } = item;
973
+ if (fence) {
974
+ fenced(text, line);
975
+ return;
976
+ }
977
+ const f = FENCE_RE.exec(text);
978
+ if (f) {
979
+ fence = { char: f.groups.marker[0], length: f.groups.marker.length };
980
+ emit(fenceInfo(f, line), line);
981
+ return;
982
+ }
983
+ const closer = CLOSE_RE.exec(text);
984
+ if (closer) {
985
+ closeLine(closer, text, line);
986
+ return;
987
+ }
988
+ endOpenCodeGroup(text, line);
989
+ const opener = OPEN_RE.exec(text);
990
+ if (opener) {
991
+ open(opener, line);
992
+ return;
993
+ }
994
+ if (!blockLine(text, line)) {
995
+ emit(inline(text, line), line);
996
+ }
997
+ };
998
+
999
+ // Write each callout's fences once its nesting depth is known: Blume needs
1000
+ // an outer fence longer than any fence nested in it.
1001
+ const finish = () => {
1002
+ const last = items.at(-1)?.line ?? 1;
1003
+ while (stack.length > 0) {
1004
+ const entry = stack.at(-1);
1005
+ if (entry.kind !== "custom" && entry.kind !== "wrapper") {
1006
+ review(
1007
+ entry.line,
1008
+ "container is never closed; VitePress ran it to the end of the page, and so does the output"
1009
+ );
1010
+ }
1011
+ close(stack.pop(), last);
1012
+ }
1013
+ for (const entry of callouts) {
1014
+ const fenceText = ":".repeat(3 + entry.depth);
1015
+ const label = entry.title ? `[${entry.title}]` : "";
1016
+ out[entry.openIndex] = `${entry.indent}${fenceText}${entry.type}${label}`;
1017
+ out[entry.closeIndex] = `${entry.indent}${fenceText}`;
1018
+ if (entry.source && entry.source !== out[entry.openIndex]) {
1019
+ edit(
1020
+ entry.title ? "::: type Title → :::type[Title]" : "::: type → :::type"
1021
+ );
1022
+ if (NEW_IN_2.has(entry.name) && /^\s*:{3,}\s/u.test(entry.source)) {
1023
+ review(
1024
+ entry.line,
1025
+ `\`::: ${entry.name}\` is a container only in VitePress 2.0; 1.x showed it as text. It is now a callout`
1026
+ );
1027
+ }
1028
+ }
1029
+ }
1030
+ };
1031
+
1032
+ for (const item of items) {
1033
+ step(item);
1034
+ }
1035
+ finish();
1036
+ return { lines: out, origin };
1037
+ };
1038
+
1039
+ // --- H1 → title ---------------------------------------------------------------------
1040
+
1041
+ const splitFrontmatter = (text) => {
1042
+ const lines = text.split("\n");
1043
+ if (lines[0] !== "---") {
1044
+ return { body: lines, fm: null, offset: 0 };
1045
+ }
1046
+ const end = lines.indexOf("---", 1);
1047
+ if (end === -1) {
1048
+ return { body: lines, fm: null, offset: 0 };
1049
+ }
1050
+ return {
1051
+ body: lines.slice(end + 1),
1052
+ fm: lines.slice(1, end),
1053
+ offset: end + 1,
1054
+ };
1055
+ };
1056
+
1057
+ // The H1 line, plus the blank after it when a blank also precedes it (so the
1058
+ // body keeps one gap below the frontmatter).
1059
+ const removedLines = (lines, i) =>
1060
+ i > 0 && lines[i - 1] === "" && lines[i + 1] === "" ? 2 : 1;
1061
+
1062
+ const moveH1 = (doc, report) => {
1063
+ let fence = null;
1064
+ for (let i = 0; i < doc.lines.length; i += 1) {
1065
+ const text = doc.lines[i];
1066
+ if (fence) {
1067
+ fence = isFenceClose(text, fence) ? null : fence;
1068
+ continue;
1069
+ }
1070
+ const f = FENCE_RE.exec(text);
1071
+ if (f) {
1072
+ fence = { char: f.groups.marker[0], length: f.groups.marker.length };
1073
+ continue;
1074
+ }
1075
+ const h1 = /^#\s+(?<text>.+?)\s*$/u.exec(text);
1076
+ if (!h1) {
1077
+ continue;
1078
+ }
1079
+ const title = h1.groups.text.replace(
1080
+ /\s*(?:\{#[\w-]+\}|\[#[\w-]+\])$/u,
1081
+ ""
1082
+ );
1083
+ const line = doc.origin[i];
1084
+ const existing = (doc.fm ?? [])
1085
+ .map((l) => /^title:\s*(?<value>.*)$/u.exec(l))
1086
+ .find(Boolean);
1087
+ if (existing) {
1088
+ const value = existing.groups.value.replace(
1089
+ /^(?<q>["'])(?<v>.*)\k<q>$/u,
1090
+ "$<v>"
1091
+ );
1092
+ if (value === title) {
1093
+ const count = removedLines(doc.lines, i);
1094
+ doc.lines.splice(i, count);
1095
+ doc.origin.splice(i, count);
1096
+ report.edit("duplicate body H1 removed");
1097
+ } else {
1098
+ report.review(
1099
+ line,
1100
+ "frontmatter `title` differs from the body H1: make the H1 the `title` and keep the old value as `seo.title`"
1101
+ );
1102
+ }
1103
+ return;
1104
+ }
1105
+ if (/[`<[*_{]/u.test(title)) {
1106
+ report.review(
1107
+ line,
1108
+ "body H1 has inline markup: move it into frontmatter `title` by hand"
1109
+ );
1110
+ return;
1111
+ }
1112
+ doc.fm = [`title: ${yamlScalar(title)}`, ...(doc.fm ?? [])];
1113
+ const count = removedLines(doc.lines, i);
1114
+ doc.lines.splice(i, count);
1115
+ doc.origin.splice(i, count);
1116
+ report.edit("body H1 → frontmatter title");
1117
+ return;
1118
+ }
1119
+ };
1120
+
1121
+ // --- MDX safety ------------------------------------------------------------------------
1122
+
1123
+ const MDX_ONLY_RE = /^\s*:{3,}[a-z]|<(?:CodeGroup|Expandable|Badge)\b/u;
1124
+
1125
+ const needsMdx = (lines) => {
1126
+ let fence = null;
1127
+ for (const text of lines) {
1128
+ if (fence) {
1129
+ fence = isFenceClose(text, fence) ? null : fence;
1130
+ continue;
1131
+ }
1132
+ const f = FENCE_RE.exec(text);
1133
+ if (f) {
1134
+ fence = { char: f.groups.marker[0], length: f.groups.marker.length };
1135
+ continue;
1136
+ }
1137
+ if (MDX_ONLY_RE.test(stripCode(text))) {
1138
+ return true;
1139
+ }
1140
+ }
1141
+ return false;
1142
+ };
1143
+
1144
+ const HAZARDS = [
1145
+ [/\{\{/u, "`{{ }}` is a JSX expression in .mdx: substitute the value"],
1146
+ [/<script\b|<style\b/u, "`<script>`/`<style>` block: remove it"],
1147
+ [
1148
+ /\s(?::[\w.-]+|v-[\w-]+|@[\w.-]+)="/u,
1149
+ "Vue binding or directive: rewrite the props in JSX form",
1150
+ ],
1151
+ [
1152
+ /<https?:\/\//u,
1153
+ "`<https://…>` autolink fails in .mdx: write a Markdown link",
1154
+ ],
1155
+ [
1156
+ /<(?:img|input|source|col|wbr)\b(?:[^>/]|\/(?!>))*>/u,
1157
+ "unclosed void tag: self-close it",
1158
+ ],
1159
+ [
1160
+ /<img\b[^>]*\ssrc="\.{1,2}\//u,
1161
+ "raw `<img>` with a relative src: Blume publishes the file but doesn't optimize it, so use a Markdown image",
1162
+ ],
1163
+ [
1164
+ /<!--/u,
1165
+ "HTML comment left in place (multi-line, or an include this pass couldn't convert): .mdx can't compile it",
1166
+ ],
1167
+ ];
1168
+
1169
+ // Make what can be made MDX-safe, and report the rest. `apply` is false for a
1170
+ // file that stays `.md` (then only components are worth reporting).
1171
+ const mdxPass = (doc, apply, report) => {
1172
+ let fence = null;
1173
+ for (let i = 0; i < doc.lines.length; i += 1) {
1174
+ const text = doc.lines[i];
1175
+ const line = doc.origin[i];
1176
+ if (fence) {
1177
+ fence = isFenceClose(text, fence) ? null : fence;
1178
+ continue;
1179
+ }
1180
+ const f = FENCE_RE.exec(text);
1181
+ if (f) {
1182
+ fence = { char: f.groups.marker[0], length: f.groups.marker.length };
1183
+ continue;
1184
+ }
1185
+ let next = text;
1186
+ if (apply) {
1187
+ next = outsideCode(next, (part) =>
1188
+ part
1189
+ .replaceAll(/<!--(?<body>(?:(?!-->).)*?)-->/gu, (all, body) => {
1190
+ if (body.includes("*/") || /^\s*@include:/u.test(body)) {
1191
+ return all;
1192
+ }
1193
+ report.edit("<!-- --> → {/* */}");
1194
+ return `{/*${body}*/}`;
1195
+ })
1196
+ .replaceAll(/<(?<tag>br|hr)\s*>/gu, (_, tag) => {
1197
+ report.edit("<br> → <br />");
1198
+ return `<${tag} />`;
1199
+ })
1200
+ );
1201
+ doc.lines[i] = next;
1202
+ }
1203
+ const prose = stripCode(next);
1204
+ for (const tag of prose.matchAll(/<(?<name>[A-Z][\w.]*)/gu)) {
1205
+ if (!PRODUCED.has(tag.groups.name)) {
1206
+ report.review(
1207
+ line,
1208
+ `component <${tag.groups.name}>: map it to a Blume component, an island, or Markdown`
1209
+ );
1210
+ }
1211
+ }
1212
+ if (!apply) {
1213
+ continue;
1214
+ }
1215
+ for (const [pattern, message] of HAZARDS) {
1216
+ if (pattern.test(prose)) {
1217
+ report.review(line, message);
1218
+ }
1219
+ }
1220
+ const text2 = removeAll(
1221
+ removeAll(
1222
+ prose.replaceAll("{{", ""),
1223
+ /<!--[\s\S]*?-->|\{\/\*[\s\S]*?\*\/\}/gu
1224
+ ),
1225
+ /<[^>]*>/gu
1226
+ );
1227
+ if (/\{(?!\/\*)/u.test(text2)) {
1228
+ report.review(
1229
+ line,
1230
+ "`{` in prose is a JSX expression in .mdx: escape it or use inline code"
1231
+ );
1232
+ }
1233
+ }
1234
+ };
1235
+
1236
+ // --- Driver -------------------------------------------------------------------------------
1237
+
1238
+ const newReport = () => {
1239
+ const edits = new Map();
1240
+ const reviews = [];
1241
+ return {
1242
+ edit: (kind) => edits.set(kind, (edits.get(kind) ?? 0) + 1),
1243
+ edits,
1244
+ review: (line, message) => reviews.push({ line, message }),
1245
+ reviews,
1246
+ };
1247
+ };
1248
+
1249
+ // Every file under `public/`, as the URL it's served at.
1250
+ const listPublic = (root) => {
1251
+ const publicDir = path.join(root, "public");
1252
+ const files = new Set();
1253
+ const walk = (dir) => {
1254
+ for (const name of readdirSync(dir)) {
1255
+ const abs = path.join(dir, name);
1256
+ if (statSync(abs).isDirectory()) {
1257
+ walk(abs);
1258
+ } else {
1259
+ files.add(
1260
+ `/${path.relative(publicDir, abs).split(path.sep).join("/")}`
1261
+ );
1262
+ }
1263
+ }
1264
+ };
1265
+ if (existsSync(publicDir)) {
1266
+ walk(publicDir);
1267
+ }
1268
+ return files;
1269
+ };
1270
+
1271
+ // Report frontmatter keys Blume's strict schema would reject.
1272
+ const reviewFrontmatter = (fm, report) => {
1273
+ for (const [i, text] of (fm ?? []).entries()) {
1274
+ const key = /^(?<key>[\w-]+):\s*(?<value>.*)$/u.exec(text);
1275
+ if (key && !BLUME_KEYS.has(key.groups.key)) {
1276
+ report.review(
1277
+ i + 2,
1278
+ `frontmatter \`${key.groups.key}\` isn't a Blume key (the schema is strict): map it per references/vitepress.md, or remove it`
1279
+ );
1280
+ } else if (
1281
+ key?.groups.key === "sidebar" &&
1282
+ /^(?:true|false)$/u.test(key.groups.value)
1283
+ ) {
1284
+ report.review(
1285
+ i + 2,
1286
+ "VitePress `sidebar: false` hides the sidebar; Blume's `sidebar` is an object: use `mode: center`, or remove it"
1287
+ );
1288
+ }
1289
+ }
1290
+ };
1291
+
1292
+ const convertFile = (file, project) => {
1293
+ const before = readFileSync(path.join(project.root, file), "utf-8");
1294
+ const split = splitFrontmatter(before);
1295
+ const report = newReport();
1296
+ reviewFrontmatter(split.fm, report);
1297
+ const items = split.body.map((text, i) => ({
1298
+ line: split.offset + i + 1,
1299
+ text,
1300
+ }));
1301
+ const body = convertBody(
1302
+ expandAlerts(items, report.review),
1303
+ file,
1304
+ project,
1305
+ report
1306
+ );
1307
+ return {
1308
+ before,
1309
+ file,
1310
+ fm: split.fm,
1311
+ lines: body.lines,
1312
+ origin: body.origin,
1313
+ report,
1314
+ };
1315
+ };
1316
+
1317
+ // Which files end up MDX: their own syntax, or a spliced partial's. A partial
1318
+ // itself keeps its name (pages include it by path).
1319
+ const mdxFiles = (docs, project, partials) => {
1320
+ const mdx = new Map(
1321
+ docs.map((doc) => [
1322
+ doc.file,
1323
+ doc.file.endsWith(".mdx") || needsMdx(doc.lines),
1324
+ ])
1325
+ );
1326
+ let changed = true;
1327
+ while (changed) {
1328
+ changed = false;
1329
+ for (const doc of docs) {
1330
+ const spliced = [...project.includes.get(doc.file)];
1331
+ if (
1332
+ !(mdx.get(doc.file) || partials.has(doc.file)) &&
1333
+ spliced.some((rel) => mdx.get(rel))
1334
+ ) {
1335
+ mdx.set(doc.file, true);
1336
+ changed = true;
1337
+ }
1338
+ }
1339
+ }
1340
+ return mdx;
1341
+ };
1342
+
1343
+ // `x.md` → `x.mdx` when the page now needs MDX and the name is free.
1344
+ const renameTarget = (doc, partial, mdx) => {
1345
+ if (partial || !doc.file.endsWith(".md") || !mdx.get(doc.file)) {
1346
+ return doc.file;
1347
+ }
1348
+ const wanted = `${doc.file}x`;
1349
+ if (mdx.has(wanted)) {
1350
+ doc.report.review(1, `${wanted} already exists: not renamed`);
1351
+ return doc.file;
1352
+ }
1353
+ return wanted;
1354
+ };
1355
+
1356
+ // The file text, keeping a trailing newline the source had.
1357
+ const serialize = (doc) => {
1358
+ const fm = doc.fm ? ["---", ...doc.fm, "---"] : [];
1359
+ const after = [...fm, ...doc.lines].join("\n");
1360
+ return doc.before.endsWith("\n") && !after.endsWith("\n")
1361
+ ? `${after}\n`
1362
+ : after;
1363
+ };
1364
+
1365
+ // Titles, MDX safety, the rename, and the write for one converted file.
1366
+ const finishFile = (doc, context) => {
1367
+ const { docs, mdx, partials, project, write } = context;
1368
+ const { report } = doc;
1369
+ const partial = partials.has(doc.file);
1370
+ if (!partial) {
1371
+ moveH1(doc, report);
1372
+ } else if (needsMdx(doc.lines) && doc.file.endsWith(".md")) {
1373
+ report.review(
1374
+ 1,
1375
+ "partial uses MDX-only syntax but keeps .md (pages include it by this path); Blume parses it in each including page's format"
1376
+ );
1377
+ }
1378
+ const includers = docs.filter((other) =>
1379
+ project.includes.get(other.file).has(doc.file)
1380
+ );
1381
+ const mdxContext = partial
1382
+ ? includers.length > 0 && includers.every((other) => mdx.get(other.file))
1383
+ : mdx.get(doc.file);
1384
+ mdxPass(doc, mdxContext, report);
1385
+ const target = renameTarget(doc, partial, mdx);
1386
+ const after = serialize(doc);
1387
+ const changed = after !== doc.before;
1388
+ if (write && changed) {
1389
+ writeFileSync(path.join(project.root, doc.file), after, "utf-8");
1390
+ }
1391
+ if (write && target !== doc.file) {
1392
+ renameSync(
1393
+ path.join(project.root, doc.file),
1394
+ path.join(project.root, target)
1395
+ );
1396
+ }
1397
+ return {
1398
+ changed: changed || target !== doc.file,
1399
+ edits: Object.fromEntries([...report.edits].toSorted()),
1400
+ file: doc.file,
1401
+ renamed: target === doc.file ? undefined : target,
1402
+ reviews: report.reviews.toSorted((a, b) => a.line - b.line),
1403
+ };
1404
+ };
1405
+
1406
+ const run = (root, write) => {
1407
+ const files = collectFiles(root);
1408
+ const project = {
1409
+ includes: new Map(files.map((file) => [file, new Set()])),
1410
+ publicFiles: listPublic(root),
1411
+ root,
1412
+ routes: new Set(files.map(routeOf)),
1413
+ };
1414
+ const docs = files.map((file) => convertFile(file, project));
1415
+ const partials = new Set(
1416
+ [...project.includes.values()].flatMap((set) => [...set])
1417
+ );
1418
+ const mdx = mdxFiles(docs, project, partials);
1419
+ const context = { docs, mdx, partials, project, write };
1420
+ return docs
1421
+ .map((doc) => finishFile(doc, context))
1422
+ .filter((result) => result.changed || result.reviews.length > 0);
1423
+ };
1424
+
1425
+ const redirects = (dist) => {
1426
+ const out = [];
1427
+ const walk = (dir) => {
1428
+ for (const name of readdirSync(dir).toSorted()) {
1429
+ const abs = path.join(dir, name);
1430
+ if (statSync(abs).isDirectory()) {
1431
+ walk(abs);
1432
+ continue;
1433
+ }
1434
+ if (
1435
+ !name.endsWith(".html") ||
1436
+ name === "index.html" ||
1437
+ name === "404.html"
1438
+ ) {
1439
+ continue;
1440
+ }
1441
+ const html = readFileSync(abs, "utf-8");
1442
+ if (!/<meta name="generator" content="VitePress/u.test(html)) {
1443
+ continue;
1444
+ }
1445
+ const from = `/${path.relative(dist, abs).split(path.sep).join("/")}`;
1446
+ out.push({ from, to: from.slice(0, -".html".length) });
1447
+ }
1448
+ };
1449
+ walk(dist);
1450
+ return out;
1451
+ };
1452
+
1453
+ const HELP = [
1454
+ "vitepress-codemod — the mechanical part of a VitePress → Blume migration",
1455
+ "",
1456
+ " node vitepress-codemod.mjs <srcDir> dry run (report only)",
1457
+ " node vitepress-codemod.mjs --write <srcDir> apply in place",
1458
+ " node vitepress-codemod.mjs --json <srcDir> JSON report",
1459
+ " node vitepress-codemod.mjs --redirects <distDir> .html redirects as JSON",
1460
+ "",
1461
+ ].join("\n");
1462
+
1463
+ const main = () => {
1464
+ const argv = process.argv.slice(2);
1465
+ const dir = argv.find((arg) => !arg.startsWith("--"));
1466
+ if (argv.includes("--help") || argv.includes("-h") || !dir) {
1467
+ process.stdout.write(`${HELP}\n`);
1468
+ return;
1469
+ }
1470
+ const root = path.resolve(dir);
1471
+ if (argv.includes("--redirects")) {
1472
+ process.stdout.write(`${JSON.stringify(redirects(root), null, 2)}\n`);
1473
+ return;
1474
+ }
1475
+ const write = argv.includes("--write");
1476
+ const results = run(root, write);
1477
+ if (argv.includes("--json")) {
1478
+ process.stdout.write(
1479
+ `${JSON.stringify({ files: results, wrote: write }, null, 2)}\n`
1480
+ );
1481
+ return;
1482
+ }
1483
+ const totals = new Map();
1484
+ let reviewCount = 0;
1485
+ for (const result of results) {
1486
+ const name = result.renamed
1487
+ ? `${result.file} → ${result.renamed}`
1488
+ : result.file;
1489
+ process.stdout.write(`\n${name}\n`);
1490
+ for (const [kind, count] of Object.entries(result.edits)) {
1491
+ process.stdout.write(` ${count} × ${kind}\n`);
1492
+ totals.set(kind, (totals.get(kind) ?? 0) + count);
1493
+ }
1494
+ for (const { line, message } of result.reviews) {
1495
+ process.stdout.write(` REVIEW line ${line}: ${message}\n`);
1496
+ }
1497
+ reviewCount += result.reviews.length;
1498
+ }
1499
+ process.stdout.write("\nTotals\n");
1500
+ for (const [kind, count] of [...totals].toSorted()) {
1501
+ process.stdout.write(` ${count} × ${kind}\n`);
1502
+ }
1503
+ const changed = results.filter((result) => result.changed).length;
1504
+ const verb = write ? "changed" : "would change";
1505
+ const hint = write ? "" : " Re-run with --write to apply.";
1506
+ process.stdout.write(
1507
+ `\n${results.length} file(s) with findings, ${changed} ${verb}, ${reviewCount} item(s) to review.${hint}\n`
1508
+ );
1509
+ };
1510
+
1511
+ main();