pulldown-cmark 源码解析
pulldown-cmark 是一个用 Rust 写的 CommonMark 解析器。README 的第一句是「This library is a pull parser for CommonMark」:它不把文档交给回调,也不交出一棵语法树,而是给你一个 Iterator<Item = Event>,你要一个事件,它算一个。实现上分两遍:Parser::new_ext 先把整份文档逐行扫一遍,建出一棵放在 Vec 里的块树(段落、列表、引用块、代码块、表格);之后每次调 next,它沿着树往下走,走到可能含有强调、链接、代码段的地方才解析行内语法。事件里的文本大多直接借用输入,HTML 输出交给 html 模块和单独发布的 pulldown-cmark-escape。这几句话落在哪些代码上、和建 AST 的 comrak、markdown-rs 比差在哪里,见为什么是 pull parser。
锁定的版本
| 项 | 本卷 | 说明 |
|---|---|---|
| 版本 | 0.13.5 | 主 crate pulldown-cmark;同仓库的 pulldown-cmark-escape 在这个 tag 上是 0.11.0,源码和 2024-05-15 发布 0.11.0 时相同 |
| tag | v0.13.5 → 48a2767(2026-10-21,「Merge pull request #1181 from pulldown-cmark/release-0.13.5」) | crates.io 上 pulldown-cmark-0.13.5 包的 .cargo_vcs_info.json 记的就是这个 commit |
| 历史 | 首个提交 2015-04-18(「Initial commit of pull parser idea」),到 tag 共 1708 个提交 | |
| 分支 | 0.13.5 从 branch_0.13 发出:0.13.4 加上从 main 摘过来的 8 个 PR | 这条分支从 0.13.0 分出,tag 不是 main 的祖先;分出之后 main 上又有 262 个提交,是下一个大版本 |
| 本站 | 0.13.4(crates/widgets,features = ["html"]) | 0.13.4 到 0.13.5 改了 firstpass.rs 和 parse.rs,差别在 08 在本站中 讲 |
源码链接一律指向 tag,形如 FirstPass::parse_block。
讲什么
- 块解析:
Tree<Item>这棵扁平数组上的树;FirstPass一行一行地扫,scan_containers判断已打开的容器还在不在;段落、标题、代码块、HTML 块等叶子块;引用块、列表、定义列表等容器块;表格与脚注;链接引用定义在第一遍就收走。 - 行内解析:
Parser::next走到可能含行内元素的节点时才调handle_inline;强调用的分隔符栈;链接与图片、标签规范化;代码段、行内 HTML、实体;数学与智能标点。 - 字符串与内存:
CowStr的三种形态,一次解析到底分配了什么。 - 输出:
HtmlWriter、pulldown-cmark-escape的三种转义、在事件流上做改写。 - 只提一句:
src/main.rs的命令行工具;entities.rs、puncttable.rs这两张生成的表只讲怎么查;fuzz/、dos-fuzzer/只讲它们防的是什么;guide/当资料读。
三条主线
全卷沿三条主线展开,每页开头标出自己在哪条线上。三条线用同一份 21 行的例子文档,原文在怎么读这一卷:
- 块主线(一份文档建成树):
Parser::new_ext→run_first_pass→FirstPass::parse_block每次处理一行,先用scan_containers匹配已打开的容器,再决定这一行开什么块 → 得到Tree<Item>和Allocations(链接定义、CowStr、表格对齐、标题属性)。 - 行内主线(一个段落变成事件):
Parser::next沿树走 → 遇到MaybeEmphasis、MaybeCode、MaybeLinkOpen这类占位节点时调handle_inline→handle_inline_pass1(代码段、链接、HTML)→handle_emphasis_and_hard_break(分隔符栈)→ 把节点改写成Emphasis、Code、Link→ 变成Event。 - 输出主线(事件变成 HTML):
html::push_html→HtmlWriter::run→start_tag/end_tag→escape_html_body_text、escape_href等转义。本站不走这条线,直接把事件转成 Dioxus 的 RSX。
每条线的每一步落在哪个函数、哪一页讲,见怎么读这一卷;三条线在一份文档上完整跑一遍,见一份文档。
flowchart TB intro[00 导读] --> overview[01 全景] overview --> blocks[02 块解析] blocks --> inlines[03 行内解析] inlines --> mem[04 字符串与内存] mem --> out[05 输出] out --> design[06 设计取舍] design --> pit[07 常见坑] pit --> site[08 在本站中] site --> hands[09 动手]
实验环境
关于行为的说法(产出哪些事件、树长什么样、分配了几次、多快)都配一个小程序和它的真实输出。平台是 M2 Max、macOS 27、rustc 1.99.0。实验用 crates.io 上的 pulldown-cmark =0.13.5(默认 feature 加 simd);对照用 comrak 0.59 和 markdown 1.0(markdown-rs);对照本站时用 =0.13.4。本机是 aarch64,simd 在这里不起作用,只在 x86_64 上编译进去,相关的页在 Rosetta 下比较相对差。模板、语料和例子文档见怎么读这一卷。
各部分
- 00 导读:锁定版本与 0.13.5 摘了哪些 PR;三条主线每一步落在哪;例子文档;feature 表和
Options每一位对应的扩展与 spec 文件;源码地图。 - 01 全景:pull parser 和建 AST 的 comrak、markdown-rs 实测对比;一份文档三条主线完整走一遍;事件模型。
- 02 块解析:扁平数组上的树、逐行扫描、叶子块、容器块、表格与脚注。
- 03 行内解析:按需解析、强调与分隔符栈、链接与图片、代码段与 HTML 与实体、数学与智能标点。
- 04 字符串与内存:
CowStr,一次解析分配了什么。 - 05 输出:HTML 写出器、转义、改写事件流。
- 06 设计取舍:拉取式、两遍、扁平树、借用输入、扩展靠
Options位、不净化 HTML。 - 07 常见坑:二十一个坑,报错原文或症状做标题,每个都先跑出最小复现。
- 08 在本站中:本站
crates/widgets/src/mdx.rs怎么把事件流转成 RSX,0.13.4 和 0.13.5 的差别。 - 09 动手:不依赖 pulldown-cmark,写一个只认段落、标题、强调和代码段的迷你拉取式解析器。