在 Lua 中创建自定义 Pandoc 写入器

引言

如果你需要渲染 Pandoc 尚不支持的格式,或者想要改变 Pandoc 渲染某种格式的方式,你可以使用 Lua 语言创建一个自定义的写入器。Pandoc 内置了 Lua 解释器,因此无需安装任何额外软件来实现这一点。

自定义写入器是一个 Lua 文件,它定义了如何渲染文档。写入器必须定义一个名为 WriterByteStringWriter 的单一函数,该函数会接收文档和写入器选项,然后处理文档转换,将其渲染成字符串。这种接口在 Pandoc 2.17.2 中引入,而 ByteString 写入器则在 Pandoc 3.0 中可用 。

Pandoc 还支持“经典”风格的自定义写入器,其中必须为每种 AST 元素类型定义一个 Lua 函数。经典风格的写入器已不推荐使用,如果可能的话,应替换为新风格的写入器。

写入器

使用新风格的自定义写入器必须包含一个名为 WriterByteStringWriter 的全局函数。Pandoc 调用此函数并以文档和写入器选项作为参数,并期望函数返回一个 UTF-8 编码的字符串。

function Writer (doc, opts)
  -- ...
end

不返回文本而是返回二进制数据的写入器应该定义一个名为 ByteStringWriter 的函数。该函数仍然必须返回一个字符串,但它不需要是 UTF-8 编码的,可以包含任意二进制数据。

如果同时定义了 WriterByteStringWriter 函数,那么只会使用 Writer 函数 。

格式扩展

写入器可以通过格式扩展进行定制,如 smartcitationshard_line_breaks 。全局的 Extensions 表通过键指示支持的扩展。默认启用的扩展被赋予 true 值,而那些支持但默认禁用的扩展被赋予 false 值。

示例:具有以下全局表的写入器支持 smartcitationsfoobar 扩展,其中 smart 启用,其余默认禁用:

Extensions = {
  smart = true,
  citations = false,
  foobar = false
}

用户如常控制扩展,例如,pandoc -t my-writer.lua+citations。扩展可通过写入器选项的 extensions 字段访问,例如:

function Writer (doc, opts)
  print(
    'The citations extension is',
    opts.extensions:includes 'citations' and 'enabled' or 'disabled'
  )
  -- ...
end

默认模板

自定义写入器的默认模板由全局函数 Template 的返回值定义。当用户未指定模板但以 -s / --standalone 标志调用 Pandoc 时,Pandoc 会使用默认模板进行渲染。

Template 全局可以留空,此时 Pandoc 在原本会使用默认模板的情况下会抛出错误。

示例:修改过的 Markdown 写入器

写入器可以访问 Lua 过滤器文档 中描述的所有模块。这包括 pandoc.write ,它可以用来以 Pandoc 已支持的格式渲染文档。在转换之前可以修改文档,如下例所示。它将文档渲染为 GitHub 风味的 Markdown,但总是使用围栏代码块,从不使用缩进代码块。

function Writer (doc, opts)
  local filter = {
    CodeBlock = function (cb)
      -- 仅在代码块没有属性时修改
      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'

使用 pandoc.scaffolding.Writer 减少样板代码

pandoc.scaffolding.Writer 结构是一个自定义写入器的脚手架,旨在定义自定义写入器时避免常见的样板代码。该对象可用作函数,并允许跳过元数据和模板处理等细节,只需要为每种 AST 元素类型添加渲染函数。

pandoc.scaffolding.Writer 的值是一个通常应分配给全局 Writer 的函数:

Writer = pandoc.scaffolding.Writer

对于 BlockInline 值的渲染函数可以分别添加到 Writer.BlockWriter.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.BlockWriter.Inline 可以用作函数;它们会为相应类型的元素应用正确的渲染函数。例如,Writer.Block(pandoc.Para 'x') 会委托给 Writer.Para 渲染函数,并返回该调用的结果。

类似地,函数 Writer.BlocksWriter.Inlines 可用于渲染元素列表,而 Writer.Pandoc 则用于渲染文档的块。函数 Writer.Blocks 可以接受一个可选的第二个参数作为分隔符,例如,Writer.Blocks(blks, pandoc.layout.cr) ;默认的块分隔符是 pandoc.layout.blankline

所有预定义的函数都可以根据需要进行覆盖。

生成的 Writer 使用渲染函数来处理元数据值,并将它们转换为模板变量。如果有提供模板,则会自动应用。

经典样式

使用经典样式的 writerpandoc AST 的每个元素定义了渲染函数。请注意,此风格已被弃用,可能会在以后的版本中移除。

例如,

function Para(s)
  return "<paragraph>" .. s .. "</paragraph>"
end

模板变量

可以通过从 Doc 函数返回第二个值来添加新的模板变量或修改现有变量。

例如,以下代码将在变量 date 中添加当前日期,除非 date 已经被定义为元数据值或变量:

function Doc (body, meta, vars)
  vars.date = vars.date or meta.data or os.date '%B %e, %Y'
  return body, vars
end

pandoc 3.0 中的变化

pandoc 3.0 起,自定义 writer 进行了重做。由于技术原因,全局变量 PANDOC_DOCUMENTPANDOC_WRITER_OPTIONS 被设置为空文档和默认值,分别。要恢复旧的行为,可以添加以下代码片段,这将把一个经典的 writer 转换为新式 writer

function Writer (doc, opts)
  PANDOC_DOCUMENT = doc
  PANDOC_WRITER_OPTIONS = opts
  loadfile(PANDOC_SCRIPT_FILE)()
  return pandoc.write_classic(doc, opts)
end
在本文档中