Pandoc RST 读取器简单表格的多行表头解析:命令测试 10338 源码级剖析
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
导读
本文以 pandoc 仓库中的命令测试用例 test/command/10338-rst-multiple-header-rows.md 为主线,深入讲解 RST(reStructuredText)简单表格(simple table)语法中**多行表头(multiple header rows)与无表头表格(headless table)**的解析规则,并结合 src/Text/Pandoc/Readers/RST.hs 与 src/Text/Pandoc/Parsing/GridTable.hs 的源码,揭示 Pandoc 内部表头结构(TableHead、Row、Cell)的构建原理。读完本文,你将掌握 RST 简单表格的完整书写格式、Pandoc 命令测试的编写与运行方式,以及 Pandoc 表格 AST 中多行表头与无表头两种形态的表示方法。
一、测试用例背景:pandoc 命令测试体系
1.1 命令测试文件的组织方式
在 pandoc 仓库中,test/command/目录下存放着大量以数字编号命名的 Markdown 文件,每个文件包含一个或多个"命令测试"。文件 test/Tests/Command.hs 的模块注释明确规定了命令测试的书写格式:
- 第一行以
%开头,后面是要执行的命令; - 之后是作为标准输入(stdin)传入命令的若干行文本;
- 输入以单独一行
^D结束; ^D之后的若干行是期望的标准输出(stdout);- 如果期望有标准错误输出,需要放在最前面,且每行以
2>前缀开头; - 如果期望非零退出码,最后一行应包含
=>和退出码。
从 test/Tests/Command.hs 可以看出,测试框架会读取command目录下所有以.md结尾的文件,逐个提取其中的代码块作为独立测试用例,并以#编号命名。
1.2 10338 测试用例的定位
test/command/10338-rst-multiple-header-rows.md 是针对 RST 读取器(-f rst)的回归测试,验证的是简单表格(simple table)支持多行表头这一行为。测试命令为:
% pandoc -f rst -t native即把 RST 源码解析为 Pandoc 的原生 AST(native 格式)输出,从而精确断言解析结果。
二、RST 简单表格语法回顾
RST 规范定义了两种主要表格:简单表格(simple table)与网格表格(grid table)。Pandoc 的 RST 读取器对两者均有支持,对应源码位于 src/Text/Pandoc/Readers/RST.hs:
table :: PandocMonad m => RSTParser m Blocks table = compactifyTable <$> (gridTable <|> simpleTable False <|> simpleTable True <?> "table")从这段源码可以看出,读取器依次尝试三种解析路径:
gridTable:网格表格;simpleTable False:带表头的简单表格;simpleTable True:无表头的简单表格(headless标志为True)。
简单表格的典型形态是用=(等号)画出的顶线、表头分隔线与底线,表格每行内容必须在单行内写完。本测试用例中的 "Multiple Headers" 表格演示了在顶线与分隔线之间放置两行表头的写法:
========== ========= Header A1 Header A2 Header B1 Header B2 ========== ========= body a1 body a1 body b1 body b2 ========== =========三、逐行解析测试用例:从 RST 到 Native AST
3.1 带多行表头的简单表格
测试输入的第一个表格是带标题的 "Multiple Headers" 表格,其期望输出(native 格式)中,表头部分被解析为一个TableHead,内部包含两个Row:
(TableHead ( "" , [] , [] ) [ Row ( "" , [] , [] ) [ Cell ( "" , [] , [] ) AlignDefault (RowSpan 1) (ColSpan 1) [ Plain [ Str "Header" , Space , Str "A1" ] ] , Cell ... "Header A2" ] , Row ( "" , [] , [] ) [ Cell ... [ Plain [ Str "Header" , Space , Str "B1" ] ] , Cell ... [ Plain [ Str "Header" , Space , Str "B2" ] ] ] ])这一输出证实了 Pandoc 的 AST 中:
TableHead的第二个字段是一个[Row]列表,可以包含任意多行;- 每个表头单元格都携带独立的属性(此处均为空属性
("", [], []))、对齐方式AlignDefault、RowSpan 1与ColSpan 1; - 表头单元格的内容类型为
Plain(纯段落)。
而主体部分TableBody中的每个单元格同样以Plain包裹,RowSpan/ColSpan均为 1,说明该表格没有跨行跨列合并。
3.2 无表头表格(Headless)
测试输入的第二个表格 "Headless" 展示了完全没有表头行的简单表格写法——顶线之后直接跟分隔线,中间没有任何表头文本:
========== ========= body a1 body a1 body b1 body b2 ========== =========其解析结果中TableHead的[Row]列表为空:
(TableHead ( "" , [] , [] ) [])主体TableBody则照常包含两行Row。这正是 src/Text/Pandoc/Readers/RST.hs 中simpleTableHeader函数对headless参数的处理结果:
simpleTableHeader :: PandocMonad m => Bool -- ^ Headerless table -> RSTParser m ([[(Blocks, RowSpan, ColSpan)]], [Alignment], [Int]) simpleTableHeader headless = try $ do optional blanklines dashes <- simpleDashedLines '=' rawContent <- if headless then return [("", Nothing)] else many1 $ notFollowedBy (simpleDashedLines '=') >> rowWithOptionalColSpan unless headless $ simpleTableSep '=' ... let rawHeads = if headless then [] else map (simpleTableSplitLine indices) rawContent ...可以看到:
- 当
headless为True时,不解析任何表头内容行(rawContent直接置空),且跳过表头分隔线simpleTableSep '='; - 当
headless为False时,使用many1(至少一个)解析表头行,每一行都可以通过rowWithOptionalColSpan附带可选的列合并信息(-虚线下方的:span:语法); - 每个表头行通过
simpleTableSplitLine indices依据顶线的列索引切分单元格,再以parseFromString'解析为内联内容。
3.3 表头行与表体行的解析分工
测试输入还验证了表头与表体使用不同的解析函数。在simpleTable中(src/Text/Pandoc/Readers/RST.hs):
simpleTable headless = do ... tbl <- runIdentity <$> tableWithSpans (wrapIdFst <$> simpleTableHeader headless) (wrapId <$> simpleTableRow) sep simpleTableFooter ...- 表头部分交给
simpleTableHeader headless; - 表体部分交给
simpleTableRow,后者通过notFollowedBy' (void blanklines <|> simpleTableFooter)判断行结束(src/Text/Pandoc/Readers/RST.hs); - 表格以
simpleTableFooter(=底线加空行)终止(src/Text/Pandoc/Readers/RST.hs)。
四、底层原理:tableWithSpans 如何组装表头
4.1 通用表格组件解析器
simpleTable调用的tableWithSpans定义在 src/Text/Pandoc/Parsing/GridTable.hs,它是 pandoc 各种纯文本表格读取器共享的通用组件:
tableWithSpans hp rp lp fp = fmap tableFromComponents <$> tableWithSpans' NoNormalization hp rp lp fp其核心逻辑(src/Text/Pandoc/Parsing/GridTable.hs):
- 先运行表头解析器
headerParser,得到(heads, aligns, indices)——即表头单元格列表、对齐方式列表、列索引; - 用
sepEndBy1反复运行行解析器rowParser indices解析表体; - 运行
footerParser收尾; - 根据列索引通过
widthsFromIndices计算相对列宽(src/Text/Pandoc/Parsing/GridTable.hs); - 最终通过
tableFromComponents(src/Text/Pandoc/Parsing/GridTable.hs)包装成 Pandoc 的Table块。
4.2 表头行的归一化规则
多行表头与无表头表格在通用层如何区分?src/Text/Pandoc/Parsing/GridTable.hs 中的toHeaderRow给出了答案:
toHeaderRow :: TableNormalization -> [(Blocks, RowSpan, ColSpan)] -> Maybe Row toHeaderRow = \case NoNormalization -> \l -> if not (null l) then Just (toRow l) else Nothing NormalizeHeader -> \l -> if not (all nullHeaderRow l) then Just (toRow l) else Nothing where nullHeaderRow (l, _, _) = null lNoNormalization模式下,只要表头行列表非空就生成Row,否则为Nothing(无表头);NormalizeHeader模式下,只有存在非空单元格时才算有效表头行。
RST 简单表格使用NoNormalization,因此 "Headless" 表格中空的表头行列表被直接映射为TableHead nullAttr [](空表头),这正是测试期望输出中(TableHead ( "" , [] , [] ) [])的来源。
4.3 单元格的组装
最终每个Row由 src/Text/Pandoc/Parsing/GridTable.hs 的toRow组装:
toRow :: [(Blocks, RowSpan, ColSpan)] -> Row toRow = Row nullAttr . map (\(blocks, rowSpan, columnSpan) -> B.cell AlignDefault rowSpan columnSpan blocks)每个单元格默认对齐方式为AlignDefault,行/列跨度由解析结果传入。测试用例中所有单元格均为RowSpan 1、ColSpan 1,与源码中 RST 简单表格单行单元格的默认跨度一致。
五、如何运行与验证该测试
5.1 本地运行测试
在仓库根目录构建并运行命令测试套件(以 cabal 为例):
cabal build test-pandoc cabal test test-pandoc --test-options='--pattern 10338'若希望单独调试该用例,也可以直接手动执行其内部命令:
pandoc -f rst -t native < test/command/10338-rst-multiple-header-rows.md注意:手动执行时需将输入截取到^D之前的内容,因为^D只是测试框架约定的 stdin 终止符,并非 RST 语法的一部分。
5.2 测试框架的判定逻辑
test/Tests/Command.hs 展示了测试判定过程:框架把^D之后的内容作为期望输出(getExpected),把实际执行命令的 stdout 作为实际输出(getActual),二者逐字符比较(过滤\r以兼容 Windows)。若不一致,会给出 diff 形式的分行对比,便于定位 RST 解析行为的变化。该机制保证了像"多行表头"这类解析行为的回归稳定性——一旦读取器实现发生变动导致输出偏离,测试会立即失败并提示差异。
六、实战要点与常见陷阱
6.1 多行表头的书写规范
从本测试用例可以提炼出 RST 简单表格多行表头的三条硬性规则:
- 表头行必须位于顶线(
=)与分隔线(=)之间,可以写任意多行; - 每一行表头都必须在单行内完成,不支持跨行换行(这是简单表格区别于网格表格的显著特征,源码注释见 src/Text/Pandoc/Readers/RST.hs 中 "Simple tables TODO: multiline support");
- 列边界由顶线的
=分隔位置决定,表头与表体各行必须与顶线的列切分对齐,否则会触发simpleTableSplitLine中的 "col spans don't match" 错误(src/Text/Pandoc/Readers/RST.hs)。
6.2 无表头表格的正确写法
无表头表格的关键在于:顶线之后紧接分隔线,中间不能有文本行。若在中间误写内容,读取器会将其当作表头行解析,产出非预期的TableHead。与之对应的语法分支是 src/Text/Pandoc/Readers/RST.hs 中simpleTable False(带表头)与simpleTable True(无表头)两条解析路径,读取器会先尝试带表头版本,失败后再尝试无表头版本。
6.3 验证输出结构的小技巧
-t native是调试表格解析最直观的方式:它把 AST 完整序列化,TableHead中的[Row]数量一目了然。结合本测试用例,你可以通过对比"多行表头"与"无表头"两种输入下的TableHead形态差异,快速理解 Pandoc 内部对表头行的建模方式。
七、延伸阅读
- 测试用例原文:test/command/10338-rst-multiple-header-rows.md
- RST 读取器表格实现:src/Text/Pandoc/Readers/RST.hs(含
simpleTableHeader、simpleTableRow、simpleTable、table) - 通用表格组件解析器:src/Text/Pandoc/Parsing/GridTable.hs(
tableWithSpans、widthsFromIndices、toHeaderRow) - 命令测试框架:test/Tests/Command.hs(格式约定)与 test/Tests/Command.hs(判定逻辑)
- RST 简单表格与
simple_tables扩展的通用文档说明可参考 MANUAL.txt
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考