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 的顶层块序列:
Div块:代表 Sphinx 的toctree指令被整体包裹为一个容器Div,三部分属性依次为:- 标识符
"tree1":来自:name: tree1字段; - 类名列表
["toctree", "foo", "bar"]:首元素"toctree"是指令名本身,foo与bar来自:class: foo bar字段(按空白切分); - 键值属性列表:
("caption", "Indice dei contenuti")、("numbered", "")、("maxdepth", "3"),分别对应:caption:、:numbered:、:maxdepth:字段,其中无值的:numbered:被解析为空字符串值。
- 标识符
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):
- 解析指令名(
directiveLabel,允许字母与连字符后跟::); - 读取指令首行剩余部分(
top)与后续的字段列表(fields); - 从字段中提取三类信息:
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),已支持的指令包括:include、table、list-table、csv-table、line-block、raw、role、container、replace、date、unicode、compound、pull-quote、epigraph、highlights、rubric、各 admonition 类指令、sidebar、topic、default-role、highlight、code/code-block/sourcecode、aafig、math、figure、image、bibliography、class等。
当指令名不匹配任何已知分支时,落入兜底分支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). Support
classandnameattributes for all directives.
即:未知指令的字段透传为Div属性,并对所有指令支持class与name属性,这正是 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.rst3.3 修改:name:与:class:的实践要点
:name:只能出现一次,用于锚点定位,映射到Div的id;: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子块的Div;code-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),仅供参考