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
@@ -17,24 +17,74 @@ import { useEffect, useState } from 'react'
17
17
  * bundle is dynamically imported and the snippet highlighted on demand.
18
18
  */
19
19
  export function CodeToHtml(props: CodeToHtml.Props) {
20
- const { code, lang } = props
21
- // Initialize from the cache so a snippet that was already highlighted (e.g.
22
- // pre-highlighted by the popover before opening, or shown previously) renders
23
- // highlighted immediately without a plain-text flash.
24
- const [html, setHtml] = useState<string | null>(() => getCached(code, lang) ?? null)
20
+ const {
21
+ anchorRanges,
22
+ code,
23
+ colorRanges,
24
+ dimRanges,
25
+ lang,
26
+ lineAnchors,
27
+ shrinkIndent = true,
28
+ } = props
29
+ const key = cacheKey(code, lang, shrinkIndent, dimRanges, lineAnchors, anchorRanges, colorRanges)
30
+ // Track the highlighted HTML alongside the key it was computed for. Storing
31
+ // the key lets us detect when `code`/`lang` change (e.g. switching the cURL /
32
+ // JavaScript request sample tab) and resync — otherwise the component keeps
33
+ // rendering the previously highlighted snippet.
34
+ const [state, setState] = useState<{ key: string; html: string } | null>(() => {
35
+ const cached = getCached(
36
+ code,
37
+ lang,
38
+ shrinkIndent,
39
+ dimRanges,
40
+ lineAnchors,
41
+ anchorRanges,
42
+ colorRanges,
43
+ )
44
+ return cached ? { key, html: cached } : null
45
+ })
46
+
47
+ // When the snippet changes, resync from the cache during render so an already
48
+ // highlighted snippet shows immediately without a plain-text flash.
49
+ if (state && state.key !== key) {
50
+ const cached = getCached(
51
+ code,
52
+ lang,
53
+ shrinkIndent,
54
+ dimRanges,
55
+ lineAnchors,
56
+ anchorRanges,
57
+ colorRanges,
58
+ )
59
+ setState(cached ? { key, html: cached } : null)
60
+ }
61
+
62
+ const html = state?.key === key ? state.html : null
25
63
 
26
64
  useEffect(() => {
27
65
  if (html) return
28
66
  let cancelled = false
29
- highlight(code, lang)
67
+ highlight(code, lang, shrinkIndent, dimRanges, lineAnchors, anchorRanges, colorRanges)
30
68
  .then((result) => {
31
- if (!cancelled) setHtml(result)
69
+ if (!cancelled)
70
+ setState({
71
+ key: cacheKey(
72
+ code,
73
+ lang,
74
+ shrinkIndent,
75
+ dimRanges,
76
+ lineAnchors,
77
+ anchorRanges,
78
+ colorRanges,
79
+ ),
80
+ html: result,
81
+ })
32
82
  })
33
83
  .catch(() => {})
34
84
  return () => {
35
85
  cancelled = true
36
86
  }
37
- }, [code, lang, html])
87
+ }, [code, lang, shrinkIndent, dimRanges, lineAnchors, anchorRanges, colorRanges, html])
38
88
 
39
89
  if (html)
40
90
  // biome-ignore lint/security/noDangerouslySetInnerHtml: highlighted by Shiki
@@ -82,21 +132,68 @@ export namespace CodeToHtml {
82
132
  export type Props = {
83
133
  code: string
84
134
  lang: string
135
+ /**
136
+ * Whether to halve leading indentation (2 spaces → 1) for a more compact
137
+ * code block. Defaults to `true`; set `false` to preserve the source's
138
+ * indentation (e.g. for pretty-printed JSON response samples).
139
+ */
140
+ shrinkIndent?: boolean | undefined
141
+ /**
142
+ * `[start, end]` character ranges to visually dim (e.g. synthesized
143
+ * placeholder values in a response example). Implemented via Shiki
144
+ * decorations, so ranges must be non-overlapping and within bounds.
145
+ */
146
+ dimRanges?: [number, number][] | undefined
147
+ /**
148
+ * Per-line anchor ids. `lineAnchors[i]` (0-based) sets `data-anchor` on line
149
+ * `i`'s element so the line can be made clickable (e.g. to jump from a
150
+ * response example to the matching schema row).
151
+ */
152
+ lineAnchors?: (string | undefined)[] | undefined
153
+ /**
154
+ * Inline clickable spans: each `range` is a `[start, end]` character range
155
+ * wrapped in a `data-anchor` span pointing at `id` (e.g. path/query params
156
+ * in a request sample). Implemented via Shiki decorations, so ranges must be
157
+ * non-overlapping and within bounds.
158
+ */
159
+ anchorRanges?: { range: [number, number]; id: string }[] | undefined
160
+ /**
161
+ * `[start, end]` character ranges to recolor with a semantic token color:
162
+ * `'key'` uses the JSON-string color and `'value'` the JSON-number color (as
163
+ * captured from the active Shiki theme). Used to make request-sample query
164
+ * parameter names/values read like the JSON response example. Ranges must be
165
+ * non-overlapping and within bounds.
166
+ */
167
+ colorRanges?: { range: [number, number]; kind: 'key' | 'value' }[] | undefined
85
168
  }
86
169
  }
87
170
 
88
171
  let highlighterPromise:
89
172
  | Promise<{
90
- codeToHtml: (code: string, lang: string) => Promise<string>
173
+ codeToHtml: (
174
+ code: string,
175
+ lang: string,
176
+ shrinkIndent: boolean,
177
+ dimRanges?: [number, number][] | undefined,
178
+ lineAnchors?: (string | undefined)[] | undefined,
179
+ anchorRanges?: { range: [number, number]; id: string }[] | undefined,
180
+ colorRanges?: { range: [number, number]; kind: 'key' | 'value' }[] | undefined,
181
+ ) => Promise<string>
91
182
  }>
92
183
  | undefined
93
184
 
94
185
  function getHighlighter() {
95
186
  if (!highlighterPromise)
96
187
  highlighterPromise = (async () => {
97
- const { bundledLanguages, createHighlighter, hastToHtml } = await import('shiki/bundle/web')
188
+ const [{ bundledLanguages, createHighlighter, hastToHtml }, { langs }] = await Promise.all([
189
+ import('shiki/bundle/web'),
190
+ import('virtual:vocs/langs'),
191
+ ])
98
192
  const { codeHighlight } = config
99
193
  const { langAlias = {}, themes } = codeHighlight
194
+ const extraLanguages = new Map(
195
+ langs.flatMap((lang) => [lang.name, ...(lang.aliases ?? [])].map((name) => [name, lang])),
196
+ )
100
197
  // Note: `langAlias` is intentionally not passed to the highlighter.
101
198
  // Passing a custom `langAlias` registers languages under their alias name
102
199
  // when lazily loaded (e.g. `loadLanguage('ts')` registers as `ts` instead
@@ -106,19 +203,67 @@ function getHighlighter() {
106
203
  themes: Object.values(themes) as never,
107
204
  langs: [],
108
205
  })
206
+ // Capture the active theme's JSON property-name/number colors once and
207
+ // expose them as CSS variables, so request samples can recolor query/path
208
+ // parameter names/values to match the JSON response example (openapi.css).
209
+ void applyTokenColors(highlighter, themes)
109
210
  return {
110
- async codeToHtml(code: string, lang: string) {
211
+ async codeToHtml(
212
+ code: string,
213
+ lang: string,
214
+ shrinkIndent: boolean,
215
+ dimRanges?: [number, number][] | undefined,
216
+ lineAnchors?: (string | undefined)[] | undefined,
217
+ anchorRanges?: { range: [number, number]; id: string }[] | undefined,
218
+ colorRanges?: { range: [number, number]; kind: 'key' | 'value' }[] | undefined,
219
+ ) {
111
220
  const base = langAlias[lang] ?? lang
112
- const resolvedLang = base in bundledLanguages ? base : 'txt'
113
- if (!highlighter.getLoadedLanguages().includes(resolvedLang))
114
- await highlighter.loadLanguage(resolvedLang as never)
221
+ const extraLanguage = extraLanguages.get(base)
222
+ const resolvedLang = base in bundledLanguages ? base : (extraLanguage?.name ?? 'txt')
223
+ if (!highlighter.getLoadedLanguages().includes(resolvedLang)) {
224
+ if (extraLanguage) await highlighter.loadLanguage(extraLanguage)
225
+ else await highlighter.loadLanguage(resolvedLang as never)
226
+ }
227
+ // Clamp/validate ranges so a bad offset can't throw inside Shiki.
228
+ const decorations = [
229
+ ...(dimRanges ?? [])
230
+ .filter(([start, end]) => start >= 0 && end <= code.length && start < end)
231
+ .map(([start, end]) => ({
232
+ start,
233
+ end,
234
+ properties: { 'data-v-openapi-placeholder': '' },
235
+ })),
236
+ ...(anchorRanges ?? [])
237
+ .filter(({ range: [start, end] }) => start >= 0 && end <= code.length && start < end)
238
+ .map(({ range: [start, end], id }) => ({
239
+ start,
240
+ end,
241
+ // Path parameter values are always values → color them blue.
242
+ properties: {
243
+ 'data-anchor': id,
244
+ 'data-v-openapi-anchor': '',
245
+ 'data-v-openapi-token': 'value',
246
+ },
247
+ })),
248
+ ...(colorRanges ?? [])
249
+ .filter(({ range: [start, end] }) => start >= 0 && end <= code.length && start < end)
250
+ .map(({ range: [start, end], kind }) => ({
251
+ start,
252
+ end,
253
+ properties: { 'data-v-openapi-token': kind },
254
+ })),
255
+ ]
115
256
  const hast = highlighter.codeToHast(code, {
116
257
  defaultColor: 'light-dark()',
117
258
  lang: resolvedLang,
118
259
  rootStyle: false,
119
260
  meta: { 'data-v-overflow-fade': true },
120
261
  themes,
121
- transformers: [transformerShrinkIndent()],
262
+ transformers: [
263
+ ...(shrinkIndent ? [transformerShrinkIndent()] : []),
264
+ ...(lineAnchors ? [transformerLineAnchors(lineAnchors)] : []),
265
+ ],
266
+ ...(decorations.length > 0 ? { decorations } : {}),
122
267
  })
123
268
  const pre = hast.children[0]
124
269
  if (pre && pre.type === 'element' && pre.tagName === 'pre')
@@ -137,22 +282,60 @@ function getHighlighter() {
137
282
 
138
283
  const cache = new Map<string, string>()
139
284
 
140
- function cacheKey(code: string, lang: string) {
141
- return `${lang}\n${code}`
285
+ function cacheKey(
286
+ code: string,
287
+ lang: string,
288
+ shrinkIndent = true,
289
+ dimRanges?: [number, number][] | undefined,
290
+ lineAnchors?: (string | undefined)[] | undefined,
291
+ anchorRanges?: { range: [number, number]; id: string }[] | undefined,
292
+ colorRanges?: { range: [number, number]; kind: 'key' | 'value' }[] | undefined,
293
+ ) {
294
+ const ranges = dimRanges && dimRanges.length > 0 ? JSON.stringify(dimRanges) : ''
295
+ const anchors = lineAnchors && lineAnchors.length > 0 ? JSON.stringify(lineAnchors) : ''
296
+ const spans = anchorRanges && anchorRanges.length > 0 ? JSON.stringify(anchorRanges) : ''
297
+ const colors = colorRanges && colorRanges.length > 0 ? JSON.stringify(colorRanges) : ''
298
+ return `${lang}\n${shrinkIndent ? '1' : '0'}\n${ranges}\n${anchors}\n${spans}\n${colors}\n${code}`
142
299
  }
143
300
 
144
301
  /** Returns the highlighted HTML for a snippet if it has already been computed. */
145
- export function getCached(code: string, lang: string) {
146
- return cache.get(cacheKey(code, lang))
302
+ export function getCached(
303
+ code: string,
304
+ lang: string,
305
+ shrinkIndent = true,
306
+ dimRanges?: [number, number][] | undefined,
307
+ lineAnchors?: (string | undefined)[] | undefined,
308
+ anchorRanges?: { range: [number, number]; id: string }[] | undefined,
309
+ colorRanges?: { range: [number, number]; kind: 'key' | 'value' }[] | undefined,
310
+ ) {
311
+ return cache.get(
312
+ cacheKey(code, lang, shrinkIndent, dimRanges, lineAnchors, anchorRanges, colorRanges),
313
+ )
147
314
  }
148
315
 
149
316
  /** Highlights a snippet (memoized), loading the Shiki bundle on first use. */
150
- export async function highlight(code: string, lang: string) {
151
- const key = cacheKey(code, lang)
317
+ export async function highlight(
318
+ code: string,
319
+ lang: string,
320
+ shrinkIndent = true,
321
+ dimRanges?: [number, number][] | undefined,
322
+ lineAnchors?: (string | undefined)[] | undefined,
323
+ anchorRanges?: { range: [number, number]; id: string }[] | undefined,
324
+ colorRanges?: { range: [number, number]; kind: 'key' | 'value' }[] | undefined,
325
+ ) {
326
+ const key = cacheKey(code, lang, shrinkIndent, dimRanges, lineAnchors, anchorRanges, colorRanges)
152
327
  const cached = cache.get(key)
153
328
  if (cached !== undefined) return cached
154
329
  const highlighter = await getHighlighter()
155
- const html = await highlighter.codeToHtml(code, lang)
330
+ const html = await highlighter.codeToHtml(
331
+ code,
332
+ lang,
333
+ shrinkIndent,
334
+ dimRanges,
335
+ lineAnchors,
336
+ anchorRanges,
337
+ colorRanges,
338
+ )
156
339
  cache.set(key, html)
157
340
  return html
158
341
  }
@@ -162,6 +345,84 @@ export function prewarm() {
162
345
  void getHighlighter()
163
346
  }
164
347
 
348
+ function transformerLineAnchors(lineAnchors: readonly (string | undefined)[]) {
349
+ return {
350
+ name: 'line-anchors',
351
+ // Shiki's `line` hook is 1-based.
352
+ line(hast: { properties?: Record<string, unknown> }, line: number) {
353
+ const anchor = lineAnchors[line - 1]
354
+ if (!anchor) return
355
+ hast.properties ??= {}
356
+ hast.properties['data-anchor'] = anchor
357
+ },
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Highlights a tiny probe JSON snippet to read the active theme's token colors,
363
+ * then exposes them as CSS variables on `:root` so request samples can recolor
364
+ * query/path parameter names/values to match the JSON response example: names
365
+ * use the JSON property-name color (green in the default dark theme) and values
366
+ * the JSON number color (blue). The captured values are `light-dark()` pairs, so
367
+ * both color schemes match the response example. Runs once per highlighter;
368
+ * failures are silent (CSS falls back to `--vocs-color-green`/`--vocs-color-blue`).
369
+ */
370
+ async function applyTokenColors(
371
+ highlighter: {
372
+ getLoadedLanguages: () => string[]
373
+ loadLanguage: (lang: never) => Promise<unknown>
374
+ codeToHast: (code: string, options: never) => unknown
375
+ },
376
+ themes: Record<string, unknown>,
377
+ ) {
378
+ if (typeof document === 'undefined') return
379
+ try {
380
+ if (!highlighter.getLoadedLanguages().includes('json'))
381
+ await highlighter.loadLanguage('json' as never)
382
+ const hast = highlighter.codeToHast('{ "a": "abc", "b": 123 }', {
383
+ defaultColor: 'light-dark()',
384
+ lang: 'json',
385
+ themes,
386
+ } as never) as HastRoot
387
+ // Names use the JSON property-name color (`"a"`); values the number color
388
+ // (`123`). In the default dark theme these are green and blue respectively.
389
+ const colors: { key?: string; value?: string } = {}
390
+ walkHast(hast, (text, style) => {
391
+ const color = style.match(/color:\s*([^;]+)/)?.[1]?.trim()
392
+ if (!color) return
393
+ if (text === '"a"') colors.key ??= color
394
+ else if (text === '123') colors.value ??= color
395
+ })
396
+ const root = document.documentElement
397
+ if (colors.key) root.style.setProperty('--vocs-openapi-token-key', colors.key)
398
+ if (colors.value) root.style.setProperty('--vocs-openapi-token-value', colors.value)
399
+ } catch {}
400
+ }
401
+
402
+ type HastNode = {
403
+ type: string
404
+ tagName?: string
405
+ value?: string
406
+ properties?: Record<string, unknown>
407
+ children?: HastNode[]
408
+ }
409
+ type HastRoot = { children?: HastNode[] }
410
+
411
+ /** Visits every text-bearing span in a hast tree with its text and inline style. */
412
+ function walkHast(node: HastNode | HastRoot, visit: (text: string, style: string) => void) {
413
+ const children = (node as HastNode).children
414
+ if (Array.isArray(children)) {
415
+ for (const child of children) {
416
+ if (child.type === 'element' && child.tagName === 'span') {
417
+ const text = child.children?.[0]?.value
418
+ const style = child.properties?.['style']
419
+ if (typeof text === 'string' && typeof style === 'string') visit(text, style)
420
+ }
421
+ walkHast(child, visit)
422
+ }
423
+ }
424
+ }
425
+
165
426
  function transformerShrinkIndent() {
166
427
  return {
167
428
  name: 'indent',
@@ -14,6 +14,76 @@ import { useSidebar } from '../useSidebar.js'
14
14
 
15
15
  const maxDepth = 5
16
16
 
17
+ /** Active in-page anchor id, for hash-link sidebar items (e.g. OpenAPI). */
18
+ const ActiveAnchorContext = React.createContext<string | null>(null)
19
+
20
+ /** Whether any sidebar item links to an in-page anchor. */
21
+ function hasHashLink(items: Sidebar_core.SidebarItem[]): boolean {
22
+ return items.some((item) => item.link?.includes('#') || (item.items && hasHashLink(item.items)))
23
+ }
24
+
25
+ /**
26
+ * Tracks the in-page section currently in view, so hash-link sidebar items
27
+ * (used by the OpenAPI section) can show active state. Mirrors the Outline's
28
+ * IntersectionObserver approach.
29
+ */
30
+ function useActiveAnchor(enabled: boolean, path: string): string | null {
31
+ const [activeId, setActiveId] = React.useState<string | null>(null)
32
+
33
+ React.useEffect(() => {
34
+ if (!enabled || typeof window === 'undefined') return
35
+
36
+ // Reset on every client-side navigation: derive the active section from the
37
+ // hash of the destination `path` (landing pages like the OpenAPI section
38
+ // root have none, so they start with no active section). Reading `path`
39
+ // here — rather than only `window.location` — keeps it a genuine effect
40
+ // dependency so navigation reliably re-runs this.
41
+ const hashFromPath = path.includes('#') ? path.slice(path.indexOf('#') + 1) : ''
42
+ const hash = hashFromPath || (window.location.hash ? window.location.hash.slice(1) : '')
43
+ setActiveId(hash || null)
44
+
45
+ // OpenAPI pages render inside `article[data-v-content]` too, so the generic
46
+ // markdown selector would also match operation sub-headings (Parameters,
47
+ // Responses, schema anchors, …) whose ids have no sidebar entry — hijacking
48
+ // the active anchor as you scroll through a section. On those pages observe
49
+ // only the group header (`<h1>`) and per-operation titles, which back the
50
+ // hash-link sidebar items, so the active item persists across the section.
51
+ const isOpenApi = document.querySelector('[data-v-openapi]') !== null
52
+ const selector = isOpenApi
53
+ ? '[data-v-openapi-h1][id], [data-v-openapi-operation-title][id]'
54
+ : 'article[data-v-content] :is(h2, h3, h4, h5, h6)[id]'
55
+ const observer = new IntersectionObserver(
56
+ (entries) => {
57
+ for (const entry of entries)
58
+ if (entry.isIntersecting) {
59
+ setActiveId(entry.target.id)
60
+ break
61
+ }
62
+ },
63
+ { rootMargin: '0px 0px -80% 0px' },
64
+ )
65
+
66
+ const observeAll = () => {
67
+ observer.disconnect()
68
+ for (const element of document.querySelectorAll(selector)) observer.observe(element)
69
+ }
70
+ observeAll()
71
+
72
+ const content =
73
+ document.querySelector('article[data-v-content]') ??
74
+ document.querySelector('[data-v-openapi]')
75
+ const mutation = content ? new MutationObserver(observeAll) : null
76
+ if (content && mutation) mutation.observe(content, { childList: true, subtree: true })
77
+
78
+ return () => {
79
+ observer.disconnect()
80
+ mutation?.disconnect()
81
+ }
82
+ }, [enabled, path])
83
+
84
+ return activeId
85
+ }
86
+
17
87
  export function Sidebar(props: Sidebar.Props) {
18
88
  const { className, onNavigate, scrollRef } = props
19
89
 
@@ -22,26 +92,31 @@ export function Sidebar(props: Sidebar.Props) {
22
92
  () => Sidebar_core.length(sidebar.items, { startDepth: 2 }) > 25,
23
93
  [sidebar.items],
24
94
  )
95
+ const hashLinks = React.useMemo(() => hasHashLink(sidebar.items), [sidebar.items])
96
+ const { path } = useRouter()
97
+ const activeAnchor = useActiveAnchor(hashLinks, path)
25
98
 
26
99
  return (
27
- <nav
28
- className={cx(
29
- "vocs:flex-1 vocs:flex vocs:flex-col vocs:text-sm vocs:font-[450] vocs:[&>*:not(:last-child)[data-collapsed='false']]:mb-4",
30
- className,
31
- )}
32
- data-v-sidebar
33
- >
34
- {sidebar.backLink && <BackLink onNavigate={onNavigate} />}
35
- {sidebar.items.map((item, i) => (
36
- <Section
37
- key={`${item.text}-${i}`}
38
- {...item}
39
- condensed={condenseSidebar}
40
- onNavigate={onNavigate}
41
- scrollRef={scrollRef}
42
- />
43
- ))}
44
- </nav>
100
+ <ActiveAnchorContext.Provider value={activeAnchor}>
101
+ <nav
102
+ className={cx(
103
+ "vocs:flex-1 vocs:flex vocs:flex-col vocs:text-sm vocs:font-[450] vocs:[&>*:not(:last-child)[data-collapsed='false']]:mb-4",
104
+ className,
105
+ )}
106
+ data-v-sidebar
107
+ >
108
+ {sidebar.backLink && <BackLink onNavigate={onNavigate} />}
109
+ {sidebar.items.map((item, i) => (
110
+ <Section
111
+ key={`${item.text}-${i}`}
112
+ {...item}
113
+ condensed={condenseSidebar}
114
+ onNavigate={onNavigate}
115
+ scrollRef={scrollRef}
116
+ />
117
+ ))}
118
+ </nav>
119
+ </ActiveAnchorContext.Provider>
45
120
  )
46
121
  }
47
122
 
@@ -99,10 +174,21 @@ function Item(props: Item.Props) {
99
174
 
100
175
  const { path } = useRouter()
101
176
  const isExternal = external ?? Path.isExternal(link)
102
- const active = React.useMemo(
103
- () => (isExternal ? false : Path.matches(path, link)),
104
- [path, link, isExternal],
105
- )
177
+ const activeAnchor = React.useContext(ActiveAnchorContext)
178
+ const hashId = link?.includes('#') ? link.split('#')[1] : undefined
179
+ const active = React.useMemo(() => {
180
+ if (isExternal) return false
181
+ if (hashId) return hashId === activeAnchor
182
+ if (!Path.matches(path, link)) return false
183
+ // Page-level item (e.g. the OpenAPI "Overview"). When in-page anchor
184
+ // tracking is active, only highlight it while viewing the top of the page —
185
+ // i.e. when the active section is the page's top heading (whose id matches
186
+ // the last path segment) or no section is active yet. Otherwise the matching
187
+ // anchor item below it is highlighted instead.
188
+ if (activeAnchor == null) return true
189
+ const topId = (link?.split('#')[0] ?? '').split('/').filter(Boolean).pop()
190
+ return activeAnchor === topId
191
+ }, [path, link, isExternal, hashId, activeAnchor])
106
192
 
107
193
  const itemRef = React.useRef<HTMLElement>(null)
108
194
  const prevPath = React.useRef(path)
@@ -135,6 +221,24 @@ function Item(props: Item.Props) {
135
221
  })
136
222
  }, [link, path, scrollRef])
137
223
 
224
+ // Keep the active in-page anchor item (OpenAPI endpoints) visible as the page
225
+ // scrolls, since `path` doesn't change between same-page anchors.
226
+ React.useEffect(() => {
227
+ if (!active || !hashId) return
228
+ const item = itemRef.current
229
+ const container = scrollRef?.current
230
+ if (!item || !container) return
231
+
232
+ const itemTop = item.offsetTop
233
+ const itemBottom = itemTop + item.offsetHeight
234
+ const containerScrollTop = container.scrollTop
235
+ const containerHeight = container.clientHeight
236
+
237
+ if (itemTop >= containerScrollTop && itemBottom <= containerScrollTop + containerHeight) return
238
+
239
+ container.scrollTo({ behavior: 'smooth', top: itemTop - 100 })
240
+ }, [active, hashId, scrollRef])
241
+
138
242
  if (link && !disabled) {
139
243
  if (isExternal)
140
244
  return (