Rust in Everything
基于 pulldown-cmark 0.13.5

脚注的第二段成了代码块

主线位置:块主线的第一遍。脚注定义是一种容器,后续行算不算它的内容由 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 列。
📝NOTE
冷知识:0.10.0 让 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 换了含义,老程序不改代码也会变;选定一种风格,换之前对全部文档比对两种输出。

💬 关联讨论

发起讨论
加载中...