mirror of https://github.com/vuejs/vitepress
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 <40380293+brc-dd@users.noreply.github.com>pull/2495/merge
parent
e8edd0a05f
commit
00dc1e6742
@ -1,40 +0,0 @@
|
|||||||
import type { MarkdownSfcBlocks } from '@mdit-vue/plugin-sfc'
|
|
||||||
import type { Header } from '../shared'
|
|
||||||
|
|
||||||
// Manually declaring all properties as rollup-plugin-dts
|
|
||||||
// is unable to merge augmented module declarations
|
|
||||||
|
|
||||||
export interface MarkdownEnv {
|
|
||||||
/**
|
|
||||||
* The raw Markdown content without frontmatter
|
|
||||||
*/
|
|
||||||
content?: string
|
|
||||||
/**
|
|
||||||
* The excerpt that extracted by `@mdit-vue/plugin-frontmatter`
|
|
||||||
*
|
|
||||||
* - Would be the rendered HTML when `renderExcerpt` is enabled
|
|
||||||
* - Would be the raw Markdown when `renderExcerpt` is disabled
|
|
||||||
*/
|
|
||||||
excerpt?: string
|
|
||||||
/**
|
|
||||||
* The frontmatter that extracted by `@mdit-vue/plugin-frontmatter`
|
|
||||||
*/
|
|
||||||
frontmatter?: Record<string, unknown>
|
|
||||||
/**
|
|
||||||
* The headers that extracted by `@mdit-vue/plugin-headers`
|
|
||||||
*/
|
|
||||||
headers?: Header[]
|
|
||||||
/**
|
|
||||||
* SFC blocks that extracted by `@mdit-vue/plugin-sfc`
|
|
||||||
*/
|
|
||||||
sfcBlocks?: MarkdownSfcBlocks
|
|
||||||
/**
|
|
||||||
* The title that extracted by `@mdit-vue/plugin-title`
|
|
||||||
*/
|
|
||||||
title?: string
|
|
||||||
path: string
|
|
||||||
relativePath: string
|
|
||||||
cleanUrls: boolean
|
|
||||||
links?: string[]
|
|
||||||
includes?: string[]
|
|
||||||
}
|
|
@ -1,2 +1,154 @@
|
|||||||
export * from './env'
|
import { componentPlugin } from '@mdit-vue/plugin-component'
|
||||||
export * from './markdown'
|
import {
|
||||||
|
frontmatterPlugin,
|
||||||
|
type FrontmatterPluginOptions
|
||||||
|
} from '@mdit-vue/plugin-frontmatter'
|
||||||
|
import {
|
||||||
|
headersPlugin,
|
||||||
|
type HeadersPluginOptions
|
||||||
|
} from '@mdit-vue/plugin-headers'
|
||||||
|
import { sfcPlugin, type SfcPluginOptions } from '@mdit-vue/plugin-sfc'
|
||||||
|
import { titlePlugin } from '@mdit-vue/plugin-title'
|
||||||
|
import { tocPlugin, type TocPluginOptions } from '@mdit-vue/plugin-toc'
|
||||||
|
import { slugify } from '@mdit-vue/shared'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import anchorPlugin from 'markdown-it-anchor'
|
||||||
|
import attrsPlugin from 'markdown-it-attrs'
|
||||||
|
import emojiPlugin from 'markdown-it-emoji'
|
||||||
|
import type { ILanguageRegistration, IThemeRegistration } from 'shiki'
|
||||||
|
import type { Logger } from 'vite'
|
||||||
|
import { containerPlugin } from './plugins/containers'
|
||||||
|
import { highlight } from './plugins/highlight'
|
||||||
|
import { highlightLinePlugin } from './plugins/highlightLines'
|
||||||
|
import { imagePlugin } from './plugins/image'
|
||||||
|
import { lineNumberPlugin } from './plugins/lineNumbers'
|
||||||
|
import { linkPlugin } from './plugins/link'
|
||||||
|
import { preWrapperPlugin } from './plugins/preWrapper'
|
||||||
|
import { snippetPlugin } from './plugins/snippet'
|
||||||
|
|
||||||
|
export type { Header } from '../shared'
|
||||||
|
|
||||||
|
export type ThemeOptions =
|
||||||
|
| IThemeRegistration
|
||||||
|
| { light: IThemeRegistration; dark: IThemeRegistration }
|
||||||
|
|
||||||
|
export interface MarkdownOptions extends MarkdownIt.Options {
|
||||||
|
lineNumbers?: boolean
|
||||||
|
preConfig?: (md: MarkdownIt) => void
|
||||||
|
config?: (md: MarkdownIt) => void
|
||||||
|
anchor?: anchorPlugin.AnchorOptions
|
||||||
|
attrs?: {
|
||||||
|
leftDelimiter?: string
|
||||||
|
rightDelimiter?: string
|
||||||
|
allowedAttributes?: string[]
|
||||||
|
disable?: boolean
|
||||||
|
}
|
||||||
|
defaultHighlightLang?: string
|
||||||
|
frontmatter?: FrontmatterPluginOptions
|
||||||
|
headers?: HeadersPluginOptions | boolean
|
||||||
|
sfc?: SfcPluginOptions
|
||||||
|
theme?: ThemeOptions
|
||||||
|
languages?: ILanguageRegistration[]
|
||||||
|
toc?: TocPluginOptions
|
||||||
|
externalLinks?: Record<string, string>
|
||||||
|
cache?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarkdownRenderer = MarkdownIt
|
||||||
|
|
||||||
|
export const createMarkdownRenderer = async (
|
||||||
|
srcDir: string,
|
||||||
|
options: MarkdownOptions = {},
|
||||||
|
base = '/',
|
||||||
|
logger: Pick<Logger, 'warn'> = console
|
||||||
|
): Promise<MarkdownRenderer> => {
|
||||||
|
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
|
||||||
|
const hasSingleTheme = typeof theme === 'string' || 'name' in theme
|
||||||
|
|
||||||
|
const md = MarkdownIt({
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
highlight:
|
||||||
|
options.highlight ||
|
||||||
|
(await highlight(
|
||||||
|
theme,
|
||||||
|
options.languages,
|
||||||
|
options.defaultHighlightLang,
|
||||||
|
logger
|
||||||
|
)),
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
|
||||||
|
md.linkify.set({ fuzzyLink: false })
|
||||||
|
|
||||||
|
if (options.preConfig) {
|
||||||
|
options.preConfig(md)
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom plugins
|
||||||
|
md.use(componentPlugin)
|
||||||
|
.use(highlightLinePlugin)
|
||||||
|
.use(preWrapperPlugin, { hasSingleTheme })
|
||||||
|
.use(snippetPlugin, srcDir)
|
||||||
|
.use(containerPlugin, { hasSingleTheme })
|
||||||
|
.use(imagePlugin)
|
||||||
|
.use(
|
||||||
|
linkPlugin,
|
||||||
|
{ target: '_blank', rel: 'noreferrer', ...options.externalLinks },
|
||||||
|
base
|
||||||
|
)
|
||||||
|
.use(lineNumberPlugin, options.lineNumbers)
|
||||||
|
|
||||||
|
// 3rd party plugins
|
||||||
|
if (!options.attrs?.disable) {
|
||||||
|
md.use(attrsPlugin, options.attrs)
|
||||||
|
}
|
||||||
|
md.use(emojiPlugin)
|
||||||
|
|
||||||
|
// mdit-vue plugins
|
||||||
|
md.use(anchorPlugin, {
|
||||||
|
slugify,
|
||||||
|
permalink: anchorPlugin.permalink.linkInsideHeader({
|
||||||
|
symbol: '​',
|
||||||
|
renderAttrs: (slug, state) => {
|
||||||
|
// Find `heading_open` with the id identical to slug
|
||||||
|
const idx = state.tokens.findIndex((token) => {
|
||||||
|
const attrs = token.attrs
|
||||||
|
const id = attrs?.find((attr) => attr[0] === 'id')
|
||||||
|
return id && slug === id[1]
|
||||||
|
})
|
||||||
|
// Get the actual heading content
|
||||||
|
const title = state.tokens[idx + 1].content
|
||||||
|
return {
|
||||||
|
'aria-label': `Permalink to "${title}"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...options.anchor
|
||||||
|
} as anchorPlugin.AnchorOptions).use(frontmatterPlugin, {
|
||||||
|
...options.frontmatter
|
||||||
|
} as FrontmatterPluginOptions)
|
||||||
|
|
||||||
|
if (options.headers) {
|
||||||
|
md.use(headersPlugin, {
|
||||||
|
level: [2, 3, 4, 5, 6],
|
||||||
|
slugify,
|
||||||
|
...(typeof options.headers === 'boolean' ? undefined : options.headers)
|
||||||
|
} as HeadersPluginOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
md.use(sfcPlugin, {
|
||||||
|
...options.sfc
|
||||||
|
} as SfcPluginOptions)
|
||||||
|
.use(titlePlugin)
|
||||||
|
.use(tocPlugin, {
|
||||||
|
...options.toc
|
||||||
|
} as TocPluginOptions)
|
||||||
|
|
||||||
|
// apply user config
|
||||||
|
if (options.config) {
|
||||||
|
options.config(md)
|
||||||
|
}
|
||||||
|
|
||||||
|
return md
|
||||||
|
}
|
||||||
|
@ -1,154 +0,0 @@
|
|||||||
import { componentPlugin } from '@mdit-vue/plugin-component'
|
|
||||||
import {
|
|
||||||
frontmatterPlugin,
|
|
||||||
type FrontmatterPluginOptions
|
|
||||||
} from '@mdit-vue/plugin-frontmatter'
|
|
||||||
import {
|
|
||||||
headersPlugin,
|
|
||||||
type HeadersPluginOptions
|
|
||||||
} from '@mdit-vue/plugin-headers'
|
|
||||||
import { sfcPlugin, type SfcPluginOptions } from '@mdit-vue/plugin-sfc'
|
|
||||||
import { titlePlugin } from '@mdit-vue/plugin-title'
|
|
||||||
import { tocPlugin, type TocPluginOptions } from '@mdit-vue/plugin-toc'
|
|
||||||
import { slugify } from '@mdit-vue/shared'
|
|
||||||
import MarkdownIt from 'markdown-it'
|
|
||||||
import anchorPlugin from 'markdown-it-anchor'
|
|
||||||
import attrsPlugin from 'markdown-it-attrs'
|
|
||||||
import emojiPlugin from 'markdown-it-emoji'
|
|
||||||
import type { ILanguageRegistration, IThemeRegistration } from 'shiki'
|
|
||||||
import type { Logger } from 'vite'
|
|
||||||
import { containerPlugin } from './plugins/containers'
|
|
||||||
import { highlight } from './plugins/highlight'
|
|
||||||
import { highlightLinePlugin } from './plugins/highlightLines'
|
|
||||||
import { imagePlugin } from './plugins/image'
|
|
||||||
import { lineNumberPlugin } from './plugins/lineNumbers'
|
|
||||||
import { linkPlugin } from './plugins/link'
|
|
||||||
import { preWrapperPlugin } from './plugins/preWrapper'
|
|
||||||
import { snippetPlugin } from './plugins/snippet'
|
|
||||||
|
|
||||||
export type { Header } from '../shared'
|
|
||||||
|
|
||||||
export type ThemeOptions =
|
|
||||||
| IThemeRegistration
|
|
||||||
| { light: IThemeRegistration; dark: IThemeRegistration }
|
|
||||||
|
|
||||||
export interface MarkdownOptions extends MarkdownIt.Options {
|
|
||||||
lineNumbers?: boolean
|
|
||||||
preConfig?: (md: MarkdownIt) => void
|
|
||||||
config?: (md: MarkdownIt) => void
|
|
||||||
anchor?: anchorPlugin.AnchorOptions
|
|
||||||
attrs?: {
|
|
||||||
leftDelimiter?: string
|
|
||||||
rightDelimiter?: string
|
|
||||||
allowedAttributes?: string[]
|
|
||||||
disable?: boolean
|
|
||||||
}
|
|
||||||
defaultHighlightLang?: string
|
|
||||||
frontmatter?: FrontmatterPluginOptions
|
|
||||||
headers?: HeadersPluginOptions | boolean
|
|
||||||
sfc?: SfcPluginOptions
|
|
||||||
theme?: ThemeOptions
|
|
||||||
languages?: ILanguageRegistration[]
|
|
||||||
toc?: TocPluginOptions
|
|
||||||
externalLinks?: Record<string, string>
|
|
||||||
cache?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MarkdownRenderer = MarkdownIt
|
|
||||||
|
|
||||||
export const createMarkdownRenderer = async (
|
|
||||||
srcDir: string,
|
|
||||||
options: MarkdownOptions = {},
|
|
||||||
base = '/',
|
|
||||||
logger: Pick<Logger, 'warn'> = console
|
|
||||||
): Promise<MarkdownRenderer> => {
|
|
||||||
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
|
|
||||||
const hasSingleTheme = typeof theme === 'string' || 'name' in theme
|
|
||||||
|
|
||||||
const md = MarkdownIt({
|
|
||||||
html: true,
|
|
||||||
linkify: true,
|
|
||||||
highlight:
|
|
||||||
options.highlight ||
|
|
||||||
(await highlight(
|
|
||||||
theme,
|
|
||||||
options.languages,
|
|
||||||
options.defaultHighlightLang,
|
|
||||||
logger
|
|
||||||
)),
|
|
||||||
...options
|
|
||||||
})
|
|
||||||
|
|
||||||
md.linkify.set({ fuzzyLink: false })
|
|
||||||
|
|
||||||
if (options.preConfig) {
|
|
||||||
options.preConfig(md)
|
|
||||||
}
|
|
||||||
|
|
||||||
// custom plugins
|
|
||||||
md.use(componentPlugin)
|
|
||||||
.use(highlightLinePlugin)
|
|
||||||
.use(preWrapperPlugin, { hasSingleTheme })
|
|
||||||
.use(snippetPlugin, srcDir)
|
|
||||||
.use(containerPlugin, { hasSingleTheme })
|
|
||||||
.use(imagePlugin)
|
|
||||||
.use(
|
|
||||||
linkPlugin,
|
|
||||||
{ target: '_blank', rel: 'noreferrer', ...options.externalLinks },
|
|
||||||
base
|
|
||||||
)
|
|
||||||
.use(lineNumberPlugin, options.lineNumbers)
|
|
||||||
|
|
||||||
// 3rd party plugins
|
|
||||||
if (!options.attrs?.disable) {
|
|
||||||
md.use(attrsPlugin, options.attrs)
|
|
||||||
}
|
|
||||||
md.use(emojiPlugin)
|
|
||||||
|
|
||||||
// mdit-vue plugins
|
|
||||||
md.use(anchorPlugin, {
|
|
||||||
slugify,
|
|
||||||
permalink: anchorPlugin.permalink.linkInsideHeader({
|
|
||||||
symbol: '​',
|
|
||||||
renderAttrs: (slug, state) => {
|
|
||||||
// Find `heading_open` with the id identical to slug
|
|
||||||
const idx = state.tokens.findIndex((token) => {
|
|
||||||
const attrs = token.attrs
|
|
||||||
const id = attrs?.find((attr) => attr[0] === 'id')
|
|
||||||
return id && slug === id[1]
|
|
||||||
})
|
|
||||||
// Get the actual heading content
|
|
||||||
const title = state.tokens[idx + 1].content
|
|
||||||
return {
|
|
||||||
'aria-label': `Permalink to "${title}"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
...options.anchor
|
|
||||||
} as anchorPlugin.AnchorOptions).use(frontmatterPlugin, {
|
|
||||||
...options.frontmatter
|
|
||||||
} as FrontmatterPluginOptions)
|
|
||||||
|
|
||||||
if (options.headers) {
|
|
||||||
md.use(headersPlugin, {
|
|
||||||
level: [2, 3, 4, 5, 6],
|
|
||||||
slugify,
|
|
||||||
...(typeof options.headers === 'boolean' ? undefined : options.headers)
|
|
||||||
} as HeadersPluginOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
md.use(sfcPlugin, {
|
|
||||||
...options.sfc
|
|
||||||
} as SfcPluginOptions)
|
|
||||||
.use(titlePlugin)
|
|
||||||
.use(tocPlugin, {
|
|
||||||
...options.toc
|
|
||||||
} as TocPluginOptions)
|
|
||||||
|
|
||||||
// apply user config
|
|
||||||
if (options.config) {
|
|
||||||
options.config(md)
|
|
||||||
}
|
|
||||||
|
|
||||||
return md
|
|
||||||
}
|
|
Loading…
Reference in new issue