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,24 @@
1
+ import type { BlumeProject } from "../core/project-graph.ts";
2
+ import { changelogIndexBlocks } from "./changelog-markdown.ts";
3
+ import type { ComponentMarkdown } from "./component-markdown.ts";
4
+
5
+ /**
6
+ * Downlevel `<Changelog />` to the release list it renders.
7
+ *
8
+ * The generated `/changelog` page publishes its list through
9
+ * `buildChangelogIndexMarkdown`, but a hand-written page that owns the route
10
+ * instead goes through the component downleveler — which leaves a tag it has
11
+ * no serializer for exactly as written. Without this, moving to an authored
12
+ * page would publish a literal `<Changelog />` to `/changelog.md`,
13
+ * llms-full.txt, MCP `get_page` and the search corpus, and an agent would see
14
+ * no releases at all.
15
+ *
16
+ * The list is the same one the mirror publishes, so both routes to a changelog
17
+ * index — generated or authored — reach agents identically.
18
+ */
19
+ export const changelogComponentSerializers = (project: BlumeProject) =>
20
+ ({
21
+ // An empty changelog replaces the tag with nothing, so the page's own prose
22
+ // is all an agent reads. Returning `null` would leave the tag as written.
23
+ Changelog: () => changelogIndexBlocks(project).join("\n\n"),
24
+ }) satisfies Record<string, ComponentMarkdown>;
@@ -1,5 +1,10 @@
1
1
  import { mountBasePath, normalizeBasePath } from "../core/base-path.ts";
2
2
  import { withChangelogIndexText } from "../core/changelog-index.ts";
3
+ import {
4
+ compareChangelogDates,
5
+ groupChangelogYears,
6
+ parseChangelogDate,
7
+ } from "../core/changelog-list.ts";
3
8
  import { EN_UI, resolveUIStrings } from "../core/i18n-ui.ts";
4
9
  import type { BlumeProject } from "../core/project-graph.ts";
5
10
  import { absoluteUrl } from "../core/site-url.ts";
@@ -19,14 +24,6 @@ interface IndexRow {
19
24
  page: PageRecord;
20
25
  }
21
26
 
22
- const parseDate = (value: string | undefined): Date | null => {
23
- if (value === undefined) {
24
- return null;
25
- }
26
- const date = new Date(value);
27
- return Number.isNaN(date.getTime()) ? null : date;
28
- };
29
-
30
27
  /** A row's link: absolute under a configured site, like llms.txt. */
31
28
  const entryUrl = (project: BlumeProject, route: string): string => {
32
29
  const { base, site } = project.config.deployment.options;
@@ -44,18 +41,19 @@ const rowLine = (project: BlumeProject, { date, page }: IndexRow): string => {
44
41
  return `- [${title}](${entryUrl(project, page.route)})${tail}`;
45
42
  };
46
43
 
47
- export const buildChangelogIndexMarkdown = (project: BlumeProject): string => {
44
+ /**
45
+ * The release list alone, as year headings over linked rows — what
46
+ * `<Changelog />` renders, in Markdown. Split out of the document below so the
47
+ * component's serializer publishes exactly the list the generated page's
48
+ * mirror does: an authored `/changelog` page brings its own heading and prose,
49
+ * and only the list comes from here.
50
+ *
51
+ * Entries are the default locale's, as everywhere else this list is published
52
+ * — see `components/content/Changelog.astro` for why the rule is the same on
53
+ * the rendered page.
54
+ */
55
+ export const changelogIndexBlocks = (project: BlumeProject): string[] => {
48
56
  const { i18n } = project.config;
49
- const ui = withChangelogIndexText(
50
- project.config,
51
- i18n
52
- ? resolveUIStrings(i18n.defaultLocale, {
53
- defaultLocale: i18n.defaultLocale,
54
- overrides: i18n.ui,
55
- })
56
- : EN_UI,
57
- i18n?.defaultLocale
58
- );
59
57
  const rows: IndexRow[] = project.graph.pages
60
58
  .filter(
61
59
  (page) =>
@@ -67,30 +65,36 @@ export const buildChangelogIndexMarkdown = (project: BlumeProject): string => {
67
65
  (!i18n || page.locale === i18n.defaultLocale)
68
66
  )
69
67
  .map((page) => ({
70
- date: parseDate(page.meta.date ?? page.meta.changelog?.date),
68
+ date: parseChangelogDate(page.meta.date ?? page.meta.changelog?.date),
71
69
  page,
72
70
  }))
73
- .toSorted((a, b) => (b.date?.getTime() ?? 0) - (a.date?.getTime() ?? 0));
71
+ .toSorted((a, b) => compareChangelogDates(a.date, b.date));
74
72
 
75
- const blocks = [`# ${ui.changelog.title}`, ui.changelog.description];
76
- let year: number | null | undefined;
77
- let lines: string[] = [];
78
- const flush = (): void => {
79
- if (lines.length > 0) {
80
- blocks.push(lines.join("\n"));
81
- lines = [];
82
- }
83
- };
84
- for (const row of rows) {
85
- const rowYear = row.date ? row.date.getUTCFullYear() : null;
86
- if (rowYear !== year) {
87
- flush();
88
- year = rowYear;
89
- // Undated entries sort last, under a heading of their own.
90
- blocks.push(rowYear === null ? "## Undated" : `## ${rowYear}`);
91
- }
92
- lines.push(rowLine(project, row));
93
- }
94
- flush();
73
+ // Undated entries sort last, under a heading of their own.
74
+ return groupChangelogYears(rows, (row) =>
75
+ row.date ? String(row.date.getUTCFullYear()) : null
76
+ ).flatMap((group) => [
77
+ `## ${group.year ?? "Undated"}`,
78
+ group.items.map((row) => rowLine(project, row)).join("\n"),
79
+ ]);
80
+ };
81
+
82
+ export const buildChangelogIndexMarkdown = (project: BlumeProject): string => {
83
+ const { i18n } = project.config;
84
+ const ui = withChangelogIndexText(
85
+ project.config,
86
+ i18n
87
+ ? resolveUIStrings(i18n.defaultLocale, {
88
+ defaultLocale: i18n.defaultLocale,
89
+ overrides: i18n.ui,
90
+ })
91
+ : EN_UI,
92
+ i18n?.defaultLocale
93
+ );
94
+ const blocks = [
95
+ `# ${ui.changelog.title}`,
96
+ ui.changelog.description,
97
+ ...changelogIndexBlocks(project),
98
+ ];
95
99
  return `${blocks.join("\n\n")}\n`;
96
100
  };
@@ -2,7 +2,10 @@ import { markdownTable } from "markdown-table";
2
2
  import { mdxToMdast } from "satteri";
3
3
 
4
4
  import { PARAM_LOCATIONS } from "../components/content/api-field.ts";
5
- import { parseYouTubeId } from "../components/content/youtube.ts";
5
+ import {
6
+ parseYouTubeId,
7
+ parseYouTubePlaylist,
8
+ } from "../components/content/youtube.ts";
6
9
  import type { ExampleLookup } from "../core/types.ts";
7
10
  import { MDX_FEATURES } from "../markdown/features.ts";
8
11
  import { readStaticExpression } from "./static-expression.ts";
@@ -553,18 +556,21 @@ const youtube: ComponentMarkdown = ({ props }) => {
553
556
  } else if (isString(props.url)) {
554
557
  input = props.url;
555
558
  }
559
+ const title =
560
+ isString(props.title) && props.title !== ""
561
+ ? props.title
562
+ : "Watch on YouTube";
556
563
  const videoId = parseYouTubeId(input);
557
564
  if (!videoId) {
558
- return null;
565
+ const list = parseYouTubePlaylist(input);
566
+ return list
567
+ ? `[${title}](https://www.youtube.com/playlist?list=${list})`
568
+ : null;
559
569
  }
560
570
  const start =
561
571
  isNumber(props.start) && props.start > 0
562
572
  ? `&t=${Math.floor(props.start)}s`
563
573
  : "";
564
- const title =
565
- isString(props.title) && props.title !== ""
566
- ? props.title
567
- : "Watch on YouTube";
568
574
  return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
569
575
  };
570
576
 
@@ -732,11 +738,18 @@ const githubInfo: ComponentMarkdown = ({ lossy, props }) => {
732
738
  return `[${linkText(`${owner}/${repo}`)}](${linkDestination(`${host}/${owner}/${repo}`)})`;
733
739
  };
734
740
 
735
- /** `CodeBlock` is a fenced block, its title kept in the fence's meta. */
736
- const codeBlock: ComponentMarkdown = ({ lossy, props }) => {
737
- if (lossy || !isString(props.code)) {
741
+ /**
742
+ * `CodeBlock` is a fenced block, its title kept in the fence's meta. Without
743
+ * `code` it renders its children (a fence wrapped in `<CodeBlock>`), so those
744
+ * are its Markdown.
745
+ */
746
+ const codeBlock: ComponentMarkdown = ({ children, lossy, props }) => {
747
+ if (lossy) {
738
748
  return null;
739
749
  }
750
+ if (!isString(props.code)) {
751
+ return props.code === undefined && children !== "" ? children : null;
752
+ }
740
753
  const lang = textProp(props.lang) || "txt";
741
754
  const title = textProp(props.title);
742
755
  return fencedBlock(
package/src/ai/llms.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { mountBasePath, normalizeBasePath } from "../core/base-path.ts";
2
- import { rewriteRelativeImages } from "../core/content-assets.ts";
2
+ import { rewriteRelativeAssets } from "../core/content-assets.ts";
3
3
  import matter from "../core/frontmatter.ts";
4
+ import { isHiddenPage } from "../core/hidden-pages.ts";
5
+ import { metaDescription } from "../core/manifest.ts";
4
6
  import type { BlumeProject } from "../core/project-graph.ts";
5
7
  import { absoluteUrl } from "../core/site-url.ts";
8
+ import { extractHeadings } from "../core/sources/normalize.ts";
6
9
  import { readExpandedEntryText } from "../core/sources/read.ts";
7
10
  import type { NavNode, Navigation, PageRecord } from "../core/types.ts";
8
11
  import { buildRssFeeds } from "../deploy/rss.ts";
@@ -121,7 +124,7 @@ const eligiblePages = (
121
124
  !(
122
125
  page.meta.ai.exclude ||
123
126
  page.meta.draft ||
124
- page.meta.sidebar.hidden ||
127
+ isHiddenPage(page, project.graph) ||
125
128
  (page.meta.seo.noindex && page.source.name !== "openapi") ||
126
129
  (options.versions === "current" && page.version !== "")
127
130
  ) &&
@@ -187,7 +190,10 @@ export const buildLlmsIndex = (
187
190
 
188
191
  const line = (page: PageRecord): string => {
189
192
  seen.add(page.route);
190
- const summary = page.description ? `: ${page.description}` : "";
193
+ // The page's meta description, so a page summarized only in
194
+ // `seo.description` (a generated reference page) still gets one.
195
+ const description = metaDescription(page);
196
+ const summary = description ? `: ${description}` : "";
191
197
  return `- [${linkText(page.title)}](${pageUrl(page.route, site, base)})${summary}`;
192
198
  };
193
199
 
@@ -306,6 +312,26 @@ export const buildLlmsIndex = (
306
312
  return `${[lead, ...blocks].join("\n\n")}\n`;
307
313
  };
308
314
 
315
+ // A line an ATX heading opens (`# Title`); any other first line can only open
316
+ // a setext heading, underlined on the line after it.
317
+ const ATX_HEADING = /^ {0,3}#/u;
318
+
319
+ /**
320
+ * A page body without the `# Heading` it opens with, when that heading's text
321
+ * is the page's title. An untitled page takes its title from that heading,
322
+ * and its `llms-full.txt` section already opens with `# <title>`, so the
323
+ * heading would show twice; the rendered page drops it the same way (see
324
+ * `markdown/title-heading.ts`).
325
+ */
326
+ const withoutTitleHeading = (body: string, title: string): string => {
327
+ const lines = body.split("\n");
328
+ const span = ATX_HEADING.test(lines[0] ?? "") ? 1 : 2;
329
+ const [heading] = extractHeadings(lines.slice(0, span).join("\n"));
330
+ return heading?.depth === 1 && heading.text === title
331
+ ? lines.slice(span).join("\n").trimStart()
332
+ : body;
333
+ };
334
+
309
335
  /** Build `llms-full.txt`: the full Markdown body of every current-docs page. */
310
336
  const buildFull = async (project: BlumeProject): Promise<string> => {
311
337
  const { config } = project;
@@ -321,7 +347,7 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
321
347
  // Colocated `./image.png` references resolve to nothing for a reader of
322
348
  // llms-full.txt; point them at the served originals instead.
323
349
  if (page.sourcePath) {
324
- raw = rewriteRelativeImages({
350
+ raw = rewriteRelativeAssets({
325
351
  deployBase: config.deployment.options.base,
326
352
  projectRoot: project.context.root,
327
353
  source: raw,
@@ -346,7 +372,12 @@ const buildFull = async (project: BlumeProject): Promise<string> => {
346
372
  config.deployment.options.site,
347
373
  normalizeBasePath(config.deployment.options.base)
348
374
  );
349
- return [`# ${page.title}`, `Source: ${url}`, "", body].join("\n");
375
+ return [
376
+ `# ${page.title}`,
377
+ `Source: ${url}`,
378
+ "",
379
+ withoutTitleHeading(body, page.title),
380
+ ].join("\n");
350
381
  })
351
382
  );
352
383
 
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
 
3
3
  import { hasGeneratedChangelog } from "../astro/pages.ts";
4
4
  import { CHANGELOG_INDEX_ROUTE } from "../core/changelog-index.ts";
5
- import { rewriteRelativeImages } from "../core/content-assets.ts";
5
+ import { rewriteRelativeAssets } from "../core/content-assets.ts";
6
6
  import { discoverPages, discoverPagesSync } from "../core/custom-pages.ts";
7
7
  import matter from "../core/frontmatter.ts";
8
8
  import type { BlumeProject } from "../core/project-graph.ts";
@@ -76,7 +76,7 @@ export const buildRawMarkdown = async (
76
76
  project.manifest.routes.map(async (route) => {
77
77
  let text = await readRoute(route);
78
78
  if (route.sourcePath) {
79
- text = rewriteRelativeImages({
79
+ text = rewriteRelativeAssets({
80
80
  deployBase: project.config.deployment.options.base,
81
81
  projectRoot: project.context.root,
82
82
  source: text,
@@ -1,6 +1,10 @@
1
1
  import { customCodeSamples } from "../components/openapi/code-samples.ts";
2
2
  import { mountBasePath, normalizeBasePath } from "../core/base-path.ts";
3
- import type { ApiOperationRef, OpenApiData } from "../openapi/model.ts";
3
+ import type {
4
+ ApiDocument,
5
+ ApiOperationRef,
6
+ OpenApiData,
7
+ } from "../openapi/model.ts";
4
8
  import {
5
9
  operationObject,
6
10
  operationOf,
@@ -19,6 +23,8 @@ import {
19
23
  isString,
20
24
  linkDestination,
21
25
  } from "./component-markdown.ts";
26
+ import { operationDetails } from "./operation-details.ts";
27
+ import type { OperationParts } from "./operation-details.ts";
22
28
 
23
29
  /**
24
30
  * Spec-authored prose as one line of inline Markdown. Unlike the props the
@@ -70,13 +76,13 @@ const listItem = (
70
76
  * Each serializer emits what its component renders and no more: the endpoint
71
77
  * in the spec kind's own notation ({@link operationSignature}), the version and
72
78
  * addresses the overview shows ({@link specAddresses}), the linked list a tag
73
- * section shows. Parameters, schemas and responses are deliberately left out:
74
- * those are `operation-model.ts` plus each component's own preparation, and a
75
- * second implementation here would be free to disagree with the page. The
76
- * endpoint and what it does is the part that was missing altogether. The
77
- * spec's own `x-codeSamples` do come along, as fenced code: they're text the
78
- * spec wrote out (often an SDK call no agent could derive from the
79
- * endpoint), read through the same `customCodeSamples` the page renders.
79
+ * section shows. The spec's own `x-codeSamples` come along, as fenced code:
80
+ * they're text the spec wrote out (often an SDK call no agent could derive
81
+ * from the endpoint), read through the same `customCodeSamples` the page
82
+ * renders. So do an OpenAPI operation's request body and responses, with
83
+ * their examples ({@link operationDetails}), built from the helpers the
84
+ * page's components use so the two can't disagree; parameters and nested
85
+ * schemas are left to the page.
80
86
  *
81
87
  * `specs` is the parsed `blume:openapi` data — empty when the project has no
82
88
  * API reference, in which case every serializer declines. `deployBase` is the
@@ -166,7 +172,17 @@ export const openapiComponentSerializers = (
166
172
  (sample) =>
167
173
  `**${inlineText(sample.label)}**\n\n${fencedBlock(sample.lang, sample.source)}`
168
174
  );
169
- return [signature, ...notes, ...samples].join("\n\n");
175
+ // SAFETY: an operation object only resolves out of an OpenAPI spec,
176
+ // whose document is the OpenAPI shape; its bodies and responses are
177
+ // the JSON `OperationParts` views, which the helpers read defensively.
178
+ const details = object
179
+ ? operationDetails(
180
+ data.document as ApiDocument,
181
+ object as OperationParts,
182
+ operation.webhook === true
183
+ )
184
+ : [];
185
+ return [signature, ...notes, ...samples, ...details].join("\n\n");
170
186
  },
171
187
  } satisfies Record<string, ComponentMarkdown>;
172
188
  };
@@ -0,0 +1,208 @@
1
+ import {
2
+ preferredMedia,
3
+ responsePanels,
4
+ } from "../components/openapi/example-panels.ts";
5
+ import type { ResponseLike } from "../components/openapi/example-panels.ts";
6
+ import {
7
+ isNullable,
8
+ mediaExample,
9
+ namedExamples,
10
+ objectProperties,
11
+ resolveComponentRef,
12
+ resolveSchema,
13
+ toJson,
14
+ typeLabel,
15
+ } from "../components/openapi/helpers.ts";
16
+ import type {
17
+ ComponentsLike,
18
+ ExampleDirection,
19
+ MediaLike,
20
+ SchemaLike,
21
+ SpecValue,
22
+ } from "../components/openapi/helpers.ts";
23
+ import type { ApiDocument } from "../openapi/model.ts";
24
+ import { fencedBlock, inlineCode } from "./component-markdown.ts";
25
+
26
+ /**
27
+ * The body and responses of an OpenAPI operation as compact Markdown, for the
28
+ * agent-facing copy of its page (`<route>.md`, llms-full.txt, MCP
29
+ * `get_page`): the request body's media type and top-level properties, its
30
+ * example (every named one), each response's status and description, and
31
+ * the examples the spec records. Without these an agent read an endpoint
32
+ * with no word of what to send or what comes back, though the spec, and the
33
+ * page, had both. Built from the helpers the page's own components use, so
34
+ * the two can't disagree: the same JSON-ish media type, the same examples,
35
+ * `readOnly` and `writeOnly` properties left out the same way.
36
+ *
37
+ * Kept compact: properties one level deep (an example shows the rest), and
38
+ * a sampled response example only for the first success response that
39
+ * records none, since error bodies often all share one schema.
40
+ */
41
+
42
+ /** A request body, `$ref` resolved. */
43
+ interface RequestBodyLike {
44
+ description?: string;
45
+ required?: boolean;
46
+ content?: Record<string, MediaLike>;
47
+ }
48
+
49
+ /** The operation fields read here. */
50
+ export interface OperationParts {
51
+ requestBody?: RequestBodyLike & { $ref?: string };
52
+ responses?: Record<string, ResponseLike & { $ref?: string }>;
53
+ }
54
+
55
+ /** Spec prose on one line, the characters Markdown reads as markup escaped. */
56
+ const oneLine = (text: string): string =>
57
+ text
58
+ .trim()
59
+ .replaceAll(/\s+/gu, " ")
60
+ .replaceAll(/[\\`*_[\]<>~]/gu, String.raw`\$&`);
61
+
62
+ /** A fenced JSON example, under its label. */
63
+ const exampleBlock = (label: string, value: SpecValue): string =>
64
+ `${label}:\n\n${fencedBlock("json", toJson(value))}`;
65
+
66
+ /** One line per top-level property: name, type, required, description. */
67
+ const propertyLines = (
68
+ schema: SchemaLike | undefined,
69
+ schemas: Record<string, SchemaLike>,
70
+ direction: ExampleDirection
71
+ ): string[] => {
72
+ if (!schema) {
73
+ return [];
74
+ }
75
+ const omitted = direction === "request" ? "readOnly" : "writeOnly";
76
+ const { properties, required } = objectProperties(schema, schemas);
77
+ return properties.flatMap(([name, property]) => {
78
+ const resolved = resolveSchema(schemas, property);
79
+ if (property[omitted] === true || resolved[omitted] === true) {
80
+ return [];
81
+ }
82
+ const type = `${typeLabel(property)}${isNullable(resolved) ? " | null" : ""}`;
83
+ const notes = [type, required.has(name) ? "required" : ""]
84
+ .filter(Boolean)
85
+ .join(", ");
86
+ const description = resolved.description?.trim();
87
+ return [
88
+ `- ${inlineCode(name)} (${notes})${description ? ` — ${oneLine(description)}` : ""}`,
89
+ ];
90
+ });
91
+ };
92
+
93
+ /** The request body (a webhook's payload): media type, properties, examples. */
94
+ const bodyMarkdown = (
95
+ body: RequestBodyLike,
96
+ schemas: Record<string, SchemaLike>,
97
+ components: ComponentsLike | undefined,
98
+ webhook: boolean
99
+ ): string[] => {
100
+ const chosen = preferredMedia(body.content);
101
+ if (!chosen) {
102
+ return [];
103
+ }
104
+ const [type, media] = chosen;
105
+ // A webhook's payload travels from the API, like a response.
106
+ const direction = webhook ? "response" : "request";
107
+ const heading = webhook ? "Payload" : "Request body";
108
+ const lines = propertyLines(media.schema, schemas, direction);
109
+ const named = namedExamples(media, schemas, direction, components);
110
+ const single = mediaExample(media, schemas, direction, components);
111
+ const examples =
112
+ named.length > 0
113
+ ? named.map((example) =>
114
+ exampleBlock(
115
+ `${heading} example, ${oneLine(example.label)}`,
116
+ example.value
117
+ )
118
+ )
119
+ : [
120
+ single === null || single === undefined
121
+ ? ""
122
+ : exampleBlock(`${heading} example`, single),
123
+ ];
124
+ return [
125
+ `**${heading}** (${[inlineCode(type), body.required ? "required" : ""].filter(Boolean).join(", ")})`,
126
+ body.description?.trim() ? oneLine(body.description) : "",
127
+ lines.join("\n"),
128
+ ...examples,
129
+ ].filter(Boolean);
130
+ };
131
+
132
+ /** The responses: one line per status, then the examples the spec records. */
133
+ const responsesMarkdown = (
134
+ responses: Record<string, ResponseLike>,
135
+ schemas: Record<string, SchemaLike>,
136
+ components: ComponentsLike | undefined
137
+ ): string[] => {
138
+ const statuses = Object.entries(responses);
139
+ if (statuses.length === 0) {
140
+ return [];
141
+ }
142
+ const recorded = new Set(
143
+ statuses.flatMap(([status, response]) => {
144
+ const media = preferredMedia(response.content)?.[1];
145
+ return media?.example === undefined && media?.examples === undefined
146
+ ? []
147
+ : [status];
148
+ })
149
+ );
150
+ const sampled = statuses.find(
151
+ ([status]) => status.startsWith("2") && !recorded.has(status)
152
+ )?.[0];
153
+ const examples = responsePanels(responses, schemas, components).flatMap(
154
+ (panel) =>
155
+ (recorded.has(panel.name) || panel.name === sampled) &&
156
+ panel.value !== null &&
157
+ panel.value !== undefined
158
+ ? [
159
+ exampleBlock(
160
+ `Response example, ${oneLine(panel.label)}`,
161
+ panel.value
162
+ ),
163
+ ]
164
+ : []
165
+ );
166
+ return [
167
+ "**Responses**",
168
+ statuses
169
+ .map(
170
+ ([status, response]) =>
171
+ `- ${inlineCode(status)}${response.description?.trim() ? ` — ${oneLine(response.description)}` : ""}`
172
+ )
173
+ .join("\n"),
174
+ ...examples,
175
+ ];
176
+ };
177
+
178
+ /**
179
+ * An operation's body and responses as Markdown blocks, `$ref`s to
180
+ * `components` resolved, ready to join with blank lines.
181
+ */
182
+ export const operationDetails = (
183
+ document: ApiDocument,
184
+ operation: OperationParts,
185
+ webhook: boolean
186
+ ): string[] => {
187
+ // SAFETY: a parsed document's `components` is the section-keyed table
188
+ // `ComponentsLike` describes; the helpers guard every entry they read.
189
+ const components = document.components as ComponentsLike | undefined;
190
+ // SAFETY: component schemas are the JSON Schema objects `SchemaLike` views.
191
+ const schemas = (document.components?.schemas ?? {}) as Record<
192
+ string,
193
+ SchemaLike
194
+ >;
195
+ const body = operation.requestBody
196
+ ? resolveComponentRef(operation.requestBody, components, "requestBodies")
197
+ : undefined;
198
+ const responses = Object.fromEntries(
199
+ Object.entries(operation.responses ?? {}).map(([status, response]) => [
200
+ status,
201
+ resolveComponentRef(response, components, "responses"),
202
+ ])
203
+ );
204
+ return [
205
+ ...(body ? bodyMarkdown(body, schemas, components, webhook) : []),
206
+ ...responsesMarkdown(responses, schemas, components),
207
+ ];
208
+ };