逐行剖析 Pandoc 的 default.markdown 模板:$body$、$toc$ 与 $include-before$ 的组装机制
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
data/templates/default.markdown是 Pandoc 输出 Markdown 时默认使用的文档级模板,虽然全文只有 21 行,却决定了输出文档的骨架顺序:标题块、头部包含文件、正文前的插入内容、目录、主体内容和正文后的插入内容。读完本文,你将掌握这套 docutils 风格模板语言的语法、每个变量在 Markdown 写出器 中的赋值来源,以及如何用--template等选项定制输出结构。
一、模板文件本体:21 行如何搭起整篇文档
default.markdown 的完整内容如下,它是所有 Markdown 变体写出器共用的“外壳”:
$if(titleblock)$ $titleblock$ $endif$ $for(header-includes)$ $header-includes$ $endfor$ $for(include-before)$ $include-before$ $endfor$ $if(toc)$ $table-of-contents$ $endif$ $body$ $for(include-after)$ $include-after$ $endfor$语法要点:
$if(var) ... $endif$:仅当模板上下文中的变量var非空/为真时输出中间内容,用于可选区块(标题块、目录)。$for(list) ... $endfor$:list是列表变量,循环体内引用的变量(如$header-includes$)逐次替换为列表中每个元素,对应命令行上重复传入的多个-H/-B/-A参数。$var$:直接替换为上下文中该变量的文本值,核心变量是$body$。
该模板由 doc-templates 库编译执行。在 Templates.hs 中,compileTemplate负责解析模板文本,renderTemplate负责把Context(一组命名变量)代入模板得到最终文本;Pandoc 侧的封装见 getDefaultTemplate 与 compileDefaultTemplate。
二、模板如何被选中:一个文件服务全部 Markdown 变体
getDefaultTemplate里有一段分支表,说明了为什么改一个模板文件会同时影响markdown_strict、multimarkdown、gfm之外的所有 Markdown 系输出:
markdown_strict、multimarkdown、markdown_github、markdown_mmd、markdown_phpextra全部回退到templates/default.markdown;gfm、commonmark_x回退到templates/default.commonmark;- 其余写出格式则读取
templates/default.<format>。
见 getDefaultTemplate 的实现。也就是说,default.markdown是 pandoc(PHP Extra 超集)、Multimarkdown、MMD、GitHub 兼容 markdown 这几个变体共同的默认模板。
模板的查找并非只读数据目录。getTemplate 的逻辑是:先在本地文件系统(或通过fetchItem)按--template给定的路径查找,找不到(PandocResourceNotFound或文件不存在)才回退到内置数据文件templates/<文件名>。这正是-o out.md --template=my-template.md能覆盖默认模板的底层原因;模板定制的一般思路另见 doc/customizing-pandoc.md。
三、变量逐个对账:模板里每个占位符由谁填充
模板变量统一在pandocToMarkdown中构造上下文,再交给renderTemplate。关键代码在 Markdown.hs 第 222–277 行。下面按模板中出现顺序逐一说明。
3.1$if(titleblock)$ / $titleblock$:标题块的四种形态
上下文构造处有一个关键判断:
(if isNullMeta meta then id else defField "titleblock" titleblock)即只有当文档元数据(title/author/date 等)非空时,titleblock变量才会被放入上下文,模板中$if(titleblock)$分支才生效。而titleblock本身的具体格式取决于写出选项与启用的扩展,源码中是一个四路分支(第 235–245 行):
| 条件 | 生成函数 | 输出形态 |
|---|---|---|
变体为 PlainText(-t plain) | plainTitleBlock | 标题/作者/日期各占一行,作者以;连接 |
启用yaml_metadata_block | yamlMetadataBlock | ---包裹的 YAML 元数据块,键按忽略大小写排序 |
启用pandoc_title_block | pandocTitleBlock | Pandoc 经典% 标题、% 作者、% 日期三行 |
启用mmd_title_block | mmdTitleBlock | 键: 值形式,多值用;连接,长值续行缩进 |
| 以上均不满足 | — | 不输出标题块(empty) |
实现分别见 pandocTitleBlock / mmdTitleBlock / plainTitleBlock / yamlMetadataBlock。YAML 形态还有一个细节:valToYaml会把yes/no/true/null/~等特殊字符串以及以0开头或形似浮点数的值加上双引号,避免 YAML 语义漂移(第 170–219 行)。
3.2$for(header-includes)$:--include-in-header
header-includes是列表变量,对应--include-in-header(-H)选项:每次传入的文件/URL 内容追加进列表,模板循环把每份内容逐字输出在正文之前,常用于注入 HTML 注释、CSS 或 Markdown 扩展语法。
3.3$for(include-before)$:-B/--include-before-body
include-before对应-B FILE, --include-before-body,MANUAL.txt 第 1051 行 有定义。其内容原样插入目录之前、正文之前,适合放置自定义前言或静态头部片段。
3.4$if(toc)$ / $table-of-contents$:目录区块
对应--toc/--table-of-contents选项(MANUAL.txt 第 932 行),目录深度由--toc-depth控制。源码中有两处值得注意的实现细节:
- 目录内容是真正渲染成 Markdown 的块:
toc变量取自toTableOfContents opts blocks的结果,再经blockToMarkdown转成 Markdown 标题列表(第 246–254 行)。 toc变量存的是目录内容而非布尔值:出于向后兼容,defField "toc" toc与defField "table-of-contents" toc都被填成了目录的渲染结果(源码注释明确写了 "for backwards compatibility we populate toc with the contents of the toc, rather than a boolean",见 第 264–268 行)。因此旧模板中if(toc)判断的其实是“目录内容是否非空”。- 若未启用
link_attributes/attributes扩展,目录中的链接会被剥掉属性再输出,保证生成的锚点链接在各变体中可解析。
3.5$body$:正文 + 脚注 + 参考文献
body变量并非只有正文块:
body <- blockListToMarkdown opts blocks' notesAndRefs' <- notesAndRefs opts let main = body <> notesAndRefs'notesAndRefs(第 346–362 行)在正文后追加脚注([^1]:形式,缩进由writerTabStop决定)与链接引用定义,并受writerReferenceLocation(文末/节末/块末)影响;启用citations时还会剥掉末尾由引用处理生成的refsDiv(第 255–260 行)。另外pandocToMarkdown在渲染前用fixBlocks对块序列做防御性修补,例如在“列表 + 缩进代码块”之间插入 HTML 注释分隔,防止代码块被误读为列表延续项(第 893–945 行)。
3.6$for(include-after)$:-A/--include-after-body
对应-A FILE, --include-after-body(MANUAL.txt 第 1064 行),在$body$(含脚注)之后原样输出,常用于附录、许可声明等尾部内容。注意模板里循环体先输出一个空行再输出内容,保证尾部片段与前文有空白行分隔。
四、渲染管线:从 AST 到成文
把上面各节串起来,pandocToMarkdown 的完整流程是:
- 从
WriterOptions取元数据并转成模板上下文metadata,提取title/author/date; - 按变体与扩展选择标题块格式(第三节 3.1 的四路分支);
- 如启用
--toc,生成并 Markdown 化目录; - 将正文块列表渲染为 Markdown,拼上脚注与引用,得到
main; - 用
defField链依次注入toc、table-of-contents、body、titleblock,再叠加addVariablesToContext把全部元数据字段($title$、$author$等)挂进上下文——这意味着自定义模板里还可以直接使用$title$、$date$及-V传入的任意变量; - 最后按模板渲染:
case writerTemplate opts of Nothing -> main Just tpl -> renderTemplate tpl context即:不指定模板时只输出main(正文+脚注+引用),标题块、-B/-A/-H插入内容全部被丢弃;使用默认default.markdown模板时才会得到本文开头展示的完整骨架。这也是为什么-t markdown的完整输出依赖模板,而writePlain、writeCommonMark、writeMarkua等入口函数虽然共用pandocToMarkdown,却各自以不同MarkdownVariant(Markdown/Commonmark/Markua/PlainText)切换细节渲染规则(writeMarkdown 等入口)。
五、验证与延伸
- 模板与写出器的行为有对应的黄金文件测试:Markdown 写出器的期望输出保存在 test/writer.markdown,各写出器测试逻辑位于 test/Tests/Writers/,可直接对照模板变量与最终文本的对应关系。
- 若需改变骨架顺序(例如把目录移到正文后、或在标题块前加 Logo 注释),复制 default.markdown 到项目内、调整占位符顺序后用
--template指向它即可;getTemplate的查找与回退逻辑保证了自定义文件缺失时仍能退回内置模板而不是直接报错。 - 模板语言本身由依赖库 doc-templates 实现,Pandoc 仅通过 Templates.hs 暴露
compileTemplate/renderTemplate/getTemplate三个入口,partial 文件({...}语法)在默认写出流程中限定从内置templates/目录读取(WithDefaultPartials实例,第 67–72 行)。
综上,default.markdown是 Pandoc “模板驱动输出”这一架构在 Markdown 家族上的最小完整样本:五个占位区块、一种for/if语法,配合写出器里明确定义的上下文填充顺序,构成了从 AST 到成文 Markdown 的最后一步。理解它,也就理解了 Pandoc 所有其他格式模板(default.latex、default.html5等)的共性结构。
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考