Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: display parent directory heading in localSearch results #3440

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 0 additions & 12 deletions __tests__/unit/client/theme-default/support/utils.test.ts

This file was deleted.

@@ -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', () => {
Expand Down Expand Up @@ -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')
})
})
})
1 change: 1 addition & 0 deletions __tests__/unit/vitest.config.ts
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion 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'

Expand Down
4 changes: 2 additions & 2 deletions 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()
Expand Down
3 changes: 1 addition & 2 deletions src/client/theme-default/composables/sidebar.ts
Expand Up @@ -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'
Expand Down
46 changes: 1 addition & 45 deletions src/client/theme-default/support/sidebar.ts
@@ -1,45 +1,11 @@
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
link: string
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.
*/
Expand Down Expand Up @@ -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
})
}
4 changes: 0 additions & 4 deletions src/client/theme-default/support/utils.ts
Expand Up @@ -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')

Expand Down
32 changes: 30 additions & 2 deletions src/node/plugins/localSearchPlugin.ts
Expand Up @@ -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'

Expand Down Expand Up @@ -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 =
Expand All @@ -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,
Expand Down
53 changes: 52 additions & 1 deletion 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,
Expand All @@ -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 = /#.*$/
Expand Down Expand Up @@ -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}`
}