-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -154,178 +175,6 @@ function poll() {
}
}
-.DocSearch {
- --docsearch-primary-color: var(--vp-c-brand);
- --docsearch-highlight-color: var(--docsearch-primary-color);
- --docsearch-text-color: var(--vp-c-text-1);
- --docsearch-muted-color: var(--vp-c-text-2);
- --docsearch-searchbox-shadow: none;
- --docsearch-searchbox-focus-background: transparent;
- --docsearch-key-gradient: transparent;
- --docsearch-key-shadow: none;
- --docsearch-modal-background: var(--vp-c-bg-soft);
- --docsearch-footer-background: var(--vp-c-bg);
-}
-
-.dark .DocSearch {
- --docsearch-modal-shadow: none;
- --docsearch-footer-shadow: none;
- --docsearch-logo-color: var(--vp-c-text-2);
- --docsearch-hit-background: var(--vp-c-bg-soft-mute);
- --docsearch-hit-color: var(--vp-c-text-2);
- --docsearch-hit-shadow: none;
-}
-
-.DocSearch-Button {
- display: flex;
- justify-content: center;
- align-items: center;
- margin: 0;
- padding: 0;
- width: 32px;
- height: 55px;
- background: transparent;
- transition: border-color 0.25s;
-}
-
-.DocSearch-Button:hover {
- background: transparent;
-}
-
-.DocSearch-Button:focus {
- outline: 1px dotted;
- outline: 5px auto -webkit-focus-ring-color;
-}
-
-.DocSearch-Button:focus:not(:focus-visible) {
- outline: none !important;
-}
-
-@media (min-width: 768px) {
- .DocSearch-Button {
- justify-content: flex-start;
- border: 1px solid transparent;
- border-radius: 8px;
- padding: 0 10px 0 12px;
- width: 100%;
- height: 40px;
- background-color: var(--vp-c-bg-alt);
- }
-
- .DocSearch-Button:hover {
- border-color: var(--vp-c-brand);
- background: var(--vp-c-bg-alt);
- }
-}
-
-.DocSearch-Button .DocSearch-Button-Container {
- display: flex;
- align-items: center;
-}
-
-.DocSearch-Button .DocSearch-Search-Icon {
- position: relative;
- width: 16px;
- height: 16px;
- color: var(--vp-c-text-1);
- fill: currentColor;
- transition: color 0.5s;
-}
-
-.DocSearch-Button:hover .DocSearch-Search-Icon {
- color: var(--vp-c-text-1);
-}
-
-@media (min-width: 768px) {
- .DocSearch-Button .DocSearch-Search-Icon {
- top: 1px;
- margin-right: 8px;
- width: 14px;
- height: 14px;
- color: var(--vp-c-text-2);
- }
-}
-
-.DocSearch-Button .DocSearch-Button-Placeholder {
- display: none;
- margin-top: 2px;
- padding: 0 16px 0 0;
- font-size: 13px;
- font-weight: 500;
- color: var(--vp-c-text-2);
- transition: color 0.5s;
-}
-
-.DocSearch-Button:hover .DocSearch-Button-Placeholder {
- color: var(--vp-c-text-1);
-}
-
-@media (min-width: 768px) {
- .DocSearch-Button .DocSearch-Button-Placeholder {
- display: inline-block;
- }
-}
-
-.DocSearch-Button .DocSearch-Button-Keys {
- /*rtl:ignore*/
- direction: ltr;
- display: none;
- min-width: auto;
-}
-
-@media (min-width: 768px) {
- .DocSearch-Button .DocSearch-Button-Keys {
- display: flex;
- align-items: center;
- }
-}
-
-.DocSearch-Button .DocSearch-Button-Key {
- display: block;
- margin: 2px 0 0 0;
- border: 1px solid var(--vp-c-divider);
- /*rtl:begin:ignore*/
- border-right: none;
- border-radius: 4px 0 0 4px;
- padding-left: 6px;
- /*rtl:end:ignore*/
- min-width: 0;
- width: auto;
- height: 22px;
- line-height: 22px;
- font-family: var(--vp-font-family-base);
- font-size: 12px;
- font-weight: 500;
- transition: color 0.5s, border-color 0.5s;
-}
-
-.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
- /*rtl:begin:ignore*/
- border-right: 1px solid var(--vp-c-divider);
- border-left: none;
- border-radius: 0 4px 4px 0;
- padding-left: 2px;
- padding-right: 6px;
- /*rtl:end:ignore*/
-}
-
-.DocSearch-Button .DocSearch-Button-Key:first-child {
- font-size: 1px;
- letter-spacing: -12px;
- color: transparent;
-}
-
-.DocSearch-Button .DocSearch-Button-Key:first-child:after {
- content: v-bind(metaKey);
- font-size: 12px;
- letter-spacing: normal;
- color: var(--docsearch-muted-color);
-}
-
-.DocSearch-Button .DocSearch-Button-Key:first-child > * {
- display: none;
-}
-
.dark .DocSearch-Footer {
border-top: 1px solid var(--vp-c-divider);
}
diff --git a/src/client/theme-default/components/VPNavBarSearchButton.vue b/src/client/theme-default/components/VPNavBarSearchButton.vue
new file mode 100644
index 000000000000..f3436a42ef61
--- /dev/null
+++ b/src/client/theme-default/components/VPNavBarSearchButton.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
diff --git a/src/client/theme-default/components/VPSidebar.vue b/src/client/theme-default/components/VPSidebar.vue
index 7e093bbea262..16f0aadd860a 100644
--- a/src/client/theme-default/components/VPSidebar.vue
+++ b/src/client/theme-default/components/VPSidebar.vue
@@ -74,6 +74,7 @@ watchPostEffect(async () => {
overflow-y: auto;
transform: translateX(-100%);
transition: opacity 0.5s, transform 0.25s ease;
+ overscroll-behavior: contain;
}
.VPSidebar.open {
diff --git a/src/client/theme-default/styles/components/custom-block.css b/src/client/theme-default/styles/components/custom-block.css
index 4d900c58758f..373b3a3af1c8 100644
--- a/src/client/theme-default/styles/components/custom-block.css
+++ b/src/client/theme-default/styles/components/custom-block.css
@@ -13,8 +13,10 @@
background-color: var(--vp-custom-block-info-bg);
}
-.custom-block.info th {
- color: var(--vp-custom-block-info-text);
+.custom-block.custom-block th,
+.custom-block.custom-block blockquote > p {
+ font-size: var(--vp-custom-block-font-size);
+ color: inherit;
}
.custom-block.info code {
@@ -27,10 +29,6 @@
background-color: var(--vp-custom-block-tip-bg);
}
-.custom-block.tip th {
- color: var(--vp-custom-block-tip-text);
-}
-
.custom-block.tip code {
background-color: var(--vp-custom-block-tip-code-bg);
}
@@ -41,10 +39,6 @@
background-color: var(--vp-custom-block-warning-bg);
}
-.custom-block.warning th {
- color: var(--vp-custom-block-warning-text);
-}
-
.custom-block.warning code {
background-color: var(--vp-custom-block-warning-code-bg);
}
@@ -55,10 +49,6 @@
background-color: var(--vp-custom-block-danger-bg);
}
-.custom-block.danger th {
- color: var(--vp-custom-block-danger-text);
-}
-
.custom-block.danger code {
background-color: var(--vp-custom-block-danger-code-bg);
}
@@ -69,10 +59,6 @@
background-color: var(--vp-custom-block-details-bg);
}
-.custom-block.details th {
- color: var(--vp-custom-block-details-text);
-}
-
.custom-block.details code {
background-color: var(--vp-custom-block-details-code-bg);
}
diff --git a/src/client/theme-default/styles/vars.css b/src/client/theme-default/styles/vars.css
index f68f39231dbb..cb709be284ec 100644
--- a/src/client/theme-default/styles/vars.css
+++ b/src/client/theme-default/styles/vars.css
@@ -401,7 +401,7 @@
}
/**
- * Component: CarbonAds
+ * Component: Carbon Ads
* -------------------------------------------------------------------------- */
:root {
@@ -411,3 +411,16 @@
--vp-carbon-ads-hover-text-color: var(--vp-c-brand);
--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1);
}
+
+/**
+ * Component: Local Search
+ * -------------------------------------------------------------------------- */
+:root {
+ --vp-local-search-bg: var(--vp-c-bg);
+ --vp-local-search-result-bg: var(--vp-c-bg);
+ --vp-local-search-result-border: var(--vp-c-divider);
+ --vp-local-search-result-selected-bg: var(--vp-c-bg);
+ --vp-local-search-result-selected-border: var(--vp-c-brand);
+ --vp-local-search-highlight-bg: var(--vp-c-green-lighter);
+ --vp-local-search-highlight-text: var(--vp-c-black);
+}
diff --git a/src/client/theme-default/support/translation.ts b/src/client/theme-default/support/translation.ts
new file mode 100644
index 000000000000..607d5db3c118
--- /dev/null
+++ b/src/client/theme-default/support/translation.ts
@@ -0,0 +1,60 @@
+import { useData } from '../composables/data'
+
+/**
+ * @param themeObject Can be an object with `translations` and `locales` properties
+ */
+export function createTranslate(
+ themeObject: any,
+ defaultTranslations: Record
+): (key: string) => string {
+ const { localeIndex } = useData()
+
+ function translate(key: string): string {
+ const keyPath = key.split('.')
+
+ const isObject = themeObject && typeof themeObject === 'object'
+ const locales =
+ (isObject && themeObject.locales?.[localeIndex.value]?.translations) ||
+ null
+ const translations = (isObject && themeObject.translations) || null
+
+ let localeResult: Record | null = locales
+ let translationResult: Record | null = translations
+ let defaultResult: Record | null = defaultTranslations
+
+ const lastKey = keyPath.pop()!
+ for (const k of keyPath) {
+ let fallbackResult: Record | null = null
+ const foundInFallback: any = defaultResult?.[k]
+ if (foundInFallback) {
+ fallbackResult = defaultResult = foundInFallback
+ }
+ const foundInTranslation: any = translationResult?.[k]
+ if (foundInTranslation) {
+ fallbackResult = translationResult = foundInTranslation
+ }
+ const foundInLocale: any = localeResult?.[k]
+ if (foundInLocale) {
+ fallbackResult = localeResult = foundInLocale
+ }
+ // Put fallback into unresolved results
+ if (!foundInFallback) {
+ defaultResult = fallbackResult
+ }
+ if (!foundInTranslation) {
+ translationResult = fallbackResult
+ }
+ if (!foundInLocale) {
+ localeResult = fallbackResult
+ }
+ }
+ return (
+ localeResult?.[lastKey] ??
+ translationResult?.[lastKey] ??
+ defaultResult?.[lastKey] ??
+ ''
+ )
+ }
+
+ return translate
+}
diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts
index c42f7d46038d..654f8b8d7ee1 100644
--- a/src/node/build/bundle.ts
+++ b/src/node/build/bundle.ts
@@ -7,11 +7,10 @@ import {
type UserConfig as ViteUserConfig
} from 'vite'
import type { GetModuleInfo, RollupOutput } from 'rollup'
-import { slash } from '../utils/slash'
import type { SiteConfig } from '../config'
import { APP_PATH } from '../alias'
import { createVitePressPlugin } from '../plugin'
-import { sanitizeFileName } from '../shared'
+import { sanitizeFileName, slash } from '../shared'
import { buildMPAClient } from './buildMPAClient'
import { fileURLToPath } from 'url'
import { normalizePath } from 'vite'
@@ -21,7 +20,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')
diff --git a/src/node/build/render.ts b/src/node/build/render.ts
index 4b63535e6023..b3be8cc9a256 100644
--- a/src/node/build/render.ts
+++ b/src/node/build/render.ts
@@ -12,11 +12,11 @@ import {
notFoundPageData,
resolveSiteDataByRoute,
sanitizeFileName,
+ slash,
type HeadConfig,
type PageData,
type SSGContext
} from '../shared'
-import { slash } from '../utils/slash'
import { deserializeFunctions } from '../utils/fnSerialize'
export async function renderPage(
diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts
index 5e17c63a91b2..056ae1b5e6ee 100644
--- a/src/node/markdownToVue.ts
+++ b/src/node/markdownToVue.ts
@@ -1,7 +1,7 @@
import { resolveTitleFromToken } from '@mdit-vue/shared'
import _debug from 'debug'
import fs from 'fs'
-import LRUCache from 'lru-cache'
+import { LRUCache } from 'lru-cache'
import path from 'path'
import c from 'picocolors'
import type { SiteConfig } from './config'
@@ -11,9 +11,13 @@ import {
type MarkdownOptions,
type MarkdownRenderer
} from './markdown'
-import { EXTERNAL_URL_RE, type HeadConfig, type PageData } from './shared'
+import {
+ EXTERNAL_URL_RE,
+ slash,
+ type HeadConfig,
+ type PageData
+} from './shared'
import { getGitTimestamp } from './utils/getGitTimestamp'
-import { slash } from './utils/slash'
const debug = _debug('vitepress:md')
const cache = new LRUCache({ max: 1024 })
diff --git a/src/node/plugin.ts b/src/node/plugin.ts
index 9e8a6617ad85..c6ab131f1a88 100644
--- a/src/node/plugin.ts
+++ b/src/node/plugin.ts
@@ -1,6 +1,5 @@
import path from 'path'
import c from 'picocolors'
-import { slash } from './utils/slash'
import type { OutputAsset, OutputChunk } from 'rollup'
import {
mergeConfig,
@@ -17,11 +16,12 @@ import {
} from './alias'
import { resolveUserConfig, resolvePages, type SiteConfig } from './config'
import { clearCache, createMarkdownToVueRenderFn } from './markdownToVue'
-import type { PageDataPayload } from './shared'
+import { slash, type PageDataPayload } from './shared'
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'
import { serializeFunctions, deserializeFunctions } from './utils/fnSerialize'
declare module 'vite' {
@@ -121,8 +121,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
@@ -360,6 +363,7 @@ export async function createVitePressPlugin(
vuePlugin,
webFontsPlugin(siteConfig.useWebFonts),
...(userViteConfig?.plugins || []),
+ await localSearchPlugin(siteConfig),
staticDataPlugin,
await dynamicRoutesPlugin(siteConfig)
]
diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts
new file mode 100644
index 000000000000..c12d03d36061
--- /dev/null
+++ b/src/node/plugins/localSearchPlugin.ts
@@ -0,0 +1,260 @@
+import path from 'node:path'
+import type { Plugin, ViteDevServer } from 'vite'
+import MiniSearch from 'minisearch'
+import fs from 'fs-extra'
+import _debug from 'debug'
+import type { SiteConfig } from '../config'
+import { createMarkdownRenderer } from '../markdown/markdown'
+import { resolveSiteDataByRoute } from '../shared'
+
+const debug = _debug('vitepress:local-search')
+
+const LOCAL_SEARCH_INDEX_ID = '@localSearchIndex'
+const LOCAL_SEARCH_INDEX_REQUEST_PATH = '/' + LOCAL_SEARCH_INDEX_ID
+
+interface IndexObject {
+ id: string
+ text: string
+ title: string
+ titles: string[]
+}
+
+export async function localSearchPlugin(
+ siteConfig: SiteConfig
+): Promise {
+ if (siteConfig.site.themeConfig?.search?.provider !== 'local') {
+ return {
+ name: 'vitepress:local-search',
+ resolveId(id) {
+ if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
+ return `/${id}`
+ }
+ },
+ load(id) {
+ if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
+ return `export default '{}'`
+ }
+ }
+ }
+ }
+
+ const md = await createMarkdownRenderer(
+ siteConfig.srcDir,
+ siteConfig.markdown,
+ siteConfig.site.base,
+ siteConfig.logger
+ )
+
+ const indexByLocales = new Map>()
+
+ function getIndexByLocale(locale: string) {
+ let index = indexByLocales.get(locale)
+ if (!index) {
+ index = new MiniSearch({
+ fields: ['title', 'titles', 'text'],
+ storeFields: ['title', 'titles']
+ })
+ indexByLocales.set(locale, index)
+ }
+ return index
+ }
+
+ function getLocaleForPath(file: string) {
+ const relativePath = path.relative(siteConfig.srcDir, file)
+ const siteData = resolveSiteDataByRoute(siteConfig.site, relativePath)
+ return siteData?.localeIndex ?? 'root'
+ }
+
+ function getIndexForPath(file: string) {
+ const locale = getLocaleForPath(file)
+ return getIndexByLocale(locale)
+ }
+
+ let server: ViteDevServer | undefined
+
+ function onIndexUpdated() {
+ if (server) {
+ server.moduleGraph.onFileChange(LOCAL_SEARCH_INDEX_REQUEST_PATH)
+ // HMR
+ const mod = server.moduleGraph.getModuleById(
+ LOCAL_SEARCH_INDEX_REQUEST_PATH
+ )
+ if (!mod) return
+ server.ws.send({
+ type: 'update',
+ updates: [
+ {
+ acceptedPath: mod.url,
+ path: mod.url,
+ timestamp: Date.now(),
+ type: 'js-update'
+ }
+ ]
+ })
+ }
+ }
+
+ function getDocId(file: string) {
+ let relFile = path.relative(siteConfig.srcDir, file)
+ relFile = siteConfig.rewrites.map[relFile] || relFile
+ let id = path.join(siteConfig.site.base, relFile)
+ id = id.replace(/\/index\.md$/, '/')
+ id = id.replace(/\.md$/, siteConfig.cleanUrls ? '' : '.html')
+ return id
+ }
+
+ async function indexAllFiles(files: string[]) {
+ const documentsByLocale = new Map()
+ await Promise.all(
+ files
+ .filter((file) => fs.existsSync(file))
+ .map(async (file) => {
+ const fileId = getDocId(file)
+ const sections = splitPageIntoSections(
+ md.render(await fs.readFile(file, 'utf-8'))
+ )
+ const locale = getLocaleForPath(file)
+ let documents = documentsByLocale.get(locale)
+ if (!documents) {
+ documents = []
+ documentsByLocale.set(locale, documents)
+ }
+ documents.push(
+ ...sections.map((section) => ({
+ id: `${fileId}#${section.anchor}`,
+ text: section.text,
+ title: section.titles.at(-1)!,
+ titles: section.titles.slice(0, -1)
+ }))
+ )
+ })
+ )
+ for (const [locale, documents] of documentsByLocale) {
+ const index = getIndexByLocale(locale)
+ index.removeAll()
+ await index.addAllAsync(documents)
+ }
+ debug(`🔍️ Indexed ${files.length} files`)
+ }
+
+ async function scanForBuild() {
+ await indexAllFiles(
+ siteConfig.pages.map((f) => path.join(siteConfig.srcDir, f))
+ )
+ }
+
+ return {
+ name: 'vitepress:local-search',
+
+ async configureServer(_server) {
+ server = _server
+ await scanForBuild()
+ onIndexUpdated()
+ },
+
+ resolveId(id) {
+ if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
+ return `/${id}`
+ }
+ },
+
+ async load(id) {
+ if (id === LOCAL_SEARCH_INDEX_REQUEST_PATH) {
+ if (process.env.NODE_ENV === 'production') {
+ await scanForBuild()
+ }
+ let records: string[] = []
+ for (const [locale] of indexByLocales) {
+ records.push(
+ `${JSON.stringify(
+ locale
+ )}: () => import('@localSearchIndex${locale}')`
+ )
+ }
+ return `export default {${records.join(',')}}`
+ } else if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
+ return `export default ${JSON.stringify(
+ JSON.stringify(
+ indexByLocales.get(
+ id.replace(LOCAL_SEARCH_INDEX_REQUEST_PATH, '')
+ ) ?? {}
+ )
+ )}`
+ }
+ },
+
+ async handleHotUpdate(ctx) {
+ if (ctx.file.endsWith('.md')) {
+ const fileId = getDocId(ctx.file)
+ if (!fs.existsSync(ctx.file)) {
+ return
+ }
+ const index = getIndexForPath(ctx.file)
+ const sections = splitPageIntoSections(
+ md.render(await fs.readFile(ctx.file, 'utf-8'))
+ )
+ for (const section of sections) {
+ const id = `${fileId}#${section.anchor}`
+ if (index.has(id)) {
+ index.discard(id)
+ }
+ index.add({
+ id,
+ text: section.text,
+ title: section.titles.at(-1)!,
+ titles: section.titles.slice(0, -1)
+ })
+ }
+ debug('🔍️ Updated', ctx.file)
+
+ onIndexUpdated()
+ }
+ }
+ }
+}
+
+const headingRegex = /(.*?.*?<\/a>)<\/h\1>/gi
+const headingContentRegex = /(.*?).*?<\/a>/i
+
+interface PageSection {
+ anchor: string
+ titles: string[]
+ text: string
+}
+
+/**
+ * Splits HTML into sections based on headings
+ */
+function splitPageIntoSections(html: string) {
+ const result = html.split(headingRegex)
+ result.shift()
+ let parentTitles: string[] = []
+ const sections: PageSection[] = []
+ for (let i = 0; i < result.length; i += 3) {
+ const level = parseInt(result[i]) - 1
+ const heading = result[i + 1]
+ const headingResult = headingContentRegex.exec(heading)
+ const title = clearHtmlTags(headingResult?.[1] ?? '').trim()
+ const anchor = headingResult?.[2] ?? ''
+ const content = result[i + 2]
+ if (!title || !content) continue
+ const titles = parentTitles.slice(0, level)
+ titles[level] = title
+ sections.push({ anchor, titles, text: getSearchableText(content) })
+ if (level === 0) {
+ parentTitles = [title]
+ } else {
+ parentTitles[level] = title
+ }
+ }
+ return sections
+}
+
+function getSearchableText(content: string) {
+ content = clearHtmlTags(content)
+ return content
+}
+
+function clearHtmlTags(str: string) {
+ return str.replace(/<[^>]*>/g, '')
+}
diff --git a/src/node/utils/slash.ts b/src/node/utils/slash.ts
deleted file mode 100644
index f06ed9399d04..000000000000
--- a/src/node/utils/slash.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function slash(p: string): string {
- return p.replace(/\\/g, '/')
-}
diff --git a/src/shared/shared.ts b/src/shared/shared.ts
index 872b0042eb15..ceab85875d77 100644
--- a/src/shared/shared.ts
+++ b/src/shared/shared.ts
@@ -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:\/\//
@@ -165,3 +165,7 @@ export function sanitizeFileName(name: string): string {
.replace(/(^|\/)_+(?=[^/]*$)/, '$1')
)
}
+
+export function slash(p: string): string {
+ return p.replace(/\\/g, '/')
+}
diff --git a/types/default-theme.d.ts b/types/default-theme.d.ts
index 520038493c3b..cf9f102eedfb 100644
--- a/types/default-theme.d.ts
+++ b/types/default-theme.d.ts
@@ -1,4 +1,5 @@
import { DocSearchProps } from './docsearch.js'
+import { LocalSearchTranslations } from './local-search.js'
export namespace DefaultTheme {
export interface Config {
@@ -100,8 +101,12 @@ export namespace DefaultTheme {
*/
langMenuLabel?: string
+ search?:
+ | { provider: 'local'; options?: LocalSearchOptions }
+ | { provider: 'algolia'; options: AlgoliaSearchOptions }
+
/**
- * The algolia options. Leave it undefined to disable the search feature.
+ * @deprecated Use `search` instead.
*/
algolia?: AlgoliaSearchOptions
@@ -285,6 +290,18 @@ export namespace DefaultTheme {
label?: string
}
+ // local search --------------------------------------------------------------
+
+ export interface LocalSearchOptions {
+ /**
+ * @default false
+ */
+ disableDetailedView?: boolean
+
+ translations?: LocalSearchTranslations
+ locales?: Record>>
+ }
+
// algolia -------------------------------------------------------------------
/**
diff --git a/types/local-search.d.ts b/types/local-search.d.ts
new file mode 100644
index 000000000000..fa7a28901a11
--- /dev/null
+++ b/types/local-search.d.ts
@@ -0,0 +1,27 @@
+export interface LocalSearchTranslations {
+ button?: ButtonTranslations
+ modal?: ModalTranslations
+}
+
+interface ButtonTranslations {
+ buttonText?: string
+ buttonAriaLabel?: string
+}
+
+interface ModalTranslations {
+ displayDetails?: string
+ resetButtonTitle?: string
+ backButtonTitle?: string
+ noResultsText?: string
+ footer?: FooterTranslations
+}
+
+interface FooterTranslations {
+ selectText?: string
+ selectKeyAriaLabel?: string
+ navigateText?: string
+ navigateUpKeyAriaLabel?: string
+ navigateDownKeyAriaLabel?: string
+ closeText?: string
+ closeKeyAriaLabel?: string
+}