diff --git a/__tests__/unit/client/theme-default/support/utils.test.ts b/__tests__/unit/client/theme-default/support/utils.test.ts deleted file mode 100644 index 880d4232c274..000000000000 --- a/__tests__/unit/client/theme-default/support/utils.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ensureStartingSlash } from 'client/theme-default/support/utils' - -describe('client/theme-default/utils', () => { - describe('ensureStartingSlash', () => { - test('it adds slash to the beginning of the given path', () => { - expect(ensureStartingSlash('path')).toBe('/path') - expect(ensureStartingSlash('path/nested')).toBe('/path/nested') - expect(ensureStartingSlash('/path')).toBe('/path') - expect(ensureStartingSlash('/path/nested')).toBe('/path/nested') - }) - }) -}) diff --git a/__tests__/unit/client/theme-default/support/sidebar.test.ts b/__tests__/unit/shared/shared.test.ts similarity index 86% rename from __tests__/unit/client/theme-default/support/sidebar.test.ts rename to __tests__/unit/shared/shared.test.ts index f33dcbd94401..cd7a4b64fcfb 100644 --- a/__tests__/unit/client/theme-default/support/sidebar.test.ts +++ b/__tests__/unit/shared/shared.test.ts @@ -1,4 +1,5 @@ -import { getSidebar, hasActiveLink } from 'client/theme-default/support/sidebar' +import { getSidebar, ensureStartingSlash } from 'shared/shared' +import { hasActiveLink } from 'client/theme-default/support/sidebar' describe('client/theme-default/support/sidebar', () => { describe('getSidebar', () => { @@ -143,3 +144,14 @@ describe('client/theme-default/support/sidebar', () => { }) }) }) + +describe('client/theme-default/utils', () => { + describe('ensureStartingSlash', () => { + test('it adds slash to the beginning of the given path', () => { + expect(ensureStartingSlash('path')).toBe('/path') + expect(ensureStartingSlash('path/nested')).toBe('/path/nested') + expect(ensureStartingSlash('/path')).toBe('/path') + expect(ensureStartingSlash('/path/nested')).toBe('/path/nested') + }) + }) +}) diff --git a/__tests__/unit/vitest.config.ts b/__tests__/unit/vitest.config.ts index ff26e6791d6a..a11d869068fb 100644 --- a/__tests__/unit/vitest.config.ts +++ b/__tests__/unit/vitest.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ { find: '@siteData', replacement: resolve(dir, './shims.ts') }, { find: 'client', replacement: resolve(dir, '../../src/client') }, { find: 'node', replacement: resolve(dir, '../../src/node') }, + { find: 'shared', replacement: resolve(dir, '../../src/shared') }, { find: /^vitepress$/, replacement: resolve(dir, '../../src/client/index.js') diff --git a/src/client/theme-default/composables/langs.ts b/src/client/theme-default/composables/langs.ts index 4d105cd71787..1d2372570228 100644 --- a/src/client/theme-default/composables/langs.ts +++ b/src/client/theme-default/composables/langs.ts @@ -1,5 +1,5 @@ import { computed } from 'vue' -import { ensureStartingSlash } from '../support/utils' +import { ensureStartingSlash } from '../../shared' import { useData } from './data' import { hashRef } from './hash' diff --git a/src/client/theme-default/composables/prev-next.ts b/src/client/theme-default/composables/prev-next.ts index 5bc39eca3909..cc8d03e775b4 100644 --- a/src/client/theme-default/composables/prev-next.ts +++ b/src/client/theme-default/composables/prev-next.ts @@ -1,7 +1,7 @@ import { computed } from 'vue' import { useData } from './data' -import { isActive } from '../../shared' -import { getSidebar, getFlatSideBarLinks } from '../support/sidebar' +import { isActive, getSidebar } from '../../shared' +import { getFlatSideBarLinks } from '../support/sidebar' export function usePrevNext() { const { page, theme, frontmatter } = useData() diff --git a/src/client/theme-default/composables/sidebar.ts b/src/client/theme-default/composables/sidebar.ts index a5a07a16789e..9992dfac0477 100644 --- a/src/client/theme-default/composables/sidebar.ts +++ b/src/client/theme-default/composables/sidebar.ts @@ -11,10 +11,9 @@ import { type ComputedRef, type Ref } from 'vue' -import { isActive } from '../../shared' +import { isActive, getSidebar } from '../../shared' import { hasActiveLink as containsActiveLink, - getSidebar, getSidebarGroups } from '../support/sidebar' import { useData } from './data' diff --git a/src/client/theme-default/support/sidebar.ts b/src/client/theme-default/support/sidebar.ts index 74533362e1f9..257490c466aa 100644 --- a/src/client/theme-default/support/sidebar.ts +++ b/src/client/theme-default/support/sidebar.ts @@ -1,6 +1,4 @@ -import type { DefaultTheme } from 'vitepress/theme' -import { ensureStartingSlash } from './utils' -import { isActive } from '../../shared' +import { isActive, type SidebarItem } from '../../shared' export interface SidebarLink { text: string @@ -8,38 +6,6 @@ export interface SidebarLink { docFooterText?: string } -type SidebarItem = DefaultTheme.SidebarItem - -/** - * Get the `Sidebar` from sidebar option. This method will ensure to get correct - * sidebar config from `MultiSideBarConfig` with various path combinations such - * as matching `guide/` and `/guide/`. If no matching config was found, it will - * return empty array. - */ -export function getSidebar( - _sidebar: DefaultTheme.Sidebar | undefined, - path: string -): SidebarItem[] { - if (Array.isArray(_sidebar)) return addBase(_sidebar) - if (_sidebar == null) return [] - - path = ensureStartingSlash(path) - - const dir = Object.keys(_sidebar) - .sort((a, b) => { - return b.split('/').length - a.split('/').length - }) - .find((dir) => { - // make sure the multi sidebar key starts with slash too - return path.startsWith(ensureStartingSlash(dir)) - }) - - const sidebar = dir ? _sidebar[dir] : [] - return Array.isArray(sidebar) - ? addBase(sidebar) - : addBase(sidebar.items, sidebar.base) -} - /** * Get or generate sidebar group from the given sidebar items. */ @@ -107,13 +73,3 @@ export function hasActiveLink( ? hasActiveLink(path, items.items) : false } - -function addBase(items: SidebarItem[], _base?: string): SidebarItem[] { - return [...items].map((_item) => { - const item = { ..._item } - const base = item.base || _base - if (base && item.link) item.link = base + item.link - if (item.items) item.items = addBase(item.items, base) - return item - }) -} diff --git a/src/client/theme-default/support/utils.ts b/src/client/theme-default/support/utils.ts index b04efa17cfc0..bd4779673601 100644 --- a/src/client/theme-default/support/utils.ts +++ b/src/client/theme-default/support/utils.ts @@ -16,10 +16,6 @@ export function throttleAndDebounce(fn: () => void, delay: number): () => void { } } -export function ensureStartingSlash(path: string): string { - return /^\//.test(path) ? path : `/${path}` -} - export function normalizeLink(url: string): string { const { pathname, search, hash, protocol } = new URL(url, 'http://a.com') diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts index e94eafb797f1..ae02e4658207 100644 --- a/src/node/plugins/localSearchPlugin.ts +++ b/src/node/plugins/localSearchPlugin.ts @@ -9,8 +9,10 @@ import { createMarkdownRenderer } from '../markdown/markdown' import { resolveSiteDataByRoute, slash, + getSidebar, type DefaultTheme, - type MarkdownEnv + type MarkdownEnv, + type SidebarItem } from '../shared' import { processIncludes } from '../utils/processIncludes' @@ -122,12 +124,37 @@ export async function localSearchPlugin( return id } + function getParentTitles(sidebar: SidebarItem[], fileId: string) { + const titles: string[] = [], + path: string[] = [] + const backtrack = (sidebar: SidebarItem[] | undefined) => { + if (!sidebar) return + for (let i = 0; i < sidebar?.length; i++) { + if (sidebar[i].link === fileId) { + titles.push(...path) + return + } + path.push(sidebar[i].text!) + backtrack(sidebar[i].items) + path.pop() + } + } + backtrack(sidebar) + return titles + } + async function indexFile(page: string) { const file = path.join(siteConfig.srcDir, page) // get file metadata const fileId = getDocId(file) const locale = getLocaleForPath(file) const index = getIndexByLocale(locale) + const sidebar = getSidebar( + siteConfig.site?.locales[locale]?.themeConfig?.sidebar ?? + siteConfig.site?.themeConfig.sidebar, + fileId + ) + const parentTitles = getParentTitles(sidebar, fileId.replace(/\.html$/, '')) // retrieve file and split into "sections" const html = await render(file) const sections = @@ -138,7 +165,8 @@ export async function localSearchPlugin( // add sections to the locale index for await (const section of sections) { if (!section || !(section.text || section.titles)) break - const { anchor, text, titles } = section + let { anchor, text, titles } = section + titles = [...parentTitles, ...titles] const id = anchor ? [fileId, anchor].join('#') : fileId index.add({ id, diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 6f2b4f702062..bbc3e94e1b10 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -1,4 +1,9 @@ -import type { HeadConfig, PageData, SiteData } from '../../types/shared' +import type { + HeadConfig, + PageData, + SiteData, + DefaultTheme +} from '../../types/shared' export type { Awaitable, @@ -14,6 +19,8 @@ export type { SiteData } from '../../types/shared' +export type SidebarItem = DefaultTheme.SidebarItem + export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i export const APPEARANCE_KEY = 'vitepress-theme-appearance' export const HASH_RE = /#.*$/ @@ -200,3 +207,47 @@ export function treatAsHtml(filename: string): boolean { export function escapeRegExp(str: string) { return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') } + +/** + * Get the `Sidebar` from sidebar option. This method will ensure to get correct + * sidebar config from `MultiSideBarConfig` with various path combinations such + * as matching `guide/` and `/guide/`. If no matching config was found, it will + * return empty array. + */ +export function getSidebar( + _sidebar: DefaultTheme.Sidebar | undefined, + path: string +): SidebarItem[] { + if (Array.isArray(_sidebar)) return addBase(_sidebar) + if (_sidebar == null) return [] + + path = ensureStartingSlash(path) + + const dir = Object.keys(_sidebar) + .sort((a, b) => { + return b.split('/').length - a.split('/').length + }) + .find((dir) => { + // make sure the multi sidebar key starts with slash too + return path.startsWith(ensureStartingSlash(dir)) + }) + + const sidebar = dir ? _sidebar[dir] : [] + return Array.isArray(sidebar) + ? addBase(sidebar) + : addBase(sidebar.items, sidebar.base) +} + +function addBase(items: SidebarItem[], _base?: string): SidebarItem[] { + return [...items].map((_item) => { + const item = { ..._item } + const base = item.base || _base + if (base && item.link) item.link = base + item.link + if (item.items) item.items = addBase(item.items, base) + return item + }) +} + +export function ensureStartingSlash(path: string): string { + return /^\//.test(path) ? path : `/${path}` +}