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.
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/globals.d.ts +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/config.d.ts +28 -0
- package/dist/internal/config.d.ts.map +1 -1
- package/dist/internal/config.js +10 -2
- package/dist/internal/config.js.map +1 -1
- package/dist/internal/llms.d.ts +21 -2
- package/dist/internal/llms.d.ts.map +1 -1
- package/dist/internal/llms.js +41 -1
- package/dist/internal/llms.js.map +1 -1
- package/dist/internal/markdown-negotiation.d.ts +36 -0
- package/dist/internal/markdown-negotiation.d.ts.map +1 -0
- package/dist/internal/markdown-negotiation.js +81 -0
- package/dist/internal/markdown-negotiation.js.map +1 -0
- package/dist/internal/markdown.d.ts.map +1 -1
- package/dist/internal/markdown.js +5 -0
- package/dist/internal/markdown.js.map +1 -1
- package/dist/internal/openapi/anchors.d.ts +25 -0
- package/dist/internal/openapi/anchors.d.ts.map +1 -0
- package/dist/internal/openapi/anchors.js +37 -0
- package/dist/internal/openapi/anchors.js.map +1 -0
- package/dist/internal/openapi/app.d.ts +89 -0
- package/dist/internal/openapi/app.d.ts.map +1 -0
- package/dist/internal/openapi/app.js +62 -0
- package/dist/internal/openapi/app.js.map +1 -0
- package/dist/internal/openapi/index.d.ts +8 -0
- package/dist/internal/openapi/index.d.ts.map +1 -0
- package/dist/internal/openapi/index.js +5 -0
- package/dist/internal/openapi/index.js.map +1 -0
- package/dist/internal/openapi/markdown.d.ts +42 -0
- package/dist/internal/openapi/markdown.d.ts.map +1 -0
- package/dist/internal/openapi/markdown.js +235 -0
- package/dist/internal/openapi/markdown.js.map +1 -0
- package/dist/internal/openapi/openapi.d.ts +187 -0
- package/dist/internal/openapi/openapi.d.ts.map +1 -0
- package/dist/internal/openapi/openapi.js +44 -0
- package/dist/internal/openapi/openapi.js.map +1 -0
- package/dist/internal/openapi/openrpc.d.ts +90 -0
- package/dist/internal/openapi/openrpc.d.ts.map +1 -0
- package/dist/internal/openapi/openrpc.js +213 -0
- package/dist/internal/openapi/openrpc.js.map +1 -0
- package/dist/internal/openapi/parser.d.ts +181 -0
- package/dist/internal/openapi/parser.d.ts.map +1 -0
- package/dist/internal/openapi/parser.js +329 -0
- package/dist/internal/openapi/parser.js.map +1 -0
- package/dist/internal/openapi/registry.d.ts +36 -0
- package/dist/internal/openapi/registry.d.ts.map +1 -0
- package/dist/internal/openapi/registry.js +79 -0
- package/dist/internal/openapi/registry.js.map +1 -0
- package/dist/internal/openapi/sample.d.ts +115 -0
- package/dist/internal/openapi/sample.d.ts.map +1 -0
- package/dist/internal/openapi/sample.js +434 -0
- package/dist/internal/openapi/sample.js.map +1 -0
- package/dist/internal/openapi/search.d.ts +19 -0
- package/dist/internal/openapi/search.d.ts.map +1 -0
- package/dist/internal/openapi/search.js +98 -0
- package/dist/internal/openapi/search.js.map +1 -0
- package/dist/internal/openapi/sidebar.d.ts +30 -0
- package/dist/internal/openapi/sidebar.d.ts.map +1 -0
- package/dist/internal/openapi/sidebar.js +67 -0
- package/dist/internal/openapi/sidebar.js.map +1 -0
- package/dist/internal/openapi/union.d.ts +36 -0
- package/dist/internal/openapi/union.d.ts.map +1 -0
- package/dist/internal/openapi/union.js +69 -0
- package/dist/internal/openapi/union.js.map +1 -0
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +20 -0
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/vite-plugins.d.ts +12 -0
- package/dist/internal/vite-plugins.d.ts.map +1 -1
- package/dist/internal/vite-plugins.js +102 -11
- package/dist/internal/vite-plugins.js.map +1 -1
- package/dist/react/Badge.d.ts +2 -3
- package/dist/react/Badge.d.ts.map +1 -1
- package/dist/react/Layout.client.d.ts.map +1 -1
- package/dist/react/Layout.client.js +2 -2
- package/dist/react/Layout.client.js.map +1 -1
- package/dist/react/OpenApi.d.ts +6 -0
- package/dist/react/OpenApi.d.ts.map +1 -0
- package/dist/react/OpenApi.js +6 -0
- package/dist/react/OpenApi.js.map +1 -0
- package/dist/react/internal/CodeToHtml.client.d.ts +53 -2
- package/dist/react/internal/CodeToHtml.client.d.ts.map +1 -1
- package/dist/react/internal/CodeToHtml.client.js +154 -21
- package/dist/react/internal/CodeToHtml.client.js.map +1 -1
- package/dist/react/internal/Sidebar.d.ts.map +1 -1
- package/dist/react/internal/Sidebar.js +99 -2
- package/dist/react/internal/Sidebar.js.map +1 -1
- package/dist/react/internal/openapi/CodeSample.client.d.ts +21 -0
- package/dist/react/internal/openapi/CodeSample.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/CodeSample.client.js +134 -0
- package/dist/react/internal/openapi/CodeSample.client.js.map +1 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts +17 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.js +18 -0
- package/dist/react/internal/openapi/CollapsibleChildren.client.js.map +1 -0
- package/dist/react/internal/openapi/Disclosure.client.d.ts +28 -0
- package/dist/react/internal/openapi/Disclosure.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/Disclosure.client.js +38 -0
- package/dist/react/internal/openapi/Disclosure.client.js.map +1 -0
- package/dist/react/internal/openapi/Endpoints.d.ts +26 -0
- package/dist/react/internal/openapi/Endpoints.d.ts.map +1 -0
- package/dist/react/internal/openapi/Endpoints.js +33 -0
- package/dist/react/internal/openapi/Endpoints.js.map +1 -0
- package/dist/react/internal/openapi/EndpointsView.d.ts +24 -0
- package/dist/react/internal/openapi/EndpointsView.d.ts.map +1 -0
- package/dist/react/internal/openapi/EndpointsView.js +26 -0
- package/dist/react/internal/openapi/EndpointsView.js.map +1 -0
- package/dist/react/internal/openapi/EnumValues.client.d.ts +14 -0
- package/dist/react/internal/openapi/EnumValues.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/EnumValues.client.js +20 -0
- package/dist/react/internal/openapi/EnumValues.client.js.map +1 -0
- package/dist/react/internal/openapi/HeadingAnchor.d.ts +15 -0
- package/dist/react/internal/openapi/HeadingAnchor.d.ts.map +1 -0
- package/dist/react/internal/openapi/HeadingAnchor.js +12 -0
- package/dist/react/internal/openapi/HeadingAnchor.js.map +1 -0
- package/dist/react/internal/openapi/OpenApiPage.d.ts +79 -0
- package/dist/react/internal/openapi/OpenApiPage.d.ts.map +1 -0
- package/dist/react/internal/openapi/OpenApiPage.js +72 -0
- package/dist/react/internal/openapi/OpenApiPage.js.map +1 -0
- package/dist/react/internal/openapi/Operation.d.ts +25 -0
- package/dist/react/internal/openapi/Operation.d.ts.map +1 -0
- package/dist/react/internal/openapi/Operation.js +101 -0
- package/dist/react/internal/openapi/Operation.js.map +1 -0
- package/dist/react/internal/openapi/Playground.client.d.ts +33 -0
- package/dist/react/internal/openapi/Playground.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/Playground.client.js +170 -0
- package/dist/react/internal/openapi/Playground.client.js.map +1 -0
- package/dist/react/internal/openapi/PropertyExample.client.d.ts +17 -0
- package/dist/react/internal/openapi/PropertyExample.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/PropertyExample.client.js +21 -0
- package/dist/react/internal/openapi/PropertyExample.client.js.map +1 -0
- package/dist/react/internal/openapi/Reference.d.ts +55 -0
- package/dist/react/internal/openapi/Reference.d.ts.map +1 -0
- package/dist/react/internal/openapi/Reference.js +42 -0
- package/dist/react/internal/openapi/Reference.js.map +1 -0
- package/dist/react/internal/openapi/Schema.d.ts +110 -0
- package/dist/react/internal/openapi/Schema.d.ts.map +1 -0
- package/dist/react/internal/openapi/Schema.js +239 -0
- package/dist/react/internal/openapi/Schema.js.map +1 -0
- package/dist/react/internal/openapi/SchemaUnion.client.d.ts +25 -0
- package/dist/react/internal/openapi/SchemaUnion.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/SchemaUnion.client.js +48 -0
- package/dist/react/internal/openapi/SchemaUnion.client.js.map +1 -0
- package/dist/react/internal/openapi/anchor-navigation.client.d.ts +47 -0
- package/dist/react/internal/openapi/anchor-navigation.client.d.ts.map +1 -0
- package/dist/react/internal/openapi/anchor-navigation.client.js +120 -0
- package/dist/react/internal/openapi/anchor-navigation.client.js.map +1 -0
- package/dist/react/internal/openapi/auth.d.ts +28 -0
- package/dist/react/internal/openapi/auth.d.ts.map +1 -0
- package/dist/react/internal/openapi/auth.js +75 -0
- package/dist/react/internal/openapi/auth.js.map +1 -0
- package/dist/react/useLayout.d.ts +2 -0
- package/dist/react/useLayout.d.ts.map +1 -1
- package/dist/react/useLayout.js +2 -0
- package/dist/react/useLayout.js.map +1 -1
- package/dist/server/handlers.d.ts +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/handlers.js +26 -5
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/og-assets.d.ts +5 -0
- package/dist/server/og-assets.d.ts.map +1 -0
- package/dist/server/og-assets.js +11 -0
- package/dist/server/og-assets.js.map +1 -0
- package/dist/server/openapi/assets.d.ts +33 -0
- package/dist/server/openapi/assets.d.ts.map +1 -0
- package/dist/server/openapi/assets.generated.d.ts +9 -0
- package/dist/server/openapi/assets.generated.d.ts.map +1 -0
- package/dist/server/openapi/assets.generated.js +1091 -0
- package/dist/server/openapi/assets.generated.js.map +1 -0
- package/dist/server/openapi/assets.js +32 -0
- package/dist/server/openapi/assets.js.map +1 -0
- package/dist/server/openapi/handler.d.ts +103 -0
- package/dist/server/openapi/handler.d.ts.map +1 -0
- package/dist/server/openapi/handler.js +198 -0
- package/dist/server/openapi/handler.js.map +1 -0
- package/dist/server/openapi/handler.test.d.ts +2 -0
- package/dist/server/openapi/handler.test.d.ts.map +1 -0
- package/dist/server/openapi/handler.test.js +203 -0
- package/dist/server/openapi/handler.test.js.map +1 -0
- package/dist/server/openapi/html.d.ts +16 -0
- package/dist/server/openapi/html.d.ts.map +1 -0
- package/dist/server/openapi/html.js +75 -0
- package/dist/server/openapi/html.js.map +1 -0
- package/dist/server/openapi/pages.d.ts +33 -0
- package/dist/server/openapi/pages.d.ts.map +1 -0
- package/dist/server/openapi/pages.js +130 -0
- package/dist/server/openapi/pages.js.map +1 -0
- package/dist/server/openapi/pages.test.d.ts +2 -0
- package/dist/server/openapi/pages.test.d.ts.map +1 -0
- package/dist/server/openapi/pages.test.js +94 -0
- package/dist/server/openapi/pages.test.js.map +1 -0
- package/dist/server/openapi/state.d.ts +42 -0
- package/dist/server/openapi/state.d.ts.map +1 -0
- package/dist/server/openapi/state.js +101 -0
- package/dist/server/openapi/state.js.map +1 -0
- package/dist/styles/index.css +16 -0
- package/dist/styles/markdown.css +9 -7
- package/dist/styles/openapi-playground.css +80 -0
- package/dist/styles/openapi.css +660 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +1 -0
- package/dist/vite.js.map +1 -1
- package/dist/waku/internal/middleware/md-router.d.ts +0 -4
- package/dist/waku/internal/middleware/md-router.d.ts.map +1 -1
- package/dist/waku/internal/middleware/md-router.js +3 -48
- package/dist/waku/internal/middleware/md-router.js.map +1 -1
- package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js +1 -1
- package/dist/waku/internal/patches/adapters/vercel-build-enhancer.js.map +1 -1
- package/dist/waku/internal/patches/router.d.ts.map +1 -1
- package/dist/waku/internal/patches/router.js +114 -1
- package/dist/waku/internal/patches/router.js.map +1 -1
- package/package.json +5 -1
- package/src/config.ts +1 -0
- package/src/globals.d.ts +16 -0
- package/src/index.ts +1 -0
- package/src/internal/config.ts +40 -1
- package/src/internal/llms.ts +51 -1
- package/src/internal/markdown-negotiation.test.ts +42 -0
- package/src/internal/markdown-negotiation.ts +95 -0
- package/src/internal/markdown.ts +5 -0
- package/src/internal/openapi/anchors.ts +44 -0
- package/src/internal/openapi/app.ts +127 -0
- package/src/internal/openapi/index.ts +24 -0
- package/src/internal/openapi/markdown.test.ts +115 -0
- package/src/internal/openapi/markdown.ts +275 -0
- package/src/internal/openapi/openapi.ts +212 -0
- package/src/internal/openapi/openrpc.test.ts +239 -0
- package/src/internal/openapi/openrpc.ts +295 -0
- package/src/internal/openapi/parser.test.ts +203 -0
- package/src/internal/openapi/parser.ts +613 -0
- package/src/internal/openapi/registry.test.ts +89 -0
- package/src/internal/openapi/registry.ts +89 -0
- package/src/internal/openapi/sample.test.ts +283 -0
- package/src/internal/openapi/sample.ts +562 -0
- package/src/internal/openapi/search.test.ts +62 -0
- package/src/internal/openapi/search.ts +108 -0
- package/src/internal/openapi/sidebar.test.ts +131 -0
- package/src/internal/openapi/sidebar.ts +94 -0
- package/src/internal/openapi/union.test.ts +51 -0
- package/src/internal/openapi/union.ts +74 -0
- package/src/internal/search.ts +20 -0
- package/src/internal/test/virtual-config.stub.ts +14 -0
- package/src/internal/vite-plugins.ts +106 -11
- package/src/openapi-app/App.tsx +64 -0
- package/src/openapi-app/blocks.tsx +33 -0
- package/src/openapi-app/client.tsx +25 -0
- package/src/openapi-app/links.test.ts +84 -0
- package/src/openapi-app/links.ts +66 -0
- package/src/openapi-app/payload.ts +20 -0
- package/src/openapi-app/virtual/config.ts +7 -0
- package/src/openapi-app/virtual/group-icons.ts +2 -0
- package/src/openapi-app/virtual/langs.ts +6 -0
- package/src/openapi-app/virtual/openapi.ts +10 -0
- package/src/openapi-app/virtual/search-index.ts +21 -0
- package/src/openapi-app/virtual/slots.ts +4 -0
- package/src/openapi-app/virtual/user-styles.ts +2 -0
- package/src/openapi-app/waku.tsx +154 -0
- package/src/react/Badge.tsx +2 -3
- package/src/react/Layout.client.tsx +17 -4
- package/src/react/OpenApi.tsx +5 -0
- package/src/react/internal/CodeToHtml.client.tsx +283 -22
- package/src/react/internal/Sidebar.tsx +126 -22
- package/src/react/internal/openapi/CodeSample.client.tsx +294 -0
- package/src/react/internal/openapi/CollapsibleChildren.client.tsx +41 -0
- package/src/react/internal/openapi/Disclosure.client.tsx +67 -0
- package/src/react/internal/openapi/Endpoints.tsx +58 -0
- package/src/react/internal/openapi/EndpointsView.tsx +76 -0
- package/src/react/internal/openapi/EnumValues.client.tsx +49 -0
- package/src/react/internal/openapi/HeadingAnchor.tsx +28 -0
- package/src/react/internal/openapi/OpenApiPage.tsx +173 -0
- package/src/react/internal/openapi/Operation.test.tsx +101 -0
- package/src/react/internal/openapi/Operation.tsx +335 -0
- package/src/react/internal/openapi/Playground.client.tsx +234 -0
- package/src/react/internal/openapi/PropertyExample.client.tsx +55 -0
- package/src/react/internal/openapi/Reference.tsx +120 -0
- package/src/react/internal/openapi/Schema.tsx +467 -0
- package/src/react/internal/openapi/SchemaUnion.client.tsx +123 -0
- package/src/react/internal/openapi/anchor-navigation.client.ts +154 -0
- package/src/react/internal/openapi/auth.ts +69 -0
- package/src/react/useLayout.ts +4 -0
- package/src/server/handlers.ts +31 -6
- package/src/server/og-assets.ts +14 -0
- package/src/server/openapi/assets.generated.ts +1093 -0
- package/src/server/openapi/assets.ts +57 -0
- package/src/server/openapi/handler.test.ts +244 -0
- package/src/server/openapi/handler.ts +277 -0
- package/src/server/openapi/html.ts +84 -0
- package/src/server/openapi/pages.test.ts +111 -0
- package/src/server/openapi/pages.ts +153 -0
- package/src/server/openapi/state.ts +136 -0
- package/src/styles/index.css +16 -0
- package/src/styles/markdown.css +9 -7
- package/src/styles/openapi-playground.css +80 -0
- package/src/styles/openapi.css +660 -0
- package/src/vite.ts +1 -0
- package/src/waku/internal/middleware/md-router.ts +8 -52
- package/src/waku/internal/patches/adapters/vercel-build-enhancer.ts +1 -1
- 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 {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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)
|
|
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: (
|
|
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
|
|
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(
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
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: [
|
|
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(
|
|
141
|
-
|
|
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(
|
|
146
|
-
|
|
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(
|
|
151
|
-
|
|
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(
|
|
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
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
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 (
|