Skip to content

Commit

Permalink
feat: add search config option
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed May 1, 2024
1 parent 01f2619 commit 28db19b
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/stale-eels-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vocs": patch
---

Added `search` config option.
19 changes: 19 additions & 0 deletions site/pages/docs/api/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,25 @@ export default defineConfig({
})
```

### search

- **Type:** `Search`

Search configuration. Accepts [`MiniSearch` options](https://lucaong.github.io/minisearch/types/MiniSearch.SearchOptions.html).

```tsx twoslash
import { defineConfig } from 'vocs'

export default defineConfig({
search: { // [!code focus]
boostDocument(documentId) { // [!code focus]
return documentId === '/core' ? 2 : 1 // [!code focus]
} // [!code focus]
}, // [!code focus]
title: 'Viem'
})
```

### sidebar

- **Type:** `Sidebar`
Expand Down
8 changes: 5 additions & 3 deletions src/app/components/SearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import * as Dialog from '@radix-ui/react-dialog'
import {
ArrowLeftIcon,
ChevronRightIcon,
FileIcon,
ListBulletIcon,
MagnifyingGlassIcon,
FileIcon,
} from '@radix-ui/react-icons'
import * as Label from '@radix-ui/react-label'
import clsx from 'clsx'
Expand All @@ -13,6 +13,7 @@ import { type SearchResult } from 'minisearch'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'

import { useConfig } from '../hooks/useConfig.js'
import { useDebounce } from '../hooks/useDebounce.js'
import { useLocalStorage } from '../hooks/useLocalStorage.js'
import { type Result, useSearchIndex } from '../hooks/useSearchIndex.js'
Expand All @@ -22,6 +23,7 @@ import { KeyboardShortcut } from './KeyboardShortcut.js'
import * as styles from './SearchDialog.css.js'

export function SearchDialog(props: { open: boolean; onClose(): void }) {
const { search: searchOptions } = useConfig()
const navigate = useNavigate()
const inputRef = useRef<HTMLInputElement>(null)
const listRef = useRef<HTMLUListElement>(null)
Expand All @@ -41,8 +43,8 @@ export function SearchDialog(props: { open: boolean; onClose(): void }) {
return []
}
setSelectedIndex(0)
return searchIndex.search(searchTerm).slice(0, 16) as (SearchResult & Result)[]
}, [searchIndex, searchTerm])
return searchIndex.search(searchTerm, searchOptions).slice(0, 16) as (SearchResult & Result)[]
}, [searchIndex, searchOptions, searchTerm])

const resultsCount = results.length
const selectedResult = results[selectedIndex]
Expand Down
8 changes: 4 additions & 4 deletions src/app/hooks/useConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { sha256 } from '@noble/hashes/sha256'
import { bytesToHex } from '@noble/hashes/utils'
import { type ReactNode, createContext, useContext, useEffect, useState } from 'react'
import type { ParsedConfig } from '../../config.js'
import { type ParsedConfig, deserializeConfig, serializeConfig } from '../../config.js'
import { config as virtualConfig } from 'virtual:config'

const ConfigContext = createContext(virtualConfig)

export const configHash = import.meta.env.DEV
? bytesToHex(sha256(JSON.stringify(virtualConfig))).slice(0, 8)
? bytesToHex(sha256(serializeConfig(virtualConfig))).slice(0, 8)
: ''

export function getConfig(): ParsedConfig {
if (typeof window !== 'undefined' && import.meta.env.DEV) {
const storedConfig = window.localStorage.getItem(`vocs.config.${configHash}`)
if (storedConfig) return JSON.parse(storedConfig)
if (storedConfig) return deserializeConfig(storedConfig)
}
return virtualConfig
}
Expand All @@ -33,7 +33,7 @@ export function ConfigProvider({

useEffect(() => {
if (typeof window !== 'undefined' && import.meta.env.DEV)
window.localStorage.setItem(`vocs.config.${configHash}`, JSON.stringify(config))
window.localStorage.setItem(`vocs.config.${configHash}`, serializeConfig(config))
}, [config])

return <ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>
Expand Down
70 changes: 70 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RehypeShikiOptions } from '@shikijs/rehype'
import type { SearchOptions } from 'minisearch'
import type { ReactElement } from 'react'
import type { TwoslashOptions } from 'twoslash'
import type { PluggableList } from 'unified'
Expand Down Expand Up @@ -117,6 +118,10 @@ export type Config<
* @default "docs"
*/
rootDir?: string
/**
* Configuration for docs search.
*/
search?: Normalize<Search>
/**
* Navigation displayed on the sidebar.
*/
Expand Down Expand Up @@ -439,6 +444,8 @@ export type Markdown<parsed extends boolean = false> = RequiredBy<
parsed extends true ? 'code' : never
>

export type Search = SearchOptions

export type SidebarItem = {
/** Whether or not to collapse the sidebar item by default. */
collapsed?: boolean
Expand Down Expand Up @@ -551,3 +558,66 @@ export type ParsedTopNavItem = TopNavItem<true> & {
export type TopNav<parsed extends boolean = false> = parsed extends true
? ParsedTopNavItem[]
: TopNavItem[]

//////////////////////////////////////////////////////
// Utilities

export function serializeConfig(config: Config) {
return JSON.stringify(serializeFunctions(config))
}

export function deserializeConfig(config: string) {
return deserializeFunctions(JSON.parse(config))
}

export function serializeFunctions(value: any, key?: string): any {
if (Array.isArray(value)) {
return value.map((v) => serializeFunctions(v))
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).reduce((acc, key) => {
if (key[0] === '_') return acc
acc[key] = serializeFunctions(value[key], key)
return acc
}, {} as any)
} else if (typeof value === 'function') {
let serialized = value.toString()
if (key && (serialized.startsWith(key) || serialized.startsWith(`async ${key}`))) {
serialized = serialized.replace(key, 'function')
}
return `_vocs-fn_${serialized}`
} else {
return value
}
}

export function deserializeFunctions(value: any): any {
if (Array.isArray(value)) {
return value.map(deserializeFunctions)
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).reduce((acc: any, key) => {
acc[key] = deserializeFunctions(value[key])
return acc
}, {})
} else if (typeof value === 'string' && value.includes('_vocs-fn_')) {
return new Function(`return ${value.slice(9)}`)()
} else {
return value
}
}

export const deserializeFunctionsStringified = `
function deserializeFunctions(value) {
if (Array.isArray(value)) {
return value.map(deserializeFunctions)
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).reduce((acc, key) => {
acc[key] = deserializeFunctions(value[key])
return acc
}, {})
} else if (typeof value === 'string' && value.includes('_vocs-fn_')) {
return new Function(\`return \${value.slice(9)}\`)()
} else {
return value
}
}
`
7 changes: 5 additions & 2 deletions src/vite/plugins/virtual-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type PluginOption } from 'vite'

import { deserializeFunctionsStringified, serializeConfig } from '../../config.js'
import { resolveVocsConfig } from '../utils/resolveVocsConfig.js'

export function virtualConfig(): PluginOption {
Expand All @@ -25,8 +26,10 @@ export function virtualConfig(): PluginOption {
async load(id) {
if (id === resolvedVirtualModuleId) {
const { config } = await resolveVocsConfig()
// TODO: serialize fns
return `export const config = ${JSON.stringify(config)}`
return `
${deserializeFunctionsStringified}
export const config = deserializeFunctions(${serializeConfig(config)})`
}
return
},
Expand Down

0 comments on commit 28db19b

Please sign in to comment.