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,111 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { compile, compileSource, compileTraits } from './pages.js'
3
+
4
+ describe('compile', () => {
5
+ test('compiles inline content without filesystem access', async () => {
6
+ const pages = await compile([{ path: '/auth', content: '# Authentication\n\nUse a token.' }])
7
+ expect(pages).toHaveLength(1)
8
+ expect(pages[0]?.path).toBe('/auth')
9
+ expect(pages[0]?.title).toBe('Authentication')
10
+ expect(pages[0]?.blocks[0]).toMatchObject({ type: 'html' })
11
+ })
12
+
13
+ test('applies a title override', async () => {
14
+ const pages = await compile([{ path: '/auth', content: '# Heading', title: 'Custom' }])
15
+ expect(pages[0]?.title).toBe('Custom')
16
+ })
17
+
18
+ test('applies title and description overrides', async () => {
19
+ const pages = await compile([
20
+ { path: '/auth', content: 'Body.', title: 'Authentication', description: 'Use a token.' },
21
+ ])
22
+ expect(pages[0]?.title).toBe('Authentication')
23
+ expect(pages[0]?.description).toBe('Use a token.')
24
+ })
25
+
26
+ test('throws when a page has neither file nor content', async () => {
27
+ await expect(compile([{ path: '/auth' }])).rejects.toThrow(/either `file` or `content`/)
28
+ })
29
+ })
30
+
31
+ describe('compileSource', () => {
32
+ test('compiles markdown to an html block', () => {
33
+ const page = compileSource('/auth', '# Authentication\n\nUse a token.')
34
+ expect(page.path).toBe('/auth')
35
+ expect(page.title).toBe('Authentication')
36
+ expect(page.blocks).toHaveLength(1)
37
+ expect(page.blocks[0]).toMatchObject({ type: 'html' })
38
+ if (page.blocks[0]?.type === 'html') expect(page.blocks[0].html).toContain('Authentication')
39
+ })
40
+
41
+ test('reads title from frontmatter', () => {
42
+ const page = compileSource('/', '---\ntitle: Overview\n---\n\nHello.')
43
+ expect(page.title).toBe('Overview')
44
+ expect(page.blocks).toHaveLength(1)
45
+ })
46
+
47
+ test('adds slug ids to body headings (for the outline + anchors)', () => {
48
+ const page = compileSource('/auth', 'Intro.\n\n## Getting a key\n\n### Header format')
49
+ const html = page.blocks[0]?.type === 'html' ? page.blocks[0].html : ''
50
+ expect(html).toContain('id="getting-a-key"')
51
+ expect(html).toContain('id="header-format"')
52
+ })
53
+
54
+ test('reads title and description from frontmatter', () => {
55
+ const page = compileSource(
56
+ '/auth',
57
+ '---\ntitle: Authentication\ndescription: How auth works.\n---\n\nHello.',
58
+ )
59
+ expect(page.title).toBe('Authentication')
60
+ expect(page.description).toBe('How auth works.')
61
+ })
62
+
63
+ test('splits around <OpenApi.Endpoints /> into blocks', () => {
64
+ const page = compileSource('/', 'Intro text.\n\n<OpenApi.Endpoints />\n\nMore text.')
65
+ expect(page.blocks.map((block) => block.type)).toEqual(['html', 'endpoints', 'html'])
66
+ })
67
+
68
+ test('parses an Endpoints path attribute and strips esm imports', () => {
69
+ const page = compileSource(
70
+ '/',
71
+ 'import { OpenApi } from \'vocs\'\n\n<OpenApi.Endpoints path="/api" />',
72
+ )
73
+ const endpoints = page.blocks.find((block) => block.type === 'endpoints')
74
+ expect(endpoints).toMatchObject({ type: 'endpoints', path: '/api' })
75
+ // The import line is stripped (not rendered as prose).
76
+ expect(page.blocks.some((b) => b.type === 'html' && b.html.includes('import'))).toBe(false)
77
+ })
78
+
79
+ test('normalizes the route path', () => {
80
+ expect(compileSource('auth/', 'x').path).toBe('/auth')
81
+ })
82
+ })
83
+
84
+ describe('compileTraits', () => {
85
+ test('uses tag name as title and x-subtitle as description', () => {
86
+ const pages = compileTraits([
87
+ { id: 'authentication', name: 'Authentication', description: 'Body.', subtitle: 'How auth.' },
88
+ ])
89
+ expect(pages).toHaveLength(1)
90
+ expect(pages[0]?.path).toBe('/authentication')
91
+ expect(pages[0]?.title).toBe('Authentication')
92
+ expect(pages[0]?.description).toBe('How auth.')
93
+ expect(pages[0]?.blocks[0]).toMatchObject({ type: 'html' })
94
+ // Trait pages render their title/subtitle as an on-page header (the body
95
+ // has no heading of its own).
96
+ expect(pages[0]?.header).toBe(true)
97
+ })
98
+
99
+ test('frontmatter overrides tag name and subtitle', () => {
100
+ const pages = compileTraits([
101
+ {
102
+ id: 'auth',
103
+ name: 'Auth',
104
+ subtitle: 'Tag subtitle.',
105
+ description: '---\ntitle: Custom\ndescription: FM subtitle.\n---\n\nBody.',
106
+ },
107
+ ])
108
+ expect(pages[0]?.title).toBe('Custom')
109
+ expect(pages[0]?.description).toBe('FM subtitle.')
110
+ })
111
+ })
@@ -0,0 +1,153 @@
1
+ import * as Markdown from '../../internal/markdown.js'
2
+ import type { CompiledPage, PageBlock } from '../../internal/openapi/app.js'
3
+ import type * as OpenApi from '../../internal/openapi/openapi.js'
4
+ import { normalizePath } from '../../internal/openapi/openapi.js'
5
+ import type { IrTrait } from '../../internal/openapi/parser.js'
6
+
7
+ /**
8
+ * Reads and compiles consumer-authored `.md`/`.mdx` override/guide pages for the
9
+ * standalone handler into serializable {@link CompiledPage}s.
10
+ *
11
+ * The standalone handler has no filesystem router, so pages are supplied
12
+ * explicitly via `config.pages`. Each file is read once at boot and split into
13
+ * an ordered list of {@link PageBlock}s: prose is rendered to HTML with the same
14
+ * `data-v` typography as the rest of the docs, while inline
15
+ * `<OpenApi.Endpoints />` components become `endpoints` blocks re-hydrated as
16
+ * real React on the client.
17
+ *
18
+ * MDX `import`/`export` statements are stripped (the only supported component is
19
+ * `<OpenApi.Endpoints />`); anything else renders as Markdown.
20
+ */
21
+ export async function compile(
22
+ pages: OpenApi.Page[] | undefined,
23
+ options: compile.Options = {},
24
+ ): Promise<CompiledPage[]> {
25
+ if (!pages || pages.length === 0) return []
26
+ const { rootDir = typeof process !== 'undefined' ? process.cwd() : '.' } = options
27
+
28
+ // Only touch the filesystem when a page actually needs a file read, so inline
29
+ // `content` pages work on runtimes without `node:fs` (e.g. Cloudflare Workers).
30
+ const needsFs = pages.some((page) => page.content === undefined && page.file !== undefined)
31
+ const fsModule = needsFs
32
+ ? await Promise.all([
33
+ import('node:fs/promises').then((module) => ({ default: module })),
34
+ import('node:path'),
35
+ ])
36
+ : undefined
37
+
38
+ const compiled: CompiledPage[] = []
39
+ for (const page of pages) {
40
+ let source: string
41
+ if (page.content !== undefined) source = page.content
42
+ else if (page.file !== undefined && fsModule) {
43
+ const [{ default: fs }, path] = fsModule
44
+ const filePath = path.isAbsolute(page.file) ? page.file : path.resolve(rootDir, page.file)
45
+ source = await fs.readFile(filePath, 'utf-8')
46
+ } else throw new Error(`[vocs] Page "${page.path}" must define either \`file\` or \`content\`.`)
47
+
48
+ const result = compileSource(page.path, source)
49
+ if (page.title) result.title = page.title
50
+ if (page.description) result.description = page.description
51
+ compiled.push(result)
52
+ }
53
+ return compiled
54
+ }
55
+
56
+ export declare namespace compile {
57
+ type Options = {
58
+ /** Directory `file` paths are resolved against. @default process.cwd() */
59
+ rootDir?: string | undefined
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Compiles doc-only "trait" tags (`x-traitTag: true`) into guide
65
+ * {@link CompiledPage}s. The tag `name` is the title and `x-subtitle` the
66
+ * subtitle, unless overridden by Markdown frontmatter in the description.
67
+ */
68
+ export function compileTraits(traits: readonly IrTrait[]): CompiledPage[] {
69
+ return traits.map((trait) => {
70
+ const result = compileSource(`/${trait.id}`, trait.description ?? '')
71
+ return {
72
+ ...result,
73
+ title: result.title ?? trait.name,
74
+ description: result.description ?? trait.subtitle,
75
+ // The tag name/subtitle are the page header (the body has no heading).
76
+ header: true,
77
+ }
78
+ })
79
+ }
80
+
81
+ /** Matches a self-closing `<OpenApi.Endpoints ... />` or `<Endpoints ... />`. */
82
+ const endpointsRe = /<(?:OpenApi\.)?Endpoints\b([^>]*?)\/>/g
83
+ /** Matches an ESM `import`/`export` statement line. */
84
+ const esmLineRe = /^\s*(?:import|export)\b.*$/gm
85
+ /** Matches a leading YAML frontmatter block. */
86
+ const frontmatterRe = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/
87
+
88
+ /** Compiles a single page source string into a {@link CompiledPage}. */
89
+ export function compileSource(routePath: string, source: string): CompiledPage {
90
+ const { body, title, description } = stripFrontmatter(source)
91
+
92
+ const blocks: PageBlock[] = []
93
+ let lastIndex = 0
94
+ for (const match of body.matchAll(endpointsRe)) {
95
+ const index = match.index ?? 0
96
+ const before = body.slice(lastIndex, index)
97
+ pushHtml(blocks, before)
98
+ blocks.push({ type: 'endpoints', path: parsePath(match[1] ?? '') })
99
+ lastIndex = index + match[0].length
100
+ }
101
+ pushHtml(blocks, body.slice(lastIndex))
102
+
103
+ // Raw Markdown for the page's `.md` / agent-facing version: drop ESM lines and
104
+ // the `<OpenApi.Endpoints />` component (it has no Markdown representation).
105
+ const markdown = body.replace(esmLineRe, '').replace(endpointsRe, '').trim() || undefined
106
+
107
+ return {
108
+ path: normalizePath(routePath),
109
+ title: title ?? firstHeading(body),
110
+ description,
111
+ blocks,
112
+ markdown,
113
+ }
114
+ }
115
+
116
+ /** Renders a Markdown segment to an HTML block (ESM lines stripped). */
117
+ function pushHtml(blocks: PageBlock[], markdown: string): void {
118
+ const cleaned = markdown.replace(esmLineRe, '').trim()
119
+ if (!cleaned) return
120
+ blocks.push({ type: 'html', html: Markdown.toHtml(cleaned) })
121
+ }
122
+
123
+ /** Extracts the `path="..."` attribute from an `<Endpoints>` tag, if present. */
124
+ function parsePath(attrs: string): string | undefined {
125
+ const match = attrs.match(/\bpath\s*=\s*["']([^"']*)["']/)
126
+ return match?.[1]
127
+ }
128
+
129
+ /** Splits a leading YAML frontmatter block, returning the body, `title`, and `description`. */
130
+ function stripFrontmatter(source: string): {
131
+ body: string
132
+ title?: string | undefined
133
+ description?: string | undefined
134
+ } {
135
+ const match = source.match(frontmatterRe)
136
+ if (!match) return { body: source }
137
+ const title = readField(match[1], 'title')
138
+ const description = readField(match[1], 'description')
139
+ return { body: source.slice(match[0].length), title, description }
140
+ }
141
+
142
+ /** Reads a scalar `key: value` field from a YAML frontmatter block. */
143
+ function readField(frontmatter: string | undefined, key: string): string | undefined {
144
+ return frontmatter
145
+ ?.match(new RegExp(`^${key}\\s*:\\s*(.+)$`, 'm'))?.[1]
146
+ ?.trim()
147
+ .replace(/^["']|["']$/g, '')
148
+ }
149
+
150
+ /** Finds the first `# heading` in a Markdown body. */
151
+ function firstHeading(body: string): string | undefined {
152
+ return body.match(/^#\s+(.+)$/m)?.[1]?.trim()
153
+ }
@@ -0,0 +1,136 @@
1
+ import * as Config from '../../internal/config.js'
2
+ import * as ConfigSerializer from '../../internal/config-serializer.js'
3
+ import type { Payload } from '../../internal/openapi/app.js'
4
+ import type * as OpenApi from '../../internal/openapi/openapi.js'
5
+ import { parse } from '../../internal/openapi/parser.js'
6
+ import * as Sidebar from '../../internal/openapi/sidebar.js'
7
+ import * as Pages from './pages.js'
8
+
9
+ /**
10
+ * Parses the configured spec, compiles override/guide pages, and synthesizes a
11
+ * Vocs config — everything the static browser bundle needs to render the real
12
+ * Vocs layout for the API reference.
13
+ *
14
+ * The sidebar mirrors the site integration: optional `sidebar.top` items, the
15
+ * generated `Introduction` + per-category items, then optional `sidebar.bottom`
16
+ * items.
17
+ */
18
+ export async function prepare(
19
+ config: OpenApi.Config,
20
+ options: prepare.Options = {},
21
+ ): Promise<Payload> {
22
+ const { rootDir, baseUrl } = options
23
+
24
+ const [ir, configPages] = await Promise.all([
25
+ parse(config, { rootDir, baseUrl }),
26
+ Pages.compile(config.pages, { rootDir }),
27
+ ])
28
+
29
+ // Doc-only `x-traitTag` tags become guide pages. By default they nest under
30
+ // `Introduction`; an `x-parent` (group name) places them under that operation
31
+ // group instead, or under a new sidebar group when the name has no operations.
32
+ const traitPages = Pages.compileTraits(ir.traits)
33
+ const pages = [...traitPages, ...configPages]
34
+
35
+ const base = ir.path === '/' ? '' : ir.path.replace(/\/$/, '')
36
+ const item = (trait: (typeof ir.traits)[number]) => ({
37
+ text: trait.name,
38
+ link: `${base}/${trait.id}`,
39
+ })
40
+
41
+ const groupIdByName = new Map(ir.groups.map((group) => [group.name, group.id]))
42
+ const traitIntro: { text: string; link: string }[] = []
43
+ const groupExtras = new Map<string, { text: string; link: string }[]>()
44
+ const extraGroups = new Map<string, { text: string; link: string }[]>()
45
+ for (const trait of ir.traits) {
46
+ if (!trait.parent) {
47
+ traitIntro.push(item(trait))
48
+ continue
49
+ }
50
+ const groupId = groupIdByName.get(trait.parent)
51
+ const bucket = groupId ? groupExtras : extraGroups
52
+ const key = groupId ?? trait.parent
53
+ bucket.set(key, [...(bucket.get(key) ?? []), item(trait)])
54
+ }
55
+
56
+ const top = config.sidebar?.top ?? []
57
+ const bottom = config.sidebar?.bottom ?? []
58
+ const intro = [...traitIntro, ...(config.sidebar?.intro ?? [])]
59
+ const collapsed = config.sidebar?.collapsed ?? false
60
+ const newGroups = [...extraGroups].map(([name, items]) => ({
61
+ text: name,
62
+ collapsed,
63
+ items,
64
+ }))
65
+ const sidebar = [
66
+ ...top,
67
+ ...Sidebar.toSidebar(ir, { intro, groupExtras, collapsed }),
68
+ ...newGroups,
69
+ ...bottom,
70
+ ]
71
+
72
+ // A real Vocs config so the browser bundle renders the genuine layout/chrome.
73
+ // `sidebar` is an array (covers the whole section, no path-scoping needed).
74
+ const vocsConfig = Config.define({
75
+ title: ir.info.title,
76
+ description: ir.info.description,
77
+ rootDir,
78
+ sidebar,
79
+ ...config.vocs,
80
+ })
81
+
82
+ return {
83
+ ir,
84
+ // `vocs.title` overrides the spec title (used for the HTML shell `<title>`).
85
+ title: vocsConfig.title ?? ir.info.title,
86
+ sidebar,
87
+ pages,
88
+ // Serialize functions (e.g. search/feedback adapters) so they survive the
89
+ // JSON embed; the browser deserializes via `virtual:vocs/config`.
90
+ config: ConfigSerializer.serializeFunctions(vocsConfig),
91
+ }
92
+ }
93
+
94
+ export declare namespace prepare {
95
+ type Options = {
96
+ /** Directory file-path specs/pages are resolved against. */
97
+ rootDir?: string | undefined
98
+ /**
99
+ * Base URL relative `x-openrpc` URLs (e.g. `/openrpc.json`) resolve against
100
+ * — the request origin, so the renderer can fetch a host-relative OpenRPC
101
+ * document at runtime.
102
+ */
103
+ baseUrl?: string | undefined
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Resolves consumer-supplied custom CSS for the shell `<head>`: an inline
109
+ * string is returned as-is, while `{ file }` is read once from disk (resolved
110
+ * against `rootDir`). Returns `undefined` when no CSS is configured.
111
+ *
112
+ * Only touches the filesystem when a `file` is configured, so inline-string CSS
113
+ * works on runtimes without `node:fs` (e.g. Cloudflare Workers).
114
+ */
115
+ export async function resolveCss(
116
+ css: string | { file: string } | undefined,
117
+ options: resolveCss.Options = {},
118
+ ): Promise<string | undefined> {
119
+ if (css === undefined) return undefined
120
+ if (typeof css === 'string') return css
121
+
122
+ const { rootDir = typeof process !== 'undefined' ? process.cwd() : '.' } = options
123
+ const [{ default: fs }, path] = await Promise.all([
124
+ import('node:fs/promises').then((module) => ({ default: module })),
125
+ import('node:path'),
126
+ ])
127
+ const filePath = path.isAbsolute(css.file) ? css.file : path.resolve(rootDir, css.file)
128
+ return fs.readFile(filePath, 'utf-8')
129
+ }
130
+
131
+ export declare namespace resolveCss {
132
+ type Options = {
133
+ /** Directory `file` paths are resolved against. @default process.cwd() */
134
+ rootDir?: string | undefined
135
+ }
136
+ }
@@ -7,6 +7,7 @@
7
7
  @import "./theme.css" layer(vocs_theme);
8
8
  @import "./markdown.css" layer(vocs_components);
9
9
  @import "./twoslash.css" layer(vocs_components);
10
+ @import "./openapi.css" layer(vocs_components);
10
11
 
11
12
  @custom-variant dark (&:where([style*=":dark"], [style*=":dark"] *, [style*=": dark"], [style*=": dark"] *));
12
13
 
@@ -149,6 +150,21 @@
149
150
  }
150
151
  }
151
152
 
153
+ /* Full-bleed content (e.g. OpenAPI reference): span the full available width.
154
+ Collapsing the centering gutter to the sidebar width re-flows the sidebar,
155
+ logo, top nav, surface background, and main margin together (they all derive
156
+ from `--vocs-spacing-gutter`), so content starts flush after the sidebar.
157
+ The article/footer max-width and the operation grid columns are set via
158
+ Tailwind utilities on the elements so they cascade in the utilities layer
159
+ rather than being overridden by it. */
160
+ [data-v-content-width="full"] {
161
+ /* biome-ignore lint/correctness/noUnknownFunction: _ */
162
+ @media (width >= theme(--breakpoint-lg)) {
163
+ --vocs-spacing-gutter: var(--vocs-spacing-sidebar);
164
+ --vocs-spacing-logo: var(--vocs-spacing-sidebar);
165
+ }
166
+ }
167
+
152
168
  /* Minimal/no-sidebar layout - content scrolls above topnav */
153
169
  [data-layout="minimal"] > [data-v-main],
154
170
  [data-layout="blank"] > [data-v-main] {
@@ -81,7 +81,7 @@ hgroup[data-v] {
81
81
  }
82
82
 
83
83
  hgroup[data-v],
84
- article[data-v-content] > h1[data-v] {
84
+ :is(article[data-v-content], [data-v-openapi-intro], [data-v-openapi-guide]) > h1[data-v] {
85
85
  @apply vocs:border-b vocs:border-primary vocs:pb-[0.75em];
86
86
  }
87
87
 
@@ -90,7 +90,7 @@ hgroup[data-v] p {
90
90
  }
91
91
 
92
92
  hgroup[data-v]:not(:last-child),
93
- article[data-v-content] > h1[data-v]:not(:last-child) {
93
+ :is(article[data-v-content], [data-v-openapi-intro], [data-v-openapi-guide]) > h1[data-v]:not(:last-child) {
94
94
  margin-block-end: calc(calc(var(--vocs-spacing) * 8) * calc(1 - var(--tw-space-y-reverse))) !important;
95
95
  }
96
96
 
@@ -98,17 +98,19 @@ article[data-v-content] > h1[data-v]:not(:last-child) {
98
98
 
99
99
  /* #region Secondary Headings (h2, h3, h4, h5, h6) */
100
100
 
101
- article[data-v-content] > :not(:is(hgroup[data-v], h1[data-v])) + h2:not(:only-child) {
101
+ :is(article[data-v-content], [data-v-openapi-intro], [data-v-openapi-guide])
102
+ > :not(:is(hgroup[data-v], h1[data-v]))
103
+ + h2:not(:only-child) {
102
104
  @apply vocs:pt-8 vocs:mt-10! vocs:border-t vocs:border-primary;
103
105
  }
104
106
 
105
- article[data-v-content] > h3:not(:only-child) {
107
+ :is(article[data-v-content], [data-v-openapi-intro], [data-v-openapi-guide]) > h3:not(:only-child) {
106
108
  @apply vocs:pt-4;
107
109
  }
108
110
 
109
- article[data-v-content] > h4:not(:only-child),
110
- article[data-v-content] > h5:not(:only-child),
111
- article[data-v-content] > h6:not(:only-child) {
111
+ :is(article[data-v-content], [data-v-openapi-intro], [data-v-openapi-guide]) > h4:not(:only-child),
112
+ :is(article[data-v-content], [data-v-openapi-intro], [data-v-openapi-guide]) > h5:not(:only-child),
113
+ :is(article[data-v-content], [data-v-openapi-intro], [data-v-openapi-guide]) > h6:not(:only-child) {
112
114
  @apply vocs:pt-4;
113
115
  }
114
116
 
@@ -0,0 +1,80 @@
1
+ /*
2
+ * Maps Scalar API client theme variables onto Vocs theme tokens so the modal
3
+ * matches the surrounding docs. Scoped to our Scalar host and given extra
4
+ * specificity (`.scalar-app[data-...]`) so it wins over Scalar's own
5
+ * `.light-mode`/`.dark-mode` definitions. Vocs tokens use `light-dark()`, which
6
+ * resolves from the inherited `color-scheme`, so a single value covers both
7
+ * modes. Variables with no clean Vocs equivalent (radius, shadows, brightness)
8
+ * are intentionally left at Scalar's defaults.
9
+ */
10
+ .scalar-app[data-v-openapi-playground-root],
11
+ .scalar-app[data-v-openapi-playground-root] .light-mode,
12
+ .scalar-app[data-v-openapi-playground-root] .dark-mode {
13
+ /* Surfaces & text.
14
+ * Scalar has four elevation levels; Vocs's `--vocs-color-gray*` tokens are
15
+ * absolute (not theme-adaptive), so map each level with an explicit
16
+ * `light-dark()` to keep elevation monotonic in both modes:
17
+ * light: white -> 13 -> 12 -> 11 (progressively darker)
18
+ * dark: 2 -> 3 -> 4 -> 5 (progressively lighter) */
19
+ --scalar-background-1: light-dark(white, var(--vocs-color-gray2));
20
+ --scalar-background-2: light-dark(var(--vocs-color-gray13), var(--vocs-color-gray3));
21
+ --scalar-background-3: light-dark(var(--vocs-color-gray12), var(--vocs-color-gray4));
22
+ --scalar-background-4: light-dark(var(--vocs-color-gray11), var(--vocs-color-gray5));
23
+ --scalar-background-accent: var(--vocs-color-accenta3);
24
+ --scalar-color-1: var(--vocs-text-color-primary);
25
+ --scalar-color-2: var(--vocs-text-color-secondary);
26
+ --scalar-color-3: var(--vocs-text-color-muted);
27
+ --scalar-color-accent: var(--vocs-color-accent);
28
+ --scalar-border-color: var(--vocs-border-color-primary);
29
+
30
+ /* Links */
31
+ --scalar-link-color: var(--vocs-text-color-link);
32
+ --scalar-link-color-hover: var(--vocs-text-color-link-hover);
33
+ --scalar-link-color-visited: var(--vocs-text-color-link);
34
+
35
+ /* Semantic colors */
36
+ --scalar-color-red: var(--vocs-color-red);
37
+ --scalar-color-danger: var(--vocs-color-red);
38
+ --scalar-color-green: var(--vocs-color-green);
39
+ --scalar-color-blue: var(--vocs-color-blue);
40
+ --scalar-color-yellow: var(--vocs-color-yellow);
41
+ --scalar-color-orange: var(--vocs-color-yellow); /* no Vocs orange */
42
+ --scalar-color-alert: var(--vocs-color-yellow); /* no Vocs orange */
43
+ --scalar-color-purple: var(--vocs-color-iris);
44
+ --scalar-background-danger: var(--vocs-color-destructive-tint);
45
+
46
+ /* Buttons */
47
+ --scalar-button-1: var(--vocs-text-color-heading);
48
+ --scalar-button-1-color: var(--vocs-background-color-surface);
49
+ --scalar-button-1-hover: var(--vocs-text-color-primary);
50
+
51
+ /* Modal sidebar */
52
+ --scalar-sidebar-background-1: var(--vocs-background-color-surface);
53
+ --scalar-sidebar-color-1: var(--vocs-text-color-primary);
54
+ --scalar-sidebar-color-2: var(--vocs-text-color-secondary);
55
+ --scalar-sidebar-border-color: var(--vocs-border-color-primary);
56
+ --scalar-sidebar-item-hover-background: var(--vocs-background-color-primary);
57
+ --scalar-sidebar-item-active-background: var(--vocs-background-color-primary);
58
+ --scalar-sidebar-color-active: var(--vocs-color-accent);
59
+ --scalar-sidebar-search-background: var(--vocs-background-color-primary);
60
+ --scalar-sidebar-search-border-color: var(--vocs-border-color-primary);
61
+ --scalar-sidebar-search-color: var(--vocs-text-color-muted);
62
+
63
+ /* Fonts */
64
+ --scalar-font: var(--vocs-font-sans);
65
+ --scalar-font-code: var(--vocs-font-mono);
66
+ }
67
+
68
+ /*
69
+ * DataTable enable/disable checkbox (e.g. query/header parameter rows).
70
+ * Scalar always renders the checkmark glyph and only swaps its color: a faint
71
+ * `--scalar-background-2` ghost when unchecked, `--scalar-color-1` when checked.
72
+ * Against Vocs surfaces that ghost reads as a solid (near-black) check, so
73
+ * disabled params look identical to enabled ones. Hide the glyph entirely when
74
+ * the checkbox is unchecked; the checked state keeps Scalar's `text-c-1`. The
75
+ * hover affordance box is a separate `<div>` and is left untouched.
76
+ */
77
+ .scalar-app input[type="checkbox"].peer:not(:checked) ~ div .scalar-icon,
78
+ .scalar-app input[type="checkbox"].peer:not(:checked) ~ div svg {
79
+ color: transparent;
80
+ }