Skip to content

Commit 00dc1e6

Browse files
damiengbrc-dd
andauthored
feat: allow customizing markdown renderer used for local search indexing (#2770)
BREAKING CHANGES: `search.options.exclude` for local search is removed in favor of more flexible `search.options._render` Co-authored-by: Divyansh Singh <[email protected]>
1 parent e8edd0a commit 00dc1e6

File tree

14 files changed

+297
-232
lines changed

14 files changed

+297
-232
lines changed

Diff for: __tests__/e2e/.vitepress/config.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,11 @@ export default defineConfig({
9191
search: {
9292
provider: 'local',
9393
options: {
94-
exclude(relativePath) {
95-
return relativePath.startsWith('local-search/excluded')
94+
_render(src, env, md) {
95+
const html = md.render(src, env)
96+
if (env.frontmatter?.search === false) return ''
97+
if (env.relativePath.startsWith('local-search/excluded')) return ''
98+
return html
9699
}
97100
}
98101
}

Diff for: docs/reference/default-theme-search.md

+63-3
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,78 @@ export default defineConfig({
9898

9999
Learn more in [MiniSearch docs](https://lucaong.github.io/minisearch/classes/_minisearch_.minisearch.html).
100100

101-
### Excluding pages from search
101+
### Custom content renderer
102102

103-
You can exclude pages from search by adding `search: false` to the frontmatter of the page. Alternatively, you can also pass `exclude` function to `themeConfig.search.options` to exclude pages based on their path relative to `srcDir`:
103+
You can customize the function used to render the markdown content before indexing it:
104104

105105
```ts
106106
import { defineConfig } from 'vitepress'
107107

108108
export default defineConfig({
109109
themeConfig: {
110110
search: {
111+
provider: 'local',
112+
options: {
113+
/**
114+
* @param {string} src
115+
* @param {import('vitepress').MarkdownEnv} env
116+
* @param {import('markdown-it')} md
117+
*/
118+
_render(src, env, md) {
119+
// return html string
120+
}
121+
}
122+
}
123+
}
124+
})
125+
```
126+
127+
This function will be stripped from client-side site data, so you can use Node.js APIs in it.
128+
129+
#### Example: Excluding pages from search
130+
131+
You can exclude pages from search by adding `search: false` to the frontmatter of the page. Alternatively:
132+
133+
```ts
134+
import { defineConfig } from 'vitepress'
135+
136+
export default defineConfig({
137+
themeConfig: {
138+
search: {
139+
provider: 'local',
111140
options: {
112-
exclude: (path) => path.startsWith('/some/path')
141+
_render(src, env, md) {
142+
const html = md.render(src, env)
143+
if (env.frontmatter?.search === false) return ''
144+
if (env.relativePath.startsWith('some/path')) return ''
145+
return html
146+
}
147+
}
148+
}
149+
}
150+
})
151+
```
152+
153+
::: warning Note
154+
In case a custom `_render` function is provided, you need to handle the `search: false` frontmatter yourself. Also, the `env` object won't be completely populated before `md.render` is called, so any checks on optional `env` properties like `frontmatter` should be done after that.
155+
:::
156+
157+
#### Example: Transforming content - adding anchors
158+
159+
```ts
160+
import { defineConfig } from 'vitepress'
161+
162+
export default defineConfig({
163+
themeConfig: {
164+
search: {
165+
provider: 'local',
166+
options: {
167+
_render(src, env, md) {
168+
const html = md.render(src, env)
169+
if (env.frontmatter?.title)
170+
return md.render(`# ${env.frontmatter.title}`) + html
171+
return html
172+
}
113173
}
114174
}
115175
}

Diff for: src/node/markdown/env.ts

-40
This file was deleted.

Diff for: src/node/markdown/index.ts

+154-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,154 @@
1-
export * from './env'
2-
export * from './markdown'
1+
import { componentPlugin } from '@mdit-vue/plugin-component'
2+
import {
3+
frontmatterPlugin,
4+
type FrontmatterPluginOptions
5+
} from '@mdit-vue/plugin-frontmatter'
6+
import {
7+
headersPlugin,
8+
type HeadersPluginOptions
9+
} from '@mdit-vue/plugin-headers'
10+
import { sfcPlugin, type SfcPluginOptions } from '@mdit-vue/plugin-sfc'
11+
import { titlePlugin } from '@mdit-vue/plugin-title'
12+
import { tocPlugin, type TocPluginOptions } from '@mdit-vue/plugin-toc'
13+
import { slugify } from '@mdit-vue/shared'
14+
import MarkdownIt from 'markdown-it'
15+
import anchorPlugin from 'markdown-it-anchor'
16+
import attrsPlugin from 'markdown-it-attrs'
17+
import emojiPlugin from 'markdown-it-emoji'
18+
import type { ILanguageRegistration, IThemeRegistration } from 'shiki'
19+
import type { Logger } from 'vite'
20+
import { containerPlugin } from './plugins/containers'
21+
import { highlight } from './plugins/highlight'
22+
import { highlightLinePlugin } from './plugins/highlightLines'
23+
import { imagePlugin } from './plugins/image'
24+
import { lineNumberPlugin } from './plugins/lineNumbers'
25+
import { linkPlugin } from './plugins/link'
26+
import { preWrapperPlugin } from './plugins/preWrapper'
27+
import { snippetPlugin } from './plugins/snippet'
28+
29+
export type { Header } from '../shared'
30+
31+
export type ThemeOptions =
32+
| IThemeRegistration
33+
| { light: IThemeRegistration; dark: IThemeRegistration }
34+
35+
export interface MarkdownOptions extends MarkdownIt.Options {
36+
lineNumbers?: boolean
37+
preConfig?: (md: MarkdownIt) => void
38+
config?: (md: MarkdownIt) => void
39+
anchor?: anchorPlugin.AnchorOptions
40+
attrs?: {
41+
leftDelimiter?: string
42+
rightDelimiter?: string
43+
allowedAttributes?: string[]
44+
disable?: boolean
45+
}
46+
defaultHighlightLang?: string
47+
frontmatter?: FrontmatterPluginOptions
48+
headers?: HeadersPluginOptions | boolean
49+
sfc?: SfcPluginOptions
50+
theme?: ThemeOptions
51+
languages?: ILanguageRegistration[]
52+
toc?: TocPluginOptions
53+
externalLinks?: Record<string, string>
54+
cache?: boolean
55+
}
56+
57+
export type MarkdownRenderer = MarkdownIt
58+
59+
export const createMarkdownRenderer = async (
60+
srcDir: string,
61+
options: MarkdownOptions = {},
62+
base = '/',
63+
logger: Pick<Logger, 'warn'> = console
64+
): Promise<MarkdownRenderer> => {
65+
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
66+
const hasSingleTheme = typeof theme === 'string' || 'name' in theme
67+
68+
const md = MarkdownIt({
69+
html: true,
70+
linkify: true,
71+
highlight:
72+
options.highlight ||
73+
(await highlight(
74+
theme,
75+
options.languages,
76+
options.defaultHighlightLang,
77+
logger
78+
)),
79+
...options
80+
})
81+
82+
md.linkify.set({ fuzzyLink: false })
83+
84+
if (options.preConfig) {
85+
options.preConfig(md)
86+
}
87+
88+
// custom plugins
89+
md.use(componentPlugin)
90+
.use(highlightLinePlugin)
91+
.use(preWrapperPlugin, { hasSingleTheme })
92+
.use(snippetPlugin, srcDir)
93+
.use(containerPlugin, { hasSingleTheme })
94+
.use(imagePlugin)
95+
.use(
96+
linkPlugin,
97+
{ target: '_blank', rel: 'noreferrer', ...options.externalLinks },
98+
base
99+
)
100+
.use(lineNumberPlugin, options.lineNumbers)
101+
102+
// 3rd party plugins
103+
if (!options.attrs?.disable) {
104+
md.use(attrsPlugin, options.attrs)
105+
}
106+
md.use(emojiPlugin)
107+
108+
// mdit-vue plugins
109+
md.use(anchorPlugin, {
110+
slugify,
111+
permalink: anchorPlugin.permalink.linkInsideHeader({
112+
symbol: '&ZeroWidthSpace;',
113+
renderAttrs: (slug, state) => {
114+
// Find `heading_open` with the id identical to slug
115+
const idx = state.tokens.findIndex((token) => {
116+
const attrs = token.attrs
117+
const id = attrs?.find((attr) => attr[0] === 'id')
118+
return id && slug === id[1]
119+
})
120+
// Get the actual heading content
121+
const title = state.tokens[idx + 1].content
122+
return {
123+
'aria-label': `Permalink to "${title}"`
124+
}
125+
}
126+
}),
127+
...options.anchor
128+
} as anchorPlugin.AnchorOptions).use(frontmatterPlugin, {
129+
...options.frontmatter
130+
} as FrontmatterPluginOptions)
131+
132+
if (options.headers) {
133+
md.use(headersPlugin, {
134+
level: [2, 3, 4, 5, 6],
135+
slugify,
136+
...(typeof options.headers === 'boolean' ? undefined : options.headers)
137+
} as HeadersPluginOptions)
138+
}
139+
140+
md.use(sfcPlugin, {
141+
...options.sfc
142+
} as SfcPluginOptions)
143+
.use(titlePlugin)
144+
.use(tocPlugin, {
145+
...options.toc
146+
} as TocPluginOptions)
147+
148+
// apply user config
149+
if (options.config) {
150+
options.config(md)
151+
}
152+
153+
return md
154+
}

0 commit comments

Comments
 (0)