Skip to content

Markdown-it 插件开发指南

1. 概述

VS Code 的 Markdown 预览功能基于 markdown-it 库,扩展可以通过提供 markdown-it 插件来修改渲染输出。

2. 核心概念

2.1. Markdown Preview 的本质

重要:Markdown Preview 是 Webview,不是原生 UI。

特性原生 UIWebviewMarkdown Preview
渲染技术VS Code 原生组件自定义 HTML/CSS/JS自定义 HTML/CSS/JS
运行进程VS Code 主进程独立渲染进程独立渲染进程
通信方式直接 API 调用postMessagepostMessage
markdown-it❌ 不支持✅ 支持✅ 使用 markdown-it
自定义渲染规则❌ 不支持✅ 支持✅ 支持

2.2. 架构位置

VS Code 主进程
    Webview 渲染进程
        Markdown Preview
            - 加载 markdown-it
            - 执行 markdown-it 插件
            - 渲染 Markdown 为 HTML

Extension Host (Node.js)
    - 注册 markdown-it 插件
    - 提供插件配置和回调函数
    - 调用外部 API 或 SDK

3. 基本用法

3.1. 注册 markdown-it 插件

typescript
// extension.ts
import * as vscode from "vscode";
import type MarkdownIt from "markdown-it";

export function activate(context: vscode.ExtensionContext) {
    return {
        extendMarkdownIt(md: MarkdownIt) {
            // 注册你的插件
            return md.use(myPlugin, options);
        },
    };
}

3.2. 简单的 markdown-it 插件

typescript
// my-plugin.ts
import type MarkdownIt from "markdown-it";

export function myPlugin(md: MarkdownIt, options: any) {
    // 保存原始渲染规则
    const originalImageRule = md.renderer.rules.image;

    // 覆盖 image 渲染规则
    md.renderer.rules.image = (tokens, idx, options, env, self) => {
        const token = tokens[idx];
        const src = token.attrGet("src");

        // 修改图片 URL
        if (src && shouldModify(src)) {
            token.attrSet("src", modifyUrl(src));
        }

        // 调用原始渲染规则
        return originalImageRule(tokens, idx, options, env, self);
    };
}

4. 支持多个插件

4.1. 链式注册

typescript
// extension.ts
export function activate(context: vscode.ExtensionContext) {
    return {
        extendMarkdownIt(md: MarkdownIt) {
            // 注册多个插件(链式调用)
            return md.use(plugin1, options1).use(plugin2, options2).use(plugin3, options3);
        },
    };
}

4.2. 插件执行顺序

插件按注册顺序执行,后注册的插件可以覆盖先注册插件的规则。

重要:需要注意插件之间的冲突。

4.3. 避免插件冲突

typescript
// 好的做法:保存原始规则
export function safePlugin(md: MarkdownIt) {
    const originalRule = md.renderer.rules.image;

    md.renderer.rules.image = (tokens, idx, options, env, self) => {
        // 你的自定义逻辑
        modifyToken(tokens[idx]);

        // 调用原始规则(而不是直接渲染)
        return originalRule(tokens, idx, options, env, self);
    };
}

5. 与 Extension Host 通信

5.1. 为什么需要通信?

markdown-it 插件运行在 Webview(浏览器环境),而某些功能需要:

  • 访问文件系统
  • 调用 Node.js SDK
  • 访问网络资源(受 CSP 限制)

5.2. 通信机制

VS Code 的 extendMarkdownIt API 已经封装了通信,通过回调函数实现:

typescript
// extension.ts (Extension Host)
export function activate(context: vscode.ExtensionContext) {
    return {
        extendMarkdownIt(md: MarkdownIt) {
            return md.use(myPlugin, {
                // 通过回调函数与 Extension Host 通信
                getData: (key: string) => {
                    // 在 Extension Host 中执行
                    return getDataFromFileSystem(key);
                },
                requestData: async (key: string) => {
                    // 异步调用 Extension Host
                    return await fetchFromAPI(key);
                },
            });
        },
    };
}

5.3. 完整示例

typescript
// 插件定义(纯渲染,零依赖)
export interface PluginOptions {
    getData: (key: string) => string;
    requestData: (key: string) => Promise<string>;
}

export function myPlugin(md: MarkdownIt, options: PluginOptions) {
    md.renderer.rules.image = (tokens, idx, opts, env, self) => {
        const token = tokens[idx];
        const src = token.attrGet("src");

        // 调用回调函数(实际在 Extension Host 执行)
        const data = options.getData(src);

        // 异步请求
        options.requestData(src).then((result) => {
            // 处理结果
            refreshPreview();
        });

        return self.renderToken(tokens, idx, opts);
    };
}

6. 插件设计原则

6.1. 纯渲染原则

推荐:markdown-it 插件只负责渲染,不直接依赖 Node.js 模块。

typescript
// 好的设计:纯渲染
export function goodPlugin(md: MarkdownIt, options: Options) {
    md.renderer.rules.image = (tokens, idx, opts, env, self) => {
        // 只操作 token,不调用外部 API
        modifyToken(tokens[idx]);
        return self.renderToken(tokens, idx, opts);
    };
}

// 坏的设计:直接依赖 Node.js 模块
export function badPlugin(md: MarkdownIt) {
    const fs = require("fs"); // 在 Webview 中会报错!

    md.renderer.rules.image = (tokens, idx, opts, env, self) => {
        const data = fs.readFileSync("..."); // 错误!
        return self.renderToken(tokens, idx, opts);
    };
}

6.2. 回调函数设计

推荐:通过回调函数与外部通信,保持插件通用性。

typescript
// 好的设计:通过回调获取数据
export interface PluginOptions {
    getData: (key: string) => string;
    requestData: (key: string) => Promise<string>;
}

export function goodPlugin(md: MarkdownIt, options: PluginOptions) {
    // 使用 options.getData 和 options.requestData
}

6.3. 错误处理

typescript
export function robustPlugin(md: MarkdownIt, options: Options) {
    const originalRule = md.renderer.rules.image;

    md.renderer.rules.image = (tokens, idx, opts, env, self) => {
        try {
            // 你的逻辑
            modifyToken(tokens[idx]);
        } catch (error) {
            // 出错时返回原始渲染
            console.error("Plugin error:", error);
        }

        return originalRule(tokens, idx, opts, env, self);
    };
}

7. 常见问题

7.1. Q1: 一个扩展可以注册多个 markdown-it 插件吗?

A: 可以,使用链式调用:

typescript
return md.use(plugin1).use(plugin2).use(plugin3);

7.2. Q2: 插件之间会有冲突吗?

A: 可能会有,如果多个插件修改同一个渲染规则。解决方案:

  • 保存原始规则
  • 在自定义逻辑后调用原始规则
  • 注意插件注册顺序

7.3. Q3: markdown-it 插件可以在 Webview 外使用吗?

A: 可以,markdown-it 插件是纯 JavaScript,可以在任何支持 markdown-it 的环境中使用(Node.js、浏览器等)。

7.4. Q4: 如何在插件中调用外部 API?

A: 通过回调函数。插件不直接调用 API,而是通过 options 中的回调函数让调用方(Extension Host)处理。

8. 参考文档


创建日期:2026-04-08

基于 MIT 许可发布