diff --git a/__tests__/e2e/local-search/index.md b/__tests__/e2e/local-search/index.md index 08d8b31a..6364f605 100644 --- a/__tests__/e2e/local-search/index.md +++ b/__tests__/e2e/local-search/index.md @@ -1 +1,6 @@ -# Local search included \ No newline at end of file +--- +title: Local search frontmatter title +--- +# Local search included + +# {{ $frontmatter.title }} \ No newline at end of file diff --git a/__tests__/e2e/local-search/local-search.test.ts b/__tests__/e2e/local-search/local-search.test.ts index 492136b6..6e19a423 100644 --- a/__tests__/e2e/local-search/local-search.test.ts +++ b/__tests__/e2e/local-search/local-search.test.ts @@ -1,18 +1,23 @@ +const getSearchResults = async (text: string) => { + await page.locator('#local-search button').click() + + const input = await page.waitForSelector('input#localsearch-input') + await input.fill(text) + + await page.waitForSelector('ul#localsearch-list', { state: 'visible' }) + + return page.locator('#localsearch-list') +} + describe('local search', () => { beforeEach(async () => { await goto('/') }) test('exclude content from search results', async () => { - await page.locator('#local-search button').click() + const searchResults = await getSearchResults('local') - const input = await page.waitForSelector('input#localsearch-input') - await input.type('local') - - await page.waitForSelector('ul#localsearch-list', { state: 'visible' }) - - const searchResults = page.locator('#localsearch-list') - expect(await searchResults.locator('li[role=option]').count()).toBe(1) + expect(await searchResults.locator('li[role=option]').count()).toBe(2) expect( await searchResults.filter({ hasText: 'Local search included' }).count() @@ -28,4 +33,14 @@ describe('local search', () => { .count() ).toBe(0) }) + + test('frontmatter content from search results', async () => { + const searchResults = await getSearchResults('local') + + expect( + await searchResults + .filter({ hasText: 'Local search frontmatter title' }) + .count() + ).toBe(1) + }) }) diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts index dc7ca8da..d6866060 100644 --- a/src/node/plugins/localSearchPlugin.ts +++ b/src/node/plugins/localSearchPlugin.ts @@ -2,6 +2,7 @@ import _debug from 'debug' import fs from 'fs-extra' import MiniSearch from 'minisearch' import path from 'path' +import { toDisplayString } from 'vue' import type { Plugin, ViteDevServer } from 'vite' import type { SiteConfig } from '../config' import { createMarkdownRenderer } from '../markdown/markdown' @@ -24,7 +25,7 @@ interface IndexObject { title: string titles: string[] } - +let mdEnv: MarkdownEnv | null export async function localSearchPlugin( siteConfig: SiteConfig ): Promise { @@ -56,12 +57,12 @@ export async function localSearchPlugin( function render(file: string) { const { srcDir, cleanUrls = false } = siteConfig const relativePath = slash(path.relative(srcDir, file)) - const env: MarkdownEnv = { path: file, relativePath, cleanUrls } + mdEnv = { path: file, relativePath, cleanUrls } let src = fs.readFileSync(file, 'utf-8') src = processIncludes(srcDir, src, file, []) - if (options._render) return options._render(src, env, md) - const html = md.render(src, env) - return env.frontmatter?.search === false ? '' : html + if (options._render) return options._render(src, mdEnv, md) + const html = md.render(src, mdEnv) + return mdEnv.frontmatter?.search === false ? '' : html } const indexByLocales = new Map>() @@ -260,7 +261,9 @@ function splitPageIntoSections(html: string) { const level = parseInt(result[i]) - 1 const heading = result[i + 1] const headingResult = headingContentRegex.exec(heading) - const title = clearHtmlTags(headingResult?.[1] ?? '').trim() + const title = replaceInterpolation( + clearHtmlTags(headingResult?.[1] ?? '').trim() + ) const anchor = headingResult?.[2] ?? '' const content = result[i + 2] if (!title || !content) continue @@ -273,6 +276,7 @@ function splitPageIntoSections(html: string) { parentTitles[level] = title } } + mdEnv = null return sections } @@ -284,3 +288,20 @@ function getSearchableText(content: string) { function clearHtmlTags(str: string) { return str.replace(/<[^>]*>/g, '') } + +function replaceInterpolation(str: string) { + if (!mdEnv?.frontmatter) { + return str + } + + return str.replace(/{{\s*([^}]+)\s*}}/g, (match, expression: string) => { + const properties = expression.trim().split('.') + let value: Record = { $frontmatter: { ...mdEnv?.frontmatter } } + + for (let prop of properties) { + value = value?.[prop] + } + + return value ? toDisplayString(value) : match + }) +}