深入 VitePress 源码解析:Markdown 解析

在 VitePress 的内部,Markdown 文件的解析和渲染是通过一系列的插件和工具来完成的。本文将带你深入了解 VitePress 如何利用 markdown-it 和其他插件实现 Markdown 的解析和渲染过程。

源码地址

1. Markdown 解析核心:createMarkdownRenderer

createMarkdownRenderer 是 VitePress 的 Markdown 渲染器核心,它的职责是创建一个 MarkdownIt 实例,并应用一系列插件来增强 Markdown 的功能。

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 codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code'
  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 })
  md.use(restoreEntities)
  ...
}
  1. 基础配置:
    • 创建 MarkdownIt 实例时,启用了 htmllinkify 选项,支持 HTML 语法和自动链接。
    • 使用 highlight 进行代码高亮处理,支持定制主题。
  2. 插件的应用:
    • restoreEntities: 用于还原 HTML 实体。
    • linkify 设置了 fuzzyLink: false,以禁止模糊链接解析。

2. 自定义插件

VitePress 使用了多个自定义插件来扩展 Markdown 的功能,例如支持代码块高亮、行号、图片处理、外部链接处理等。

  • 自定义组件插件componentPlugin

    md.use(componentPlugin, { ...options.component })

    这个插件支持在 Markdown 中使用 Vue 组件。

  • 代码高亮插件highlightLinePlugin

    md.use(highlightLinePlugin)

    用于代码块的行高亮功能,通常用于演示代码片段时突出显示重要行。

  • 预包装插件preWrapperPlugin

    md.use(preWrapperPlugin, { codeCopyButtonTitle, hasSingleTheme })

    为代码块添加额外的包装层,支持代码复制按钮和双主题显示。

  • 代码片段插件snippetPlugin

    md.use(snippetPlugin, srcDir)

    用于从文件中插入代码片段的插件。

3. 第三方插件

VitePress 还使用了多种第三方插件,例如 markdown-it-anchormarkdown-it-attrsmarkdown-it-emoji 等。

  • 锚点插件anchorPlugin

    md.use(anchorPlugin, {
      slugify,
      permalink: anchorPlugin.permalink.linkInsideHeader({ ... })
    } as anchorPlugin.AnchorOptions)

    用于为标题生成锚点链接,使得页面内导航更加便捷。

  • 属性插件attrsPlugin

    if (!options.attrs?.disable) {
      md.use(attrsPlugin, options.attrs)
    }

    支持在 Markdown 中使用自定义属性。

  • 表情符号插件emojiPlugin

    md.use(emojiPlugin, { ...options.emoji })

    使得 Markdown 支持 GitHub 风格的表情符号。

4. VitePress 专属插件

VitePress 定制了多个插件,以提供更丰富的 Markdown 体验。

  • GitHub Alerts 插件gitHubAlertsPlugin

    if (options.gfmAlerts !== false) {
      md.use(gitHubAlertsPlugin)
    }

    支持 GitHub 风格的警告框,默认启用。

  • 容器插件containerPlugin

    md.use(containerPlugin, { hasSingleTheme }, options.container)

    这个插件允许你创建自定义的 Markdown 容器,例如提示、警告框等。

5. 自定义 Markdown 配置

MarkdownOptions 提供了丰富的选项以供用户自定义 Markdown 行为,包括语法高亮、数学公式支持、Markdown-it 插件配置等。

  • 代码高亮主题theme
  • 自定义语言别名languageAlias
  • 代码复制按钮文本codeCopyButtonTitle
  • Markdown-it 插件配置:如 anchorattrsemoji 等。

总结

通过 createMarkdownRenderer 函数,VitePress 使用 markdown-it 及其插件生态,结合自定义插件,实现了一个强大而灵活的 Markdown 渲染器。这样不仅提升了 Markdown 的解析能力,也使得内容展示更加多样化和用户友好。深入理解这些插件的实现细节,将有助于开发者更好地定制和扩展 VitePress 的功能。

js版本源码

// 引入各种插件和依赖项
const MarkdownIt = require('markdown-it');
const anchorPlugin = require('markdown-it-anchor');
const attrsPlugin = require('markdown-it-attrs');
const { full: emojiPlugin } = require('markdown-it-emoji');
const { slugify } = require('@mdit-vue/shared');
const { componentPlugin } = require('@mdit-vue/plugin-component');
const { frontmatterPlugin } = require('@mdit-vue/plugin-frontmatter');
const { headersPlugin } = require('@mdit-vue/plugin-headers');
const { sfcPlugin } = require('@mdit-vue/plugin-sfc');
const { titlePlugin } = require('@mdit-vue/plugin-title');
const { tocPlugin } = require('@mdit-vue/plugin-toc');
const { containerPlugin } = require('./plugins/containers');
const { gitHubAlertsPlugin } = require('./plugins/githubAlerts');
const { highlight } = require('./plugins/highlight');
const { highlightLinePlugin } = require('./plugins/highlightLines');
const { imagePlugin } = require('./plugins/image');
const { lineNumberPlugin } = require('./plugins/lineNumbers');
const { linkPlugin } = require('./plugins/link');
const { preWrapperPlugin } = require('./plugins/preWrapper');
const { restoreEntities } = require('./plugins/restoreEntities');
const { snippetPlugin } = require('./plugins/snippet');
 
/**
 * @typedef {Object} ThemeOptions
 * @property {string|Object} ThemeRegistrationAny 主题名称或注册对象
 * @property {string} BuiltinTheme 内置主题
 * @property {{ light: string, dark: string }} DualTheme 支持双主题模式
 */
 
/**
 * @typedef {Object} MarkdownOptions
 * @property {Function} preConfig - 在应用插件之前设置 `markdown-it` 实例
 * @property {Function} config - 设置 `markdown-it` 实例
 * @property {boolean} cache - 是否禁用缓存(实验性)
 * @property {Object} externalLinks - 外部链接的配置对象
 * @property {ThemeOptions} theme - 语法高亮的自定义主题
 * @property {Array} languages - 用于语法高亮的语言列表
 * @property {Object} languageAlias - 自定义语言别名
 * @property {boolean} lineNumbers - 是否在代码块中显示行号
 * @property {string} defaultHighlightLang - 代码块默认语言
 * @property {Array} codeTransformers - 用于代码块的变换器
 * @property {Function} shikiSetup - 设置 Shiki 实例
 * @property {string} codeCopyButtonTitle - 代码块中复制按钮的工具提示文本
 * @property {Object} anchor - `markdown-it-anchor` 插件的选项
 * @property {Object} attrs - `markdown-it-attrs` 插件的选项
 * @property {Object} emoji - `markdown-it-emoji` 插件的选项
 * @property {Object} frontmatter - `@mdit-vue/plugin-frontmatter` 插件的选项
 * @property {Object} headers - `@mdit-vue/plugin-headers` 插件的选项
 * @property {Object} sfc - `@mdit-vue/plugin-sfc` 插件的选项
 * @property {Object} toc - `@mdit-vue/plugin-toc` 插件的选项
 * @property {Object} component - `@mdit-vue/plugin-component` 插件的选项
 * @property {Object} container - `markdown-it-container` 插件的选项
 * @property {boolean|Object} math - 数学公式支持选项
 * @property {Object} image - 图片插件的选项
 * @property {boolean} gfmAlerts - 是否启用 GitHub 风格的警告插件
 */
 
/**
 * @typedef {Object} MarkdownRenderer
 * @description 定义了 `MarkdownIt` 实例作为 Markdown 渲染器
 */
 
/**
 * 创建一个新的 Markdown 渲染器
 * @param {string} srcDir - Markdown 文件的源目录
 * @param {MarkdownOptions} [options={}] - Markdown 渲染的选项
 * @param {string} [base='/'] - 基础 URL
 * @param {Object} [logger=console] - 日志记录器,用于输出警告
 * @returns {Promise<MarkdownRenderer>} 返回 `MarkdownIt` 实例作为渲染器
 */
const createMarkdownRenderer = async (srcDir, options = {}, base = '/', logger = console) => {
  const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' };
  const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code';
  const hasSingleTheme = typeof theme === 'string' || 'name' in theme;
 
  // 创建 MarkdownIt 实例
  const md = new MarkdownIt({
    html: true,
    linkify: true,
    highlight: options.highlight || (await highlight(theme, options, logger)),
    ...options
  });
 
  md.linkify.set({ fuzzyLink: false }); // 配置 `linkify` 选项以禁用模糊链接检测
  md.use(restoreEntities); // 使用 `restoreEntities` 插件
 
  if (options.preConfig) {
    options.preConfig(md); // 在应用插件之前设置 `markdown-it` 实例
  }
 
  // 使用自定义插件
  md.use(componentPlugin, { ...options.component })
    .use(highlightLinePlugin)
    .use(preWrapperPlugin, { codeCopyButtonTitle, 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);
 
  // 设置表格的 HTML 输出规则,使其具有键盘焦点支持
  md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
    return '<table tabindex="0">\n';
  };
 
  // 启用 GitHub 风格的警告插件(如果选项启用)
  if (options.gfmAlerts !== false) {
    md.use(gitHubAlertsPlugin);
  }
 
  // 使用第三方插件
  if (!options.attrs?.disable) {
    md.use(attrsPlugin, options.attrs);
  }
  md.use(emojiPlugin, { ...options.emoji });
 
  // 使用 mdit-vue 插件
  md.use(anchorPlugin, {
    slugify,
    permalink: anchorPlugin.permalink.linkInsideHeader({
      symbol: '&ZeroWidthSpace;',
      renderAttrs: (slug, state) => {
        const idx = state.tokens.findIndex((token) => {
          const attrs = token.attrs;
          const id = attrs?.find((attr) => attr[0] === 'id');
          return id && slug === id[1];
        });
        const title = state.tokens[idx + 1].content;
        return {
          'aria-label': `Permalink to "${title}"`
        };
      }
    }),
    ...options.anchor
  }).use(frontmatterPlugin, { ...options.frontmatter });
 
  // 处理标题插件
  if (options.headers) {
    md.use(headersPlugin, {
      level: [2, 3, 4, 5, 6],
      slugify,
      ...(typeof options.headers === 'boolean' ? undefined : options.headers)
    });
  }
 
  // 如果提供了配置回调,则调用配置回调
  if (options.config) {
    options.config(md);
  }
 
  return md; // 返回渲染器实例
};
 
module.exports = {
  createMarkdownRenderer
};