Markdown 渲染(Markdown to HTML)
手写的 Rust 解析器编译成 WebAssembly,边输入边把 Markdown 转成 HTML,代码块带语法高亮
在左侧输入,右侧实时预览 | 原始 HTML 一律被转义,链接只允许 http、https、mailto 和相对地址,所以输出可以直接放进页面
📖 原理说明
Markdown 用 # 表示标题、* 表示强调、- 表示列表,写起来像纯文本,又能转换成排版好的 HTML。这个页面的转换器完全手写:先按行识别出标题、列表、引用、代码块、表格这些「块」,再在每个块里解析强调、链接、行内代码这些「行内」标记。渲染结果直接放进页面,所以另一个重点是安全:用户输入里的任何 HTML 都不会被执行。
🧮算法原理
文本 → 块结构 → 行内标记 → HTML两遍解析:块级决定「这一段是什么」,行内级决定「这几个字怎么显示」
- 块级解析
- 逐行扫描:以 # 开头的是标题,``` 开头的是代码块(一直读到结束标记),> 开头的是引用(去掉 > 后递归解析里面的内容),- 或 1. 开头的是列表项(缩进的后续行属于这一项,同样递归解析),带 | 且下一行是 |---| 分隔行的是表格,其余连续的行合成段落。列表项之间有空行时是「松散」列表,每项包在 <p> 里。
- 强调的配对
- 星号和下划线的规则比看起来复杂:
2 * 3 * 4 里的星号不是强调,snake_case_name 里的下划线也不是。解析器按 CommonMark 的规则判断每一串 * 或 _ 能否开始、能否结束强调(看它前后是空白、标点还是字母),再用一个栈从左到右配对,两个配成 <strong>,一个配成 <em>,所以 ***both*** 会得到嵌套正确的 <em><strong>。
- 防 XSS
- 输出用 innerHTML 插入页面,所以绝不能让输入变成可执行的代码。三条措施:所有文字中的 & < > " ' 一律转义,原始 HTML 只会原样显示;属性值统一用双引号包住并转义,标题、图片说明里的引号无法「跳出」属性;链接和图片地址只允许 http、https、mailto 和相对地址,判断前先去掉空白和控制字符,所以
JaVaScRiPt:、java<Tab>script: 都会被替换成 #。点「XSS 测试」可以看到常见攻击写法的输出。
- 代码高亮
- 带语言标注的代码块(rust、js、python、bash)会被切分成关键字、字符串、注释、数字和类型名,分别套上不同颜色的 <span>。这是按字符扫描的简单分词,不做完整的语法分析。
- 支持的范围
- 实现的是 CommonMark 的常用子集加上 GFM 的表格、删除线和任务列表。不支持:原始 HTML 透传(出于安全考虑)、「下划线式」标题(=== / ---,--- 会被当作分隔线)、缩进式代码块、脚注和引用式链接。
🔄Rust 与 JavaScript 的分工
- JS每次输入(每帧最多一次)JS 调用
md_render(文本)。
- RustRust 解析出块结构树,对每个块做行内解析,拼接成 HTML 字符串返回。异常输入(上千个未闭合的 [、几百层嵌套的引用)也有线性时间和递归深度的保护。
- JSJS 把结果放进预览区;也可以切换成查看 HTML 源码。
⚡性能要点
- 实测(桌面 Chromium):示例文档(约 1 KB)每次渲染 0.1~2ms,完全跟得上打字;「测速」把示例重复成约 1 MB,最快约 30ms,约 32 MB/s。
- 这个 .wasm 约 72 KB,是本项目较大的一个:大量的字符串处理和格式化代码占了体积。
- 测试包括每种语法的逐字输出对比、一组 XSS 攻击用例、3000 份由特殊字符和中文随机拼成的文档(检查不会崩溃、不会产生渲染器以外的标签),以及几种会让简单实现变成平方级耗时的病态输入:成千上万个未闭合的 [、不闭合的链接地址(「[a](xxx」重复上万次)、长度依次递增的反引号串。链接地址最长扫描 2048 个字符,反引号串的位置预先建好索引,所以耗时始终和输入长度成正比。
源码crates/tools/markdown/src/block.rscrates/tools/markdown/src/inline.rscrates/tools/markdown/src/escape.rswww/markdown/index.js