【免费下载链接】mermaid-rs-renderer
A fast native Rust Mermaid diagram renderer. No browser required. 500-1000x faster than mermaid-cli.
mermaid-rs-renderer(简称mmdr)是一个用纯 Rust 写的Mermaid 图表渲染器,无需浏览器,比 mermaid-cli 快 100–1400 倍。它把一段.mmd文本解析成图表,再渲染为 SVG/PNG。本篇从源码角度拆解它最核心的两步——Parser(解析器)与IR(中间表示),讲清楚「一段 Mermaid 语法是如何变成一个可编程的图的」。
渲染管线全景:一行 Mermaid 到 SVG 的四段路
在深入之前,先建立全局地图。mmdr把渲染拆成了四个清晰分离的阶段:
.mmd 文本 → parser.rs → ir.rs (Graph) → layout.rs → render.rs → SVG → (resvg) → PNG每个阶段只做一件事:
| 阶段 | 职责 | 关键文件 |
|---|---|---|
| Parse(解析) | 识别图表类型、逐行解析语法 | src/parser.rs |
| IR(中间表示) | 把语法存成结构化数据Graph | src/ir.rs |
| Layout(布局) | 算出每个节点/连线的坐标 | src/layout/ |
| Render(渲染) | 把坐标画成 SVG 字符串 | src/render.rs |
本篇聚焦前两步。整体架构决策记录在 docs/architecture.md。
先看mmdr的解析+渲染最终能产出什么效果(左为mmdr,右为官方 mermaid-cli,对比来自 benches/fixtures):
第一步:Parser 如何认出 23 种图表
一切入口是 parse_mermaid():
pub fn parse_mermaid(input: &str) -> Result<ParseOutput> { validate_init_directives(input)?; let Some(kind) = detect_diagram_kind(input) else { bail!("unknown or missing Mermaid diagram header"); }; match kind { /* 分派到 23 个解析函数 */ } }它做了两件关键的事:
- 识别图表类型—— detect_diagram_kind() 扫描每一行,看开头是不是
flowchart、sequenceDiagram、erDiagram……等 23 个关键词,返回一个 DiagramKind 枚举。 - 分派给专门的解析函数—— 每种图表一个函数(
parse_flowchart、parse_sequence_diagram、parse_er_diagram……),互不干扰。
这个「关键词开头 + 分派」的设计非常易读:想加新图表,只需在detect_diagram_kind里加一个匹配,再写一个parse_xxx即可。🧩
预处理:先"打扫"输入再解析
真正逐行解析前,preprocess_input() 会先把原始文本"打扫"干净:
- 跳过 frontmatter:以
---包裹的 Markdown 元数据区域直接忽略; - 提取
%%{init: ...}%%指令:这是 Mermaid 的主题/配置开关(比如换主题色),解析成 JSON 单独保存,不参与图形本身; - 去掉
%%注释:行尾注释被 strip_trailing_comment() 剥离; - 去空行:空行直接丢弃。
这样一来,交给解析器的每一行都是干净的、有效的语句。预处理把"配置"和"图形"分开,是 IR 设计能保持简洁的前提。
Flowchart 逐行解析:一个手写状态机
Flowchart(流程图)是最常用也最典型的图表,我们以它为例。parse_flowchart() 是一个逐行扫描的状态机,用一张 benches/typical/flowchart.mmd 来对照:
flowchart TD A[Start] --> B{Decision} B -->|Yes| C[Process 1] B -->|No| D[Process 2]每一行都会按顺序尝试匹配:
HEADER_RE→ 识别flowchart TD,记下方向TD;end→ 关闭当前子图(subgraph);SUBGRAPH_RE→ 遇到subgraph,压入子图栈;direction LR→ 设置子图内部方向;classDef/class/style/linkStyle→ 样式指令,直接落到 IR;- 连边→
A --> B这类带箭头的行; - 孤节点→ 只声明节点不带边的行。
注意它用了一个subgraph_stack(子图栈)来跟踪"当前节点属于哪个子图",从而支持嵌套subgraph。这是典型的手写递归下降解析,不依赖任何解析器生成器,逻辑一目了然。📐
节点形状:方括号里藏着什么
一个节点 token 如B{Decision},{和}决定了它的形状。parse_node_token() 负责拆解出id、label、shape三要素:
let (id, label, shape) = split_id_label(trimmed); // "B{Decision}" → ("B", "Decision", Diamond)split_id_label() 按括号类型映射到 NodeShape 枚举,这套映射几乎与 Mermaid 官方一一对应:
| 语法 | 形状 | 语法 | 形状 |
|---|---|---|---|
[text] | 矩形 | ((text)) | 双圆 |
(text) | 圆角 | [[text]] | 子程序 |
{text} | 菱形 | [(text)] | 圆柱 |
{{text}} | 六边形 | >text] | 不对称 |
解析结果里,:::class这类内联样式会被 split_inline_classes() 单独剥离出来。
连边解析:拆链、拆标签
Flowchart 里最"狡猾"的是连边。一行A --> B --> C其实是一条边链(chain),split_edge_chain() 会先把它拆成A --> B和B --> C两条。
再看带标签的B -->|Yes| C,parse_edge_line() 用一组正则(PIPE_LABEL_RE等)把「左节点」「箭头样式」「标签」「右节点」四部分切开,再交给 parse_edge_meta() 判断箭头方向、虚线/粗线等细节。
解析完,连边变成一个 Edge 结构:from、to、label、directed、style……每个字段都为后续布局/渲染准备好。
第二步:IR——Graph 结构体
解析的终点是一个统一的 Graph 结构体。它是整个项目的心脏,把 23 种图表"拍平"进同一个类型:
pub struct Graph { pub kind: DiagramKind, pub direction: Direction, pub nodes: BTreeMap<String, Node>, pub node_order: HashMap<String, usize>, pub edges: Vec<Edge>, pub subgraphs: Vec<Subgraph>, pub sequence_participants: Vec<String>, pub pie_slices: Vec<PieSlice>, pub gantt_tasks: Vec<GanttTask>, /* ...每种图表专属的字段 */ }设计上有两个巧思值得注意:
BTreeMap存节点:用有序映射而非Vec,保证节点按 id 稳定排序,这是输出确定性(deterministic output)的关键——同样输入永远得到同样坐标,方便做 diff。node_order记录写入顺序:虽然BTreeMap有序,但布局时还需要"作者书写顺序",所以额外用HashMap记下每个节点首次出现的序号。
节点通过 ensure_node() 创建:如果节点已存在就更新标签/形状,不存在才新建——这完美匹配 Mermaid「在连边里提到节点就自动创建」的语义。
下面这张 C4 架构图(benches/fixtures/c4_medium.mmd)就来自这样一个Graph:人物、系统、边界框、带标签的连线,全部是nodes+edges+subgraphs的组合:
一个 Graph 承载 23 种图表
你可能会问:为什么流程图、序列图、饼图、Gantt……全塞进同一个Graph?
答案是统一管线。每种图表解析后,把自己特有的数据填进Graph的对应字段(流程图填nodes/edges,序列图填sequence_participants+sequence_frames,饼图填pie_slices),然后走同一条layout → render。布局层只需按kind分派,而不用面对 23 套完全不同的类型系统。
比如序列图 benches/typical/sequence.mmd,它的参与者、消息、alt分支框,最终分别落在sequence_participants、edges、sequence_frames三个字段里:
这种"宽结构体 + 枚举分派"的取舍,是用一定字段冗余换取了管线复用与代码可维护性。
从 Parser 到 Layout 的交接
解析完成后,parse_mermaid() 返回一个 ParseOutput,包含两样东西:
pub struct ParseOutput { pub graph: Graph, // 结构化的图 pub init_config: Option<Value>, // %%{init}%% 提取的配置 }graph交给layout阶段算坐标;init_config交给render阶段决定主题(在 resolve_options() 里,init 指令会覆盖默认主题,与 mermaid.js 语义一致)。
如果输入不合法,mmdr还有一套带行列号的类型化错误 ParseError(如"第 N 行未声明的参与者""未闭合的 subgraph"),让编辑器、CMS 甚至 LLM 修复循环都能给出可操作的诊断。
小结
本篇带你看完了mmdr的"上半场":
- Parser(src/parser.rs)——逐行、手写、按图表类型分派,把 Mermaid 语法拆成节点、连边、子图和各类指令;
- IR(src/ir.rs)——用一个统一的
Graph结构体承载全部 23 种图表,用有序结构保证输出确定性。
这两步把"文本"变成了"可编程的数据"。下一篇《源码剖析(二)》我们继续深入Layout 布局引擎——节点如何分层、连线如何避让、preferredAspectRatio又是如何重排几何的。
更多示例与基准见 benches/ 目录,逐图渲染对比报告在 docs/conformance-report/。
【免费下载链接】mermaid-rs-renderer
A fast native Rust Mermaid diagram renderer. No browser required. 500-1000x faster than mermaid-cli.
相关推荐
Mermaid Venn 图(venn-beta)语法详解与源码实现剖析
Mermaid Venn 图(venn beta)语法详解与源码实现剖析 本文围绕 Mermaid 官方语法文档 docs/syntax/venn.md htt
图表库前端数据可视化如何用 mermaid-rs-renderer 批量渲染 Markdown 中的 Mermaid 图表:CLI 实战指南
如何用 mermaid rs renderer 批量渲染 Markdown 中的 Mermaid 图表:CLI 实战指南 mermaid rs renderer
Mermaid 图表一次画 23 种:mermaid-rs-renderer 全类型支持完整清单与示例
Mermaid 图表一次画 23 种:mermaid rs renderer 全类型支持完整清单与示例 mermaid rs renderer(mmdr)是一个
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考