Update fuse to latest version

Closes #752 Also refactors search to use the current or default version of the CLI for results. This also refactors the get nav util to make more items private.

Luke Karrys committed Oct 21, 2023 at 15:56 UTC 0dd46845a3156da2333ae24a475f013206d2aedb
11 files changed +314 -262
package-lock.json
+5 -5
@@ -24,7 +24,7 @@
24 "details-element-polyfill": "^2.4.0",
25 "downshift": "^8.2.2",
26 "framer-motion": "^10.16.4",
27 - "fuse.js": "^3.6.1",
27 + "fuse.js": "^6.6.2",
28 "gatsby": "^5.12.7",
29 "gatsby-plugin-manifest": "^5.12.1",
30 "gatsby-plugin-mdx": "^5.12.1",
@@ -17009,11 +17009,11 @@
17009 }
17010 },
17011 "node_modules/fuse.js": {
17012 - "version": "3.6.1",
17013 - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz",
17014 - "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==",
17012 + "version": "6.6.2",
17013 + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz",
17014 + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==",
17015 "engines": {
17016 - "node": ">=6"
17016 + "node": ">=10"
17017 }
17018 },
17019 "node_modules/fzy.js": {
package.json
+1 -2
@@ -7,7 +7,6 @@
7 },
8 "private": true,
9 "scripts": {
10 - "//": "Fix for parcel segfaults",
10 "//": "https://github.com/parcel-bundler/parcel/issues/7702",
11 "gatsby": "PARCEL_WORKERS=0 gatsby",
12 "develop": "npm run gatsby -- develop",
@@ -42,7 +41,7 @@
41 "details-element-polyfill": "^2.4.0",
42 "downshift": "^8.2.2",
43 "framer-motion": "^10.16.4",
45 - "fuse.js": "^3.6.1",
44 + "fuse.js": "^6.6.2",
45 "gatsby": "^5.12.7",
46 "gatsby-plugin-manifest": "^5.12.1",
47 "gatsby-plugin-mdx": "^5.12.1",
src/components/breadcrumbs.js
+5 -7
@@ -5,20 +5,18 @@ import * as getNav from '../util/get-nav'
5 import usePage from '../hooks/use-page'
6
7 const BreadcrumbItem = ({item, path}) => {
8 - const href = getNav.getLocation(item.url)
9 - const selected = getNav.isPathForItem(path, getNav.getItem(href))
8 + const selected = getNav.isPathForItem(path, getNav.getItem(item.url))
9
10 return (
12 - <PrimerBreadcrumbs.Item as={GatsbyLink} to={href} {...(selected ? {selected} : {})}>
11 + <PrimerBreadcrumbs.Item as={GatsbyLink} to={item.url} {...(selected ? {selected} : {})}>
12 {item.shortName || item.title}
13 </PrimerBreadcrumbs.Item>
14 )
15 }
16
17 const Breadcrumbs = () => {
19 - const {location} = usePage()
20 - const path = getNav.getLocation(location.pathname)
21 - const items = getNav.getItemBreadcrumbs(location.pathname, {hideVariants: true})
18 + const {pathname} = usePage().location
19 + const items = getNav.getItemBreadcrumbs(pathname, {hideVariants: true})
20
21 if (items.length <= 1) {
22 return null
@@ -27,7 +25,7 @@ const Breadcrumbs = () => {
25 return (
26 <PrimerBreadcrumbs sx={{mb: 4}}>
27 {items.map(item => (
30 - <BreadcrumbItem key={item.url} item={item} path={path} />
28 + <BreadcrumbItem key={item.url} item={item} path={pathname} />
29 ))}
30 </PrimerBreadcrumbs>
31 )
src/components/nav-items.js
+6 -8
@@ -10,14 +10,13 @@ import usePage from '../hooks/use-page'
10 import useSiteMetadata from '../hooks/use-site-metadata'
11
12 const NavItem = ({item, path, depth}) => {
13 - const href = getNav.getLocation(item.url)
14 - const isCurrent = getNav.isActiveUrl(path, href)
15 - const items = getNav.getHierarchy(item, {path: item.url, hideVariants: true})
13 + const isCurrent = getNav.isActiveUrl(path, item.url)
14 + const items = getNav.getHierarchy(item, item.url, {hideVariants: true})
15
16 return (
17 <NavList.Item
18 as={Link}
20 - to={href}
19 + to={item.url}
20 defaultOpen={items && isCurrent}
21 aria-current={isCurrent ? 'page' : null}
22 sx={{fontSize: depth === 1 ? 2 : 1}}
@@ -54,10 +53,9 @@ const ExternalNavItem = ({title, ...props}) => (
53 )
54
55 const Navigation = () => {
57 - const {location} = usePage()
56 const {repositoryUrl} = useSiteMetadata()
59 - const path = getNav.getLocation(location.pathname)
60 - const items = getNav.getHierarchy(null, {path, hideVariants: true})
57 + const {pathname} = usePage().location
58 + const items = getNav.getHierarchy(null, pathname, {hideVariants: true})
59
60 return (
61 <>
@@ -65,7 +63,7 @@ const Navigation = () => {
63 <h3>Site navigation</h3>
64 </VisuallyHidden>
65 <NavList aria-label="Site">
68 - <NavItems items={items} path={path} />
66 + <NavItems items={items} path={pathname} />
67 <NavList.Divider />
68 {headerNavItems.map(item => (
69 <Box key={item.title} sx={{display: ['flex', null, null, 'none']}}>
src/components/search-results.js
+9 -1
@@ -5,7 +5,15 @@ import * as getNav from '../util/get-nav'
5
6 const Breadcrumbs = ({item}) => {
7 const siteMetadata = useSiteMetadata()
8 - const hierarchy = getNav.getItemBreadcrumbs(item.path).slice(0, -1)
8 + const variant = getNav.getVariant(getNav.getVariantRoot(item.path), item.path)
9 + const hierarchy = getNav.getItemBreadcrumbs(item.path)
10 +
11 + // keep the variant in the breadcrumb if we have one and its not the
12 + // same as the last breadcrumb. this makes sure that variant index pages
13 + // don't all appear the same in the search results
14 + if (!variant || variant !== hierarchy[hierarchy.length - 1].shortName) {
15 + hierarchy.pop()
16 + }
17
18 const text = hierarchy.length ? hierarchy.map(s => s.shortName || s.title).join(' / ') : siteMetadata.shortName
19
src/components/variant-select.js
+9 -14
@@ -59,13 +59,11 @@ const VariantMenu = ({title, latest, current, prerelease, legacy, path}) => {
59 {current && <VariantItem {...current} />}
60 {prerelease && <VariantItem {...prerelease} />}
61 </ActionList.Group>
62 - {legacy && (
63 - <ActionList.Group title="Legacy">
64 - {legacy.map(item => (
65 - <VariantItem key={item.title} {...item} />
66 - ))}
67 - </ActionList.Group>
68 - )}
62 + <ActionList.Group title="Legacy">
63 + {legacy.map(item => (
64 + <VariantItem key={item.title} {...item} />
65 + ))}
66 + </ActionList.Group>
67 </ActionList>
68 </ActionMenu.Overlay>
69 </ActionMenu>
@@ -74,13 +72,10 @@ const VariantMenu = ({title, latest, current, prerelease, legacy, path}) => {
72 }
73
74 const useVariants = () => {
77 - const {pathname} = usePage().location
75 + const {pathname: path} = usePage().location
76
77 return React.useMemo(() => {
80 - const root = getNav.getVariantRoot(pathname)
81 - const path = getNav.getPath(pathname)
82 - const vp = getNav.getVariantAndPage(root, path)
83 - const variantPages = vp ? getNav.getVariantsForPage(root, vp.page) : []
78 + const variantPages = getNav.getVariantsForPath(path)
79
80 if (!variantPages.length) {
81 return null
@@ -106,7 +101,7 @@ const useVariants = () => {
101 break
102 default:
103 result.legacy.push(item)
109 - typeDesc = ' Legacy'
104 + typeDesc = ' (Legacy)'
105 }
106 if (item.active) {
107 result.title = `${item.title}${typeDesc}`
@@ -116,7 +111,7 @@ const useVariants = () => {
111 result.legacy.sort((a, b) => parseInt(b.shortName.slice(1)) - parseInt(a.shortName.slice(1)))
112
113 return result
119 - }, [pathname])
114 + }, [path])
115 }
116
117 const VariantSelect = () => {
src/hooks/use-search.js
+40 -33
@@ -2,11 +2,15 @@ import React from 'react'
2 import {useCombobox} from 'downshift'
3 import {navigate, graphql, useStaticQuery} from 'gatsby'
4 import {useIsMobile} from './use-breakpoint'
5 +import usePage from './use-page'
6 +import * as getNav from '../util/get-nav'
7 +
8 +// This worker can live for the entire duraction of the site
9 +const WORKER = new Worker(new URL('../util/search.worker.js', import.meta.url))
10 +const CLI_ROOT = '/cli'
11
12 const useSearchData = () => {
7 - // TODO: this static query runs for all pages changes in dev mode. When not
8 - // testing search explicitly it should be replaced with some static data.
9 - const rawData = useStaticQuery(graphql`
13 + const data = useStaticQuery(graphql`
14 {
15 allMdx {
16 nodes {
@@ -27,15 +31,13 @@ const useSearchData = () => {
31 `)
32
33 return React.useMemo(() => {
30 - const mdxNodes = rawData.allMdx.nodes.reduce((map, obj) => {
34 + const mdxNodes = data.allMdx.nodes.reduce((map, obj) => {
35 map[obj.id] = obj
36 return map
37 }, {})
38
35 - return rawData.allSitePage.nodes
36 - .filter(node => {
37 - return node.pageContext && node.pageContext.mdxId && mdxNodes[node.pageContext.mdxId] != null
38 - })
39 + return data.allSitePage.nodes
40 + .filter(node => mdxNodes[node.pageContext?.mdxId] != null)
41 .map(node => {
42 const mdxNode = mdxNodes[node.pageContext.mdxId]
43 return {
@@ -44,53 +46,58 @@ const useSearchData = () => {
46 body: mdxNode.body,
47 }
48 })
47 - }, [rawData])
49 + }, [data])
50 }
51
50 -function useSearch() {
51 - const isMobile = useIsMobile()
52 -
53 - const queryRef = React.useRef()
54 - const workerRef = React.useRef()
52 +const useCliVersion = () => {
53 + return getNav.getCurrentOrDefaultVariant(
54 + getNav.getItem(getNav.getVariantRoot(`${CLI_ROOT}/`, {stripTrailing: false})),
55 + usePage().location.pathname,
56 + )
57 +}
58
59 +function useSearch() {
60 const [query, setQuery] = React.useState()
57 - const [items, setItems] = React.useState(null)
58 -
59 - const searchData = useSearchData()
61 + const [results, setResults] = React.useState(null)
62 + const queryRef = React.useRef()
63 + const items = useSearchData()
64 + const isMobile = useIsMobile()
65 + const {url: cliUrl} = useCliVersion()
66
67 const handleSearchResults = React.useCallback(({data}) => {
68 + if (data.debug) {
69 + console.log(data.debug)
70 + }
71 if (data.query && data.results && data.query === queryRef.current) {
63 - setItems(data.results)
72 + setResults(data.results)
73 }
74 }, [])
75
76 React.useEffect(() => {
68 - const worker = new Worker(new URL('../util/search.worker.js', import.meta.url))
69 - workerRef.current = worker
77 + WORKER.addEventListener('message', handleSearchResults)
78 + }, [handleSearchResults])
79
71 - worker.addEventListener('message', handleSearchResults)
72 - worker.postMessage({data: searchData})
80 + React.useEffect(() => {
81 + WORKER.postMessage({items})
82 + }, [items])
83
74 - return () => worker.terminate()
75 - }, [searchData, handleSearchResults])
84 + React.useEffect(() => {
85 + WORKER.postMessage({cli: {root: CLI_ROOT, current: cliUrl}})
86 + }, [cliUrl])
87
88 React.useEffect(() => {
89 queryRef.current = query
90
91 if (query) {
81 - workerRef.current.postMessage({query})
92 + WORKER.postMessage({query})
93 } else {
83 - setItems(null)
94 + setResults(null)
95 }
96 }, [query])
97
98 const combobox = useCombobox({
99 id: 'search-box',
89 - // We don't need Downshift to keep track of a selected item because as
90 - // soon as an item is selected we navigate to a new page.
91 - // Let's avoid any unexpected states related to the selected item
92 - // by setting it to always be `null`.
93 - items: items || [],
100 + items: results || [],
101 onInputValueChange: ({inputValue}) => setQuery(inputValue),
102 onSelectedItemChange: ({selectedItem}) => {
103 if (selectedItem) {
@@ -126,8 +133,8 @@ function useSearch() {
133
134 return {
135 ...combobox,
129 - results: items,
130 - isOpen: !!(combobox.isOpen && items),
136 + results,
137 + isOpen: !!(combobox.isOpen && results),
138 }
139 }
140
src/mdx/index.js
+1 -4
@@ -1,6 +1,5 @@
1 import React from 'react'
2 import {Box, Heading, themeGet, Text, Octicon} from '@primer/react'
3 -import {withPrefix} from 'gatsby'
3 import styled from 'styled-components'
4 import {variant as styledVariant} from 'styled-system'
5 import {LinkIcon} from '@primer/octicons-react'
@@ -312,9 +311,7 @@ export const Image = styled(RequiredImage)`
311 box-sizing: content-box;
312 `
313
315 -const ScreenshotImage = ({src, ...props}) => <RequiredImage src={withPrefix(src)} {...props} />
316 -
317 -export const Screenshot = styled(ScreenshotImage)`
314 +export const Screenshot = styled(RequiredImage)`
315 margin-top: 15px;
316 max-width: min(100%, 525px);
317 max-height: 300px;
src/mdx/nav-hierarchy.js
+12 -14
@@ -4,8 +4,8 @@ import Link from '../components/link'
4 import * as getNav from '../util/get-nav'
5 import usePage from '../hooks/use-page'
6
7 -const HierarchyItem = ({item, depth, ...props}) => {
8 - const hierarchy = getNav.getHierarchy(item, props)
7 +const HierarchyItem = ({item, maxDepth, depth, hideVariants}) => {
8 + const hierarchy = getNav.getHierarchy(item, null, {hideVariants})
9
10 return (
11 <Box as="li" key={item.url}>
@@ -13,34 +13,32 @@ const HierarchyItem = ({item, depth, ...props}) => {
13 {item.title}
14 </Link>
15 {item.description ? <Box sx={{fontSize: 1, mb: 1}}>{item.description}</Box> : null}
16 - {hierarchy ? <Hierarchy items={hierarchy} depth={depth + 1} {...props} /> : null}
16 + {hierarchy ? (
17 + <Hierarchy items={hierarchy} maxDepth={maxDepth} depth={depth + 1} hideVariants={hideVariants} />
18 + ) : null}
19 </Box>
20 )
21 }
22
21 -const Hierarchy = ({items, ...props}) => {
22 - if (props.maxDepth && props.depth > props.maxDepth) {
23 +const Hierarchy = ({items, maxDepth, depth, hideVariants}) => {
24 + if (maxDepth && depth > maxDepth) {
25 return null
26 }
27
28 return (
29 <Box as="ul">
30 {items.map(item => (
29 - <HierarchyItem key={item.url} item={item} {...props} />
31 + <HierarchyItem key={item.url} item={item} maxDepth={maxDepth} depth={depth} hideVariants={hideVariants} />
32 ))}
33 </Box>
34 )
35 }
36
35 -function NavHierarchy({depth, ...props}) {
36 - const {location} = usePage()
37 - const path = getNav.getLocation(location.pathname)
38 - const navRoot = path.replace(/\/+$/g, '')
37 +function NavHierarchy({depth, hideVariants}) {
38 + const {pathname} = usePage().location
39 + const hierarchy = getNav.getHierarchy(getNav.getItem(pathname), null, {hideVariants})
40
40 - const rootItem = getNav.getItem(navRoot)
41 - const hierarchy = getNav.getHierarchy(rootItem, props)
42 -
43 - return <Hierarchy items={hierarchy} maxDepth={depth} depth={1} {...props} />
41 + return <Hierarchy items={hierarchy} maxDepth={depth} depth={1} hideVariants={hideVariants} />
42 }
43
44 export default NavHierarchy
src/util/get-nav.js
+163 -146
@@ -1,71 +1,76 @@
1 -import {withPrefix} from 'gatsby'
1 import navItems from '../../content/nav.yml'
2
4 -export const getItemBreadcrumbs = (path, props = {}) => {
5 - let hierarchy = getItemHierarchy(path)
6 -
7 - if (!hierarchy) {
8 - return []
9 - }
10 -
11 - if (props.hideVariants) {
12 - const root = getVariantRoot(path)
13 - const vp = getVariantAndPage(root, path)
14 - hierarchy = hierarchy.filter(item => (vp ? item.shortName !== vp.variant : true))
3 +/*
4 + *
5 + * Internal functions
6 + *
7 + */
8 +const getPath = path => {
9 + while (path && path.endsWith('/')) {
10 + path = path.substring(0, path.length - 1)
11 }
12
17 - return hierarchy
13 + return path
14 }
15
20 -export const getLocation = path => {
21 - const pathPrefix = withPrefix('/')
22 -
23 - if (!pathPrefix || pathPrefix === '/') {
24 - return path
16 +const getCurrentVariant = (root, path) => {
17 + for (const v of root.variants) {
18 + if (isActiveItem(path, v)) {
19 + return v
20 + }
21 }
22
27 - const match = new RegExp(`^${pathPrefix}`)
28 - return path.replace(match, '/')
23 + return null
24 }
25
31 -export const getPath = path => {
32 - while (path && path.endsWith('/')) {
33 - path = path.substring(0, path.length - 1)
26 +const getDefaultVariant = root => {
27 + for (const v of root.variants) {
28 + if (v.default) {
29 + return v
30 + }
31 }
32
36 - return path
33 + return root.variants[0]
34 }
35
39 -export const getVariantRoot = path => {
40 - path = getPath(path)
36 +const getVariantAndPage = (root, path) => {
37 + if (!root || !path.startsWith(`${root}/`)) {
38 + return null
39 + }
40
42 - return findItem(item => {
43 - if (item.variants && path.startsWith(`${item.url}/`)) {
44 - return item.url
45 - }
41 + path = path.substring(root.length + 1)
42 +
43 + const match = /^([^/]+)(?:\/(.*))?/.exec(path)
44
45 + if (!match) {
46 return null
48 - })
47 + }
48 +
49 + return {variant: match[1], page: match[2]}
50 }
51
51 -export const findItem = (fn, items = navItems) => {
52 - for (let i = 0; i < items.length; i++) {
53 - const item = items[i]
54 - let result = fn(item)
52 +const hideVariantsForItems = (items, path) => {
53 + if (!items) {
54 + return null
55 + }
56
56 - if (!result && item.children) {
57 - result = findItem(fn, item.children)
58 - }
57 + const updated = []
58
60 - if (result) {
61 - return result
59 + for (const item of items) {
60 + if (item.variants) {
61 + updated.push({
62 + ...item,
63 + url: getCurrentOrDefaultVariant(item, path).url,
64 + })
65 + } else {
66 + updated.push(item)
67 }
68 }
69
65 - return null
70 + return updated
71 }
72
68 -export const getItemHierarchy = (path, items = navItems) => {
73 +const getItemHierarchy = (path, items = navItems) => {
74 if (!path) {
75 return null
76 }
@@ -91,7 +96,107 @@ export const getItemHierarchy = (path, items = navItems) => {
96 return null
97 }
98
99 +const isChildItem = (path, items = navItems) => {
100 + if (!path) {
101 + return false
102 + }
103 +
104 + return findItem(item => (isPathForItem(path, item) ? item : null), items) != null
105 +}
106 +
107 +const findItem = (fn, items = navItems) => {
108 + for (let i = 0; i < items.length; i++) {
109 + const item = items[i]
110 + let result = fn(item)
111 +
112 + if (!result && item.children) {
113 + result = findItem(fn, item.children)
114 + }
115 +
116 + if (result) {
117 + return result
118 + }
119 + }
120 +
121 + return null
122 +}
123 +
124 +const isActiveItem = (currentPath, linkItem) => {
125 + return (
126 + isPathForItem(currentPath, linkItem) ||
127 + (linkItem.children && isChildItem(currentPath, linkItem.children)) ||
128 + (linkItem.variants && isChildItem(currentPath, linkItem.variants))
129 + )
130 +}
131 +
132 +const getVariantsForPage = (root, page) => {
133 + const pages = []
134 + const rootItem = findItem(item => (getPath(item.url) === getPath(root) ? item : null))
135 +
136 + if (rootItem && rootItem.variants) {
137 + for (const variant of rootItem.variants) {
138 + if (!variant.children) {
139 + continue
140 + }
141 +
142 + const vp = getVariantPage(root, variant.url)
143 + const variantPage =
144 + vp === page
145 + ? variant
146 + : findItem(item => {
147 + const itemVp = getVariantPage(root, item.url)
148 + return itemVp === page ? item : null
149 + }, variant.children)
150 +
151 + if (!variantPage) {
152 + continue
153 + }
154 +
155 + pages.push({variant, page: variantPage})
156 + }
157 + }
158 +
159 + return pages
160 +}
161 +
162 +/*
163 + *
164 + * Export functions
165 + *
166 + */
167 +export const getItemBreadcrumbs = (path, {hideVariants} = {}) => {
168 + let hierarchy = getItemHierarchy(path)
169 +
170 + if (!hierarchy) {
171 + return []
172 + }
173 +
174 + if (hideVariants) {
175 + const root = getVariantRoot(path)
176 + const variant = getVariant(root, path)
177 + hierarchy = hierarchy.filter(item => (variant ? item.shortName !== variant : true))
178 + }
179 +
180 + return hierarchy
181 +}
182 +
183 +export const getVariantRoot = (path, {stripTrailing = true} = {}) => {
184 + if (stripTrailing) {
185 + path = getPath(path)
186 + }
187 +
188 + return findItem(item => {
189 + if (item.variants && path.startsWith(`${item.url}/`)) {
190 + return item.url
191 + }
192 +
193 + return null
194 + })
195 +}
196 +
197 export const getItem = (path, items = navItems) => {
198 + path = getPath(path)
199 +
200 if (!path) {
201 return {url: '/', children: items}
202 }
@@ -121,141 +226,53 @@ export const isPathForItem = (path, item) => {
226 return getPath(item.url) === getPath(path)
227 }
228
124 -export const isChildItem = (path, items = navItems) => {
125 - if (!path) {
126 - return false
127 - }
128 -
129 - return findItem(item => (isPathForItem(path, item) ? item : null), items) != null
130 -}
131 -
132 -export const getHierarchy = (root, props = {}) => {
229 +export const getHierarchy = (root, path, {hideVariants} = {}) => {
230 let children
231
232 if (!root) {
233 children = navItems
137 - } else if (root.variants && props.hideVariants === true) {
138 - const variant = getCurrentOrDefaultVariant(root, props.path)
139 - children = variant.children
234 + } else if (root.variants && hideVariants === true) {
235 + children = getCurrentOrDefaultVariant(root, path).children
236 } else if (root.variants) {
237 children = root.variants
238 } else {
239 children = root.children
240 }
241
146 - return children && props.hideVariants === true ? hideVariantsForItems(children, props.path) : children
147 -}
148 -
149 -export const hideVariantsForItems = (items, path) => {
150 - if (!items) {
151 - return null
152 - }
153 -
154 - const updated = []
155 -
156 - for (const item of items) {
157 - if (item.variants) {
158 - updated.push({
159 - ...item,
160 - url: getCurrentOrDefaultVariant(item, path).url,
161 - })
162 - } else {
163 - updated.push(item)
164 - }
165 - }
166 -
167 - return updated
242 + return children && hideVariants === true ? hideVariantsForItems(children, path) : children
243 }
244
245 export const getCurrentOrDefaultVariant = (root, path) => {
171 - return getCurrentVariant(root, path) || getDefaultVariant(root)
172 -}
246 + let variant = path ? getCurrentVariant(root, path) : null
247
174 -export const getCurrentVariant = (root, path) => {
175 - for (const v of root.variants) {
176 - if (isActiveItem(path, v)) {
177 - return v
178 - }
248 + if (!variant) {
249 + variant = getDefaultVariant(root)
250 }
251
181 - return null
252 + return variant
253 }
254
184 -export const getDefaultVariant = root => {
185 - for (const v of root.variants) {
186 - if (v.default) {
187 - return v
188 - }
189 - }
255 +export const getVariant = (root, path) => getVariantAndPage(root, path)?.variant ?? null
256
191 - return root.variants[0]
192 -}
193 -
194 -export const getVariantAndPage = (root, path) => {
195 - if (!root || !path.startsWith(`${root}/`)) {
196 - return null
197 - }
198 -
199 - path = path.substring(root.length + 1)
200 -
201 - const match = /^([^/]+)(?:\/(.*))?/.exec(path)
202 -
203 - if (!match) {
204 - return null
205 - }
206 -
207 - return {variant: match[1], page: match[2]}
208 -}
257 +export const getVariantPage = (root, path) => getVariantAndPage(root, path)?.page ?? null
258
259 export const didVariantChange = (oldPath, newPath) => {
260 if (!oldPath || !newPath || oldPath === newPath) {
261 return false
262 }
263
215 - const oldVariant = getVariantAndPage(getVariantRoot(oldPath), getPath(oldPath))?.variant
264 + const oldVariant = getVariant(getVariantRoot(oldPath), getPath(oldPath))
265 if (!oldVariant) {
266 return false
267 }
268
220 - return oldVariant !== getVariantAndPage(getVariantRoot(newPath), getPath(newPath))?.variant
269 + return oldVariant !== getVariant(getVariantRoot(newPath), getPath(newPath))
270 }
271
223 -export const getVariantsForPage = (root, page) => {
224 - const pages = []
225 - const rootItem = findItem(item => (getPath(item.url) === getPath(root) ? item : null))
226 -
227 - if (rootItem && rootItem.variants) {
228 - for (const variant of rootItem.variants) {
229 - if (!variant.children) {
230 - continue
231 - }
232 -
233 - const vp = getVariantAndPage(root, variant.url)
234 - const variantPage =
235 - vp.page === page
236 - ? variant
237 - : findItem(item => {
238 - const itemVp = getVariantAndPage(root, item.url)
239 - return itemVp?.page === page ? item : null
240 - }, variant.children)
241 -
242 - if (!variantPage) {
243 - continue
244 - }
245 -
246 - pages.push({variant, page: variantPage})
247 - }
248 - }
249 -
250 - return pages
251 -}
252 -
253 -export const isActiveItem = (currentPath, linkItem) => {
254 - return (
255 - isPathForItem(currentPath, linkItem) ||
256 - (linkItem.children && isChildItem(currentPath, linkItem.children)) ||
257 - (linkItem.variants && isChildItem(currentPath, linkItem.variants))
258 - )
272 +export const getVariantsForPath = path => {
273 + const root = getVariantRoot(path)
274 + const vPage = getVariantPage(root, path)
275 + return vPage ? getVariantsForPage(root, vPage) : []
276 }
277
278 export const isActiveUrl = (currentPath, linkPath) => {
src/util/search.worker.js
+63 -28
@@ -1,35 +1,70 @@
1 -import Fuse from 'fuse.js'
1 +import FuseJs from 'fuse.js'
2 import debounce from 'lodash.debounce'
3
4 -let fuse = null
5 -
6 -// [MKT]: I landed on the debouce wait value of 50 based mostly on
7 -// experimentation. With both `leading` and `trailing` set to `true`, this
8 -// feels pretty snappy.
9 -//
10 -// From https://lodash.com/docs/#debounce:
11 -//
12 -// > Note: If `leading` and `trailing` options are `true`, `func` is invoked
13 -// > on the trailing edge of the timeout only if the debounced function is
14 -// > invoked more than once during the wait timeout.
15 -const performSearch = debounce(
16 - query =>
4 +class Fuse {
5 + constructor() {
6 + this.allItems = null
7 + this.cliVersion = null
8 + this.instances = new Map()
9 + // [MKT]: I landed on the debouce wait value of 50 based mostly on
10 + // experimentation. With both `leading` and `trailing` set to `true`, this
11 + // feels pretty snappy.
12 + //
13 + // From https://lodash.com/docs/#debounce:
14 + //
15 + // > Note: If `leading` and `trailing` options are `true`, `func` is invoked
16 + // > on the trailing edge of the timeout only if the debounced function is
17 + // > invoked more than once during the wait timeout.
18 + this.search = debounce(this.search, 50, {leading: true, trailing: true})
19 + }
20 +
21 + search(query) {
22 postMessage({
23 query,
19 - results: fuse.search(query).slice(0, 20),
20 - }),
21 - 50,
22 - {leading: true, trailing: true},
23 -)
24 -
25 -onmessage = function ({data}) {
26 - if (data.data) {
27 - fuse = new Fuse(data.data, {
28 - threshold: 0.2,
29 - keys: ['title', 'body'],
30 - tokenize: true,
24 + results: this.instances
25 + .get(this.cliVersion.current)
26 + .search(query)
27 + .slice(0, 20)
28 + .map(i => i.item),
29 })
32 - } else if (data.query) {
33 - performSearch(data.query)
30 + }
31 +
32 + createInstance() {
33 + if (!this.allItems || !this.cliVersion) {
34 + return
35 + }
36 + const items = this.allItems.filter(item =>
37 + item.path.startsWith(this.cliVersion.root) ? item.path.startsWith(this.cliVersion.current) : true,
38 + )
39 + this.instances.set(this.cliVersion.current, new FuseJs(items, {threshold: 0.2, keys: ['title', 'body']}))
40 + }
41 +
42 + setItems(items) {
43 + this.allItems = items
44 + this.createInstance()
45 + }
46 +
47 + setCliVersion({root, current}) {
48 + this.cliVersion = {root, current}
49 + if (this.instances.has(this.cliVersion.current)) {
50 + return
51 + }
52 + this.createInstance()
53 + }
54 +}
55 +
56 +const fuse = new Fuse()
57 +
58 +onmessage = function ({data: {items, query, cli}}) {
59 + if (items) {
60 + fuse.setItems(items)
61 + }
62 +
63 + if (cli) {
64 + fuse.setCliVersion(cli)
65 + }
66 +
67 + if (query) {
68 + fuse.search(query)
69 }
70 }