feat(markdown): support disabling built-in markdown plugins

Allows custom themes to opt out of markup and behavior added on top of
vanilla markdown rendering:

- `anchor`, `emoji`, `toc`, `component`, `image` now also accept `false`
- new `preWrapper` and `snippet` boolean options (default true)
- `lineNumbers` is a no-op when `preWrapper` is disabled, as its markup
  depends on the wrapper

close #4484
close #4556

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/5335/head
Divyansh Singh 2 months ago
parent e235dbeb8a
commit b8d9c8f9a9

@ -0,0 +1,105 @@
import {
createMarkdownRenderer,
disposeMdItInstance,
type MarkdownOptions
} from 'node/markdown/markdown'
async function render(src: string, options: MarkdownOptions = {}) {
disposeMdItInstance()
const md = await createMarkdownRenderer('.', {
highlight: (code) => code,
...options
})
return md.renderAsync(src)
}
describe('node/markdown/markdown', () => {
describe('disabling built-in plugins', () => {
test('anchor', async () => {
const enabled = await render('# Hello World')
expect(enabled).toContain('id="hello-world"')
expect(enabled).toContain('header-anchor')
const disabled = await render('# Hello World', { anchor: false })
expect(disabled).not.toContain('id=')
expect(disabled).not.toContain('header-anchor')
})
test('attrs', async () => {
const enabled = await render('## Title {#custom-id}')
expect(enabled).toContain('id="custom-id"')
const disabled = await render('## Title {#custom-id}', { attrs: false })
expect(disabled).not.toContain('id="custom-id"')
expect(disabled).toContain('{#custom-id}')
})
test('emoji', async () => {
expect(await render(':tada:')).toContain('🎉')
expect(await render(':tada:', { emoji: false })).toContain(':tada:')
})
test('toc', async () => {
const src = '# Title\n\n[[toc]]'
expect(await render(src)).toContain('table-of-contents')
const disabled = await render(src, { toc: false })
expect(disabled).not.toContain('table-of-contents')
expect(disabled).toContain('[[toc]]')
})
test('preWrapper', async () => {
const src = '```js\nconst a = 1\n```'
const enabled = await render(src)
expect(enabled).toContain('<div class="language-js">')
expect(enabled).toContain('class="copy"')
const disabled = await render(src, { preWrapper: false })
expect(disabled).not.toContain('<div class="language-js">')
expect(disabled).not.toContain('class="copy"')
})
test('preWrapper disables line numbers with it', async () => {
const src = '```js\nconst a = 1\n```'
const enabled = await render(src, { lineNumbers: true })
expect(enabled).toContain('line-numbers-wrapper')
const disabled = await render(src, {
preWrapper: false,
lineNumbers: true
})
expect(disabled).not.toContain('line-numbers-wrapper')
})
test('snippet', async () => {
const disabled = await render('<<< ./foo.js', { snippet: false })
expect(disabled).toContain('&lt;&lt;&lt; ./foo.js')
})
test('image', async () => {
const src = '![img](/foo.png)'
const enabled = await render(src, { image: { lazyLoad: true } })
expect(enabled).toContain('loading="lazy"')
const disabled = await render(src, { image: false })
expect(disabled).not.toContain('loading="lazy"')
})
test('component', async () => {
const src = 'text\n<MyComponent/>\nmore'
const enabled = await render(src)
expect(enabled).toContain('</p>\n<MyComponent/><p>')
const disabled = await render(src, { component: false })
expect(disabled).toContain('<p>text\n<MyComponent/>\nmore</p>')
})
test('tableTabIndex', async () => {
const src = '| a |\n| --- |\n| b |'
expect(await render(src)).toContain('tabindex="0"')
expect(await render(src, { tableTabIndex: false })).not.toContain(
'tabindex'
)
})
})
})

@ -121,10 +121,17 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
*/ */
languageLabel?: Record<string, string> languageLabel?: Record<string, string>
/** /**
* Show line numbers in code blocks * Show line numbers in code blocks. Requires the `preWrapper` plugin.
* @default false * @default false
*/ */
lineNumbers?: boolean lineNumbers?: boolean
/**
* Wrap code blocks in a container carrying the language label and the
* copy button. The default theme's code block styling relies on this
* markup. Disabling it also disables `lineNumbers`.
* @default true
*/
preWrapper?: boolean
/** /**
* Fallback language when the specified language is not available. * Fallback language when the specified language is not available.
*/ */
@ -153,24 +160,28 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
/* ==================== Markdown It Plugins ==================== */ /* ==================== Markdown It Plugins ==================== */
/** /**
* Options for `markdown-it-anchor` * Options for `markdown-it-anchor`. Set to `false` to disable adding ids
* and anchor links to headings. Note that the default theme's outline and
* heading hash links rely on these ids.
* @see https://github.com/valeriangalliat/markdown-it-anchor * @see https://github.com/valeriangalliat/markdown-it-anchor
*/ */
anchor?: anchorPlugin.AnchorOptions anchor?: anchorPlugin.AnchorOptions | false
/** /**
* Options for `markdown-it-attrs`. Set to `false` to disable. * Options for `markdown-it-attrs`. Set to `false` to disable.
* @see https://github.com/arve0/markdown-it-attrs * @see https://github.com/arve0/markdown-it-attrs
*/ */
attrs?: MarkdownItAttrsOptions | false attrs?: MarkdownItAttrsOptions | false
/** /**
* Options for `markdown-it-emoji` * Options for `markdown-it-emoji`. Set to `false` to disable.
* @see https://github.com/markdown-it/markdown-it-emoji * @see https://github.com/markdown-it/markdown-it-emoji
*/ */
emoji?: { emoji?:
defs?: Record<string, string> | {
enabled?: string[] defs?: Record<string, string>
shortcuts?: Record<string, string | string[]> enabled?: string[]
} shortcuts?: Record<string, string | string[]>
}
| false
/** /**
* Options for `@mdit-vue/plugin-frontmatter` * Options for `@mdit-vue/plugin-frontmatter`
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter
@ -187,15 +198,22 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
*/ */
sfc?: SfcPluginOptions sfc?: SfcPluginOptions
/** /**
* Options for `@mdit-vue/plugin-toc` * Options for `@mdit-vue/plugin-toc`. Set to `false` to disable the
* `[[toc]]` syntax.
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-toc * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-toc
*/ */
toc?: TocPluginOptions toc?: TocPluginOptions | false
/** /**
* Options for `@mdit-vue/plugin-component` * Options for `@mdit-vue/plugin-component`. Set to `false` to disable.
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component
*/ */
component?: ComponentPluginOptions component?: ComponentPluginOptions | false
/**
* Enables importing code snippets from files with `<<<`.
* @default true
* @see https://vitepress.dev/guide/markdown#import-code-snippets
*/
snippet?: boolean
/** /**
* Options for `markdown-it-container` * Options for `markdown-it-container`
* @see https://github.com/markdown-it/markdown-it-container * @see https://github.com/markdown-it/markdown-it-container
@ -210,7 +228,13 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
* @see https://vitepress.dev/guide/markdown#math-equations * @see https://vitepress.dev/guide/markdown#math-equations
*/ */
math?: boolean | any math?: boolean | any
image?: ImageOptions /**
* Options for the image plugin (resolves image sources against the public
* directory, adds dimensions, and supports lazy loading). Set to `false`
* to disable.
* @see https://vitepress.dev/guide/markdown#image-lazy-loading
*/
image?: ImageOptions | false
/** /**
* Allows disabling the github alerts plugin * Allows disabling the github alerts plugin
* @default true * @default true
@ -278,24 +302,33 @@ export async function createMarkdownRenderer(
await options.preConfig(md) await options.preConfig(md)
} }
const slugify = options.anchor?.slugify ?? defaultSlugify const slugify =
(options.anchor ? options.anchor.slugify : undefined) ?? defaultSlugify
// custom plugins // custom plugins
componentPlugin(md, options.component) if (options.component !== false) {
preWrapperPlugin(md, { componentPlugin(md, options.component)
codeCopyButtonTitle, }
languageLabel: options.languageLabel if (options.preWrapper !== false) {
}) preWrapperPlugin(md, {
snippetPlugin(md, srcDir) codeCopyButtonTitle,
languageLabel: options.languageLabel
})
lineNumberPlugin(md, options.lineNumbers)
}
if (options.snippet !== false) {
snippetPlugin(md, srcDir)
}
containerPlugin(md, options.container) containerPlugin(md, options.container)
imagePlugin(md, publicDir, options.image) if (options.image !== false) {
imagePlugin(md, publicDir, options.image)
}
linkPlugin( linkPlugin(
md, md,
{ target: '_blank', rel: 'noreferrer', ...options.externalLinks }, { target: '_blank', rel: 'noreferrer', ...options.externalLinks },
base, base,
slugify slugify
) )
lineNumberPlugin(md, options.lineNumbers)
if (options.tableTabIndex !== false) { if (options.tableTabIndex !== false) {
tablePlugin(md) tablePlugin(md)
@ -309,44 +342,48 @@ export async function createMarkdownRenderer(
if (options.attrs !== false) { if (options.attrs !== false) {
attrsPlugin(md, options.attrs) attrsPlugin(md, options.attrs)
} }
emojiPlugin(md, options.emoji) if (options.emoji !== false) {
emojiPlugin(md, options.emoji)
}
// mdit-vue plugins // mdit-vue plugins
anchorPlugin(md, { if (options.anchor !== false) {
slugify, anchorPlugin(md, {
getTokensText: (tokens) => { slugify,
return tokens getTokensText: (tokens) => {
.filter((t) => !['html_inline', 'emoji'].includes(t.type)) return tokens
.map((t) => t.content) .filter((t) => !['html_inline', 'emoji'].includes(t.type))
.join('') .map((t) => t.content)
}, .join('')
permalink: (slug, _, state, idx) => { },
const title = permalink: (slug, _, state, idx) => {
state.tokens[idx + 1]?.children const title =
?.filter((token) => ['text', 'code_inline'].includes(token.type)) state.tokens[idx + 1]?.children
.reduce((acc, t) => acc + t.content, '') ?.filter((token) => ['text', 'code_inline'].includes(token.type))
.trim() || '' .reduce((acc, t) => acc + t.content, '')
.trim() || ''
const linkTokens = [
Object.assign(new state.Token('text', '', 0), { content: ' ' }), const linkTokens = [
Object.assign(new state.Token('link_open', 'a', 1), { Object.assign(new state.Token('text', '', 0), { content: ' ' }),
attrs: [ Object.assign(new state.Token('link_open', 'a', 1), {
['class', 'header-anchor'], attrs: [
['href', `#${slug}`], ['class', 'header-anchor'],
['aria-label', `Permalink to “${title}”`] ['href', `#${slug}`],
] ['aria-label', `Permalink to “${title}”`]
}), ]
Object.assign(new state.Token('html_inline', '', 0), { }),
content: '&#8203;', Object.assign(new state.Token('html_inline', '', 0), {
meta: { isPermalinkSymbol: true } content: '&#8203;',
}), meta: { isPermalinkSymbol: true }
new state.Token('link_close', 'a', -1) }),
] new state.Token('link_close', 'a', -1)
]
state.tokens[idx + 1].children?.push(...linkTokens)
}, state.tokens[idx + 1].children?.push(...linkTokens)
...options.anchor },
}) ...options.anchor
})
}
frontmatterPlugin(md, options.frontmatter) frontmatterPlugin(md, options.frontmatter)
@ -360,14 +397,18 @@ export async function createMarkdownRenderer(
sfcPlugin(md, options.sfc) sfcPlugin(md, options.sfc)
titlePlugin(md) titlePlugin(md)
tocPlugin(md, {
slugify, const tocOptions = options.toc
...options.toc, if (tocOptions !== false) {
format: (s) => { tocPlugin(md, {
const title = s.replaceAll('&amp;', '&') // encoded twice because of restoreEntities slugify,
return options.toc?.format?.(title) ?? title ...tocOptions,
} format: (s) => {
}) const title = s.replaceAll('&amp;', '&') // encoded twice because of restoreEntities
return tocOptions?.format?.(title) ?? title
}
})
}
if (options.math) { if (options.math) {
try { try {

Loading…
Cancel
Save