feat!: organized search config (#2218)

pull/2224/head
Divyansh Singh 1 year ago committed by GitHub
parent 8059ec390a
commit 713a35cbd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -47,14 +47,15 @@ export default defineConfig({
copyright: 'Copyright © 2019-present Evan You'
},
algolia: {
appId: '8J64VVRP8K',
apiKey: 'a18e2f4cc5665f6602c5631fd868adfd',
indexName: 'vitepress'
search: {
provider: 'algolia',
options: {
appId: '8J64VVRP8K',
apiKey: 'a18e2f4cc5665f6602c5631fd868adfd',
indexName: 'vitepress'
}
},
// localSearch: true,
carbonAds: {
code: 'CEBDT27Y',
placement: 'vuejsorg'

@ -9,7 +9,9 @@ import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
localSearch: true
search: {
provider: 'local'
}
}
})
```
@ -29,20 +31,23 @@ import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
localSearch: {
locales: {
zh: {
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
footer: {
selectText: '选择',
navigateText: '切换'
search: {
provider: 'local',
options: {
locales: {
zh: {
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
footer: {
selectText: '选择',
navigateText: '切换'
}
}
}
}
@ -62,10 +67,13 @@ import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
algolia: {
appId: '...',
apiKey: '...',
indexName: '...'
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...'
}
}
}
})
@ -79,52 +87,52 @@ You can use a config like this to use multilingual search:
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
themeConfig: {
// ...
algolia: {
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
}
}
}
}

@ -3,7 +3,7 @@
"version": "1.0.0-alpha.65",
"description": "Vite & Vue powered static site generator",
"type": "module",
"packageManager": "pnpm@8.1.1",
"packageManager": "pnpm@8.2.0",
"main": "dist/node/index.js",
"types": "types/index.d.ts",
"exports": {
@ -91,7 +91,7 @@
"@docsearch/js": "^3.3.3",
"@vitejs/plugin-vue": "^4.1.0",
"@vue/devtools-api": "^6.5.0",
"@vueuse/core": "^9.13.0",
"@vueuse/core": "^10.0.2",
"body-scroll-lock": "4.0.0-beta.0",
"minisearch": "^6.0.1",
"shiki": "^0.14.1",
@ -107,10 +107,10 @@
"@mdit-vue/plugin-title": "^0.12.0",
"@mdit-vue/plugin-toc": "^0.12.0",
"@mdit-vue/shared": "^0.12.0",
"@rollup/plugin-alias": "^4.0.3",
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-replace": "^5.0.2",
"@types/body-scroll-lock": "^3.1.0",
"@types/compression": "^1.7.2",
@ -134,16 +134,16 @@
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
"enquirer": "^2.3.6",
"esbuild": "^0.17.15",
"esbuild": "^0.17.16",
"escape-html": "^1.0.3",
"execa": "^7.1.1",
"fast-glob": "^3.2.12",
"fs-extra": "^11.1.1",
"get-port": "^6.1.2",
"gray-matter": "^4.0.3",
"lint-staged": "^13.2.0",
"lint-staged": "^13.2.1",
"lodash.template": "^4.5.0",
"lru-cache": "^8.0.4",
"lru-cache": "^8.0.5",
"markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.7",
"markdown-it-attrs": "^4.1.6",
@ -157,22 +157,22 @@
"path-to-regexp": "^6.2.1",
"picocolors": "^1.0.0",
"pkg-dir": "^7.0.0",
"playwright-chromium": "^1.32.2",
"playwright-chromium": "^1.32.3",
"polka": "1.0.0-next.22",
"prettier": "^2.8.7",
"prompts": "^2.4.2",
"punycode": "^2.3.0",
"rimraf": "^4.4.1",
"rimraf": "^5.0.0",
"rollup": "^3.20.2",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",
"semver": "^7.3.8",
"semver": "^7.4.0",
"shiki-processor": "^0.1.3",
"simple-git-hooks": "^2.8.1",
"sirv": "^2.0.2",
"supports-color": "^9.3.1",
"typescript": "^5.0.3",
"vitest": "^0.29.8",
"typescript": "^5.0.4",
"vitest": "^0.30.1",
"vue-tsc": "^1.3.2",
"wait-on": "^7.0.1"
},

File diff suppressed because it is too large Load Diff

@ -1,4 +1,5 @@
declare const __VP_HASH_MAP__: Record<string, string>
declare const __VP_LOCAL_SEARCH__: boolean
declare const __ALGOLIA__: boolean
declare const __CARBON__: boolean
declare const __VUE_PROD_DEVTOOLS__: boolean

@ -1,13 +1,29 @@
<script lang="ts" setup>
import { markRaw, nextTick, onMounted, ref, shallowRef, watch, type Ref, createApp } from 'vue'
import { useRouter } from 'vitepress'
import { onKeyStroke, useSessionStorage, debouncedWatch, useLocalStorage, useEventListener, computedAsync } from '@vueuse/core'
import MiniSearch, { type SearchResult } from 'minisearch'
import localSearchIndex from '@localSearchIndex'
import {
computedAsync,
debouncedWatch,
onKeyStroke,
useEventListener,
useLocalStorage,
useSessionStorage
} from '@vueuse/core'
import MiniSearch, { type SearchResult } from 'minisearch'
import { useRouter } from 'vitepress'
import {
createApp,
markRaw,
nextTick,
onMounted,
ref,
shallowRef,
watch,
type Ref
} from 'vue'
import type { ModalTranslations } from '../../../../types/local-search'
import { pathToFile, withBase } from '../../app/utils'
import { useData } from '../composables/data'
import { createTranslate } from '../support/translation'
import type { ModalTranslations } from '../../../../types/local-search'
import { pathToFile, withBase } from '../../app/utils.js'
defineProps<{
placeholder: string
@ -40,19 +56,29 @@ interface Result {
const { localeIndex } = useData()
const searchIndex = computedAsync(async () => markRaw(MiniSearch.loadJSON<Result>((await searchIndexData.value[localeIndex.value]?.())?.default, {
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
searchOptions: {
fuzzy: 0.2,
prefix: true,
boost: { title: 4, text: 2, titles: 1 },
},
})))
const searchIndex = computedAsync(async () =>
markRaw(
MiniSearch.loadJSON<Result>(
(await searchIndexData.value[localeIndex.value]?.())?.default,
{
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
searchOptions: {
fuzzy: 0.2,
prefix: true,
boost: { title: 4, text: 2, titles: 1 }
}
}
)
)
)
const filterText = useSessionStorage('vitepress:local-search-filter', '')
const showDetailedList = useLocalStorage('vitepress:local-search-detailed-list', false)
const showDetailedList = useLocalStorage(
'vitepress:local-search-detailed-list',
false
)
const results: Ref<(SearchResult & Result)[]> = shallowRef([])
@ -66,92 +92,100 @@ watch(filterText, () => {
enableNoResults.value = false
})
debouncedWatch(() => [searchIndex.value, filterText.value, showDetailedList.value] as const, async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
let canceled = false
onCleanup(() => {
canceled = true
})
debouncedWatch(
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
let canceled = false
onCleanup(() => {
canceled = true
})
if (!index) return
// Search
results.value = index.search(filterTextValue).slice(0, 16) as (SearchResult & Result)[]
enableNoResults.value = true
// Highlighting
const mods = showDetailedListValue ? await Promise.all(results.value.map(r => fetchExcerpt(r.id))) : []
if (canceled) return
const c = new Map<string, Map<string, string>>()
for (const { id, mod } of mods) {
const comp = mod.default ?? mod
if (comp?.render) {
const app = createApp(comp)
// Silence warnings about missing components
app.config.warnHandler = () => {}
const div = document.createElement('div')
app.mount(div)
const sections = div.innerHTML.split(headingRegex)
app.unmount()
sections.shift()
const mapId = id.slice(0, id.indexOf('#'))
let map = c.get(mapId)
if (!map) {
map = new Map()
c.set(mapId, map)
}
for (let i = 0; i < sections.length; i += 3) {
const anchor = sections[i + 1]
const html = sections[i + 2]
map.set(anchor, html)
}
}
if (!index) return
// Search
results.value = index
.search(filterTextValue)
.slice(0, 16) as (SearchResult & Result)[]
enableNoResults.value = true
// Highlighting
const mods = showDetailedListValue
? await Promise.all(results.value.map((r) => fetchExcerpt(r.id)))
: []
if (canceled) return
}
results.value = results.value.map(r => {
let title = r.title
let titles = r.titles
let text = ''
// Highlight in text
const [id, anchor] = r.id.split('#')
const map = c.get(id)
if (map) {
text = map.get(anchor) ?? ''
const c = new Map<string, Map<string, string>>()
for (const { id, mod } of mods) {
const comp = mod.default ?? mod
if (comp?.render) {
const app = createApp(comp)
// Silence warnings about missing components
app.config.warnHandler = () => {}
const div = document.createElement('div')
app.mount(div)
const sections = div.innerHTML.split(headingRegex)
app.unmount()
sections.shift()
const mapId = id.slice(0, id.indexOf('#'))
let map = c.get(mapId)
if (!map) {
map = new Map()
c.set(mapId, map)
}
for (let i = 0; i < sections.length; i += 3) {
const anchor = sections[i + 1]
const html = sections[i + 2]
map.set(anchor, html)
}
}
if (canceled) return
}
for (const term in r.match) {
const match = r.match[term]
const reg = new RegExp(term, 'gi')
if (match.includes('title')) {
title = title.replace(reg, `<mark>$&</mark>`)
results.value = results.value.map((r) => {
let title = r.title
let titles = r.titles
let text = ''
// Highlight in text
const [id, anchor] = r.id.split('#')
const map = c.get(id)
if (map) {
text = map.get(anchor) ?? ''
}
if (match.includes('titles')) {
titles = titles.map(t => t.replace(reg, `<mark>$&</mark>`))
for (const term in r.match) {
const match = r.match[term]
const reg = new RegExp(term, 'gi')
if (match.includes('title')) {
title = title.replace(reg, `<mark>$&</mark>`)
}
if (match.includes('titles')) {
titles = titles.map((t) => t.replace(reg, `<mark>$&</mark>`))
}
if (showDetailedListValue && match.includes('text')) {
text = text.replace(reg, `<mark>$&</mark>`)
}
}
if (showDetailedListValue && match.includes('text')) {
text = text.replace(reg, `<mark>$&</mark>`)
return {
...r,
title,
titles,
text
}
}
})
contents.value = c
return {
...r,
title,
titles,
text,
await nextTick()
const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? []
for (const excerpt of excerpts) {
excerpt.querySelector('mark')?.scrollIntoView({
block: 'center'
})
}
})
contents.value = c
await nextTick()
const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? []
for (const excerpt of excerpts) {
excerpt.querySelector('mark')?.scrollIntoView({
block: 'center',
})
}
}, { debounce: 200, immediate: true })
},
{ debounce: 200, immediate: true }
)
async function fetchExcerpt (id: string) {
async function fetchExcerpt(id: string) {
const file = pathToFile(withBase(id.slice(0, id.indexOf('#'))))
try {
return { id, mod: await import(/*@vite-ignore*/ file) }
@ -165,7 +199,7 @@ async function fetchExcerpt (id: string) {
const searchInput = ref<HTMLInputElement>()
function focusSearchInput () {
function focusSearchInput() {
searchInput.value?.focus()
searchInput.value?.select()
}
@ -174,7 +208,7 @@ onMounted(() => {
focusSearchInput()
})
function onSearchBarClick (event: PointerEvent) {
function onSearchBarClick(event: PointerEvent) {
if (event.pointerType === 'mouse') {
focusSearchInput()
}
@ -190,12 +224,12 @@ watch(results, () => {
scrollToSelectedResult()
})
function scrollToSelectedResult () {
function scrollToSelectedResult() {
nextTick(() => {
const selectedEl = document.querySelector('.result.selected')
if (selectedEl) {
selectedEl.scrollIntoView({
block: 'nearest',
block: 'nearest'
})
}
})
@ -250,12 +284,12 @@ const defaultTranslations: { modal: ModalTranslations } = {
selectKeyAriaLabel: 'enter',
navigateText: 'to navigate',
navigateUpKeyAriaLabel: 'up arrow',
navigateDownKeyAriaLabel: 'down arrow',
navigateDownKeyAriaLabel: 'down arrow'
}
}
}
const $t = createTranslate(theme.value.localSearch, defaultTranslations)
const $t = createTranslate(theme.value.search?.options, defaultTranslations)
// Back
@ -264,7 +298,7 @@ onMounted(() => {
window.history.pushState(null, '', null)
})
useEventListener('popstate', event => {
useEventListener('popstate', (event) => {
event.preventDefault()
emit('close')
})
@ -277,14 +311,34 @@ useEventListener('popstate', event => {
<div class="shell">
<div class="search-bar" @pointerup="onSearchBarClick($event)">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21l-4.35-4.35"/></g></svg>
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24">
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21l-4.35-4.35" />
</g>
</svg>
<div class="search-actions before">
<button
class="back-button"
:title="$t('modal.backButtonTitle')"
@click="$emit('close')"
>
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 12H5m7 7l-7-7l7-7"/></svg>
<svg width="16" height="16" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 12H5m7 7l-7-7l7-7"
/>
</svg>
</button>
</div>
<input
@ -292,17 +346,26 @@ useEventListener('popstate', event => {
v-model="filterText"
:placeholder="placeholder"
class="search-input"
>
/>
<div class="search-actions">
<button
class="toggle-layout-button"
:class="{
'detailed-list': showDetailedList,
'detailed-list': showDetailedList
}"
:title="$t('modal.displayDetails')"
@click="showDetailedList = !showDetailedList"
>
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 14h7v7H3zM3 3h7v7H3zm11 1h7m-7 5h7m-7 6h7m-7 5h7"/></svg>
<svg width="16" height="16" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 14h7v7H3zM3 3h7v7H3zm11 1h7m-7 5h7m-7 6h7m-7 5h7"
/>
</svg>
</button>
<button
@ -310,22 +373,28 @@ useEventListener('popstate', event => {
:title="$t('modal.resetButtonTitle')"
@click="filterText = ''"
>
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 5H9l-7 7l7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm-2 4l-6 6m0-6l6 6"/></svg>
<svg width="16" height="16" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20 5H9l-7 7l7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm-2 4l-6 6m0-6l6 6"
/>
</svg>
</button>
</div>
</div>
<div
class="results"
@mousemove="disableMouseOver = false"
>
<div class="results" @mousemove="disableMouseOver = false">
<a
v-for="(p, index) in results"
:key="p.id"
:href="withBase(p.id)"
class="result"
:class="{
selected: selectedIndex === index,
selected: selectedIndex === index
}"
:aria-title="[...p.titles, p.title].join(' > ')"
@mouseenter="!disableMouseOver && (selectedIndex = index)"
@ -334,19 +403,24 @@ useEventListener('popstate', event => {
<div>
<div class="titles">
<span class="title-icon">#</span>
<span
v-for="(t, index) in p.titles"
:key="index"
class="title"
>
<span v-for="(t, index) in p.titles" :key="index" class="title">
<span class="text" v-html="t" />
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 18l6-6l-6-6"/></svg>
<svg width="16" height="16" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 18l6-6l-6-6"
/>
</svg>
</span>
<span class="title main">
<span class="text" v-html="p.title" />
</span>
</div>
<div v-if="showDetailedList" class="excerpt-wrapper">
<div v-if="p.text" class="excerpt">
<div class="vp-doc" v-html="p.text" />
@ -357,18 +431,59 @@ useEventListener('popstate', event => {
</div>
</a>
<div v-if="filterText && !results.length && enableNoResults" class="no-results">
{{ $t('modal.noResultsText') }} "<strong>{{ filterText }}</strong>"
<div
v-if="filterText && !results.length && enableNoResults"
class="no-results"
>
{{ $t('modal.noResultsText') }} "<strong>{{ filterText }}</strong
>"
</div>
</div>
<div class="search-keyboard-shortcuts">
<span>
<kbd :aria-title="$t('modal.footer.navigateUpKeyAriaLabel')"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19V5m-7 7l7-7l7 7"/></svg></kbd> <kbd :aria-title="$t('modal.footer.navigateDownKeyAriaLabel')"><svg width="14" height="14" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m7-7l-7 7l-7-7"/></svg></kbd>
<kbd :aria-title="$t('modal.footer.navigateUpKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 19V5m-7 7l7-7l7 7"
/>
</svg>
</kbd>
{{ ' ' }}
<kbd :aria-title="$t('modal.footer.navigateDownKeyAriaLabel')">
<svg width="14" height="14" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m7-7l-7 7l-7-7"
/>
</svg>
</kbd>
{{ $t('modal.footer.navigateText') }}
</span>
<span>
<kbd :aria-title="$t('modal.footer.selectKeyAriaLabel')"><svg width="14" height="14" viewBox="0 0 24 24"><g fill="none" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="m9 10l-5 5l5 5"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></g></svg></kbd>
<kbd :aria-title="$t('modal.footer.selectKeyAriaLabel')"
><svg width="14" height="14" viewBox="0 0 24 24">
<g
fill="none"
stroke="currentcolor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="m9 10l-5 5l5 5" />
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
</g>
</svg>
</kbd>
{{ $t('modal.footer.selectText') }}
</span>
</div>

@ -8,17 +8,18 @@ import {
onUnmounted,
ref
} from 'vue'
import type { DefaultTheme } from '../../shared'
import { useData } from '../composables/data'
import VPNavBarSearchButton from './VPNavBarSearchButton.vue'
const VPLocalSearchBox = __VP_LOCAL_SEARCH__
? defineAsyncComponent(() => import('./VPLocalSearchBox.vue'))
: () => null
const VPAlgoliaSearchBox = __ALGOLIA__
? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue'))
: () => null
const VPLocalSearchBox = __ALGOLIA__
? () => null
: defineAsyncComponent(() => import('./VPLocalSearchBox.vue'))
const { theme, localeIndex } = useData()
// to avoid loading the docsearch js upfront (which is more than 1/3 of the
@ -27,17 +28,13 @@ const { theme, localeIndex } = useData()
const loaded = ref(false)
const buttonText = computed(() => {
if (theme.value.algolia) {
return theme.value.algolia.locales?.[localeIndex.value]?.translations?.button
?.buttonText ||
theme.value.algolia.translations?.button?.buttonText ||
'Search'
} else if (typeof theme.value.localSearch === 'object') {
return theme.value.localSearch.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
theme.value.localSearch.translations?.button?.buttonText ||
'Search'
}
return 'Search'
const options = theme.value.search?.options ?? theme.value.algolia
return (
options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
options?.translations?.button?.buttonText ||
'Search'
)
})
const preconnect = () => {
@ -48,14 +45,17 @@ const preconnect = () => {
const preconnect = document.createElement('link')
preconnect.id = id
preconnect.rel = 'preconnect'
preconnect.href = `https://${theme.value.algolia!.appId}-dsn.algolia.net`
preconnect.href = `https://${
((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
theme.value.algolia)!.appId
}-dsn.algolia.net`
preconnect.crossOrigin = ''
document.head.appendChild(preconnect)
})
}
onMounted(() => {
if (!theme.value.algolia) {
if (!__ALGOLIA__) {
return
}
@ -105,8 +105,8 @@ function poll() {
const showSearch = ref(false)
if (!__ALGOLIA__ && theme.value.localSearch) {
onKeyStroke('k', event => {
if (__VP_LOCAL_SEARCH__) {
onKeyStroke('k', (event) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault()
showSearch.value = true
@ -122,23 +122,35 @@ onMounted(() => {
? `'⌘'`
: `'Ctrl'`
})
const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
</script>
<template>
<div class="VPNavBarSearch" :style="{ '--vp-meta-key': metaKey }">
<template v-if="theme.algolia">
<VPAlgoliaSearchBox v-if="loaded" :algolia="theme.algolia" />
<template v-if="provider === 'local'">
<VPLocalSearchBox
v-if="showSearch"
:placeholder="buttonText"
@close="showSearch = false"
/>
<div v-else id="docsearch">
<VPNavBarSearchButton :placeholder="buttonText" @click="load" />
<div id="local-search">
<VPNavBarSearchButton
:placeholder="buttonText"
@click="showSearch = true"
/>
</div>
</template>
<template v-else-if="theme.localSearch">
<VPLocalSearchBox v-if="showSearch" :placeholder="buttonText" @close="showSearch = false" />
<div id="local-search">
<VPNavBarSearchButton :placeholder="buttonText" @click="showSearch = true" />
<template v-else-if="provider === 'algolia'">
<VPAlgoliaSearchBox
v-if="loaded"
:algolia="theme.search?.options ?? theme.algolia"
/>
<div v-else id="docsearch">
<VPNavBarSearchButton :placeholder="buttonText" @click="load" />
</div>
</template>
</div>

@ -5,11 +5,7 @@ defineProps<{
</script>
<template>
<button
type="button"
class="DocSearch DocSearch-Button"
aria-label="Search"
>
<button type="button" class="DocSearch DocSearch-Button" aria-label="Search">
<span class="DocSearch-Button-Container">
<svg
class="DocSearch-Search-Icon"

@ -21,7 +21,7 @@ export const failMark = '\x1b[31m✖\x1b[0m'
// A list of default theme components that should only be loaded on demand.
const lazyDefaultThemeComponentsRE =
/VP(HomeSponsors|DocAsideSponsors|TeamPage|TeamMembers|AlgoliaSearch|CarbonAds|DocAsideCarbonAds)/
/VP(HomeSponsors|DocAsideSponsors|TeamPage|TeamMembers|LocalSearchBox|AlgoliaSearchBox|CarbonAds|DocAsideCarbonAds)/
const clientDir = normalizePath(
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client')

@ -22,7 +22,7 @@ import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin'
import { rewritesPlugin } from './plugins/rewritesPlugin'
import { localSearchPlugin } from './plugins/localSearchPlugin.js'
import { localSearchPlugin } from './plugins/localSearchPlugin'
import { serializeFunctions, deserializeFunctions } from './utils/fnSerialize'
declare module 'vite' {
@ -122,8 +122,11 @@ export async function createVitePressPlugin(
alias: resolveAliases(siteConfig, ssr)
},
define: {
__ALGOLIA__: !!site.themeConfig.algolia,
__CARBON__: !!site.themeConfig.carbonAds
__VP_LOCAL_SEARCH__: site.themeConfig?.search?.provider === 'local',
__ALGOLIA__:
site.themeConfig?.search?.provider === 'algolia' ||
!!site.themeConfig?.algolia, // legacy
__CARBON__: !!site.themeConfig?.carbonAds
},
optimizeDeps: {
// force include vue to avoid duplicated copies when linked + optimized

@ -4,13 +4,13 @@ import MiniSearch from 'minisearch'
import fs from 'fs-extra'
import _debug from 'debug'
import type { SiteConfig } from '../config'
import { createMarkdownRenderer } from '../markdown/markdown.js'
import { resolveSiteDataByRoute } from '../shared.js'
import { createMarkdownRenderer } from '../markdown/markdown'
import { resolveSiteDataByRoute } from '../shared'
const debug = _debug('vitepress:local-search')
const OFFLINE_SEARCH_INDEX_ID = '@localSearchIndex'
const OFFLINE_SEARCH_INDEX_REQUEST_PATH = '/' + OFFLINE_SEARCH_INDEX_ID
const LOCAL_SEARCH_INDEX_ID = '@localSearchIndex'
const LOCAL_SEARCH_INDEX_REQUEST_PATH = '/' + LOCAL_SEARCH_INDEX_ID
interface IndexObject {
id: string
@ -22,19 +22,16 @@ interface IndexObject {
export async function localSearchPlugin(
siteConfig: SiteConfig
): Promise<Plugin> {
if (
siteConfig.userConfig.themeConfig?.algolia ||
!siteConfig.userConfig.themeConfig?.localSearch
) {
if (siteConfig.userConfig.themeConfig?.search?.provider !== 'local') {
return {
name: 'vitepress:local-search',
resolveId(id) {
if (id.startsWith(OFFLINE_SEARCH_INDEX_ID)) {
if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
return `/${id}`
}
},
load(id) {
if (id.startsWith(OFFLINE_SEARCH_INDEX_REQUEST_PATH)) {
if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
return `export default '{}'`
}
}
@ -77,10 +74,10 @@ export async function localSearchPlugin(
async function onIndexUpdated() {
if (server) {
server.moduleGraph.onFileChange(OFFLINE_SEARCH_INDEX_REQUEST_PATH)
server.moduleGraph.onFileChange(LOCAL_SEARCH_INDEX_REQUEST_PATH)
// HMR
const mod = server.moduleGraph.getModuleById(
OFFLINE_SEARCH_INDEX_REQUEST_PATH
LOCAL_SEARCH_INDEX_REQUEST_PATH
)
if (!mod) return
server.ws.send({
@ -113,7 +110,7 @@ export async function localSearchPlugin(
.map(async (file) => {
const fileId = getDocId(file)
const sections = splitPageIntoSections(
await md.render(await fs.readFile(file, 'utf-8'))
md.render(await fs.readFile(file, 'utf-8'))
)
const locale = getLocaleForPath(file)
let documents = documentsByLocale.get(locale)
@ -167,13 +164,13 @@ export async function localSearchPlugin(
},
resolveId(id) {
if (id.startsWith(OFFLINE_SEARCH_INDEX_ID)) {
if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
return `/${id}`
}
},
async load(id) {
if (id === OFFLINE_SEARCH_INDEX_REQUEST_PATH) {
if (id === LOCAL_SEARCH_INDEX_REQUEST_PATH) {
if (process.env.NODE_ENV === 'production') {
await scanForBuild()
}
@ -186,11 +183,11 @@ export async function localSearchPlugin(
)
}
return `export default {${records.join(',')}}`
} else if (id.startsWith(OFFLINE_SEARCH_INDEX_REQUEST_PATH)) {
} else if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
return `export default ${JSON.stringify(
JSON.stringify(
indexByLocales.get(
id.replace(OFFLINE_SEARCH_INDEX_REQUEST_PATH, '')
id.replace(LOCAL_SEARCH_INDEX_REQUEST_PATH, '')
) ?? {}
)
)}`

@ -11,7 +11,7 @@ export type {
PageDataPayload,
SiteData,
SSGContext
} from '../../types/shared.js'
} from '../../types/shared'
export const EXTERNAL_URL_RE = /^[a-z]+:/i
export const PATHNAME_PROTOCOL_RE = /^pathname:\/\//

@ -101,15 +101,14 @@ export namespace DefaultTheme {
*/
langMenuLabel?: string
/**
* The algolia options. Leave it undefined to disable the search feature.
*/
algolia?: AlgoliaSearchOptions
search?:
| { provider: 'local'; options?: LocalSearchOptions }
| { provider: 'algolia'; options: AlgoliaSearchOptions }
/**
* The local search options. Set to `true` or an object to enable, `false` to disable.
* @deprecated Use `search` instead.
*/
localSearch?: LocalSearchOptions | boolean
algolia?: AlgoliaSearchOptions
/**
* The carbon ads options. Leave it undefined to disable the ads feature.
@ -291,6 +290,13 @@ export namespace DefaultTheme {
label?: string
}
// local search --------------------------------------------------------------
export interface LocalSearchOptions {
translations?: LocalSearchTranslations
locales?: Record<string, Partial<Omit<LocalSearchOptions, 'locales'>>>
}
// algolia -------------------------------------------------------------------
/**
@ -301,13 +307,6 @@ export namespace DefaultTheme {
locales?: Record<string, Partial<DocSearchProps>>
}
// local search ------------------------------------------------------------
export interface LocalSearchOptions {
translations?: LocalSearchTranslations
locales?: Record<string, Partial<Omit<LocalSearchOptions, 'locales'>>>
}
// carbon ads ----------------------------------------------------------------
export interface CarbonAdsOptions {

Loading…
Cancel
Save