blume 2.0.1 → 2.0.3

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 (492) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/cli/{chunk-qwsrynx5.js → chunk-1d7ve1dm.js} +38 -19
  3. package/dist/cli/{chunk-qwsrynx5.js.map → chunk-1d7ve1dm.js.map} +4 -4
  4. package/dist/cli/{chunk-s6jhgk0q.js → chunk-2eytanqx.js} +2 -2
  5. package/dist/cli/{chunk-kdp5q7ke.js → chunk-2hsdwb9n.js} +18 -19
  6. package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-2hsdwb9n.js.map} +2 -2
  7. package/dist/cli/{chunk-hdpx1tax.js → chunk-2z928egk.js} +5 -5
  8. package/dist/cli/{chunk-jwyddg7y.js → chunk-35d4wj9f.js} +30 -24
  9. package/dist/cli/chunk-35d4wj9f.js.map +15 -0
  10. package/dist/cli/{chunk-ah61y8py.js → chunk-364znk6q.js} +2 -2
  11. package/dist/cli/{chunk-jts8mvcz.js → chunk-3em5wd2y.js} +25 -6
  12. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-3em5wd2y.js.map} +3 -3
  13. package/dist/cli/{chunk-s1p84fyh.js → chunk-5f86nr5m.js} +63 -22
  14. package/dist/cli/chunk-5f86nr5m.js.map +11 -0
  15. package/dist/cli/{chunk-2hn4b8z7.js → chunk-5m5nmvyq.js} +170 -76
  16. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  17. package/dist/cli/{chunk-epjnccmv.js → chunk-5xvm6tfj.js} +18 -15
  18. package/dist/cli/chunk-5xvm6tfj.js.map +10 -0
  19. package/dist/cli/{chunk-fz5wtpmh.js → chunk-6dsbexzp.js} +18 -15
  20. package/dist/cli/chunk-6dsbexzp.js.map +10 -0
  21. package/dist/cli/{chunk-vtk4a6dg.js → chunk-8ktnccpt.js} +1 -1
  22. package/dist/cli/{chunk-6hsn950k.js → chunk-9t7a85s3.js} +178 -48
  23. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  24. package/dist/cli/{chunk-fxypxtvm.js → chunk-a58773jm.js} +2 -2
  25. package/dist/cli/{chunk-mb2919y2.js → chunk-acanzt5p.js} +17 -6
  26. package/dist/cli/chunk-acanzt5p.js.map +10 -0
  27. package/dist/cli/{chunk-27g6wdth.js → chunk-akbpwfxc.js} +90 -26
  28. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  29. package/dist/cli/{chunk-wm7js3j9.js → chunk-b07cmahc.js} +2 -2
  30. package/dist/cli/{chunk-wgm7m9qk.js → chunk-c8chx29p.js} +179 -51
  31. package/dist/cli/chunk-c8chx29p.js.map +36 -0
  32. package/dist/cli/{chunk-qs4q5p4e.js → chunk-crgn1q09.js} +32 -14
  33. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  34. package/dist/cli/chunk-e04dxsz1.js +39 -0
  35. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  36. package/dist/cli/{chunk-yt5n7ppj.js → chunk-ey84smr6.js} +17 -7
  37. package/dist/cli/chunk-ey84smr6.js.map +10 -0
  38. package/dist/cli/{chunk-f2z5v128.js → chunk-g4hq16wv.js} +14 -15
  39. package/dist/cli/{chunk-f2z5v128.js.map → chunk-g4hq16wv.js.map} +2 -2
  40. package/dist/cli/{chunk-fa25z98p.js → chunk-ga0pf4aj.js} +9 -8
  41. package/dist/cli/chunk-ga0pf4aj.js.map +11 -0
  42. package/dist/cli/{chunk-kpf8rrjc.js → chunk-hm3vjy5s.js} +109 -58
  43. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  44. package/dist/cli/{chunk-fs23ddbb.js → chunk-j85vccga.js} +625 -750
  45. package/dist/cli/chunk-j85vccga.js.map +36 -0
  46. package/dist/cli/{chunk-ch6g3ar0.js → chunk-p3v96n38.js} +6 -6
  47. package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-p3v96n38.js.map} +3 -3
  48. package/dist/cli/{chunk-q5163e60.js → chunk-p73c0m7w.js} +21 -19
  49. package/dist/cli/chunk-p73c0m7w.js.map +11 -0
  50. package/dist/cli/{chunk-dh8cwk36.js → chunk-pehfxfta.js} +24 -9
  51. package/dist/cli/chunk-pehfxfta.js.map +10 -0
  52. package/dist/cli/{chunk-qkqwkpte.js → chunk-pv29h0wf.js} +3688 -1313
  53. package/dist/cli/chunk-pv29h0wf.js.map +190 -0
  54. package/dist/cli/{chunk-6vm74dry.js → chunk-q58y5e6a.js} +10 -10
  55. package/dist/cli/{chunk-6vm74dry.js.map → chunk-q58y5e6a.js.map} +3 -3
  56. package/dist/cli/{chunk-zxcczpyx.js → chunk-r20tn01b.js} +1 -1
  57. package/dist/cli/{chunk-m3vmjgmq.js → chunk-r9rcc4w7.js} +16 -8
  58. package/dist/cli/chunk-r9rcc4w7.js.map +10 -0
  59. package/dist/cli/{chunk-5shv93fd.js → chunk-tkacnehg.js} +2 -2
  60. package/dist/cli/{chunk-zxh4d9vy.js → chunk-tzmab476.js} +4 -4
  61. package/dist/cli/{chunk-yw7dm696.js → chunk-vg9r4eb9.js} +24 -11
  62. package/dist/cli/chunk-vg9r4eb9.js.map +14 -0
  63. package/dist/cli/{chunk-79jhk4py.js → chunk-wrr3j9w9.js} +260 -132
  64. package/dist/cli/chunk-wrr3j9w9.js.map +35 -0
  65. package/dist/cli/{chunk-6crbhc3x.js → chunk-yfyb25rh.js} +54 -27
  66. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  67. package/dist/cli/index.js +162 -35
  68. package/dist/cli/index.js.map +4 -4
  69. package/dist/types/ai/agent-surface.d.ts +32 -0
  70. package/dist/types/ai/api-catalog.d.ts +7 -1
  71. package/dist/types/ai/ask-context.d.ts +7 -0
  72. package/dist/types/ai/component-markdown.d.ts +4 -4
  73. package/dist/types/ai/link-headers.d.ts +8 -1
  74. package/dist/types/ai/openapi-components.d.ts +5 -2
  75. package/dist/types/ai/relative-links.d.ts +11 -4
  76. package/dist/types/ai/skills.d.ts +4 -1
  77. package/dist/types/ai/static-expression.d.ts +28 -0
  78. package/dist/types/ai/tar.d.ts +1 -3
  79. package/dist/types/analytics/databuddy.d.ts +43 -0
  80. package/dist/types/analytics/index.d.ts +4 -0
  81. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  82. package/dist/types/analytics/schema.d.ts +28 -0
  83. package/dist/types/astro/integration.d.ts +3 -2
  84. package/dist/types/cli/env.d.ts +5 -0
  85. package/dist/types/cli/init/scaffold.d.ts +19 -3
  86. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  87. package/dist/types/core/base-path.d.ts +24 -1
  88. package/dist/types/core/config-input.d.ts +13 -11
  89. package/dist/types/core/config.d.ts +2 -2
  90. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  91. package/dist/types/core/graph.d.ts +2 -0
  92. package/dist/types/core/heading-markers.d.ts +5 -7
  93. package/dist/types/core/i18n-ui.d.ts +31 -0
  94. package/dist/types/core/i18n.d.ts +9 -1
  95. package/dist/types/core/last-modified.d.ts +10 -0
  96. package/dist/types/core/links.d.ts +3 -1
  97. package/dist/types/core/load-module.d.ts +10 -0
  98. package/dist/types/core/locale-links.d.ts +12 -2
  99. package/dist/types/core/meta.d.ts +13 -1
  100. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  101. package/dist/types/core/navigation.d.ts +38 -0
  102. package/dist/types/core/ordering-prefix.d.ts +4 -0
  103. package/dist/types/core/safe-links.d.ts +3 -1
  104. package/dist/types/core/schema.d.ts +59 -13
  105. package/dist/types/core/sources/github-releases.d.ts +5 -0
  106. package/dist/types/core/sources/lower.d.ts +38 -13
  107. package/dist/types/core/sources/normalize.d.ts +23 -2
  108. package/dist/types/core/sources/remote.d.ts +11 -1
  109. package/dist/types/core/sources/resolve.d.ts +12 -0
  110. package/dist/types/core/sources/types.d.ts +31 -0
  111. package/dist/types/core/sources/watch.d.ts +12 -5
  112. package/dist/types/core/standard-schema.d.ts +5 -0
  113. package/dist/types/core/types.d.ts +9 -0
  114. package/dist/types/deploy/adapters/node.d.ts +5 -2
  115. package/dist/types/deploy/adapters/types.d.ts +7 -0
  116. package/dist/types/deploy/artifacts.d.ts +6 -4
  117. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  118. package/dist/types/deploy/headers.d.ts +36 -7
  119. package/dist/types/deploy/node-headers.d.ts +43 -8
  120. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  121. package/dist/types/deploy/platforms/node.d.ts +6 -5
  122. package/dist/types/deploy/platforms/types.d.ts +15 -0
  123. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  124. package/dist/types/deploy/redirects.d.ts +31 -15
  125. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  126. package/dist/types/markdown/directives.d.ts +62 -0
  127. package/dist/types/markdown/features.d.ts +21 -0
  128. package/dist/types/markdown/mdast.d.ts +63 -0
  129. package/dist/types/openapi/asyncapi.d.ts +4 -2
  130. package/dist/types/openapi/model.d.ts +20 -6
  131. package/dist/types/search/sync/algolia.d.ts +3 -1
  132. package/docs/01-quickstart.mdx +3 -2
  133. package/docs/02-deployment.mdx +20 -9
  134. package/docs/08-faq.mdx +10 -3
  135. package/docs/advanced/changelog.mdx +1 -1
  136. package/docs/advanced/custom-pages.mdx +7 -5
  137. package/docs/cli/audit.mdx +19 -3
  138. package/docs/cli/doctor.mdx +2 -2
  139. package/docs/cli/evals.mdx +4 -4
  140. package/docs/cli/index.mdx +4 -1
  141. package/docs/cli/translate.mdx +4 -4
  142. package/docs/cli/version.mdx +1 -1
  143. package/docs/configuration/analytics.mdx +42 -2
  144. package/docs/configuration/assistant.mdx +1 -1
  145. package/docs/configuration/customization.mdx +6 -3
  146. package/docs/configuration/index.mdx +5 -3
  147. package/docs/configuration/search.mdx +2 -2
  148. package/docs/content/components.mdx +2 -2
  149. package/docs/content/frontmatter.mdx +5 -1
  150. package/docs/content/i18n.mdx +3 -1
  151. package/docs/content/index.mdx +8 -4
  152. package/docs/content/islands.mdx +1 -1
  153. package/docs/content/meta.mdx +7 -3
  154. package/docs/content/navigation.mdx +29 -4
  155. package/docs/content/sources.mdx +18 -16
  156. package/docs/content/syntax.mdx +14 -0
  157. package/docs/content/versioning.mdx +2 -1
  158. package/docs/discoverability/agent-discovery.mdx +22 -8
  159. package/docs/discoverability/index.mdx +4 -3
  160. package/docs/discoverability/llms-txt.mdx +2 -5
  161. package/docs/discoverability/markdown.mdx +6 -4
  162. package/docs/discoverability/mcp.mdx +4 -0
  163. package/docs/discoverability/metadata.mdx +3 -2
  164. package/docs/discoverability/open-graph.mdx +8 -4
  165. package/docs/discoverability/rss.mdx +4 -2
  166. package/docs/references/asyncapi.mdx +2 -2
  167. package/docs/references/graphql.mdx +2 -2
  168. package/docs/references/openapi.mdx +6 -4
  169. package/package.json +1 -1
  170. package/skills/blume-migrate/SKILL.md +6 -6
  171. package/skills/blume-migrate/references/docusaurus.md +6 -6
  172. package/skills/blume-migrate/references/fumadocs.md +1 -1
  173. package/skills/blume-migrate/references/mintlify.md +3 -3
  174. package/skills/blume-migrate/references/monorepo.md +1 -1
  175. package/skills/blume-migrate/references/nextra.md +2 -2
  176. package/skills/blume-migrate/references/starlight.md +5 -5
  177. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +9 -4
  178. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  179. package/src/ai/agent-readability.ts +2 -2
  180. package/src/ai/agent-surface.ts +56 -0
  181. package/src/ai/ai-catalog.ts +6 -2
  182. package/src/ai/api/handlers.ts +2 -2
  183. package/src/ai/api/spec.ts +17 -4
  184. package/src/ai/api-catalog.ts +13 -3
  185. package/src/ai/ask-context.ts +14 -2
  186. package/src/ai/ask-data.ts +26 -12
  187. package/src/ai/changelog-markdown.ts +2 -2
  188. package/src/ai/component-markdown.ts +43 -37
  189. package/src/ai/link-headers.ts +22 -8
  190. package/src/ai/llms.ts +22 -10
  191. package/src/ai/markdown.ts +23 -11
  192. package/src/ai/mcp/discovery.ts +2 -2
  193. package/src/ai/mcp/query.ts +2 -2
  194. package/src/ai/mcp/server.ts +80 -9
  195. package/src/ai/openapi-components.ts +22 -6
  196. package/src/ai/relative-links.ts +99 -15
  197. package/src/ai/serializers.ts +2 -1
  198. package/src/ai/skills.ts +14 -1
  199. package/src/ai/static-expression.ts +416 -0
  200. package/src/ai/tar.ts +139 -12
  201. package/src/ai/visibility.ts +45 -14
  202. package/src/analytics/databuddy.ts +67 -0
  203. package/src/analytics/head.ts +8 -0
  204. package/src/analytics/index.ts +7 -0
  205. package/src/analytics/one-dollar-stats.ts +86 -0
  206. package/src/analytics/posthog.ts +21 -3
  207. package/src/analytics/schema.ts +4 -0
  208. package/src/astro/generate.ts +72 -28
  209. package/src/astro/include-hmr.ts +31 -12
  210. package/src/astro/include-refresh.ts +19 -8
  211. package/src/astro/integration.ts +108 -14
  212. package/src/astro/runtime-modules.ts +28 -13
  213. package/src/astro/templates.ts +139 -57
  214. package/src/audit/catalog.ts +2 -2
  215. package/src/audit/checks/assets.ts +25 -5
  216. package/src/audit/checks/content.ts +20 -2
  217. package/src/audit/checks/i18n.ts +46 -8
  218. package/src/audit/checks/indexability.ts +39 -19
  219. package/src/audit/checks/links.ts +22 -1
  220. package/src/audit/checks/llms.ts +6 -3
  221. package/src/audit/checks/network.ts +3 -1
  222. package/src/audit/checks/og-image.ts +10 -0
  223. package/src/audit/checks/robots.ts +6 -1
  224. package/src/audit/checks/sitemap.ts +56 -31
  225. package/src/audit/checks/social.ts +21 -2
  226. package/src/audit/crawl.ts +88 -15
  227. package/src/audit/graph.ts +3 -1
  228. package/src/audit/report.ts +54 -17
  229. package/src/audit/run.ts +13 -9
  230. package/src/audit/snapshot.ts +5 -0
  231. package/src/audit/types.ts +18 -0
  232. package/src/audit/url.ts +37 -6
  233. package/src/cli/args.ts +32 -0
  234. package/src/cli/build-failure.ts +50 -0
  235. package/src/cli/commands/audit.ts +11 -1
  236. package/src/cli/commands/build.ts +19 -5
  237. package/src/cli/commands/check.ts +2 -0
  238. package/src/cli/commands/doctor.ts +3 -1
  239. package/src/cli/commands/eval.ts +20 -18
  240. package/src/cli/commands/init.ts +15 -5
  241. package/src/cli/commands/preview.ts +15 -0
  242. package/src/cli/commands/sync.ts +2 -0
  243. package/src/cli/commands/translate.ts +18 -18
  244. package/src/cli/commands/upgrade.ts +11 -0
  245. package/src/cli/commands/validate.ts +3 -1
  246. package/src/cli/dev-lock.ts +157 -37
  247. package/src/cli/eject-scripts.ts +32 -7
  248. package/src/cli/env.ts +12 -1
  249. package/src/cli/init/scaffold.ts +117 -11
  250. package/src/cli/init/starter-spec.ts +235 -0
  251. package/src/cli/report-format.ts +11 -6
  252. package/src/components/colors.ts +19 -0
  253. package/src/components/content/AccordionItem.astro +26 -22
  254. package/src/components/content/Badge.astro +7 -12
  255. package/src/components/content/Card.astro +2 -2
  256. package/src/components/content/Component.astro +11 -5
  257. package/src/components/content/Expandable.astro +5 -1
  258. package/src/components/content/Frame.astro +2 -7
  259. package/src/components/content/GithubInfo.astro +12 -2
  260. package/src/components/content/Prompt.astro +2 -7
  261. package/src/components/content/Tab.astro +0 -1
  262. package/src/components/content/Tabs.astro +58 -5
  263. package/src/components/content/Tile.astro +1 -1
  264. package/src/components/content/Tooltip.astro +69 -7
  265. package/src/components/content/Tree.astro +7 -2
  266. package/src/components/content/TypeTable.astro +10 -5
  267. package/src/components/content/Update.astro +8 -2
  268. package/src/components/content/auto-type-table.ts +4 -1
  269. package/src/components/content/badge-color.ts +20 -0
  270. package/src/components/content/base-href.ts +14 -22
  271. package/src/components/content/inline-markdown.ts +27 -7
  272. package/src/components/copy-feedback.ts +35 -8
  273. package/src/components/islands/assistant.tsx +37 -6
  274. package/src/components/islands/base-path.ts +47 -10
  275. package/src/components/islands/hooks.ts +42 -21
  276. package/src/components/islands/webmcp.ts +16 -10
  277. package/src/components/layout/Banner.astro +23 -4
  278. package/src/components/layout/Breadcrumbs.astro +5 -2
  279. package/src/components/layout/DiscoveryLinks.astro +9 -5
  280. package/src/components/layout/Header.astro +23 -10
  281. package/src/components/layout/LanguageSwitcher.astro +2 -2
  282. package/src/components/layout/Logo.astro +5 -0
  283. package/src/components/layout/NavSelector.astro +8 -3
  284. package/src/components/layout/NavTabMenu.astro +133 -0
  285. package/src/components/layout/NavTree.astro +31 -12
  286. package/src/components/layout/NavTreeCache.astro +5 -2
  287. package/src/components/layout/NavTreeScript.astro +45 -5
  288. package/src/components/layout/PageActions.astro +4 -3
  289. package/src/components/layout/PageLayout.astro +28 -13
  290. package/src/components/layout/Pagination.astro +3 -3
  291. package/src/components/layout/ReferenceLayout.astro +4 -1
  292. package/src/components/layout/RootLayout.astro +25 -12
  293. package/src/components/layout/Search.astro +38 -14
  294. package/src/components/layout/VersionBanner.astro +2 -2
  295. package/src/components/layout/analytics-client.ts +14 -0
  296. package/src/components/layout/toc-active.ts +41 -0
  297. package/src/components/layout/toc-element.ts +8 -14
  298. package/src/components/openapi/ApiTagOperations.astro +2 -2
  299. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  300. package/src/components/openapi/GraphqlChip.astro +2 -2
  301. package/src/components/openapi/GraphqlType.astro +11 -3
  302. package/src/components/openapi/MessageComposer.astro +1 -1
  303. package/src/components/openapi/Operation.astro +23 -5
  304. package/src/components/openapi/PanelTabs.astro +4 -1
  305. package/src/components/openapi/Playground.astro +8 -2
  306. package/src/components/openapi/RequestPanel.astro +13 -6
  307. package/src/components/openapi/SchemaProperty.astro +11 -33
  308. package/src/components/openapi/SchemaTable.astro +34 -52
  309. package/src/components/openapi/async.ts +38 -6
  310. package/src/components/openapi/helpers.ts +100 -12
  311. package/src/components/openapi/message-composer.ts +14 -1
  312. package/src/components/openapi/message-model.ts +12 -2
  313. package/src/components/openapi/message.ts +21 -3
  314. package/src/components/openapi/operation-model.ts +119 -17
  315. package/src/components/openapi/panel.ts +31 -5
  316. package/src/components/openapi/param-style.ts +181 -0
  317. package/src/components/openapi/playground-client.ts +110 -23
  318. package/src/components/openapi/playground-schema.ts +25 -6
  319. package/src/components/openapi/request.ts +192 -26
  320. package/src/components/openapi/schema-tree.ts +209 -0
  321. package/src/components/openapi/snippets.ts +113 -19
  322. package/src/components/openapi/validate-json.ts +1 -1
  323. package/src/components/openapi/ws-client.ts +18 -2
  324. package/src/core/base-path.ts +69 -8
  325. package/src/core/config-input.ts +13 -11
  326. package/src/core/config.ts +18 -4
  327. package/src/core/diagnostics.ts +219 -30
  328. package/src/core/directive-diagnostics.ts +99 -0
  329. package/src/core/frontmatter.ts +21 -18
  330. package/src/core/graph.ts +120 -11
  331. package/src/core/heading-markers.ts +5 -18
  332. package/src/core/i18n-ui.ts +39 -2
  333. package/src/core/i18n.ts +44 -5
  334. package/src/core/last-modified.ts +25 -3
  335. package/src/core/links.ts +7 -4
  336. package/src/core/load-module.ts +20 -0
  337. package/src/core/locale-links.ts +22 -18
  338. package/src/core/manifest.ts +3 -2
  339. package/src/core/meta.ts +89 -12
  340. package/src/core/nav-diagnostics.ts +56 -1
  341. package/src/core/navigation.ts +272 -83
  342. package/src/core/ordering-prefix.ts +27 -0
  343. package/src/core/project-graph.ts +50 -6
  344. package/src/core/safe-href.ts +53 -1
  345. package/src/core/safe-links.ts +11 -2
  346. package/src/core/schema.ts +94 -18
  347. package/src/core/sources/assets.ts +83 -41
  348. package/src/core/sources/contentful-rich-text.ts +28 -20
  349. package/src/core/sources/contentful.ts +25 -12
  350. package/src/core/sources/filesystem.ts +25 -3
  351. package/src/core/sources/github-releases.ts +131 -11
  352. package/src/core/sources/lexical.ts +30 -20
  353. package/src/core/sources/lower.ts +276 -56
  354. package/src/core/sources/mdx-remote.ts +52 -16
  355. package/src/core/sources/normalize.ts +456 -119
  356. package/src/core/sources/notion.ts +77 -34
  357. package/src/core/sources/obsidian.ts +40 -9
  358. package/src/core/sources/payload.ts +1 -0
  359. package/src/core/sources/portable-text.ts +85 -49
  360. package/src/core/sources/remote.ts +18 -2
  361. package/src/core/sources/resolve.ts +52 -33
  362. package/src/core/sources/sanity.ts +1 -0
  363. package/src/core/sources/strapi-blocks.ts +28 -19
  364. package/src/core/sources/strapi.ts +1 -0
  365. package/src/core/sources/types.ts +31 -0
  366. package/src/core/sources/watch.ts +20 -7
  367. package/src/core/standard-schema.ts +10 -6
  368. package/src/core/types.ts +9 -0
  369. package/src/core/ui-packs/ar.ts +13 -0
  370. package/src/core/ui-packs/bg.ts +13 -0
  371. package/src/core/ui-packs/bn.ts +13 -0
  372. package/src/core/ui-packs/ca.ts +13 -0
  373. package/src/core/ui-packs/cs.ts +13 -0
  374. package/src/core/ui-packs/da.ts +13 -0
  375. package/src/core/ui-packs/de.ts +13 -0
  376. package/src/core/ui-packs/el.ts +13 -0
  377. package/src/core/ui-packs/es.ts +13 -0
  378. package/src/core/ui-packs/fa.ts +13 -0
  379. package/src/core/ui-packs/fi.ts +13 -0
  380. package/src/core/ui-packs/fr.ts +13 -0
  381. package/src/core/ui-packs/he.ts +13 -0
  382. package/src/core/ui-packs/hi.ts +13 -0
  383. package/src/core/ui-packs/hr.ts +13 -0
  384. package/src/core/ui-packs/hu.ts +13 -0
  385. package/src/core/ui-packs/id.ts +13 -0
  386. package/src/core/ui-packs/it.ts +13 -0
  387. package/src/core/ui-packs/ja.ts +13 -0
  388. package/src/core/ui-packs/ko.ts +13 -0
  389. package/src/core/ui-packs/nl.ts +13 -0
  390. package/src/core/ui-packs/no.ts +13 -0
  391. package/src/core/ui-packs/pl.ts +13 -0
  392. package/src/core/ui-packs/pt-br.ts +13 -0
  393. package/src/core/ui-packs/pt.ts +13 -0
  394. package/src/core/ui-packs/ro.ts +13 -0
  395. package/src/core/ui-packs/ru.ts +13 -0
  396. package/src/core/ui-packs/sk.ts +13 -0
  397. package/src/core/ui-packs/sr.ts +13 -0
  398. package/src/core/ui-packs/sv.ts +13 -0
  399. package/src/core/ui-packs/th.ts +13 -0
  400. package/src/core/ui-packs/tr.ts +13 -0
  401. package/src/core/ui-packs/uk.ts +13 -0
  402. package/src/core/ui-packs/vi.ts +13 -0
  403. package/src/core/ui-packs/zh-tw.ts +13 -0
  404. package/src/core/ui-packs/zh.ts +13 -0
  405. package/src/core/version-cut.ts +69 -17
  406. package/src/core/versions.ts +4 -1
  407. package/src/deploy/adapters/node.ts +5 -2
  408. package/src/deploy/adapters/registry.ts +2 -1
  409. package/src/deploy/adapters/types.ts +13 -1
  410. package/src/deploy/artifacts.ts +52 -11
  411. package/src/deploy/cloudflare-negotiation.ts +27 -16
  412. package/src/deploy/headers.ts +75 -55
  413. package/src/deploy/node-headers.ts +148 -27
  414. package/src/deploy/platforms/cloudflare.ts +13 -3
  415. package/src/deploy/platforms/netlify.ts +83 -5
  416. package/src/deploy/platforms/node.ts +8 -5
  417. package/src/deploy/platforms/static.ts +2 -0
  418. package/src/deploy/platforms/types.ts +15 -0
  419. package/src/deploy/platforms/vercel.ts +10 -3
  420. package/src/deploy/redirects.ts +68 -21
  421. package/src/deploy/robots.ts +2 -2
  422. package/src/deploy/rss.ts +4 -3
  423. package/src/deploy/sitemap.ts +7 -5
  424. package/src/deploy/vercel-negotiation.ts +35 -3
  425. package/src/eval/agents.ts +10 -2
  426. package/src/eval/run.ts +25 -0
  427. package/src/markdown/base-links.ts +74 -29
  428. package/src/markdown/directives.ts +242 -36
  429. package/src/markdown/features.ts +17 -0
  430. package/src/markdown/index.ts +13 -9
  431. package/src/markdown/mdast.ts +5 -2
  432. package/src/markdown/relative-links.ts +15 -27
  433. package/src/markdown/route-snapshot.ts +37 -0
  434. package/src/og/card.ts +149 -9
  435. package/src/og/derive.ts +156 -5
  436. package/src/og/index.ts +1 -0
  437. package/src/openapi/asyncapi.ts +4 -2
  438. package/src/openapi/graphql-build.ts +28 -2
  439. package/src/openapi/model.ts +99 -27
  440. package/src/openapi/proxy.ts +63 -10
  441. package/src/openapi/render-mdx.ts +51 -3
  442. package/src/registry/eject.ts +297 -43
  443. package/src/search/adapters/version-scope.ts +30 -0
  444. package/src/search/documents.ts +89 -27
  445. package/src/search/popular.ts +2 -1
  446. package/src/search/sync/algolia.ts +36 -2
  447. package/src/seo/jsonld.ts +7 -3
  448. package/src/sources/registry.ts +5 -0
  449. package/src/theme/entry.ts +11 -4
  450. package/src/translate/agents.ts +6 -1
  451. package/src/translate/ledger.ts +26 -3
  452. package/src/translate/meta.ts +76 -24
  453. package/src/translate/run.ts +14 -8
  454. package/src/translate/validate.ts +14 -2
  455. package/src/translate/work-list.ts +91 -19
  456. package/src/upgrade/upgrade.ts +36 -4
  457. package/dist/cli/chunk-27g6wdth.js.map +0 -10
  458. package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
  459. package/dist/cli/chunk-6crbhc3x.js.map +0 -14
  460. package/dist/cli/chunk-6hsn950k.js.map +0 -10
  461. package/dist/cli/chunk-79jhk4py.js.map +0 -35
  462. package/dist/cli/chunk-82bbrxdn.js +0 -51
  463. package/dist/cli/chunk-82bbrxdn.js.map +0 -10
  464. package/dist/cli/chunk-abh8yjkn.js +0 -31
  465. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  466. package/dist/cli/chunk-ce574jw2.js +0 -23
  467. package/dist/cli/chunk-ce574jw2.js.map +0 -10
  468. package/dist/cli/chunk-dh8cwk36.js.map +0 -10
  469. package/dist/cli/chunk-epjnccmv.js.map +0 -10
  470. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  471. package/dist/cli/chunk-fs23ddbb.js.map +0 -35
  472. package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
  473. package/dist/cli/chunk-jwyddg7y.js.map +0 -15
  474. package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
  475. package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
  476. package/dist/cli/chunk-mb2919y2.js.map +0 -10
  477. package/dist/cli/chunk-q5163e60.js.map +0 -11
  478. package/dist/cli/chunk-qkqwkpte.js.map +0 -182
  479. package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
  480. package/dist/cli/chunk-s1p84fyh.js.map +0 -10
  481. package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
  482. package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
  483. package/dist/cli/chunk-yw7dm696.js.map +0 -14
  484. /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-2eytanqx.js.map} +0 -0
  485. /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-2z928egk.js.map} +0 -0
  486. /package/dist/cli/{chunk-ah61y8py.js.map → chunk-364znk6q.js.map} +0 -0
  487. /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-8ktnccpt.js.map} +0 -0
  488. /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-a58773jm.js.map} +0 -0
  489. /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-b07cmahc.js.map} +0 -0
  490. /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-r20tn01b.js.map} +0 -0
  491. /package/dist/cli/{chunk-5shv93fd.js.map → chunk-tkacnehg.js.map} +0 -0
  492. /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-tzmab476.js.map} +0 -0
@@ -1,6 +1,10 @@
1
1
  import type { Document, OperationObject } from "@scalar/openapi-types/3.1";
2
2
 
3
- import type { AsyncApiAction, AsyncApiDocument } from "./asyncapi.ts";
3
+ import type {
4
+ AsyncApiAction,
5
+ AsyncApiDocument,
6
+ AsyncApiSpecValue,
7
+ } from "./asyncapi.ts";
4
8
  // Type-only, so the import can't cycle at runtime (graphql.ts imports the
5
9
  // collector from here).
6
10
  import type { GraphqlDocument, GraphqlMember } from "./graphql.ts";
@@ -99,7 +103,10 @@ export interface ApiOperationRef {
99
103
  channelId?: string;
100
104
  }
101
105
 
102
- /** A tag/section, in first-seen order. */
106
+ /**
107
+ * A tag/section: the spec's declared tags in their declared order, then any
108
+ * undeclared tag an operation uses, in first-seen order.
109
+ */
103
110
  export interface ApiTagRef {
104
111
  slug: string;
105
112
  name: string;
@@ -212,6 +219,40 @@ export const specAddresses = (spec: ApiSpecData): SpecAddresses => {
212
219
  return { addresses, label: "Base URL" };
213
220
  };
214
221
 
222
+ const SERVER_VARIABLE = /\{(?<name>[^{}]+)\}/gu;
223
+
224
+ const isDocumentObject = (
225
+ value: AsyncApiSpecValue
226
+ ): value is Record<string, AsyncApiSpecValue> =>
227
+ typeof value === "object" && value !== null && !Array.isArray(value);
228
+
229
+ // YAML reads an unquoted `default: 8443` as a number; it is still the port.
230
+ const isVariableDefault = (
231
+ value: AsyncApiSpecValue
232
+ ): value is string | number =>
233
+ typeof value === "string" || typeof value === "number";
234
+
235
+ /**
236
+ * A server URL template — an OpenAPI `servers[].url`, an AsyncAPI server's
237
+ * `host` or `pathname` — with each `{variable}` replaced by the `default` its
238
+ * `variables` map declares. Code samples, the playground's Send, and the
239
+ * proxy allowlist all need a real address, and
240
+ * `https://{region}.api.example.com` is not one. A variable the map doesn't
241
+ * define with a default (invalid per both specs) stays templated.
242
+ */
243
+ export const withServerDefaults = (
244
+ template: string,
245
+ variables: AsyncApiSpecValue
246
+ ): string =>
247
+ template.replaceAll(SERVER_VARIABLE, (match, name: string) => {
248
+ const variable =
249
+ isDocumentObject(variables) && Object.hasOwn(variables, name)
250
+ ? variables[name]
251
+ : undefined;
252
+ const value = isDocumentObject(variable) ? variable.default : undefined;
253
+ return isVariableDefault(value) ? String(value) : match;
254
+ });
255
+
215
256
  // The runtime object check stands guard because the document was parsed from
216
257
  // arbitrary YAML/JSON: a spec can put a scalar where the type promises an
217
258
  // operation object.
@@ -219,9 +260,18 @@ const isOperation = (
219
260
  value: OperationObject | undefined
220
261
  ): value is OperationObject => typeof value === "object" && value !== null;
221
262
 
222
- /** A declared tag whose `name` really is a string at runtime, type aside. */
223
- const hasTagName = (tag: SpecTag): tag is SpecTag =>
224
- typeof tag.name === "string";
263
+ /** Whether a spec value is text YAML may have read as a number. */
264
+ const isText = <Value>(value: Value): value is Value & (string | number) =>
265
+ typeof value === "string" || typeof value === "number";
266
+
267
+ /**
268
+ * A spec field that names something, as a string. YAML reads an unquoted
269
+ * `operationId: 404` or `tags: [2024]` as a number, which still names the
270
+ * operation or tag; anything else a hand-written spec puts there counts as
271
+ * absent.
272
+ */
273
+ const specText = <Value>(value: Value): string | undefined =>
274
+ isText(value) ? String(value) : undefined;
225
275
 
226
276
  /**
227
277
  * Assign each distinct tag name a unique slug. `slugify` can collapse
@@ -254,9 +304,6 @@ export const tagSlugger = (): ((name: string) => string) => {
254
304
  /** An operation before the collector assigns its unique key and route. */
255
305
  type CollectedOperation = Omit<ApiOperationRef, "route" | "tagSlug">;
256
306
 
257
- /** A document's declared tag entry (`tags[n]`). */
258
- type SpecTag = NonNullable<ApiDocument["tags"]>[number];
259
-
260
307
  /** The flattened output both extractors produce. */
261
308
  export interface CollectedOperations {
262
309
  operations: ApiOperationRef[];
@@ -276,9 +323,10 @@ export interface ExtractedOperations extends CollectedOperations {
276
323
 
277
324
  /**
278
325
  * The collector behind both extractors (OpenAPI here, AsyncAPI in
279
- * `asyncapi.ts`): first-seen tag ordering, key de-duplication (a repeated key
280
- * gains its method/action as a suffix), and the shared route template — so
281
- * URL shape and slug rules can never drift between the two spec kinds.
326
+ * `asyncapi.ts`): declared-then-first-seen tag ordering, key de-duplication
327
+ * (a repeated key gains its method/action as a suffix), and the shared route
328
+ * template — so URL shape and slug rules can never drift between the two spec
329
+ * kinds.
282
330
  */
283
331
  export const operationCollector = (
284
332
  baseRoute: string,
@@ -310,15 +358,27 @@ export const operationCollector = (
310
358
  });
311
359
  };
312
360
 
361
+ // Declared tags in the order the spec's `tags` list gives them, then any
362
+ // tag an operation uses without declaring it, in first-use order. The
363
+ // declared order is the author's section order, not an accident of which
364
+ // path happens to come first.
365
+ const declared = [...tagMeta.keys()];
366
+ const rank = (name: string): number => {
367
+ const index = declared.indexOf(name);
368
+ return index === -1 ? declared.length : index;
369
+ };
370
+
313
371
  const finish = (): CollectedOperations => ({
314
372
  operations,
315
- tags: tagOrder.map((name) => ({
316
- description: tagMeta.get(name) ?? "",
317
- name,
318
- // The same slugger instance, so every tag resolves to the slug its
319
- // operations were routed under.
320
- slug: slugForTag(name),
321
- })),
373
+ tags: tagOrder
374
+ .toSorted((a, b) => rank(a) - rank(b))
375
+ .map((name) => ({
376
+ description: tagMeta.get(name) ?? "",
377
+ name,
378
+ // The same slugger instance, so every tag resolves to the slug its
379
+ // operations were routed under.
380
+ slug: slugForTag(name),
381
+ })),
322
382
  });
323
383
 
324
384
  return { add, finish };
@@ -328,7 +388,8 @@ export const operationCollector = (
328
388
  * Flatten a 3.1 document into a route-mapped operation list and its ordered
329
389
  * tags. Operations inherit the first tag they declare; keys are de-duplicated so
330
390
  * a repeated `operationId` still yields distinct routes. `warnings` reports
331
- * anything skipped (a `$ref` path item), so missing operations aren't silent.
391
+ * anything skipped (a `$ref` path item, webhooks), so missing operations
392
+ * aren't silent.
332
393
  */
333
394
  export const extractOperations = (
334
395
  document: ApiDocument,
@@ -336,9 +397,10 @@ export const extractOperations = (
336
397
  ): ExtractedOperations => {
337
398
  const warnings: string[] = [];
338
399
  const tagMeta = new Map(
339
- (document.tags ?? [])
340
- .filter(hasTagName)
341
- .map((tag) => [tag.name, tag.description ?? ""])
400
+ (document.tags ?? []).flatMap((tag): [string, string][] => {
401
+ const name = specText(tag.name);
402
+ return name === undefined ? [] : [[name, tag.description ?? ""]];
403
+ })
342
404
  );
343
405
  const collector = operationCollector(baseRoute, tagMeta);
344
406
 
@@ -358,19 +420,29 @@ export const extractOperations = (
358
420
  if (!isOperation(operation)) {
359
421
  continue;
360
422
  }
423
+ const operationId = specText(operation.operationId);
361
424
  collector.add({
362
425
  deprecated: operation.deprecated ?? false,
363
- description: operation.description ?? "",
364
- key: operationKey(method, path, operation.operationId),
426
+ description: specText(operation.description) ?? "",
427
+ key: operationKey(method, path, operationId),
365
428
  method,
366
- operationId: operation.operationId,
429
+ operationId,
367
430
  path,
368
- summary: operation.summary ?? "",
369
- tag: operation.tags?.[0] ?? UNTAGGED,
431
+ summary: specText(operation.summary) ?? "",
432
+ tag: specText(operation.tags?.[0]) ?? UNTAGGED,
370
433
  });
371
434
  }
372
435
  }
373
436
 
437
+ // OpenAPI 3.1 webhooks (requests the API sends, not ones it serves) have no
438
+ // page renderer; say so rather than dropping them silently.
439
+ const webhooks = Object.keys(document.webhooks ?? {});
440
+ if (webhooks.length > 0) {
441
+ warnings.push(
442
+ `The spec declares ${webhooks.length === 1 ? "a webhook" : `${webhooks.length} webhooks`} under "webhooks" (${webhooks.join(", ")}); webhooks aren't rendered, so they are missing from the reference.`
443
+ );
444
+ }
445
+
374
446
  return { ...collector.finish(), warnings };
375
447
  };
376
448
 
@@ -12,25 +12,52 @@
12
12
  * network and stays safe to bundle into the generated endpoint file.
13
13
  */
14
14
 
15
+ import { PROXY_HEADERS_HEADER } from "../components/openapi/request.ts";
15
16
  import { readCappedBody } from "../core/request-body.ts";
16
17
 
17
18
  /**
18
- * Request headers never forwarded upstream: hop-by-hop headers describe this
19
- * connection (not the upstream one), `host`/`origin`/`referer` would leak or
20
- * misattribute the docs site, and `cookie` would forward reader credentials
21
- * to an arbitrary target. `accept-encoding`/`content-length` are recomputed
22
- * by the runtime's own fetch.
19
+ * Request headers never forwarded upstream, even when the playground names
20
+ * them: hop-by-hop headers describe this connection (not the upstream one),
21
+ * `host`/`origin`/`referer` would leak or misattribute the docs site, `cookie`
22
+ * would forward reader credentials to an arbitrary target, and the rest are
23
+ * set or rewritten by the platform in front of the docs server — the
24
+ * reader's address, the edge's own identity — whatever the browser sent.
25
+ * `accept-encoding`/`content-length` are recomputed by the runtime's own
26
+ * fetch.
23
27
  */
24
28
  const REQUEST_DROP = {
25
29
  "accept-encoding": true,
30
+ "cdn-loop": true,
26
31
  connection: true,
27
32
  "content-length": true,
28
33
  cookie: true,
34
+ forwarded: true,
29
35
  host: true,
36
+ "keep-alive": true,
30
37
  origin: true,
38
+ "proxy-authorization": true,
31
39
  referer: true,
40
+ te: true,
41
+ trailer: true,
42
+ "transfer-encoding": true,
43
+ "true-client-ip": true,
44
+ upgrade: true,
45
+ via: true,
46
+ "x-client-ip": true,
47
+ "x-real-ip": true,
32
48
  } satisfies Record<string, true>;
33
49
 
50
+ /**
51
+ * Platform header families, dropped like {@link REQUEST_DROP}: Cloudflare's
52
+ * (`cf-connecting-ip`, the Access `cf-access-jwt-assertion`), the
53
+ * `x-forwarded-*` set, Vercel's (`x-vercel-oidc-token`), Netlify's, Fly's,
54
+ * and AWS load balancers' (`x-amzn-oidc-data`).
55
+ */
56
+ const PLATFORM_HEADER = /^(?:cf-|x-forwarded-|x-vercel-|x-nf-|fly-|x-amzn-)/u;
57
+
58
+ /** The {@link PROXY_HEADERS_HEADER} name as `Headers` iterates it. */
59
+ const DECLARED = PROXY_HEADERS_HEADER.toLowerCase();
60
+
34
61
  /**
35
62
  * Upstream response headers never returned to the browser: the runtime's
36
63
  * fetch already decoded the body (so `content-encoding`/`content-length` no
@@ -99,20 +126,45 @@ const DOCUMENT_TYPE =
99
126
  const badRequest = (error: string): Response =>
100
127
  Response.json({ error }, { status: 400 });
101
128
 
102
- /** Copy headers, skipping the given denylist (names are already lowercase). */
129
+ /**
130
+ * Copy headers, skipping the given denylist and any name `keep` rejects
131
+ * (names are already lowercase).
132
+ */
103
133
  const filterHeaders = (
104
134
  source: Headers,
105
- drop: Record<string, true>
135
+ drop: Record<string, true>,
136
+ keep: (name: string) => boolean = () => true
106
137
  ): Headers => {
107
138
  const headers = new Headers();
108
139
  for (const [name, value] of source) {
109
- if (!drop[name]) {
140
+ if (!drop[name] && keep(name)) {
110
141
  headers.set(name, value);
111
142
  }
112
143
  }
113
144
  return headers;
114
145
  };
115
146
 
147
+ /**
148
+ * The headers to send upstream: only those the playground set itself, which
149
+ * it names in {@link PROXY_HEADERS_HEADER}. Forwarding everything else minus
150
+ * a denylist would hand the documented API whatever the browser and the
151
+ * platform attach on their own — the HTTP Basic credentials of a docs site
152
+ * behind a password (a same-origin fetch with no `Authorization` of its own
153
+ * carries them), a Cloudflare Access assertion, a Vercel OIDC token.
154
+ */
155
+ const forwardedHeaders = (source: Headers): Headers => {
156
+ const named = new Set(
157
+ (source.get(DECLARED) ?? "")
158
+ .split(",")
159
+ .map((name) => name.trim().toLowerCase())
160
+ );
161
+ return filterHeaders(
162
+ source,
163
+ REQUEST_DROP,
164
+ (name) => named.has(name) && !PLATFORM_HEADER.test(name)
165
+ );
166
+ };
167
+
116
168
  /** A 403 for a target no configured spec declares as one of its servers. */
117
169
  const forbidden = (origin: string): Response =>
118
170
  Response.json(
@@ -186,7 +238,8 @@ const followUpstream = async (args: {
186
238
  /**
187
239
  * Build the `/_api-proxy` fetch handler. The client sends its REAL method,
188
240
  * headers, and body to `?url=<encodeURIComponent(target)>`; the handler
189
- * forwards them (minus {@link REQUEST_DROP}) and mirrors the upstream response
241
+ * forwards the method, the body, and the headers the client names (see
242
+ * {@link forwardedHeaders}), and mirrors the upstream response
190
243
  * (minus {@link RESPONSE_DROP}) with an `x-blume-proxy` marker. An unreachable
191
244
  * upstream is a 502 with a JSON `error`.
192
245
  *
@@ -252,7 +305,7 @@ export const createPlaygroundProxyHandler = (
252
305
  allowed,
253
306
  body,
254
307
  fetchImpl,
255
- headers: filterHeaders(request.headers, REQUEST_DROP),
308
+ headers: forwardedHeaders(request.headers),
256
309
  hop: 0,
257
310
  method: request.method,
258
311
  signal: AbortSignal.timeout(timeoutMs),
@@ -90,6 +90,9 @@ const codeSpans = (text: string, tree: Root): [number, number][] => {
90
90
  return spans;
91
91
  };
92
92
 
93
+ /** Stands in for `<` when locating code spans (see `mdxSafe`). */
94
+ const HTML_MASK = "";
95
+
93
96
  /**
94
97
  * Escape MDX-special syntax in prose while leaving code verbatim. A spec is
95
98
  * someone else's content, so a link whose destination isn't a web, mail, or
@@ -101,8 +104,14 @@ const mdxSafe = (text: string): string => {
101
104
  const unsafe = unsafeLinkSpans(tree);
102
105
  const insideUnsafe = (offset: number): boolean =>
103
106
  unsafe.some((span) => offset >= span.start && offset < span.end);
107
+ // Code spans come from a parse with every `<` masked. The emitted MDX
108
+ // escapes each `<` outside code to `&lt;`, so no line of it is an HTML
109
+ // block — but CommonMark reads `<div>` as one and never looks for the code
110
+ // spans inside it, which would leave their braces to be escaped and shown
111
+ // as a literal `&#123;`. The mask is one code unit, so offsets line up.
112
+ const masked = text.replaceAll("<", HTML_MASK);
104
113
  const segments = [
105
- ...codeSpans(text, tree)
114
+ ...codeSpans(masked, fromMarkdown(masked))
106
115
  .filter(([start]) => !insideUnsafe(start))
107
116
  .map(([start, end]) => ({ end, start, text: text.slice(start, end) })),
108
117
  ...unsafe,
@@ -117,6 +126,45 @@ const mdxSafe = (text: string): string => {
117
126
  return out + escapeProse(text.slice(cursor));
118
127
  };
119
128
 
129
+ // A fence opener: up to three columns of indentation, then a run of three or
130
+ // more backticks or tildes.
131
+ const FENCE_OPEN = /^ {0,3}(?<fence>`{3,}|~{3,})/u;
132
+
133
+ /**
134
+ * Close a fenced code block the text leaves open. An unclosed fence runs to
135
+ * the end of the document, so the component appended after a description
136
+ * would render as code, and the page would lose its parameters, responses,
137
+ * and playground. Only the last top-level block can run that far — a fence in
138
+ * a quote or list closes with its container — and it's read from the same
139
+ * `<`-masked parse as `mdxSafe`, since the emitted MDX has no HTML blocks to
140
+ * hide a fence in.
141
+ */
142
+ const closeOpenFence = (text: string): string => {
143
+ const last = fromMarkdown(text.replaceAll("<", HTML_MASK)).children.at(-1);
144
+ if (last?.type !== "code") {
145
+ return text;
146
+ }
147
+ // fromMarkdown always stamps positions; 0 is an unreachable guard.
148
+ const block = text.slice(last.position?.start.offset ?? 0);
149
+ const fence = FENCE_OPEN.exec(block)?.groups?.fence;
150
+ // No fence: indented code, which MDX reads as a paragraph.
151
+ if (!fence) {
152
+ return text;
153
+ }
154
+ const lines = block.split("\n");
155
+ const closer = new RegExp(
156
+ `^ {0,3}${fence[0]}{${fence.length},}[ \\t\\r]*$`,
157
+ "u"
158
+ );
159
+ return lines.length > 1 && closer.test(lines.at(-1) ?? "")
160
+ ? text
161
+ : `${text}\n${fence}`;
162
+ };
163
+
164
+ /** Spec prose as MDX that is safe to follow with a component. */
165
+ const descriptionMdx = (text: string): string =>
166
+ mdxSafe(closeOpenFence(text.trim()));
167
+
120
168
  /**
121
169
  * Frontmatter emitted for one operation or overview page. Boolean flags are
122
170
  * assigned only when set, so absent keys stay absent in the staged MDX.
@@ -240,7 +288,7 @@ const operationDescription = (
240
288
  /** Prepend a markdown description (if any) above a component invocation. */
241
289
  const withDescription = (description: string, component: string): string =>
242
290
  description.trim()
243
- ? `${mdxSafe(description.trim())}\n\n${component}`
291
+ ? `${descriptionMdx(description)}\n\n${component}`
244
292
  : component;
245
293
 
246
294
  export const operationMdx = (
@@ -356,7 +404,7 @@ export const overviewMdx = (
356
404
  continue;
357
405
  }
358
406
  const description = tag.description.trim()
359
- ? [mdxSafe(tag.description.trim())]
407
+ ? [descriptionMdx(tag.description)]
360
408
  : [];
361
409
  tagSections.push(
362
410
  [