boltdocs 2.6.2 → 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 +167 -1338
- package/dist/client/index.d.ts +166 -1337
- package/dist/client/index.js +1 -1
- package/dist/{package-CFP44vfn.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 +55 -11
- package/dist/node/index.d.mts +55 -12
- 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-Bqbn1AYK.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 +116 -50
- package/src/client/hooks/use-localized-to.ts +70 -27
- 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 +63 -80
- 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 -80
- package/src/client/hooks/use-tabs.ts +3 -4
- package/src/client/hooks/use-version.ts +44 -29
- package/src/client/index.ts +13 -87
- package/src/client/mdx.ts +2 -0
- package/src/client/primitives.ts +19 -0
- package/src/client/ssg/boltdocs-shell.tsx +68 -79
- package/src/client/ssg/create-routes.tsx +268 -72
- package/src/client/ssg/mdx-page.tsx +2 -1
- package/src/client/store/boltdocs-context.tsx +72 -20
- 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 +82 -22
- package/dist/node-Bogvkxao.mjs +0 -101
- package/dist/node-CXaog6St.cjs +0 -101
- package/dist/search-dialog-CV3eJzMm.cjs +0 -6
- package/dist/search-dialog-DNTomKgu.js +0 -6
- package/dist/use-search-CS3gH19M.js +0 -6
- package/dist/use-search-DBpJZQuw.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 -83
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
1
2
|
import { useLocation } from 'react-router-dom'
|
|
2
3
|
import { useConfig } from '../app/config-context'
|
|
3
4
|
import { useTheme } from '../app/theme-context'
|
|
@@ -18,53 +19,82 @@ export function useNavbar() {
|
|
|
18
19
|
const githubRepo = themeConfig.githubRepo
|
|
19
20
|
|
|
20
21
|
// Transform links to the new NavbarLink structure
|
|
21
|
-
const links: NavbarLink[] =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
const links: NavbarLink[] = useMemo(() => {
|
|
23
|
+
return rawLinks.map((item: any) => {
|
|
24
|
+
const href = (item.href || item.to || item.link || '') as string
|
|
25
|
+
|
|
26
|
+
// Robust active state calculation
|
|
27
|
+
const getIsActive = (h: string) => {
|
|
28
|
+
const activePath = location.pathname
|
|
29
|
+
if (activePath === h) return true
|
|
30
|
+
if (!h || h === '/') return activePath === '/'
|
|
31
|
+
|
|
32
|
+
const cleanPathParts = (p: string) => {
|
|
33
|
+
const parts = p.split('/').filter(Boolean)
|
|
34
|
+
let i = 0
|
|
35
|
+
// Skip locale
|
|
36
|
+
if (
|
|
37
|
+
config.i18n?.locales &&
|
|
38
|
+
parts[i] &&
|
|
39
|
+
config.i18n.locales[parts[i]]
|
|
40
|
+
) {
|
|
40
41
|
i++
|
|
41
42
|
}
|
|
43
|
+
// Skip version
|
|
44
|
+
if (config.versions?.versions && parts[i]) {
|
|
45
|
+
if (config.versions.versions.some((v) => v.path === parts[i])) {
|
|
46
|
+
i++
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return parts.slice(i)
|
|
42
50
|
}
|
|
43
|
-
return parts.slice(i)
|
|
44
|
-
}
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
const hParts = cleanPathParts(h)
|
|
53
|
+
const pParts = cleanPathParts(activePath)
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
if (hParts.length === 0) return pParts.length === 0
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
// Must match at least as many parts as the candidate link
|
|
58
|
+
if (pParts.length < hParts.length) return false
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
// Every part of hParts must match pParts at the same position
|
|
61
|
+
return hParts.every((part, i) => pParts[i] === part)
|
|
62
|
+
}
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
// Process nested items recursively
|
|
65
|
+
const processItems = (items?: any[]): NavbarLink[] => {
|
|
66
|
+
if (!items || items.length === 0) return undefined as any
|
|
67
|
+
return items.map((subItem: any) => {
|
|
68
|
+
const subHref = (subItem.href ||
|
|
69
|
+
subItem.to ||
|
|
70
|
+
subItem.link ||
|
|
71
|
+
'') as string
|
|
72
|
+
return {
|
|
73
|
+
label: getTranslated(subItem.label || subItem.text, currentLocale),
|
|
74
|
+
href: subHref,
|
|
75
|
+
active: getIsActive(subHref),
|
|
76
|
+
to:
|
|
77
|
+
subHref.startsWith('http') || subHref.startsWith('//')
|
|
78
|
+
? 'external'
|
|
79
|
+
: undefined,
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const linkItems = processItems(item.items)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
label: getTranslated(item.label || item.text, currentLocale),
|
|
88
|
+
href,
|
|
89
|
+
active: getIsActive(href),
|
|
90
|
+
to:
|
|
91
|
+
href.startsWith('http') || href.startsWith('//')
|
|
92
|
+
? 'external'
|
|
93
|
+
: undefined,
|
|
94
|
+
items: linkItems,
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}, [rawLinks, location.pathname, currentLocale, config])
|
|
68
98
|
|
|
69
99
|
const logo = themeConfig.logo
|
|
70
100
|
// Use resolvedTheme so 'system' correctly maps to 'dark' or 'light'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
1
2
|
import { useLocation } from 'react-router-dom'
|
|
2
3
|
import { useRoutes } from './use-routes'
|
|
3
4
|
|
|
@@ -9,35 +10,37 @@ export function usePageNav() {
|
|
|
9
10
|
const { routes, currentRoute } = useRoutes()
|
|
10
11
|
const location = useLocation()
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
return useMemo(() => {
|
|
14
|
+
if (!currentRoute) {
|
|
15
|
+
return {
|
|
16
|
+
prevPage: null,
|
|
17
|
+
nextPage: null,
|
|
18
|
+
currentRoute: null,
|
|
19
|
+
}
|
|
17
20
|
}
|
|
18
|
-
}
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
const activeTabId = currentRoute.tab?.toLowerCase()
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// Subset of routes that match the current context (locale and version are already filtered by useRoutes)
|
|
25
|
+
// We further filter by tab to keep the user in the same logical section
|
|
26
|
+
const contextRoutes = activeTabId
|
|
27
|
+
? routes.filter((r) => r.tab?.toLowerCase() === activeTabId)
|
|
28
|
+
: routes.filter((r) => !r.tab)
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const currentIndex = contextRoutes.findIndex(
|
|
31
|
+
(r) => r.path === location.pathname,
|
|
32
|
+
)
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const prevPage = currentIndex > 0 ? contextRoutes[currentIndex - 1] : null
|
|
35
|
+
const nextPage =
|
|
36
|
+
currentIndex !== -1 && currentIndex < contextRoutes.length - 1
|
|
37
|
+
? contextRoutes[currentIndex + 1]
|
|
38
|
+
: null
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
return {
|
|
41
|
+
prevPage,
|
|
42
|
+
nextPage,
|
|
43
|
+
currentRoute,
|
|
44
|
+
}
|
|
45
|
+
}, [routes, currentRoute, location.pathname])
|
|
43
46
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
1
2
|
import { useLocation } from 'react-router-dom'
|
|
2
3
|
import { useConfig } from '../app/config-context'
|
|
3
4
|
import { useRoutesContext } from '../app/routes-context'
|
|
4
5
|
import { useBoltdocsContext } from '../store/boltdocs-context'
|
|
6
|
+
import { normalizePath } from '../utils/path'
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Hook to access the framework's routing state.
|
|
@@ -20,106 +22,87 @@ export function useRoutes() {
|
|
|
20
22
|
currentVersion: currentVersionStore,
|
|
21
23
|
} = useBoltdocsContext()
|
|
22
24
|
|
|
23
|
-
const
|
|
24
|
-
p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p
|
|
25
|
-
const currentPath = normalize(location.pathname)
|
|
25
|
+
const currentPath = normalizePath(location.pathname)
|
|
26
26
|
|
|
27
|
-
// Find the current route matching the pathname
|
|
27
|
+
// Find the current active route matching the pathname
|
|
28
28
|
const currentRoute = allRoutes?.find?.(
|
|
29
|
-
(r) =>
|
|
29
|
+
(r) => normalizePath(r.path) === currentPath,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
32
|
+
// 2. STRICT SOURCE OF TRUTH:
|
|
33
|
+
// Derive the active states exclusively from the hydrated Context Store.
|
|
34
|
+
// This ensures that user preference (LocalStorage) takes precedence over ambiguous URL fallbacks.
|
|
34
35
|
const currentLocale = config.i18n
|
|
35
|
-
?
|
|
36
|
-
(hasHydrated ? currentLocaleStore : undefined) ||
|
|
37
|
-
config.i18n.defaultLocale
|
|
36
|
+
? currentLocaleStore || config.i18n.defaultLocale
|
|
38
37
|
: undefined
|
|
39
38
|
|
|
40
39
|
const currentVersion = config.versions
|
|
41
|
-
?
|
|
42
|
-
(hasHydrated ? currentVersionStore : undefined) ||
|
|
43
|
-
config.versions.defaultVersion
|
|
40
|
+
? currentVersionStore || config.versions.defaultVersion
|
|
44
41
|
: undefined
|
|
45
42
|
|
|
46
43
|
// Filter routes to those matching the current version and locale
|
|
47
|
-
const routes =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const isCurrentRoutePrefixed = !!currentRoute?.locale
|
|
62
|
-
const isRoutePrefixed = !!r.locale
|
|
63
|
-
|
|
64
|
-
const hasAlternate = allRoutes?.some?.(
|
|
65
|
-
(alt) =>
|
|
66
|
-
alt !== r &&
|
|
67
|
-
alt.filePath === r.filePath &&
|
|
68
|
-
alt.version === r.version &&
|
|
69
|
-
(alt.locale || i18n.defaultLocale) ===
|
|
70
|
-
(r.locale || i18n.defaultLocale),
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
if (hasAlternate && isCurrentRoutePrefixed !== isRoutePrefixed) {
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
44
|
+
const routes = useMemo(() => {
|
|
45
|
+
if (!allRoutes) return []
|
|
46
|
+
|
|
47
|
+
// Pre-calculate alternate presence using a Map of maps or a composite key
|
|
48
|
+
// Key: filePath | (locale || defaultLocale) | (version || defaultVersion)
|
|
49
|
+
const alternateCounts = new Map<string, number>()
|
|
50
|
+
const defaultLocale = config.i18n?.defaultLocale || ''
|
|
51
|
+
const defaultVersion = config.versions?.defaultVersion || ''
|
|
52
|
+
|
|
53
|
+
for (const r of allRoutes) {
|
|
54
|
+
const locale = r.locale || defaultLocale
|
|
55
|
+
const version = r.version || defaultVersion
|
|
56
|
+
const key = `${r.filePath}::${locale}::${version}`
|
|
57
|
+
alternateCounts.set(key, (alternateCounts.get(key) || 0) + 1)
|
|
76
58
|
}
|
|
77
59
|
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
60
|
+
return allRoutes.filter((r) => {
|
|
61
|
+
const localeMatch = config.i18n
|
|
62
|
+
? (r.locale || config.i18n.defaultLocale) === currentLocale
|
|
63
|
+
: true
|
|
64
|
+
const versionMatch = config.versions
|
|
65
|
+
? (r.version || config.versions.defaultVersion) === currentVersion
|
|
66
|
+
: true
|
|
67
|
+
|
|
68
|
+
if (!(localeMatch && versionMatch)) return false
|
|
69
|
+
|
|
70
|
+
// Resolve duplicate paths (aliases) like /docs vs /docs/en
|
|
71
|
+
// 3. Resolve duplicate route aliases (e.g., /docs/page vs /docs/latest/page or /docs/es/page)
|
|
72
|
+
// If duplicates exist, we only show the style (prefixed or unprefixed) that matches the user's current page style.
|
|
73
|
+
const isCurrentLocalePrefixed = !!currentRoute?.locale
|
|
74
|
+
const isCurrentVersionPrefixed = !!currentRoute?.version
|
|
75
|
+
|
|
76
|
+
const isRouteLocalePrefixed = !!r.locale
|
|
77
|
+
const isRouteVersionPrefixed = !!r.version
|
|
78
|
+
|
|
79
|
+
const locale = r.locale || defaultLocale
|
|
80
|
+
const version = r.version || defaultVersion
|
|
81
|
+
const key = `${r.filePath}::${locale}::${version}`
|
|
82
|
+
const hasAlternate = (alternateCounts.get(key) || 0) > 1
|
|
83
|
+
|
|
84
|
+
if (hasAlternate) {
|
|
85
|
+
// Style mismatch checks
|
|
86
|
+
const localeMismatch =
|
|
87
|
+
config.i18n && isCurrentLocalePrefixed !== isRouteLocalePrefixed
|
|
88
|
+
const versionMismatch =
|
|
89
|
+
config.versions && isCurrentVersionPrefixed !== isRouteVersionPrefixed
|
|
90
|
+
|
|
91
|
+
if (localeMismatch || versionMismatch) {
|
|
92
|
+
return false
|
|
101
93
|
}
|
|
102
|
-
}
|
|
103
|
-
: []
|
|
94
|
+
}
|
|
104
95
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
label: v.label,
|
|
109
|
-
isCurrent: v.path === currentVersion,
|
|
110
|
-
}))
|
|
111
|
-
: []
|
|
96
|
+
return true
|
|
97
|
+
})
|
|
98
|
+
}, [allRoutes, config, currentLocale, currentVersion, currentRoute])
|
|
112
99
|
|
|
113
100
|
return {
|
|
114
101
|
routes,
|
|
115
102
|
allRoutes,
|
|
116
103
|
currentRoute,
|
|
117
104
|
currentLocale: currentLocale as import('../../shared/types').BoltdocsLocale,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
currentVersion: currentVersion as import('../../shared/types').BoltdocsVersion,
|
|
121
|
-
currentVersionLabel,
|
|
122
|
-
availableVersions,
|
|
123
|
-
config,
|
|
105
|
+
currentVersion:
|
|
106
|
+
currentVersion as import('../../shared/types').BoltdocsVersion,
|
|
124
107
|
}
|
|
125
108
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { useLocation } from './use-location'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to highlight search terms based on the 'hl' query parameter.
|
|
6
|
+
*/
|
|
7
|
+
export function useSearchHighlight(
|
|
8
|
+
containerSelector: string = '.boltdocs-page',
|
|
9
|
+
) {
|
|
10
|
+
const { search } = useLocation()
|
|
11
|
+
const query = new URLSearchParams(search).get('hl')
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!query) {
|
|
15
|
+
clearHighlights(containerSelector)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const container = document.querySelector(containerSelector)
|
|
20
|
+
if (!container) return
|
|
21
|
+
|
|
22
|
+
let rafId: number
|
|
23
|
+
|
|
24
|
+
// Observe changes to the content (e.g. navigation or lazy loading)
|
|
25
|
+
const observer = new MutationObserver((mutations) => {
|
|
26
|
+
const hasExternalChanges = mutations.some((m) => {
|
|
27
|
+
const addedNodes = Array.from(m.addedNodes)
|
|
28
|
+
const removedNodes = Array.from(m.removedNodes)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
addedNodes.some(
|
|
32
|
+
(n) =>
|
|
33
|
+
!(
|
|
34
|
+
n instanceof HTMLElement &&
|
|
35
|
+
n.hasAttribute('data-search-highlight')
|
|
36
|
+
),
|
|
37
|
+
) ||
|
|
38
|
+
removedNodes.some(
|
|
39
|
+
(n) =>
|
|
40
|
+
!(
|
|
41
|
+
n instanceof HTMLElement &&
|
|
42
|
+
n.hasAttribute('data-search-highlight')
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
if (hasExternalChanges) {
|
|
49
|
+
run()
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Function to run highlighting
|
|
54
|
+
function run() {
|
|
55
|
+
cancelAnimationFrame(rafId)
|
|
56
|
+
rafId = requestAnimationFrame(() => {
|
|
57
|
+
// Disconnect to avoid observing our own cleanup/highlight cycle
|
|
58
|
+
observer.disconnect()
|
|
59
|
+
clearHighlights(containerSelector)
|
|
60
|
+
|
|
61
|
+
// Split query into individual words (minimum 2 chars)
|
|
62
|
+
const terms = query!
|
|
63
|
+
.split(/\s+/)
|
|
64
|
+
.map((t) => t.trim())
|
|
65
|
+
.filter((t) => t.length >= 2)
|
|
66
|
+
|
|
67
|
+
if (terms.length > 0) {
|
|
68
|
+
highlightTerms(container!, terms)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Re-observe
|
|
72
|
+
observer.observe(container!, { childList: true, subtree: true })
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Initial run
|
|
77
|
+
run()
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
cancelAnimationFrame(rafId)
|
|
81
|
+
observer.disconnect()
|
|
82
|
+
clearHighlights(containerSelector)
|
|
83
|
+
}
|
|
84
|
+
}, [query, search, containerSelector])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearHighlights(selector: string) {
|
|
88
|
+
const marks = document.querySelectorAll(
|
|
89
|
+
`${selector} mark[data-search-highlight]`,
|
|
90
|
+
)
|
|
91
|
+
marks.forEach((mark) => {
|
|
92
|
+
try {
|
|
93
|
+
const parent = mark.parentNode
|
|
94
|
+
if (parent && parent.contains(mark)) {
|
|
95
|
+
const text = mark.textContent || ''
|
|
96
|
+
parent.replaceChild(document.createTextNode(text), mark)
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// Ignore DOM errors during cleanup
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function highlightTerms(container: Element, terms: string[]) {
|
|
105
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
|
|
106
|
+
acceptNode: (node) => {
|
|
107
|
+
const parent = node.parentElement
|
|
108
|
+
if (
|
|
109
|
+
parent &&
|
|
110
|
+
(parent.tagName === 'SCRIPT' ||
|
|
111
|
+
parent.tagName === 'STYLE' ||
|
|
112
|
+
parent.tagName === 'MARK' ||
|
|
113
|
+
parent.closest('pre') ||
|
|
114
|
+
parent.closest('code'))
|
|
115
|
+
) {
|
|
116
|
+
return NodeFilter.FILTER_REJECT
|
|
117
|
+
}
|
|
118
|
+
return NodeFilter.FILTER_ACCEPT
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const nodes: Text[] = []
|
|
123
|
+
let node: Node | null
|
|
124
|
+
while ((node = walker.nextNode())) {
|
|
125
|
+
nodes.push(node as Text)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create a combined regex for all terms
|
|
129
|
+
// Accent-insensitive helper: replaces 'a' with '[aáàä...]'
|
|
130
|
+
const accentMap: Record<string, string> = {
|
|
131
|
+
a: '[aáàäâã]',
|
|
132
|
+
e: '[eéèëê]',
|
|
133
|
+
i: '[iíìïî]',
|
|
134
|
+
o: '[oóòöôõ]',
|
|
135
|
+
u: '[uúùüû]',
|
|
136
|
+
n: '[nñ]',
|
|
137
|
+
c: '[cç]',
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const prepareRegex = (term: string) => {
|
|
141
|
+
let pattern = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
142
|
+
// Make it accent insensitive
|
|
143
|
+
pattern = pattern
|
|
144
|
+
.split('')
|
|
145
|
+
.map((char) => {
|
|
146
|
+
const lower = char.toLowerCase()
|
|
147
|
+
return accentMap[lower] || char
|
|
148
|
+
})
|
|
149
|
+
.join('')
|
|
150
|
+
return pattern
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const combinedPattern = terms.map(prepareRegex).join('|')
|
|
154
|
+
const regex = new RegExp(`(${combinedPattern})`, 'gi')
|
|
155
|
+
|
|
156
|
+
const matchRegexes = terms.map((term) => {
|
|
157
|
+
const p = prepareRegex(term)
|
|
158
|
+
return new RegExp(`^${p}$`, 'i')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
nodes.forEach((textNode) => {
|
|
162
|
+
const text = textNode.textContent
|
|
163
|
+
if (text && regex.test(text)) {
|
|
164
|
+
const fragment = document.createDocumentFragment()
|
|
165
|
+
const parts = text.split(regex)
|
|
166
|
+
|
|
167
|
+
parts.forEach((part) => {
|
|
168
|
+
const isMatch = matchRegexes.some((rx) => rx.test(part))
|
|
169
|
+
|
|
170
|
+
if (isMatch) {
|
|
171
|
+
const mark = document.createElement('mark')
|
|
172
|
+
mark.textContent = part
|
|
173
|
+
mark.setAttribute('data-search-highlight', 'true')
|
|
174
|
+
fragment.appendChild(mark)
|
|
175
|
+
} else if (part) {
|
|
176
|
+
fragment.appendChild(document.createTextNode(part))
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
if (textNode.parentNode) {
|
|
181
|
+
textNode.parentNode.replaceChild(fragment, textNode)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
@@ -2,7 +2,7 @@ import { useState, useMemo, useEffect } from 'react'
|
|
|
2
2
|
import { Index } from 'flexsearch'
|
|
3
3
|
import { useRoutes } from './use-routes'
|
|
4
4
|
import type { ComponentRoute } from '../types'
|
|
5
|
-
// @ts-
|
|
5
|
+
// @ts-expect-error
|
|
6
6
|
import searchData from 'virtual:boltdocs-search'
|
|
7
7
|
|
|
8
8
|
interface SearchDataItem {
|
|
@@ -40,6 +40,15 @@ export function useSearch(routes: ComponentRoute[]) {
|
|
|
40
40
|
setIndex(newIndex)
|
|
41
41
|
}, [isOpen, index])
|
|
42
42
|
|
|
43
|
+
// Pre-index searchData for O(1) lookups
|
|
44
|
+
const searchDataMap = useMemo(() => {
|
|
45
|
+
const map = new Map<string, SearchDataItem>()
|
|
46
|
+
for (const doc of searchData as SearchDataItem[]) {
|
|
47
|
+
map.set(doc.id, doc)
|
|
48
|
+
}
|
|
49
|
+
return map
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
43
52
|
const list = useMemo(() => {
|
|
44
53
|
if (!query) {
|
|
45
54
|
// Default results: just active routes
|
|
@@ -70,7 +79,7 @@ export function useSearch(routes: ComponentRoute[]) {
|
|
|
70
79
|
const seen = new Set<string>()
|
|
71
80
|
|
|
72
81
|
for (const id of searchResults) {
|
|
73
|
-
const doc =
|
|
82
|
+
const doc = searchDataMap.get(id as string)
|
|
74
83
|
if (!doc) continue
|
|
75
84
|
|
|
76
85
|
// Filter by locale and version
|
|
@@ -92,7 +101,7 @@ export function useSearch(routes: ComponentRoute[]) {
|
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
return results.slice(0, 10)
|
|
95
|
-
}, [query, index, currentLocale, currentVersion, routes])
|
|
104
|
+
}, [query, index, currentLocale, currentVersion, routes, searchDataMap])
|
|
96
105
|
|
|
97
106
|
return {
|
|
98
107
|
isOpen,
|