Pandoc 自定义 Writer 开发指南:用 Lua 为任意文档格式编写专属渲染器
Pandoc 自定义 Writer 开发指南用 Lua 为任意文档格式编写专属渲染器【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandocPandoc 内置了 Lua 解释器允许用户通过一个纯 Lua 脚本文件定义全新的输出格式或彻底改变既有格式的渲染行为这份文档doc/custom-writers.md就是这一能力的官方技术说明。读完本文你将掌握新式new-style与经典classic两种自定义 Writer 的编写方式、如何声明格式扩展与默认模板、如何使用pandoc.scaffolding.Writer快速搭建渲染器并能理解其背后在 pandoc 源码中的实现原理与测试验证方式。为什么需要自定义 Writer当 pandoc 内置的数十种输出格式LaTeX、HTML、docx、typst 等完整清单见 src/Text/Pandoc/Writers.hs仍无法满足需求时你有两条路渲染一种 pandoc 尚未支持的格式或者改变 pandoc 渲染某种既有格式的方式。自定义 Writer 正是为此设计的机制。自定义 Writer 本质上是一个 Lua 文件它定义了如何渲染整个文档。由于 pandoc 自带 Lua 解释器pandoc-lua-engine组件你无需安装任何额外的软件即可使用这一功能。在 pandoc 的 Lua 世界中自定义 Writer 与 Lua 过滤器Lua filters 是姊妹机制过滤器在文档解析后、渲染前对 AST 进行变换而自定义 Writer 则接管了从 AST 到目标格式的最终渲染环节。Writer 与 ByteStringWriter新式自定义 Writer 的入口新式new-style自定义 Writer 的核心要求非常简洁在 Lua 文件中定义一个全局函数Writer或ByteStringWriter。pandoc 会以文档doc和 Writer 选项opts为参数调用该函数并期望它返回一个 UTF-8 编码的字符串function Writer (doc, opts) -- ... enddoc是 pandoc 的文档对象pandoc.Pandoc类型opts是WriterOptions对象两者都可通过 doc/lua-filters.md 中介绍的全部 Lua 模块访问与操作。这一接口自 pandoc 2.17.2 引入。如果你需要输出的不是文本而是二进制数据例如 docx、odt、pptx 这类打包格式则应定义一个名为ByteStringWriter的全局函数。它同样返回字符串但不要求是 UTF-8 编码可以包含任意的二进制字节function ByteStringWriter (doc, opts) -- 返回任意二进制数据 end需要注意的是如果同一个文件中同时定义了Writer和ByteStringWriterpandoc 只会使用Writer函数。从源码看这一设计对应 src/Text/Pandoc/Writers.hs 中的 Writer 代数数据类型——内置 Writer 同样区分为文本与字节流两类TextWriter (WriterOptions - Pandoc - m Text)与ByteStringWriter (WriterOptions - Pandoc - m BL.ByteString)docx、odt、pptx、epub、chunkedhtml 等内置格式都属于后者自定义的ByteStringWriter与它们走的是同一条渲染管线。在 src/Text/Pandoc/App/OutputSettings.hs 中两类自定义 Writer 都会被统一包装进 sandbox 沙箱执行保证脚本只能访问被允许的资源。通过 Extensions 表声明格式扩展格式扩展format extensions是 pandoc 中控制渲染细节的开关例如smart智能标点、citations引用、hard_line_breaks硬换行。自定义 Writer 通过一个全局Extensions表来声明自己支持哪些扩展值为true表示该扩展默认启用值为false表示该扩展受支持但默认禁用表里没有的扩展则完全不受支持。例如下面的 Writer 支持smart、citations、foobar三个扩展其中smart默认开启其余两个默认关闭Extensions { smart true, citations false, foobar false }扩展的启停完全由命令行用户控制用法与内置格式一致例如pandoc -t my-writer.luacitations会在调用你的 Writer 时启用citations扩展。在 Writer 函数内部可以通过opts.extensions字段查询扩展当前是否启用其includes方法接受扩展名并返回布尔值function Writer (doc, opts) print( The citations extension is, opts.extensions:includes citations and enabled or disabled ) -- ... end这一机制的测试用例就存放在仓库中pandoc-lua-engine/test/extensions.lua 定义了一个声明smart true, citations false的 Writer 并在函数体中打印两个扩展的状态pandoc-lua-engine/test/Tests/Lua/Writer.hs 中则通过ExtensionsDiffExt_citations的启用/禁用差异验证默认情况下输出smart extension is enabled; citations extension is disabled当命令行传入citations后输出变为两个都 enabled。Template 全局函数定义默认模板模板template用于在-s/--standalone独立文档模式下包裹正文。自定义 Writer 的默认模板由全局函数Template的返回值定义function Template () return !DOCTYPE html\nhtml\n$body$\n/html endpandoc 只有在用户未显式指定模板、但使用了-s/--standalone标志时才会使用这个默认模板。Template可以留空不定义但那样的话一旦 pandoc 在需要默认模板的场合运行就会直接抛出错误。仓库中的测试 pandoc-lua-engine/test/writer-template.lua 演示了Template与Writer的组合Writer委托给内置的pandoc.write渲染 gfmTemplate返回一个包含$body$占位符的简单模板对应的 golden 测试pandoc-lua-engine/test/Tests/Lua/Writer.hs会把模板内容与writer-template.out.txt做比对。实战示例基于 pandoc.write 的修改版 Markdown Writer自定义 Writer 可以访问 Lua 过滤器文档 中描述的全部模块其中就包括pandoc.write。pandoc.write能调用 pandoc 内置的任意 Writer 完成格式转换因此一个非常实用的模式是先用 Lua 变换 AST再把结果交给内置 Writer 渲染。下面的例子就是一个完整的自定义 Writer它把文档渲染成 GitHub Flavored Markdowngfm但强制所有代码块使用围栏fenced形式绝不使用缩进式代码块function Writer (doc, opts) local filter { CodeBlock function (cb) -- only modify if code block has no attributes if cb.attr pandoc.Attr() then local delimited \n .. cb.text .. \n return pandoc.RawBlock(markdown, delimited) end end } return pandoc.write(doc:walk(filter), gfm, opts) end Template pandoc.template.default gfm这个例子展示了三个关键点doc:walk(filter)对文档执行一次 Lua 过滤器遍历。这里的过滤器只处理无属性的CodeBlockcb.attr pandoc.Attr()判断属性是否为空将其替换为markdown格式的RawBlock从而保证输出围栏代码块pandoc.write(doc, gfm, opts)将变换后的文档交给内置的 gfm Writeropts原样传递保证了命令行传入的扩展、宽度等选项生效。pandoc.write的实现位于 pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs它接受(doc, formatspec, writer_options)三个参数——第二个参数是带扩展的格式串如html、gfmsmart第三个参数可以是一个完整的WriterOptions对象也可以是一个只包含部分键值对的表Template pandoc.template.default gfm直接把 gfm 的内置默认模板仓库中为 data/templates/default.gfm拿来用作本 Writer 的默认模板省去手写模板的麻烦。pandoc.scaffolding.Writer减少样板代码如果你不想每个 Writer 都重复处理元数据metadata、模板应用、块/行内元素遍历等繁琐细节可以使用pandoc.scaffolding.Writer。它是一个自定义 Writer 脚手架把pandoc.scaffolding.Writer这个值赋给全局Writer即可之后你只需要为各种 AST 元素类型提供渲染函数Writer pandoc.scaffolding.Writer然后分别在Writer.Block和Writer.Inline两个表中注册 Block 与 Inline 元素的渲染函数。每个渲染函数接收元素本身以及可选的WriterOptions作为参数Writer.Inline.Str function (str) return str.text end Writer.Inline.SoftBreak function (_, opts) return opts.wrap_text wrap-preserve and cr or space end Writer.Inline.LineBreak cr Writer.Block.Para function (para) return {Writer.Inlines(para.content), pandoc.layout.blankline} end关于渲染函数的返回值有以下规则可以返回字符串、pandoc.layout的Doc元素或此类元素的列表返回列表时各值会像传给pandoc.layout.concat一样被拼接如果某个渲染结果与输入无关是常量也可以直接用一个常量代替函数例如上面的Writer.Inline.LineBreak cr。Writer.Block与Writer.Inline这两个表本身可以作为函数调用它们会根据元素的类型分派到对应的渲染函数。例如Writer.Block(pandoc.Para x)会委托给Writer.Para渲染函数并返回其调用结果。此外脚手架还提供了三个组合级别的渲染入口Writer.Blocks/Writer.Inlines渲染 Block/Inline 元素列表Writer.Blocks接受一个可选的分隔符作为第二个参数例如Writer.Blocks(blks, pandoc.layout.cr)默认的块分隔符是pandoc.layout.blanklineWriter.Pandoc渲染整个文档的所有块。所有预定义的渲染函数都可以按需覆盖。最终脚手架生成的 Writer 会使用这些渲染函数处理元数据值并将其转换为模板变量template variables在提供了模板的情况下自动应用模板例如命令行-s且未指定模板时使用Template全局函数的返回值。从源码看pandoc.scaffolding模块定义在 pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Scaffolding.hs而核心实现位于 pandoc-lua-engine/src/Text/Pandoc/Lua/Writer/Scaffolding.hs其中有几个值得注意的实现细节分派机制renderBlock/renderInline渲染时通过 Haskell 的showConstr . toConstr取得 AST 构造器名如Para、Str再去Writer.Block/Writer.Inline表中按名字查找渲染函数getNestedWriterField还会回退到Writer顶层查找缺失函数的报错如果某个元素类型没有定义渲染函数handleMissingField会抛出类似No render function for Block value Para; define a function Writer.Block.Para that returns a string or Doc.的明确错误直接告诉你该补哪个函数自动换行处理runWriter中会根据opts.wrap_text是否为WrapAuto决定渲染时是否按writerColumns指定的列宽折行元数据与模板元数据通过metaToContext转换为模板上下文模板存在时自动用renderTemplate渲染最终统一输出为文本。经典Classic风格 Writer已被弃用的旧接口在 2.17.2 的新式接口之前自定义 Writer 采用的是经典风格为 pandoc AST 的每一种元素类型定义一个同名 Lua 函数pandoc 递归遍历文档时逐一调用。例如function Para(s) return paragraph .. s .. /paragraph end注意经典风格已被官方标记为弃用deprecated可能在未来的版本中移除新项目应优先采用新式 WriterWriter/ByteStringWriter以及pandoc.scaffolding.Writer脚手架。经典风格的完整调用约定可以从实现文件 pandoc-lua-engine/src/Text/Pandoc/Lua/Writer/Classic.hs 中还原Block 类元素调用Plain、Para、Header、CodeBlock、BlockQuote、BulletList、OrderedList、DefinitionList、Div、Table、Figure等函数Inline 类元素调用Str、Space、SoftBreak、Emph、Strong、Code、Math、Link、Image、Note等函数块列表之间用Blocksep函数的返回值分隔。经典风格的模板变量经典风格中如果需要在模板中新增变量或修改已有变量可以让Doc函数返回第二个值。下面的例子会在date变量既非元数据值也非已有变量时把当前日期写入其中function Doc (body, meta, vars) vars.date vars.date or meta.date or os.date %B %e, %Y return body, vars end其中body是已渲染的正文meta是文档元数据表vars是模板变量表返回的第二个值vars会被解析为模板上下文与元数据转换出的上下文合并后用于模板渲染。这在 Classic.hs 的docToCustom中有直接对应它调用全局Doc函数并期望 2 个返回值第二个返回值通过 JSON 解析为Context Text。pandoc 3.0 的变更与兼容性恢复pandoc 3.0 对自定义 Writer 机制做了一次较大的重构由此带来一个技术性变化出于技术原因经典风格依赖的全局变量PANDOC_DOCUMENT和PANDOC_WRITER_OPTIONS在新式接口下分别被设置为空文档与默认值而不是真实的当前文档和 Writer 选项。如果你有依赖这两个全局变量的旧脚本可以在脚本开头加入下面这段代码来恢复旧行为——它的原理是把经典风格脚本包装成一个新式 Writer先捕获真实的doc与opts存入全局变量再重新加载脚本文件本身此时经典风格的各元素函数已被定义最后调用pandoc.write_classic完成经典风格的渲染function Writer (doc, opts) PANDOC_DOCUMENT doc PANDOC_WRITER_OPTIONS opts loadfile(PANDOC_SCRIPT_FILE)() return pandoc.write_classic(doc, opts) endpandoc.write_classic在 pandoc-lua-engine/src/Text/Pandoc/Lua/Module/Pandoc.hs 中定义其官方文档注释里给出的用法示例与上面这段代码完全一致它调用 Classic.hs 中的runCustom使用当前 Lua 环境中已定义的经典风格函数来渲染文档。运行与验证从命令行到测试套件编写好my-writer.lua后把它当作普通格式名传给-t/--to即可扩展名也可以是.lua之外的任意名字只要文件内容符合规范pandoc input.md -t my-writer.lua -o output.txt pandoc input.md -t my-writer.luacitations -s -o output.html # 启用 citations 扩展并使用默认模板仓库的测试套件为自定义 Writer 提供了三类现成的验证样例可作为你编写脚本时的参考模板文本渲染pandoc-lua-engine/test/sample.lua 与 pandoc-lua-engine/test/tables.custom 构成 golden 测试对验证经典/新式 Writer 的文本输出二进制输出pandoc-lua-engine/test/bytestring.lua 定义一个ByteStringWriter逐字节生成 0~255 的全部字节与 pandoc-lua-engine/test/bytestring.bin 做二进制 golden 比对见 Tests/Lua/Writer.hs扩展与模板extensions.lua 与 writer-template.lua 分别覆盖Extensions表和Template函数的验证。小结自定义 Writer 让 pandoc 的通用标记转换器能力可以按需无限延伸新式Writer/ByteStringWriter接口适合从零编写格式pandoc.write委托模式适合在既有格式基础上做 AST 级定制pandoc.scaffolding.Writer则用最小样板帮你快速搭起一个完整渲染器而经典风格作为历史接口虽已弃用其模板变量与pandoc.write_classic兼容方案在维护老脚本时仍然有用。若你想进一步了解自定义 Reader读取方向的对应机制可以继续阅读 doc/custom-readers.md。【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考