Markdown-it 插件体系详解
目录
- markdown-it-collapsible
- markdown-it-directive + markdown-it-directive-webcomponents
- markdown-it-container
- markdown-it-mathjax3
- 各插件对比与选型建议
- 其他常用 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-container | markdown-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~O → H<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-plantuml | UML 图 | ```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' });