Markdown-it 插件体系详解

2 阅读3分钟

Markdown-it 插件体系详解


目录

  1. markdown-it-collapsible
  2. markdown-it-directive + markdown-it-directive-webcomponents
  3. markdown-it-container
  4. markdown-it-mathjax3
  5. 各插件对比与选型建议
  6. 其他常用 Markdown-it 插件参考

一、markdown-it-collapsible

语法

使用 +++ 分隔线定义折叠块,第一行是标题(summary),其余是折叠内容:

+++ 点击展开思考过程
这里是折叠的内容,支持 **Markdown**

- 列表项 1
- 列表项 2
+++

渲染结果

<details>
  <summary>点击展开思考过程</summary>
  <p>这里是折叠的内容,支持 <strong>Markdown</strong></p>
  <ul>
    <li>列表项 1</li>
    <li>列表项 2</li>
  </ul>
</details>

浏览器原生支持 <details>/<summary>,无需额外 JS 即可折叠展开。

项目中的用法

AI 模型(如 DeepSeek-R1)回复中会包含 <think>...</think> 思考过程,项目将其预处理转换为折叠块:

// useAnalysisChat.ts - 流式实时渲染
answer
  .replace(/<think>/g,  `++>${thoughtText}\n\n`)  // +++ 开始折叠
  .replace(/<\/think>/g, '\n++>\n\n')             // +++ 结束折叠

// index.vue - 历史会话还原(兼容旧格式)
content
  .replace(/\+\+>/g, '+++')
  .replace(/<think>/g,  `+++${thoughtText}\n\n`)
  .replace(/<\/think>/g, '\n+++\n\n')

渲染效果(配合 CSS 自定义样式):

▶ 思考过程 (2.35s)     ← <summary>,点击展开
  ...AI 的推理内容...   ← <details> 内容,默认折叠

使用场景

  • AI 思考过程(Chain-of-Thought)的折叠展示
  • FAQ 问答页面
  • 文档中的"展开详情"块
  • 代码注释说明的折叠

二、markdown-it-directive + markdown-it-directive-webcomponents

背景:什么是 Directive 语法?

CommonMark 规范没有定义自定义扩展语法,markdown-it-directive 实现了一套通用指令语法提案(Generic Directives):

:name[内联内容]{属性}         ← 内联指令(Leaf Inline Directive)

::name[内联内容]{属性}        ← 叶块指令(Leaf Block Directive)

:::name{属性}
  块内容
:::                           ← 容器指令(Container Block Directive)

markdown-it-directive 基础

markdown-it-directive 本身只是解析器,它把上述语法解析成 token,具体如何渲染由配套插件决定。

import MarkdownItDirective from 'markdown-it-directive';
md.use(MarkdownItDirective);
// 此时仅有解析能力,还需要注册具体渲染器

markdown-it-directive-webcomponents

markdown-it-directive-webcomponents 将指令语法渲染为 Web Component 标签,实现 Markdown 和前端组件的桥接。

项目配置
// src/utils/markdown.ts
.use(MarkdownItWebcomponents, {
  components: [{
    present: 'both',              // 'inline' | 'block' | 'both'
    name: 'tooltip-reference',    // 指令名(Markdown 中使用的名称)
    tag: 'ce-tooltip',            // 渲染为的 HTML/Web Component 标签名
    allowedAttrs: ['src', 'class', 'title', 'tips', 'content'],
    destStringName: 'tips',       // 内联内容映射到哪个属性
    parseInner: true              // 内部内容是否继续解析 Markdown
  }]
})
Markdown 语法
:tooltip-reference[参考文献 1]{src="/ref/1" tips="作者: 张三, 2024"}
渲染结果
<ce-tooltip src="/ref/1" tips="参考文献 1" content="作者: 张三, 2024"></ce-tooltip>
Web Component 定义(自行实现)
// 注册自定义元素
class CeTooltip extends HTMLElement {
  connectedCallback() {
    const tips = this.getAttribute('tips');
    const content = this.getAttribute('content');
    this.innerHTML = `
      <span class="tooltip-trigger">${tips}</span>
      <div class="tooltip-popup">${content}</div>
    `;
  }
}
customElements.define('ce-tooltip', CeTooltip);

使用场景

场景指令写法渲染组件
学术引用悬浮提示:tooltip-reference[^1]{tips="..."}<ce-tooltip>
内嵌视频::video{src="xxx.mp4"}<ce-video>
警告/提示框:::warning\n内容\n:::<ce-alert type="warning">
可交互图表::chart{data="..."}<ce-chart>
代码沙箱:::sandbox{lang="vue"}\n代码\n:::<ce-sandbox>

与普通 Markdown 扩展的区别

<!-- 普通方式:嵌入原生 HTML(需要 html:true) -->
<my-component data="xxx"></my-component>

<!-- Directive 方式:保持 Markdown 语法风格,更安全 -->
:my-component{data="xxx"}

Directive 方式的优势:

  • 语法更干净,不需要在 Markdown 里写 HTML
  • 可以精确控制哪些组件允许被使用(白名单 allowedAttrs
  • 内部内容仍可以是 Markdown(parseInner: true

三、markdown-it-container

语法

::: 容器名称 可选信息
内容(支持 Markdown)
:::

与 Directive 的区别

markdown-it-containermarkdown-it-directive
语法::: 三冒号:::name{attrs}
配置复杂度低,直接写 render 函数需要配套 webcomponents 插件
灵活性完全自定义 HTML映射到固定组件标签
适合场景复杂自定义渲染组件化映射

基本用法

render 函数只会被调用两次

markdown-it-container 的设计决定了 render 只在两种 token 上触发:

  • container_xxx_open(nesting === 1)
  • container_xxx_close(nesting === -1)

nesting === 0 的 token 永远不会触发 container 的 render 函数,它们有自己的渲染规则(inline、fence 等各管各的)。所以 else 分支里不需要判断是不是 -1,因为根本不会出现 0 进来的情况。

import MarkdownItContainer from 'markdown-it-container';

md.use(MarkdownItContainer, 'warning', {
  validate(params) {
    return params.trim() === 'warning';
  },
  render(tokens, idx) {
    if (tokens[idx].nesting === 1) {
      return '<div class="warning-box">\n';
    } else {
      return '</div>\n';
    }
  }
});
::: warning
这是一个警告提示框
:::

渲染结果:

<div class="warning-box">
  <p>这是一个警告提示框</p>
</div>

项目中的高级用法

项目用 markdown-it-container 实现了深度搜索流程的可视化渲染,核心技巧:

技巧 1:内部解析 JSON
// AI 输出的内容是 JSON,容器插件负责解析并渲染成结构化 HTML
.use(MarkdownItContainer, 'planner', {
  render(tokens, idx, options, env) {
    if (tokens[idx].nesting === 1) {
      const nextToken = tokens[idx + 2]; // 取容器内的 inline token
      const json = JSON.parse(nextToken.content);
      // 将 steps 数组渲染为有序列表
      const stepsHtml = json.steps.map((step, i) =>
        `<li><strong>${i+1}. ${step.title}</strong></li>`
      ).join('');
      return `<div class="planner"><ul>${stepsHtml}</ul><!--`;
    } else {
      return '-->';  // 注释掉原始 JSON 文本,不显示给用户
    }
  }
})

关键技巧:用 <!-- 开始 HTML 注释,--> 结束,把 AI 输出的原始 JSON 文本"藏"起来,只展示渲染后的 HTML。

技巧 2:通过 env 对象在容器间传递状态
// env 是 md.render(text, env) 的第二个参数
// 所有容器插件共享同一个 env 对象,可以用来传递状态

.use(MarkdownItContainer, 'planner', {
  render(tokens, idx, options, env) {
    if (tokens[idx].nesting === 1) {
      env.planner_steps = json.steps;  // ① planner 解析后存入 env
      env.stepIdx = 0;
    }
  }
})

.use(MarkdownItContainer, 'researcher', {
  render(tokens, idx, options, env) {
    if (tokens[idx].nesting === 1) {
      env.stepIdx++;
      // ② researcher 从 env 读取 planner 的步骤标题
      const stepTitle = env.planner_steps?.[env.stepIdx - 1]?.title || '';
      return `<details><summary>${stepTitle}</summary>`;
    }
  }
})

渲染时传入 env:

md.render(content, {
  stepIdx: 0,
  currentitleStepIdx: 0,
  thinkingFinished: false,
  messageList: messageList.value,
  currentAction: currentAction.value,
  currentAnswerIndex: currentAnswerIndex.value,
})
技巧 3:前瞻(Lookahead)后续 token
// 在 researcher 结束标签中,向后扫描是否还有更多 researcher
// 决定是否需要关闭外层 thinking-process 容器
const hasNextResearcherContainer = (tokens, idx) => {
  for (let i = idx + 1; i < tokens.length; i++) {
    if (tokens[i].type === 'container_researcher_open') {
      return { hasNext: true, isNotLastToken: true };
    }
    // 遇到非容器 token 就停止查找
    if (!tokens[i].type.startsWith('container_')) break;
  }
  return { hasNext: false, isNotLastToken: idx !== tokens.length - 1 };
};

render(tokens, idx, options, env) {
  if (tokens[idx].nesting === -1) { // 结束标签
    const { hasNext, isNotLastToken } = hasNextResearcherContainer(tokens, idx);
    if (!hasNext && isNotLastToken) {
      return `</details></div></div>`; // 关闭整个思考过程
    }
    return '</details>';
  }
}

四、markdown-it-mathjax3

语法

类型写法示例
内联公式$公式$$E=mc^2$
块公式$$公式$$$$\sum_{i=1}^{n} x_i$$
内联(括号)\(公式\)\(E=mc^2\)
块(方括号)\[公式\]\[\sum_{i=1}^{n} x_i\]

项目配置

.use(MarkdownItMathjax, {
  tex: {
    inlineMath: [['$ ', ' $'], ['\\(', '\\)']]
    // 注意:内联公式前后要有空格,避免与普通 $ 符号冲突
  }
})

为什么需要 escapeBrackets 预处理

AI 返回的 LaTeX 使用 \[...\]\(...\) 语法,Markdown-it 默认不识别,需要提前转换:

export const escapeBrackets = (text: string) => {
  const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
  return text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => {
    if (codeBlock) return escapeDollarSpace(codeBlock); // 代码块不处理
    if (squareBracket) return `$$${squareBracket}$$`;   // \[...\] → $$...$$
    if (roundBracket) return `$${roundBracket.trim()}$`; // \(...\) → $...$
    return match;
  });
};

五、各插件对比与选型建议

什么时候用哪个插件?

需要折叠/展开块?
  → markdown-it-collapsible(+++ 语法,渲染 <details>)

需要在 Markdown 中嵌入前端组件/Web Component?
  → markdown-it-directive + markdown-it-directive-webcomponents

需要完全自定义一段内容的渲染逻辑(含 JSON 解析、状态传递)?
  → markdown-it-container(最灵活)

需要渲染数学公式?
  → markdown-it-mathjax3(服务端渲染) 或 markdown-it-katex(纯前端)

三种"块扩展"语法对比

<!-- markdown-it-collapsible:固定的折叠语义 -->
+++ 标题
内容
+++

<!-- markdown-it-container:灵活的自定义语义 -->
::: warning
内容
:::

<!-- markdown-it-directive(容器指令):带属性的自定义语义 -->
:::name{key="value"}
内容
:::

六、其他常用 Markdown-it 插件参考

插件功能语法示例
markdown-it-anchor标题自动生成锚点 id## 标题<h2 id="biao-ti">
markdown-it-toc-done-right自动生成目录[[toc]]
markdown-it-task-lists任务列表- [x] 已完成 / - [ ] 未完成
markdown-it-mark高亮文本==高亮==<mark>高亮</mark>
markdown-it-footnote脚注文字[^1] + [^1]: 脚注内容
markdown-it-sub下标H~2~OH<sub>2</sub>O
markdown-it-sup上标x^2^x<sup>2</sup>
markdown-it-katex数学公式(KaTeX,纯前端)$E=mc^2$
markdown-it-mermaid流程图 ```mermaid\ngraph LR\n...
markdown-it-code-copy代码块复制按钮(开箱即用)自动注入复制按钮
markdown-it-attrs给任意元素添加属性# 标题 {.class #id}
markdown-it-multimd-table增强表格(合并单元格)`` 表示合并
markdown-it-plantumlUML 图 ```plantuml\n@startuml...

自定义渲染规则(不用插件)

有时不需要插件,直接覆盖 renderer.rules 即可:

// 项目中自定义表格渲染,外层加滚动容器
md.renderer.rules.table_open = () => '<div class="table-scroll"><table>';
md.renderer.rules.table_close = () => '</table></div>';

// 其他常见自定义
// 让所有链接在新标签页打开
const defaultRender = md.renderer.rules.link_open || ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
  tokens[idx].attrSet('target', '_blank');
  tokens[idx].attrSet('rel', 'noopener noreferrer');
  return defaultRender(tokens, idx, options, env, self);
};

// 图片懒加载
md.renderer.rules.image = (tokens, idx, options, env, self) => {
  tokens[idx].attrSet('loading', 'lazy');
  return self.renderToken(tokens, idx, options);
};

附:Markdown-it 插件开发模式

所有插件本质上是一个函数,接收 md 实例并注册规则:

// 插件的基本结构
function myPlugin(md: MarkdownIt, options?: any) {
  // 1. 添加新的内联/块规则
  md.inline.ruler.push('my-rule', (state, silent) => { ... });
  md.block.ruler.push('my-rule', (state, startLine, endLine, silent) => { ... });

  // 2. 覆盖渲染规则
  md.renderer.rules['my-token'] = (tokens, idx) => `<div>${tokens[idx].content}</div>`;

  // 3. 使用 core 规则(处理整个 token 流)
  md.core.ruler.push('my-rule', (state) => {
    state.tokens.forEach(token => { ... });
  });
}

md.use(myPlugin, { option1: 'value' });