Pandoc 原始 LaTeX 解析边界探秘:\start命令的 raw_tex 处理与 Markdown 读取器解析逻辑
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
导读
在 Pandoc 的 Markdown 读取器中,\start开头的原始 TeX 命令长期存在一个微妙的解析边界问题:它们既可能是普通的 LaTeX/TeX 命令,也可能是 ConTeXt 环境的起始标记(如\starttext),后者必须与\stop成对出现。位于test/command/3558.md的回归测试记录了 Pandoc 修复此问题的完整过程——从错误地将\startmulti当作 ConTeXt 环境吞掉,到修复后正确产出RawBlock/RawInline节点。阅读本文后,你将理解raw_tex扩展在 Markdown 输入中的工作原理、rawConTeXtEnvironment的匹配策略,以及如何用pandoc -t native验证原始 TeX 内容的解析结果。
测试文件解读:一个最小化的解析回归用例
测试内容与预期输出
test/command/3558.md是 Pandoc 的命令式测试(command test)文件,其格式约定为:以```包裹 shell 会话,% pandoc -t native后的内容是命令行,^D标志输入结束,其后的行是期望的输出(native 表示 Pandoc 内部 AST 的 Haskell 表示):
% pandoc -t native \multi hello \endmulti ^D [ RawBlock (Format "tex") "\\multi" , Para [ Str "hello" ] , RawBlock (Format "tex") "\\endmulti" ]测试内容本身非常简单:输入三段内容——\multi、空行、hello、空行、\endmulti,期望的输出是三个 AST 节点:两个RawBlock (Format "tex")包裹的原始 LaTeX 块,以及中间的普通段落Para [Str "hello"]。即\multi和\endmulti被当作独立的原始 TeX 块原样保留,中间的hello按普通 Markdown 段落解析。
注意\multi是一个并不真实存在的 LaTeX 宏——这正是测试的关键所在:解析器必须能处理任意\开头的控制序列,而不要求其是已知命令。同时,\multi与 ConTeXt 环境起始标记\start...的命名约定不同,这里恰好用了一个边界附近的命令名。
测试背后的历史:issue #3558
通过 changelog 可以还原此测试的来龙去脉。changelog.md 中 2.0 版本的 Markdown reader 条目记录道:
Allow raw latex commands starting with
\start(#3558). Previously these weren't allowed because they were interpreted as starting ConTeXt environments, even without a corresponding\stop...
也就是说,修复前的问题是:任何以\start开头的命令都会被预先拦截并解释为 ConTeXt 环境的开始,即使后面根本没有配对的\stop。例如\startmulti单独出现时,解析器会试图寻找\stopmulti将中间内容整体吞入,导致命令无法作为普通 raw TeX 被保留。这正是本测试用\multi(不触碰\start前缀)来验证修复效果的原因——它证明了解析器已经不会再对start前缀做一刀切的假设。
raw_tex扩展:Markdown 中内嵌原始 TeX 的入口
扩展的语义
在 Pandoc 的 Markdown 语法中,原始 LaTeX/TeX/ConTeXt 的透传由raw_tex扩展控制。其默认状态与markdown变体的差异详见 MANUAL.txt 的扩展表:raw_tex允许在 Markdown 源文档中直接内嵌原始 LaTeX、TeX 与 ConTeXt 代码。MANUAL 对该扩展的解释(Extension:raw_tex)要点如下:
行内 TeX 命令会被原样保留,并在输出到 LaTeX、ConTeXt 等目标格式时透传,例如:
This result was proved in \cite{jones.1967}.对于
\begin{...}与\end{...}包裹的 LaTeX 环境,环境内部的内容整体按原始 LaTeX 解释,不再当作 Markdown 解析。行内 LaTeX 在输出到非 Markdown、LaTeX、Emacs Org mode、ConTeXt 的格式时会被忽略。
更显式、更灵活的替代方案是
raw_attribute扩展,可用 {=latex} ``` 围栏代码块或{=latex}行内属性显式标记原始内容(Extension:raw_attribute)。
raw_tex扩展关闭时(例如使用-raw_tex禁用),文档中的 TeX 命令不再被识别为 raw,可能退化为普通文本,因此在跨格式转换时(如转 docx)需要注意此扩展对输出内容的影响。
块级与行内级两条解析路径
raw_tex在 Markdown 读取器中对应两个入口函数,均位于 src/Text/Pandoc/Readers/Markdown.hs:
- 块级路径
rawTeXBlock(Markdown.hs#L1161-L1172):先guardEnabled Ext_raw_tex检查扩展是否启用,然后尝试用rawConTeXtEnvironment或rawLaTeXBlock匹配一行或多行原始 TeX,最终以B.rawBlock "tex"生成块节点。若匹配结果全是空白字符,则返回空块(不产生无意义的空 RawBlock)。 - 行内路径
rawLaTeXInline'(Markdown.hs#L2139-L2144):同样先检查Ext_raw_tex,随后调用rawLaTeXInline匹配单个行内命令,结果以B.rawInline "tex"生成行内节点。
两个函数都统一使用"tex"作为格式名——源码注释明确说明这是因为匹配到的内容“might be context”(可能是 ConTeXt 而非纯 LaTeX),所以Format "tex"是一个涵盖 LaTeX、TeX 与 ConTeXt 的通用格式标签。这与测试期望输出中的RawBlock (Format "tex")完全吻合。
修复的核心:rawConTeXtEnvironment的精确匹配
修复前的缺陷
从提交12ae1df5b("Allow raw latex commands starting with\startin Markdown",2017-04-06)的 diff 可以看到,修复前的rawLaTeXInline'使用了如下过于宽泛的拦截逻辑:
rawLaTeXInline' = try $ do guardEnabled Ext_raw_tex lookAhead $ char '\\' >> notFollowedBy' (string "start") -- context env RawInline _ s <- rawLaTeXInline ...即:只要看到反斜杠后紧跟start四个字母,就直接判定为 ConTeXt 环境并拒绝按普通行内 raw 处理——即使后续内容并非合法的\startXXX环境形式。修复后的代码改为:
rawLaTeXInline' = try $ do guardEnabled Ext_raw_tex lookAhead (char '\\') notFollowedBy' rawConTeXtEnvironment RawInline _ s <- rawLaTeXInline ...差别在于:不再用字符串前缀做粗粒度判断,而是先lookAhead确认以反斜杠开头,再通过notFollowedBy' rawConTeXtEnvironment做结构化的负向前瞻——只有当后续输入确实能被rawConTeXtEnvironment完整匹配时,才认为这是 ConTeXt 环境并拒绝行内 raw 解析。
rawConTeXtEnvironment的匹配规则
rawConTeXtEnvironment定义于 Markdown.hs#L2146-L2153:
rawConTeXtEnvironment :: PandocMonad m => ParsecT Sources st Text m Text rawConTeXtEnvironment = try $ do string "\\start" completion <- inBrackets (letter <|> digit <|> spaceChar) <|> takeWhile1P isLetter !contents <- manyTill (rawConTeXtEnvironment <|> countChar 1 anyChar) (try $ string "\\stop" >> textStr completion) return $! "\\start" <> completion <> T.concat contents <> "\\stop" <> completion其匹配策略是成对匹配:
- 要求字面量
\start; - 环境名
completion可以是方括号包裹的任意字母/数字/空格序列,或连续的字母串; - 之后必须出现
\stop加上相同的环境名才闭合(且允许嵌套环境递归匹配); - 整个结构必须完整闭合,
rawConTeXtEnvironment才匹配成功。
因此在修复后的逻辑中:
\starttext ... \stoptext:能被rawConTeXtEnvironment完整匹配,走 ConTeXt 环境路径;\startmulti后没有\stopmulti:rawConTeXtEnvironment匹配失败,notFollowedBy'通过,于是\startmulti落回rawLaTeXInline按普通 raw 命令处理。
这也是 3558 测试选择\multi的原因——它验证了修复不再对start前缀“一刀切”,同时\multi本身又刻意不用\start前缀,确保测试结果不受 ConTeXt 环境匹配器的干扰,聚焦于“未知命令可被原样保留”这一基本能力。
与 LaTeX 读取器的协作:rawLaTeXBlock与rawLaTeXInline
Markdown 读取器中的 raw 解析并非全部自己实现,而是大量复用了 LaTeX 读取器(src/Text/Pandoc/Readers/LaTeX.hs)中的底层解析器,在文件头部即可看到导入语句:
import Text.Pandoc.Readers.LaTeX (applyMacros, rawLaTeXBlock, rawLaTeXInline)这两个函数的定义在 LaTeX.hs#L164-L219:
rawLaTeXBlock:先lookAhead确认以\加字母开头,然后对输入做分词(getInputTokens),优先尝试识别\include、\input、\subfile、\usepackage等文件级命令及宏定义(这些被消费但不产出内容),否则尝试匹配environment或blockCommand,再将后续内容交给块解析器继续消费。可见它对“已知命令列表”与“环境”有明确区分。rawLaTeXInline:类似地处理行内命令,额外会补全命令后跟随的空花括号{}(源码注释提到#5439相关的边界情况)。
值得注意的是 LaTeX.hs#L1050-L1055 中rawMaybeBlock的一段注释,明确回应了 #3558 的修复:
...But we stop if we hit a
\startXXX, since this might start a raw ConTeXt environment (this is important because this parser is used by the Markdown reader).
即:在“块级命令连续出现”的启发式扫描中,一旦遇到\startXXX就停止继续按块命令收集,因为其后可能开启一个原始 ConTeXt 环境。对应实现为startCommand守卫:guard $ "start"T.isPrefixOfn。这说明修复后的策略在 LaTeX 读取器中同样生效:\start前缀的命令不再被无条件当作普通块命令吞并,而是给 ConTeXt 环境路径留出判断空间。
在真实环境中验证:命令运行与结果对照
复现测试预期
在安装了 pandoc 的环境中,可直接复现测试文件中的命令。使用与测试完全一致的输入:
printf '\\multi\n\nhello\n\n\\endmulti\n' | pandoc -t native期望输出与test/command/3558.md中记录的完全一致:
[ RawBlock (Format "tex") "\\multi" , Para [ Str "hello" ] , RawBlock (Format "tex") "\\endmulti" ]反例验证:真正的 ConTeXt 环境
为了对照,再看一个能被rawConTeXtEnvironment完整匹配的输入:
printf '\\starttext\nhello\n\\stoptext\n' | pandoc -t native此时\starttext ... \stoptext作为闭合的 ConTeXt 环境被整体保留,输出中会呈现一个包含完整环境内容的RawBlock (Format "tex")。两相对照即可直观看到:#3558 修复的边界在于“无\stop配对的\start前缀命令应回退为普通 raw 命令”,而有完整配对的 ConTeXt 环境依旧走环境路径。
输出到 LaTeX 时的透传效果
RawBlock (Format "tex")节点在-t latex输出时会被原样透传。因此上述 Markdown 文档转换为 LaTeX 后,\multi与\endmulti会逐字出现在输出中,这保证了混合 Markdown + 原始 TeX 文档在面向 LaTeX/PDF 工作流中的无损往返;而输出到其他格式(如 HTML)时,这些 raw tex 块会被丢弃,体现 MANUAL.txt 中“行内 LaTeX 在非 TeX 类格式中被忽略”的规则。
测试框架视角:command test 的编写约定
test/command/3558.md采用 Pandoc 的 command test 格式,这类测试由 test/test-pandoc.hs 驱动执行。其约定为:
- 代码块内首行
% pandoc ...是待执行的命令行; - 以
^D标志标准输入结束; ^D之后至代码块结束的内容是期望输出,逐字符比较(diff)判定通过与否。
这种格式特别适合回归测试:它把“命令 + 输入 + 期望输出”封装成一个自包含的用例,任何解析器行为变化都能被立即捕获。3558.md正是借此把“\start前缀命令的 raw 处理”固化为永久回归防线——即便未来重构 Markdown 或 LaTeX 读取器,此用例也会持续验证该行为不被破坏。同目录下的 test/command/ 中还有大量同类编号用例(如3558.md附近的3558前后编号文件),共同构成 Pandoc 行为回归测试的庞大语料库。
小结:本次修复带来的行为边界
综合测试、源码与 changelog 三条证据链,可以得到关于\start前缀命令的清晰结论:
- 有完整
\stop配对的结构(如\starttext ... \stoptext)仍被识别为 ConTeXt 环境,整体作为原始内容保留,格式标签为"tex"; - 无配对的
\start前缀命令(如\startmulti)在 #3558 修复后不再被误判为环境,而是回退为普通 raw TeX 命令,产出RawBlock/RawInline (Format "tex")节点; - 判断依据从“字符串前缀
start”升级为“结构化匹配rawConTeXtEnvironment”,该逻辑同时作用于 Markdown 读取器的块级(rawTeXBlock)与行内(rawLaTeXInline')路径,并在 LaTeX 读取器的rawMaybeBlock启发式中同步生效。
这一案例同时展示了 Pandoc 的一个工程习惯:解析边界问题用最小化回归测试固化。test/command/3558.md虽然只有 8 行,却精准覆盖了 raw TeX 解析中最容易出错的 ConTeXt 环境判定逻辑,是理解raw_tex扩展行为的最佳起点。
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考