Pandoc 的 Markdown 到 RST 转换:显式标题 ID 如何生成 reStructuredText 标签
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
导读
本文围绕 Pandoc 官方测试用例 test/command/3937.md 展开,深入剖析 Markdown 源文档中带显式标识符({#mysection})的标题在转换为 reStructuredText(RST)时的标签生成规则。读完本文,你将理解 Pandoc RST Writer 如何利用auto_identifiers扩展计算隐式 ID、何时输出显式.. _name:标签、RST 锚点语法与引用规则的细节,并能据此准确预测任意标题的 RST 输出结果。
测试用例全景:一次最小可复现的转换
test/command/3937.md 是一个典型的 Pandoc “command test”(命令行回归测试),它以 shell 脚本片段的形式给出输入、调用命令与期望输出:
% pandoc -t rst # My Great Section {#mysection} # Other section ^D% pandoc -t rst:表示执行pandoc命令并以-t rst指定输出格式为 reStructuredText;- 随后的两行是标准输入(stdin)内容,
^D表示输入结束(EOF); - 期望输出如下:
.. _mysection: My Great Section ================ Other section =============这个用例的测试点非常集中:只有带显式 ID 的第一个标题生成了.. _mysection:标签,第二个没有显式 ID 的标题则直接输出为标题文本与下划线装饰。也就是说,RST 输出中不会为Other section生成显式标签。
测试机制说明
这类用例由 test/Tests/Command.hs 驱动的 golden test(黄金测试)框架执行:每个test/command/*.md文件即一个用例,Pandoc 以文档首行声明的参数运行,并将实际输出与文件中的期望输出逐字节比对。一旦 Writer 行为发生变化导致输出不一致,测试即失败,从而保护该行为的稳定性。3937 号用例因此长期守护着 “标题显式 ID → RST 标签” 这一转换语义。
RST 标题输出:源码中的两条分支
在 Pandoc 的 RST Writer 中,标题(Header)块的转换逻辑位于 src/Text/Pandoc/Writers/RST.hs。核心代码如下:
blockToRST (Header level (name,classes,_) inlines) = do contents <- inlineListToRST inlines -- we calculate the id that would be used by auto_identifiers -- so we know whether to print an explicit identifier opts <- gets stOptions let autoId = uniqueIdent (writerExtensions opts) inlines mempty isTopLevel <- gets stTopLevel if isTopLevel then do let headerChar = if level > 5 then ' ' else "=-~^'" !! (level - 1) let border = literal $ T.replicate (offset contents) $ T.singleton headerChar let anchor | T.null name || name == autoId = empty | otherwise = ".. _" <> (if T.any (==':') name || T.take 1 name == "_" then "`" <> literal name <> "`" else literal name) <> ":" $$ blankline return $ nowrap $ anchor $$ contents $$ border $$ blankline关键点可拆解如下:
- 显式 ID 优先:标题的 attrs 三元组
(name, classes, keyvals)中,name即 Markdown 语法{#id}指定的显式标识符。 - 隐式 ID 回退:
uniqueIdent (writerExtensions opts) inlines mempty按当前 writer 扩展计算 “auto_identifiers 本应生成的 ID”。如果标题未显式指定 ID,则name为空;如果显式指定的 ID 恰好与自动生成的 ID 相同,name == autoId也成立——这两种情况下都不输出显式标签,避免冗余。 - 非顶层标题走 rubric:当
stTopLevel为 False(即标题出现在列表等嵌套上下文中)时,标题不再渲染为 RST 章节标题,而是输出为.. rubric::指令(含可选的:name:、:class:选项)。
3937 用例的完整对照
| 输入标题 | 显式 name | 自动 ID(autoId) | 是否输出标签 | 输出 |
|---|---|---|---|---|
# My Great Section {#mysection} | mysection | my-great-section | 是(mysection≠my-great-section) | .. _mysection: |
# Other section | 空 | other-section | 否(name为空) | 仅标题 + 下划线 |
这正是测试期望输出中第一条标题多出一行.. _mysection:的原因。
RST 锚点语法:为什么要反引号包裹
RST 的超链接目标(hyperlink target)语法为.. _name: target。其中name作为标签存在语法限制:若标签中包含冒号(:),或标签以_开头,则必须用反引号包裹,写成.. _`name`:的形式,否则 RST 解析器无法正确识别。
这一点在源码中有对应实现:
(if T.any (==':') name || T.take 1 name == "_" then "`" <> literal name <> "`" else literal name)即:显式 ID 中含有:或首字符为_时,生成的标签会被反引号包裹。例如# Foo {#:bar}会输出.. _`:bar`:,而# Foo {#bar}输出.. _bar:。
RST Reader 侧的解析印证
反向读取时,RST Reader 在 src/Text/Pandoc/Readers/RST.hs 的explicitLink中处理`label<src>`_形式的显式链接,并通过key = toKey $ stringifyInlines label'将标签文本规范化后存入状态,供后续引用匹配。Writer 输出的.. _name:正是与之配套的标准 RST 标签语法,保证了 “Pandoc 写出的 RST 再读回 Pandoc” 时 ID 语义不丢失。
auto_identifiers 扩展:隐式 ID 的计算依据
当标题没有显式 ID 时,RST Writer 通过uniqueIdent结合writerExtensions opts计算自动 ID。这里writerExtensions决定了auto_identifiers扩展是否启用,而uniqueIdent负责把标题内联内容转换为符合规范、且不与其他 ID 冲突的唯一标识符。正是这一机制让 RST Writer 能判断 “显式 ID 与自动 ID 是否重复”,从而决定是否输出冗余标签。
需要留意的是:Markdown Reader 解析{#id}属于 Markdown 的header_attributes扩展(默认启用,测试用例 test/Tests/Readers/Markdown.hs 中亦有对{#i .j .z k=v}属性的解析测试),而是否输出标签则取决于 RST Writer 侧的上述逻辑——即 “读取端解析 ID、写入端按需生成标签” 的职责划分。
装饰线与多级标题的渲染规则
RST 使用标题文本下方的装饰线(underline,必要时叠加 overline)表示章节层级。Writer 中通过如下表达式选择装饰字符:
let headerChar = if level > 5 then ' ' else "=-~^'" !! (level - 1)- 1 级标题使用
=,2 级使用-,3 级使用~,4 级使用^,5 级使用'(与 RST 文档约定的默认层级顺序一致); - 超过 5 级的标题使用空格,即不绘制可见装饰线;
- 装饰线的长度严格等于标题内容渲染后的宽度(
T.replicate (offset contents)),保证装饰线覆盖标题文本。
3937 用例中的两个 1 级标题因此都使用====与=====长度的=线。
实战验证:如何复现与扩展该行为
你可以在本地用真实命令复现 3937 用例:
printf '# My Great Section {#mysection}\n# Other section\n' | pandoc -t rst输出应与测试期望完全一致。进一步验证源码中的分支逻辑,可尝试以下输入:
# 显式 ID 与自动 ID 相同:不会输出冗余标签 printf '# My Great Section\n' | pandoc -t rst printf '# My Great Section {#my-great-section}\n' | pandoc -t rst # 含冒号或下划线开头的 ID:标签被反引号包裹 printf '# Foo {#:bar}\n' | pandoc -t rst printf '# Foo {#_bar}\n' | pandoc -t rst其中前两条命令的输出应当完全相同(显式 ID 等于自动 ID 时标签被省略),后两条则验证反引号包裹规则。这也是调试 RST Writer 行为时最直接的验证手段。
小结
- 带显式
{#id}的标题在 RST 输出中会生成.. _id:显式标签;不带 ID 的标题仅依赖 RST 自身的隐式标题引用,不会输出标签; - 若显式 ID 与
auto_identifiers扩展计算出的隐式 ID 一致,标签同样被省略; - 标签中含冒号或以下划线开头时,必须用反引号包裹以符合 RST 语法;
- 标题层级由
= - ~ ^ '五种装饰字符表达,装饰线长度与标题文本严格等宽。
3937 号测试用例以最小的输入体量,锁定了上述全部语义,是理解 Pandoc Markdown→RST 标题转换机制的最佳切入点。相关实现可继续阅读 src/Text/Pandoc/Writers/RST.hs,测试框架见 test/Tests/Command.hs。
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考