Pandoc man 手册转 Typst 的括号转义修复:11210 测试用例与 escapeParens 实现解析
2026/9/19 20:45:55 网站建设 项目流程
  • 文档
  • 开发工具
  • CLI

【免费下载链接】pandoc

Universal markup converter

项目地址:https://gitcode.com/gh_mirrors/pa/pandoc
点击查看免费下载

本文以 pandoc 仓库中的回归测试用例 test/command/11210.md 为切入点,完整剖析一个真实缺陷的成因与修复:当 man(roff)格式的文档经 pandoc 转为 Typst 时,紧跟在强调元素之后的左括号若不转义,会在 Typst 编译器中被误判为函数调用而报错。读完本文,你将理解 golden 测试的运作方式、Typst 的#emph...语法歧义、以及 pandoc Typst writer 中escapeParens的实现细节。

一、测试用例全貌:一段 man 输入,一行关键输出

test/command/11210.md 是 pandoc 测试体系中典型的「命令式 golden test」:文件内以%开头的第一行声明要执行的 pandoc 命令行,随后是标准输入(以^D结束),^D之后的内容则是期望的标准输出。

用例原文如下:

% pandoc -t typst -f man .PP .IR login (1) .PP and a regular (paren) that should not be escaped. ^D #emph[login]\(1) and a regular (paren) that should not be escaped.

该用例对应 GitHub issue #11210,测试的是一条「man → Typst」的转换链路:

  • 输入是 man(troff)格式,.PP表示段落分隔;
  • .IR login (1)是 man 的行内字体宏,期望login以斜体渲染、(1)以正体渲染;
  • 关键断言在期望输出的第一行:#emph[login]\(1)——]之后的左括号前多了一个反斜杠转义;
  • 而第二段普通文本and a regular (paren)中的括号则不应被转义。

同一行输入、两种截然不同的处理,正是这次修复的核心。

二、本地复现:让同一段 man 输入经过 pandoc

在已构建好 pandoc 的环境中,可以直接复现这一转换。把上文中的输入部分写入文件(或直接通过管道喂给标准输入):

.PP .IR login (1) .PP and a regular (paren) that should not be escaped.

然后执行:

pandoc -t typst -f man input.man

在修复该缺陷的版本中,第一行输出应为:

#emph[login]\(1)

而第二段中的(paren)保持原样输出。如果你使用的是旧版本,则会看到未转义的#emphlogin——这正是 Typst 编译器无法接受的形式。命令格式上,-f man显式指定 man 读取器,-t typst指定 Typst writer,与用例命令行完全一致。

三、为什么这是个 bug:Typst 函数调用语法带来的歧义

要理解修复动机,需要先了解 Typst 标记语言的一个语法特征。在 Typst 中,#emph[login]是对emph函数的调用,方括号是其参数;紧随其后的圆括号(1)会被 Typst 解析为对上一次调用结果的再次调用,即:

#emphlogin

等价于试图以参数1调用#emph[login]的返回值。这种写法在 Typst 中会引发类型错误,导致整个文档编译失败。而 man 手册页中「程序名(章节号)」是极常见的写法——login (1)ls (1)几乎出现在每一份手册的第一行,因此这个问题对 man → Typst 转换是实际且高频的。

pandoc 的变更记录 changelog.md 对此修复的说明是:

Typst writer: Escape open paren after non-space (#11210). This fixes an issue that occurs if an open paren comes right after e.g.#strong[test].

注意其中的限定词after non-space——这正是用例第二段断言and a regular (paren)不转义的原因:那里的左括号前面是空格,不在修复范围内。

四、修复源码:Typst writer 中的 escapeParens

修复实现位于 Typst writer 的 src/Text/Pandoc/Writers/Typst.hs,核心是新增的escapeParens函数:

-- Add an escape before a parenthesis right after a non-space element. -- Otherwise we risk `#emphtest` which will error. See #11210. escapeParens :: [Inline] -> [Inline] escapeParens [] = [] escapeParens (s : x : xs) | isSpacey s = s : x : escapeParens xs escapeParens (Str t : xs) | Just ('(',_) <- T.uncons t = RawInline (Format "typst") "\\" : Str t : escapeParens xs escapeParens (x : xs) = x : escapeParens xs

该函数对 Pandoc 内联元素序列做逐个扫描,逻辑分三层:

  1. 空列表:直接返回,作为递归终止条件;
  2. 前一个元素是空白isSpacey判断SpaceSoftBreakLineBreak,见 同文件 L446-L450):跳过检查,保持原样递归处理后续——对应「普通文本中的括号不转义」;
  3. 当前元素是Str且以(开头:在该文本元素之前插入一个RawInline (Format "typst") "\\",即一个 Typst 格式的原生反斜杠,然后再输出该文本。

它被挂接在行内序列渲染的入口 inlinesToTypst 中:所有内联元素在交给inlineToTypst逐项渲染之前,先经过escapeParens预处理。从源码结构看,这是渲染前的「语法消毒」层——pandoc 内部 AST 本身无需感知 Typst 语法,只在最终输出 Typst 文本时做适配。

五、man 读取器侧:.IR 宏如何一步步产生触发 bug 的序列

修复在 writer,但触发条件由 reader 产生。追溯输入.IR login (1)在 man 读取器中的解析路径:

src/Text/Pandoc/Readers/Man.hs 的handleInlineMacro中,行内宏分发表将IR映射为:

"IR" -> parseAlternatingFonts [emph, id] args

即交替字体宏:第一个参数用emph(斜体)包装,第二个参数保持id(正体)。因此.IR login (1)的两个参数login(1)被解析为:

  • Emph [Str "login"](斜体 login)
  • Str "(1)"(正体 (1))

而在 parseInlines 中,相邻内联之间会以B.space连接,于是最终 AST 序列为Emph [Str "login"]SpaceStr "(1)"

回到escapeParens的扫描逻辑:

  • 第一个元素Emph [...]不是空白、也不是以(开头的Str,落入兜底分支原样输出;
  • 第二个元素Space满足isSpacey,跳过;
  • 第三个元素Str "(1)"(开头,于是插入RawInline "\\"

最终渲染结果正是#emph[login]后跟空格、再跟\(1)。整个过程可以从 Man.hs 的宏解析与 Typst.hs 的转义预处理中完整还原。

六、边界情况与工程启示

这个 13 行的测试用例背后,是一个值得注意的边界设计:转义只针对「紧贴非空格元素」的左括号。原因在于 Typst 的调用语法中,#emphtest的歧义仅当圆括号与方括号直接相邻时才会出现;一旦中间存在空格,(3)就是独立的普通文本。isSpacey覆盖了SpaceSoftBreakLineBreak三种情况,说明无论是普通空格、自动换行插入的断行还是显式换行,都被视为「安全间隔」。

从工程方法上看,该用例体现了 pandoc 处理跨格式语法冲突的通用套路:

  1. 在 reader 侧忠实还原源格式语义(man 的字体交替宏 → 统一的Emph内联),不提前为目标格式做特判;
  2. 在 writer 侧输出前统一做目标格式的语法消毒(escapeParens),通过插入RawInline原生内容精确控制输出;
  3. 用 golden test 固定「该转义处转义、不该转义处不转义」的边界行为,防止后续改动回归。

类似的「writer 侧转义适配」思路在 Typst writer 中还有多处体现,例如标签前插入零宽空格以避免标签误绑前序元素(见 Typst.hs L427-L431 中对 #11568 的处理注释)。如果你正在为 pandoc 贡献新的 writer 或修改 Typst 输出逻辑,test/command/目录下的此类用例(如 11210.md)是最直接的回归保护网——修改后运行对应用例即可验证是否破坏既有边界行为。

  • 文档
  • 开发工具
  • CLI

【免费下载链接】pandoc

Universal markup converter

项目地址:https://gitcode.com/gh_mirrors/pa/pandoc
点击查看免费下载

相关推荐

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

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

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

立即咨询