Pandoc 网格表 rowspan/colspan 转换实战:HTML 表格到 Markdown 网格表的完整解析
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
导读
本文围绕 Pandoc 仓库中针对复杂表格转换的回归测试 test/command/10848.md,系统讲解 Pandoc 如何把带rowspan/colspan的 HTML 表格无损地转换为 Markdown 网格表(grid table)。你将掌握网格表的边界字符语义、单元格跨行跨列的铺展(cell expansion)规则、simple_tables/multiline_tables/pipe_tables等 Markdown 扩展开关对表格输出的影响,以及背后的源码实现原理,能够直接复现并验证相关转换行为。
背景:为什么需要一个专门的网格表回归测试
在 Pandoc 3.7.0.1 的 changelog.md 中记录了一项重要修复:
Text.Pandoc.Shared.Writer: Fix numerous problems withgridTableand add tests (#10848). These fixes affect the Markdown, RST, and Muse writers.
也就是说,网格表生成逻辑(gridTable)此前存在多处缺陷,例如单元格跨行跨列时边线错位、无法在保持非空白字符串不断裂的前提下完成单元格铺展等问题。修复之后,官方将本次修复涉及的输入输出样例沉淀为命令测试,即 test/command/10848.md,用于防止回归。该测试同时影响 Markdown、RST 与 Muse 三种写出的表格式样,因为这三者都复用同一套网格表渲染核心(src/Text/Pandoc/Writers/Shared.hs)。
测试用例解读:10848.md的结构与运行方式
命令测试文件的组织方式
Pandoc 的命令测试(command test)文件遵循统一的“输入块 + 期望输出块”格式:
- 以
```包裹的代码块,内部第一行是完整的 pandoc 命令行; - 命令行之后到
^D(文件结束标记)之间的内容为标准输入; ^D之后到下一个代码块之间的内容为期望的标准输出。
运行整个命令测试套件的方式是执行测试驱动文件 test/test-pandoc.hs,它会遍历 test/command 目录下所有命令测试文件,逐一执行并比对输出。
10848.md一共包含三组用例,覆盖了三种不同的场景:普通跨行跨列表格、深层嵌套跨行跨列表格、以及禁用表格类扩展后的回退输出。
用例一:基础colspan/rowspan混合表格
输入与期望输出
第一组用例的命令与输入为:
% pandoc -f html -t markdown <table> <tr> <td colspan="3">A</td> <td rowspan="1" colspan="2">F</td> </tr> <tr> <td>C</td> <td colspan="2">B</td> <td colspan="2">H</td> </tr> <tr> <td colspan="2">D</td> <td colspan="2">E</td> <td>G</td> </tr> </table> ^D期望输出为如下 5 列网格表:
+---+---+---+---+---+ | A | F | +---+-------+-------+ | C | B | H | +---+---+---+---+---+ | D | E | G | +-------+-------+---+这张 3 行 × 5 列的网格表完整保留了原 HTML 表格的合并语义:
- 第一行:
A横向跨越 3 列(colspan="3"),F跨越 2 列(colspan="2",rowspan="1"为冗余写法,等价于不跨行); - 第二行:
C独占 1 列,B跨越 2 列,H跨越 2 列; - 第三行:
D、E各跨越 2 列,G独占 1 列。
网格表语法速览
网格表(grid table)是 Pandoc 的 Markdown 扩展表格格式之一(grid_tables扩展),其语法要点如下:
- 单元格边界由
+、-、|字符拼出; - 每一行单元格内容以
|开头和结尾; - 行与行之间的分隔线由
+与-组成; - 跨行单元格会在后续行中继续以
|包裹,但不再重复内容,例如用例一中第二、三行的首列位置出现空列(C下方的| |)。
用例二:深层嵌套的跨行跨列结构
输入与期望输出
第二组用例的输入包含更复杂的嵌套关系——同一表格中同时存在跨 3 行的单元格、跨 2 行 2 列的单元格以及跨 4 列的单元格:
% pandoc -f html -t markdown <table> <tr> <td colspan="2">A</td> <td colspan="2">J</td> <td rowspan="3">F</td> </tr> <tr> <td rowspan="3">C</td> <td>B</td> <td rowspan="2" colspan="2">H</td> </tr> <tr> <td>D</td> </tr> <tr> <td colspan="4">K</td> </tr> </table> ^D期望输出为:
+---+---+-------+---+ | A | J | F | +---+---+-------+ | | C | B | H | | | +---+ | | | | D | | | | +---+-------+---+ | | K | +---+---------------+这张输出最能体现网格表对“跨行 + 跨列”组合的处理方式:
F单元格rowspan="3",因此在输出中第一、二、三行最右侧都保留了| F |所在的列,且第二、三行该列不再书写内容,而是由垂直方向延续的边框表示;C单元格rowspan="3"且占据表格最左列,同样在后续两行保持空位(第二、三行行首的| |);H单元格rowspan="2" colspan="2"同时跨两行两列,其内容只在第二行出现,第三行对应位置以空列延续;- 第四行的
K单元格colspan="4"横跨前四列,与左侧延续下来的C空位形成| | K |的布局。
关键点:rowspan与colspan的叠加语义
当rowspan与colspan同时出现时,该单元格占据的是一个矩形区域(行 × 列)。网格表输出必须同时满足两个约束:
- 横向:单元格宽度等于其所跨各列宽度之和加上列间分隔;
- 纵向:跨行期间该区域不再输出内容,但边框必须正确延续,不能出现断线。
这正是gridTable渲染管线中addDummies与makeDummy两个函数的工作内容(见下文“源码实现解析”)。
用例三:禁用表格扩展后的回退输出
输入与期望输出
第三组用例通过命令行选项显式关闭三种表格扩展:
% pandoc -f html -t markdown-simple_tables-multiline_tables-pipe_tables <table> <tbody> <tr> <td>a</td> <td></td> </tr> <tr> <td></td> <td></td> </tr> </tbody> </table> ^D期望输出为:
+---+---+ | a | | +---+---+ | | | +---+---+命令行中的-t markdown-simple_tables-multiline_tables-pipe_tables表示:以 Markdown 为目标格式,但关闭simple_tables、multiline_tables、pipe_tables三个扩展。这样处理后,Pandoc 只能使用网格表这一种表格语法来表达表格,因此输出退化为最朴素的 2×2 网格表。
命令行选项的扩展开关语法
-t/--to指定输出格式;- 格式名后跟
+扩展名表示启用扩展,跟-扩展名表示禁用扩展; - 多个扩展可以用
+/-连续叠加书写。
例如-t markdown-simple_tables-multiline_tables-pipe_tables等价于“Markdown 格式,禁用三种表格扩展”。当所有其他表格扩展都被禁用而grid_tables仍处于启用状态(它是默认启用的 Markdown 扩展)时,Pandoc 的输出就会回退到网格表。
空单元格的处理
注意输入中第二行存在两个完全空的<td></td>。Pandoc 依然为它们生成宽度为 0 的单元格,并在网格表中以空格填充(| | |),保证表格结构的完整性。这一行为由单元格宽度计算与铺展逻辑共同保证(见下文redoWidths与resetWidths)。
从 HTML 读取端看rowspan/colspan的解析
HTML 表格读取器对属性的处理
在 src/Text/Pandoc/Readers/HTML/Table.hs 中,HTML 表格读取器解析<td>/<th>单元格时:
let rowspan = RowSpan . fromMaybe 1 $ safeRead =<< lookup "rowspan" attribs let colspan = ColSpan . fromMaybe 1 $ safeRead =<< lookup "colspan" attribs- 通过
lookup "rowspan"/lookup "colspan"从属性表中取字符串值; - 用
safeRead将字符串安全解析为整数; fromMaybe 1保证当属性缺失或无法解析时默认取 1(即不跨行 / 不跨列)。
随后colspan、rowspan等属性会被从通用属性列表中剔除(handledAttribs),因为它们已经被结构化为 Pandoc 表格单元格的RowSpan/ColSpan字段,不应再作为普通属性(如style、class)保留。
在 src/Text/Pandoc/Readers/HTML/Table.hs 处,读取器还使用行列跨度信息来跳过后续行中已被跨行单元格占用的列位置:
(Cell _ _ (RowSpan rowspan) (ColSpan colspan) _) = ... i < currentrow + rowspan then x + colspan也就是说,当一个单元格跨越多行时,读取器会记录其占用范围,在后续行计算下一个单元格的起始列时会自动跳过被占用的位置,从而正确重建表格结构。
内部表格模型
Pandoc 的表格在内部统一表示为带ColSpan/RowSpan的单元格网格(见 src/Text/Pandoc/Writers/Shared.hs 中Ann.Cell的构造),无论输入格式是 HTML、Markdown 还是其他格式,最终写出网格表时都共享同一套渲染逻辑。这也是为什么 #10848 的修复会同时影响 Markdown、RST 与 Muse 三种写出格式。
网格表渲染核心:gridTable的源码实现解析
渲染入口
Markdown 写出器在生成表格时调用位于 src/Text/Pandoc/Writers/Shared.hs 的gridTable:
gridTable :: Monad m => WriterOptions -> (WriterOptions -> [Block] -> m (Doc Text)) -> [ColSpec] -> TableHead -> [TableBody] -> TableFoot -> m (Doc Text)其调用点之一在 src/Text/Pandoc/Writers/Markdown.hs:
tbl <- gridTable opts blockListToMarkdowngridTable的职责是把内部表格模型(表头、表体、表尾)渲染为RenderedCell列表,再交由gridRows输出最终的网格文本。
跨行单元格的“占位”机制:addDummies与makeDummy
网格表不能像 HTML 那样用rowspan属性表达跨行,它必须通过边框延续 + 空单元格占位来还原跨行效果。这一任务由 src/Text/Pandoc/Writers/Shared.hs 中的makeDummy与addDummies完成:
makeDummy c = RenderedCell{ cellColNum = cellColNum c, cellColSpan = cellColSpan c, ... cellRowSpan = cellRowSpan c - 1, cellWidth = cellWidth c, cellContents = mempty, cellBottomBorder = NoLine, cellTopBorder = NoLine }- 每个跨行单元格在每一后续行都会生成一个“占位单元格”(dummy cell);
- 占位单元格的内容为空(
mempty),rowSpan递减,直到跨行结束; - 占位单元格的上下边框设为
NoLine,使内容区呈现“镂空”效果,而外侧边框依然延续。
addDummies通过按列号归并(addDummiesToRow按cellColNum比较插入占位),把跨行单元格的占位正确地插入到后续行的对应列位置。
宽度重算:redoWidths与resetWidths
在 src/Text/Pandoc/Writers/Shared.hs 中,redoWidths负责根据实际内容宽度重新分配各列宽度:
extractColWidths计算每列的指定宽度(specifiedwidths)、完整宽度(fullwidths)与最小宽度(minwidths);- 对于跨列的单元格,其内容宽度会按所跨列数均分(见
getCellWidths中calcOffset c \div` (cellColSpan c)` 的逻辑); recalculateWidths采用递归迭代策略分配默认宽度,最多迭代 4 轮(numRuns > 4终止),优先让能放得下的列使用完整宽度,剩余列再均分剩余空间;- 总宽度受
writerColumns(即 pandoc 的--columns选项,默认 72)约束,见colsAvailable = writerColumns opts - (3 * numcols) - 1。
resetWidths(src/Text/Pandoc/Writers/Shared.hs)则把计算好的列宽写回每个单元格,对colSpan > 1的单元格,其总宽度为各列宽度之和再加上3 * (n-1)个分隔字符的宽度。
changelog 中提到的“expand cells when it isn't possible to lay them out without breaking string of non-whitespace”(当不拆分非空白字符串就无法排版时扩展单元格),正是这套宽度重算逻辑要解决的问题:当某列内容包含无法断行的长字符串时,列宽必须扩展以保证内容不被切断。
边框合并与表头线
gridRows(src/Text/Pandoc/Writers/Shared.hs)把每一行的顶边框、内容行、底边框组合输出,其中combineBorders(src/Text/Pandoc/Writers/Shared.hs)负责逐字符合并相邻两行的边框线,规则包括:
|与-相遇变为+;|与=相遇变为+;- 空格会被相邻行的实际字符覆盖;
- 表头线(
=)优先保留。
formatHeaderLine与formatBorder(src/Text/Pandoc/Writers/Shared.hs)则按LineStyle(SingleLine/DoubleLine/SingleHeaderLine/DoubleHeaderLine)生成对应的-/=线,并在alignMarkers模式下输出:对齐标记(网格表扩展grid_tables支持用:表示列对齐)。这解释了用例一、二中所有+交汇点为何能严格对齐:它们都由同一套+/-/|排版函数生成。
在真实环境中复现验证
前提
以下复现步骤需要本仓库源码构建出的 pandoc 可执行文件。按 INSTALL.md 中的指引使用cabal或stack构建后,即可在 test/command 目录下执行对应命令。
复现用例一
将下面的内容保存为输入文件(或直接通过管道输入),然后执行:
cat <<'EOF' | pandoc -f html -t markdown <table> <tr> <td colspan="3">A</td> <td rowspan="1" colspan="2">F</td> </tr> <tr> <td>C</td> <td colspan="2">B</td> <td colspan="2">H</td> </tr> <tr> <td colspan="2">D</td> <td colspan="2">E</td> <td>G</td> </tr> </table> EOF应得到与10848.md期望输出完全一致的 5 列网格表。
复现用例三(扩展开关)
cat <<'EOF' | pandoc -f html -t markdown-simple_tables-multiline_tables-pipe_tables <table> <tbody> <tr> <td>a</td> <td></td> </tr> <tr> <td></td> <td></td> </tr> </tbody> </table> EOF应得到 2×2 网格表,验证“禁用其他表格扩展后回退到网格表”的行为。
反向转换验证
网格表也可以作为输入格式被 Pandoc 读取。将期望输出保存为 Markdown 文件再执行pandoc -f markdown -t native,可以查看内部表格模型中每个单元格的RowSpan/ColSpan是否与原 HTML 表格一致,从而验证转换的可逆性。
与其他表格输出格式的关系
#10848 修复的gridTable同时服务于 Markdown、RST 与 Muse 写出器。若将-t markdown换成-t rst或-t muse,同样会调用 src/Text/Pandoc/Writers/Shared.hs 中的网格表渲染核心,只是行分隔符与表头线的表达方式略有差异。这也是该回归测试被设计为“HTML → 通用格式”的原因——它直接验证了内部表格模型与网格渲染核心的正确性,而不局限于某一种输出格式。
小结
通过 test/command/10848.md 的三组用例,本文梳理了 Pandoc 处理带rowspan/colspanHTML 表格的完整链路:
- HTML 读取器在 src/Text/Pandoc/Readers/HTML/Table.hs 中解析跨行跨列属性并重建内部表格模型;
gridTable渲染核心(src/Text/Pandoc/Writers/Shared.hs)通过addDummies/makeDummy生成跨行占位单元格,通过redoWidths/resetWidths完成列宽分配与跨列单元格铺展,通过combineBorders合并边框;- 命令行扩展开关(如
-markdown-simple_tables)决定了输出是否回退到网格表。
这套机制使 Pandoc 能够在网格表这种“纯 ASCII 边框”格式中忠实还原复杂的表格合并结构,同时保持内容不因换行而断裂,是 Pandoc 通用文档转换能力中表格处理部分的关键实现之一。
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考