You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vitepress/src/node/markdown/markdown.ts

285 lines
8.1 KiB

import {
componentPlugin,
type ComponentPluginOptions
} 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'
// @ts-ignore
import { full as emojiPlugin } from 'markdown-it-emoji'
import type {
BuiltinTheme,
Highlighter,
LanguageInput,
ShikijiTransformer,
ThemeRegistrationAny
} from 'shikiji'
import type { Logger } from 'vite'
import { containerPlugin, type ContainerOptions } from './plugins/containers'
import { highlight } from './plugins/highlight'
import { highlightLinePlugin } from './plugins/highlightLines'
import { imagePlugin, type Options as ImageOptions } 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 =
| ThemeRegistrationAny
| BuiltinTheme
| {
light: ThemeRegistrationAny | BuiltinTheme
dark: ThemeRegistrationAny | BuiltinTheme
}
export interface MarkdownOptions extends MarkdownIt.Options {
/* ==================== General Options ==================== */
/**
* Setup markdown-it instance before applying plugins
*/
preConfig?: (md: MarkdownIt) => void
/**
* Setup markdown-it instance
*/
config?: (md: MarkdownIt) => void
/**
* Disable cache (experimental)
*/
cache?: boolean
externalLinks?: Record<string, string>
/* ==================== Syntax Highlighting ==================== */
/**
* Custom theme for syntax highlighting.
*
* You can also pass an object with `light` and `dark` themes to support dual themes.
*
* @example { theme: 'github-dark' }
* @example { theme: { light: 'github-light', dark: 'github-dark' } }
*
* You can use an existing theme.
* @see https://github.com/antfu/shikiji/blob/main/docs/themes.md#all-themes
* Or add your own theme.
* @see https://github.com/antfu/shikiji/blob/main/docs/themes.md#load-custom-themes
*/
theme?: ThemeOptions
/**
* Languages for syntax highlighting.
* @see https://github.com/antfu/shikiji/blob/main/docs/languages.md#all-themes
*/
languages?: LanguageInput[]
/**
* Custom language aliases.
*
* @example { 'my-lang': 'js' }
* @see https://github.com/antfu/shikiji/tree/main#custom-language-aliases
*/
languageAlias?: Record<string, string>
/**
* Show line numbers in code blocks
* @default false
*/
lineNumbers?: boolean
/**
* Fallback language when the specified language is not available.
*/
defaultHighlightLang?: string
/**
* Transformers applied to code blocks
* @see https://github.com/antfu/shikiji#hast-transformers
*/
codeTransformers?: ShikijiTransformer[]
/**
* Setup Shikiji instance
*/
shikijiSetup?: (shikiji: Highlighter) => void | Promise<void>
/* ==================== Markdown It Plugins ==================== */
/**
* Options for `markdown-it-anchor`
* @see https://github.com/valeriangalliat/markdown-it-anchor
*/
anchor?: anchorPlugin.AnchorOptions
/**
* Options for `markdown-it-attrs`
* @see https://github.com/arve0/markdown-it-attrs
*/
attrs?: {
leftDelimiter?: string
rightDelimiter?: string
allowedAttributes?: Array<string | RegExp>
disable?: boolean
}
/**
* Options for `markdown-it-emoji`
* @see https://github.com/markdown-it/markdown-it-emoji
*/
emoji?: {
defs?: Record<string, string>
enabled?: string[]
shortcuts?: Record<string, string | string[]>
}
/**
* Options for `@mdit-vue/plugin-frontmatter`
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter
*/
frontmatter?: FrontmatterPluginOptions
/**
* Options for `@mdit-vue/plugin-headers`
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers
*/
headers?: HeadersPluginOptions | boolean
/**
* Options for `@mdit-vue/plugin-sfc`
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-sfc
*/
sfc?: SfcPluginOptions
/**
* Options for `@mdit-vue/plugin-toc`
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-toc
*/
toc?: TocPluginOptions
/**
* Options for `@mdit-vue/plugin-component`
* @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component
*/
component?: ComponentPluginOptions
/**
* Options for `markdown-it-container`
* @see https://github.com/markdown-it/markdown-it-container
*/
container?: ContainerOptions
/**
* Math support (experimental)
*
* 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
*/
math?: boolean | any
image?: ImageOptions
}
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, logger)),
...options
})
md.linkify.set({ fuzzyLink: false })
if (options.preConfig) {
options.preConfig(md)
}
// custom plugins
md.use(componentPlugin, { ...options.component })
.use(highlightLinePlugin)
.use(preWrapperPlugin, { hasSingleTheme })
.use(snippetPlugin, srcDir)
.use(containerPlugin, { hasSingleTheme }, options.container)
.use(imagePlugin, options.image)
.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, { ...options.emoji })
// mdit-vue plugins
md.use(anchorPlugin, {
slugify,
permalink: anchorPlugin.permalink.linkInsideHeader({
symbol: '&ZeroWidthSpace;',
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)
if (options.math) {
try {
const mathPlugin = await import('markdown-it-mathjax3')
md.use(mathPlugin.default ?? mathPlugin, {
...(typeof options.math === 'boolean' ? {} : options.math)
})
} catch (error) {
throw new Error(
'You need to install `markdown-it-mathjax3` to use math support.'
)
}
}
// apply user config
if (options.config) {
options.config(md)
}
return md
}