Rust in Everything
基于 pulldown-cmark 0.13.5

Parser::new 不认表格和脚注

主线位置:块主线的起点。Parser::new_ext(text, opts) 把 opts 交给第一遍,第一遍按位决定要不要试表格、脚注、任务框,要不要把 ~、| 当成特殊字节。每一位对应什么扩展,总表在怎么读这一卷。

flowchart TB
  new["Parser::new"] --> empty["Options::empty"]
  empty --> fp["第一遍"]
  fp --> tbl["表格行进段落"]
  fp --> refdef["脚注定义成链接定义"]
  fp --> lit["~~ 和 [x] 是文字"]
  refdef --> a["[^1] 成了链接"]

实验在 exp-pulldown-cmark/pita-lab,bin p01_options(release),输出存在 out-p01.txt。M2 Max、macOS 27、rustc 1.99.0,pulldown-cmark 0.13.5。

现象

一段 GitHub 上看起来正常的文档:

| 名称 | 版本 |
| --- | --- |
| pulldown-cmark | 0.13.5 |

~~旧接口~~已删除[^1]。

- [x] 解析
- [ ] 渲染

[^1]: 见变更记录。

用 Parser::new 解析,交给 html::push_html:

<p>| 名称 | 版本 |
| --- | --- |
| pulldown-cmark | 0.13.5 |</p>
<p>~~旧接口~~已删除<a href="%E8%A7%81%E5%8F%98%E6%9B%B4%E8%AE%B0%E5%BD%95%E3%80%82">^1</a>。</p>
<ul>
<li>[x] 解析</li>
<li>[ ] 渲染</li>
</ul>
  • 表格三行成了一个段落,竖线原样输出。
  • ~~旧接口~~ 和两个任务框都是普通文字。
  • 脚注最奇怪:[^1] 变成了一个链接,地址是百分号编码后的 见变更记录。,文末的脚注定义不见了。

没有报错,也没有警告,HTML 照样生成。

原因

Parser::new 只是一层转发:

    pub fn new(text: &'input str) -> Self {
        Self::new_ext(text, Options::empty())
    }

Options::empty() 下只有 CommonMark。扩展在第一遍里一位一位地查:

  • 表格:parse_paragraph 只在 ENABLE_TABLES 打开时用 TableParseMode::Scan 扫第一行,否则 | 连特殊字节都不是(special_bytes 里 |、~、$ 都按位加入)。
  • 任务框:parse_block 开了列表项之后,ENABLE_TASKLISTS 打开才调 scan_task_list_marker。
  • 脚注:同一个函数里,ENABLE_FOOTNOTES 打开才试 parse_footnote。没打开时,[^1]: 见变更记录。 落到链接引用定义的分支:^1 是合法的链接标签,见变更记录。 是合法的地址。于是它进了 refdefs,正文的 [^1] 是一个快捷引用链接。定义怎么收、标签怎么比较,见表格与脚注。

这是有意的设计:扩展全是 Options 的位,默认什么都不开(设计取舍);Options::empty() 下 CommonMark 的 652 条例子全过(为什么是 pull parser)。

修法

用 new_ext,把要的扩展一个个写出来:

let opts = Options::ENABLE_TABLES
  | Options::ENABLE_FOOTNOTES
  | Options::ENABLE_STRIKETHROUGH
  | Options::ENABLE_TASKLISTS;
html::push_html(&mut out, Parser::new_ext(doc, opts));
<table><thead><tr><th>名称</th><th>版本</th></tr></thead><tbody>
<tr><td>pulldown-cmark</td><td>0.13.5</td></tr>
</tbody></table>
<p><del>旧接口</del>已删除<sup class="footnote-reference"><a href="#1">1</a></sup>。</p>
<ul>
<li><input disabled="" type="checkbox" checked=""/>
解析</li>
<li><input disabled="" type="checkbox"/>
渲染</li>
</ul>
<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>
<p>见变更记录。</p>
</div>

不要图省事写 Options::all()。它把所有位都打开,包括几个会改变普通文字的:

<p>正文<sup class="footnote-reference"><a href="#9">1</a></sup>。</p>
<p>原价<span class="math math-inline">5,现价</span>3</p>
<p>“quoted” – ok…</p>

三行输入是 正文[^9]。、原价$5,现价$3、"quoted" -- ok...。ENABLE_OLD_FOOTNOTES 也在里面,它同时置位 ENABLE_FOOTNOTES,于是脚注是旧风格,没有定义的 [^9] 也成了引用(见脚注的两种风格);ENABLE_MATH 把两个价格之间的文字变成公式(见价格变成了公式);ENABLE_SMART_PUNCTUATION 换掉了引号、连字符和省略号。以后的版本加了新位,all() 还会悄悄多开几样。

怎么检查

  • 在代码里搜 Parser::new(。渲染用户写的 Markdown 时,它几乎总是错的;只想要纯 CommonMark 的地方,写成 Parser::new_ext(text, Options::empty()),读代码的人就知道这是有意的。
  • 给渲染函数写一条测试:输入里每种要用的扩展各放一个,断言输出里有 <table>、<del>、type="checkbox"、footnote-reference。上面那份文档就能当测试输入。
  • 选项集中定义成一个常量,所有调用点共用,避免一处开了、另一处没开。

小结

  • Parser::new 等于 new_ext(text, Options::empty()),只认 CommonMark,表格、删除线、任务框、脚注都按普通文字处理。
  • 没开 ENABLE_FOOTNOTES 时,[^1]: … 是一条链接引用定义,[^1] 成了链接,表面上看不出错。
  • 修法是 new_ext 加上明确列出的位;Options::all() 会带进旧式脚注、数学和智能标点。

💬 关联讨论

发起讨论
加载中...