news 2026/9/20 2:59:33

pandoc LaTeX 阅读器宏展开与 figure 环境解析:基于 test/command/2118.md 的源码级剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pandoc LaTeX 阅读器宏展开与 figure 环境解析:基于 test/command/2118.md 的源码级剖析
  • 文档
  • 开发工具
  • CLI

【免费下载链接】pandoc

Universal markup converter

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

导读

pandoc 的 LaTeX 阅读器不仅要处理常规的 LaTeX 命令与环境,还必须支持\newcommand等宏定义,并正确理解figure等浮动环境的结构。test/command/2118.md正是这样一个端到端验证用例:它在同一份 LaTeX 文档中定义了一个图片宏\inclgraph,随后在figure环境中使用该宏,最终期望输出一个带latex-placement属性与 80% 宽度的Figure块。本文以该测试用例为线索,逐层讲解宏定义如何被捕获与展开、figure环境如何被解析为Figure块,以及\includegraphics的宽度选项如何被转换为 pandoc 的内部表示,让读者既能复现测试,也能理解背后的实现原理。

一、测试用例全景:从 LaTeX 输入到 Native 输出

test/command/2118.md是一个典型的 pandoc 命令测试(command test)文件,全文只有一个代码块,完整内容如下:

% pandoc -f latex -t native \newcommand{\inclgraph}{\includegraphics[width=0.8\textwidth]} \begin{figure}[ht] \inclgraph{setminus.png} \caption{Set subtraction} \label{fig:setminus} \end{figure} ^D [ Figure ( "fig:setminus" , [] , [ ( "latex-placement" , "ht" ) ] ) (Caption Nothing [ Plain [ Str "Set" , Space , Str "subtraction" ] ]) [ Plain [ Image ( "" , [] , [ ( "width" , "80%" ) ] ) [] ( "setminus.png" , "" ) ] ] ]

测试通过pandoc -f latex -t native把 LaTeX 输入转换为 pandoc 的 Native(AST 文本表示)输出。这个用例一次性覆盖了 LaTeX 阅读器中三个核心机制:

  1. 宏定义与展开\newcommand{\inclgraph}{...}定义了一个无参宏,其展开内容是一个带可选参数的\includegraphics调用;
  2. figure 浮动环境解析:环境头部可选参数[ht]被提取为latex-placement属性,\caption内容成为Figure块的标题,\label成为块的标识符;
  3. 图片尺寸换算width=0.8\textwidth被转换为width="80%",即基于\textwidth的相对宽度被换算为百分比。

预期输出中,Figure块由figure'解析器构造,标题内图片的Plain [Image ...]结构则说明阅读器会把原本包裹图片的段落层(用于承载标题)剥掉,这正是 figure' 解析器 中go (Para [Image attr [Str "image"] target]) = Plain [Image attr [] target]这一转换规则的作用:去掉Image内部的占位文本与多余的Para包装。

二、命令测试的格式约定:理解 2118.md 的骨架

test/command/2118.md属于 pandoc 的 golden 测试体系,其格式约定定义在 test/Tests/Command.hs 中:

  • 第一行以%开头,后面是要执行的命令行(这里是pandoc -f latex -t native);
  • 之后是作为标准输入传给命令的内容(LaTeX 源码);
  • 输入以单独一行^D结束;
  • ^D之后的内容是期望的标准输出(这里是 Native 格式的 AST);
  • 如果还期望 stderr 输出,需以2>前缀放在 stdout 期望之前;如果期望非零退出码,最后一行应以=>开头跟上退出码。

测试运行时,runCommandTest 会把%后的命令交给execTest执行(实际会替换为test-pandoc --emulate,见 pandocToEmulate),然后与文件中的期望输出做 golden 比较。因此 2118.md 本质上是一个可重复运行的回归测试:只要 LaTeX 阅读器对宏与 figure 的处理行为发生变化,该用例就会立即失败并给出 diff。

三、宏定义解析:\newcommand是如何被捕获的

3.1 macroDef:宏定义的分发入口

在 LaTeX 阅读器中,所有宏定义都由 src/Text/Pandoc/Readers/LaTeX/Macro.hs 模块统一处理。入口函数macroDef(Macro.hs#L24-L51)先通过peekTok检查下一个控制序列是否属于macroDefCommands(Macro.hs#L56-L71)集合,只有命中才继续解析,否则快速失败、避免干扰普通命令。该集合覆盖了:

  • 经典 TeX 命令:\def\gdef\edef\xdef\let\newif\global
  • 标准 LaTeX 命令:\newcommand\renewcommand\providecommand\DeclareMathOperator\DeclareRobustCommand
  • LaTeX3/xparse 系列:\NewDocumentCommand\RenewDocumentCommand\ProvideDocumentCommand及其可展开变体、\NewDocumentEnvironment等;
  • 环境定义:\newenvironment\renewenvironment\provideenvironment

2118.md 中的\newcommand正是由这里分发到newcommand解析器的。macroDef还有一点值得注意:解析过程使用withRaw捕获原始 token 流,如果latex_macros扩展被禁用(guardDisabled Ext_latex_macros),宏定义不会被注册到状态中,但仍会以 RawBlock 形式保留原样输出。

3.2 newcommand:参数个数、可选参数与内容捕获

newcommand解析器位于 Macro.hs#L187-L222,同时处理\newcommand\renewcommand\providecommand\DeclareMathOperator\DeclareRobustCommand五种命令。其解析流程:

  1. withVerbatimMode进入逐字模式,避免在定义阶段就展开宏内容(宏应在使用时展开,而非定义时);
  2. 读取被定义的控制序列名,支持\foo{\foo}两种写法,也支持带星号的\newcommand*变体;
  3. 通过bracketedNum解析可选的参数个数([n]),生成map ArgNum [1..n]的参数规格argspecs
  4. 通过bracketedToks解析可选的默认参数[default],存为optarg
  5. bracedOrToken捕获宏体contents'

最终构造Macro GroupScope ExpandWhenUsed argspecs optarg contents(Macro.hs#L216):ExpandWhenUsed表示延迟到使用点展开,GroupScope表示作用域限于当前分组(\global可提升为GlobalScope)。

对于 2118.md 中的\newcommand{\inclgraph}{\includegraphics[width=0.8\textwidth]}

  • 宏名为inclgraph
  • 无参数(numargs = 0)、无默认可选参数;
  • 宏体是\includegraphics[width=0.8\textwidth]这条 token 序列。

后续遇到\inclgraph{setminus.png}时,阅读器会把宏体展开为\includegraphics[width=0.8\textwidth]{setminus.png}再继续解析,最终落入\includegraphics的处理逻辑。这正是本测试用例的核心验证点:宏定义必须在使用前被捕获,且展开必须在解析普通命令之前完成

另外注意 Macro.hs#L217-L222 的重定义语义:若宏已存在,\providecommand静默忽略,\renewcommand允许覆盖,而\newcommand会输出MacroAlreadyDefined日志消息并放弃定义。

四、figure 环境解析:placement、caption 与 label 的归宿

4.1 figure' 解析器

\begin{figure}[ht]由 LaTeX.hs#L1233-L1235 注册的命令表映射到figure'解析器(LaTeX.hs#L1400-L1427)。figure'的执行步骤:

  1. poshint <- option "" $ untokenize <$> bracketedToks:解析环境头部的可选参数[ht],得到字符串"ht"
  2. resetCaption+ 遍历内部内容:用label解析器捕获\labelsLastLabel状态,用block解析器解析正文;\caption会把标题写入sCaption状态;
  3. 从状态中取出caption'sCaption)与mblabelsLastLabel);
  4. 构造属性kvs = [("latex-placement", poshint) | not (T.null poshint)]——即只有显式写了放置参数时才生成该属性,这正是预期输出中("latex-placement" , "ht")的来源;
  5. 若存在\label,将其登记到sLabels,供\ref/\autoref交叉引用解析使用(编号由sLastFigureNum维护);
  6. 最终return $ B.figureWith attr caption' content构造Figure块。

所以 2118.md 期望输出中的Figure ("fig:setminus", [], [("latex-placement", "ht")])三个字段分别来自:\label的标识符、空的 classes、[ht]放置参数。latex-placement属性在后续通过 LaTeX 写入器输出时,会还原为\begin{figure}[ht],实现转换的往返保真。

4.2 caption 的归一化

\caption{Set subtraction}的标题文本经阅读器解析后成为Caption Nothing [Plain [Str "Set", Space, Str "subtraction"]]——短标题参数[...]空缺时为Nothing。注意图片本身不再重复包含标题文字:figure'中的go函数会把Para [Image attr [Str "image"] target]改写为Plain [Image attr [] target],即删除图片内部的占位Str "image"文本,因为标题已经由Caption承载(LaTeX.hs#L1424-L1427)。

五、\includegraphics 与尺寸换算:0.8\textwidth 如何变成 80%

宏展开后,\includegraphics[width=0.8\textwidth]{setminus.png}由 LaTeX.hs#L451-L453 处理:

("includegraphics", do options <- option [] keyvals src <- bracedFilename mkImage options . unescapeURL $ src)

keyvals解析[width=0.8\textwidth]得到[("width", "0.8\\textwidth")],随后交给mkImage(LaTeX.hs#L245-L257)。mkImage的关键逻辑:

Just (num, "\\textwidth") -> (k, showFl (num * 100) <> "%") Just (num, "\\linewidth") -> (k, showFl (num * 100) <> "%")

当宽高值是\textwidth\linewidth的倍数时,0.8 * 100 = 80,得到width="80%";随后只保留widthheight两个键。这解释了预期输出中的Image ("", [], [("width", "80%")]) [] ("setminus.png", ""):一个无标识符、无 classes、带 80% 宽度属性的图片,指向setminus.png。此外还支持\columnwidth\paperwidth等同类相对单位(换算为百分比),以及height键的同类处理;非相对单位(如width=5cm)则直接保留原始值。

六、实战演练与扩展

6.1 复现测试

在构建好 pandoc 后,进入test/command/目录(该测试的工作目录约定),将 2118.md 中的 LaTeX 部分存为输入文件,执行:

pandoc -f latex -t native input.tex

即可得到与^D之后完全一致的 Native 输出;也可以通过项目测试框架单独运行该用例:

cabal test pandoc --test-options='-p Command'

-p Command会运行 Tests.Command.tests 注册的所有命令测试(每个test/command/*.md对应一个用例,2118 即其中之一),这是验证宏与 figure 行为的最佳回归手段。

6.2 常见变体:带参宏、可选参数与 renewcommand

将 2118 的思路推广,LaTeX 阅读器对以下写法同样支持(对应newcommand解析器的参数处理):

% 带一个必选参数与一个默认可选参数 \newcommand{\mypic}[2][0.5]{\includegraphics[width=#1\textwidth]{#2}} % 覆盖已有定义 \renewcommand{\inclgraph}[1]{\includegraphics[width=0.9\linewidth]{#1}}

其中[n]声明参数个数并生成ArgNum 1..n规格,[default]提供可选参数默认值;宏体内可用#1#2引用参数。使用点的解析会先按参数规格抓取参数,再做宏体展开,最后才交给普通命令解析器。

6.3 注意事项

  • 宏必须在使用之前定义;宏定义与figure环境一样受latex_macros扩展控制,可通过-f latex+latex_macros显式启用(默认启用);
  • \graphicspath会通过setResourcePath追加资源搜索路径(LaTeX.hs#L1437-L1442),转换含相对路径图片的文档时可用于调整查找目录;
  • Figure块与latex-placement属性是 LaTeX 阅读/写出往返的桥梁:LaTeX 写入器会依据latex-placement还原\begin{figure}[...],保证转换不丢失浮动放置语义。

结语

test/command/2118.md虽小,却串联起 LaTeX 阅读器中宏定义捕获(Macro.hs)、宏体展开、figure 环境解析、caption/label 状态管理以及图片尺寸归一化(LaTeX.hs)整条链路。阅读它,等于同时读懂了 pandoc 处理 LaTeX 浮动体与自定义宏的两套核心机制;需要继续深入时,可以对照 Tests.Command 的 golden 测试框架,在test/command/目录下找到更多覆盖不同语法的同类用例。

  • 文档
  • 开发工具
  • CLI

【免费下载链接】pandoc

Universal markup converter

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

相关推荐

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 2:57:52

KWIC系统:四种经典软件体系结构风格实战对比

简介&#xff1a;本资源是一份面向软件工程专业高年级学生与架构初学者的体系结构风格实践分析材料&#xff0c;聚焦KWIC关键词索引系统这一经典教学案例&#xff0c;系统对比数据流、调用/返回、仓库和独立构件四类核心架构风格的设计实现差异与适用边界。PDF文档完整覆盖实验…

作者头像 李华
网站建设 2026/9/20 2:56:19

MiniMax H3本地部署实战:从零搭建AI视频生成环境

如果你混过AI视频生成的圈子&#xff0c;应该发现最近有个词频繁出现&#xff1a;Minmax H3。有人写成MiniMax H3&#xff0c;也有人直接叫H3&#xff0c;绕来绕去指的都是MiniMax开源的那套视频生成模型。标题里用“Minmax”是我故意保留的写法&#xff0c;因为社区里这么搜反…

作者头像 李华
网站建设 2026/9/20 2:55:52

Win11光标卡顿深度排查:从输入延迟到DWM渲染链路优化

1. 从一次鼠标“发飘”说起&#xff1a;Win11 光标卡顿到底卡在哪先说结论&#xff1a;如果你在 Win11 下遇到鼠标光标间歇性卡顿、拖影、掉帧&#xff0c;八成不是鼠标坏了&#xff0c;而是系统某个环节在“抢时间”。我这次排查了整整三天&#xff0c;从硬件换到驱动、从注册…

作者头像 李华