Pandoc DocBook 读取器如何解析有序列表的编号样式与内嵌标题:以 test/command/10594.md 命令测试为例
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
test/command/10594.md是 pandoc 项目中的一个命令行级(command-level)回归测试:它以一段包含内嵌<title>的 DocBook<orderedlist>为输入,通过pandoc -f docbook -t native输出内部 AST,验证 DocBook 读取器对有序列表numeration编号样式的映射、列表内<title>元素的 Div 化处理,以及<listitem>/<simpara>的块级转换行为。读完本文,你将能看懂这类test/command/*.md测试文件的格式约定,掌握 DocBook 列表在 pandoc 中的 AST 表示,并能对照 DocBook 读取器源码 复现与扩展验证。
一、测试文件全景:一个最小可复现的命令测试
test/command/10594.md全文是一个用反引号包裹的代码块,其内容遵循 pandoc 命令测试(command tests)的标准格式,包含三部分:
% pandoc -f docbook -t native <orderedlist numeration="loweralpha"> <title>header inside listing</title> // not rendered in any output format! <listitem> <simpara>first step</simpara> </listitem> </orderedlist> ^D [ Div ( "" , [] , [] ) ... ]- 第一行
%:声明要执行的 pandoc 命令行,这里是pandoc -f docbook -t native,即把输入当作 DocBook 解析,并以 native 格式(pandoc 内部 AST 的文本表示)输出; ^D之前:喂给命令的标准输入(heredoc 形式,^D模拟 EOF);^D之后:期望的标准输出,即 golden 结果,测试框架会把实际输出与它逐字节比对。
这类测试由 test/Tests/Command.hs 驱动:测试发现器会扫描test/command/目录下所有以.md结尾的文件(test/Tests/Command.hs),逐个解析出命令、输入与期望输出,并通过goldenTest做 golden 比对。值得注意的实现细节是,实际执行时命令中的pandoc会被替换为test-pandoc --emulate(test/Tests/Command.hs),从而使用与发布版 pandoc 行为一致的测试专用二进制。从命名规律可以推断,这类编号文件通常对应 pandoc 历史上的 issue/PR 编号,10594即为此用例的回归编号。
二、输入 DocBook 片段逐段拆解
测试输入是一段结构清晰的 DocBook 5 文档片段:
<orderedlist numeration="loweralpha"> <title>header inside listing</title> // not rendered in any output format! <listitem> <simpara>first step</simpara> </listitem> </orderedlist>各元素含义如下:
| 元素/属性 | 含义 |
|---|---|
<orderedlist> | DocBook 有序列表,numeration="loweralpha"指定编号样式为小写字母 |
<title> | 列表的可选标题;测试中用//注释注明它在各输出格式中通常不会被渲染 |
<listitem> | 列表项容器 |
<simpara> | "simple paragraph",只含文本与内联标记、不含块级元素的段落 |
注意<title>是直接嵌在<orderedlist>内部、而非位于listinfo中——这正是本用例的焦点:读取器需要把列表标题作为一种"可选的块级前置内容"捕获下来。
三、native 输出解读:Div 嵌套与 OrderedList 属性
期望输出揭示了 DocBook 读取器生成的内部 AST:
[ Div ( "" , [] , [] ) [ Div ( "" , [ "title" ] , [] ) [ Plain [ Str "header", Space, Str "inside", Space, Str "listing" ] ] , OrderedList ( 1 , LowerAlpha , DefaultDelim ) [ [ Para [ Str "first", Space, Str "step" ] ] ] ] ]对照源码可以逐层还原:
- 外层
Div ("" , [] , []):整个<orderedlist>被包装为 Div,id、class、键值属性均为空(本例未设置id,也没有role等属性); - 内层
Div ("" , ["title"] , []):<title>的文本 "header inside listing" 被解析为行内内容并以Plain块呈现,随后被打包成带titleclass 的 Div; OrderedList (1 , LowerAlpha , DefaultDelim):元组三个分量分别是起始编号、编号样式、分隔符类型——起始号为 1,样式为LowerAlpha(小写字母),分隔符为DefaultDelim(DocBook 本身不编码分隔符信息,因此固定取默认值);[ [ Para [ Str "first", Space, Str "step" ] ] ]:唯一的<listitem>解析为一个列表项,其内部的<simpara>被转换为Para块。
测试注释说该<title>"not rendered in any output format"(在输出格式中通常不渲染),但 native 输出恰恰证明了它在 AST 层面是被保留的——这是"保留信息、渲染交给 writer"的典型设计。
四、源码实现(一):orderedlist 分支与 numeration 映射
<orderedlist>的解析逻辑位于 src/Text/Pandoc/Readers/DocBook.hs:
"orderedlist" -> withOptionalTitle $ do let listStyle = case attrValue "numeration" e of "arabic" -> Decimal "loweralpha" -> LowerAlpha "upperalpha" -> UpperAlpha "lowerroman" -> LowerRoman "upperroman" -> UpperRoman _ -> Decimal let start = fromMaybe 1 $ safeRead $ attrValue "startingnumber" e orderedListWith (start,listStyle,DefaultDelim) . handleCompact <$> listitems这段代码揭示了完整的编号样式映射关系:
DocBooknumeration属性值 | pandoc 列表样式 |
|---|---|
arabic(及未识别值,默认) | Decimal(十进制数字) |
loweralpha | LowerAlpha(小写字母 a, b, c…) |
upperalpha | UpperAlpha(大写字母 A, B, C…) |
lowerroman | LowerRoman(小写罗马数字 i, ii, iii…) |
upperroman | UpperRoman(大写罗马数字 I, II, III…) |
起始编号则读取startingnumber属性,缺省时回退为 1;分隔符固定为DefaultDelim。测试用例中的numeration="loweralpha"因此精确命中LowerAlpha分支,验证了这条映射链。
与之相邻的列表类元素解析同样值得对照:itemizedlist走bulletList,variablelist走definitionList,procedure与substeps直接使用默认样式的orderedList(src/Text/Pandoc/Readers/DocBook.hs)。这些标签以及title、listitem、simpara等都会先经过读取器的元素白名单检查(参见 src/Text/Pandoc/Readers/DocBook.hs),未列入白名单的标签将被跳过。
五、源码实现(二):withOptionalTitle 与 title 的 Div 化
列表项内容的收集很直观:listitems = mapM getBlocks $ filterChildren (named "listitem") e(src/Text/Pandoc/Readers/DocBook.hs),即把每个<listitem>子元素递归解析成块列表;而<simpara>通过parseMixed para被解析为Para块(src/Text/Pandoc/Readers/DocBook.hs)。
真正有意思的是<title>的处理。在getBlocks的分支表中,顶层出现<title>时直接返回mempty,注释写明"handled in parent element"(由父元素处理,src/Text/Pandoc/Readers/DocBook.hs)。也就是说,<title>是否被消费,完全取决于父元素是否调用withOptionalTitle。其实现如下(src/Text/Pandoc/Readers/DocBook.hs):
withOptionalTitle p = do mbt <- getTitle b <- p case mbt of Nothing -> return b Just t -> return $ divWith (attrValue "id" e, [], getRoleAttr e) (divWith ("", ["title"], []) (plain t) <> b)getTitle用filterChild (named "title") e查找直接子级<title>,取其行内内容;- 若存在,则把标题包装为
divWith ("", ["title"], []) (plain t),再与列表主体b拼接,外包一层带元素id与 role 属性的 Div; - 若不存在,则原样返回列表内容,不产生额外 Div。
这正是 native 输出中两层 Div 的由来:外层 Div 的 id 取自<orderedlist>的id属性(本测试未设置,故为空),内层titleDiv 则是标题的固定容器。同一个withOptionalTitle也被calloutlist、itemizedlist等复用,而表格与图表的标题走的是另一条title/caption处理路径。
六、补充机制:compact 紧凑列表
orderedListWith ... . handleCompact中的handleCompact由spacing属性控制(src/Text/Pandoc/Readers/DocBook.hs):
compactSpacing = case attrValue "spacing" e of "compact" -> True _ -> False handleCompact = if compactSpacing then map (fmap paraToPlain) else id当列表声明spacing="compact"时,每个列表项内的Para会被降级为Plain(紧凑呈现);否则保持Para不变。10594 用例未设置spacing,因此<simpara>生成的Para原样保留——这也解释了为什么期望输出中列表项内容是Para而非Plain。
七、如何本地复现与验证
在已构建 pandoc 的环境中,可以直接用 heredoc 复现该测试(结果应与^D后的 golden 输出完全一致):
pandoc -f docbook -t native <<'EOF' <orderedlist numeration="loweralpha"> <title>header inside listing</title> <listitem> <simpara>first step</simpara> </listitem> </orderedlist> EOF也可以运行整个命令测试套件来验证该用例:
cabal test --test-options='-p "#10594"'-p的匹配串来自 test/Tests/Command.hs 中的testname = "#" <> show num,即每个.md文件名去掉扩展名后即为测试名。若实际输出与 golden 不一致,测试框架会给出--- test/command/10594.md与+++ pandoc -f docbook -t native形式的 diff,方便定位读取器行为变化(test/Tests/Command.hs)。
八、小结:从一条测试看 pandoc 的回归测试方法论
test/command/10594.md虽只有二十余行,却浓缩了 pandoc 三个层面的工程实践:
- 读取器语义:
numeration→ListNumberStyle的六路映射、startingnumber→ 起始编号、spacing="compact"→Para/Plain切换,以及<title>由父元素按需消费的"handled in parent element"设计; - AST 约定:可选列表标题被编码为带
titleclass 的 Div,这一约定被withOptionalTitle统一实现,并被 itemizedlist、calloutlist 等列表类元素共享; - 测试基建:
.md即用例、%/^D即输入边界、golden 比对与test-pandoc --emulate替身机制,构成了覆盖读者与写者行为的低成本回归体系。
理解这一条测试,等于掌握了阅读test/command/目录下数百个用例的通用钥匙——每个文件都是一段可直接复现的"命令 + 输入 + 期望输出"三元组,随时可以对照 DocBook 读取器 或 命令测试驱动 深入验证。
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考