pandoc LaTeX 输出的 `?``/`!`` 连字(ligature)问题与修复:从 test/command/5407.md 解读 `{\kern0pt}` 的插入机制
2026/9/20 12:43:15 网站建设 项目流程

pandoc LaTeX 输出的?``/!`` 连字(ligature)问题与修复:从 test/command/5407.md 解读{\kern0pt}的插入机制

【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc

导读

本文以 pandoc 仓库中的黄金回归测试用例 test/command/5407.md 为主线,深入讲解 LaTeX 输出路径中一个容易踩坑的细节:当正文文本以问号?或感叹号!紧接一个左引号时,TeX 会把?`` 与!`` 组合排版成倒置的问号(¿)和感叹号(¡)。文章将完整解读该测试文件的输入/输出结构、pandoc 命令行测试(golden test)的编写与运行机制,并溯源到 LaTeX 转义核心实现 中stringToLaTeX的逐字符转义逻辑,帮助你理解 pandoc 如何用{\kern0pt}精确阻断这类连字,以及该机制适用的前提条件(smart 扩展开启、纯文本上下文)。

一、测试用例的原始内容与逐行解读

test/command/5407.md 全文是一个被三重反引号包裹的代码块,内容如下:

% pandoc -t latex --wrap=preserve hi there?“ hi there!“ hi there?‘ hi there!‘ hi there! ^D hi there?{\kern0pt}`` hi there!{\kern0pt}`` hi there?{\kern0pt}` hi there!{\kern0pt}` hi there!

这是一个典型的 pandoc 命令行测试用例,可以拆解为三个部分:

部分内容含义
命令行% pandoc -t latex --wrap=preserve以 LaTeX 为输出格式,--wrap=preserve保证输出不自动换行,便于逐行比对
标准输入hi there?“等 5 行文本,以^D结束模拟终端输入的 EOF 结束符
期望输出`hi there?{\kern0pt}`` 等 5 行与输入一一对应的 LaTeX 转义结果

注意输入文本中紧跟问号/感叹号的是两个真正的 Unicode 左引号字符:(U+201C 左双引号)与(U+2018 左单引号)。它们在 pandoc 的 LaTeX 输出中分别被渲染为 TeX 反引号序列` ``。于是:

  • hi there?“hi there?``,若不处理,TeX 会将其排成 ¿(倒置问号)连字;
  • hi there!“hi there!``,若不处理,会排成 ¡(倒置感叹号);
  • hi there?‘hi there?
  • hi there!‘hi there!
  • 最后一行hi there!后面没有引号,原样输出。

期望输出中,凡是可能形成连字的位置都被插入了{\kern0pt}(零字距 kern,用于阻断 TeX 连字),最终得到 `hi there?{\kern0pt}`` 这样的安全序列。这个测试用例对应的是 GitHub issue #5407:pandoc 从 Markdown(默认开启 smart 扩展)转换到 LaTeX 时,不应“无意中”制造出倒置问号/感叹号。

二、命令行测试(golden test)的格式与执行机制

test/command/目录下存放大量以 issue 编号命名的.md文件,每个文件里的每一个代码块都是一条独立的命令测试。解析逻辑位于 test/Tests/Command.hs:

  • extractCommandTest(Command.hs)用 pandoc 自身的 Markdown 读取器把测试文件解析成代码块,逐个构造测试用例;
  • runCommandTest(Command.hs)按行解析代码块:
    • %开头的行为命令行(dropPercent去除%及随后的空格,Command.hs);
    • 遇到单独的^D行之前的所有行为标准输入(break (=="^D"));
    • ^D之后、单独.行之前的内容为期望输出;
    • 实际执行时用execTest运行 pandoc 并把输出与期望值比对,不一致则报告 diff;
    • 测试名即为文件名,例如#5407

也就是说,5407.md本质上是把“复现 issue 5407 的最小命令”固化成了可自动回归的测试。你完全可以手工复现这条命令,观察修复前后的行为差异:

% pandoc -t latex --wrap=preserve hi there?“ hi there!“ hi there?‘ hi there!‘ hi there!

三、问题本质:TeX 的?`` 与!`` 连字

在 TeX 排版体系中,反引号与某些字符组合会触发“连字”(ligature):

  • ?`组合被渲染为倒置问号 ¿;
  • !`组合被渲染为倒置感叹号 ¡;
  • 两个连字符--会形成 en-dash,---形成 em-dash。

这在西班牙语等使用倒置标点的语境里是 TeX 的刻意设计(配合fontenc/T1 编码),但对于从 Markdown 转换而来的普通英文文本则是灾难:用户明明写的是“hi there?”加一个左引号,编译出来却变成“hi there¿”。pandoc 在 changelog.md(pandoc 2.7.2,2019-04-05)中明确记录了这次修复:

LaTeX writer: Avoid inadvertently creating?`` or!`` ligatures (#5407). These are upside down ? and !, resp.

中文含义即:避免无意间制造?`` 或!`` 连字(它们分别对应倒置的 ? 和 !)。

四、源码级实现:stringToLaTeX的“向后看”转义

修复的核心位于 src/Text/Pandoc/Writers/LaTeX/Util.hs 的stringToLaTeX。该函数用foldr从右向左逐字符转义字符串:

stringToLaTeX context zs = do opts <- gets stOptions ... return $ T.pack $ foldr (go opts context) mempty $ T.unpack zs

关键点在于foldr的处理方向:当处理到某个字符x时,累加器xs中存放的已经是其右侧所有字符转义后的输出。因此go里的守卫可以直接检查xs的开头是否是反引号,从而实现“预览右侧转义结果”的效果:

'?' | ligatures -> -- avoid ?` ligature case xs of '`':_ -> emits "?{\\kern0pt}" -- se #10610 _ -> emitc x '!' | ligatures -> -- avoid !` ligature case xs of '`':_ -> emits "!{\\kern0pt}" _ -> emitc x

(见 Util.hs。)

结合测试用例可以完整还原执行链路:输入行hi there?“中,最右侧的先被转义为(见 `'\x201C' | ligatures -> emitquote "",[Util.hs](https://link.gitcode.com/i/ba8fa6ddb24b70289e5d7ccdb58fdbc4#L146));随后处理到?时,右侧输出以 ``开头,命中 `'`':_` 分支,于是输出 `?{\kern0pt}`。两个字符最终拼接为 `?{\kern0pt}`,与 5407.md 的期望输出完全一致。

其中ligatures的定义限定了修复的触发条件:

let ligatures = isEnabled Ext_smart opts && ctx == TextString

(Util.hs。)

它包含两个硬性前提:

  1. smart 扩展必须开启Ext_smart(定义于 src/Text/Pandoc/Extensions.hs)是 Markdown 输入的默认扩展之一,负责智能引号、撇号、省略号、连字符转换。只有在 smart 开启时,pandoc 才会把引号字符输出为 TeX 反引号序列,也因此才需要防范随之而来的连字;
  2. 上下文必须是普通文本StringContext区分TextStringURLStringCodeString(Util.hs)。在 URL 上下文中反引号会被转义为\%60(Util.hs),在代码上下文中反引号会被转义为\textasciigrave(Util.hs),均不会产生 TeX 连字,自然无需干预。

五、同一模式下的其他连字防护

?/!的处理并非孤例,stringToLaTeX中应用了相同的“检查右侧转义输出”模式来防护多种 TeX 连字:

  • 连字符'-' -> case xs of ('-':_) -> emits "-\\/"(Util.hs),当右侧输出以-开头时插入\/,防止相邻连字符形成--en-dash;
  • 引号之间的薄空格emitquote在引号后紧跟另一个引号时插入\,薄空格(Util.hs),防止`''`等组合被错误合并;
  • \kern0pt的同类用法\x200B(零宽空格)字符在输出中同样被映射为\hspace{0pt}(Util.hs),与{\kern0pt}一样都是通过插入零宽度元素来阻断 TeX 的自动合字。

从源码结构看,该修复是LaTeX 输出路径专属的:ConTeXt 写手的转义函数escapeCharForConTeXt(src/Text/Pandoc/Writers/ConTeXt.hs)虽然同样用ligatures = isEnabled Ext_smart opts控制--/---/'的转换,但并未出现{\kern0pt}形式的?/!特殊处理——这与两种排版引擎对引号字符的处理方式不同有关。

六、测试的维护与回归价值

5407.md这类文件的价值在于把一次 bug 修复固化为永久回归测试:

  • 它验证了“输入含?/!加左引号时,LaTeX 输出必须插入{\kern0pt}”这一行为契约;
  • 它同时覆盖了单引号与双引号、问号与感叹号的四种组合,以及一个无引号的对照行(hi there!),保证修复不过度作用;
  • 任何未来改动如果破坏了这一转义逻辑(例如改变了foldr方向、关闭了 smart 判断、调整了引号转义序列),#5407测试都会立即失败并给出精确 diff。

如果你在本地构建了 pandoc(cabal buildstack build),也可以通过运行测试套件(如cabal test中的 command 测试组)来执行该用例;或者直接用发布版/自建二进制手工执行上文的管道命令,观察输出中{\kern0pt}是否存在。理解了这个测试,你就掌握了 pandoc 命令行 golden test 的读写方式,也明白了 LaTeX 输出中“字符转义 + 连字防护”这一层容易被忽略但至关重要的工程细节。

参考路径速查

  • 测试用例:test/command/5407.md
  • 测试解析与执行:test/Tests/Command.hs
  • LaTeX 转义核心实现:src/Text/Pandoc/Writers/LaTeX/Util.hs
  • smart 扩展定义:src/Text/Pandoc/Extensions.hs
  • 修复记录:changelog.md(pandoc 2.7.2)
  • ConTeXt 转义对照实现:src/Text/Pandoc/Writers/ConTeXt.hs

【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc

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

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

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

立即咨询