给 Pandoc 写一个 Lua Filter:自动给 Word 转出的标题加自定义标记
Word 转 Markdown 时,样式会变成结构:一级标题变顶层 Header,二级变二级。有时这还不够:你可能希望 Word 导入的标题带上模板或管道能识别的标记,手工改几百个不现实。
Pandoc 内置了答案:Lua filter,在转换过程中对文档做变换的小脚本。本文解释其原理,针对加标记任务一步步搭一个过滤器,并讲清如何运行调试。
Lua filter 是什么,pandoc 为什么选 Lua
Pandoc 先把输入解析成统一的抽象语法树(AST),即标题、段落、表格等元素组成的内部表示,再写输出。filter 遍历并改写这棵树,docx 和 Markdown 输入下行为一致。
Pandoc 内嵌 Lua 解释器(目前是 Lua 5.4),filter 直接在进程内跑,无需额外运行时和编译扩展。写个文本文件交给 pandoc,还省掉外部 filter 的 JSON 往返开销。
filter 的解剖
filter 文件里定义以元素类型命名的普通函数:名为 Header 的函数对每个标题各跑一次,Para 对每段各跑一次。若没有显式返回 filter 表,pandoc 会按名字收集它们。
返回值决定元素被什么替换:返回 nil(包括忘了写 return)则元素原样保留;返回元素本身则以新版本替换;返回列表把元素组接在原位;返回空列表则删除元素。
Header 元素与它的属性
AST 里每个 Header 带三样东西:作为数字的 level、一组行内内容、一个属性集。属性集分 identifier(锚点)、类名列表 classes、键值对 attributes;HTML 输出里就是 id 和 class。
属性去哪取决于输出格式。pandoc Markdown 在标题后用花括号注记渲染,HTML 写在标签上。纯 GFM 没有属性语法,类名会被丢弃,选格式前值得知道。
任务:给 Word 转出的标题打标记
目标是写一个检查每个 Header、符合条件就追加自定义 class 的 filter,比如全部标题或只处理二级。逻辑是读 level 字段判断,再把类名追加进 classes 列表。
条件判断让它真正可用:给顶层标题一个类、更深的另一个类;只处理以特定词开头的标题;跳过已带该类的标题,让 filter 幂等,跑两遍也不重复打标。
逐步过一遍代码逻辑
写出来只有四步:定义一个以 Header 命名的函数,让 pandoc 把每个标题路由进来;函数内用 level 和阈值比较;匹配就把类名追加进 classes,需要时设个键值属性;最后返回该元素。
最后的 return 值得强调:就地修改再返回是文档推荐的模式,忘了 return 时 pandoc 保留未修改的原件,改动白算。有了 return,标题才带着新 class 输出。
如何运行
命令行里 pandoc 用 --lua-filter 参数指向你的文件,参数可重复以按顺序串联多个 filter。filter 在读取和写出之间运行,输出格式不限。
本站 Pro 选项支持上传 Lua filter:把同一个 .lua 文件和 Word 文档一起交上去,浏览器 worker 交给同一个 pandoc 引擎,效果与本地一致,不向服务器发任何东西。
调试手段
最快的工具是 print:filter 打印的内容走 stderr,不混进转换结果,可随手打印标题的 level、classes。Pandoc 还内置日志模块 pandoc.log,info 和 warning 带级别。
好习惯是拿小文件开发:做一个每个层级各有一个标题的 docx,对它跑 filter 并检查输出,再指向真实文档库。多数失败是字段名拼错,错误通道会立刻暴露。
延伸玩法与性能
同一模式不止用于标题:删除没文字的空段落、从标题文字生成 id 获得稳定锚点、把表格包进 Div 让模板按组加样式,都是在同一文件里为各元素类型各写一个函数。
性能很少成为问题:filter 在转换进程内逐元素运行,很大的文档遍历也快。要避免的只是在函数里做重复重活,比如读文件,那应放到脚本顶部只跑一次。