diff --git a/__tests__/unit/node/plugins/localSearchPlugin.test.ts b/__tests__/unit/node/plugins/localSearchPlugin.test.ts index 9f6d4d65..ffbf3e46 100644 --- a/__tests__/unit/node/plugins/localSearchPlugin.test.ts +++ b/__tests__/unit/node/plugins/localSearchPlugin.test.ts @@ -1,7 +1,9 @@ import MiniSearch from 'minisearch' import { resolveConfig } from 'node/config' +import { disposeMdItInstance } from 'node/markdown/markdown' +import { createMarkdownToVueRenderFn } from 'node/markdownToVue' import { localSearchPlugin } from 'node/plugins/localSearchPlugin' -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' @@ -12,6 +14,9 @@ describe('node/plugins/localSearchPlugin', () => { beforeEach(() => { nodeEnv = process.env.NODE_ENV process.env.NODE_ENV = 'production' + // createMarkdownRenderer is a module-level singleton keyed on nothing — + // reset it so each test's config actually reaches the renderer + disposeMdItInstance() }) afterEach(async () => { @@ -102,6 +107,83 @@ describe('node/plugins/localSearchPlugin', () => { expect(zhIndex.search('rootonlytoken')).toEqual([]) }) + test('applies per-locale markdown options (#5350)', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-')) + const configDir = path.join(root, '.vitepress') + await mkdir(configDir) + await mkdir(path.join(root, 'zh')) + + const content = '::: tip\ntipcontent\n:::\n' + await writeFile(path.join(root, 'index.md'), `# Home\n\n${content}`) + await writeFile( + path.join(root, 'zh', 'index.md'), + `# Chinese home\n\n${content}` + ) + await writeFile( + path.join(configDir, 'config.ts'), + [ + 'export default {', + ' locales: {', + " root: { label: 'English', lang: 'en' },", + ' zh: {', + " label: 'Chinese',", + " lang: 'zh',", + " markdown: { container: { tipLabel: 'zhtiplabel' } }", + ' }', + ' },', + ' themeConfig: {', + " search: { provider: 'local' }", + ' }', + '}' + ].join('\n') + ) + + const siteConfig = await resolveConfig(root, 'build', 'production') + const plugin = await localSearchPlugin(siteConfig) + + // configResolved hooks run concurrently, so the search plugin may be the + // one that creates the shared markdown renderer — page renders must still + // pick up per-locale options from it + await (plugin.configResolved as any)?.call( + {}, + { publicDir: siteConfig.publicDir } + ) + + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + siteConfig.markdown ?? {}, + siteConfig.site.base, + false, + false, + siteConfig + ) + + const rootFile = path.join(root, 'index.md') + const zhFile = path.join(root, 'zh', 'index.md') + const rootPage = await render(await readFile(rootFile, 'utf-8'), rootFile) + const zhPage = await render(await readFile(zhFile, 'utf-8'), zhFile) + expect(rootPage.vueSrc).toContain('TIP') + expect(zhPage.vueSrc).toContain('zhtiplabel') + + // the indexed text must use the localized labels too + await (plugin.load as any)?.handler.call({}, '/@localSearchIndex') + const rootIndex = loadIndex( + (await (plugin.load as any)?.handler.call( + {}, + '/@localSearchIndexroot' + )) as string + ) + const zhIndex = loadIndex( + (await (plugin.load as any)?.handler.call( + {}, + '/@localSearchIndexzh' + )) as string + ) + expect(zhIndex.search('zhtiplabel')).toHaveLength(1) + expect(rootIndex.search('zhtiplabel')).toEqual([]) + expect(rootIndex.search('tip')).toHaveLength(1) + }) + test('warns and skips pages that fail to render', async () => { root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-')) const configDir = path.join(root, '.vitepress') diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts index 7247d415..35332af3 100644 --- a/src/node/plugins/localSearchPlugin.ts +++ b/src/node/plugins/localSearchPlugin.ts @@ -6,7 +6,10 @@ import c from 'picocolors' import type { Plugin, ViteDevServer } from 'vite' import type { SiteConfig } from '../config' import type { DefaultTheme } from '../defaultTheme' -import { createMarkdownRenderer } from '../markdown/markdown' +import { + createMarkdownRenderer, + mergeMarkdownLocales +} from '../markdown/markdown' import { getLocaleForPath, slash, type MarkdownEnv } from '../shared' import { readTextFile } from '../utils/fs' @@ -51,10 +54,15 @@ export async function localSearchPlugin( const options = siteConfig.site.themeConfig.search.options || {} - async function render(file: string) { + async function render(file: string, localeIndex: string) { const { srcDir, cleanUrls = false } = siteConfig const relativePath = slash(path.relative(srcDir, file)) - const env: MarkdownEnv = { path: file, relativePath, cleanUrls } + const env: MarkdownEnv = { + path: file, + relativePath, + cleanUrls, + localeIndex + } const raw = await readTextFile(file).catch((e) => { if (e.code === 'ENOENT') { debug(`File not found: ${file}`) @@ -135,7 +143,7 @@ export async function localSearchPlugin( // retrieve file and split into "sections" let html: string try { - html = await render(file) + html = await render(file, locale) } catch (e) { // in dev this runs unawaited, so a page that fails to render must not // reject and take the server down with it — the page reports the error @@ -178,9 +186,12 @@ export async function localSearchPlugin( name: 'vitepress:local-search', async configResolved(config) { + // fold in `locales.*.markdown` like markdownToVue does — the renderer + // is a singleton, so whichever configResolved hook creates it first + // must pass the complete options (#5350) md = await createMarkdownRenderer( siteConfig.srcDir, - siteConfig.markdown, + mergeMarkdownLocales(siteConfig.markdown, siteConfig.site.locales), siteConfig.site.base, siteConfig.logger, config.publicDir