boltdocs 2.6.1 → 2.7.0
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/bin/boltdocs.js +0 -1
- package/dist/cache-CQKlT4fI.mjs +6 -0
- package/dist/cache-DorPMFgW.cjs +6 -0
- package/dist/cards-BLoSiRuL.d.ts +30 -0
- package/dist/cards-CQn9mXZS.d.cts +30 -0
- package/dist/chunk-Ds5LZdWN.cjs +6 -0
- package/dist/client/index.cjs +1 -1
- package/dist/client/index.d.cts +173 -1328
- package/dist/client/index.d.ts +172 -1327
- package/dist/client/index.js +1 -1
- package/dist/{package-c99Cs7mD.cjs → client/mdx.cjs} +1 -1
- package/dist/client/mdx.d.cts +128 -0
- package/dist/client/mdx.d.ts +129 -0
- package/dist/client/mdx.js +6 -0
- package/dist/client/primitives.cjs +6 -0
- package/dist/client/primitives.d.cts +818 -0
- package/dist/client/primitives.d.ts +818 -0
- package/dist/client/primitives.js +6 -0
- package/dist/client/theme/neutral.css +74 -361
- package/dist/client/theme/reset.css +189 -0
- package/dist/docs-layout-BlDhcQRv.cjs +6 -0
- package/dist/docs-layout-BvAOWEJw.js +6 -0
- package/dist/doctor-BQiQhCTl.cjs +6 -0
- package/dist/doctor-COpf35L2.cjs +20 -0
- package/dist/doctor-Dh1XP7Pz.mjs +20 -0
- package/dist/generator-DGW6pkCC.cjs +22 -0
- package/dist/generator-Dv3wEmhZ.mjs +22 -0
- package/dist/icons-dev-CrQLjoQp.js +6 -0
- package/dist/icons-dev-rzdz6Lf3.cjs +6 -0
- package/dist/image-BkIfa9oo.js +6 -0
- package/dist/image-DIGjCPe6.cjs +6 -0
- package/dist/mdx-K0WYBAJ3.js +7 -0
- package/dist/mdx-hpErbRUe.cjs +7 -0
- package/dist/meta-loader-0gJ4PtBC.cjs +6 -0
- package/dist/meta-loader-9IpAHWDS.mjs +6 -0
- package/dist/node/cli-entry.cjs +1 -2
- package/dist/node/cli-entry.mjs +1 -2
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +66 -13
- package/dist/node/index.d.mts +66 -14
- package/dist/node/index.mjs +1 -1
- package/dist/node/routes/worker.cjs +6 -0
- package/dist/node/routes/worker.d.cts +2 -0
- package/dist/node/routes/worker.d.mts +2 -0
- package/dist/node/routes/worker.mjs +6 -0
- package/dist/node-C2nWXElP.mjs +112 -0
- package/dist/node-CinkUtxV.cjs +112 -0
- package/dist/package-BMYLDBBP.cjs +6 -0
- package/dist/{package-DukYeKmD.mjs → package-HegMOTL_.mjs} +1 -1
- package/dist/parser-Bh11BsdA.cjs +6 -0
- package/dist/parser-D8eQvE7N.mjs +6 -0
- package/dist/parser-DYRzXWmA.cjs +6 -0
- package/dist/routes-CHf76Ye4.cjs +6 -0
- package/dist/routes-CMUZGI6T.mjs +6 -0
- package/dist/routes-Co1mRM58.cjs +6 -0
- package/dist/search-dialog-BACuzoVX.cjs +6 -0
- package/dist/search-dialog-BKagVT17.js +6 -0
- package/dist/search-dialog-C8w12eUx.js +6 -0
- package/dist/search-dialog-CGyrozZE.cjs +6 -0
- package/dist/search-dialog-D26rUnJ_.cjs +6 -0
- package/dist/sidebar-DKvg6KOc.d.cts +491 -0
- package/dist/sidebar-Dr1TiRIy.d.ts +491 -0
- package/dist/utils-BxNAXhZZ.mjs +7 -0
- package/dist/utils-Clzu7jvb.cjs +7 -0
- package/dist/worker-pool-Bd8Y9KDv.mjs +6 -0
- package/dist/worker-pool-BwU8ckrg.cjs +6 -0
- package/package.json +27 -8
- package/src/client/app/doc-page.tsx +9 -5
- package/src/client/app/docs-layout.tsx +17 -3
- package/src/client/app/head.tsx +122 -0
- package/src/client/app/helmet-compat.tsx +36 -0
- package/src/client/app/mdx-component.tsx +5 -52
- package/src/client/app/mdx-components-context.tsx +32 -8
- package/src/client/app/routes-context.tsx +2 -2
- package/src/client/app/scroll-handler.tsx +1 -1
- package/src/client/app/theme-context.tsx +5 -5
- package/src/client/app/ui-context.tsx +42 -0
- package/src/client/components/docs-layout-default.tsx +85 -0
- package/src/client/components/icons-dev.tsx +38 -15
- package/src/client/components/mdx/callout.tsx +97 -0
- package/src/client/components/mdx/card.tsx +73 -98
- package/src/client/components/mdx/cards.tsx +27 -0
- package/src/client/components/mdx/code-block.tsx +37 -17
- package/src/client/components/mdx/field.tsx +24 -56
- package/src/client/components/mdx/image.tsx +36 -15
- package/src/client/components/mdx/index.ts +19 -53
- package/src/client/components/mdx/table.tsx +46 -148
- package/src/client/components/mdx/typographics.tsx +120 -0
- package/src/client/components/mdx/{hooks/use-code-block.ts → use-code-block.ts} +5 -7
- package/src/client/components/primitives/breadcrumbs.tsx +5 -24
- package/src/client/components/primitives/button.tsx +3 -142
- package/src/client/components/primitives/code-block.tsx +104 -97
- package/src/client/components/{docs-layout.tsx → primitives/docs-layout.tsx} +15 -24
- package/src/client/components/primitives/error-boundary.tsx +107 -0
- package/src/client/components/primitives/heading.tsx +128 -0
- package/src/client/components/primitives/helpers/observer.ts +62 -32
- package/src/client/components/primitives/image.tsx +26 -0
- package/src/client/components/primitives/link.tsx +50 -52
- package/src/client/components/primitives/menu.tsx +25 -49
- package/src/client/components/primitives/navbar.tsx +234 -59
- package/src/client/components/primitives/on-this-page.tsx +169 -40
- package/src/client/components/primitives/page-nav.tsx +11 -39
- package/src/client/components/primitives/popover.tsx +12 -30
- package/src/client/components/primitives/search-dialog.tsx +77 -71
- package/src/client/components/primitives/sidebar.tsx +312 -119
- package/src/client/components/primitives/skeleton.tsx +1 -1
- package/src/client/components/primitives/tabs.tsx +5 -16
- package/src/client/components/primitives/tooltip.tsx +1 -1
- package/src/client/components/ui-base/banner.tsx +66 -0
- package/src/client/components/ui-base/breadcrumbs.tsx +26 -20
- package/src/client/components/ui-base/copy-markdown.tsx +43 -35
- package/src/client/components/ui-base/error-boundary.tsx +9 -46
- package/src/client/components/ui-base/github-stars.tsx +5 -3
- package/src/client/components/ui-base/index.ts +3 -3
- package/src/client/components/ui-base/last-updated.tsx +27 -0
- package/src/client/components/ui-base/navbar.tsx +183 -89
- package/src/client/components/ui-base/not-found.tsx +11 -9
- package/src/client/components/ui-base/on-this-page.tsx +8 -104
- package/src/client/components/ui-base/page-nav.tsx +23 -9
- package/src/client/components/ui-base/search-dialog.tsx +111 -36
- package/src/client/components/ui-base/search-highlight.tsx +10 -0
- package/src/client/components/ui-base/sidebar.tsx +77 -154
- package/src/client/components/ui-base/tabs.tsx +20 -7
- package/src/client/components/ui-base/theme-toggle.tsx +88 -10
- package/src/client/components/ui-base/version-i18n.tsx +80 -0
- package/src/client/hooks/index.ts +2 -1
- package/src/client/hooks/use-analytics.ts +272 -0
- package/src/client/hooks/use-i18n.ts +120 -53
- package/src/client/hooks/use-localized-to.ts +70 -30
- package/src/client/hooks/use-navbar.ts +69 -39
- package/src/client/hooks/use-page-nav.ts +28 -25
- package/src/client/hooks/use-routes.ts +64 -81
- package/src/client/hooks/use-search-highlight.ts +185 -0
- package/src/client/hooks/use-search.ts +12 -3
- package/src/client/hooks/use-sidebar.ts +183 -77
- package/src/client/hooks/use-tabs.ts +3 -4
- package/src/client/hooks/use-version.ts +46 -18
- package/src/client/index.ts +13 -86
- package/src/client/mdx.ts +2 -0
- package/src/client/primitives.ts +19 -0
- package/src/client/ssg/boltdocs-shell.tsx +78 -57
- package/src/client/ssg/create-routes.tsx +290 -50
- package/src/client/ssg/mdx-page.tsx +2 -1
- package/src/client/store/boltdocs-context.tsx +83 -12
- package/src/client/theme/neutral.css +74 -361
- package/src/client/theme/reset.css +189 -0
- package/src/client/types.ts +10 -2
- package/src/client/utils/path.ts +9 -0
- package/src/client/utils/react-to-text.ts +24 -24
- package/src/client/virtual.d.ts +1 -1
- package/src/shared/types.ts +97 -21
- package/dist/node-CWN8U_p8.mjs +0 -88
- package/dist/node-D5iosYXv.cjs +0 -88
- package/dist/search-dialog-3lvKsbVG.js +0 -6
- package/dist/search-dialog-DMK5OpgH.cjs +0 -6
- package/dist/use-search-C9bxCqfF.js +0 -6
- package/dist/use-search-DcfZSunO.cjs +0 -6
- package/src/client/components/mdx/admonition.tsx +0 -91
- package/src/client/components/mdx/badge.tsx +0 -41
- package/src/client/components/mdx/button.tsx +0 -35
- package/src/client/components/mdx/component-preview.tsx +0 -37
- package/src/client/components/mdx/component-props.tsx +0 -83
- package/src/client/components/mdx/file-tree.tsx +0 -325
- package/src/client/components/mdx/hooks/use-component-preview.ts +0 -16
- package/src/client/components/mdx/hooks/useTable.ts +0 -74
- package/src/client/components/mdx/hooks/useTabs.ts +0 -68
- package/src/client/components/mdx/link.tsx +0 -38
- package/src/client/components/mdx/list.tsx +0 -192
- package/src/client/components/mdx/tabs.tsx +0 -135
- package/src/client/components/mdx/video.tsx +0 -68
- package/src/client/components/primitives/index.ts +0 -19
- package/src/client/components/primitives/navigation-menu.tsx +0 -114
- package/src/client/components/ui-base/head.tsx +0 -76
- package/src/client/components/ui-base/loading.tsx +0 -57
- package/src/client/components/ui-base/powered-by.tsx +0 -25
- package/src/client/hooks/use-onthispage.ts +0 -23
- package/src/client/utils/use-on-change.ts +0 -15
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
|
+
import { useLocation } from './use-location'
|
|
3
|
+
import type { BoltdocsIntegrationsConfig } from '../../shared/types'
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
gtag?: (...args: unknown[]) => void
|
|
8
|
+
dataLayer?: unknown[]
|
|
9
|
+
gtag_report_conversion?: (url?: string) => boolean
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AnalyticsEvent {
|
|
14
|
+
action: string
|
|
15
|
+
category?: string
|
|
16
|
+
label?: string
|
|
17
|
+
value?: number
|
|
18
|
+
params?: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AnalyticsInstance {
|
|
22
|
+
trackPageView: (path: string, title?: string) => void
|
|
23
|
+
trackEvent: (event: AnalyticsEvent) => void
|
|
24
|
+
trackSearch: (query: string, resultsCount?: number) => void
|
|
25
|
+
trackDownload: (file: string, type?: string) => void
|
|
26
|
+
trackExternalLink: (url: string) => void
|
|
27
|
+
isEnabled: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createAnalyticsInstance(
|
|
31
|
+
config?: BoltdocsIntegrationsConfig,
|
|
32
|
+
): AnalyticsInstance {
|
|
33
|
+
if (typeof window === 'undefined') {
|
|
34
|
+
return createDisabledAnalytics()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isGtagAvailable = typeof window.gtag === 'function'
|
|
38
|
+
|
|
39
|
+
if (isGtagAvailable) {
|
|
40
|
+
return createGtagAnalytics(config)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (window.dataLayer) {
|
|
44
|
+
return createDataLayerAnalytics(config)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return createDisabledAnalytics()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createGtagAnalytics(
|
|
51
|
+
config?: BoltdocsIntegrationsConfig,
|
|
52
|
+
): AnalyticsInstance {
|
|
53
|
+
return {
|
|
54
|
+
trackPageView: (path: string, title?: string) => {
|
|
55
|
+
window.gtag?.('event', 'page_view', {
|
|
56
|
+
page_path: path,
|
|
57
|
+
page_title: title || document.title,
|
|
58
|
+
send_to: config?.ga4?.measurementId,
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
trackEvent: ({ action, category, label, value, params }) => {
|
|
62
|
+
window.gtag?.('event', action, {
|
|
63
|
+
event_category: category,
|
|
64
|
+
event_label: label,
|
|
65
|
+
value,
|
|
66
|
+
send_to: config?.ga4?.measurementId,
|
|
67
|
+
...params,
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
trackSearch: (query: string, resultsCount?: number) => {
|
|
71
|
+
window.gtag?.('event', 'search', {
|
|
72
|
+
search_term: query,
|
|
73
|
+
results_count: resultsCount,
|
|
74
|
+
send_to: config?.ga4?.measurementId,
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
trackDownload: (file: string, type?: string) => {
|
|
78
|
+
window.gtag?.('event', 'file_download', {
|
|
79
|
+
file_name: file,
|
|
80
|
+
file_type: type || file.split('.').pop(),
|
|
81
|
+
send_to: config?.ga4?.measurementId,
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
trackExternalLink: (url: string) => {
|
|
85
|
+
window.gtag?.('event', 'external_link', {
|
|
86
|
+
link_url: url,
|
|
87
|
+
send_to: config?.ga4?.measurementId,
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
isEnabled: true,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createDataLayerAnalytics(
|
|
95
|
+
config?: BoltdocsIntegrationsConfig,
|
|
96
|
+
): AnalyticsInstance {
|
|
97
|
+
return {
|
|
98
|
+
trackPageView: (path: string, title?: string) => {
|
|
99
|
+
window.dataLayer?.push({
|
|
100
|
+
event: 'page_view',
|
|
101
|
+
page_path: path,
|
|
102
|
+
page_title: title || document.title,
|
|
103
|
+
send_to: config?.gtm?.tagId,
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
trackEvent: ({ action, category, label, value, params }) => {
|
|
107
|
+
window.dataLayer?.push({
|
|
108
|
+
event: action,
|
|
109
|
+
event_category: category,
|
|
110
|
+
event_label: label,
|
|
111
|
+
value,
|
|
112
|
+
send_to: config?.gtm?.tagId,
|
|
113
|
+
...params,
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
trackSearch: (query: string, resultsCount?: number) => {
|
|
117
|
+
window.dataLayer?.push({
|
|
118
|
+
event: 'search',
|
|
119
|
+
search_term: query,
|
|
120
|
+
results_count: resultsCount,
|
|
121
|
+
send_to: config?.gtm?.tagId,
|
|
122
|
+
})
|
|
123
|
+
},
|
|
124
|
+
trackDownload: (file: string, type?: string) => {
|
|
125
|
+
window.dataLayer?.push({
|
|
126
|
+
event: 'file_download',
|
|
127
|
+
file_name: file,
|
|
128
|
+
file_type: type || file.split('.').pop(),
|
|
129
|
+
send_to: config?.gtm?.tagId,
|
|
130
|
+
})
|
|
131
|
+
},
|
|
132
|
+
trackExternalLink: (url: string) => {
|
|
133
|
+
window.dataLayer?.push({
|
|
134
|
+
event: 'external_link',
|
|
135
|
+
link_url: url,
|
|
136
|
+
send_to: config?.gtm?.tagId,
|
|
137
|
+
})
|
|
138
|
+
},
|
|
139
|
+
isEnabled: true,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createDisabledAnalytics(): AnalyticsInstance {
|
|
144
|
+
return {
|
|
145
|
+
trackPageView: () => {},
|
|
146
|
+
trackEvent: () => {},
|
|
147
|
+
trackSearch: () => {},
|
|
148
|
+
trackDownload: () => {},
|
|
149
|
+
trackExternalLink: () => {},
|
|
150
|
+
isEnabled: false,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface UseAnalyticsOptions {
|
|
155
|
+
config?: BoltdocsIntegrationsConfig
|
|
156
|
+
autoTrackPageViews?: boolean
|
|
157
|
+
autoTrackDownloads?: boolean
|
|
158
|
+
autoTrackExternalLinks?: boolean
|
|
159
|
+
excludePatterns?: RegExp[]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const CONFIG_INSTANCE_SYMBOL = Symbol.for('__BDOCS_CONFIG_INSTANCE__')
|
|
163
|
+
|
|
164
|
+
export function useAnalytics(options: UseAnalyticsOptions = {}) {
|
|
165
|
+
const {
|
|
166
|
+
config: optionsConfig,
|
|
167
|
+
autoTrackPageViews = true,
|
|
168
|
+
autoTrackDownloads = true,
|
|
169
|
+
autoTrackExternalLinks = true,
|
|
170
|
+
excludePatterns = [],
|
|
171
|
+
} = options
|
|
172
|
+
|
|
173
|
+
const globalConfig =
|
|
174
|
+
typeof globalThis !== 'undefined'
|
|
175
|
+
? ((globalThis as any)[CONFIG_INSTANCE_SYMBOL] as
|
|
176
|
+
| { integrations?: BoltdocsIntegrationsConfig }
|
|
177
|
+
| undefined)
|
|
178
|
+
: undefined
|
|
179
|
+
const config = optionsConfig ?? globalConfig?.integrations
|
|
180
|
+
|
|
181
|
+
const analytics = useMemo(() => createAnalyticsInstance(config), [config])
|
|
182
|
+
|
|
183
|
+
const previousPath = useRef<string>('')
|
|
184
|
+
const location = useLocation()
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (!autoTrackPageViews || !analytics.isEnabled) return
|
|
188
|
+
|
|
189
|
+
const path = location.pathname + location.search
|
|
190
|
+
|
|
191
|
+
if (path !== previousPath.current) {
|
|
192
|
+
previousPath.current = path
|
|
193
|
+
analytics.trackPageView(path, document.title)
|
|
194
|
+
}
|
|
195
|
+
}, [location.pathname, autoTrackPageViews, analytics])
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!autoTrackDownloads || !analytics.isEnabled) return
|
|
199
|
+
|
|
200
|
+
const handleClick = (event: MouseEvent) => {
|
|
201
|
+
const target = (event.target as Element)?.closest('a')
|
|
202
|
+
if (!target) return
|
|
203
|
+
|
|
204
|
+
const href = target.getAttribute('href')
|
|
205
|
+
if (!href) return
|
|
206
|
+
|
|
207
|
+
if (excludePatterns.some((pattern) => pattern.test(href))) return
|
|
208
|
+
|
|
209
|
+
const isDownload =
|
|
210
|
+
target.hasAttribute('download') ||
|
|
211
|
+
/\.(pdf|doc|docx|xls|xlsx|ppt|pptx|zip|rar|7z|tar|gz|mp3|mp4|avi|mov|png|jpg|jpeg|gif|svg|webp)$/i.test(
|
|
212
|
+
href,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if (isDownload) {
|
|
216
|
+
const fileName = href.split('/').pop() || href
|
|
217
|
+
analytics.trackDownload(fileName, fileName.split('.').pop())
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
document.addEventListener('click', handleClick)
|
|
222
|
+
return () => document.removeEventListener('click', handleClick)
|
|
223
|
+
}, [autoTrackDownloads, autoTrackExternalLinks, analytics, excludePatterns])
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (!autoTrackExternalLinks || !analytics.isEnabled) return
|
|
227
|
+
|
|
228
|
+
const handleClick = (event: MouseEvent) => {
|
|
229
|
+
const target = (event.target as Element)?.closest('a')
|
|
230
|
+
if (!target) return
|
|
231
|
+
|
|
232
|
+
const href = target.getAttribute('href')
|
|
233
|
+
if (!href) return
|
|
234
|
+
|
|
235
|
+
if (excludePatterns.some((pattern) => pattern.test(href))) return
|
|
236
|
+
|
|
237
|
+
const isExternal =
|
|
238
|
+
href.startsWith('http://') ||
|
|
239
|
+
href.startsWith('https://') ||
|
|
240
|
+
href.startsWith('//')
|
|
241
|
+
|
|
242
|
+
if (isExternal && !href.includes(window.location.hostname)) {
|
|
243
|
+
analytics.trackExternalLink(href)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
document.addEventListener('click', handleClick)
|
|
248
|
+
return () => document.removeEventListener('click', handleClick)
|
|
249
|
+
}, [autoTrackExternalLinks, analytics, excludePatterns])
|
|
250
|
+
|
|
251
|
+
return analytics
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function useTrackPageView() {
|
|
255
|
+
const analytics = useMemo(() => createAnalyticsInstance(), [])
|
|
256
|
+
return useCallback(
|
|
257
|
+
(path: string, title?: string) => {
|
|
258
|
+
analytics.trackPageView(path, title)
|
|
259
|
+
},
|
|
260
|
+
[analytics],
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function useTrackEvent() {
|
|
265
|
+
const analytics = useMemo(() => createAnalyticsInstance(), [])
|
|
266
|
+
return useCallback(
|
|
267
|
+
(event: AnalyticsEvent) => {
|
|
268
|
+
analytics.trackEvent(event)
|
|
269
|
+
},
|
|
270
|
+
[analytics],
|
|
271
|
+
)
|
|
272
|
+
}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
1
2
|
import { useNavigate } from 'react-router-dom'
|
|
2
3
|
import { getBaseFilePath } from '../utils/get-base-file-path'
|
|
3
4
|
import { useRoutes } from './use-routes'
|
|
5
|
+
import { useConfig } from '../app/config-context'
|
|
4
6
|
import { useBoltdocsContext } from '../store/boltdocs-context'
|
|
7
|
+
import type { BoltdocsLocale } from '../../shared/types'
|
|
5
8
|
|
|
6
9
|
export interface LocaleOption {
|
|
7
|
-
key:
|
|
10
|
+
key: BoltdocsLocale
|
|
8
11
|
label: string
|
|
9
12
|
value: string
|
|
10
13
|
isCurrent: boolean
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export interface UseI18nReturn {
|
|
14
|
-
currentLocale:
|
|
17
|
+
currentLocale: BoltdocsLocale | undefined
|
|
15
18
|
currentLocaleLabel: string | undefined
|
|
16
19
|
availableLocales: LocaleOption[]
|
|
17
|
-
handleLocaleChange: (locale:
|
|
20
|
+
handleLocaleChange: (locale: BoltdocsLocale) => void
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -22,8 +25,8 @@ export interface UseI18nReturn {
|
|
|
22
25
|
*/
|
|
23
26
|
export function useI18n(): UseI18nReturn {
|
|
24
27
|
const navigate = useNavigate()
|
|
25
|
-
const
|
|
26
|
-
const { allRoutes, currentRoute, currentLocale,
|
|
28
|
+
const config = useConfig()
|
|
29
|
+
const { allRoutes, currentRoute, currentLocale, currentVersion } = useRoutes()
|
|
27
30
|
const i18n = config.i18n
|
|
28
31
|
const { setLocale } = useBoltdocsContext()
|
|
29
32
|
|
|
@@ -33,78 +36,142 @@ export function useI18n(): UseI18nReturn {
|
|
|
33
36
|
// Update store
|
|
34
37
|
setLocale(locale)
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
const base = config.base || '/'
|
|
40
|
+
const safeBase = base === '/' ? '' : base.replace(/\/$/, '')
|
|
41
|
+
const isDocRoute = !!currentRoute?.filePath
|
|
42
|
+
|
|
43
|
+
let targetPath = ''
|
|
37
44
|
|
|
38
45
|
if (currentRoute) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
(r.locale || i18n.defaultLocale) === locale &&
|
|
48
|
-
r.version === currentRoute.version,
|
|
49
|
-
)
|
|
46
|
+
// Case A: We are on a known route. Determine if it is doc or external.
|
|
47
|
+
if (isDocRoute) {
|
|
48
|
+
// Documentation Context logic
|
|
49
|
+
const baseFile = getBaseFilePath(
|
|
50
|
+
currentRoute.filePath,
|
|
51
|
+
currentRoute.version,
|
|
52
|
+
currentRoute.locale,
|
|
53
|
+
)
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
targetPath = targetRoute.path
|
|
53
|
-
} else {
|
|
54
|
-
const defaultIndexRoute = allRoutes.find(
|
|
55
|
+
const targetRoute = allRoutes.find(
|
|
55
56
|
(r) =>
|
|
56
|
-
getBaseFilePath(r.filePath, r.version, r.locale) ===
|
|
57
|
+
getBaseFilePath(r.filePath, r.version, r.locale) === baseFile &&
|
|
57
58
|
(r.locale || i18n.defaultLocale) === locale &&
|
|
58
59
|
r.version === currentRoute.version,
|
|
59
60
|
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
|
|
62
|
+
if (targetRoute) {
|
|
63
|
+
targetPath = targetRoute.path
|
|
64
|
+
} else {
|
|
65
|
+
// Recovery: Find target index, or hardcode reconstruct using version space
|
|
66
|
+
const defaultIndexRoute = allRoutes.find(
|
|
67
|
+
(r) =>
|
|
68
|
+
getBaseFilePath(r.filePath, r.version, r.locale) === 'index.md' &&
|
|
69
|
+
(r.locale || i18n.defaultLocale) === locale &&
|
|
70
|
+
r.version === currentRoute.version,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if (defaultIndexRoute) {
|
|
74
|
+
targetPath = defaultIndexRoute.path
|
|
75
|
+
} else {
|
|
76
|
+
// Hardcoded absolute construction preserving existing version structure
|
|
77
|
+
const vPath = currentRoute.version ? `/${currentRoute.version}` : ''
|
|
78
|
+
const lPath = locale === i18n.defaultLocale ? '' : `/${locale}`
|
|
79
|
+
targetPath = `${safeBase}${vPath}${lPath}` || '/'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// External Context Logic: simply rewrite the current absolute path
|
|
84
|
+
// Extract pure relative component by stripping existing locale prefix
|
|
85
|
+
let rawExternal = currentRoute.path
|
|
86
|
+
|
|
87
|
+
// Strip existing locale if any
|
|
88
|
+
const parts = rawExternal.split('/').filter(Boolean)
|
|
89
|
+
if (
|
|
90
|
+
parts.length > 0 &&
|
|
91
|
+
(Array.isArray(i18n.locales)
|
|
92
|
+
? i18n.locales.includes(parts[0])
|
|
93
|
+
: !!i18n.locales[parts[0]])
|
|
94
|
+
) {
|
|
95
|
+
// Already prefixed external route like /es/about -> become /about
|
|
96
|
+
parts.shift()
|
|
97
|
+
rawExternal = '/' + parts.join('/')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Re-apply new locale
|
|
101
|
+
if (locale === i18n.defaultLocale) {
|
|
102
|
+
targetPath = rawExternal === '' ? '/' : rawExternal
|
|
103
|
+
} else {
|
|
104
|
+
const cleanExt = rawExternal.startsWith('/')
|
|
105
|
+
? rawExternal
|
|
106
|
+
: `/${rawExternal}`
|
|
107
|
+
targetPath = `/${locale}${cleanExt === '/' ? '' : cleanExt}`
|
|
108
|
+
}
|
|
69
109
|
}
|
|
70
110
|
} else {
|
|
71
|
-
// Fallback for
|
|
72
|
-
// Try to find
|
|
111
|
+
// Case B: Fallback for Unknown / 404 page
|
|
112
|
+
// Try to find first available page that matches the intended combo
|
|
73
113
|
const targetRoute = allRoutes.find(
|
|
74
114
|
(r) =>
|
|
75
|
-
(r.filePath === 'index.mdx' || r.filePath === 'index.md') &&
|
|
76
115
|
(r.locale || i18n.defaultLocale) === locale &&
|
|
77
|
-
|
|
116
|
+
(r.version || config.versions?.defaultVersion) ===
|
|
117
|
+
(currentVersion || config.versions?.defaultVersion),
|
|
78
118
|
)
|
|
79
119
|
|
|
80
120
|
if (targetRoute) {
|
|
81
121
|
targetPath = targetRoute.path
|
|
82
122
|
} else {
|
|
83
|
-
|
|
123
|
+
const vPath =
|
|
124
|
+
currentVersion && currentVersion !== config.versions?.defaultVersion
|
|
125
|
+
? `/${currentVersion}`
|
|
126
|
+
: ''
|
|
127
|
+
targetPath =
|
|
128
|
+
locale === i18n.defaultLocale
|
|
129
|
+
? `${safeBase}${vPath}`
|
|
130
|
+
: `${safeBase}${vPath}/${locale}`
|
|
84
131
|
}
|
|
85
132
|
}
|
|
86
133
|
|
|
134
|
+
// Final safety check: cleanup double slashes and empty targets
|
|
135
|
+
if (!targetPath || targetPath === '') targetPath = '/'
|
|
136
|
+
targetPath = targetPath.replace(/\/+/g, '/')
|
|
137
|
+
|
|
87
138
|
navigate(targetPath)
|
|
88
139
|
}
|
|
89
140
|
|
|
90
|
-
const
|
|
91
|
-
|
|
141
|
+
const locales = i18n?.locales
|
|
142
|
+
const defaultLabel = locales
|
|
143
|
+
? Array.isArray(locales)
|
|
144
|
+
? currentLocale
|
|
145
|
+
: (locales as Record<string, string>)[currentLocale as string]
|
|
146
|
+
: undefined
|
|
147
|
+
|
|
148
|
+
const currentLocaleConfig = i18n?.localeConfigs?.[currentLocale as string]
|
|
92
149
|
const currentLocaleLabel =
|
|
93
|
-
currentLocaleConfig?.label ||
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
currentLocaleConfig?.label || defaultLabel || currentLocale
|
|
151
|
+
|
|
152
|
+
const availableLocales = useMemo(() => {
|
|
153
|
+
return i18n
|
|
154
|
+
? Array.isArray(i18n.locales)
|
|
155
|
+
? i18n.locales.map((key) => {
|
|
156
|
+
const localeConfig = i18n?.localeConfigs?.[key]
|
|
157
|
+
return {
|
|
158
|
+
key: key as BoltdocsLocale,
|
|
159
|
+
label: localeConfig?.label || key,
|
|
160
|
+
value: key,
|
|
161
|
+
isCurrent: key === currentLocale,
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
: Object.entries(i18n.locales).map(([key, label]) => {
|
|
165
|
+
const localeConfig = i18n?.localeConfigs?.[key]
|
|
166
|
+
return {
|
|
167
|
+
key: key as BoltdocsLocale,
|
|
168
|
+
label: localeConfig?.label || label,
|
|
169
|
+
value: key,
|
|
170
|
+
isCurrent: key === currentLocale,
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
: []
|
|
174
|
+
}, [i18n, currentLocale])
|
|
108
175
|
|
|
109
176
|
return {
|
|
110
177
|
currentLocale,
|
|
@@ -6,30 +6,62 @@ import { useRoutes } from './use-routes'
|
|
|
6
6
|
* Hook to automatically localize a path based on the current version and locale context.
|
|
7
7
|
* It ensures that navigation preserves the active version and language across the entire site.
|
|
8
8
|
*/
|
|
9
|
-
export function useLocalizedTo(to:
|
|
9
|
+
export function useLocalizedTo(to: string): string
|
|
10
|
+
export function useLocalizedTo(to: RouterLinkProps['to']): RouterLinkProps['to']
|
|
11
|
+
export function useLocalizedTo(
|
|
12
|
+
to: RouterLinkProps['to'],
|
|
13
|
+
): RouterLinkProps['to'] {
|
|
10
14
|
const config = useConfig()
|
|
11
|
-
const {
|
|
12
|
-
|
|
15
|
+
const {
|
|
16
|
+
currentLocale: activeLocale,
|
|
17
|
+
currentVersion: activeVersion,
|
|
18
|
+
allRoutes,
|
|
19
|
+
} = useRoutes()
|
|
13
20
|
|
|
14
21
|
if (!config || typeof to !== 'string') return to
|
|
15
22
|
|
|
16
|
-
// External or
|
|
17
|
-
if (
|
|
23
|
+
// External, absolute, or anchor links don't need localization prefixing
|
|
24
|
+
if (
|
|
25
|
+
to.startsWith('http') ||
|
|
26
|
+
to.startsWith('//') ||
|
|
27
|
+
to.startsWith('#') ||
|
|
28
|
+
to.startsWith('site:')
|
|
29
|
+
) {
|
|
30
|
+
return to.replace('site:', '')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 0. Identify if the incoming path is explicitly registered as a known route
|
|
34
|
+
const [pathOnly, hashAndQuery] = to.split(/([?#].*)/s)
|
|
35
|
+
const normalizedTo =
|
|
36
|
+
pathOnly.endsWith('/') && pathOnly.length > 1
|
|
37
|
+
? pathOnly.slice(0, -1)
|
|
38
|
+
: pathOnly
|
|
39
|
+
|
|
40
|
+
const isKnownRoute = allRoutes?.some((r) => {
|
|
41
|
+
const rp =
|
|
42
|
+
r.path.endsWith('/') && r.path.length > 1 ? r.path.slice(0, -1) : r.path
|
|
43
|
+
return rp === (normalizedTo || '/')
|
|
44
|
+
})
|
|
18
45
|
|
|
19
46
|
const i18n = config.i18n
|
|
20
47
|
const versions = config.versions
|
|
48
|
+
const base = (config.base || '/docs').replace(/\/$/, '')
|
|
49
|
+
const baseSegment = base.startsWith('/') ? base.substring(1) : base
|
|
21
50
|
|
|
22
|
-
|
|
51
|
+
const rawParts = pathOnly.split('/').filter(Boolean)
|
|
23
52
|
|
|
24
|
-
//
|
|
25
|
-
|
|
53
|
+
// Classify: it's a Doc Path if it explicitly contains base segment,
|
|
54
|
+
// OR if it's an 'unknown' path (backward compatible fallback assumes unknown = doc).
|
|
55
|
+
const hasExplicitBase =
|
|
56
|
+
baseSegment && rawParts.length > 0 && rawParts[0] === baseSegment
|
|
57
|
+
const isDocsPath = hasExplicitBase || (!isKnownRoute && rawParts.length > 0)
|
|
26
58
|
|
|
27
59
|
// 3. Clean the 'to' path of ANY existing prefixes to avoid stacking
|
|
28
|
-
const parts =
|
|
60
|
+
const parts = [...rawParts]
|
|
29
61
|
let pIdx = 0
|
|
30
62
|
|
|
31
|
-
// Strip
|
|
32
|
-
if (parts[pIdx] ===
|
|
63
|
+
// Strip base segment if present at start
|
|
64
|
+
if (baseSegment && parts[pIdx] === baseSegment) pIdx++
|
|
33
65
|
|
|
34
66
|
// Strip versions if present
|
|
35
67
|
if (versions && parts.length > pIdx) {
|
|
@@ -38,36 +70,44 @@ export function useLocalizedTo(to: RouterLinkProps['to']) {
|
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
// Strip locales if present
|
|
41
|
-
|
|
73
|
+
const isLocale =
|
|
74
|
+
i18n &&
|
|
75
|
+
parts.length > pIdx &&
|
|
76
|
+
(Array.isArray(i18n.locales)
|
|
77
|
+
? i18n.locales.includes(parts[pIdx])
|
|
78
|
+
: parts[pIdx] in i18n.locales)
|
|
79
|
+
if (isLocale) pIdx++
|
|
42
80
|
|
|
43
81
|
// The actual relative route remaining
|
|
44
82
|
const routeContent = parts.slice(pIdx)
|
|
45
83
|
|
|
46
|
-
// 4. Reconstruct
|
|
84
|
+
// 4. Reconstruct dynamically based on context
|
|
47
85
|
const resultParts: string[] = []
|
|
48
86
|
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// Only prefix if it's NOT the default locale (cleaner URLs)
|
|
58
|
-
if (activeLocale !== i18n.defaultLocale) {
|
|
59
|
-
resultParts.push(activeLocale)
|
|
60
|
-
}
|
|
87
|
+
if (isDocsPath) {
|
|
88
|
+
// Reconstruct DOCS path: /base/version/locale/content
|
|
89
|
+
if (baseSegment) resultParts.push(baseSegment)
|
|
90
|
+
if (versions && activeVersion) resultParts.push(activeVersion)
|
|
91
|
+
if (i18n && activeLocale) resultParts.push(activeLocale)
|
|
92
|
+
} else {
|
|
93
|
+
// Reconstruct EXTERNAL path: /locale/content
|
|
94
|
+
if (i18n && activeLocale) resultParts.push(activeLocale)
|
|
61
95
|
}
|
|
62
96
|
|
|
63
97
|
resultParts.push(...routeContent)
|
|
64
98
|
|
|
65
|
-
|
|
99
|
+
let finalPath = `/${resultParts.join('/')}`
|
|
66
100
|
|
|
67
|
-
//
|
|
68
|
-
if (
|
|
69
|
-
|
|
101
|
+
// Preserve trailing slash if present in input and output isn't just root
|
|
102
|
+
if (
|
|
103
|
+
pathOnly.endsWith('/') &&
|
|
104
|
+
pathOnly.length > 1 &&
|
|
105
|
+
!finalPath.endsWith('/')
|
|
106
|
+
) {
|
|
107
|
+
finalPath += '/'
|
|
70
108
|
}
|
|
71
109
|
|
|
72
|
-
|
|
110
|
+
// Restore original query/hash
|
|
111
|
+
const result = (finalPath || '/') + (hashAndQuery || '')
|
|
112
|
+
return result
|
|
73
113
|
}
|