news 2026/10/10 21:18:05

mermaid-rs-renderer 源码剖析(一):Parser 与 IR——如何把 Mermaid 语法解析成图

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
mermaid-rs-renderer 源码剖析(一):Parser 与 IR——如何把 Mermaid 语法解析成图

【免费下载链接】mermaid-rs-renderer

A fast native Rust Mermaid diagram renderer. No browser required. 500-1000x faster than mermaid-cli.

项目地址:https://gitcode.com/gh_mirrors/me/mermaid-rs-renderer
点击查看免费下载

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(中间表示)把语法存成结构化数据Graphsrc/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 个解析函数 */ } }

它做了两件关键的事:

  1. 识别图表类型—— detect_diagram_kind() 扫描每一行,看开头是不是flowchart、sequenceDiagram、erDiagram……等 23 个关键词,返回一个 DiagramKind 枚举。
  2. 分派给专门的解析函数—— 每种图表一个函数(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]

每一行都会按顺序尝试匹配:

  1. HEADER_RE→ 识别flowchart TD,记下方向TD;
  2. end→ 关闭当前子图(subgraph);
  3. SUBGRAPH_RE→ 遇到subgraph,压入子图栈;
  4. direction LR→ 设置子图内部方向;
  5. classDef/class/style/linkStyle→ 样式指令,直接落到 IR;
  6. 连边→A --> B这类带箭头的行;
  7. 孤节点→ 只声明节点不带边的行。

注意它用了一个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.

项目地址:https://gitcode.com/gh_mirrors/me/mermaid-rs-renderer
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 21:18:04

ThinkPHP项目迁移Swoole实战:常驻内存、连接池与协程优化

1. 为什么要把 ThinkPHP 项目跑在 Swoole 上先聊个现象。很多 PHP 开发者一说 Swoole 就摇头&#xff0c;觉得那是“常驻内存”的东西&#xff0c;心智负担重&#xff0c;学了容易忘&#xff0c;项目一忙就想扔回 FPM 的怀抱。但真实场景是——你的 ThinkPHP 项目一旦流量上来&…

作者头像 李华
网站建设 2026/10/10 21:12:47

Selenium显式等待优化实战:回归测试耗时降低41%的改造方案

我曾经接过一个支付类后台的回归测试优化任务。套件里有 60 条用例&#xff0c;跑完要一个小时出头&#xff0c;其中大量时间花在界面上转圈、按钮半天不亮、弹窗迟迟不弹这些"无谓等待"上。把耗时明细打出来以后发现&#xff0c;Selenium 的WebDriverWait占了差不多…

作者头像 李华
网站建设 2026/10/10 21:10:06

电子产品设计开发管理流程:从需求到量产的落地指南

简介&#xff1a;这份《电子产品设计开发管理流程》文档面向硬件产品经理、研发工程师及项目管理者&#xff0c;用于规范自主产品从需求到量产的全过程管理。内容围绕角色职责、工程启动、流程图与开发流程展开&#xff0c;明确产品经理、工程经理、软件、硬件、结构、测试、采…

作者头像 李华
网站建设 2026/10/10 21:07:48

特征工程全流程实战:从数据清洗到特征选择提升模型性能

直接甩开键盘写吧。特征工程这活儿&#xff0c;干久了你会觉得它像做饭——模型是锅&#xff0c;数据是菜&#xff0c;特征工程就是切配和调味。菜切得乱七八糟&#xff0c;锅再好也白搭&#xff1b;调料放得恰到好处&#xff0c;家常豆腐也能做出宴席味。今天这篇&#xff0c;…

作者头像 李华
网站建设 2026/10/10 21:05:06

AnyPS5:面向PS5游戏内容的本地化管理与兼容性分析框架

项目标题&#xff1a;“AnyPS5”这个名称本身带有强烈的指向性与模糊性并存的特征——它既像一个技术代号&#xff0c;又像一句口号&#xff1b;既暗示兼容性、泛用性&#xff08;“Any”&#xff09;&#xff0c;又锚定在特定硬件生态&#xff08;“PS5”&#xff09;。但问题…

作者头像 李华
网站建设 2026/10/10 21:01:07

Spring Boot + Redis + Kafka 构建高并发秒杀系统架构设计与实战

秒杀&#xff0c;几乎是Java后端面试里绕不开的“高并发试金石”。Spring Boot、Kafka、Redis 这三件套&#xff0c;是电商秒杀方案里最常被问到的组合。面试官只要问出“让你设计一个秒杀系统&#xff0c;你怎么做”&#xff0c;大概率就是想从流量削峰、库存扣减、数据一致性…

作者头像 李华