blume 2.0.1 → 2.0.2

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 (410) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli/{chunk-vtk4a6dg.js → chunk-00gs3wqs.js} +1 -1
  3. package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
  4. package/dist/cli/chunk-273ygyr4.js.map +11 -0
  5. package/dist/cli/{chunk-yw7dm696.js → chunk-3yce002v.js} +21 -8
  6. package/dist/cli/{chunk-yw7dm696.js.map → chunk-3yce002v.js.map} +3 -3
  7. package/dist/cli/{chunk-yt5n7ppj.js → chunk-4e9b9ra6.js} +17 -7
  8. package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
  9. package/dist/cli/{chunk-fs23ddbb.js → chunk-5r8g91qn.js} +352 -676
  10. package/dist/cli/chunk-5r8g91qn.js.map +34 -0
  11. package/dist/cli/{chunk-6vm74dry.js → chunk-6dtt0zfn.js} +8 -8
  12. package/dist/cli/{chunk-qwsrynx5.js → chunk-6k4ftwze.js} +38 -19
  13. package/dist/cli/{chunk-qwsrynx5.js.map → chunk-6k4ftwze.js.map} +4 -4
  14. package/dist/cli/{chunk-qs4q5p4e.js → chunk-7mbqtmgb.js} +16 -7
  15. package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
  16. package/dist/cli/{chunk-q5163e60.js → chunk-7vtckvaw.js} +21 -19
  17. package/dist/cli/chunk-7vtckvaw.js.map +11 -0
  18. package/dist/cli/{chunk-kdp5q7ke.js → chunk-8g8ytmgx.js} +17 -18
  19. package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-8g8ytmgx.js.map} +2 -2
  20. package/dist/cli/{chunk-epjnccmv.js → chunk-91ws1n6j.js} +18 -15
  21. package/dist/cli/chunk-91ws1n6j.js.map +10 -0
  22. package/dist/cli/{chunk-fxypxtvm.js → chunk-bbnwccaz.js} +2 -2
  23. package/dist/cli/{chunk-m3vmjgmq.js → chunk-bfwp9vp6.js} +16 -8
  24. package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
  25. package/dist/cli/{chunk-f2z5v128.js → chunk-d1v5rhy0.js} +14 -15
  26. package/dist/cli/{chunk-f2z5v128.js.map → chunk-d1v5rhy0.js.map} +2 -2
  27. package/dist/cli/{chunk-6crbhc3x.js → chunk-ddndchfr.js} +21 -6
  28. package/dist/cli/chunk-ddndchfr.js.map +14 -0
  29. package/dist/cli/{chunk-ce574jw2.js → chunk-esh98wmb.js} +1 -1
  30. package/dist/cli/{chunk-zxcczpyx.js → chunk-fsmrqk8a.js} +1 -1
  31. package/dist/cli/{chunk-hdpx1tax.js → chunk-g698a744.js} +5 -5
  32. package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
  33. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
  34. package/dist/cli/{chunk-zxh4d9vy.js → chunk-h2ez8dzb.js} +4 -4
  35. package/dist/cli/{chunk-5shv93fd.js → chunk-h7k3nq3v.js} +2 -2
  36. package/dist/cli/{chunk-79jhk4py.js → chunk-hqp2ajnh.js} +251 -122
  37. package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
  38. package/dist/cli/{chunk-2hn4b8z7.js → chunk-hr8ne106.js} +109 -40
  39. package/dist/cli/chunk-hr8ne106.js.map +13 -0
  40. package/dist/cli/{chunk-6hsn950k.js → chunk-j85scx15.js} +62 -18
  41. package/dist/cli/chunk-j85scx15.js.map +10 -0
  42. package/dist/cli/{chunk-s1p84fyh.js → chunk-k7pj68a8.js} +63 -22
  43. package/dist/cli/chunk-k7pj68a8.js.map +11 -0
  44. package/dist/cli/{chunk-ah61y8py.js → chunk-mqc662a6.js} +2 -2
  45. package/dist/cli/{chunk-ch6g3ar0.js → chunk-n1yg3tj3.js} +4 -4
  46. package/dist/cli/{chunk-mb2919y2.js → chunk-nfcyttvj.js} +17 -6
  47. package/dist/cli/chunk-nfcyttvj.js.map +10 -0
  48. package/dist/cli/{chunk-kpf8rrjc.js → chunk-pbg5a4s3.js} +56 -24
  49. package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
  50. package/dist/cli/{chunk-jwyddg7y.js → chunk-ppzjqwx2.js} +21 -14
  51. package/dist/cli/{chunk-jwyddg7y.js.map → chunk-ppzjqwx2.js.map} +4 -4
  52. package/dist/cli/{chunk-dh8cwk36.js → chunk-qkb5a8sa.js} +24 -9
  53. package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
  54. package/dist/cli/{chunk-wm7js3j9.js → chunk-sqw4ekg1.js} +2 -2
  55. package/dist/cli/{chunk-qkqwkpte.js → chunk-v6ya5kcb.js} +3083 -953
  56. package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
  57. package/dist/cli/{chunk-fz5wtpmh.js → chunk-w4bxdvsa.js} +18 -15
  58. package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
  59. package/dist/cli/{chunk-27g6wdth.js → chunk-wdrt2k2v.js} +2 -2
  60. package/dist/cli/{chunk-wgm7m9qk.js → chunk-xh43dwgw.js} +158 -43
  61. package/dist/cli/chunk-xh43dwgw.js.map +36 -0
  62. package/dist/cli/{chunk-s6jhgk0q.js → chunk-zp79m0ts.js} +2 -2
  63. package/dist/cli/index.js +160 -35
  64. package/dist/cli/index.js.map +4 -4
  65. package/dist/types/ai/agent-surface.d.ts +32 -0
  66. package/dist/types/ai/api-catalog.d.ts +7 -1
  67. package/dist/types/ai/ask-context.d.ts +7 -0
  68. package/dist/types/ai/component-markdown.d.ts +4 -4
  69. package/dist/types/ai/relative-links.d.ts +9 -4
  70. package/dist/types/ai/static-expression.d.ts +28 -0
  71. package/dist/types/analytics/databuddy.d.ts +43 -0
  72. package/dist/types/analytics/index.d.ts +2 -0
  73. package/dist/types/analytics/schema.d.ts +14 -0
  74. package/dist/types/cli/env.d.ts +5 -0
  75. package/dist/types/cli/init/scaffold.d.ts +19 -3
  76. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  77. package/dist/types/core/base-path.d.ts +9 -0
  78. package/dist/types/core/config-input.d.ts +4 -4
  79. package/dist/types/core/config.d.ts +2 -2
  80. package/dist/types/core/graph.d.ts +2 -0
  81. package/dist/types/core/i18n-ui.d.ts +31 -0
  82. package/dist/types/core/i18n.d.ts +7 -1
  83. package/dist/types/core/links.d.ts +3 -1
  84. package/dist/types/core/load-module.d.ts +10 -0
  85. package/dist/types/core/locale-links.d.ts +12 -2
  86. package/dist/types/core/meta.d.ts +5 -1
  87. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  88. package/dist/types/core/navigation.d.ts +38 -0
  89. package/dist/types/core/ordering-prefix.d.ts +4 -0
  90. package/dist/types/core/safe-links.d.ts +3 -1
  91. package/dist/types/core/schema.d.ts +52 -13
  92. package/dist/types/core/sources/github-releases.d.ts +5 -0
  93. package/dist/types/core/sources/lower.d.ts +14 -2
  94. package/dist/types/core/sources/normalize.d.ts +10 -1
  95. package/dist/types/core/sources/remote.d.ts +11 -1
  96. package/dist/types/core/sources/resolve.d.ts +12 -0
  97. package/dist/types/core/sources/types.d.ts +31 -0
  98. package/dist/types/core/types.d.ts +9 -0
  99. package/dist/types/deploy/adapters/node.d.ts +5 -2
  100. package/dist/types/deploy/adapters/types.d.ts +7 -0
  101. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  102. package/dist/types/deploy/headers.d.ts +31 -7
  103. package/dist/types/deploy/node-headers.d.ts +43 -8
  104. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  105. package/dist/types/deploy/platforms/node.d.ts +6 -5
  106. package/dist/types/deploy/platforms/types.d.ts +7 -0
  107. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  108. package/dist/types/deploy/redirects.d.ts +7 -2
  109. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  110. package/dist/types/openapi/model.d.ts +20 -6
  111. package/dist/types/search/sync/algolia.d.ts +3 -1
  112. package/docs/01-quickstart.mdx +3 -2
  113. package/docs/02-deployment.mdx +17 -8
  114. package/docs/08-faq.mdx +9 -2
  115. package/docs/advanced/changelog.mdx +1 -1
  116. package/docs/advanced/custom-pages.mdx +4 -2
  117. package/docs/cli/audit.mdx +2 -2
  118. package/docs/cli/doctor.mdx +2 -2
  119. package/docs/cli/evals.mdx +2 -2
  120. package/docs/cli/index.mdx +4 -1
  121. package/docs/cli/translate.mdx +1 -1
  122. package/docs/configuration/analytics.mdx +23 -2
  123. package/docs/configuration/assistant.mdx +1 -1
  124. package/docs/configuration/customization.mdx +4 -3
  125. package/docs/configuration/index.mdx +5 -3
  126. package/docs/configuration/search.mdx +1 -1
  127. package/docs/content/components.mdx +1 -1
  128. package/docs/content/frontmatter.mdx +5 -1
  129. package/docs/content/i18n.mdx +1 -1
  130. package/docs/content/index.mdx +4 -2
  131. package/docs/content/meta.mdx +5 -3
  132. package/docs/content/navigation.mdx +29 -4
  133. package/docs/content/sources.mdx +18 -12
  134. package/docs/content/versioning.mdx +1 -0
  135. package/docs/discoverability/agent-discovery.mdx +21 -7
  136. package/docs/discoverability/index.mdx +2 -2
  137. package/docs/discoverability/llms-txt.mdx +2 -5
  138. package/docs/discoverability/markdown.mdx +5 -3
  139. package/docs/discoverability/mcp.mdx +4 -0
  140. package/docs/discoverability/metadata.mdx +3 -2
  141. package/docs/discoverability/open-graph.mdx +6 -4
  142. package/docs/discoverability/rss.mdx +3 -1
  143. package/docs/references/asyncapi.mdx +1 -1
  144. package/docs/references/graphql.mdx +2 -2
  145. package/docs/references/openapi.mdx +3 -3
  146. package/package.json +1 -1
  147. package/skills/blume-migrate/SKILL.md +6 -6
  148. package/skills/blume-migrate/references/docusaurus.md +6 -6
  149. package/skills/blume-migrate/references/mintlify.md +3 -3
  150. package/skills/blume-migrate/references/monorepo.md +1 -1
  151. package/skills/blume-migrate/references/nextra.md +1 -1
  152. package/skills/blume-migrate/references/starlight.md +5 -5
  153. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
  154. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  155. package/src/ai/agent-surface.ts +56 -0
  156. package/src/ai/api/spec.ts +15 -2
  157. package/src/ai/api-catalog.ts +7 -1
  158. package/src/ai/ask-context.ts +14 -2
  159. package/src/ai/ask-data.ts +26 -12
  160. package/src/ai/component-markdown.ts +43 -37
  161. package/src/ai/link-headers.ts +8 -3
  162. package/src/ai/llms.ts +4 -3
  163. package/src/ai/markdown.ts +23 -11
  164. package/src/ai/mcp/server.ts +78 -7
  165. package/src/ai/relative-links.ts +78 -10
  166. package/src/ai/static-expression.ts +416 -0
  167. package/src/ai/visibility.ts +45 -14
  168. package/src/analytics/databuddy.ts +67 -0
  169. package/src/analytics/head.ts +4 -0
  170. package/src/analytics/index.ts +2 -0
  171. package/src/analytics/posthog.ts +21 -3
  172. package/src/analytics/schema.ts +2 -0
  173. package/src/astro/generate.ts +71 -27
  174. package/src/astro/templates.ts +61 -23
  175. package/src/audit/catalog.ts +2 -2
  176. package/src/audit/checks/assets.ts +25 -5
  177. package/src/audit/checks/content.ts +20 -2
  178. package/src/audit/checks/i18n.ts +46 -8
  179. package/src/audit/checks/indexability.ts +39 -19
  180. package/src/audit/checks/links.ts +14 -0
  181. package/src/audit/checks/llms.ts +6 -3
  182. package/src/audit/checks/network.ts +3 -1
  183. package/src/audit/checks/og-image.ts +10 -0
  184. package/src/audit/checks/robots.ts +6 -1
  185. package/src/audit/checks/sitemap.ts +56 -31
  186. package/src/audit/checks/social.ts +21 -2
  187. package/src/audit/crawl.ts +88 -15
  188. package/src/audit/report.ts +54 -17
  189. package/src/audit/run.ts +1 -0
  190. package/src/audit/types.ts +18 -0
  191. package/src/audit/url.ts +37 -6
  192. package/src/cli/build-failure.ts +50 -0
  193. package/src/cli/commands/audit.ts +7 -0
  194. package/src/cli/commands/build.ts +19 -5
  195. package/src/cli/commands/check.ts +2 -0
  196. package/src/cli/commands/eval.ts +1 -7
  197. package/src/cli/commands/init.ts +15 -5
  198. package/src/cli/commands/preview.ts +15 -0
  199. package/src/cli/commands/sync.ts +2 -0
  200. package/src/cli/commands/translate.ts +9 -6
  201. package/src/cli/commands/upgrade.ts +11 -0
  202. package/src/cli/eject-scripts.ts +32 -7
  203. package/src/cli/env.ts +12 -1
  204. package/src/cli/init/scaffold.ts +117 -11
  205. package/src/cli/init/starter-spec.ts +235 -0
  206. package/src/components/content/AccordionItem.astro +26 -22
  207. package/src/components/content/Badge.astro +2 -9
  208. package/src/components/content/Card.astro +2 -2
  209. package/src/components/content/Component.astro +9 -3
  210. package/src/components/content/Expandable.astro +5 -1
  211. package/src/components/content/Frame.astro +2 -7
  212. package/src/components/content/GithubInfo.astro +12 -2
  213. package/src/components/content/Prompt.astro +2 -7
  214. package/src/components/content/Tabs.astro +54 -5
  215. package/src/components/content/Tile.astro +1 -1
  216. package/src/components/content/Tooltip.astro +69 -7
  217. package/src/components/content/Tree.astro +7 -2
  218. package/src/components/content/TypeTable.astro +10 -5
  219. package/src/components/content/Update.astro +8 -2
  220. package/src/components/content/auto-type-table.ts +4 -1
  221. package/src/components/content/badge-color.ts +17 -0
  222. package/src/components/content/base-href.ts +18 -3
  223. package/src/components/content/inline-markdown.ts +27 -7
  224. package/src/components/copy-feedback.ts +35 -8
  225. package/src/components/islands/assistant.tsx +16 -2
  226. package/src/components/islands/hooks.ts +7 -2
  227. package/src/components/islands/webmcp.ts +12 -8
  228. package/src/components/layout/Banner.astro +23 -4
  229. package/src/components/layout/Header.astro +22 -9
  230. package/src/components/layout/Logo.astro +5 -0
  231. package/src/components/layout/NavSelector.astro +6 -1
  232. package/src/components/layout/NavTabMenu.astro +133 -0
  233. package/src/components/layout/NavTree.astro +15 -6
  234. package/src/components/layout/NavTreeCache.astro +5 -2
  235. package/src/components/layout/NavTreeScript.astro +45 -5
  236. package/src/components/layout/PageLayout.astro +22 -8
  237. package/src/components/layout/ReferenceLayout.astro +4 -1
  238. package/src/components/layout/RootLayout.astro +21 -8
  239. package/src/components/layout/Search.astro +5 -1
  240. package/src/components/layout/analytics-client.ts +2 -0
  241. package/src/components/openapi/GraphqlType.astro +11 -3
  242. package/src/components/openapi/MessageComposer.astro +1 -1
  243. package/src/components/openapi/Operation.astro +21 -5
  244. package/src/components/openapi/PanelTabs.astro +4 -1
  245. package/src/components/openapi/Playground.astro +8 -2
  246. package/src/components/openapi/RequestPanel.astro +9 -5
  247. package/src/components/openapi/SchemaProperty.astro +11 -33
  248. package/src/components/openapi/SchemaTable.astro +27 -52
  249. package/src/components/openapi/helpers.ts +90 -12
  250. package/src/components/openapi/message-composer.ts +8 -0
  251. package/src/components/openapi/message-model.ts +12 -2
  252. package/src/components/openapi/message.ts +4 -1
  253. package/src/components/openapi/operation-model.ts +40 -8
  254. package/src/components/openapi/panel.ts +29 -5
  255. package/src/components/openapi/playground-client.ts +37 -7
  256. package/src/components/openapi/playground-schema.ts +25 -6
  257. package/src/components/openapi/request.ts +2 -0
  258. package/src/components/openapi/schema-tree.ts +203 -0
  259. package/src/components/openapi/snippets.ts +30 -15
  260. package/src/components/openapi/validate-json.ts +1 -1
  261. package/src/core/base-path.ts +15 -0
  262. package/src/core/config-input.ts +4 -4
  263. package/src/core/config.ts +18 -4
  264. package/src/core/diagnostics.ts +217 -30
  265. package/src/core/graph.ts +120 -11
  266. package/src/core/i18n-ui.ts +39 -2
  267. package/src/core/i18n.ts +13 -2
  268. package/src/core/links.ts +7 -4
  269. package/src/core/load-module.ts +20 -0
  270. package/src/core/locale-links.ts +17 -17
  271. package/src/core/manifest.ts +3 -2
  272. package/src/core/meta.ts +69 -6
  273. package/src/core/nav-diagnostics.ts +56 -1
  274. package/src/core/navigation.ts +245 -76
  275. package/src/core/ordering-prefix.ts +27 -0
  276. package/src/core/project-graph.ts +23 -1
  277. package/src/core/safe-href.ts +53 -1
  278. package/src/core/safe-links.ts +11 -2
  279. package/src/core/schema.ts +70 -14
  280. package/src/core/sources/assets.ts +83 -41
  281. package/src/core/sources/contentful-rich-text.ts +3 -2
  282. package/src/core/sources/contentful.ts +25 -12
  283. package/src/core/sources/filesystem.ts +5 -1
  284. package/src/core/sources/github-releases.ts +106 -5
  285. package/src/core/sources/lexical.ts +9 -4
  286. package/src/core/sources/lower.ts +79 -26
  287. package/src/core/sources/mdx-remote.ts +1 -0
  288. package/src/core/sources/normalize.ts +132 -30
  289. package/src/core/sources/notion.ts +26 -10
  290. package/src/core/sources/obsidian.ts +17 -3
  291. package/src/core/sources/payload.ts +1 -0
  292. package/src/core/sources/portable-text.ts +63 -44
  293. package/src/core/sources/remote.ts +18 -2
  294. package/src/core/sources/resolve.ts +52 -33
  295. package/src/core/sources/sanity.ts +1 -0
  296. package/src/core/sources/strapi-blocks.ts +9 -4
  297. package/src/core/sources/strapi.ts +1 -0
  298. package/src/core/sources/types.ts +31 -0
  299. package/src/core/types.ts +9 -0
  300. package/src/core/ui-packs/ar.ts +13 -0
  301. package/src/core/ui-packs/bg.ts +13 -0
  302. package/src/core/ui-packs/bn.ts +13 -0
  303. package/src/core/ui-packs/ca.ts +13 -0
  304. package/src/core/ui-packs/cs.ts +13 -0
  305. package/src/core/ui-packs/da.ts +13 -0
  306. package/src/core/ui-packs/de.ts +13 -0
  307. package/src/core/ui-packs/el.ts +13 -0
  308. package/src/core/ui-packs/es.ts +13 -0
  309. package/src/core/ui-packs/fa.ts +13 -0
  310. package/src/core/ui-packs/fi.ts +13 -0
  311. package/src/core/ui-packs/fr.ts +13 -0
  312. package/src/core/ui-packs/he.ts +13 -0
  313. package/src/core/ui-packs/hi.ts +13 -0
  314. package/src/core/ui-packs/hr.ts +13 -0
  315. package/src/core/ui-packs/hu.ts +13 -0
  316. package/src/core/ui-packs/id.ts +13 -0
  317. package/src/core/ui-packs/it.ts +13 -0
  318. package/src/core/ui-packs/ja.ts +13 -0
  319. package/src/core/ui-packs/ko.ts +13 -0
  320. package/src/core/ui-packs/nl.ts +13 -0
  321. package/src/core/ui-packs/no.ts +13 -0
  322. package/src/core/ui-packs/pl.ts +13 -0
  323. package/src/core/ui-packs/pt-br.ts +13 -0
  324. package/src/core/ui-packs/pt.ts +13 -0
  325. package/src/core/ui-packs/ro.ts +13 -0
  326. package/src/core/ui-packs/ru.ts +13 -0
  327. package/src/core/ui-packs/sk.ts +13 -0
  328. package/src/core/ui-packs/sr.ts +13 -0
  329. package/src/core/ui-packs/sv.ts +13 -0
  330. package/src/core/ui-packs/th.ts +13 -0
  331. package/src/core/ui-packs/tr.ts +13 -0
  332. package/src/core/ui-packs/uk.ts +13 -0
  333. package/src/core/ui-packs/vi.ts +13 -0
  334. package/src/core/ui-packs/zh-tw.ts +13 -0
  335. package/src/core/ui-packs/zh.ts +13 -0
  336. package/src/core/version-cut.ts +17 -2
  337. package/src/core/versions.ts +4 -1
  338. package/src/deploy/adapters/node.ts +5 -2
  339. package/src/deploy/adapters/registry.ts +2 -1
  340. package/src/deploy/adapters/types.ts +13 -1
  341. package/src/deploy/artifacts.ts +25 -5
  342. package/src/deploy/cloudflare-negotiation.ts +23 -4
  343. package/src/deploy/headers.ts +67 -51
  344. package/src/deploy/node-headers.ts +148 -27
  345. package/src/deploy/platforms/cloudflare.ts +1 -0
  346. package/src/deploy/platforms/netlify.ts +82 -5
  347. package/src/deploy/platforms/node.ts +7 -5
  348. package/src/deploy/platforms/static.ts +1 -0
  349. package/src/deploy/platforms/types.ts +7 -0
  350. package/src/deploy/platforms/vercel.ts +9 -3
  351. package/src/deploy/redirects.ts +14 -3
  352. package/src/deploy/vercel-negotiation.ts +35 -3
  353. package/src/eval/agents.ts +10 -2
  354. package/src/eval/run.ts +25 -0
  355. package/src/markdown/base-links.ts +55 -8
  356. package/src/markdown/index.ts +6 -3
  357. package/src/markdown/relative-links.ts +3 -23
  358. package/src/markdown/route-snapshot.ts +37 -0
  359. package/src/og/card.ts +20 -4
  360. package/src/og/derive.ts +145 -4
  361. package/src/openapi/graphql-build.ts +28 -2
  362. package/src/openapi/model.ts +77 -13
  363. package/src/openapi/render-mdx.ts +10 -1
  364. package/src/registry/eject.ts +119 -22
  365. package/src/search/documents.ts +22 -1
  366. package/src/search/sync/algolia.ts +36 -2
  367. package/src/sources/registry.ts +5 -0
  368. package/src/theme/entry.ts +11 -4
  369. package/src/translate/agents.ts +6 -1
  370. package/src/translate/ledger.ts +26 -3
  371. package/src/translate/meta.ts +11 -3
  372. package/src/translate/run.ts +11 -5
  373. package/src/translate/validate.ts +10 -1
  374. package/src/translate/work-list.ts +36 -3
  375. package/src/upgrade/upgrade.ts +36 -4
  376. package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
  377. package/dist/cli/chunk-6crbhc3x.js.map +0 -14
  378. package/dist/cli/chunk-6hsn950k.js.map +0 -10
  379. package/dist/cli/chunk-79jhk4py.js.map +0 -35
  380. package/dist/cli/chunk-82bbrxdn.js +0 -51
  381. package/dist/cli/chunk-82bbrxdn.js.map +0 -10
  382. package/dist/cli/chunk-abh8yjkn.js +0 -31
  383. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  384. package/dist/cli/chunk-dh8cwk36.js.map +0 -10
  385. package/dist/cli/chunk-epjnccmv.js.map +0 -10
  386. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  387. package/dist/cli/chunk-fs23ddbb.js.map +0 -35
  388. package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
  389. package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
  390. package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
  391. package/dist/cli/chunk-mb2919y2.js.map +0 -10
  392. package/dist/cli/chunk-q5163e60.js.map +0 -11
  393. package/dist/cli/chunk-qkqwkpte.js.map +0 -182
  394. package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
  395. package/dist/cli/chunk-s1p84fyh.js.map +0 -10
  396. package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
  397. package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
  398. /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-00gs3wqs.js.map} +0 -0
  399. /package/dist/cli/{chunk-6vm74dry.js.map → chunk-6dtt0zfn.js.map} +0 -0
  400. /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-bbnwccaz.js.map} +0 -0
  401. /package/dist/cli/{chunk-ce574jw2.js.map → chunk-esh98wmb.js.map} +0 -0
  402. /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-fsmrqk8a.js.map} +0 -0
  403. /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-g698a744.js.map} +0 -0
  404. /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-h2ez8dzb.js.map} +0 -0
  405. /package/dist/cli/{chunk-5shv93fd.js.map → chunk-h7k3nq3v.js.map} +0 -0
  406. /package/dist/cli/{chunk-ah61y8py.js.map → chunk-mqc662a6.js.map} +0 -0
  407. /package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-n1yg3tj3.js.map} +0 -0
  408. /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-sqw4ekg1.js.map} +0 -0
  409. /package/dist/cli/{chunk-27g6wdth.js.map → chunk-wdrt2k2v.js.map} +0 -0
  410. /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-zp79m0ts.js.map} +0 -0
@@ -1,4 +1,6 @@
1
+ import { withServerDefaults } from "../../openapi/model.ts";
1
2
  import {
3
+ declaredExample,
2
4
  exampleValue,
3
5
  objectProperties,
4
6
  resolveSchema,
@@ -30,9 +32,30 @@ import type { OperationSecurity, ResolvedScheme } from "./security.ts";
30
32
 
31
33
  interface MediaTypeLike {
32
34
  schema?: SchemaLike;
33
- example?: unknown;
35
+ example?: SpecValue;
36
+ examples?: SpecValue;
34
37
  }
35
38
 
39
+ /** A server entry: its URL template and the variables that fill it. */
40
+ export interface ServerLike {
41
+ url?: string;
42
+ variables?: SpecValue;
43
+ }
44
+
45
+ /**
46
+ * The servers an operation is sent to: its own `servers` when it declares
47
+ * any, else its path item's, else the document's — each level overrides the
48
+ * one above it, as the OpenAPI spec defines.
49
+ */
50
+ export const effectiveServers = (
51
+ operation: ServerLike[] | undefined,
52
+ pathItem: ServerLike[] | undefined,
53
+ document: ServerLike[] | undefined
54
+ ): ServerLike[] =>
55
+ [operation, pathItem, document].find(
56
+ (servers) => Array.isArray(servers) && servers.length > 0
57
+ ) ?? [];
58
+
36
59
  /** Primitive types the flat-body fields UI can edit directly. */
37
60
  const PRIMITIVE_TYPES = {
38
61
  boolean: true,
@@ -78,7 +101,9 @@ const modelParams = (
78
101
  required,
79
102
  type: scalarType(param.schema, schemas),
80
103
  value: required
81
- ? inputValue(param.example ?? exampleValue(param.schema, schemas))
104
+ ? inputValue(
105
+ declaredExample(param) ?? exampleValue(param.schema, schemas)
106
+ )
82
107
  : "",
83
108
  });
84
109
  }
@@ -127,7 +152,12 @@ const bodyFields = (
127
152
  description: propertySchema.description,
128
153
  enum: propertySchema.enum?.map(String),
129
154
  name,
130
- required: required.has(name),
155
+ // A `readOnly` property's `required` binds responses only: a request
156
+ // shouldn't send the server-generated field at all.
157
+ required:
158
+ required.has(name) &&
159
+ property.readOnly !== true &&
160
+ propertySchema.readOnly !== true,
131
161
  type,
132
162
  value: inputValue(defaults?.[name]),
133
163
  });
@@ -145,10 +175,8 @@ const modelBody = (
145
175
  return undefined;
146
176
  }
147
177
  const [contentType, mediaType] = media;
148
- // SAFETY: `example` comes from the parsed spec document (YAML/JSON), whose
149
- // values are exactly the JSON-shaped tree `SpecValue` models.
150
178
  const exampleData =
151
- (mediaType.example as SpecValue) ?? exampleValue(mediaType.schema, schemas);
179
+ declaredExample(mediaType) ?? exampleValue(mediaType.schema, schemas);
152
180
  return {
153
181
  contentType,
154
182
  example: toJson(exampleData) ?? "",
@@ -241,7 +269,8 @@ export const operationModel = (args: {
241
269
  /** Pre-merged/resolved (`mergeParameters` output). */
242
270
  parameters: ParameterLike[];
243
271
  requestBody?: { content?: Record<string, MediaTypeLike> };
244
- servers: { url?: string }[];
272
+ /** The operation's effective servers (`effectiveServers` output). */
273
+ servers: ServerLike[];
245
274
  schemas: Record<string, SchemaLike>;
246
275
  security: OperationSecurity;
247
276
  }): PlaygroundModel => ({
@@ -256,5 +285,8 @@ export const operationModel = (args: {
256
285
  method: args.method.toUpperCase(),
257
286
  params: modelParams(args.parameters, args.schemas),
258
287
  path: args.path,
259
- servers: args.servers.map((server) => server.url ?? ""),
288
+ // Variables resolve to their defaults: the samples and Send need a real URL.
289
+ servers: args.servers.map((server) =>
290
+ withServerDefaults(server.url ?? "", server.variables)
291
+ ),
260
292
  });
@@ -18,23 +18,47 @@ class BlumePanelTabs extends HTMLElement {
18
18
 
19
19
  const activate = (key: string): void => {
20
20
  for (const tab of tabs) {
21
- tab.setAttribute(
22
- "aria-selected",
23
- tab.dataset.panelTab === key ? "true" : "false"
24
- );
21
+ const selected = tab.dataset.panelTab === key;
22
+ tab.setAttribute("aria-selected", selected ? "true" : "false");
23
+ // Roving tabindex: Tab reaches the selected tab only, the arrow keys
24
+ // move between the rest.
25
+ tab.tabIndex = selected ? 0 : -1;
25
26
  }
26
27
  for (const panel of panels) {
27
28
  panel.classList.toggle("hidden", panel.dataset.panel !== key);
28
29
  }
29
30
  };
30
31
 
31
- for (const tab of tabs) {
32
+ for (const [index, tab] of tabs.entries()) {
32
33
  tab.addEventListener("click", () => {
33
34
  const key = tab.dataset.panelTab;
34
35
  if (key) {
35
36
  activate(key);
36
37
  }
37
38
  });
39
+ // The WAI-ARIA tabs keyboard pattern, matching `<Tabs>`: arrows move and
40
+ // wrap, Home and End jump to the ends, and focus follows selection.
41
+ tab.addEventListener("keydown", (event) => {
42
+ const last = tabs.length - 1;
43
+ let next: number | undefined;
44
+ if (event.key === "ArrowRight") {
45
+ next = index === last ? 0 : index + 1;
46
+ } else if (event.key === "ArrowLeft") {
47
+ next = index === 0 ? last : index - 1;
48
+ } else if (event.key === "Home") {
49
+ next = 0;
50
+ } else if (event.key === "End") {
51
+ next = last;
52
+ }
53
+ const target = next === undefined ? undefined : tabs[next];
54
+ const key = target?.dataset.panelTab;
55
+ if (!(target && key)) {
56
+ return;
57
+ }
58
+ event.preventDefault();
59
+ activate(key);
60
+ target.focus();
61
+ });
38
62
  }
39
63
 
40
64
  if (copy) {
@@ -155,6 +155,36 @@ const prettyBody = (text: string): string => {
155
155
  }
156
156
  };
157
157
 
158
+ /*
159
+ * Remembered credentials are a convenience, never a requirement. With storage
160
+ * blocked (Safari's "Block All Cookies", a sandboxed iframe) even reading the
161
+ * `localStorage` global throws a SecurityError, which would take the whole
162
+ * panel down during init; each access is guarded so the form simply forgets.
163
+ */
164
+ const readStored = (key: string): string | null => {
165
+ try {
166
+ return localStorage.getItem(key);
167
+ } catch {
168
+ return null;
169
+ }
170
+ };
171
+
172
+ const writeStored = (key: string, value: string): void => {
173
+ try {
174
+ localStorage.setItem(key, value);
175
+ } catch {
176
+ // Blocked or full storage: the credentials stay in the form only.
177
+ }
178
+ };
179
+
180
+ const removeStored = (key: string): void => {
181
+ try {
182
+ localStorage.removeItem(key);
183
+ } catch {
184
+ // Blocked storage never held the entry in the first place.
185
+ }
186
+ };
187
+
158
188
  /** A one-line text element for the response/error regions. */
159
189
  const line = (className: string, text: string): HTMLElement => {
160
190
  const el = document.createElement("div");
@@ -316,7 +346,7 @@ export const initPlayground = (root: HTMLElement): void => {
316
346
 
317
347
  /** Restore remembered credentials into the inputs and re-check the box. */
318
348
  const restoreAuth = (): void => {
319
- const stored = localStorage.getItem(storageKey);
349
+ const stored = readStored(storageKey);
320
350
  if (!stored) {
321
351
  return;
322
352
  }
@@ -355,7 +385,7 @@ export const initPlayground = (root: HTMLElement): void => {
355
385
  }
356
386
  } catch {
357
387
  // A corrupt entry (older format, manual edit) must not break init.
358
- localStorage.removeItem(storageKey);
388
+ removeStored(storageKey);
359
389
  }
360
390
  };
361
391
 
@@ -374,12 +404,12 @@ export const initPlayground = (root: HTMLElement): void => {
374
404
  )
375
405
  );
376
406
  const table = document.createElement("table");
377
- table.className = "w-full text-left text-xs";
407
+ table.className = "w-full text-start text-xs";
378
408
  for (const [name, value] of res.headers) {
379
409
  const row = document.createElement("tr");
380
410
  const header = document.createElement("th");
381
411
  header.setAttribute("scope", "row");
382
- header.className = "pr-3 align-top font-medium text-muted-foreground";
412
+ header.className = "pe-3 align-top font-medium text-muted-foreground";
383
413
  header.textContent = name;
384
414
  const cell = document.createElement("td");
385
415
  cell.className = "break-all font-mono text-foreground";
@@ -548,9 +578,9 @@ export const initPlayground = (root: HTMLElement): void => {
548
578
  }
549
579
  if (remember && target === remember) {
550
580
  if (remember.checked) {
551
- localStorage.setItem(storageKey, JSON.stringify(collectAuth()));
581
+ writeStored(storageKey, JSON.stringify(collectAuth()));
552
582
  } else {
553
- localStorage.removeItem(storageKey);
583
+ removeStored(storageKey);
554
584
  }
555
585
  } else if (
556
586
  remember?.checked &&
@@ -558,7 +588,7 @@ export const initPlayground = (root: HTMLElement): void => {
558
588
  target.dataset.authUsername !== undefined ||
559
589
  target.dataset.authPassword !== undefined)
560
590
  ) {
561
- localStorage.setItem(storageKey, JSON.stringify(collectAuth()));
591
+ writeStored(storageKey, JSON.stringify(collectAuth()));
562
592
  }
563
593
  syncSamples();
564
594
  };
@@ -1,4 +1,4 @@
1
- import { resolveSchema } from "./helpers.ts";
1
+ import { isNullable, resolveSchema } from "./helpers.ts";
2
2
  import type { SchemaLike, SpecValue } from "./helpers.ts";
3
3
  import type { ValidationSchema } from "./request.ts";
4
4
 
@@ -50,6 +50,14 @@ export const inputValue = (value: SpecValue): string => {
50
50
  /** Recursion limit for pruned validation schemas — deep enough for real specs. */
51
51
  const MAX_SCHEMA_DEPTH = 6;
52
52
 
53
+ /** Whether a property is `readOnly`, on the property or the schema it refs. */
54
+ const isReadOnly = (
55
+ property: SchemaLike | undefined,
56
+ schemas: Record<string, SchemaLike>
57
+ ): boolean =>
58
+ property?.readOnly === true ||
59
+ resolveSchema(schemas, property).readOnly === true;
60
+
53
61
  /**
54
62
  * Prune a spec schema into the tiny subset `validate-json.ts` understands:
55
63
  * `$ref`s resolved inline, cycles cut (the visited set is copied per branch so
@@ -74,15 +82,26 @@ const pruneSchema = (
74
82
  }
75
83
  const resolved = resolveSchema(schemas, schema);
76
84
  const out: ValidationSchema = {};
77
- const [type] = declaredTypes(resolved.type);
78
- if (type) {
79
- out.type = type;
85
+ // Only a single declared type is checked: a 3.1 type array like
86
+ // `["string", "integer"]` accepts either, which one `type` can't say.
87
+ const types = declaredTypes(resolved.type);
88
+ if (types.length === 1) {
89
+ [out.type] = types;
90
+ }
91
+ if (isNullable(resolved)) {
92
+ out.nullable = true;
80
93
  }
81
94
  if (resolved.enum) {
82
95
  out.enum = resolved.enum;
83
96
  }
84
- if (resolved.required && resolved.required.length > 0) {
85
- out.required = resolved.required;
97
+ // A `readOnly` property's `required` binds responses only: the server
98
+ // generates it, so a request body — which the prefilled example already
99
+ // leaves it out of — must not be told it is missing.
100
+ const required = (resolved.required ?? []).filter(
101
+ (name) => !isReadOnly(resolved.properties?.[name], schemas)
102
+ );
103
+ if (required.length > 0) {
104
+ out.required = required;
86
105
  }
87
106
  if (resolved.properties) {
88
107
  const properties: Record<string, ValidationSchema> = {};
@@ -37,6 +37,8 @@ export interface PlaygroundBodyField {
37
37
  /** A pruned, cycle-free JSON-schema subset `validate-json.ts` understands. */
38
38
  export interface ValidationSchema {
39
39
  type?: string;
40
+ /** `null` is also accepted: 3.0 `nullable`, or `"null"` in a 3.1 type array. */
41
+ nullable?: boolean;
40
42
  properties?: Record<string, ValidationSchema>;
41
43
  required?: string[];
42
44
  items?: ValidationSchema;
@@ -0,0 +1,203 @@
1
+ import {
2
+ objectProperties,
3
+ refName,
4
+ resolveSchema,
5
+ typeLabel,
6
+ } from "./helpers.ts";
7
+ import type { SchemaLike, SpecValue } from "./helpers.ts";
8
+
9
+ /**
10
+ * The expansion plan behind the schema tables (`SchemaTable.astro` and
11
+ * `SchemaProperty.astro`). Every nested object is server-rendered in full
12
+ * inside a collapsed `<details>`, so the plan is the page's weight: expanding
13
+ * each named schema wherever it appears — stopping only at a model nested in
14
+ * itself — grows exponentially with cross-references (twelve schemas that
15
+ * each reference four others render tens of thousands of nested tables for
16
+ * one request body).
17
+ *
18
+ * So a named schema expands wherever it appears in the first
19
+ * {@link REPEAT_DEPTH} named levels, and deeper than that only where it
20
+ * appears first in the tree, breadth-first; a later mention shows its name
21
+ * alone. Beyond the first levels each named schema expands at most once, so
22
+ * the tree stays proportional to the spec instead of to the number of paths
23
+ * through it, while the shallow nesting typical specs use — `billing` and
24
+ * `shipping` both an `Address` — renders exactly as before.
25
+ */
26
+
27
+ /**
28
+ * Named-schema nesting depth (the root being 0) up to which a named schema
29
+ * expands at every mention, even one already expanded elsewhere in the tree.
30
+ */
31
+ export const REPEAT_DEPTH = 2;
32
+
33
+ /** A place in the tree a node is planned into; filled in breadth-first. */
34
+ export interface SchemaSlot {
35
+ node: SchemaNode;
36
+ }
37
+
38
+ /** One property row of an object table. */
39
+ export interface SchemaRow {
40
+ name: string;
41
+ required: boolean;
42
+ schema: SchemaLike;
43
+ /** The row's disclosure contents; absent when the row has nothing to show. */
44
+ children?: SchemaSlot;
45
+ }
46
+
47
+ /** What one schema table renders. */
48
+ export type SchemaNode =
49
+ /** A named schema nested inside itself. */
50
+ | { kind: "circular"; name: string }
51
+ /** A named schema expanded elsewhere in this tree: its name only. */
52
+ | { kind: "named"; name: string }
53
+ | { kind: "array"; itemsLabel: string; items?: SchemaSlot }
54
+ | {
55
+ kind: "branches";
56
+ label: "One of" | "Any of";
57
+ branches: { label: string; slot: SchemaSlot }[];
58
+ }
59
+ | { kind: "properties"; rows: SchemaRow[] }
60
+ | { kind: "type"; label: string };
61
+
62
+ const isString = (value: SpecValue): value is string =>
63
+ typeof value === "string";
64
+
65
+ const typesOf = (schema: SchemaLike): (string | undefined)[] =>
66
+ Array.isArray(schema.type) ? schema.type : [schema.type];
67
+
68
+ /** Whether a schema has nested fields a row can disclose. */
69
+ const hasNestedFields = (candidate: SchemaLike): boolean =>
70
+ Boolean(
71
+ candidate.properties ||
72
+ candidate.allOf ||
73
+ candidate.oneOf ||
74
+ candidate.anyOf ||
75
+ isString(candidate.$ref)
76
+ );
77
+
78
+ /** A named schema waiting for its breadth-first turn. */
79
+ interface PendingRef {
80
+ ancestors: string[];
81
+ name: string;
82
+ schema: SchemaLike;
83
+ slot: SchemaSlot;
84
+ }
85
+
86
+ /** Plan the table tree for one schema (a request body, a response, a payload). */
87
+ export const schemaTree = (
88
+ root: SchemaLike,
89
+ schemas: Record<string, SchemaLike>
90
+ ): SchemaNode => {
91
+ const queue: PendingRef[] = [];
92
+ const expanded = new Set<string>();
93
+
94
+ /**
95
+ * A table for `schema`, where `ancestors` are the named schemas already
96
+ * open above it. A `$ref` waits in the queue, so every mention at one depth
97
+ * is planned before any deeper one; `resolved` plans a dequeued schema's
98
+ * own body instead.
99
+ */
100
+ const table = (
101
+ schema: SchemaLike,
102
+ ancestors: string[],
103
+ resolved = false
104
+ ): SchemaSlot => {
105
+ if (!resolved && isString(schema.$ref)) {
106
+ const name = refName(schema.$ref);
107
+ if (ancestors.includes(name)) {
108
+ return { node: { kind: "circular", name } };
109
+ }
110
+ const slot: SchemaSlot = { node: { kind: "named", name } };
111
+ queue.push({ ancestors, name, schema, slot });
112
+ return slot;
113
+ }
114
+ if (typesOf(schema).includes("array")) {
115
+ // Items stay unresolved, so a self-referential item `$ref` is still
116
+ // recognized as the circle it is.
117
+ const { items } = schema;
118
+ return {
119
+ node: items
120
+ ? {
121
+ items: table(items, ancestors),
122
+ itemsLabel: typeLabel(items),
123
+ kind: "array",
124
+ }
125
+ : { itemsLabel: typeLabel({}), kind: "array" },
126
+ };
127
+ }
128
+ const branches = schema.oneOf ?? schema.anyOf;
129
+ if (branches) {
130
+ return {
131
+ node: {
132
+ branches: branches.map((branch, index) => ({
133
+ label: typeLabel(branch) || `Option ${index + 1}`,
134
+ slot: table(branch, ancestors),
135
+ })),
136
+ kind: "branches",
137
+ label: schema.oneOf ? "One of" : "Any of",
138
+ },
139
+ };
140
+ }
141
+ // A row discloses nested structure — never a model inside itself.
142
+ const rowChildren = (property: SchemaLike): SchemaSlot | undefined => {
143
+ if (
144
+ isString(property.$ref) &&
145
+ ancestors.includes(refName(property.$ref))
146
+ ) {
147
+ return undefined;
148
+ }
149
+ const target = resolveSchema(schemas, property);
150
+ const items = typesOf(target).includes("array")
151
+ ? resolveSchema(schemas, target.items)
152
+ : null;
153
+ return hasNestedFields(target) || (items && hasNestedFields(items))
154
+ ? table(property, ancestors)
155
+ : undefined;
156
+ };
157
+ const { properties, required } = objectProperties(schema, schemas);
158
+ if (properties.length > 0) {
159
+ return {
160
+ node: {
161
+ kind: "properties",
162
+ rows: properties.map(([name, property]) => ({
163
+ children: rowChildren(property),
164
+ name,
165
+ required: required.has(name),
166
+ schema: property,
167
+ })),
168
+ },
169
+ };
170
+ }
171
+ return { node: { kind: "type", label: typeLabel(schema) } };
172
+ };
173
+
174
+ const tree = table(root, []);
175
+ // The queue grows while it drains, in depth order: every mention at one
176
+ // named depth is queued before the first one at the next.
177
+ for (const pending of queue) {
178
+ const depth = pending.ancestors.length;
179
+ if (depth > REPEAT_DEPTH && expanded.has(pending.name)) {
180
+ continue;
181
+ }
182
+ expanded.add(pending.name);
183
+ pending.slot.node = table(
184
+ resolveSchema(schemas, pending.schema),
185
+ [...pending.ancestors, pending.name],
186
+ true
187
+ ).node;
188
+ }
189
+ return tree.node;
190
+ };
191
+
192
+ /**
193
+ * Whether a row gets a disclosure: it has nested structure, and that structure
194
+ * isn't just the name of a schema expanded elsewhere (directly, or as the
195
+ * items of an array).
196
+ */
197
+ export const rowExpands = (row: SchemaRow): boolean => {
198
+ const node = row.children?.node;
199
+ if (!node || node.kind === "named") {
200
+ return false;
201
+ }
202
+ return !(node.kind === "array" && node.items?.node.kind === "named");
203
+ };
@@ -22,26 +22,42 @@ const headerLines = (
22
22
  ): string[] =>
23
23
  Object.entries(headers).map(([key, value]) => format(key, value));
24
24
 
25
+ /**
26
+ * A single-quoted POSIX shell word. Nothing expands inside single quotes — no
27
+ * `$`, backtick, `!`, or `\` — so a value reaches curl byte for byte; a
28
+ * literal `'` closes the quote, adds an escaped one, and reopens it.
29
+ */
30
+ const shellQuote = (value: string): string =>
31
+ `'${value.replaceAll("'", String.raw`'\''`)}'`;
32
+
33
+ /**
34
+ * A double-quoted string literal for the JavaScript and Python samples. JSON's
35
+ * string escapes are a subset of both languages' — a quote in a header value
36
+ * (`If-Match: "33a64df5"`) or a backslash becomes an escape rather than
37
+ * ending the literal early.
38
+ */
39
+ const stringLiteral = (value: string): string => JSON.stringify(value);
40
+
25
41
  const curlSnippet = (sample: RequestSample): string => {
26
42
  const lines = [
27
- `curl -X ${sample.method} "${sample.url}"`,
28
- ...headerLines(sample.headers, (key, value) => ` -H "${key}: ${value}"`),
43
+ `curl -X ${sample.method} ${shellQuote(sample.url)}`,
44
+ ...headerLines(
45
+ sample.headers,
46
+ (key, value) => ` -H ${shellQuote(`${key}: ${value}`)}`
47
+ ),
29
48
  ];
30
49
  if (sample.body) {
31
- // Close-quote/escaped-quote/reopen: the POSIX way to put a literal ' in a
32
- // single-quoted string, so an example like "it's" doesn't break the shell.
33
- const escapedBody = sample.body.replaceAll("'", String.raw`'\''`);
34
- lines.push(` -d '${escapedBody}'`);
50
+ lines.push(` -d ${shellQuote(sample.body)}`);
35
51
  }
36
52
  return lines.join(" \\\n");
37
53
  };
38
54
 
39
55
  const fetchSnippet = (sample: RequestSample): string => {
40
- const options = [` method: "${sample.method}"`];
56
+ const options = [` method: ${stringLiteral(sample.method)}`];
41
57
  if (Object.keys(sample.headers).length > 0) {
42
58
  const headers = headerLines(
43
59
  sample.headers,
44
- (key, value) => ` "${key}": "${value}"`
60
+ (key, value) => ` ${stringLiteral(key)}: ${stringLiteral(value)}`
45
61
  ).join(",\n");
46
62
  options.push(` headers: {\n${headers}\n }`);
47
63
  }
@@ -53,28 +69,27 @@ const fetchSnippet = (sample: RequestSample): string => {
53
69
  // and an id past 2^53 loses digits through a JS number. The string
54
70
  // literal also stays syntactically valid while the editor holds mid-edit
55
71
  // text that isn't JSON yet.
56
- options.push(` body: ${JSON.stringify(sample.body)}`);
72
+ options.push(` body: ${stringLiteral(sample.body)}`);
57
73
  }
58
- return `const response = await fetch("${sample.url}", {\n${options.join(
74
+ return `const response = await fetch(${stringLiteral(sample.url)}, {\n${options.join(
59
75
  ",\n"
60
76
  )}\n});`;
61
77
  };
62
78
 
63
79
  const pythonSnippet = (sample: RequestSample): string => {
64
- const args = [` "${sample.url}"`];
80
+ const args = [` ${stringLiteral(sample.url)}`];
65
81
  if (Object.keys(sample.headers).length > 0) {
66
82
  const headers = headerLines(
67
83
  sample.headers,
68
- (key, value) => ` "${key}": "${value}"`
84
+ (key, value) => ` ${stringLiteral(key)}: ${stringLiteral(value)}`
69
85
  ).join(",\n");
70
86
  args.push(` headers={\n${headers}\n }`);
71
87
  }
72
88
  if (sample.body) {
73
89
  // Same rule as the fetch snippet: the raw text travels as a string via
74
- // `data=` (JSON string escapes are a subset of Python's, so the literal is
75
- // valid) rather than a `json=` dict — a Python literal re-serializes
90
+ // `data=` rather than a `json=` dict — a Python literal re-serializes
76
91
  // `1e400` as `Infinity` and would otherwise diverge from the live send.
77
- args.push(` data=${JSON.stringify(sample.body)}`);
92
+ args.push(` data=${stringLiteral(sample.body)}`);
78
93
  }
79
94
  return `import requests\n\nresponse = requests.${sample.method.toLowerCase()}(\n${args.join(
80
95
  ",\n"
@@ -74,7 +74,7 @@ const walk = (
74
74
  path: string,
75
75
  errors: string[]
76
76
  ): void => {
77
- if (!schema) {
77
+ if (!schema || (value === null && schema.nullable === true)) {
78
78
  return;
79
79
  }
80
80
  if (schema.type !== undefined && !matchesType(value, schema.type)) {
@@ -88,6 +88,21 @@ export const withBasePath = (basePath: string, route: string): string => {
88
88
  return route === "/" ? basePath : `${basePath}${route}`;
89
89
  };
90
90
 
91
+ /**
92
+ * Mount a route Blume generates from content under `basePath`,
93
+ * unconditionally. {@link withBasePath} leaves a route that already starts
94
+ * with the base alone, which is right for a link an author based by hand but
95
+ * wrong for a content route: with `basePath: "/docs"`, `docs/guide.md` routes
96
+ * to `/docs/guide` before the base and publishes at `/docs/docs/guide`, not on
97
+ * top of the root `guide.md`.
98
+ */
99
+ export const mountBasePath = (basePath: string, route: string): string => {
100
+ if (!basePath) {
101
+ return route;
102
+ }
103
+ return route === "/" ? basePath : `${basePath}${route}`;
104
+ };
105
+
91
106
  /**
92
107
  * {@link withBasePath} for the composed `deployment.base` + `basePath` stack
93
108
  * (`/base` + `/docs` serves pages at `/base/docs/x`). The hand-written-base
@@ -596,14 +596,14 @@ export interface LlmsTxtConfig {
596
596
  /**
597
597
  * Markdown placed after the title and summary in `llms.txt`, before the
598
598
  * page sections — the llms.txt spec's "details" block. Use it to tell
599
- * agents when to reach for the product and how to call it (a "When to use"
600
- * heading, the install command, the package name). Blank values are
601
- * dropped.
599
+ * agents when to reach for the product and how to call it (the install
600
+ * command, the package name). The spec allows any Markdown there but
601
+ * headings. Blank values are dropped.
602
602
  *
603
603
  * ```ts
604
604
  * agents: {
605
605
  * llmsTxt: {
606
- * details: "## When to use Acme\n\nUse Acme when…",
606
+ * details: "Use Acme when…",
607
607
  * },
608
608
  * }
609
609
  * ```