Rust in Everything
基于 pulldown-cmark 0.13.5

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 时相同
tagv0.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 下比较相对差。模板、语料和例子文档见怎么读这一卷。

各部分

  1. 00 导读:锁定版本与 0.13.5 摘了哪些 PR;三条主线每一步落在哪;例子文档;feature 表和 Options 每一位对应的扩展与 spec 文件;源码地图。
  2. 01 全景:pull parser 和建 AST 的 comrak、markdown-rs 实测对比;一份文档三条主线完整走一遍;事件模型。
  3. 02 块解析:扁平数组上的树、逐行扫描、叶子块、容器块、表格与脚注。
  4. 03 行内解析:按需解析、强调与分隔符栈、链接与图片、代码段与 HTML 与实体、数学与智能标点。
  5. 04 字符串与内存:CowStr,一次解析分配了什么。
  6. 05 输出:HTML 写出器、转义、改写事件流。
  7. 06 设计取舍:拉取式、两遍、扁平树、借用输入、扩展靠 Options 位、不净化 HTML。
  8. 07 常见坑:二十一个坑,报错原文或症状做标题,每个都先跑出最小复现。
  9. 08 在本站中:本站 crates/widgets/src/mdx.rs 怎么把事件流转成 RSX,0.13.4 和 0.13.5 的差别。
  10. 09 动手:不依赖 pulldown-cmark,写一个只认段落、标题、强调和代码段的迷你拉取式解析器。

适合谁读

  • 用 pulldown-cmark 渲染 Markdown、想知道事件为什么这样切的读者:先读 01 全景,再读 事件模型 和 05 输出。
  • 想写自己的 Markdown 处理逻辑(目录、锚点、改链接)的读者:一份文档 之后直接读 改写事件流。
  • 想读懂 CommonMark 解析算法的读者:按顺序读 02 和 03,对照 CommonMark spec 附录的「A parsing strategy」。
  • 需要的前置知识:Markdown 的基本语法;Rust 的迭代器、生命周期和 Cow。读过 serde 卷讲借用的那一页 会帮助理解 CowStr。

💬 关联讨论

发起讨论
加载中...