blume 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +126 -0
  3. package/README.md +4 -4
  4. package/dist/cli/{chunk-74wfhv79.js → chunk-05f21r8h.js} +1 -1
  5. package/dist/cli/{chunk-9qzgg6mj.js → chunk-1cm0h819.js} +3 -3
  6. package/dist/cli/{chunk-nbyvgnnm.js → chunk-1rkxzg3n.js} +104 -47
  7. package/dist/cli/chunk-1rkxzg3n.js.map +36 -0
  8. package/dist/cli/{chunk-desvm72z.js → chunk-2nr29y95.js} +22 -26
  9. package/dist/cli/chunk-2nr29y95.js.map +13 -0
  10. package/dist/cli/{chunk-32khwcb6.js → chunk-49r370mb.js} +42 -69
  11. package/dist/cli/{chunk-32khwcb6.js.map → chunk-49r370mb.js.map} +9 -10
  12. package/dist/cli/{chunk-ms9z66nz.js → chunk-4rftmttv.js} +2 -2
  13. package/dist/cli/{chunk-s0bdnc7v.js → chunk-79bzzkx4.js} +5 -4
  14. package/dist/cli/chunk-79bzzkx4.js.map +10 -0
  15. package/dist/cli/{chunk-hvcxng5f.js → chunk-7tekknev.js} +6 -6
  16. package/dist/cli/{chunk-dv2z0rzf.js → chunk-8b489v1b.js} +38 -13
  17. package/dist/cli/{chunk-dv2z0rzf.js.map → chunk-8b489v1b.js.map} +5 -4
  18. package/dist/cli/{chunk-0pmb4tr4.js → chunk-8f8xw4x2.js} +1195 -222
  19. package/dist/cli/chunk-8f8xw4x2.js.map +44 -0
  20. package/dist/cli/chunk-96snx8yy.js +329 -0
  21. package/dist/cli/chunk-96snx8yy.js.map +13 -0
  22. package/dist/cli/{chunk-ckrcm08h.js → chunk-a3ydrqny.js} +48 -62
  23. package/dist/cli/{chunk-ckrcm08h.js.map → chunk-a3ydrqny.js.map} +4 -5
  24. package/dist/cli/{chunk-bxwhtvw8.js → chunk-ahc5gc9h.js} +5 -5
  25. package/dist/cli/{chunk-dpy8qdjf.js → chunk-as09pafp.js} +73 -182
  26. package/dist/cli/chunk-as09pafp.js.map +18 -0
  27. package/dist/cli/{chunk-tfycddbt.js → chunk-bjk964fp.js} +2 -2
  28. package/dist/cli/{chunk-gfc5fqvf.js → chunk-br0n8n3e.js} +18 -18
  29. package/dist/cli/{chunk-gfc5fqvf.js.map → chunk-br0n8n3e.js.map} +1 -1
  30. package/dist/cli/{chunk-297ajdcv.js → chunk-c69tw29j.js} +6 -6
  31. package/dist/cli/{chunk-297ajdcv.js.map → chunk-c69tw29j.js.map} +3 -3
  32. package/dist/cli/chunk-cn3dy3hz.js +146 -0
  33. package/dist/cli/chunk-cn3dy3hz.js.map +11 -0
  34. package/dist/cli/{chunk-124fvmzq.js → chunk-dm4mg9bj.js} +11064 -8742
  35. package/dist/cli/{chunk-124fvmzq.js.map → chunk-dm4mg9bj.js.map} +78 -64
  36. package/dist/cli/{chunk-dqhg54mr.js → chunk-dmx6cezx.js} +1 -1
  37. package/dist/cli/{chunk-60hz265r.js → chunk-dpcahcrb.js} +24 -11
  38. package/dist/cli/chunk-dpcahcrb.js.map +10 -0
  39. package/dist/cli/{chunk-a57yh0h9.js → chunk-dyk3r7jw.js} +7 -7
  40. package/dist/cli/{chunk-a57yh0h9.js.map → chunk-dyk3r7jw.js.map} +1 -1
  41. package/dist/cli/{chunk-twy7psdz.js → chunk-enk76qd5.js} +14 -10
  42. package/dist/cli/chunk-enk76qd5.js.map +10 -0
  43. package/dist/cli/{chunk-k3gr1nm9.js → chunk-fqy4n3q2.js} +13 -16
  44. package/dist/cli/{chunk-k3gr1nm9.js.map → chunk-fqy4n3q2.js.map} +3 -3
  45. package/dist/cli/{chunk-sc93apfs.js → chunk-frk2wdq1.js} +17 -17
  46. package/dist/cli/{chunk-sc93apfs.js.map → chunk-frk2wdq1.js.map} +2 -2
  47. package/dist/cli/{chunk-kdpzw38a.js → chunk-gprwej7h.js} +95 -7
  48. package/dist/cli/chunk-gprwej7h.js.map +11 -0
  49. package/dist/cli/chunk-kkrteb15.js +157 -0
  50. package/dist/cli/chunk-kkrteb15.js.map +10 -0
  51. package/dist/cli/{chunk-ave1wcbr.js → chunk-m9bp2hc1.js} +1 -1
  52. package/dist/cli/{chunk-58bt3hag.js → chunk-mrvh4c88.js} +4 -4
  53. package/dist/cli/{chunk-5cmts6dv.js → chunk-p8zg3a2h.js} +7 -7
  54. package/dist/cli/{chunk-5cmts6dv.js.map → chunk-p8zg3a2h.js.map} +1 -1
  55. package/dist/cli/{chunk-0w6hs479.js → chunk-rkqae7kh.js} +30 -18
  56. package/dist/cli/chunk-rkqae7kh.js.map +10 -0
  57. package/dist/cli/{chunk-bqxaqn5p.js → chunk-t5xx74rd.js} +8 -8
  58. package/dist/cli/{chunk-bqxaqn5p.js.map → chunk-t5xx74rd.js.map} +1 -1
  59. package/dist/cli/{chunk-1f882900.js → chunk-th0pgaj5.js} +2 -2
  60. package/dist/cli/{chunk-dhwtqnnd.js → chunk-vnbvhgmd.js} +2 -2
  61. package/dist/cli/{chunk-gxtg6ed2.js → chunk-vzy9rh6y.js} +21 -21
  62. package/dist/cli/{chunk-gxtg6ed2.js.map → chunk-vzy9rh6y.js.map} +3 -3
  63. package/dist/cli/{chunk-j1x92mx9.js → chunk-w6e455pz.js} +1 -1
  64. package/dist/cli/chunk-wehv26ry.js +155 -0
  65. package/dist/cli/chunk-wehv26ry.js.map +11 -0
  66. package/dist/cli/{chunk-y4msrwm9.js → chunk-xgq4eytj.js} +15 -15
  67. package/dist/cli/{chunk-36bxs9c7.js → chunk-xt0qvz1t.js} +27 -23
  68. package/dist/cli/chunk-xt0qvz1t.js.map +10 -0
  69. package/dist/cli/index.js +78 -42
  70. package/dist/cli/index.js.map +7 -6
  71. package/dist/types/ai/changelog-components.d.ts +18 -0
  72. package/dist/types/ai/changelog-markdown.d.ts +12 -0
  73. package/dist/types/ai/openapi-components.d.ts +7 -7
  74. package/dist/types/ai/operation-details.d.ts +39 -0
  75. package/dist/types/ai/relative-links.d.ts +3 -1
  76. package/dist/types/ai/serializers.d.ts +6 -4
  77. package/dist/types/analytics/head.d.ts +4 -2
  78. package/dist/types/analytics/script.d.ts +8 -2
  79. package/dist/types/components/content/youtube.d.ts +20 -0
  80. package/dist/types/components/layout/nav-utils.d.ts +8 -0
  81. package/dist/types/components/openapi/example-panels.d.ts +33 -0
  82. package/dist/types/components/openapi/helpers.d.ts +53 -11
  83. package/dist/types/components/openapi/snippets.d.ts +7 -1
  84. package/dist/types/core/alert-diagnostics.d.ts +10 -0
  85. package/dist/types/core/builtin-tags.d.ts +16 -0
  86. package/dist/types/core/changelog-list.d.ts +48 -0
  87. package/dist/types/core/closest-match.d.ts +6 -0
  88. package/dist/types/core/code-fence-diagnostics.d.ts +18 -0
  89. package/dist/types/core/code-fences.d.ts +8 -1
  90. package/dist/types/core/config-input.d.ts +1 -1
  91. package/dist/types/core/content-assets.d.ts +16 -18
  92. package/dist/types/core/diagnostics.d.ts +14 -0
  93. package/dist/types/core/directive-diagnostics.d.ts +22 -8
  94. package/dist/types/core/former-routes.d.ts +14 -0
  95. package/dist/types/core/github.d.ts +5 -4
  96. package/dist/types/core/heading-markers.d.ts +65 -2
  97. package/dist/types/core/hidden-pages.d.ts +3 -0
  98. package/dist/types/core/includes.d.ts +23 -0
  99. package/dist/types/core/last-modified.d.ts +18 -6
  100. package/dist/types/core/links.d.ts +42 -2
  101. package/dist/types/core/load-module.d.ts +19 -4
  102. package/dist/types/core/manifest.d.ts +14 -1
  103. package/dist/types/core/markdown-mirrors.d.ts +22 -0
  104. package/dist/types/core/mdx-elements.d.ts +32 -0
  105. package/dist/types/core/mdx-syntax.d.ts +100 -0
  106. package/dist/types/core/meta-diagnostics.d.ts +7 -0
  107. package/dist/types/core/meta.d.ts +32 -5
  108. package/dist/types/core/nav-diagnostics.d.ts +9 -2
  109. package/dist/types/core/navigation.d.ts +13 -0
  110. package/dist/types/core/ordering-prefix.d.ts +6 -0
  111. package/dist/types/core/pinned-ids.d.ts +23 -0
  112. package/dist/types/core/project-graph.d.ts +9 -1
  113. package/dist/types/core/redirect-patterns.d.ts +18 -0
  114. package/dist/types/core/relative-files.d.ts +31 -0
  115. package/dist/types/core/schema.d.ts +267 -7
  116. package/dist/types/core/sources/normalize.d.ts +101 -9
  117. package/dist/types/core/sources/types.d.ts +16 -0
  118. package/dist/types/core/static-files.d.ts +10 -0
  119. package/dist/types/core/static-host.d.ts +42 -0
  120. package/dist/types/core/svg-images.d.ts +26 -0
  121. package/dist/types/core/syntax-diagnostics.d.ts +14 -0
  122. package/dist/types/core/types.d.ts +42 -1
  123. package/dist/types/deploy/generated-files.d.ts +41 -0
  124. package/dist/types/deploy/redirects.d.ts +21 -7
  125. package/dist/types/markdown/code-title.d.ts +9 -5
  126. package/dist/types/markdown/directives.d.ts +8 -0
  127. package/dist/types/markdown/fence-language.d.ts +51 -0
  128. package/dist/types/markdown/fence-meta.d.ts +5 -0
  129. package/dist/types/markdown/github-alerts.d.ts +35 -0
  130. package/dist/types/openapi/asyncapi.d.ts +6 -5
  131. package/dist/types/openapi/checks.d.ts +10 -5
  132. package/dist/types/openapi/model.d.ts +55 -8
  133. package/dist/types/openapi/parse.d.ts +4 -1
  134. package/dist/types/sources/filesystem.d.ts +14 -4
  135. package/dist/types/sources/mdx-remote.d.ts +6 -3
  136. package/dist/types/sources/registry.d.ts +3 -3
  137. package/dist/types/theme/color.d.ts +33 -0
  138. package/dist/types/theme/palette.d.ts +122 -0
  139. package/docs/01-quickstart.mdx +1 -1
  140. package/docs/02-deployment.mdx +23 -8
  141. package/docs/04-migrating.mdx +16 -4
  142. package/docs/advanced/changelog.mdx +27 -1
  143. package/docs/advanced/skills.mdx +1 -1
  144. package/docs/cli/audit.mdx +1 -1
  145. package/docs/cli/doctor.mdx +2 -2
  146. package/docs/cli/evals.mdx +1 -1
  147. package/docs/cli/index.mdx +4 -2
  148. package/docs/cli/validate.mdx +10 -7
  149. package/docs/configuration/analytics.mdx +2 -0
  150. package/docs/configuration/customization.mdx +3 -1
  151. package/docs/configuration/index.mdx +2 -2
  152. package/docs/configuration/theming.mdx +5 -1
  153. package/docs/content/components.mdx +26 -6
  154. package/docs/content/frontmatter.mdx +1 -1
  155. package/docs/content/includes.mdx +6 -2
  156. package/docs/content/index.mdx +11 -1
  157. package/docs/content/islands.mdx +2 -0
  158. package/docs/content/meta.mdx +9 -3
  159. package/docs/content/navigation.mdx +14 -6
  160. package/docs/content/sources/obsidian.mdx +1 -1
  161. package/docs/content/sources/remote-mdx.mdx +3 -1
  162. package/docs/content/syntax.mdx +76 -8
  163. package/docs/content/variables.mdx +3 -1
  164. package/docs/discoverability/agent-discovery.mdx +1 -1
  165. package/docs/discoverability/json-api.mdx +1 -1
  166. package/docs/discoverability/llms-txt.mdx +1 -1
  167. package/docs/discoverability/markdown.mdx +2 -2
  168. package/docs/discoverability/open-graph.mdx +1 -1
  169. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  170. package/docs/references/graphql.mdx +2 -0
  171. package/docs/references/openapi.mdx +45 -7
  172. package/package.json +4 -4
  173. package/skills/blume/SKILL.md +2 -2
  174. package/skills/blume-migrate/SKILL.md +49 -27
  175. package/skills/blume-migrate/references/docsify.md +262 -0
  176. package/skills/blume-migrate/references/docus.md +298 -0
  177. package/skills/blume-migrate/references/docusaurus.md +3 -3
  178. package/skills/blume-migrate/references/fern.md +327 -0
  179. package/skills/blume-migrate/references/fumadocs.md +1 -1
  180. package/skills/blume-migrate/references/gitbook.md +267 -0
  181. package/skills/blume-migrate/references/github-wiki.md +259 -0
  182. package/skills/blume-migrate/references/jekyll.md +253 -0
  183. package/skills/blume-migrate/references/mdbook.md +256 -0
  184. package/skills/blume-migrate/references/mintlify.md +5 -5
  185. package/skills/blume-migrate/references/mkdocs.md +236 -0
  186. package/skills/blume-migrate/references/monorepo.md +20 -8
  187. package/skills/blume-migrate/references/nextra.md +2 -2
  188. package/skills/blume-migrate/references/readme.md +257 -0
  189. package/skills/blume-migrate/references/redocly.md +198 -0
  190. package/skills/blume-migrate/references/starlight.md +3 -3
  191. package/skills/blume-migrate/references/vitepress.md +204 -0
  192. package/skills/blume-migrate/references/vuepress.md +271 -0
  193. package/skills/blume-migrate/scripts/docsify-codemod.mjs +2387 -0
  194. package/skills/blume-migrate/scripts/docus-codemod.mjs +2542 -0
  195. package/skills/blume-migrate/scripts/fern-codemod.mjs +2633 -0
  196. package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +2808 -0
  197. package/skills/blume-migrate/scripts/include-excerpts.mjs +649 -0
  198. package/skills/blume-migrate/scripts/jekyll-codemod.mjs +3649 -0
  199. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +17 -13
  200. package/skills/blume-migrate/scripts/mkdocs-codemod.mjs +2526 -0
  201. package/skills/blume-migrate/scripts/operation-routes.mjs +508 -0
  202. package/skills/blume-migrate/scripts/pin-heading-ids.mjs +1049 -0
  203. package/skills/blume-migrate/scripts/readme-codemod.mjs +4139 -0
  204. package/skills/blume-migrate/scripts/vitepress-codemod.mjs +1511 -0
  205. package/src/ai/changelog-components.ts +24 -0
  206. package/src/ai/changelog-markdown.ts +45 -41
  207. package/src/ai/component-markdown.ts +22 -9
  208. package/src/ai/llms.ts +36 -5
  209. package/src/ai/markdown.ts +2 -2
  210. package/src/ai/openapi-components.ts +25 -9
  211. package/src/ai/operation-details.ts +208 -0
  212. package/src/ai/relative-links.ts +48 -8
  213. package/src/ai/serializers.ts +7 -6
  214. package/src/ai/site-skill.ts +6 -2
  215. package/src/analytics/head.ts +11 -5
  216. package/src/analytics/script.ts +13 -3
  217. package/src/astro/generate.ts +32 -6
  218. package/src/astro/index.ts +1 -0
  219. package/src/astro/integration.ts +27 -1
  220. package/src/astro/mdx-source-errors.ts +46 -0
  221. package/src/astro/templates.ts +89 -185
  222. package/src/audit/checks/redirects.ts +5 -1
  223. package/src/audit/redirects.ts +36 -19
  224. package/src/audit/report.ts +22 -9
  225. package/src/audit/run.ts +12 -28
  226. package/src/audit/terms.ts +23 -0
  227. package/src/audit/url.ts +2 -2
  228. package/src/cli/build-failure.ts +256 -7
  229. package/src/cli/command-meta.ts +1 -1
  230. package/src/cli/commands/audit.ts +1 -1
  231. package/src/cli/commands/build.ts +12 -3
  232. package/src/cli/commands/check.ts +13 -2
  233. package/src/cli/commands/dev.ts +1 -1
  234. package/src/cli/commands/doctor.ts +4 -2
  235. package/src/cli/commands/init.ts +3 -3
  236. package/src/cli/commands/preview.ts +17 -1
  237. package/src/cli/commands/validate.ts +22 -10
  238. package/src/cli/prepare.ts +1 -1
  239. package/src/cli/preview-pages.ts +161 -0
  240. package/src/cli/unknown-flags.ts +1 -42
  241. package/src/components/code-copy.ts +51 -0
  242. package/src/components/code-language.ts +75 -0
  243. package/src/components/content/AccordionItem.astro +16 -1
  244. package/src/components/content/Callout.astro +11 -1
  245. package/src/components/content/Changelog.astro +189 -0
  246. package/src/components/content/CodeBlock.astro +8 -11
  247. package/src/components/content/Tabs.astro +16 -2
  248. package/src/components/content/YouTube.astro +6 -4
  249. package/src/components/content/code-block.ts +33 -0
  250. package/src/components/content/content-strings.ts +5 -1
  251. package/src/components/content/inline-markdown.ts +35 -8
  252. package/src/components/content/youtube.ts +49 -7
  253. package/src/components/layout/Analytics.astro +5 -1
  254. package/src/components/layout/NavTree.astro +8 -3
  255. package/src/components/layout/RootLayout.astro +18 -31
  256. package/src/components/layout/nav-utils.ts +14 -0
  257. package/src/components/layout/search/pagefind.ts +2 -1
  258. package/src/components/openapi/Operation.astro +4 -0
  259. package/src/components/openapi/PanelTabs.astro +9 -3
  260. package/src/components/openapi/RequestBody.astro +42 -8
  261. package/src/components/openapi/RequestPanel.astro +34 -53
  262. package/src/components/openapi/example-panels.ts +110 -0
  263. package/src/components/openapi/helpers.ts +195 -20
  264. package/src/components/openapi/operation-model.ts +63 -26
  265. package/src/components/openapi/request.ts +13 -0
  266. package/src/components/openapi/snippets.ts +14 -2
  267. package/src/core/alert-diagnostics.ts +89 -0
  268. package/src/core/builtin-tags.ts +76 -0
  269. package/src/core/changelog-list.ts +87 -0
  270. package/src/core/closest-match.ts +41 -0
  271. package/src/core/code-fence-diagnostics.ts +267 -0
  272. package/src/core/code-fences.ts +43 -7
  273. package/src/core/component-diagnostics.ts +65 -12
  274. package/src/core/config-input.ts +1 -1
  275. package/src/core/config.ts +46 -1
  276. package/src/core/content-assets.ts +64 -144
  277. package/src/core/diagnostics.ts +36 -3
  278. package/src/core/directive-diagnostics.ts +265 -47
  279. package/src/core/former-routes.ts +46 -0
  280. package/src/core/git-ignored.ts +53 -0
  281. package/src/core/github.ts +5 -4
  282. package/src/core/heading-markers.ts +156 -2
  283. package/src/core/hidden-pages.ts +64 -0
  284. package/src/core/includes.ts +181 -41
  285. package/src/core/last-modified.ts +47 -10
  286. package/src/core/links.ts +438 -83
  287. package/src/core/load-module.ts +98 -9
  288. package/src/core/manifest.ts +12 -9
  289. package/src/core/markdown-mirrors.ts +52 -0
  290. package/src/core/mdx-elements.ts +153 -0
  291. package/src/core/mdx-syntax.ts +377 -0
  292. package/src/core/meta-diagnostics.ts +134 -0
  293. package/src/core/meta.ts +154 -55
  294. package/src/core/nav-diagnostics.ts +16 -4
  295. package/src/core/navigation.ts +70 -10
  296. package/src/core/ordering-prefix.ts +22 -2
  297. package/src/core/pinned-ids.ts +141 -0
  298. package/src/core/project-graph.ts +221 -29
  299. package/src/core/redirect-patterns.ts +41 -0
  300. package/src/core/relative-files.ts +145 -0
  301. package/src/core/schema.ts +17 -3
  302. package/src/core/sources/mdx-remote.ts +75 -14
  303. package/src/core/sources/normalize.ts +885 -179
  304. package/src/core/sources/notion.ts +5 -4
  305. package/src/core/sources/obsidian.ts +2 -5
  306. package/src/core/sources/types.ts +17 -0
  307. package/src/core/static-files.ts +40 -0
  308. package/src/core/static-host.ts +125 -0
  309. package/src/core/svg-images.ts +98 -0
  310. package/src/core/syntax-diagnostics.ts +491 -0
  311. package/src/core/types.ts +43 -1
  312. package/src/deploy/artifacts.ts +2 -28
  313. package/src/deploy/generated-files.ts +128 -0
  314. package/src/deploy/platforms/vercel.ts +25 -1
  315. package/src/deploy/redirects.ts +43 -16
  316. package/src/deploy/sitemap.ts +2 -1
  317. package/src/markdown/base-links.ts +87 -6
  318. package/src/markdown/code-title.ts +29 -13
  319. package/src/markdown/content-assets.ts +210 -0
  320. package/src/markdown/directives.ts +69 -5
  321. package/src/markdown/fence-language.ts +106 -0
  322. package/src/markdown/fence-meta.ts +42 -0
  323. package/src/markdown/github-alerts.ts +172 -0
  324. package/src/markdown/heading-anchors.ts +265 -56
  325. package/src/markdown/include.ts +30 -3
  326. package/src/markdown/index.ts +145 -40
  327. package/src/markdown/prompt-text.ts +95 -0
  328. package/src/markdown/relative-links.ts +37 -2
  329. package/src/markdown/route-snapshot.ts +22 -12
  330. package/src/markdown/svg-fallback.ts +51 -0
  331. package/src/markdown/title-heading.ts +134 -0
  332. package/src/migrate/migrate.ts +127 -2
  333. package/src/og/card.ts +14 -4
  334. package/src/openapi/asyncapi.ts +14 -6
  335. package/src/openapi/checks.ts +160 -15
  336. package/src/openapi/model.ts +172 -33
  337. package/src/openapi/parse.ts +13 -4
  338. package/src/openapi/render-mdx.ts +55 -9
  339. package/src/openapi/source.ts +87 -6
  340. package/src/registry/eject.ts +0 -1
  341. package/src/search/documents.ts +10 -2
  342. package/src/sources/filesystem.ts +28 -3
  343. package/src/sources/mdx-remote.ts +7 -15
  344. package/src/theme/color.ts +14 -0
  345. package/src/theme/entry.ts +22 -1
  346. package/src/theme/palette.ts +74 -1
  347. package/dist/cli/chunk-0pmb4tr4.js.map +0 -35
  348. package/dist/cli/chunk-0w6hs479.js.map +0 -10
  349. package/dist/cli/chunk-150jk7km.js +0 -52
  350. package/dist/cli/chunk-150jk7km.js.map +0 -10
  351. package/dist/cli/chunk-36bxs9c7.js.map +0 -10
  352. package/dist/cli/chunk-53jqw8z0.js +0 -89
  353. package/dist/cli/chunk-53jqw8z0.js.map +0 -10
  354. package/dist/cli/chunk-60hz265r.js.map +0 -10
  355. package/dist/cli/chunk-desvm72z.js.map +0 -14
  356. package/dist/cli/chunk-dpy8qdjf.js.map +0 -17
  357. package/dist/cli/chunk-es8h0v3j.js +0 -441
  358. package/dist/cli/chunk-es8h0v3j.js.map +0 -11
  359. package/dist/cli/chunk-kdpzw38a.js.map +0 -11
  360. package/dist/cli/chunk-nbyvgnnm.js.map +0 -36
  361. package/dist/cli/chunk-s0bdnc7v.js.map +0 -10
  362. package/dist/cli/chunk-twy7psdz.js.map +0 -10
  363. package/dist/cli/chunk-ydp3065p.js +0 -64
  364. package/dist/cli/chunk-ydp3065p.js.map +0 -10
  365. /package/dist/cli/{chunk-74wfhv79.js.map → chunk-05f21r8h.js.map} +0 -0
  366. /package/dist/cli/{chunk-9qzgg6mj.js.map → chunk-1cm0h819.js.map} +0 -0
  367. /package/dist/cli/{chunk-ms9z66nz.js.map → chunk-4rftmttv.js.map} +0 -0
  368. /package/dist/cli/{chunk-hvcxng5f.js.map → chunk-7tekknev.js.map} +0 -0
  369. /package/dist/cli/{chunk-bxwhtvw8.js.map → chunk-ahc5gc9h.js.map} +0 -0
  370. /package/dist/cli/{chunk-tfycddbt.js.map → chunk-bjk964fp.js.map} +0 -0
  371. /package/dist/cli/{chunk-dqhg54mr.js.map → chunk-dmx6cezx.js.map} +0 -0
  372. /package/dist/cli/{chunk-ave1wcbr.js.map → chunk-m9bp2hc1.js.map} +0 -0
  373. /package/dist/cli/{chunk-58bt3hag.js.map → chunk-mrvh4c88.js.map} +0 -0
  374. /package/dist/cli/{chunk-1f882900.js.map → chunk-th0pgaj5.js.map} +0 -0
  375. /package/dist/cli/{chunk-dhwtqnnd.js.map → chunk-vnbvhgmd.js.map} +0 -0
  376. /package/dist/cli/{chunk-j1x92mx9.js.map → chunk-w6e455pz.js.map} +0 -0
  377. /package/dist/cli/{chunk-y4msrwm9.js.map → chunk-xgq4eytj.js.map} +0 -0
@@ -1,43 +1,191 @@
1
- import type { Nodes } from "mdast";
2
- import { mdxToMdast } from "satteri";
1
+ import type { Nodes, Paragraph } from "mdast";
2
+ import { markdownToMdast, mdxToMdast } from "satteri";
3
3
 
4
4
  import {
5
5
  CALLOUT_ALIASES,
6
6
  CALLOUT_TYPES,
7
7
  calloutTypeFor,
8
8
  } from "../markdown/directives.ts";
9
- import { MDX_BODY_FEATURES } from "../markdown/features.ts";
9
+ import {
10
+ MARKDOWN_BODY_FEATURES,
11
+ MDX_BODY_FEATURES,
12
+ } from "../markdown/features.ts";
10
13
  import { strippedLineOffset } from "./sources/normalize.ts";
11
14
  import type { SourceEntry } from "./sources/types.ts";
12
15
  import type { Diagnostic } from "./types.ts";
13
16
 
17
+ type ContainerDirective = Extract<Nodes, { type: "containerDirective" }>;
18
+
14
19
  // A line that may open a container directive: a colon fence and a name, after
15
- // any quote or list indentation. A page is parsed only when one of these
16
- // names something other than a callout, so pages without one cost a scan.
20
+ // any quote or list indentation.
17
21
  const CONTAINER_OPENING = /^[\t >]*:{3,}(?<name>[a-z][\w-]*)/gimu;
18
22
 
23
+ // A colon fence followed by text that opens no directive: text after a space
24
+ // (`::: card`), or a character no name starts with (`:::{.x}`). Such a line
25
+ // either closes a container and loses its text, or is a spaced opener.
26
+ const FENCE_WITH_TEXT = /^[\t >]*:{3,}(?:[\t ]+\S|[^\s:a-z])/imu;
27
+
28
+ // A container's closing line: the colon fence and whatever follows it.
29
+ const CLOSING_LINE =
30
+ /^[\t >]*(?<written>(?<fence>:{3,})[\t ]*(?<text>.*?))\s*$/u;
31
+
32
+ // A container opener with text after its name, and after any `[label]` or
33
+ // `{attributes}`: `:::tip Some title`. Sätteri drops that text.
34
+ const OPENING_TEXT =
35
+ /^[\t >]*(?<written>(?<fence>:{3,})(?<name>[a-z][\w-]*)(?<label>\[[^\]\n]*\])?(?<attributes>\{[^}\n]*\})?[\t ]+(?<text>\S.*?))\s*$/iu;
36
+ const OPENING_TEXT_LINE =
37
+ /^[\t >]*:{3,}[a-z][\w-]*(?:\[[^\]\n]*\])?(?:\{[^}\n]*\})?[\t ]+\S/imu;
38
+
39
+ // A markdown-it style opener, `::: tip Title`, which is no directive at all.
40
+ const SPACED_OPENING =
41
+ /^[\t >]*(?<written>(?<fence>:{3,})[\t ]+(?<name>[a-z][\w-]*)(?<title>.*?))\s*$/iu;
42
+
43
+ // A container opener's colon fence and name, after any quote or list
44
+ // indentation, as one line of a `.md` paragraph holds it.
45
+ const MD_OPENING = /^[\t >]*(?<written>:{3,}(?<name>[a-z][\w-]*))/iu;
46
+ const MD_OPENING_LINE = /^[\t >]*:{3,}[a-z]/imu;
47
+
19
48
  const codeList = (names: Iterable<string>): string =>
20
49
  [...names].map((name) => `\`${name}\``).join(", ");
21
50
 
22
51
  const CALLOUT_NAMES = `${codeList(CALLOUT_TYPES)} (or the aliases ${codeList(Object.keys(CALLOUT_ALIASES))})`;
23
52
 
24
- /** A container directive that isn't a callout, and its line in `text`. */
25
- interface UnknownContainer {
53
+ /** A directive problem at a line of the parsed text, before it's placed in a file. */
54
+ interface Finding {
55
+ code: string;
26
56
  line: number;
27
- name: string;
57
+ message: string;
58
+ suggestion: string;
28
59
  }
29
60
 
61
+ const unknownContainer = (node: ContainerDirective): Finding[] =>
62
+ calloutTypeFor(node.name) === null
63
+ ? [
64
+ {
65
+ code: "BLUME_UNKNOWN_DIRECTIVE",
66
+ line: node.position?.start.line ?? 1,
67
+ message: `\`:::${node.name}\` isn't a callout type, so the page shows its \`:::\` lines as written around its content.`,
68
+ suggestion: `Use a callout type — ${CALLOUT_NAMES} — or remove the \`:::\` lines to keep the content as plain prose.`,
69
+ },
70
+ ]
71
+ : [];
72
+
73
+ /**
74
+ * Text after the colons of a container's closing fence. Sätteri closes a
75
+ * container on any line that starts with at least its fence's colons and
76
+ * drops the rest of that line (micromark's directive grammar, which
77
+ * `remark-directive` uses, keeps such a line as content), so a `::: card`
78
+ * inside a `:::warning` ends the callout early and `card` never reaches the
79
+ * page. A container left unclosed ends with its content rather than on a
80
+ * fence line of its own.
81
+ */
82
+ const closingText = (
83
+ node: ContainerDirective,
84
+ lines: readonly string[]
85
+ ): Finding[] => {
86
+ const start = node.position?.start.line ?? 1;
87
+ const end = node.position?.end.line ?? start;
88
+ const content = node.children.at(-1)?.position?.end.line ?? start;
89
+ const groups =
90
+ end > content ? CLOSING_LINE.exec(lines[end - 1] ?? "")?.groups : undefined;
91
+ if (!(groups?.text && groups.fence && groups.written)) {
92
+ return [];
93
+ }
94
+ const longer = ":".repeat(groups.fence.length + 1);
95
+ return [
96
+ {
97
+ code: "BLUME_DIRECTIVE_CLOSING_TEXT",
98
+ line: end,
99
+ message: `\`${groups.written}\` closes the \`:::${node.name}\` container above it, so \`${groups.text}\` never reaches the page.`,
100
+ 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.`,
101
+ },
102
+ ];
103
+ };
104
+
105
+ /**
106
+ * Text after a callout opener's name (`:::tip Some title`). Sätteri ends the
107
+ * name at the space and drops the rest of the line, so the callout renders
108
+ * untitled and the text never shows. Docusaurus and the markdown-it
109
+ * containers VitePress uses read that text as the title, which is where the
110
+ * spelling comes from. Only callouts count: any other container renders its
111
+ * opening line as written (see `markdown/directives.ts`), text and all.
112
+ */
113
+ const openingText = (
114
+ node: ContainerDirective,
115
+ lines: readonly string[]
116
+ ): Finding[] => {
117
+ const line = node.position?.start.line ?? 1;
118
+ const groups = OPENING_TEXT.exec(lines[line - 1] ?? "")?.groups;
119
+ if (
120
+ !(groups?.fence && groups.name && groups.text && groups.written) ||
121
+ calloutTypeFor(node.name) === null
122
+ ) {
123
+ return [];
124
+ }
125
+ const opener = `${groups.fence}${groups.name}`;
126
+ return [
127
+ {
128
+ code: "BLUME_DIRECTIVE_OPENING_TEXT",
129
+ line,
130
+ message: `\`${groups.written}\` opens a \`${groups.name}\` callout, but the text after its name, \`${groups.text}\`, never reaches the page.`,
131
+ suggestion:
132
+ groups.label || groups.attributes
133
+ ? `Move \`${groups.text}\` into the callout's title, or onto the next line as its text.`
134
+ : `Put the title in brackets: \`${opener}[${groups.text}]\`.`,
135
+ },
136
+ ];
137
+ };
138
+
139
+ /**
140
+ * Callout openers written with a space after the colons (`::: tip`, the
141
+ * markdown-it spelling VitePress, VuePress, and Docusaurus v2 use). That is
142
+ * never a directive, so the line and its whole block render as plain text.
143
+ * Only paragraph lines count, so an example in a code block stays quiet.
144
+ */
145
+ const spacedOpenings = (
146
+ node: Paragraph,
147
+ lines: readonly string[]
148
+ ): Finding[] => {
149
+ const start = node.position?.start.line ?? 1;
150
+ const end = node.position?.end.line ?? start;
151
+ return lines.slice(start - 1, end).flatMap((text, index) => {
152
+ const groups = SPACED_OPENING.exec(text)?.groups;
153
+ if (
154
+ !(groups?.fence && groups.name && groups.written) ||
155
+ calloutTypeFor(groups.name) === null
156
+ ) {
157
+ return [];
158
+ }
159
+ const opener = `${groups.fence}${groups.name}`;
160
+ const title = groups.title?.trim();
161
+ return [
162
+ {
163
+ code: "BLUME_DIRECTIVE_SPACED_NAME",
164
+ line: start + index,
165
+ 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.`,
166
+ suggestion: title
167
+ ? `Remove the space and put the title in brackets: \`${opener}[${title}]\`.`
168
+ : `Remove the space: \`${opener}\`.`,
169
+ },
170
+ ];
171
+ });
172
+ };
173
+
30
174
  /**
31
- * Every container directive in an `.mdx` body whose name isn't a callout, in
32
- * source order. The body is parsed as the renderer reads it, so a fence in a
33
- * code block or a `::: name` that isn't a directive at all never counts. A
34
- * body MDX can't parse fails to render anyway, with its own error.
175
+ * The directive problems in an `.mdx` body, in source order. The body is
176
+ * parsed as the renderer reads it, so a fence in a code block never counts. A
177
+ * page is parsed only when a line looks like one of these problems, so pages
178
+ * without one cost a scan. A body MDX can't parse fails to render anyway, with
179
+ * its own error.
35
180
  */
36
- const unknownContainers = (text: string): UnknownContainer[] => {
37
- const named = [...text.matchAll(CONTAINER_OPENING)].some(
38
- (match) => calloutTypeFor(match.groups?.name ?? "") === null
39
- );
40
- if (!named) {
181
+ const directiveFindings = (text: string): Finding[] => {
182
+ const suspect =
183
+ FENCE_WITH_TEXT.test(text) ||
184
+ OPENING_TEXT_LINE.test(text) ||
185
+ [...text.matchAll(CONTAINER_OPENING)].some(
186
+ (match) => calloutTypeFor(match.groups?.name ?? "") === null
187
+ );
188
+ if (!suspect) {
41
189
  return [];
42
190
  }
43
191
  let tree: Nodes;
@@ -46,13 +194,17 @@ const unknownContainers = (text: string): UnknownContainer[] => {
46
194
  } catch {
47
195
  return [];
48
196
  }
49
- const found: UnknownContainer[] = [];
197
+ const lines = text.split("\n");
198
+ const found: Finding[] = [];
50
199
  const walk = (node: Nodes): void => {
51
- if (
52
- node.type === "containerDirective" &&
53
- calloutTypeFor(node.name) === null
54
- ) {
55
- found.push({ line: node.position?.start.line ?? 1, name: node.name });
200
+ if (node.type === "containerDirective") {
201
+ found.push(
202
+ ...unknownContainer(node),
203
+ ...openingText(node, lines),
204
+ ...closingText(node, lines)
205
+ );
206
+ } else if (node.type === "paragraph") {
207
+ found.push(...spacedOpenings(node, lines));
56
208
  }
57
209
  if ("children" in node) {
58
210
  for (const child of node.children) {
@@ -61,39 +213,105 @@ const unknownContainers = (text: string): UnknownContainer[] => {
61
213
  }
62
214
  };
63
215
  walk(tree);
216
+ return found.toSorted((a, b) => a.line - b.line);
217
+ };
218
+
219
+ /**
220
+ * Container directive openers on the lines of an `.md` paragraph (`:::note`).
221
+ * A `.md` page renders no directives, so the opener, its content, and its
222
+ * closing `:::` show as text. A directive in a code block is no paragraph.
223
+ */
224
+ const mdOpenings = (node: Paragraph, lines: readonly string[]): Finding[] => {
225
+ const start = node.position?.start.line ?? 1;
226
+ const end = node.position?.end.line ?? start;
227
+ return lines.slice(start - 1, end).flatMap((text, index) => {
228
+ const groups = MD_OPENING.exec(text)?.groups;
229
+ if (!(groups?.name && groups.written)) {
230
+ return [];
231
+ }
232
+ const callout = calloutTypeFor(groups.name);
233
+ return [
234
+ {
235
+ code: "BLUME_MD_DIRECTIVE",
236
+ line: start + index,
237
+ message: `\`${groups.written}\` opens a directive, which Blume renders only in .mdx, so this .md page shows it and its closing \`:::\` as text.`,
238
+ suggestion: callout
239
+ ? `Rename the page to .mdx to render it as a \`${callout}\` callout.`
240
+ : `Rename the page to .mdx and use a callout type — ${CALLOUT_NAMES} — or remove the \`:::\` lines to keep the content as plain prose.`,
241
+ },
242
+ ];
243
+ });
244
+ };
245
+
246
+ /**
247
+ * The directives in an `.md` body, in source order, read the way the
248
+ * renderer reads the page: as paragraphs, since `.md` has no directives. A
249
+ * page is parsed only when a line looks like an opener.
250
+ */
251
+ const mdDirectiveFindings = (text: string): Finding[] => {
252
+ if (!MD_OPENING_LINE.test(text)) {
253
+ return [];
254
+ }
255
+ const tree: Nodes = markdownToMdast(text, {
256
+ features: MARKDOWN_BODY_FEATURES,
257
+ });
258
+ const lines = text.split("\n");
259
+ const found: Finding[] = [];
260
+ const walk = (node: Nodes): void => {
261
+ if (node.type === "paragraph") {
262
+ found.push(...mdOpenings(node, lines));
263
+ } else if ("children" in node) {
264
+ for (const child of node.children) {
265
+ walk(child);
266
+ }
267
+ }
268
+ };
269
+ walk(tree);
64
270
  return found;
65
271
  };
66
272
 
67
273
  /**
68
- * Warn about each `:::name` container in an `.mdx` entry that isn't a callout.
69
- * The page keeps its content — the body renders between the literal `:::`
70
- * lines (see `markdown/directives.ts`) — but a typo like `:::warnig`, or a
71
- * `:::details` carried over from another docs tool, should read as the
72
- * mistake it is rather than as a finished page. Lines point into the file
73
- * the author wrote, a partial's own file for a container an `<include>`
74
- * brought in.
274
+ * Warn about the `:::` directives in an entry that don't render as their
275
+ * author meant. In `.mdx`:
276
+ *
277
+ * - `BLUME_UNKNOWN_DIRECTIVE`: a `:::name` container that isn't a callout.
278
+ * The page keeps its content — the body renders between the literal `:::`
279
+ * lines (see `markdown/directives.ts`) — but a typo like `:::warnig`, or a
280
+ * `:::details` carried over from another docs tool, should read as the
281
+ * mistake it is rather than as a finished page.
282
+ * - `BLUME_DIRECTIVE_OPENING_TEXT`: a callout opener with text after its
283
+ * name, which the page drops (`:::tip Some title`).
284
+ * - `BLUME_DIRECTIVE_CLOSING_TEXT`: a closing fence with text after it, which
285
+ * the page drops (`::: card` inside a `:::warning`).
286
+ * - `BLUME_DIRECTIVE_SPACED_NAME`: a spaced callout opener (`::: tip`), which
287
+ * the page shows as text.
288
+ *
289
+ * In `.md`, which renders no directives:
290
+ *
291
+ * - `BLUME_MD_DIRECTIVE`: a `:::name` opener, which the page shows as text.
292
+ *
293
+ * Lines point into the file the author wrote, a partial's own file for a
294
+ * directive an `<include>` brought in.
75
295
  */
76
- export const unknownDirectiveDiagnostics = (
296
+ export const directiveDiagnostics = (
77
297
  entry: SourceEntry,
78
298
  sourceName: string
79
299
  ): Diagnostic[] => {
80
- if (entry.body.format !== "mdx") {
81
- return [];
82
- }
83
300
  const page = entry.sourcePath ?? `${sourceName}:${entry.ref}`;
84
301
  const offset =
85
302
  entry.bodyLineOffset ?? strippedLineOffset(entry.raw, entry.body.text);
86
- return unknownContainers(entry.expanded?.text ?? entry.body.text).map(
87
- ({ line, name }) => {
88
- const origin = entry.expanded?.origins[line - 1];
89
- return {
90
- code: "BLUME_UNKNOWN_DIRECTIVE",
91
- file: origin?.file ?? page,
92
- line: origin?.line ?? line + offset,
93
- message: `\`:::${name}\` isn't a callout type, so the page shows its \`:::\` lines as written around its content.`,
94
- severity: "warning",
95
- suggestion: `Use a callout type — ${CALLOUT_NAMES} — or remove the \`:::\` lines to keep the content as plain prose.`,
96
- };
97
- }
98
- );
303
+ const text = entry.expanded?.text ?? entry.body.text;
304
+ const findings =
305
+ entry.body.format === "mdx"
306
+ ? directiveFindings(text)
307
+ : mdDirectiveFindings(text);
308
+ return findings.map(({ line, ...finding }) => {
309
+ const origin = entry.expanded?.origins[line - 1];
310
+ return {
311
+ ...finding,
312
+ file: origin?.file ?? page,
313
+ line: origin?.line ?? line + offset,
314
+ severity: "warning",
315
+ };
316
+ });
99
317
  };
@@ -0,0 +1,46 @@
1
+ import { normalizePath, stripBasePath, withBasePath } from "./base-path.ts";
2
+ import type { ResolvedConfig } from "./schema.ts";
3
+ import type { PageRecord } from "./types.ts";
4
+
5
+ type Redirect = ResolvedConfig["redirects"][number];
6
+
7
+ /**
8
+ * Redirects from the routes an earlier Blume published pages at
9
+ * (`PageRecord.formerRoute`) to where they publish now, so a date-named
10
+ * page's old URL keeps working. None is added where the old URL is still
11
+ * served (another page, or a custom page, lives there now), where an authored
12
+ * redirect already starts, or where two pages once resolved to it: that was a
13
+ * duplicate route, which failed the build, so the URL never served either.
14
+ * Both ends are written base-less, like authored redirects.
15
+ */
16
+ export const formerRouteRedirects = (
17
+ pages: readonly PageRecord[],
18
+ served: ReadonlySet<string>,
19
+ config: Pick<ResolvedConfig, "basePath" | "redirects">
20
+ ): Redirect[] => {
21
+ const targets = new Map<string, string | undefined>();
22
+ for (const page of pages) {
23
+ if (page.formerRoute !== undefined) {
24
+ targets.set(
25
+ page.formerRoute,
26
+ targets.has(page.formerRoute) ? undefined : page.route
27
+ );
28
+ }
29
+ }
30
+ const authored = new Set(
31
+ config.redirects.map((redirect) =>
32
+ normalizePath(withBasePath(config.basePath, redirect.from))
33
+ )
34
+ );
35
+ return [...targets].flatMap(([from, to]) =>
36
+ to === undefined || served.has(from) || authored.has(from)
37
+ ? []
38
+ : [
39
+ {
40
+ from: stripBasePath(config.basePath, from),
41
+ status: 301,
42
+ to: stripBasePath(config.basePath, to),
43
+ } satisfies Redirect,
44
+ ]
45
+ );
46
+ };
@@ -0,0 +1,53 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ import { isAbsolute, relative } from "pathe";
4
+
5
+ import { gitEnv, gitRepositoryRoot, realPath } from "./last-modified.ts";
6
+
7
+ /**
8
+ * The paths among `paths` that git ignores: a `.gitignore` rule matches them
9
+ * and the index doesn't track them, so they never reach the repository.
10
+ * Generated pages are the usual case, like a TypeDoc build written into a
11
+ * gitignored folder under the content root. One `git check-ignore` reads
12
+ * every path. Empty when git is unavailable or `root` isn't in a repository.
13
+ */
14
+ export const gitIgnoredPaths = (root: string, paths: string[]): Set<string> => {
15
+ const gitRoot = paths.length > 0 ? gitRepositoryRoot(root) : null;
16
+ if (gitRoot === null) {
17
+ return new Set();
18
+ }
19
+ // Git names paths by their real location (see `realPath`), and a path
20
+ // outside the repository would fail the whole call, so only the paths
21
+ // inside it are asked about, by their repository-relative spelling.
22
+ const top = realPath(gitRoot);
23
+ const byRepoPath = new Map<string, string>();
24
+ for (const path of paths) {
25
+ const rel = relative(top, realPath(path));
26
+ if (!(rel.startsWith("..") || isAbsolute(rel))) {
27
+ byRepoPath.set(rel, path);
28
+ }
29
+ }
30
+ try {
31
+ const output = execFileSync(
32
+ // oxlint-disable-next-line sonarjs/no-os-command-from-path -- git is a required dev-tool dependency resolved from PATH
33
+ "git",
34
+ ["-C", top, "check-ignore", "-z", "--stdin"],
35
+ {
36
+ encoding: "utf-8",
37
+ env: gitEnv(),
38
+ input: [...byRepoPath.keys()].map((rel) => `${rel}\0`).join(""),
39
+ maxBuffer: 256 * 1024 * 1024,
40
+ stdio: ["pipe", "pipe", "ignore"],
41
+ }
42
+ );
43
+ return new Set(
44
+ output.split("\0").flatMap((rel) => {
45
+ const path = byRepoPath.get(rel);
46
+ return path === undefined ? [] : [path];
47
+ })
48
+ );
49
+ } catch {
50
+ // `check-ignore` exits 1 when no path is ignored.
51
+ return new Set();
52
+ }
53
+ };
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Repository URL derivation for `config.github`.
3
3
  *
4
- * Every repo-derived link — the header mark, per-page edit links, the agent
5
- * manifest's `repository` — is built from `host`, which defaults to github.com
6
- * but accepts a GitHub Enterprise origin so self-hosted and data-resident
7
- * installations get working links instead of ones pointing at the public site.
4
+ * Every repo-derived link — the footer's repository link, per-page edit links,
5
+ * the agent manifest's `repository` — is built from `host`, which defaults to
6
+ * github.com but accepts a GitHub Enterprise origin so self-hosted and
7
+ * data-resident installations get working links instead of ones pointing at the
8
+ * public site.
8
9
  *
9
10
  * The REST base is derived from the host unless `api` is set explicitly, since
10
11
  * the two Enterprise flavors expose it differently: Enterprise Cloud with data
@@ -13,9 +13,12 @@
13
13
  * Both heading pipelines share this parser — the render-time hast plugin
14
14
  * (`markdown/heading-anchors.ts`) and the scan-time source scanner
15
15
  * (`core/sources/normalize.ts`) — so the rendered `id`, the TOC entry, and the
16
- * anchor index `blume validate` checks against always agree.
16
+ * anchor index `blume validate` checks against always agree. Both also escape
17
+ * a pinned id's punctuation before they parse (`core/pinned-ids.ts`), so
18
+ * smart punctuation never rewrites the id an author pinned.
17
19
  */
18
20
 
21
+ import { slug } from "github-slugger";
19
22
  import type GithubSlugger from "github-slugger";
20
23
 
21
24
  /**
@@ -25,6 +28,154 @@ import type GithubSlugger from "github-slugger";
25
28
  */
26
29
  export const TOC_HIDDEN_KEY = "__blumeTocHidden";
27
30
 
31
+ /**
32
+ * Frontmatter key carrying TOC text out of a render, keyed by slug, for the
33
+ * headings whose TOC entry must read differently from the heading's text
34
+ * content: a heading holding a `<Badge>` lists without the badge's text. The
35
+ * page template reads it back the way it reads {@link TOC_HIDDEN_KEY}.
36
+ */
37
+ export const TOC_TEXT_KEY = "__blumeTocText";
38
+
39
+ const TRAILING_DASHES = /-+$/u;
40
+
41
+ /**
42
+ * A heading's auto-generated anchor id, registered with the document slugger.
43
+ * The text is trimmed first: a heading that opens or ends with something that
44
+ * has no text (an inline component, an image, raw HTML) would otherwise slug
45
+ * that edge whitespace into a leading or trailing dash. A slug can still end
46
+ * in dashes when the slugger drops a symbol after a space (`Features ✨`);
47
+ * those go too, so no id ends in a dash.
48
+ */
49
+ export const headingSlug = (slugger: GithubSlugger, text: string): string => {
50
+ const trimmed = text.trim();
51
+ const base = slug(trimmed);
52
+ // A slug is stable under re-slugging, so the stripped one registers as is.
53
+ return slugger.slug(
54
+ base.endsWith("-") ? base.replace(TRAILING_DASHES, "") : trimmed
55
+ );
56
+ };
57
+
58
+ /**
59
+ * A heading's text with its badges taken out. `pieces` is the heading's text
60
+ * in order, with `null` where a badge stood; a badge between two words
61
+ * leaves one space, not the two around it (`Install <Badge>beta</Badge> now`
62
+ * reads `Install now`).
63
+ */
64
+ export const joinHeadingText = (pieces: readonly (string | null)[]): string => {
65
+ let text = "";
66
+ let gap = false;
67
+ for (const piece of pieces) {
68
+ if (piece === null) {
69
+ gap = true;
70
+ continue;
71
+ }
72
+ text += gap && /\s$/u.test(text) ? piece.replace(/^[\t ]+/u, "") : piece;
73
+ gap = false;
74
+ }
75
+ return text;
76
+ };
77
+
78
+ /** One raw HTML tag, as an inline `.md` (or scanned) heading holds it. */
79
+ interface RawTag {
80
+ /** The attribute source after the name, a self-closing `/` included. */
81
+ attributes: string;
82
+ closing: boolean;
83
+ name: string;
84
+ }
85
+
86
+ const RAW_TAG =
87
+ /^<(?<closing>\/)?(?<name>[A-Za-z][\w.-]*)(?<attributes>[^>]*)>$/u;
88
+
89
+ /** A raw inline HTML node's tag, or null when it holds something else. */
90
+ const rawTag = (value: string): RawTag | null => {
91
+ const groups = RAW_TAG.exec(value.trim())?.groups;
92
+ return groups?.name === undefined
93
+ ? null
94
+ : {
95
+ attributes: groups.attributes ?? "",
96
+ closing: groups.closing !== undefined,
97
+ name: groups.name,
98
+ };
99
+ };
100
+
101
+ const isSelfClosing = (tag: RawTag): boolean =>
102
+ tag.attributes.trimEnd().endsWith("/");
103
+
104
+ /**
105
+ * How a raw HTML node in a heading reads for its anchor: a `<Badge>` tag that
106
+ * opens or closes a run of badge text (a self-closing one is a badge with no
107
+ * text), the opening tag of an `<a>` (`target` its `id`, else its `name`),
108
+ * the `</a>` that closes one, or none of these.
109
+ */
110
+ export type RawHeadingTag =
111
+ | { kind: "anchor-close" }
112
+ | { kind: "anchor-open"; selfClosing: boolean; target?: string }
113
+ | { kind: "badge"; selfClosing: boolean; closing: boolean }
114
+ | { kind: "other" };
115
+
116
+ const ANCHOR_TARGET =
117
+ /(?:^|\s)(?<key>id|name)\s*=\s*(?:"(?<double>[^"]*)"|'(?<single>[^']*)'|(?<bare>[^\s"'=<>`/]+))/giu;
118
+
119
+ /** An anchor's own fragment target: its `id`, else its `name`. */
120
+ const anchorTarget = (attributes: string): string | undefined => {
121
+ const values = new Map<string, string>();
122
+ for (const match of attributes.matchAll(ANCHOR_TARGET)) {
123
+ const groups = match.groups ?? {};
124
+ const value = groups.double ?? groups.single ?? groups.bare;
125
+ const key = groups.key?.toLowerCase();
126
+ if (key && value && !values.has(key)) {
127
+ values.set(key, value);
128
+ }
129
+ }
130
+ return values.get("id") ?? values.get("name");
131
+ };
132
+
133
+ /** Classify a raw HTML node in a heading — see {@link RawHeadingTag}. */
134
+ export const rawHeadingTag = (value: string): RawHeadingTag => {
135
+ const tag = rawTag(value);
136
+ if (tag?.name === "Badge") {
137
+ return {
138
+ closing: tag.closing,
139
+ kind: "badge",
140
+ selfClosing: isSelfClosing(tag),
141
+ };
142
+ }
143
+ if (tag?.name.toLowerCase() !== "a") {
144
+ return { kind: "other" };
145
+ }
146
+ return tag.closing
147
+ ? { kind: "anchor-close" }
148
+ : {
149
+ kind: "anchor-open",
150
+ selfClosing: isSelfClosing(tag),
151
+ target: anchorTarget(tag.attributes),
152
+ };
153
+ };
154
+
155
+ /**
156
+ * A JSX attribute: a name with a string value or an `{expression}` one, or a
157
+ * `{...spread}` (no name).
158
+ */
159
+ export interface JsxAttribute {
160
+ name?: string;
161
+ type: string;
162
+ value?: string | { type: string; value?: string } | null;
163
+ }
164
+
165
+ const isStringValue = (value: JsxAttribute["value"]): value is string =>
166
+ typeof value === "string";
167
+
168
+ /** A JSX `<a>`'s own fragment target: its string `id`, else its `name`. */
169
+ export const jsxAnchorTarget = (
170
+ attributes: readonly JsxAttribute[]
171
+ ): string | undefined => {
172
+ const value = (key: string): string | undefined => {
173
+ const found = attributes.find((attribute) => attribute.name === key)?.value;
174
+ return isStringValue(found) && found !== "" ? found : undefined;
175
+ };
176
+ return value("id") ?? value("name");
177
+ };
178
+
28
179
  /**
29
180
  * Register a pinned `[#id]` with the document slugger, the way
30
181
  * `GithubSlugger#slug` registers the slugs it returns — so a later heading
@@ -42,7 +193,10 @@ const MARKER =
42
193
  /\s*(?:\{#(?<curlyId>[^\s}]+)\}|\[(?:#(?<id>[^\s\]]+)|(?<hide>!)?toc)\])\s*$/u;
43
194
 
44
195
  export interface HeadingMarkers {
45
- /** Author-pinned anchor id from `[#id]` or `{#id}`, used verbatim (never re-slugged). */
196
+ /**
197
+ * Author-pinned anchor id from `[#id]` or `{#id}`, used verbatim (never
198
+ * re-slugged, and never smart-punctuated: see `core/pinned-ids.ts`).
199
+ */
46
200
  id?: string;
47
201
  /** The heading text with every trailing marker stripped. */
48
202
  text: string;