refactor(markdown): restructure options and plugin registration

- group `MarkdownOptions` into sections by concern (general, syntax
  highlighting, code blocks, markdown extensions, vue integration) and
  rewrite the jsdocs with a consistent voice, documenting the previously
  undocumented `externalLinks` default and correcting the `container`
  description (label customization, not plugin pass-through)
- register plugins in accurately-labeled groups (vitepress
  customizations, community plugins, mdit-vue plugins) and note the
  order-sensitive couplings inline (lineNumbers after preWrapper,
  anchor after attrs)
- add a test for the `cjkFriendlyEmphasis` toggle

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

@ -101,5 +101,13 @@ describe('node/markdown/markdown', () => {
'tabindex' 'tabindex'
) )
}) })
test('cjkFriendlyEmphasis', async () => {
const src = 'これは**「テスト」**です'
expect(await render(src)).toContain('<strong>「テスト」</strong>')
expect(await render(src, { cjkFriendlyEmphasis: false })).not.toContain(
'<strong>'
)
})
}) })
}) })

@ -54,17 +54,21 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
/* ==================== General Options ==================== */ /* ==================== General Options ==================== */
/** /**
* Setup markdown-it instance before applying plugins * Configure the markdown-it instance before any plugins are applied.
*/ */
preConfig?: (md: MarkdownItAsync) => Awaitable<void> preConfig?: (md: MarkdownItAsync) => Awaitable<void>
/** /**
* Setup markdown-it instance * Configure the markdown-it instance after all built-in plugins are applied.
*/ */
config?: (md: MarkdownItAsync) => Awaitable<void> config?: (md: MarkdownItAsync) => Awaitable<void>
/** /**
* Disable cache (experimental) * Disable cache (experimental)
*/ */
cache?: boolean cache?: boolean
/**
* HTML attributes applied to external links.
* @default { target: '_blank', rel: 'noreferrer' }
*/
externalLinks?: Record<string, string> externalLinks?: Record<string, string>
/* ==================== Syntax Highlighting ==================== */ /* ==================== Syntax Highlighting ==================== */
@ -72,7 +76,8 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
/** /**
* Custom theme for syntax highlighting. * Custom theme for syntax highlighting.
* *
* You can also pass an object with `light` and `dark` themes to support dual themes. * You can also pass an object with `light` and `dark` themes to support
* dual themes.
* *
* @example { theme: 'github-dark' } * @example { theme: 'github-dark' }
* @example { theme: { light: 'github-light', dark: 'github-dark' } } * @example { theme: { light: 'github-light', dark: 'github-dark' } }
@ -91,7 +96,8 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
/** /**
* Custom language aliases for syntax highlighting. * Custom language aliases for syntax highlighting.
* Maps custom language names to existing languages. * Maps custom language names to existing languages.
* Alias lookup is case-insensitive and underscores in language names are displayed as spaces. * Alias lookup is case-insensitive and underscores in language names are
* displayed as spaces.
* *
* @example * @example
* *
@ -113,31 +119,11 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
*/ */
languageAlias?: Record<string, string> languageAlias?: Record<string, string>
/** /**
* Custom language labels for display. * Fallback language used when the specified language is not available.
* Overrides the default language label shown in code blocks.
* Keys are case-insensitive.
*
* @example { 'vue': 'Vue SFC' }
*/
languageLabel?: Record<string, string>
/**
* Show line numbers in code blocks. Requires the `preWrapper` plugin.
* @default false
*/
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.
*/ */
defaultHighlightLang?: string defaultHighlightLang?: string
/** /**
* Transformers applied to code blocks * Transformers applied to code blocks.
* @see https://shiki.style/guide/transformers * @see https://shiki.style/guide/transformers
*/ */
codeTransformers?: ShikiTransformer[] codeTransformers?: ShikiTransformer[]
@ -148,24 +134,46 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
*/ */
colorReplacements?: CodeToHastOptions['colorReplacements'] colorReplacements?: CodeToHastOptions['colorReplacements']
/** /**
* Setup Shiki instance * Configure the Shiki instance.
*/ */
shikiSetup?: (shiki: Highlighter) => void | Promise<void> shikiSetup?: (shiki: Highlighter) => void | Promise<void>
/* ==================== Code Blocks ==================== */
/**
* 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
/** /**
* The tooltip text for the copy button in code blocks * The tooltip text for the copy button in code blocks.
* @default 'Copy Code' * @default 'Copy Code'
*/ */
codeCopyButtonTitle?: string codeCopyButtonTitle?: string
/* ==================== Markdown It Plugins ==================== */
/** /**
* Options for `markdown-it-anchor`. Set to `false` to disable adding ids * Custom language labels for display.
* and anchor links to headings. Note that the default theme's outline and * Overrides the default language label shown in code blocks.
* heading hash links rely on these ids. * Keys are case-insensitive.
* @see https://github.com/valeriangalliat/markdown-it-anchor *
* @example { 'vue': 'Vue SFC' }
*/ */
anchor?: anchorPlugin.AnchorOptions | false languageLabel?: Record<string, string>
/**
* Show line numbers in code blocks. Requires the `preWrapper` plugin.
* @default false
*/
lineNumbers?: boolean
/**
* Enables importing code snippets from files with `<<<`.
* @default true
* @see https://vitepress.dev/guide/markdown#import-code-snippets
*/
snippet?: boolean
/* ==================== Markdown Extensions ==================== */
/** /**
* 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
@ -183,20 +191,26 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
} }
| false | false
/** /**
* Options for `@mdit-vue/plugin-frontmatter` * Improves emphasis (`**bold**`) handling in Japanese, Chinese, and
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter * Korean text.
* @default true
* @see https://github.com/tats-u/markdown-cjk-friendly
*/ */
frontmatter?: FrontmatterPluginOptions cjkFriendlyEmphasis?: boolean
/** /**
* Options for `@mdit-vue/plugin-headers` * Options for `markdown-it-anchor`. Set to `false` to disable adding ids
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers * 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
*/ */
headers?: HeadersPluginOptions | boolean anchor?: anchorPlugin.AnchorOptions | false
/** /**
* Options for `@mdit-vue/plugin-sfc` * Options for `@mdit-vue/plugin-headers`. Set to `true` or pass options
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-sfc * to collect page headers into page data.
* @default false
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers
*/ */
sfc?: SfcPluginOptions headers?: HeadersPluginOptions | boolean
/** /**
* Options for `@mdit-vue/plugin-toc`. Set to `false` to disable the * Options for `@mdit-vue/plugin-toc`. Set to `false` to disable the
* `[[toc]]` syntax. * `[[toc]]` syntax.
@ -204,30 +218,32 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
*/ */
toc?: TocPluginOptions | false toc?: TocPluginOptions | false
/** /**
* Options for `@mdit-vue/plugin-component`. Set to `false` to disable. * Math support.
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component *
* You need to install `markdown-it-mathjax3` and set `math` to `true` to
* enable it. You can also pass options to `markdown-it-mathjax3` here.
* @default false
* @see https://vitepress.dev/guide/markdown#math-equations
*/ */
component?: ComponentPluginOptions | false math?: boolean | any
/** /**
* Enables importing code snippets from files with `<<<`. * Custom labels for the built-in containers (`::: tip` etc.). Also used
* @default true * as the default titles of GitHub-flavored alerts.
* @see https://vitepress.dev/guide/markdown#import-code-snippets * @see https://vitepress.dev/guide/markdown#custom-containers
*/ */
snippet?: boolean container?: ContainerOptions
/** /**
* Options for `markdown-it-container` * Whether to enable GitHub-flavored alerts (`> [!NOTE]`).
* @see https://github.com/markdown-it/markdown-it-container * @default true
* @see https://vitepress.dev/guide/markdown#github-flavored-alerts
*/ */
container?: ContainerOptions gfmAlerts?: boolean
/** /**
* Math support * Add `tabindex="0"` to tables so keyboard users can focus and scroll
* * them.
* You need to install `markdown-it-mathjax3` and set `math` to `true` to enable it. * @default true
* You can also pass options to `markdown-it-mathjax3` here.
* @default false
* @see https://vitepress.dev/guide/markdown#math-equations
*/ */
math?: boolean | any tableTabIndex?: boolean
/** /**
* Options for the image plugin (resolves image sources against the public * Options for the image plugin (resolves image sources against the public
* directory, adds dimensions, and supports lazy loading). Set to `false` * directory, adds dimensions, and supports lazy loading). Set to `false`
@ -235,24 +251,24 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
* @see https://vitepress.dev/guide/markdown#image-lazy-loading * @see https://vitepress.dev/guide/markdown#image-lazy-loading
*/ */
image?: ImageOptions | false image?: ImageOptions | false
/* ==================== Vue Integration ==================== */
/** /**
* Allows disabling the github alerts plugin * Options for `@mdit-vue/plugin-component`. Set to `false` to disable.
* @default true * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component
* @see https://vitepress.dev/guide/markdown#github-flavored-alerts
*/ */
gfmAlerts?: boolean component?: ComponentPluginOptions | false
/** /**
* Add `tabindex="0"` to tables so keyboard users can focus and scroll them. * Options for `@mdit-vue/plugin-frontmatter`.
* @default true * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter
*/ */
tableTabIndex?: boolean frontmatter?: FrontmatterPluginOptions
/** /**
* Allows disabling the CJK-friendly plugin. * Options for `@mdit-vue/plugin-sfc`.
* This plugin adds support for emphasis marks (**bold**) in Japanese, Chinese, and Korean text. * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-sfc
* @default true
* @see https://github.com/tats-u/markdown-cjk-friendly
*/ */
cjkFriendlyEmphasis?: boolean sfc?: SfcPluginOptions
} }
export type MarkdownRenderer = MarkdownItAsync export type MarkdownRenderer = MarkdownItAsync
@ -287,7 +303,7 @@ export async function createMarkdownRenderer(
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' } const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code' const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code'
let [highlight, dispose] = options.highlight const [highlight, dispose] = options.highlight
? [options.highlight, () => {}] ? [options.highlight, () => {}]
: await createHighlighter(theme, options, logger) : await createHighlighter(theme, options, logger)
@ -305,21 +321,22 @@ export async function createMarkdownRenderer(
const slugify = const slugify =
(options.anchor ? options.anchor.slugify : undefined) ?? defaultSlugify (options.anchor ? options.anchor.slugify : undefined) ?? defaultSlugify
// custom plugins // VitePress customizations
if (options.component !== false) {
componentPlugin(md, options.component)
}
if (options.preWrapper !== false) { if (options.preWrapper !== false) {
preWrapperPlugin(md, { preWrapperPlugin(md, {
codeCopyButtonTitle, codeCopyButtonTitle,
languageLabel: options.languageLabel languageLabel: options.languageLabel
}) })
// must be applied after preWrapper as it augments its output
lineNumberPlugin(md, options.lineNumbers) lineNumberPlugin(md, options.lineNumbers)
} }
if (options.snippet !== false) { if (options.snippet !== false) {
snippetPlugin(md, srcDir) snippetPlugin(md, srcDir)
} }
containerPlugin(md, options.container) containerPlugin(md, options.container)
if (options.gfmAlerts !== false) {
gitHubAlertsPlugin(md, options.container)
}
if (options.image !== false) { if (options.image !== false) {
imagePlugin(md, publicDir, options.image) imagePlugin(md, publicDir, options.image)
} }
@ -329,25 +346,23 @@ export async function createMarkdownRenderer(
base, base,
slugify slugify
) )
if (options.tableTabIndex !== false) { if (options.tableTabIndex !== false) {
tablePlugin(md) tablePlugin(md)
} }
if (options.gfmAlerts !== false) { // community plugins
gitHubAlertsPlugin(md, options.container)
}
// third party plugins
if (options.attrs !== false) { if (options.attrs !== false) {
attrsPlugin(md, options.attrs) attrsPlugin(md, options.attrs)
} }
if (options.emoji !== false) { if (options.emoji !== false) {
emojiPlugin(md, options.emoji) emojiPlugin(md, options.emoji)
} }
if (options.cjkFriendlyEmphasis !== false) {
// mdit-vue plugins mditCjkFriendly(md)
}
if (options.anchor !== false) { if (options.anchor !== false) {
// must be applied after attrs so that user-defined ids from curly
// attributes take precedence over slugified ones
anchorPlugin(md, { anchorPlugin(md, {
slugify, slugify,
getTokensText: (tokens) => { getTokensText: (tokens) => {
@ -384,32 +399,6 @@ export async function createMarkdownRenderer(
...options.anchor ...options.anchor
}) })
} }
frontmatterPlugin(md, options.frontmatter)
if (options.headers) {
headersPlugin(md, {
level: [2, 3, 4, 5, 6],
slugify,
...(typeof options.headers === 'boolean' ? undefined : options.headers)
})
}
sfcPlugin(md, options.sfc)
titlePlugin(md)
const tocOptions = options.toc
if (tocOptions !== false) {
tocPlugin(md, {
slugify,
...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 {
const mathPlugin = await import('markdown-it-mathjax3') const mathPlugin = await import('markdown-it-mathjax3')
@ -435,8 +424,30 @@ export async function createMarkdownRenderer(
} }
} }
if (options.cjkFriendlyEmphasis !== false) { // mdit-vue plugins
mditCjkFriendly(md) if (options.component !== false) {
componentPlugin(md, options.component)
}
frontmatterPlugin(md, options.frontmatter)
if (options.headers) {
headersPlugin(md, {
level: [2, 3, 4, 5, 6],
slugify,
...(typeof options.headers === 'boolean' ? undefined : options.headers)
})
}
sfcPlugin(md, options.sfc)
titlePlugin(md)
const tocOptions = options.toc
if (tocOptions !== false) {
tocPlugin(md, {
slugify,
...tocOptions,
format: (s) => {
const title = s.replaceAll('&amp;', '&') // encoded twice because of restoreEntities
return tocOptions?.format?.(title) ?? title
}
})
} }
// apply user config // apply user config

Loading…
Cancel
Save