Pandoc RST 读取器如何处理未知指令:以 Sphinx toctree 透传为范例(含源码解析与复现验证)
2026/9/20 22:55:04 网站建设 项目流程

Pandoc RST 读取器如何处理未知指令:以 Sphinx toctree 透传为范例(含源码解析与复现验证)

【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc

导读

本文围绕 Pandoc 仓库中的一条命令级测试用例 test/command/4715.md,深入讲解 Pandoc 的 reStructuredText(RST)读取器如何处理 Sphinx 特有的toctree指令:它会被当作未知指令解析为带属性的Div块,指令名成为 CSS 类、:name::class:字段成为标识与类名、其余字段原样透传为键值属性。读完本文,你将掌握pandoc -f rst -t native下未知指令的完整转换规则、源码实现位置与验证方法,并能在自己的 RST 文档中预判toctree等 Sphinx 指令的转换结果。

一、测试用例全景:从命令行到 Native 输出

test/command/4715.md是 Pandoc 的"命令测试"(command test)文件,其格式约定为:第一段代码块内的首行是完整的 pandoc 命令行,随后是标准输入(以^D结束),之后为该命令的期望输出。该用例完整内容如下:

% pandoc -f rst -t native .. toctree:: :name: tree1 :class: foo bar :caption: Indice dei contenuti :numbered: :maxdepth: 3 premessa.rst acquisizione-software.rst riuso-software.rst ^D [ Div ( "tree1" , [ "toctree" , "foo" , "bar" ] , [ ( "caption" , "Indice dei contenuti" ) , ( "numbered" , "" ) , ( "maxdepth" , "3" ) ] ) [ Para [ Str "premessa.rst" , SoftBreak , Str "acquisizione-software.rst" , SoftBreak , Str "riuso-software.rst" ] ] ]

1.1 复现步骤

在仓库根目录(或任意包含 pandoc 可执行文件的目录)执行:

pandoc -f rst -t native

然后粘贴上方的 RST 输入并按Ctrl+D^D)结束输入,即可得到与测试一致的 Native 输出。这里的两个关键参数含义为:

  • -f rst:指定输入格式为 reStructuredText,实际对应读取器模块 src/Text/Pandoc/Readers/RST.hs;
  • -t native:指定输出为 Pandoc 内部 AST 的文本表示(Pandoc类型的 show 形式),是排查解析行为最直观的手段。

1.2 逐段解读输出结构

输出是一个包含单个元素的列表,即文档 Body 的顶层块序列:

  1. Div:代表 Sphinx 的toctree指令被整体包裹为一个容器Div,三部分属性依次为:
    • 标识符"tree1":来自:name: tree1字段;
    • 类名列表["toctree", "foo", "bar"]:首元素"toctree"是指令名本身,foobar来自:class: foo bar字段(按空白切分);
    • 键值属性列表:("caption", "Indice dei contenuti")("numbered", "")("maxdepth", "3"),分别对应:caption::numbered::maxdepth:字段,其中无值的:numbered:被解析为空字符串值。
  2. Para:指令的正文(三个 RST 文件名)被当作普通段落解析,行与行之间以SoftBreak连接,文件名文本由Str承载。

可见,Pandoc 并不理解toctree的语义(它属于 Sphinx 文档构建体系的指令),而是采取"未知指令通用透传"策略:保留下全部属性信息,让下游过滤器或自定义模板自行消费

二、源码级原理:未知指令的通用透传路径

2.1 指令的识别与字段解析

RST 读取器中,所有指令统一由directive解析器入口处理(src/Text/Pandoc/Readers/RST.hs#L793-L797):

directive :: PandocMonad m => RSTParser m Blocks directive = try $ do string ".." directive'

directive'完成三件事(src/Text/Pandoc/Readers/RST.hs#L798-L824):

  1. 解析指令名(directiveLabel,允许字母与连字符后跟::);
  2. 读取指令首行剩余部分(top)与后续的字段列表(fields);
  3. 从字段中提取三类信息:
let name = trim $ fromMaybe "" (lookup "name" fields) classes = T.words $ maybe "" trim (lookup "class" fields) keyvals = [(k, trim v) | (k, v) <- fields, k /= "name", k /= "class"]

即::name:字段成为Div的标识符,:class:字段按空白切分后并入类名列表,其余所有字段(如:caption::numbered::maxdepth:)作为键值对keyvals保留。

2.2 已知指令的分发与未知指令的兜底

随后,directive'通过case label of对指令名做模式匹配分发(src/Text/Pandoc/Readers/RST.hs#L848-L957),已支持的指令包括:includetablelist-tablecsv-tableline-blockrawrolecontainerreplacedateunicodecompoundpull-quoteepigraphhighlightsrubric、各 admonition 类指令、sidebartopicdefault-rolehighlightcode/code-block/sourcecodeaafigmathfigureimagebibliographyclass等。

当指令名不匹配任何已知分支时,落入兜底分支other(src/Text/Pandoc/Readers/RST.hs#L953-L957):

other -> do pos <- getPosition logMessage $ SkippedContent (".. " <> other) pos bod <- parseFromString' parseBlocks $ top <> "\n\n" <> body' return $ B.divWith (name, other:classes, keyvals) bod

这一分支正是toctree测试用例的实现依据:

  • 指令名other(:)前置到类名列表首位,故输出中第一个类为"toctree"
  • :name:/:class:之外的字段keyvals被原样挂到Div属性上,:numbered:这类无值字段解析后值为空字符串""
  • 指令正文与首行top拼接后交给parseBlocks递归解析,因此三个文件名被解析成带SoftBreak的段落;
  • 同时通过logMessage $ SkippedContent ...记录一条"跳过内容"日志——在命令行加--verbose可看到类似[WARNING] SkippedContent .. toctree的提示,表明该指令语义未被 Pandoc 消费,仅做结构保留。

2.3 版本依据

该行为在 Pandoc 变更日志中有明确记录(changelog.md#L15659-L15660):

RST reader: Pass through fields in unknown directives as div attributes (#4715). Supportclassandnameattributes for all directives.

即:未知指令的字段透传为Div属性,并对所有指令支持classname属性,这正是 issue #4715(也是本测试文件名 4715 的由来)所要求的特性。测试文件 test/command/4715.md 本身即为该回归特性提供了命令级验证。

三、实战:把透传结果用起来

3.1 在 Native / JSON 中间产物中消费属性

toctree被解析为Div后,属性中的类名与键值对可供下游使用。例如转换为 JSON 格式查看:

pandoc -f rst -t json input.rst

输出中对应块为:

{"t":"Div","c":[["tree1",["toctree","foo","bar"],[["caption","Indice dei contenuti"],["numbered",""],["maxdepth","3"]]],[...]]}

3.2 借助 Lua 过滤器还原 Sphinx 目录语义

由于 Pandoc 只负责保真透传,真正的toctree语义(目录层级、编号、展开深度)应由过滤器实现。一个典型的 Lua 过滤器(toctree.lua)可写成:

function Div(el) local cls = el.classes or {} if cls[1] == "toctree" then local caption = el.attributes["caption"] local numbered = el.attributes["numbered"] ~= nil local maxdepth = tonumber(el.attributes["maxdepth"] or "1") -- 基于 el.content 中的段落与文件名,生成自定义目录结构 return pandoc.Div(el.content, pandoc.Attr("", {"toc"}, { caption = caption, numbered = tostring(numbered), maxdepth = tostring(maxdepth) })) end end

配合--lua-filter使用:

pandoc -f rst -t html --lua-filter=toctree.lua input.rst

3.3 修改:name::class:的实践要点

  • :name:只能出现一次,用于锚点定位,映射到Divid
  • :class:支持空格分隔的多个类名,会追加在指令名类之后(指令名类永远在首位);
  • 其余字段全部进入keyvals,因此自定义字段(如:titlesonly::glob:等 Sphinx 扩展选项)也会被无差别透传,供过滤器读取;
  • 需要特别注意的是:Pandoc 不校验字段名,任何拼写错误的字段都会被静默透传,建议在过滤器中做白名单校验。

四、扩展认知:未知指令与已知指令的边界

4.1 指令名的大小写与解析规则

directiveLabel在 src/Text/Pandoc/Readers/RST.hs#L789-L791 中通过T.toLower将指令名统一转为小写,因此.. TOCTREE::.. toctree::等价;指令名仅允许字母与连字符,后跟::

4.2 与已知指令的差异

toctree不同,像.. container::.. admonition::.. code-block::等已知指令会被 Pandoc 专门处理(例如container生成Div,但类名语义不同;admonition生成带title子块的Divcode-block直接生成CodeBlock)。因此只有对 Pandoc 未知的指令才会走other兜底分支并保留字段透传。这一点可以从同一源码文件的case label of分发逻辑中验证。

4.3 与测试套件的关系

该用例隶属于 Pandoc 的命令测试体系(test/Command.hs),运行整套测试时会自动执行test/command/*.md中的每个命令行示例并比对输出。若你对 Pandoc 的 RST 行为做了修改,可通过运行命令测试来验证该用例是否仍然通过,确保toctree等未知指令的透传行为不回归。

结语

通过 test/command/4715.md 这条精炼的测试用例,我们完整还原了 Pandoc RST 读取器对未知指令的通用处理模型:指令名进入类名首位,:name::class:字段特殊化处理,其余字段键值透传,正文按 RST 递归解析。这一设计让 Pandoc 无需理解 Sphinx 的toctree语义,即可无损地把结构信息交给下游过滤器与模板,是"解析与语义解耦"思想的典型体现。掌握该规律后,你在处理任何包含 Sphinx 扩展指令的 RST 文档时,都能准确预测并利用其转换结果。

【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询