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(node): localSearchPlugin support interpolation #3032

Open
wants to merge 4 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion __tests__/e2e/local-search/index.md
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
# Local search included
---
title: Local search frontmatter title
---
# Local search included

# {{ $frontmatter.title }}
31 changes: 23 additions & 8 deletions __tests__/e2e/local-search/local-search.test.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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)
})
})
33 changes: 27 additions & 6 deletions src/node/plugins/localSearchPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -24,7 +25,7 @@ interface IndexObject {
title: string
titles: string[]
}

let mdEnv: MarkdownEnv | null
export async function localSearchPlugin(
siteConfig: SiteConfig<DefaultTheme.Config>
): Promise<Plugin> {
Expand Down Expand Up @@ -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<string, MiniSearch<IndexObject>>()
Expand Down Expand Up @@ -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
Expand All @@ -273,6 +276,7 @@ function splitPageIntoSections(html: string) {
parentTitles[level] = title
}
}
mdEnv = null
return sections
}

Expand All @@ -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<string, any> = { $frontmatter: { ...mdEnv?.frontmatter } }

for (let prop of properties) {
value = value?.[prop]
}

return value ? toDisplayString(value) : match
})
}