题记与说明
主题:Pandoc 与通用文档转换:AST 模型、过滤器机制、学术写作管线
精读日期:2026-10-31 | 类型:主题研究(检索 5 个来源并精读 + 本地实测)
精读员:knowledge-p5j2vm4l-1
0. 来源清单
| # | 来源 | 类型 | 角色 |
|---|---|---|---|
| S1 | Twenty Years of Pandoc(John MacFarlane, 2026-08-02) | 一手回顾 | 设计动机、架构演化史、Haskell 选型反思、LLM 对比 |
| S2 | Pandoc filters(官方文档) | 一手文档 | JSON 过滤器机制、AST 结构、--filter 调用细节 |
| S3 | Pandoc Lua Filters(官方文档) | 一手文档 | 内嵌 Lua 过滤器、遍历模型、性能对比、完整 API |
| S4 | pandoc-scholar(GitHub README) | 项目文档 | 学术出版工具链、元数据增强、JSON-LD 语义输出 |
| S5 | 本地 pandoc 3.9.0.2 实测(Windows,C:\Program Files\Pandoc\pandoc.exe) | 实证 | AST/JSON/Lua 过滤器/citeproc/格式数量五项验证 |
辅助线索:Wikipedia: Pandoc、GitHub issue #2640(JSON AST 携带版本号以保过滤器兼容)、Awesome Scientific Writing(学术写作工具索引)。
1. 核心问题(problem)
通用文档转换要解决的根本矛盾是:文档格式的组合爆炸。N 种格式两两互转需要 O(N²) 个转换器,且每种格式的解析、渲染逻辑会互相纠缠。具体到 MacFarlane 的处境(2006 年,一位需要讲义和论文的哲学教授):Markdown、reStructuredText、HTML、LaTeX 之间的互转靠正则替换脚本,脆弱且不可扩展。
更深一层的子问题有三:
- 语义保真问题:正则直接改文本会被注释、代码块、转义符骗到(S2 的开篇例子:
## My heading ##、HTML 注释里的##、已含星号的文本加斜体会变粗体); - 可定制性问题:转换器内置的固定行为无法满足长尾需求(如生成本地讲义、WordPress 专用 LaTeX、 Ruby 注音);
- 学术写作的管线问题:写作(Markdown/org)、引用管理(BibTeX/CSL)、排版(LaTeX)、提交(Word/PDF/EPUB)各环节格式割裂。
Pandoc 的回答:用一个中间表示(AST)把"解析"与"渲染"解耦——N 个 reader + M 个 writer 即得 N×M 种转换;把 AST 暴露为可编程接口(过滤器),让定制发生在解析与渲染之间而非文本层面。
2. 主要主张(claim)
- AST 是正确的抽象层。S1:*“It parsed the Markdown using parser combinators and produced a real abstract syntax tree (AST), which it could then render to HTML or another format. This was a more reliable architecture (avoiding many quirks of the regex versions). It was also a more extensible one: by writing N parsers (“readers”) and M renderers (“writers”), one could support N × M conversions.”*——同一段话把可靠性和可扩展性都归于 AST 架构。
- 转换不是保真排版而是保真语义。Pandoc 的目标是保留文档结构含义(标题、强调、引用、脚注),而非像素级格式还原;这正是 AST 能在 51 种输入格式间共享的原因。
- 过滤器机制把 AST 变成生态接口。S2 定义了管线:
INPUT --reader--> AST --filter--> AST --writer--> OUTPUT。JSON 过滤器用任意语言写;Lua 过滤器(2.0 起)内嵌解释器,无外部依赖且更快。 - Haskell 的类型系统是这种架构可行的工程保障。S1:“代数数据类型给出干净的结构化文档表示;强类型系统让大改动不破坏东西;纯函数性使
--sandbox能形式化保证 reader/writer 不碰文件系统。” - 面向未来:确定性转换仍是刚需。S1 对比 LLM 转换给出三点:生态成本低(能耗)、输出确定可预测、(目前)更可靠;但也承认边界:*“a time may come when LLMs can produce more reliable conversions than pandoc or anything that works like it.”*
3. 方法(method)
3.1 AST 模型
- AST 定义在
pandoc-types包的Text.Pandoc.Definition模块(S2)。文档 =Meta+[Block];Block/Inline 是代数数据类型(Header、Para、Emph、Math、Note、RawBlock等,完整清单见 S3 的 type reference)。 - 原生展示格式
pandoc -t native直接输出 Haskell 构造器形式;pandoc -t json输出机器可读 AST,JSON 头部带pandoc-api-version版本戳(本地实测输出[1,23,1,1]),对应 GitHub issue #2640 的设计——AST 随 API 演化,过滤器可据此做兼容检查。 - AST 是演进的而非冻结的:1.15 加 image/link 属性、SoftBreak、LineBlock;2.0 引入 PandocMonad 抽象允许 reader/writer 做 I/O;2.10 重设计 Table 类型支持行列合并;3.0 引入原生
Figure元素(S1)。
3.2 过滤器机制(两种)
| JSON 过滤器(1.x, 2013) | Lua 过滤器(2.0, 2017) | |
|---|---|---|
| 形态 | stdin/stdout 管道程序,读写 JSON AST | 内嵌 Lua 5.4 解释器,直接操作 Lua 化的 AST |
| 语言 | 任意(Haskell/Python/PHP/Perl/JS/Groovy/Ruby…) | Lua(附完整 pandoc 模块 API) |
| 依赖 | 需目标语言解释器 + AST 库 | 仅 pandoc 本体 |
| 性能 | 序列化/反序列化开销(S3 实测:1.36s/1.40s vs 基线 1.01s) | 接近原生(S3 实测:1.03s) |
| 调用 | --filter prog(自动传目标格式作首参、设 PANDOC_VERSION 等环境变量、按扩展名猜解释器) | --lua-filter=x.lua |
- 过滤器函数契约(S3):返回
nil表示不变;返回同类型对象表示替换;返回列表表示拼接(空列表即删除);类型不匹配则报错。钩子可挂在具体元素名(Str、Para…)、序列级(Inlines/Blocks,2.9.2 起)、或兜底(Inline/Block)。 - 遍历策略可配:默认
typewise(Inline → Inlines → Block → Blocks → Meta → Pandoc),可选topdown(深度优先,返回false剪枝);也可手动doc:walk{...}控制顺序。 - 全局环境:
FORMAT(目标格式,可写条件逻辑)、PANDOC_VERSION、PANDOC_STATE等;pandoc.read/write/pipe、pandoc.mediabag(二进制媒体袋)、pandoc.utils.citeproc等把 pandoc 内部能力暴露给脚本。
3.3 学术写作管线
- 写作→多格式发布:Markdown(或 org/org-mode)写作 →
pandoc --citeproc处理@key引用(BibTeX/CSL-JSON 书目 + CSL 样式)→ LaTeX/PDF、docx、EPUB、reveal.js 等。Pandoc 的引用处理经历了内置(1.x, citeproc-hs)→ 外置过滤pandoc-citeproc(1.13, 2013)→ 重新内置(2.11, 2020,MacFarlane 依 CSL 规范从零重写 citeproc 库,更快更忠实)(S1)。 - 模板系统(1.4, 2010 起)+ YAML 元数据块(1.13 起)+ defaults 文件(2.8 起)构成"一源多投"的配置层。
- pandoc-scholar(S4):在此之上补齐学术元数据短板——*“Plain pandoc is already excellent at document conversion, but it lacks in metadata handling.”* 提供作者/机构/联系方式的 YAML 写法、引文的语义标注,并把全部元数据导出为 JSON-LD(语义网标准)。自 2.0 起其功能基本由 pandoc Lua 过滤器承载(*“most of the functionality of pandoc Scholar is now provided via pandoc Lua filters”*),可拆散混用、可嵌入 RMarkdown。仅依赖 pandoc + make。
- 上层生态:Quarto、Jupyter Notebook 内嵌 pandoc(S1: *“it would become integrated into academic writing tools like Quarto and Jupyter Notebook”*);常见工作流合集见 Awesome Scientific Writing。
4. 数据与证据(evidence)
文献证据:
- S1(一手):2006-08-03 发布 0.1(约 3000 行 Haskell,Markdown/RST/HTML/LaTeX 互转 + RTF/S5 输出);20 年间 200+ 个 release;当前 51 输入格式 × 76 输出格式 = 3876 种转换;四个核心包(pandoc/pandoc-lua-engine/pandoc-server/pandoc-cli)共 85,684 行 Haskell(不含测试),计入配套库约翻倍;GitHub 上解决 7346 个 issue,600+ 贡献者(Top20 表格详列,MacFarlane 本人 372,317 行变更)。
- S2(一手):管线图
INPUT --reader--> AST --filter--> AST --writer--> OUTPUT;behead.hs12 行 Haskell 实现正则难以可靠完成的"二级标题转斜体段落";--filter自动传目标格式为首参;支持.py/.hs/.pl/.rb/.php/.js/.r扩展名猜测解释器。 - S3(一手):性能表——对 pandoc 手册转 HTML,基线 1.01s,JSON 过滤器(编译 Haskell)1.36s,Python 1.40s,Lua 过滤器 1.03s;即过滤器引入约 35% 开销,Lua 方案基本归零。
- S4(一手):pandoc-scholar 的 PeerJ CS 论文(peerj.com/articles/cs-112/)记录其动机;示例项目一条
make产出 PDF/docx/EPUB/JSON-LD 四种产物。
本地实测(pandoc 3.9.0.2,Windows,本次运行命令与输出):
- AST 形状:
pandoc -s -t native test.md(输入# My Heading+ 含斜体与脚注的段落)→
Pandoc (Meta { unMeta = fromList [] })
[ Header 1 ("my-heading",[],[]) [Str "My",Space,Str "Heading"]
, Para [Str "text",Space,Str "with",Space,Emph [Str "italics"],...,
Note [Para [Str "the",Space,Str "note."]], Str "."] ]
pandoc -t json test.md → {"pandoc-api-version":[1,23,1,1],"meta":{},"blocks":[{"t":"Header","c":[1,...]}],...}——验证 AST 的 Meta+Blocks 双层结构与 API 版本戳。
- Lua 过滤器:写
behead.lua(function Header(el) if el.level >= 2 then return pandoc.Para{pandoc.Emph(el.content)} end end),对## H2 heading执行pandoc --lua-filter=behead.lua -t native→ 输出[ Para [ Emph [ Str "H2" , Space , Str "heading" ] ] , ...];-t html→<p><em>H2 heading</em></p>。7 行 Lua 完成官方文档同款改造,且无需任何外部安装。 - 格式数量:
pandoc --list-input-formats | wc -l= 51;pandoc --list-output-formats | wc -l= 75(S1 记 76,差 1 属 3.9.0.2 与文末 2026 统计的版本微差,方向一致)。 - citeproc 管线:
refs.bib(一条 BibTeX)+paper.md(YAML 指定 bibliography,正文As shown by @doe2020)→pandoc --citeproc paper.md -t plain→
As shown by Doe (2020).
References
Doe, Jane. 2020. “Deep Reading.” J. Docs.
即引用解析 + 文末书目一步完成,验证学术管线最小闭环。
- 安装特征:
pandoc --version显示Features: +server +lua / Scripting engine: Lua 5.4,印证 S1 所述 3.x 的 pandoc-lua-engine/pandoc-server 拆包设计。
证据充分性:一手文档 + 作者回顾 + 可复现的本地实验三重印证,对"AST 架构、双过滤器机制、citeproc 管线"的结论置信度高;唯一未覆盖的是大规模生产环境的性能数据(仅官方单点基准)。
5. 结论(conclusion)
- Pandoc 验证了一条通用架构定律:把"输入→中间表示→输出"中的中间表示做成一等公民并开放出来,就把单一工具变成了平台。AST 让 51×76 种转换收敛为 51+76 个模块;JSON/Lua 过滤器让 AST 成为可编程接口,长尾需求不再需要改内核。
- 两种过滤器的取舍清晰:JSON 过滤器胜在语言自由(任何语言可写、可独立分发复现),Lua 过滤器胜在零依赖与近原生性能(S3 数据:1.03s vs 1.36s)。生态 accordingly 演化:pandoc-scholar 等大型工具链已从 Python 迁往 Lua 过滤器。
- 学术写作管线是 Pandoc 最初与最大的受益场景:写作格式(Markdown/org)与发布格式(PDF/docx/EPUB)彻底解耦,引用经 citeproc + CSL 一步渲染,元数据经 YAML/模板注入;Quarto、Jupyter 的内嵌使其成为事实上的学术写作基础设施。20 年回顾的一句话概括其地位:*“pandoc would come to support over fifty document formats... become integrated into academic writing tools like Quarto and Jupyter Notebook... installed on millions of computers around the world.”*
- 作者本人的终局判断值得记录:确定性、低能耗、可靠的格式转换在当下仍是 LLM 难以替代的,但他不否认 LLM 终可能胜出——这个架构的护城河是"规则确定性",而非"理解力"。
6. 局限(limits)
- 作者承认的:pandoc 至今仍用 pre-commonmark 的 legacy Markdown 解析器(S1: *“Ironically, although I was the author of the commonmark spec, pandoc still uses a pre-commonmark Markdown parser!”*),因若干关键扩展未在 commonmark 核心实现,用户需在
markdown/gfm/commonmark_x间取舍;Haskell 选型是"先学语言后想项目"的偶然而非优化决策;3.0 之前单体过重(含 web server 与 Lua 解释器),才被迫拆四包。 - 可推断的:① AST 是有损中转——任何输出格式不支持的语义必然降级或丢失(RawBlock/RawInline 只是逃生通道,非解决方案);② 表格这类复杂结构历经多版重设计(2.10 Table 重构),说明 AST schema 本身难以一次定型,过滤器作者需面对跨版本兼容成本(故 JSON 才带 api-version 戳);③ Lua 过滤器绑定 pandoc 版本(2.17 才有
traverse等),分发 Lua 过滤器需写版本检查;④ 单点性能基准(MANUAL.txt 转 HTML)不能代表复杂文档;⑤ citeproc 曾因原库维护者消失而被迫重写(S1: "Rossato had long since disappeared from the scene")——单人依赖链是这类开源基础设施的隐性风险。 - 本笔记局限:Quarto 侧、Jupyter ipynb 支持细节未展开(来自 S1 一句话提及);未实测 JSON 过滤器管道(本地无 Haskell 环境,机制引自 S2/S3 文档);S1 为 2026 年发布的一手回顾,20 年统计数据以作者自述为准。
7. 对我的启发(relevance)
- "中转表示 + 两端适配"模式可直接迁移:知识库/博客管线里,把 Markdown→发布 的各环节改为"解析为 AST → Lua 过滤器改写 → 输出",比正则改文本可靠得多。本机已装 pandoc 3.9 + Lua 5.4,改造即时可行——例如:自动给外链加注、给代码块注入运行结果、按 frontmatter 拆分讲义(S3 的 handout filter 即模板)。
- 过滤器优先于分支脚本:凡"如果是 HTML 输出则 X,如果是 LaTeX 输出则 Y"的需求,用
FORMAT全局量写一个 Lua 过滤器统一处理,替代维护多份模板分支(S3 的居中图片示例是标准写法)。 - 学术/技术写作可直接套用最小管线:
Markdown + YAML 元数据 + refs.bib + --citeproc + --pdf-engine=xelatex(本次实测已验证引用闭环),加上 pandoc-scholar 的 Lua 过滤器补作者/机构元数据与 JSON-LD,即得"一源多投"论文流水线;实测细节(CSL 样式文件下载、YAML bibliography 字段)已在第 4 节记录,可直接复用。 - 确定性 vs 生成式的边界意识:MacFarlane 的 LLM 对比提示一条工程准则——结构转换要可预测、可重跑、可 diff;LLM 适合做"意图理解"(如识别无标记的格式意图),但不适合做构建管线中的确定性变换。两者是分工而非替代。
- API 版本戳 + 类型驱动重构:
pandoc-api-version进 JSON 输出、Haskell 编译器兜底大重构(S1),是"演进型中间表示"的工程范式——若我维护任何带序列化接口的数据模型(如笔记矩阵、schema 迁移),值得照抄这两招。
附:关键原文引述存档
“It parsed the Markdown using parser combinators and produced a real abstract syntax tree (AST), which it could then render to HTML or another format. This was a more reliable architecture (avoiding many quirks of the regex versions). It was also a more extensible one: by writing N parsers (“readers”) and M renderers (“writers”), one could support N × M conversions.” —— S1
“Pandoc consists of a set of readers and writers. When converting a document from one format to another, text is parsed by a reader into pandoc's intermediate representation of the document—an ‘abstract syntax tree’ or AST—which is then converted by the writer into the target format. ... A ‘filter’ is a program that modifies the AST, between the reader and the writer.” —— S2
“Pandoc data types are marshaled to Lua directly, avoiding the overhead of writing JSON to stdout and reading it from stdin.” —— S3(附性能表:pandoc 1.01s / --filter(hs) 1.36s / --filter(py) 1.40s / --lua-filter 1.03s)
“The first is ecological; it simply requires far less energy for the same conversion. The second is that pandoc's output is deterministic ... The third is that, for the moment at least, pandoc's conversions are going to be more reliable.” —— S1(对 LLM 转换的三点优势)
“Plain pandoc is already excellent at document conversion, but it lacks in metadata handling.” / “since version 2.0, most of the functionality of pandoc Scholar is now provided via pandoc Lua filters” —— S4