vocs 2.0.17 → 2.1.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 (306) hide show
  1. package/dist/config.d.ts +1 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +1 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/globals.d.ts +16 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/config.d.ts +28 -0
  11. package/dist/internal/config.d.ts.map +1 -1
  12. package/dist/internal/config.js +10 -2
  13. package/dist/internal/config.js.map +1 -1
  14. package/dist/internal/llms.d.ts +21 -2
  15. package/dist/internal/llms.d.ts.map +1 -1
  16. package/dist/internal/llms.js +41 -1
  17. package/dist/internal/llms.js.map +1 -1
  18. package/dist/internal/markdown-negotiation.d.ts +36 -0
  19. package/dist/internal/markdown-negotiation.d.ts.map +1 -0
  20. package/dist/internal/markdown-negotiation.js +81 -0
  21. package/dist/internal/markdown-negotiation.js.map +1 -0
  22. package/dist/internal/markdown.d.ts.map +1 -1
  23. package/dist/internal/markdown.js +5 -0
  24. package/dist/internal/markdown.js.map +1 -1
  25. package/dist/internal/openapi/anchors.d.ts +25 -0
  26. package/dist/internal/openapi/anchors.d.ts.map +1 -0
  27. package/dist/internal/openapi/anchors.js +37 -0
  28. package/dist/internal/openapi/anchors.js.map +1 -0
  29. package/dist/internal/openapi/app.d.ts +89 -0
  30. package/dist/internal/openapi/app.d.ts.map +1 -0
  31. package/dist/internal/openapi/app.js +62 -0
  32. package/dist/internal/openapi/app.js.map +1 -0
  33. package/dist/internal/openapi/index.d.ts +8 -0
  34. package/dist/internal/openapi/index.d.ts.map +1 -0
  35. package/dist/internal/openapi/index.js +5 -0
  36. package/dist/internal/openapi/index.js.map +1 -0
  37. package/dist/internal/openapi/markdown.d.ts +42 -0
  38. package/dist/internal/openapi/markdown.d.ts.map +1 -0
  39. package/dist/internal/openapi/markdown.js +235 -0
  40. package/dist/internal/openapi/markdown.js.map +1 -0
  41. package/dist/internal/openapi/openapi.d.ts +187 -0
  42. package/dist/internal/openapi/openapi.d.ts.map +1 -0
  43. package/dist/internal/openapi/openapi.js +44 -0
  44. package/dist/internal/openapi/openapi.js.map +1 -0
  45. package/dist/internal/openapi/openrpc.d.ts +90 -0
  46. package/dist/internal/openapi/openrpc.d.ts.map +1 -0
  47. package/dist/internal/openapi/openrpc.js +213 -0
  48. package/dist/internal/openapi/openrpc.js.map +1 -0
  49. package/dist/internal/openapi/parser.d.ts +181 -0
  50. package/dist/internal/openapi/parser.d.ts.map +1 -0
  51. package/dist/internal/openapi/parser.js +329 -0
  52. package/dist/internal/openapi/parser.js.map +1 -0
  53. package/dist/internal/openapi/registry.d.ts +36 -0
  54. package/dist/internal/openapi/registry.d.ts.map +1 -0
  55. package/dist/internal/openapi/registry.js +79 -0
  56. package/dist/internal/openapi/registry.js.map +1 -0
  57. package/dist/internal/openapi/sample.d.ts +115 -0
  58. package/dist/internal/openapi/sample.d.ts.map +1 -0
  59. package/dist/internal/openapi/sample.js +434 -0
  60. package/dist/internal/openapi/sample.js.map +1 -0
  61. package/dist/internal/openapi/search.d.ts +19 -0
  62. package/dist/internal/openapi/search.d.ts.map +1 -0
  63. package/dist/internal/openapi/search.js +98 -0
  64. package/dist/internal/openapi/search.js.map +1 -0
  65. package/dist/internal/openapi/sidebar.d.ts +30 -0
  66. package/dist/internal/openapi/sidebar.d.ts.map +1 -0
  67. package/dist/internal/openapi/sidebar.js +67 -0
  68. package/dist/internal/openapi/sidebar.js.map +1 -0
  69. package/dist/internal/openapi/union.d.ts +36 -0
  70. package/dist/internal/openapi/union.d.ts.map +1 -0
  71. package/dist/internal/openapi/union.js +69 -0
  72. package/dist/internal/openapi/union.js.map +1 -0
  73. package/dist/internal/search.d.ts.map +1 -1
  74. package/dist/internal/search.js +20 -0
  75. package/dist/internal/search.js.map +1 -1
  76. package/dist/internal/vite-plugins.d.ts +12 -0
  77. package/dist/internal/vite-plugins.d.ts.map +1 -1
  78. package/dist/internal/vite-plugins.js +102 -11
  79. package/dist/internal/vite-plugins.js.map +1 -1
  80. package/dist/react/Badge.d.ts +2 -3
  81. package/dist/react/Badge.d.ts.map +1 -1
  82. package/dist/react/Layout.client.d.ts.map +1 -1
  83. package/dist/react/Layout.client.js +2 -2
  84. package/dist/react/Layout.client.js.map +1 -1
  85. package/dist/react/OpenApi.d.ts +6 -0
  86. package/dist/react/OpenApi.d.ts.map +1 -0
  87. package/dist/react/OpenApi.js +6 -0
  88. package/dist/react/OpenApi.js.map +1 -0
  89. package/dist/react/internal/CodeToHtml.client.d.ts +53 -2
  90. package/dist/react/internal/CodeToHtml.client.d.ts.map +1 -1
  91. package/dist/react/internal/CodeToHtml.client.js +154 -21
  92. package/dist/react/internal/CodeToHtml.client.js.map +1 -1
  93. package/dist/react/internal/Sidebar.d.ts.map +1 -1
  94. package/dist/react/internal/Sidebar.js +99 -2
  95. package/dist/react/internal/Sidebar.js.map +1 -1
  96. package/dist/react/internal/openapi/CodeSample.client.d.ts +21 -0
  97. package/dist/react/internal/openapi/CodeSample.client.d.ts.map +1 -0
  98. package/dist/react/internal/openapi/CodeSample.client.js +134 -0
  99. package/dist/react/internal/openapi/CodeSample.client.js.map +1 -0
  100. package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts +17 -0
  101. package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts.map +1 -0
  102. package/dist/react/internal/openapi/CollapsibleChildren.client.js +18 -0
  103. package/dist/react/internal/openapi/CollapsibleChildren.client.js.map +1 -0
  104. package/dist/react/internal/openapi/Disclosure.client.d.ts +28 -0
  105. package/dist/react/internal/openapi/Disclosure.client.d.ts.map +1 -0
  106. package/dist/react/internal/openapi/Disclosure.client.js +38 -0
  107. package/dist/react/internal/openapi/Disclosure.client.js.map +1 -0
  108. package/dist/react/internal/openapi/Endpoints.d.ts +26 -0
  109. package/dist/react/internal/openapi/Endpoints.d.ts.map +1 -0
  110. package/dist/react/internal/openapi/Endpoints.js +33 -0
  111. package/dist/react/internal/openapi/Endpoints.js.map +1 -0
  112. package/dist/react/internal/openapi/EndpointsView.d.ts +24 -0
  113. package/dist/react/internal/openapi/EndpointsView.d.ts.map +1 -0
  114. package/dist/react/internal/openapi/EndpointsView.js +26 -0
  115. package/dist/react/internal/openapi/EndpointsView.js.map +1 -0
  116. package/dist/react/internal/openapi/EnumValues.client.d.ts +14 -0
  117. package/dist/react/internal/openapi/EnumValues.client.d.ts.map +1 -0
  118. package/dist/react/internal/openapi/EnumValues.client.js +20 -0
  119. package/dist/react/internal/openapi/EnumValues.client.js.map +1 -0
  120. package/dist/react/internal/openapi/HeadingAnchor.d.ts +15 -0
  121. package/dist/react/internal/openapi/HeadingAnchor.d.ts.map +1 -0
  122. package/dist/react/internal/openapi/HeadingAnchor.js +12 -0
  123. package/dist/react/internal/openapi/HeadingAnchor.js.map +1 -0
  124. package/dist/react/internal/openapi/OpenApiPage.d.ts +79 -0
  125. package/dist/react/internal/openapi/OpenApiPage.d.ts.map +1 -0
  126. package/dist/react/internal/openapi/OpenApiPage.js +72 -0
  127. package/dist/react/internal/openapi/OpenApiPage.js.map +1 -0
  128. package/dist/react/internal/openapi/Operation.d.ts +25 -0
  129. package/dist/react/internal/openapi/Operation.d.ts.map +1 -0
  130. package/dist/react/internal/openapi/Operation.js +101 -0
  131. package/dist/react/internal/openapi/Operation.js.map +1 -0
  132. package/dist/react/internal/openapi/Playground.client.d.ts +33 -0
  133. package/dist/react/internal/openapi/Playground.client.d.ts.map +1 -0
  134. package/dist/react/internal/openapi/Playground.client.js +170 -0
  135. package/dist/react/internal/openapi/Playground.client.js.map +1 -0
  136. package/dist/react/internal/openapi/PropertyExample.client.d.ts +17 -0
  137. package/dist/react/internal/openapi/PropertyExample.client.d.ts.map +1 -0
  138. package/dist/react/internal/openapi/PropertyExample.client.js +21 -0
  139. package/dist/react/internal/openapi/PropertyExample.client.js.map +1 -0
  140. package/dist/react/internal/openapi/Reference.d.ts +55 -0
  141. package/dist/react/internal/openapi/Reference.d.ts.map +1 -0
  142. package/dist/react/internal/openapi/Reference.js +42 -0
  143. package/dist/react/internal/openapi/Reference.js.map +1 -0
  144. package/dist/react/internal/openapi/Schema.d.ts +110 -0
  145. package/dist/react/internal/openapi/Schema.d.ts.map +1 -0
  146. package/dist/react/internal/openapi/Schema.js +239 -0
  147. package/dist/react/internal/openapi/Schema.js.map +1 -0
  148. package/dist/react/internal/openapi/SchemaUnion.client.d.ts +25 -0
  149. package/dist/react/internal/openapi/SchemaUnion.client.d.ts.map +1 -0
  150. package/dist/react/internal/openapi/SchemaUnion.client.js +48 -0
  151. package/dist/react/internal/openapi/SchemaUnion.client.js.map +1 -0
  152. package/dist/react/internal/openapi/anchor-navigation.client.d.ts +47 -0
  153. package/dist/react/internal/openapi/anchor-navigation.client.d.ts.map +1 -0
  154. package/dist/react/internal/openapi/anchor-navigation.client.js +120 -0
  155. package/dist/react/internal/openapi/anchor-navigation.client.js.map +1 -0
  156. package/dist/react/internal/openapi/auth.d.ts +28 -0
  157. package/dist/react/internal/openapi/auth.d.ts.map +1 -0
  158. package/dist/react/internal/openapi/auth.js +75 -0
  159. package/dist/react/internal/openapi/auth.js.map +1 -0
  160. package/dist/react/useLayout.d.ts +2 -0
  161. package/dist/react/useLayout.d.ts.map +1 -1
  162. package/dist/react/useLayout.js +2 -0
  163. package/dist/react/useLayout.js.map +1 -1
  164. package/dist/server/handlers.d.ts +1 -1
  165. package/dist/server/handlers.d.ts.map +1 -1
  166. package/dist/server/handlers.js +26 -5
  167. package/dist/server/handlers.js.map +1 -1
  168. package/dist/server/og-assets.d.ts +5 -0
  169. package/dist/server/og-assets.d.ts.map +1 -0
  170. package/dist/server/og-assets.js +11 -0
  171. package/dist/server/og-assets.js.map +1 -0
  172. package/dist/server/openapi/assets.d.ts +33 -0
  173. package/dist/server/openapi/assets.d.ts.map +1 -0
  174. package/dist/server/openapi/assets.generated.d.ts +9 -0
  175. package/dist/server/openapi/assets.generated.d.ts.map +1 -0
  176. package/dist/server/openapi/assets.generated.js +1091 -0
  177. package/dist/server/openapi/assets.generated.js.map +1 -0
  178. package/dist/server/openapi/assets.js +32 -0
  179. package/dist/server/openapi/assets.js.map +1 -0
  180. package/dist/server/openapi/handler.d.ts +103 -0
  181. package/dist/server/openapi/handler.d.ts.map +1 -0
  182. package/dist/server/openapi/handler.js +198 -0
  183. package/dist/server/openapi/handler.js.map +1 -0
  184. package/dist/server/openapi/handler.test.d.ts +2 -0
  185. package/dist/server/openapi/handler.test.d.ts.map +1 -0
  186. package/dist/server/openapi/handler.test.js +203 -0
  187. package/dist/server/openapi/handler.test.js.map +1 -0
  188. package/dist/server/openapi/html.d.ts +16 -0
  189. package/dist/server/openapi/html.d.ts.map +1 -0
  190. package/dist/server/openapi/html.js +75 -0
  191. package/dist/server/openapi/html.js.map +1 -0
  192. package/dist/server/openapi/pages.d.ts +33 -0
  193. package/dist/server/openapi/pages.d.ts.map +1 -0
  194. package/dist/server/openapi/pages.js +130 -0
  195. package/dist/server/openapi/pages.js.map +1 -0
  196. package/dist/server/openapi/pages.test.d.ts +2 -0
  197. package/dist/server/openapi/pages.test.d.ts.map +1 -0
  198. package/dist/server/openapi/pages.test.js +94 -0
  199. package/dist/server/openapi/pages.test.js.map +1 -0
  200. package/dist/server/openapi/state.d.ts +42 -0
  201. package/dist/server/openapi/state.d.ts.map +1 -0
  202. package/dist/server/openapi/state.js +101 -0
  203. package/dist/server/openapi/state.js.map +1 -0
  204. package/dist/styles/index.css +16 -0
  205. package/dist/styles/markdown.css +9 -7
  206. package/dist/styles/openapi-playground.css +80 -0
  207. package/dist/styles/openapi.css +660 -0
  208. package/dist/vite.d.ts.map +1 -1
  209. package/dist/vite.js +1 -0
  210. package/dist/vite.js.map +1 -1
  211. package/dist/waku/internal/middleware/md-router.d.ts +0 -4
  212. package/dist/waku/internal/middleware/md-router.d.ts.map +1 -1
  213. package/dist/waku/internal/middleware/md-router.js +3 -48
  214. package/dist/waku/internal/middleware/md-router.js.map +1 -1
  215. package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js +1 -1
  216. package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js.map +1 -1
  217. package/dist/waku/internal/patches/router.d.ts.map +1 -1
  218. package/dist/waku/internal/patches/router.js +114 -1
  219. package/dist/waku/internal/patches/router.js.map +1 -1
  220. package/package.json +5 -1
  221. package/src/config.ts +1 -0
  222. package/src/globals.d.ts +16 -0
  223. package/src/index.ts +1 -0
  224. package/src/internal/config.ts +40 -1
  225. package/src/internal/llms.ts +51 -1
  226. package/src/internal/markdown-negotiation.test.ts +42 -0
  227. package/src/internal/markdown-negotiation.ts +95 -0
  228. package/src/internal/markdown.ts +5 -0
  229. package/src/internal/openapi/anchors.ts +44 -0
  230. package/src/internal/openapi/app.ts +127 -0
  231. package/src/internal/openapi/index.ts +24 -0
  232. package/src/internal/openapi/markdown.test.ts +115 -0
  233. package/src/internal/openapi/markdown.ts +275 -0
  234. package/src/internal/openapi/openapi.ts +212 -0
  235. package/src/internal/openapi/openrpc.test.ts +239 -0
  236. package/src/internal/openapi/openrpc.ts +295 -0
  237. package/src/internal/openapi/parser.test.ts +203 -0
  238. package/src/internal/openapi/parser.ts +613 -0
  239. package/src/internal/openapi/registry.test.ts +89 -0
  240. package/src/internal/openapi/registry.ts +89 -0
  241. package/src/internal/openapi/sample.test.ts +283 -0
  242. package/src/internal/openapi/sample.ts +562 -0
  243. package/src/internal/openapi/search.test.ts +62 -0
  244. package/src/internal/openapi/search.ts +108 -0
  245. package/src/internal/openapi/sidebar.test.ts +131 -0
  246. package/src/internal/openapi/sidebar.ts +94 -0
  247. package/src/internal/openapi/union.test.ts +51 -0
  248. package/src/internal/openapi/union.ts +74 -0
  249. package/src/internal/search.ts +20 -0
  250. package/src/internal/test/virtual-config.stub.ts +14 -0
  251. package/src/internal/vite-plugins.ts +106 -11
  252. package/src/openapi-app/App.tsx +64 -0
  253. package/src/openapi-app/blocks.tsx +33 -0
  254. package/src/openapi-app/client.tsx +25 -0
  255. package/src/openapi-app/links.test.ts +84 -0
  256. package/src/openapi-app/links.ts +66 -0
  257. package/src/openapi-app/payload.ts +20 -0
  258. package/src/openapi-app/virtual/config.ts +7 -0
  259. package/src/openapi-app/virtual/group-icons.ts +2 -0
  260. package/src/openapi-app/virtual/langs.ts +6 -0
  261. package/src/openapi-app/virtual/openapi.ts +10 -0
  262. package/src/openapi-app/virtual/search-index.ts +21 -0
  263. package/src/openapi-app/virtual/slots.ts +4 -0
  264. package/src/openapi-app/virtual/user-styles.ts +2 -0
  265. package/src/openapi-app/waku.tsx +154 -0
  266. package/src/react/Badge.tsx +2 -3
  267. package/src/react/Layout.client.tsx +17 -4
  268. package/src/react/OpenApi.tsx +5 -0
  269. package/src/react/internal/CodeToHtml.client.tsx +283 -22
  270. package/src/react/internal/Sidebar.tsx +126 -22
  271. package/src/react/internal/openapi/CodeSample.client.tsx +294 -0
  272. package/src/react/internal/openapi/CollapsibleChildren.client.tsx +41 -0
  273. package/src/react/internal/openapi/Disclosure.client.tsx +67 -0
  274. package/src/react/internal/openapi/Endpoints.tsx +58 -0
  275. package/src/react/internal/openapi/EndpointsView.tsx +76 -0
  276. package/src/react/internal/openapi/EnumValues.client.tsx +49 -0
  277. package/src/react/internal/openapi/HeadingAnchor.tsx +28 -0
  278. package/src/react/internal/openapi/OpenApiPage.tsx +173 -0
  279. package/src/react/internal/openapi/Operation.test.tsx +101 -0
  280. package/src/react/internal/openapi/Operation.tsx +335 -0
  281. package/src/react/internal/openapi/Playground.client.tsx +234 -0
  282. package/src/react/internal/openapi/PropertyExample.client.tsx +55 -0
  283. package/src/react/internal/openapi/Reference.tsx +120 -0
  284. package/src/react/internal/openapi/Schema.tsx +467 -0
  285. package/src/react/internal/openapi/SchemaUnion.client.tsx +123 -0
  286. package/src/react/internal/openapi/anchor-navigation.client.ts +154 -0
  287. package/src/react/internal/openapi/auth.ts +69 -0
  288. package/src/react/useLayout.ts +4 -0
  289. package/src/server/handlers.ts +31 -6
  290. package/src/server/og-assets.ts +14 -0
  291. package/src/server/openapi/assets.generated.ts +1093 -0
  292. package/src/server/openapi/assets.ts +57 -0
  293. package/src/server/openapi/handler.test.ts +244 -0
  294. package/src/server/openapi/handler.ts +277 -0
  295. package/src/server/openapi/html.ts +84 -0
  296. package/src/server/openapi/pages.test.ts +111 -0
  297. package/src/server/openapi/pages.ts +153 -0
  298. package/src/server/openapi/state.ts +136 -0
  299. package/src/styles/index.css +16 -0
  300. package/src/styles/markdown.css +9 -7
  301. package/src/styles/openapi-playground.css +80 -0
  302. package/src/styles/openapi.css +660 -0
  303. package/src/vite.ts +1 -0
  304. package/src/waku/internal/middleware/md-router.ts +8 -52
  305. package/src/waku/internal/patches/adapters/vercel-build-enhancer.ts +1 -1
  306. package/src/waku/internal/patches/router.ts +131 -1
@@ -0,0 +1,562 @@
1
+ import { type HarRequest, snippetz } from '@scalar/snippetz'
2
+ import { mediaIdBase, responseIdBase, type SchemaPath, slug } from './anchors.js'
3
+ import type { IrMediaType, IrOperation, IrParameter } from './parser.js'
4
+ import { unionVariantSchemas, unionVariantSegment } from './union.js'
5
+
6
+ /**
7
+ * A generated request code sample for a single client (e.g. cURL, JS fetch).
8
+ *
9
+ * `lang` is a Shiki language id so the React layer can highlight it with the
10
+ * same highlighter used for the rest of the docs.
11
+ */
12
+ export type CodeSample = {
13
+ /**
14
+ * Clickable spans within `display` that link to a parameter's documentation
15
+ * row. Each `range` is a `[start, end]` character range into `display`; `id`
16
+ * is the target element id. Used for path parameters, which sit inline within
17
+ * the URL line, so only their value (not the whole line) is clickable.
18
+ */
19
+ anchors: { range: [number, number]; id: string }[]
20
+ /** Generated source code, used verbatim for the copy-to-clipboard action. */
21
+ code: string
22
+ /**
23
+ * Collapsed view shown by default when the request has more than 2 query
24
+ * parameters: the same snippet rendered with only the first 2, plus its own
25
+ * `anchors`/`lineAnchors`/`colorRanges` (line counts differ from the full
26
+ * `display`). Absent when there is nothing to collapse.
27
+ */
28
+ collapsed?: {
29
+ display: string
30
+ anchors: { range: [number, number]; id: string }[]
31
+ colorRanges: { range: [number, number]; kind: 'key' | 'value' }[]
32
+ lineAnchors: (string | undefined)[]
33
+ }
34
+ /**
35
+ * `[start, end]` character ranges within `display` to recolor so the URL reads
36
+ * like the JSON response example: parameter names use the JSON-string color
37
+ * (`'key'`) and parameter values the JSON-number color (`'value'`). Covers
38
+ * query parameter names/values; path parameter values are colored via their
39
+ * {@link anchors} entry instead (those already wrap the value).
40
+ */
41
+ colorRanges: { range: [number, number]; kind: 'key' | 'value' }[]
42
+ /**
43
+ * Display variant of {@link code} with each query parameter placed on its own
44
+ * line for readability. Formatted language-aware so it stays syntactically
45
+ * highlightable (TypeScript uses string concatenation; cURL breaks inside the
46
+ * quoted URL). This is what gets highlighted/rendered; `code` is copied.
47
+ */
48
+ display: string
49
+ /** Number of query parameters hidden in {@link collapsed}. */
50
+ hiddenQueryCount?: number
51
+ /** Stable id, e.g. `shell/curl`. */
52
+ id: string
53
+ /** Display label for the language tab, e.g. `cURL`. */
54
+ label: string
55
+ /** Shiki language id for highlighting, e.g. `bash`, `js`. */
56
+ lang: string
57
+ /**
58
+ * Per-line anchor ids (0-based) for `display`. Since each query parameter is
59
+ * placed on its own line, the whole line is made clickable — mirroring the
60
+ * response example lines. `undefined` for non-parameter lines.
61
+ */
62
+ lineAnchors: (string | undefined)[]
63
+ }
64
+
65
+ /** Query parameters above this count collapse behind a "Show more" toggle. */
66
+ const collapseQueryThreshold = 2
67
+
68
+ /**
69
+ * The set of clients we expose in the code-sample selector. Kept intentionally
70
+ * small (cURL + JavaScript fetch) to start; the `snippetz` library supports many
71
+ * more targets if we widen this later.
72
+ */
73
+ const clients = [
74
+ { id: 'shell/curl', target: 'shell', client: 'curl', label: 'cURL', lang: 'bash' },
75
+ { id: 'js/fetch', target: 'js', client: 'fetch', label: 'TypeScript', lang: 'ts' },
76
+ ] as const
77
+
78
+ /**
79
+ * Builds request code samples for an operation using `@scalar/snippetz` — the
80
+ * same snippet generator Scalar uses. Returns one entry per supported client.
81
+ */
82
+ export function codeSamples(operation: IrOperation, server?: string): CodeSample[] {
83
+ const request = harRequest(operation, server)
84
+ const generator = snippetz()
85
+ const queryCount = request.queryString?.length ?? 0
86
+ // When there are many query params, also prepare a request limited to the
87
+ // first few so the sample can collapse the rest behind a "Show more" toggle.
88
+ const collapsedRequest =
89
+ queryCount > collapseQueryThreshold
90
+ ? { ...request, queryString: (request.queryString ?? []).slice(0, collapseQueryThreshold) }
91
+ : undefined
92
+
93
+ const samples: CodeSample[] = []
94
+ for (const client of clients) {
95
+ const code = generator.print(client.target, client.client, request)
96
+ if (!code) continue
97
+ const display = splitQueryLines(code, client.lang)
98
+ const sample: CodeSample = {
99
+ id: client.id,
100
+ label: client.label,
101
+ lang: client.lang,
102
+ code,
103
+ display,
104
+ anchors: pathAnchors(display, operation),
105
+ colorRanges: queryColorRanges(display, operation),
106
+ lineAnchors: queryLineAnchors(display, operation),
107
+ }
108
+ if (collapsedRequest) {
109
+ const collapsedCode = generator.print(client.target, client.client, collapsedRequest)
110
+ if (collapsedCode) {
111
+ const collapsedDisplay = splitQueryLines(collapsedCode, client.lang)
112
+ sample.collapsed = {
113
+ display: collapsedDisplay,
114
+ anchors: pathAnchors(collapsedDisplay, operation),
115
+ colorRanges: queryColorRanges(collapsedDisplay, operation),
116
+ lineAnchors: queryLineAnchors(collapsedDisplay, operation),
117
+ }
118
+ sample.hiddenQueryCount = queryCount - collapseQueryThreshold
119
+ }
120
+ }
121
+ samples.push(sample)
122
+ }
123
+ return samples
124
+ }
125
+
126
+ /**
127
+ * Reformats a generated request snippet so each query parameter sits on its own
128
+ * line. The query string lives in the first single-quoted URL containing `?`.
129
+ *
130
+ * - TypeScript: splits the URL literal into `'base' + '?a=1' + '&b=2'`, which is
131
+ * valid JS (and identical at runtime), so the copied/displayed code both work.
132
+ * - cURL (and other shells): inserts literal newlines inside the quoted URL.
133
+ * This is for display only — the unformatted {@link CodeSample.code} is what
134
+ * the copy button uses, so the runnable request stays intact.
135
+ */
136
+ function splitQueryLines(code: string, lang: string): string {
137
+ const match = code.match(/'([^'\n]*\?[^'\n]*)'/)
138
+ if (!match) return code
139
+ const [token, url = ''] = match
140
+ const questionIndex = url.indexOf('?')
141
+ const base = url.slice(0, questionIndex)
142
+ const params = url.slice(questionIndex + 1).split('&')
143
+ if (params.length === 0) return code
144
+
145
+ const replacement =
146
+ lang === 'ts' || lang === 'js'
147
+ ? [`'${base}'`, `'?${params[0]}'`, ...params.slice(1).map((p) => `'&${p}'`)].join(' +\n ')
148
+ : `'${base}\n ?${params[0]}${params
149
+ .slice(1)
150
+ .map((p) => `\n &${p}`)
151
+ .join('')}'`
152
+ return code.replace(token, replacement)
153
+ }
154
+
155
+ /**
156
+ * Locates clickable path-parameter spans within a request sample. The
157
+ * substituted value is matched against `display` and mapped to its docs row id
158
+ * (`<operationId>-<slug(name)>`). Path params sit inline within the URL line, so
159
+ * only their value is anchored (query params get whole-line anchors instead).
160
+ *
161
+ * Matching is deliberately conservative: a value is only anchored when it
162
+ * appears exactly once, so an ambiguous value (e.g. a generic `0`/`string` that
163
+ * also appears in the body) silently degrades to non-clickable.
164
+ */
165
+ function pathAnchors(
166
+ display: string,
167
+ operation: IrOperation,
168
+ ): { range: [number, number]; id: string }[] {
169
+ const anchors: { range: [number, number]; id: string }[] = []
170
+ for (const parameter of operation.parameters) {
171
+ if (parameter.deprecated || parameter.in !== 'path') continue
172
+ const value = encodeURIComponent(String(sampleParameter(parameter)))
173
+ const index = singleIndexOf(display, value)
174
+ if (index >= 0)
175
+ anchors.push({
176
+ range: [index, index + value.length],
177
+ id: `${operation.id}-${slug(parameter.name)}`,
178
+ })
179
+ }
180
+ return anchors
181
+ }
182
+
183
+ /**
184
+ * Maps each line of `display` to a query parameter's docs row id, so the whole
185
+ * line can be made clickable (like response example lines). Relies on
186
+ * {@link splitQueryLines} having placed each query parameter on its own line
187
+ * starting with `?`/`&` (optionally quoted in the TS variant).
188
+ */
189
+ function queryLineAnchors(display: string, operation: IrOperation): (string | undefined)[] {
190
+ const ids = new Map<string, string>()
191
+ for (const parameter of operation.parameters)
192
+ if (parameter.in === 'query' && !parameter.deprecated)
193
+ ids.set(parameter.name, `${operation.id}-${slug(parameter.name)}`)
194
+ return display.split('\n').map((line) => {
195
+ const match = line.match(/^\s*'?[?&]([^=]+)=/)
196
+ return match ? ids.get(match[1] ?? '') : undefined
197
+ })
198
+ }
199
+
200
+ /**
201
+ * Computes character ranges for each query parameter's name and value within
202
+ * `display` so the UI can recolor them like the JSON response example (names use
203
+ * the JSON-string color, values the JSON-number color). Relies on
204
+ * {@link splitQueryLines} having placed each query parameter on its own line
205
+ * starting with `?`/`&` (optionally quoted in the TS variant).
206
+ */
207
+ function queryColorRanges(
208
+ display: string,
209
+ operation: IrOperation,
210
+ ): { range: [number, number]; kind: 'key' | 'value' }[] {
211
+ const names = new Set<string>()
212
+ for (const parameter of operation.parameters)
213
+ if (parameter.in === 'query' && !parameter.deprecated) names.add(parameter.name)
214
+
215
+ const ranges: { range: [number, number]; kind: 'key' | 'value' }[] = []
216
+ let offset = 0
217
+ for (const line of display.split('\n')) {
218
+ // Capture: (prefix incl. quote + ?/&)(name)(=)(value up to closing quote/EOL)
219
+ const match = line.match(/^(\s*'?[?&])([^=]+)(=)([^'\n]*)/)
220
+ if (match) {
221
+ const [, prefix = '', name = '', , value = ''] = match
222
+ if (names.has(name)) {
223
+ // Color the `?`/`&` separator (last char of the prefix) and the name
224
+ // together as one green span.
225
+ const nameStart = offset + prefix.length - 1
226
+ const nameEnd = offset + prefix.length + name.length
227
+ ranges.push({ range: [nameStart, nameEnd], kind: 'key' })
228
+ if (value.length > 0) {
229
+ const valueStart = nameEnd + 1 // skip '='
230
+ ranges.push({ range: [valueStart, valueStart + value.length], kind: 'value' })
231
+ }
232
+ }
233
+ }
234
+ offset += line.length + 1 // +1 for the consumed newline
235
+ }
236
+ return ranges
237
+ }
238
+
239
+ /** Returns the index of `needle` in `haystack`, or -1 unless it occurs once. */
240
+ function singleIndexOf(haystack: string, needle: string): number {
241
+ if (!needle) return -1
242
+ const first = haystack.indexOf(needle)
243
+ if (first < 0) return -1
244
+ if (haystack.indexOf(needle, first + 1) >= 0) return -1
245
+ return first
246
+ }
247
+
248
+ /**
249
+ * Constructs a HAR-style request from an operation by filling path/query/header
250
+ * parameters and the request body with representative sample values. This is the
251
+ * input `snippetz` consumes to render code samples.
252
+ */
253
+ export function harRequest(operation: IrOperation, server?: string): Partial<HarRequest> {
254
+ const base = (server ?? '').replace(/\/$/, '')
255
+
256
+ let path = operation.path
257
+ const headers: { name: string; value: string }[] = []
258
+ const queryString: { name: string; value: string }[] = []
259
+
260
+ for (const parameter of operation.parameters) {
261
+ if (parameter.deprecated) continue
262
+ const value = String(sampleParameter(parameter))
263
+ if (parameter.in === 'path')
264
+ path = path.replace(`{${parameter.name}}`, encodeURIComponent(value))
265
+ else if (parameter.in === 'query') queryString.push({ name: parameter.name, value })
266
+ else if (parameter.in === 'header' && parameter.required)
267
+ headers.push({ name: parameter.name, value })
268
+ }
269
+
270
+ let postData: { mimeType: string; text: string } | undefined
271
+ const media = operation.requestBody?.content[0]
272
+ if (media) {
273
+ headers.push({ name: 'Content-Type', value: media.mediaType })
274
+ postData = { mimeType: media.mediaType, text: sampleBody(media) }
275
+ }
276
+
277
+ return {
278
+ method: operation.method,
279
+ url: `${base}${path}`,
280
+ headers,
281
+ queryString,
282
+ ...(postData ? { postData } : {}),
283
+ }
284
+ }
285
+
286
+ /**
287
+ * A response code sample (the example body for a single status code), ready to
288
+ * be highlighted as JSON in the right-hand panel.
289
+ */
290
+ export type ResponseSample = {
291
+ status: string
292
+ description?: string | undefined
293
+ /**
294
+ * Id base for this response's schema rows, used to resolve per-line anchors
295
+ * (see {@link linePaths}) to the matching documentation row's element id.
296
+ */
297
+ idBase: string
298
+ lang: string
299
+ code: string
300
+ /**
301
+ * `[start, end]` character ranges (into `code`) of synthesized placeholder
302
+ * values — leaves with no authored `example`/`default`/`enum` whose value was
303
+ * derived solely from the declared type (e.g. `"string"`, `0`). The UI dims
304
+ * these so authored examples stand out.
305
+ */
306
+ placeholders: [number, number][]
307
+ /**
308
+ * Per-line schema path: `linePaths[i]` is the path of the property whose value
309
+ * appears on line `i` of `code` (undefined for closing braces / blank lines).
310
+ * The UI turns these into clickable anchors to the left-hand schema rows.
311
+ */
312
+ linePaths: (SchemaPath | undefined)[]
313
+ }
314
+
315
+ /** Builds JSON response samples for each response that has a usable schema/example. */
316
+ export function responseSamples(operation: IrOperation): ResponseSample[] {
317
+ const samples: ResponseSample[] = []
318
+ for (const response of operation.responses) {
319
+ const media = response.content.find((entry) => entry.mediaType.includes('json'))
320
+ if (!media) continue
321
+ const idBase = mediaIdBase(
322
+ responseIdBase(operation.id, response.status),
323
+ media.mediaType,
324
+ response.content.length,
325
+ )
326
+ // An authored example for the whole media type has no synthesized
327
+ // placeholders; build a tree from the literal value so we can still emit
328
+ // per-line anchors. Otherwise build a tagged tree from the schema so we can
329
+ // also dim type-derived leaves.
330
+ const tree =
331
+ media.example !== undefined ? sampleTreeFromValue(media.example) : sampleTree(media.schema)
332
+ if (tree === undefined) continue
333
+ const { code, placeholders, linePaths } = serializeSample(tree)
334
+ samples.push({
335
+ status: response.status,
336
+ description: response.description,
337
+ idBase,
338
+ lang: 'json',
339
+ code,
340
+ placeholders,
341
+ linePaths,
342
+ })
343
+ }
344
+ return samples
345
+ }
346
+
347
+ /** Serializes a request body media type to its example/sample JSON text. */
348
+ function sampleBody(media: IrMediaType): string {
349
+ const value = media.example ?? sampleFromSchema(media.schema)
350
+ if (media.mediaType.includes('json')) return JSON.stringify(value ?? {}, null, 2)
351
+ return typeof value === 'string' ? value : JSON.stringify(value ?? {})
352
+ }
353
+
354
+ /** Produces a representative value for a single parameter. */
355
+ function sampleParameter(parameter: IrParameter): unknown {
356
+ const value = sampleFromSchema(parameter.schema)
357
+ if (value !== undefined) return value
358
+ return parameter.name
359
+ }
360
+
361
+ /**
362
+ * A tagged sample value tree. Leaves carry a `placeholder` flag indicating the
363
+ * value was synthesized from the declared type (no authored `example`/`default`/
364
+ * `enum`), so the UI can dim it.
365
+ */
366
+ type SampleNode =
367
+ | { kind: 'object'; entries: [string, SampleNode][]; pathSegment?: string }
368
+ | { kind: 'array'; items: SampleNode[]; pathSegment?: string }
369
+ | { kind: 'leaf'; value: unknown; placeholder: boolean }
370
+
371
+ /**
372
+ * Builds a tagged sample tree from a (dereferenced) JSON Schema, preferring
373
+ * authored `example`/`default`/`enum` values and otherwise synthesizing one
374
+ * from the declared type. Bounded by a recursion depth to guard against cycles.
375
+ */
376
+ function sampleTree(
377
+ schema: Record<string, unknown> | undefined,
378
+ depth = 0,
379
+ ): SampleNode | undefined {
380
+ if (!schema || depth > 6) return undefined
381
+
382
+ if (schema['example'] !== undefined)
383
+ return { kind: 'leaf', value: schema['example'], placeholder: false }
384
+ const examples = schema['examples']
385
+ if (Array.isArray(examples) && examples.length > 0)
386
+ return { kind: 'leaf', value: examples[0], placeholder: false }
387
+ if (schema['default'] !== undefined)
388
+ return { kind: 'leaf', value: schema['default'], placeholder: false }
389
+ const enumValues = schema['enum']
390
+ if (Array.isArray(enumValues) && enumValues.length > 0)
391
+ return { kind: 'leaf', value: enumValues[0], placeholder: false }
392
+
393
+ // oneOf/anyOf variant picker (matching the docs renderer): sample the first
394
+ // variant and tag the resulting node with the variant's path segment, so its
395
+ // children's anchor paths line up with the variant-qualified docs rows.
396
+ if (schema['type'] !== 'array') {
397
+ const variants = unionVariantSchemas(schema)
398
+ if (variants) {
399
+ const node = sampleTree(variants[0] as Record<string, unknown>, depth + 1)
400
+ if (node && (node.kind === 'object' || node.kind === 'array'))
401
+ return { ...node, pathSegment: unionVariantSegment(0) }
402
+ return node
403
+ }
404
+ }
405
+
406
+ // Composition keywords: fall back to the first usable subschema.
407
+ for (const key of ['allOf', 'anyOf', 'oneOf'] as const) {
408
+ const subschemas = schema[key]
409
+ if (Array.isArray(subschemas) && subschemas[0])
410
+ return sampleTree(subschemas[0] as Record<string, unknown>, depth + 1)
411
+ }
412
+
413
+ const rawType = schema['type']
414
+ const type = Array.isArray(rawType) ? rawType[0] : rawType
415
+
416
+ if (type === 'object' || schema['properties']) {
417
+ const properties = (schema['properties'] ?? {}) as Record<string, Record<string, unknown>>
418
+ const entries: [string, SampleNode][] = []
419
+ for (const [name, property] of Object.entries(properties)) {
420
+ const node = sampleTree(property, depth + 1)
421
+ // Match `JSON.stringify`, which drops keys whose value is `undefined`.
422
+ if (node !== undefined) entries.push([name, node])
423
+ }
424
+ return { kind: 'object', entries }
425
+ }
426
+
427
+ if (type === 'array') {
428
+ const items = schema['items'] as Record<string, unknown> | undefined
429
+ const item = sampleTree(items, depth + 1)
430
+ return { kind: 'array', items: item === undefined ? [] : [item] }
431
+ }
432
+
433
+ if (type === 'string') {
434
+ const format = schema['format']
435
+ if (format === 'date-time')
436
+ return { kind: 'leaf', value: new Date(0).toISOString(), placeholder: true }
437
+ if (format === 'date')
438
+ return { kind: 'leaf', value: new Date(0).toISOString().slice(0, 10), placeholder: true }
439
+ if (format === 'uuid')
440
+ return { kind: 'leaf', value: '00000000-0000-0000-0000-000000000000', placeholder: true }
441
+ return { kind: 'leaf', value: 'string', placeholder: true }
442
+ }
443
+ if (type === 'integer' || type === 'number') return { kind: 'leaf', value: 0, placeholder: true }
444
+ if (type === 'boolean') return { kind: 'leaf', value: true, placeholder: true }
445
+
446
+ return undefined
447
+ }
448
+
449
+ /**
450
+ * Builds a {@link SampleNode} tree from an already-resolved literal value (an
451
+ * authored media-type example). All leaves are non-placeholders since the value
452
+ * was authored. Object keys whose value serializes to `undefined` are dropped to
453
+ * match `JSON.stringify`.
454
+ */
455
+ function sampleTreeFromValue(value: unknown): SampleNode {
456
+ if (Array.isArray(value))
457
+ return { kind: 'array', items: value.map((item) => sampleTreeFromValue(item ?? null)) }
458
+ if (value && typeof value === 'object') {
459
+ const entries: [string, SampleNode][] = []
460
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
461
+ if (JSON.stringify(child) === undefined) continue
462
+ entries.push([key, sampleTreeFromValue(child)])
463
+ }
464
+ return { kind: 'object', entries }
465
+ }
466
+ return { kind: 'leaf', value, placeholder: false }
467
+ }
468
+
469
+ /** Strips the placeholder tags from a {@link SampleNode} into a plain value. */
470
+ function toValue(node: SampleNode | undefined): unknown {
471
+ if (node === undefined) return undefined
472
+ if (node.kind === 'leaf') return node.value
473
+ if (node.kind === 'array') return node.items.map(toValue)
474
+ const result: Record<string, unknown> = {}
475
+ for (const [name, child] of node.entries) result[name] = toValue(child)
476
+ return result
477
+ }
478
+
479
+ /**
480
+ * Pretty-prints a {@link SampleNode} as JSON (matching
481
+ * `JSON.stringify(value, null, 2)`) while recording `[start, end]` character
482
+ * ranges of placeholder leaf literals.
483
+ */
484
+ function serializeSample(root: SampleNode): {
485
+ code: string
486
+ placeholders: [number, number][]
487
+ linePaths: (SchemaPath | undefined)[]
488
+ } {
489
+ let code = ''
490
+ let line = 0
491
+ const placeholders: [number, number][] = []
492
+ const linePaths: (SchemaPath | undefined)[] = []
493
+
494
+ function mark(path: SchemaPath) {
495
+ if (path.length > 0) linePaths[line] = path
496
+ }
497
+
498
+ function newline() {
499
+ code += '\n'
500
+ line++
501
+ }
502
+
503
+ function walk(node: SampleNode, indent: number, path: SchemaPath) {
504
+ const pad = ' '.repeat(indent)
505
+ const padInner = ' '.repeat(indent + 1)
506
+ // Union variant nodes carry a virtual path segment (e.g. `variant-0`) so
507
+ // their children's anchor paths match the variant-qualified docs rows.
508
+ const childBase = node.kind !== 'leaf' && node.pathSegment ? [...path, node.pathSegment] : path
509
+ if (node.kind === 'object') {
510
+ if (node.entries.length === 0) {
511
+ code += '{}'
512
+ return
513
+ }
514
+ code += '{'
515
+ newline()
516
+ node.entries.forEach(([key, value], index) => {
517
+ const childPath = [...childBase, key]
518
+ mark(childPath)
519
+ code += `${padInner}${JSON.stringify(key)}: `
520
+ walk(value, indent + 1, childPath)
521
+ code += index < node.entries.length - 1 ? ',' : ''
522
+ newline()
523
+ })
524
+ code += `${pad}}`
525
+ return
526
+ }
527
+ if (node.kind === 'array') {
528
+ if (node.items.length === 0) {
529
+ code += '[]'
530
+ return
531
+ }
532
+ code += '['
533
+ newline()
534
+ node.items.forEach((item, index) => {
535
+ // Scalar array items map to the array property's own path.
536
+ if (item.kind === 'leaf') mark(childBase)
537
+ code += padInner
538
+ walk(item, indent + 1, childBase)
539
+ code += index < node.items.length - 1 ? ',' : ''
540
+ newline()
541
+ })
542
+ code += `${pad}]`
543
+ return
544
+ }
545
+ const literal = JSON.stringify(node.value) ?? 'null'
546
+ const start = code.length
547
+ code += literal
548
+ if (node.placeholder) placeholders.push([start, code.length])
549
+ }
550
+
551
+ walk(root, 0, [])
552
+ return { code, placeholders, linePaths }
553
+ }
554
+
555
+ /**
556
+ * Generates a representative value from a (dereferenced) JSON Schema, preferring
557
+ * authored `example`/`default`/`enum` values and otherwise synthesizing one from
558
+ * the declared type. Bounded by a recursion depth to guard against cycles.
559
+ */
560
+ function sampleFromSchema(schema: Record<string, unknown> | undefined, depth = 0): unknown {
561
+ return toValue(sampleTree(schema, depth))
562
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import type { Ir } from './parser.js'
3
+ import { toSearchDocuments } from './search.js'
4
+
5
+ const ir: Ir = {
6
+ path: '/api',
7
+ client: { url: 'https://example.com/openapi.json' },
8
+ info: { title: 'Petstore', description: 'The **best** pet [API](https://x).' },
9
+ servers: [],
10
+ securitySchemes: {},
11
+ traits: [],
12
+ groups: [
13
+ {
14
+ id: 'pets',
15
+ name: 'Pets',
16
+ description: 'Manage pets.',
17
+ operations: [
18
+ {
19
+ id: 'listpets',
20
+ method: 'GET',
21
+ path: '/pets',
22
+ summary: 'List pets',
23
+ description: 'Returns all pets.',
24
+ parameters: [
25
+ { name: 'limit', in: 'query', description: 'Max items' },
26
+ { name: 'cursor', in: 'query', deprecated: true },
27
+ ],
28
+ responses: [{ status: '200', description: 'A list of pets', content: [], headers: [] }],
29
+ },
30
+ ],
31
+ },
32
+ ],
33
+ }
34
+
35
+ describe('toSearchDocuments', () => {
36
+ const docs = toSearchDocuments(ir)
37
+
38
+ test('emits a landing page, a category page, and an operation section', () => {
39
+ expect(docs.map((doc) => ({ href: doc.href, title: doc.title, type: doc.type }))).toEqual([
40
+ { href: '/api', title: 'Petstore', type: 'page' },
41
+ { href: '/api/pets', title: 'Pets', type: 'page' },
42
+ { href: '/api/pets#listpets', title: 'List pets', type: 'section' },
43
+ ])
44
+ })
45
+
46
+ test('landing page strips markdown from the description', () => {
47
+ const landing = docs.find((doc) => doc.href === '/api')
48
+ expect(landing?.text).toBe('The best pet API.')
49
+ })
50
+
51
+ test('operation folds method/path, params and responses into searchable text', () => {
52
+ const operation = docs.find((doc) => doc.href === '/api/pets#listpets')
53
+ expect(operation?.subtitle).toBe('GET /pets')
54
+ expect(operation?.titles).toEqual(['Petstore', 'Pets'])
55
+ expect(operation?.text).toContain('GET /pets')
56
+ expect(operation?.text).toContain('Returns all pets.')
57
+ expect(operation?.text).toContain('limit Max items')
58
+ expect(operation?.text).toContain('200 A list of pets')
59
+ // Deprecated parameters are excluded.
60
+ expect(operation?.text).not.toContain('cursor')
61
+ })
62
+ })