脚注的第二段成了代码块
主线位置:块主线的第一遍。脚注定义是一种容器,后续行算不算它的内容由
scan_containers和parse_block决定,两种风格在这里分开走;第二遍遇到[^x]时,没有定义算不算引用也看风格。两种风格的规则对照见表格与脚注。
flowchart TB bit["脚注位"] --> gfm["ENABLE_FOOTNOTES"] bit --> old["ENABLE_OLD_FOOTNOTES"] gfm --> g1["续行要缩进 4 列"] gfm --> g2["无定义是文字"] old --> o1["空行就关"] old --> o2["无定义也是引用"] o1 --> code["缩进段落成代码块"]
实验在 exp-pulldown-cmark/pita-lab,bin p06_footnotes(release),输出存在 out-p06.txt(比本页多两组:列表缩进进脚注、脚注和列表之间空一行)。M2 Max、macOS 27、rustc 1.99.0,pulldown-cmark 0.13.5。
现象
同一份内容,换一个脚注位,push_html 的输出就变了。四段输入,每段先是 ENABLE_FOOTNOTES,再是 ENABLE_OLD_FOOTNOTES。
两段的脚注,第二段按 GitHub 的写法缩进四格:
正文[^1]。
[^1]: 第一段
第二段
== FOOTNOTES
<p>正文<sup class="footnote-reference"><a href="#1">1</a></sup>。</p>
<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>
<p>第一段</p>
<p>第二段</p>
</div>
== OLD_FOOTNOTES
<p>正文<sup class="footnote-reference"><a href="#1">1</a></sup>。</p>
<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>
<p>第一段</p>
</div>
<pre><code>第二段
</code></pre>
脚注后面紧跟一个列表,再空一行写正文:
正文[^1]。
[^1]: 注释
- 列表
后文
== FOOTNOTES
<p>正文<sup class="footnote-reference"><a href="#1">1</a></sup>。</p>
<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>
<p>注释</p>
</div>
<ul>
<li>列表</li>
</ul>
<p>后文</p>
== OLD_FOOTNOTES
<p>正文<sup class="footnote-reference"><a href="#1">1</a></sup>。</p>
<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>
<p>注释</p>
<ul>
<li>列表</li>
</ul>
<p>后文</p>
</div>
没有定义的引用 正文[^9]。:
== FOOTNOTES
<p>正文[^9]。</p>
== OLD_FOOTNOTES
<p>正文<sup class="footnote-reference"><a href="#9">1</a></sup>。</p>
标签单独一行,内容在下一行缩进:
正文[^n]。
[^n]:
内容
== FOOTNOTES
...
<div class="footnote-definition" id="n"><sup class="footnote-definition-label">1</sup>
<p>内容</p>
</div>
== OLD_FOOTNOTES
...
<div class="footnote-definition" id="n"><sup class="footnote-definition-label">1</sup>
<pre><code>内容
</code></pre>
</div>
四种情况全部不同:旧风格把缩进的第二段放到脚注外面当代码块,把列表之后的正文一直吞进脚注,给没有定义的引用编了号(链接指向一个不存在的 id),把下一行的内容当成缩进代码块;新风格则相反。
原因
两种风格共用 ENABLE_FOOTNOTES 这一位(第 2 位)。ENABLE_OLD_FOOTNOTES 是 (1 << 9) | (1 << 2),代码用 has_gfm_footnotes(第 2 位开、第 9 位没开)区分。上面四种差异分别落在:
- 续行。 新风格下,
scan_containers要求脚注的后续行缩进 4 列或者是空行(parse.rs第 1532 行),缩进四格的第二段留在脚注里,缩进被吃掉,成了段落。旧风格不检查缩进,parse_block开头只看「上一行是空行、spine 顶上是脚注」就关掉脚注(firstpass.rs第 97 行);第二段回到文档顶层,四格缩进就是缩进代码块。 - 脚注里的列表。 同一个检查只看 spine 顶上那一个节点。空行之后顶上是列表,不是脚注,检查不成立;随后列表被关掉,脚注却没人关,后面的正文都进了脚注。这是旧风格的一个边角,tag 之后 main 上这段代码没有变。
- 没有定义的引用。 第二遍解析
[^…]时的条件是!self.options.has_gfm_footnotes() || self.allocs.footdefs.contains(…)(parse.rs第 789 行):旧风格不查定义。 - 标签单独一行。 旧风格在
[^n]:之后跳过整个空行,把下一行当作内容的开头(parse_block第 121 行起,ENABLE_OLD_FOOTNOTES时多调一次scan_blank_line),四格缩进于是是代码块;新风格下下一行是脚注的续行,先去掉 4 列。
ENABLE_FOOTNOTES 换了含义0.10.0 之前只有一种脚注,就是今天的旧风格,ENABLE_FOOTNOTES 打开的就是它。PR #654 加 GitHub 风格时,第一个提交 3d184fb「Add a separate mode to parse footnotes the same way GitHub does」新增的是一个可选的位:ENABLE_GFM_FOOTNOTES = 1 << 9,文档写着「GitHub-compatible footnote syntax. Mutually-exclusive with ENABLE_FOOTNOTES.」,老用户什么都不改,行为不变。五天后同一个 PR 的 e8e998f「Address nitpicks for GFM-style footnotes」把它倒了过来:ENABLE_FOOTNOTES 的文档改成「GitHub-compatible footnote syntax.」,第 9 位改名为 ENABLE_OLD_FOOTNOTES = (1 << 9) | (1 << 2),「Older footnote syntax. Implies ENABLE_FOOTNOTES.」。PR 首次发布于 0.10.0。从 0.9 升上来、只开了 ENABLE_FOOTNOTES 的程序,代码一行没改,脚注就换成了新风格,上面四种差异就是升级后会看到的变化。
修法
按内容的写法选定一种,写在一个地方:
- 内容是照 GitHub 写的(续行缩进四格,引用都有定义),用
ENABLE_FOOTNOTES。 - 内容是 0.10 之前用 pulldown-cmark 渲染、照旧行为写的,用
ENABLE_OLD_FOOTNOTES,或者把内容改成新写法:脚注的续段缩进四格,删掉没有定义的引用。旧风格的「列表之后吞掉正文」要在内容里避开:在脚注定义和列表之间空一行,列表就在脚注外面,两种风格输出相同。把列表缩进四格放进脚注只对新风格有效,旧风格不吃掉这四格缩进,缩进四格的行打断不了段落,- 列表成了注释那一段的续行,输出<p>注释\n- 列表</p>。
不要用 Options::all() 来「顺便」打开脚注,它包含 ENABLE_OLD_FOOTNOTES,得到的是旧风格(Parser::new 不认表格和脚注)。
选好之后,渲染还要另外处理:push_html 把定义写在原地、编号按首次出现分配,见 HTML 写出器;本站的渲染对脚注事件只做了最简单的处理,见本站的 Markdown 渲染。
怎么检查
换风格(包括升级跨过 0.10)之前,用两种位把全部文档各渲染一遍,列出 HTML 不同的文件,逐个看:
let a = to_html(&src, Options::ENABLE_FOOTNOTES);
let b = to_html(&src, Options::ENABLE_OLD_FOOTNOTES);
if a != b {
println!("differs: {}", path.display());
}
上面四段输入在这个比对下全部报出。只比对含 [^ 的文件就够了:不含它的文档两种风格输出相同。另外,测试里放一个没有定义的 [^x],断言它的输出符合你选的风格(新风格是文字,旧风格是一个指向不存在 id 的链接)。
小结
ENABLE_FOOTNOTES是 GitHub 风格,ENABLE_OLD_FOOTNOTES是 0.10 之前的旧风格,后者同时置位前者。- 缩进的续段、单独一行的标签、没有定义的引用、脚注里的列表,两种风格结果都不同;旧风格的列表会把后文吞进脚注。
- 0.10.0 起
ENABLE_FOOTNOTES换了含义,老程序不改代码也会变;选定一种风格,换之前对全部文档比对两种输出。