Pandoc Lua 过滤器示例

以下是作为示例呈现的几个过滤器。一个包含有用 Lua 过滤器(也可以作为很好的示例)的仓库可以在 https://github.com/pandoc/lua-filters 找到。

宏替换

下面的过滤器将字符串 \{\{helloworld\}\} 转换为强调文本 “Hello, World” 。

return {
  {
    Str = function(elem)
      if elem.text == "\{\{helloworld\}\}" then
        return pandoc.Emp {pandoc.Str "Hello, World"}
      else
        return elem
      end
    end,
  }
}

在 LaTeX 和 HTML 输出中居中图像

对于 LaTeX,使用 LaTeX 片段包裹图像以实现水平居中。在 HTML 中,使用图像元素的样式属性来实现居中。

-- 如果目标格式是LaTeX,则用此函数过滤图像。
if FORMAT:match 'latex' then
  function Image (elem)
    -- 使用使图像居中的原始LaTeX包围所有图像。
    return {
      pandoc.RawInline('latex', '\\hfill\\break{\\centering'),
      elem,
      pandoc.RawInline('latex', '\\par}')
    }
  end
end

-- 如果目标格式是HTML,则用此函数过滤图像。
if FORMAT:match 'html' then
  function Image (elem)
    -- 使用CSS样式使图像居中
    elem.attributes.style = 'margin:auto; display: block;'
    return elem
  end
end

在元数据中设置日期

此过滤器将文档的元数据中的日期设置为当前日期,如果尚未设置日期的话:

function Meta(m)
  if m.date == nil then
    m.date = os.date("%B %e, %Y")
    return m
  end
end

删除引用前的空间

此过滤器删除所有位于 “作者在文本中” 的引用之前的空格。在 Markdown 中,“作者在文本中”的引用(例如 @citekey)必须由一个空格前置。如果这些空格不需要,必须通过过滤器删除它们。

local function is_space_before_author_in_text(spc, cite)
  return spc and spc.t == 'Space'
    and cite and cite.t == 'Cite'
    -- 必须只有一个引用,并且其模式必须为 'AuthorInText'
    and #cite.citations == 1
    and cite.citations[1].mode == 'AuthorInText'
end

function Inlines (inlines)
  -- 从末尾开始以避免索引移动问题。
  for i = #inlines-1, 1, -1 do
    if is_space_before_author_in_text(inlines[i], inlines[i+1]) then
      inlines:remove(i)
    end
  end
  return inlines
end

将占位符替换为其元数据值

Lua 过滤器函数的运行顺序如下:

内联 → 块 → 元数据 → pandoc

从较高层级(如元数据)向较低层级(如内联)传递信息仍然是可能的,只要使用在同一文件中的两个过滤器:

local vars = {}

function get_vars (meta)
  for k, v in pairs(meta) do
    if pandoc.utils.type(v) == 'Inlines' then
      vars["%" .. k .. "%"] = {table.unpack(v)}
    end
  end
end

function replace (el)
  if vars[el.text] then
    return pandoc.Span(vars[el.text])
  else
    return el
  end
end

return { {Meta = get_vars}, {Str = replace} }

如果文件 occupations.md 的内容是:

---
name: Samuel Q. Smith
occupation: Professor of Oenology
---

Name

: %name%

Occupation

: %occupation%

那么运行 pandoc --lua-filter=meta-vars.lua occupations.md 将输出:

<dl>
  <dt>Name</dt>
  <dd>
    <p><span>Samuel Q. Smith</span></p>
  </dd>
  <dt>Occupation</dt>
  <dd>
    <p><span>Professor of Oenology</span></p>
  </dd>
</dl>

修改 pandoc 的 MANUAL.txt 文件以生成 man 页面

这是我们在将 MANUAL.txt 转换为 man 页面时使用的过滤器。它将一级标题转换为大写(使用 walk 来转换标题内的内联元素),删除脚注,并用普通文本替换链接。

-- 使用 pandoc.text 获取 UTF-8 意识的 'upper' 函数
local text = pandoc.text

function Header(el)
    if el.level == 1 then
      return el:walk {
        Str = function(el)
            return pandoc.Str(text.upper(el.text))
        end
      }
    end
end

function Link(el)
    return el.content
end

function Note(el)
    return {}
end

从论文创建讲义

此过滤器从文档中提取所有编号示例、部分标题、块引用和图像, 以及任何具有类 handoutdiv ( 注意:只包含 “外部级别” 的块; 这会忽略嵌套结构内的块, 例如列表项 )。

-- 从文章创建讲义,使用其标题、
-- 块引用、编号示例、图像,以及任何
-- 类为 "handout" 的 Div

function Pandoc(doc)
    local hblocks = {}
    for i,el in pairs(doc.blocks) do
        if (el.t == "Div" and el.classes[1] == "handout") or
           (el.t == "BlockQuote") or
           (el.t == "OrderedList" and el.style == "Example") or
           (el.t == "Para" and #el.c == 1 and el.c[1].t == "Image") or
           (el.t == "Header") then
           table.insert(hblocks, el)
        end
    end
    return pandoc.Pandoc(hblocks, doc.meta)
end

计算文档中的单词数

此过滤器计算文档正文中的单词数 (省略诸如标题和摘要等元数据), 包括代码中的单词。与直接在 Markdown 文档上运行 wc -w 相比,它应该更准确,因为后者会将标记字符 ( 如 ATX 标题前面的 # ) 或 HTML 文档中的标签计为单词。要运行它,请使用 pandoc --lua-filter wordcount.lua myfile.md

-- 计算文档中的单词数

words = 0

wordcount = {
  Str = function(el)
    -- 如果单词完全由标点符号组成,则不计数
    if el.text:match("%P") then
        words = words + 1
    end
  end,

  Code = function(el)
    _,n = el.text:gsub("%S+", "")
    words = words + n
  end,

  CodeBlock = function(el)
    _,n = el.text:gsub("%S+", "")
    words = words + n
  end
}

function Pandoc(el)
    -- 省略元数据,仅计算正文:
    el.blocks:walk(wordcount)
    print(words .. " words in body")
    os.exit(0)
end

将 ABC 代码转换为乐谱记号

此滤镜会将具有 abc 类别的代码块内容通过 abcm2ps 和 ImageMagick 的 convert 命令行工具转换成图片。更多关于 ABC 表记法的信息,请访问 https://abcnotation.com

图片会被添加到媒体包中。对于二进制格式输出,Pandoc 将使用媒体包中的图片。对于文本格式输出,可以使用 --extract-media 参数指定一个目录,将媒体包中的文件写入该目录;或者对于 HTML 输出,可以使用 --embed-resources 参数。

以下是用于处理包含 ABC 表记法的 abc 类别代码块的 Pandoc 滤镜示例:

-- Pandoc 滤镜用于将具有 "abc" 类别的代码块中的 ABC 表记法转换为图片。
--
-- * 假设 abcm2ps 和 ImageMagick 的 convert 已经安装并可被调用。
-- * 对于文本输出格式,使用 --extract-media=abc-images
-- * 对于 HTML 格式,也可以使用 --embed-resources

local filetypes = {
  html = {"png", "image/png"},
  latex = {"pdf", "application/pdf"}
}
local filetype = filetypes[FORMAT][1] or "png"
local mimetype = filetypes[FORMAT][2] or "image/png"

local function abc2eps(abc, filetype)
    local eps = pandoc.pipe("abcm2ps", {"-q", "-O", "-", "-"}, abc)
    local final = pandoc.pipe("convert", {"-", filetype .. ":-"}, eps)
    return final
end

function CodeBlock(block)
    if block.classes[1] == "abc" then
        local img = abc2eps(block.text, filetype)
        local fname = pandoc.sha1(img) .. "." .. filetype
        pandoc.mediabag.insert(fname, mimetype, img)
        return pandoc.Para{ pandoc.Image({pandoc.Str("abc tune")}, fname) }
    end
end

使用 TikZ 构建图像

这段内容描述了一个用于将原始 LaTeX 中的 TikZ 环境转换为图像的滤镜。它可以用于生成 PDF 和 HTML 输出。TikZ 代码通过 pdflatex 编译成图像,并使用 pdf2svg 将 PDF 图像转换为 SVG 格式,因此这两个工具都必须在系统的路径中可用。转换后的图像会缓存在工作目录中,并根据源代码的哈希值命名,这样在构建文档时无需每次都重新生成这些图像(更复杂的做法可能会将它们放在专门的缓存目录中)。

local system = require 'pandoc.system'

local tikz_doc_template = [[
\documentclass{standalone}
\usepackage{xcolor}
\usepackage{tikz}
\begin{document}
\nopagecolor
%s
\end{document}
]]

local function tikz2image(src, filetype, outfile)
  system.with_temporary_directory('tikz2image', function (tmpdir)
    system.with_working_directory(tmpdir, function()
      local f = io.open('tikz.tex', 'w')
      f:write(tikz_doc_template:format(src))
      f:close()
      os.execute('pdflatex tikz.tex')
      if filetype == 'pdf' then
        os.rename('tikz.pdf', outfile)
      else
        os.execute('pdf2svg tikz.pdf ' .. outfile)
      end
    end)
  end)
end

extension_for = {
  html = 'svg',
  html4 = 'svg',
  html5 = 'svg',
  latex = 'pdf',
  beamer = 'pdf' }

local function file_exists(name)
  local f = io.open(name, 'r')
  if f ~= nil then
    io.close(f)
    return true
  else
    return false
  end
end

local function starts_with(start, str)
  return str:sub(1, #start) == start
end


function RawBlock(el)
  if starts_with('\\begin{tikzpicture}', el.text) then
    local filetype = extension_for[FORMAT] or 'svg'
    local fbasename = pandoc.sha1(el.text) .. '.' .. filetype
    local fname = system.get_working_directory() .. '/' .. fbasename
    if not file_exists(fname) then
      tikz2image(el.text, filetype, fname)
    end
    return pandoc.Para({pandoc.Image({}, fbasename)})
  else
   return el
  end
end

使用示例:

pandoc --lua-filter tikz.lua -s -o cycle.html <<'EOF'
这里是一个循环图的示例:

\begin{tikzpicture}

\def \n {5}
\def \radius {3cm}
\def \margin {8} % 角度的边缘,取决于半径

\foreach \s in {1,...,\n}
{
  \node[draw, circle] at ({360/\n * (\s - 1)}:\radius) {$\s$};
  \draw[-&gt;, &gt;=latex] ({360/\n * (\s - 1)+\margin}:\radius)
    arc ({360/\n * (\s - 1)+\margin}:{360/\n * (\s)-\margin}:\radius);
}
\end{tikzpicture}
EOF
在本文档中