pandoc 命令回归测试 4589 详解:Markdown 中的原始 LaTeX 宏与行内格式的正确解析
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
本指南以 test/command/4589.md 这一命令式回归测试用例为主体,讲解 pandoc 在将 Markdown 转换为 LaTeX 时如何处理文档中嵌入的\newcommand宏定义与行内原始 TeX 命令,以及为什么"紧贴在一起的多个 LaTeX 命令会破坏后续 Markdown 强调格式"这一缺陷需要专门修复。读完本文,你将理解raw_tex扩展的行内解析入口、宏展开(applyMacros)的实现位置,掌握命令测试(command test)文件的书写与运行方法,并能复现验证该测试用例。
1. 测试用例全貌:从 Markdown 到 LaTeX 的往返
4589.md是 pandoc 仓库中典型的**命令测试(command test)**文件。它没有讲解文字,而是用一段"输入 + 期望输出"的黄金样例(golden test)锁定了某个具体行为的正确结果:
% pandoc -f markdown -t latex \newcommand{\one}[1]{#1} \newcommand{\two}[1]{#1} Formatting *is* working **here**. But sticking \one{two }\two{commands} together *breaks* formatting. ^D \newcommand{\one}[1]{#1} \newcommand{\two}[1]{#1} Formatting \emph{is} working \textbf{here}. But sticking two commands together \emph{breaks} formatting.这段内容包含三个层次的信息:
- 命令:首行
%之后是要执行的命令,即pandoc -f markdown -t latex——从 Markdown 读取、以 LaTeX 格式输出; - 标准输入:
^D之前是喂给 pandoc 的输入文档,其中声明了两个 LaTeX 宏\one、\two,并用它们与 Markdown 强调语法混合书写; - 期望输出:
^D之后是本次转换应当产生的标准输出,其中\one{two }与\two{commands}这两个原始命令被"折叠"展开为文本two commands,而两侧的*is*、**here**、*breaks*均被正确转换为\emph{...}与\textbf{...}。
注意一个关键细节:输入中的\one{two }、\two{commands}在输出中消失了,取而代之的是宏展开后的纯文本two commands。这正是本用例要守护的行为——详见下文第 3 节。
1.1 命令测试文件的格式约定
该文件的格式并非随意的,而是由 pandoc 的测试框架 test/Tests/Command.hs 明确定义:
- 第一个代码块首行以
%开头,后面是要运行的命令; - 随后若干行作为命令的 stdin 输入;
- stdin 以单独一行
^D结束; ^D之后的行是期望的 stdout 输出;- 若还期望 stderr 输出,须放在前面并以
2>前缀标记; - 若期望非零退出码,最后一行须形如
=> <exit status>。
因此4589.md本身就是一个可独立运行的回归测试:只要把文件内容中的输入喂给pandoc -f markdown -t latex,再与期望输出逐行比对即可判定通过与否。
2. 背景:这条用例来自 pandoc 2.2 的原始 LaTeX 处理改进
在 changelog.md 中,pandoc 2.2(2018-04-27 发布,见 changelog.md)的 LaTeX 读取器一节明确记录了本次修复的来源:
- Improve handling of raw LaTeX (for markdown etc.) (#4589, #4594). Previously there were some bugs in how macros were handled.
也就是说,4589.md是对 issue #4589(以及相关 PR #4594)所报告缺陷的回归守护:修复之前,Markdown 中对宏的处理存在 bug——具体表现为像\one{two }\two{commands}这样连续使用多个宏时,会干扰紧随其后的 Markdown 强调解析,导致*breaks*无法被识别为斜体。修复之后,输出中的宏被正确展开,强调格式也得以保全,即本用例中\emph{breaks}的正确结果。
3. 底层原理:Markdown 读取器如何识别行内 LaTeX 命令
要理解这个测试为何会失败又为何被修复,需要追踪 Markdown 读取器对\开头的行内内容的解析分派。
3.1\\的解析入口
在 Markdown 读取器 src/Text/Pandoc/Readers/Markdown.hs 的行内元素分派表中,遇到反斜杠时的解析顺序为:
'\\' -> math <|> escapedNewline <|> escapedChar <|> rawLaTeXInline'即依次尝试:数学公式、转义换行、转义字符、原始 LaTeX 行内元素。其中rawLaTeXInline'的实现位于 src/Text/Pandoc/Readers/Markdown.hs:
rawLaTeXInline' :: PandocMonad m => MarkdownParser m (F Inlines) rawLaTeXInline' = do guardEnabled Ext_raw_tex notFollowedBy' rawConTeXtEnvironment !s <- rawLaTeXInline return $ return $ B.rawInline "tex" s -- "tex" because it might be context这段代码说明三件事:
- 该行为受
raw_tex扩展控制(guardEnabled Ext_raw_tex),关闭该扩展后行内 TeX 命令不会被当作原始 LaTeX 解析; - 若命中 ConTeXt 环境(
\start...\stop...)则不按普通行内命令处理; - 解析成功的文本会被包装为
RawInline "tex"元素。
3.2 宏展开发生在哪一层
真正执行宏展开的是 LaTeX 读取器导出的rawLaTeXInline(在 src/Text/Pandoc/Readers/LaTeX.hs):
rawLaTeXInline = do lookAhead (try (char '\\' >> letter)) toks <- getInputTokens raw <- snd <$> ( rawLaTeXParser latexEnv toks (mempty <$ (controlSeq "input" >> skipMany rawopt >> braced)) inlines <|> rawLaTeXParser latexEnv toks (void inline) inlines ) finalbraces <- mconcat <$> many (try (string "{}")) -- see #5439 return $ raw <> T.pack finalbraces它先把输入切成 TeX 记号流(getInputTokens),再用rawLaTeXParser在"行内解析模式"下解析命令,过程中会把已注册的宏定义应用到命令文本上。而宏展开的核心函数applyMacros定义在 src/Text/Pandoc/Readers/LaTeX/Parsing.hs,其行为是:若关闭了latex_macros扩展则原样返回;否则将输入重新分词(tokenize)、在收集到的宏表(sMacros)下重放解析(runParserT retokenize),最后untokenize回文本。
把这条链路串起来:4589.md输入中的\newcommand{\one}[1]{#1}会被 Markdown 读取器识别为块级原始 LaTeX 并注册进宏表;随后行内的\one{two }、\two{commands}经rawLaTeXInline'→rawLaTeXInline→applyMacros被逐一展开为two与commands,最终在 LaTeX 输出中不再出现命令本身,而只留下展开后的文本。修复前的 bug 就在于这一展开与后续*...*强调解析的衔接不完整,导致紧跟命令序列的强调标记被吞掉或破坏。
3.3 输出端:\emph与\textbf从哪来
期望输出中的\emph{is}、\textbf{here}、\emph{breaks}是 pandoc 内部Emph、Strong行内元素在 LaTeX 写入器中的标准渲染结果:*...*对应Emph,**...**对应Strong。本用例验证的正是"原始 LaTeX 命令与 Markdown 强调标记交错出现时,两者都能各归其位"这一完整场景。
4. 验证与复现:把测试跑起来
4589.md属于test/command/目录下的命令测试集,它们由 test/Tests/Command.hs 驱动,与仓库其他测试(test-pandoc.hs、各读取器/写入器测试模块)一起构成整体测试套件。若要手动验证本用例,只需按%行给出的命令执行:
pandoc -f markdown -t latex然后粘贴如下输入并回车、再输入^D:
\newcommand{\one}[1]{#1} \newcommand{\two}[1]{#1} Formatting *is* working **here**. But sticking \one{two }\two{commands} together *breaks* formatting.预期输出应与测试文件^D之后的内容一致。你也可以把这段输入存为input.md后用pandoc -f markdown -t latex input.md验证。若你的环境是 2.2 之前的版本,输出中\emph{breaks}可能残缺或命令序列后出现多余文本——这正是该回归测试存在的意义。
5. 关联阅读:raw_tex扩展与更灵活的raw_attribute
4589.md守护的行为本质上属于raw_tex扩展的范畴。在 MANUAL.txt 中,该扩展被描述为:允许在文档中包含原始 LaTeX、TeX 与 ConTeXt,行内 TeX 命令会被原样保留并透传给 LaTeX 与 ConTeXt 写入器;但行内 LaTeX 在输出为 Markdown、LaTeX、Emacs Org mode、ConTeXt 之外的其他格式时会被忽略。例如:
This result was proved in \cite{jones.1967}.即可在 LaTeX 输出中保留 BibTeX 引用。若需要更显式、更可控地嵌入原始 TeX,可改用raw_attribute扩展(MANUAL.txt 中亦有说明),例如text{...}{=tex}形式的显式原始块/行内元素。
6. 小结
test/command/4589.md虽仅十余行,却浓缩了 pandoc 中一条完整的解析链路:命令测试框架(test/Tests/Command.hs)→ Markdown 行内分派(Markdown.hs)→ 行内原始 LaTeX 解析(LaTeX.hs)→ 宏展开(LaTeX/Parsing.hs)→ LaTeX 写入器。它用黄金样例锁定了 pandoc 2.2 对原始 LaTeX 宏处理缺陷(#4589/#4594)的修复结果,确保此后任何对 Markdown 解析器、LaTeX 读取器或宏系统的改动,都不会再次破坏"Markdown 强调与内联 TeX 命令混排"这一常见写作场景。理解这个用例,也就理解了 pandoc 如何以"注册宏表 + 记号流重放"的方式,在通用 Markdown 语法中安全地嵌入领域特定的 LaTeX 命令。
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考