请帮助乌克兰!
赞助商
Pandoc   一个通用文档转换器

Pandoc 过滤器

摘要

Pandoc 提供了一个接口,允许用户编写作用于 pandoc AST 的程序(称为过滤器)。

Pandoc 由一组读取器和写入器组成。当文档从一种格式转换为另一种格式时,文本会由读取器解析为 pandoc 的中间文档表示——一个“抽象语法树”(AST)——然后由写入器转换为目标格式。pandoc AST 格式定义在 pandoc-types 包的 Text.Pandoc.Definition 模块中。

“过滤器”是一个程序,它在读取器和写入器之间修改 AST。

INPUT --reader--> AST --filter--> AST --writer--> OUTPUT

Pandoc 支持两种类型的过滤器

  • Lua 过滤器使用 Lua 语言定义对 pandoc AST 的转换。它们在单独的文档中进行了描述。

  • JSON 过滤器(本文档中描述)是管道,它们从标准输入读取并写入标准输出,消费并生成 pandoc AST 的 JSON 表示

                           source format
                                ↓
                             (pandoc)
                                ↓
                        JSON-formatted AST
                                ↓
                          (JSON filter)
                                ↓
                        JSON-formatted AST
                                ↓
                             (pandoc)
                                ↓
                          target format

Lua 过滤器有几个优点。它们使用嵌入在 pandoc 中的 Lua 解释器,因此您无需安装任何外部软件。而且它们通常比 JSON 过滤器更快。但是,如果您希望使用 Lua 以外的语言编写过滤器,您可能更喜欢使用 JSON 过滤器。JSON 过滤器可以用任何编程语言编写。

您可以在管道中直接使用 JSON 过滤器

pandoc -s input.txt -t json | \
 pandoc-citeproc | \
 pandoc -s -f json -o output.html

但更方便的做法是使用 --filter 选项,它会自动处理管道连接。

pandoc -s input.txt --filter pandoc-citeproc -o output.html

要温和地了解如何编写自己的过滤器,请继续阅读本指南。在 wiki 上也有第三方过滤器列表

一个简单示例

假设您想将 Markdown 文档中所有 2 级及以上的标题替换为普通段落,并将文本设置为斜体。您将如何实现这一点?

首先想到的是使用正则表达式。像这样:

perl -pe 's/^##+ (.*)$/\*\1\*/' source.txt

这在大多数情况下应该有效。但别忘了 ATX 样式的标题可以以一串 # 符号结尾,而这些符号并非标题文本的一部分

## My heading ##

如果您的文档在 HTML 注释或分隔代码块中包含以 ## 开头的行怎么办?

<!--
## This is just a comment
-->

~~~~
### A third level heading in standard markdown
~~~~

我们不想触碰这些行。此外,Setext 样式的二级标题又如何呢?

A heading
---------

我们也需要处理这些。最后,我们能确定在字符串两边添加星号就能使其变为斜体吗?如果字符串本身已经包含星号怎么办?那样我们最终会得到粗体文本,这不是我们想要的。如果它包含一个普通的未转义星号呢?

您将如何修改您的正则表达式来处理这些情况?至少可以说,这会非常棘手。

更好的方法是让 pandoc 处理解析,然后在文档写入之前修改 AST。为此,我们可以使用过滤器。

要查看 pandoc 解析我们的文本时会生成什么样的 AST,我们可以使用 pandoc 的 native 输出格式。

% cat test.txt
## my heading

text with *italics*
% pandoc -s -t native test.txt
Pandoc (Meta {unMeta = fromList []})
[Header 2 ("my-heading",[],[]) [Str "My",Space,Str "heading"]
, Para [Str "text",Space,Str "with",Space,Emph [Str "italics"]] ]

一个 Pandoc 文档包含一个 Meta 块(包含标题、作者和日期等元数据)和一组 Block 元素列表。在本例中,我们有两个 Block,一个 Header 和一个 Para。每个块的内容都是一个 Inline 元素列表。有关 pandoc AST 的更多详细信息,请参阅 Text.Pandoc.Definition 的 haddock 文档

我们可以使用 Haskell 创建一个 JSON 过滤器来转换这个 AST,将每个级别 >= 2Header 块替换为一个 Para 块,其内容包装在一个 Emph 内联元素中

#!/usr/bin/env runhaskell
-- behead.hs
import Text.Pandoc.JSON

main :: IO ()
main = toJSONFilter behead

behead :: Block -> Block
behead (Header n _ xs) | n >= 2 = Para [Emph xs]
behead x = x

toJSONFilter 函数完成两件事。首先,它将 behead 函数(将 Block -> Block 映射)提升为对整个 Pandoc AST 的转换,遍历 AST 并转换每个块。其次,它将这个 Pandoc -> Pandoc 转换与必要的 JSON 序列化和反序列化包装在一起,从而生成一个可执行文件,该文件从标准输入消费 JSON 并将 JSON 产生到标准输出。

要使用该过滤器,请使其可执行:

chmod +x behead.hs

然后:

pandoc -f SOURCEFORMAT -t TARGETFORMAT --filter ./behead.hs

(还需要在本地包仓库中安装 pandoc-types。使用 cabal-install 进行安装的命令是:cabal v2-update && cabal v2-install --lib pandoc-types --package-env .。)

或者,我们可以编译过滤器:

ghc -package-env=default --make behead.hs
pandoc -f SOURCEFORMAT -t TARGETFORMAT --filter ./behead

请注意,如果过滤器放置在系统 PATH 中,则不需要开头的 ./。另请注意,命令行可以包含多个 --filter 实例:过滤器将按顺序应用。

WordPress 的 LaTeX

另一个简单示例。WordPress 博客对 LaTeX 数学公式要求特殊的格式。您需要 $LaTeX e=mc^2$,而不是 $e=mc^2$。我们如何相应地转换 Markdown 文档呢?

同样,使用正则表达式可靠地完成这项工作很困难。$ 可能是一个普通的货币符号,或者它可能出现在注释、代码块或内联代码段中。我们只想找到开始 LaTeX 数学公式的 $ 符号。要是我们有一个解析器就好了……

我们有。Pandoc 已经可以提取 LaTeX 数学公式,所以:

#!/usr/bin/env runhaskell
-- wordpressify.hs
import Text.Pandoc.JSON

main = toJSONFilter wordpressify
  where wordpressify (Math x y) = Math x ("LaTeX " ++ y)
        wordpressify x = x

任务完成。(我在此省略了类型签名,只是为了展示它可行。)

但我不想学习 Haskell!

虽然用 Haskell 编写 pandoc 过滤器最容易,但使用 pandocfilters 包在 Python 中编写它们也相当容易。该包在 PyPI 中,可以使用 pip install pandocfilterseasy_install pandocfilters 进行安装。

这是我们用 Python 编写的“砍头”过滤器:

#!/usr/bin/env python

"""
Pandoc filter to convert all level 2+ headings to paragraphs with
emphasized text.
"""

from pandocfilters import toJSONFilter, Emph, Para

def behead(key, value, format, meta):
  if key == 'Header' and value[0] >= 2:
    return Para([Emph(value[2])])

if __name__ == "__main__":
  toJSONFilter(behead)

toJSONFilter(behead) 遍历 AST 并对每个元素应用 behead 操作。如果 behead 不返回任何内容,则节点保持不变;如果它返回一个对象,则节点被替换;如果它返回一个列表,则新列表被插入。

请注意,尽管本例中未使用这些参数,但 format 提供了对目标格式的访问,而 meta 提供了对文档元数据的访问。

pandocfilters 仓库中有很多 Python 过滤器的示例。

对于 pandocfilters 的更具 Pythonic 风格的替代方案,请参阅 panflute 库。不喜欢 Python?pandocfilters 也有以下语言的移植版本:

从 pandoc 2.0 开始,pandoc 内置支持用 Lua 编写过滤器。Lua 解释器已内置在 pandoc 中,因此 Lua 过滤器无需任何额外软件即可运行。请参阅Lua 过滤器文档

包含文件

到目前为止,我们所有的转换都不涉及 IO。那么,一个脚本如何呢?它读取 Markdown 文档,找到所有带有 include 属性的内联代码块,并将其内容替换为给定文件的内容?

#!/usr/bin/env runhaskell
-- includes.hs
import Text.Pandoc.JSON
import qualified Data.Text.IO as TIO
import qualified Data.Text as T

doInclude :: Block -> IO Block
doInclude cb@(CodeBlock (id, classes, namevals) contents) =
  case lookup (T.pack "include") namevals of
       Just f     -> CodeBlock (id, classes, namevals) <$>
                      TIO.readFile (T.unpack f)
       Nothing    -> return cb
doInclude x = return x

main :: IO ()
main = toJSONFilter doInclude

请在以下内容上尝试:

Here's the pandoc README:

~~~~ {include="README"}
this will be replaced by contents of README
~~~~

Ruby 注音文本过滤器

最后,这里有一个很好的实际示例,是在 pandoc-discuss 列表中开发的。Qubyte 写道:

我感兴趣的是使用 pandoc 将我的日语 Markdown 笔记转换为排版精美的 HTML 和 (Xe)LaTeX。在 HTML5 中,ruby(通常用于在汉字上方或旁边放置文本来注音)是标准功能,并且浏览器支持正在兴起(基于 Webkit 的浏览器似乎完全支持它)。对于那些尚未支持的浏览器(特别是 Firefox),该功能会以一种很好的方式回退:将注音文本放在每个汉字旁边的括号内,这同样适用于其他输出格式。至于 (Xe)LaTeX,ruby 并不是问题。

目前,当转换为 HTML 时,我使用内联 HTML 来实现效果,但这很丑陋,并且需要大量按键,例如:

<ruby>ご<rt></rt>飯<rp>(</rp><rt>はん</rt><rp>)</rp></ruby>

设置 ご飯 “gohan”,其中“han”的注音文本位于第二个字符上方,或者如果浏览器不支持 ruby,则放在其右侧的括号中。我希望能有更像这样的东西:

r[はん](飯)

或者任何能节省按键的约定都受欢迎。

我们提出了以下脚本,它使用了一个约定:以连字符开头的 URL 的 Markdown 链接被解释为 ruby 注音文本。

[はん](-飯)
{-# LANGUAGE OverloadedStrings #-}
-- handleruby.hs
import Text.Pandoc.JSON
import System.Environment (getArgs)
import qualified Data.Text as T

handleRuby :: Maybe Format -> Inline -> Inline
handleRuby (Just format) x@(Link attr [Str ruby] (src,_)) =
  case T.uncons src of
    Just ('-',kanji)
      | format == Format "html" -> RawInline format $
        "<ruby>" <> kanji <> "<rp>(</rp><rt>" <> ruby <>
        "</rt><rp>)</rp></ruby>"
      | format == Format "latex" -> RawInline format $
        "\\ruby{" <> kanji <> "}{" <> ruby <> "}"
      | otherwise -> Str ruby
    _ -> x
handleRuby _ x = x

main :: IO ()
main = toJSONFilter handleRuby

请注意,当使用 --filter 调用脚本时,pandoc 会将其目标格式作为第一个参数传递。当函数的第一个参数类型为 Maybe Format 时,toJSONFilter 会自动将其赋值为目标格式的 JustNothing

我们编译脚本:

# first, make sure pandoc-types is installed:
cabal install --lib pandoc-types --package-env .
ghc --make handleRuby

然后运行它:

% pandoc -F ./handleRuby -t html
[はん](-飯)
^D
<p><ruby>飯<rp>(</rp><rt>はん</rt><rp>)</rp></ruby></p>
% pandoc -F ./handleRuby -t latex
[はん](-飯)
^D
\ruby{飯}{はん}

注意:要使用此功能通过 LaTeX 生成 PDF,您需要使用 --pdf-engine=xelatex,指定一个包含日文字符的 mainfont(例如“Noto Sans CJK JP”),并将 \usepackage{ruby} 添加到您的模板或 header-includes 中。

练习

  1. 将 Markdown 文档中的所有常规文本转换为大写(不触及 URL 或链接标题中的文本)。

  2. 从文档中删除所有水平线。

  3. 将所有编号列表重新编号为罗马数字。

  4. 将每个带有 dot 类的分隔代码块替换为通过对代码块内容运行 dot -Tpng(来自 graphviz)生成的图像。

  5. 查找所有带有 python 类的代码块,并使用 Python 解释器运行它们,将结果打印到控制台。

JSON 过滤器的技术细节

JSON 过滤器是任何能够消费和生成有效 pandoc JSON 文档表示的程序。本节描述了过滤器调用相关的技术细节。

参数

程序将始终以目标格式作为唯一参数被调用。像这样的 pandoc 调用:

pandoc --filter demo --to=html

将导致 pandoc 以参数 html 调用程序 demo

环境变量

Pandoc 在调用过滤器之前设置额外的环境变量。

PANDOC_VERSION
用于处理文档的 pandoc 二进制文件的版本。示例:2.11.1
PANDOC_READER_OPTIONS

传递给输入解析器的选项的 JSON 对象表示。

对象字段

abbreviations
已知缩写词集(字符串数组)。
columns
终端中的列数;一个整数。
default-image-extension
图像的默认扩展名;一个字符串。
extensions
语法扩展位字段的整数表示。
indented-code-classes
缩进代码块的默认类;字符串数组。
standalone
输入是否是带有标题的独立文档;truefalse
strip-comments
HTML 注释被去除而不是解析为原始 HTML;truefalse
tab-stop
制表符停止位的宽度(即等效空格数);整数。
track-changes
docx 的修订跟踪设置;"accept-changes""reject-changes""all-changes" 之一。

支持的解释器

传递给 --filter/-F 参数的文件应是可执行文件。但是,如果未设置可执行位,则 pandoc 会尝试根据文件扩展名猜测合适的解释器。

文件扩展名 解释器
.py python
.hs runhaskell
.pl Perl
.rb ruby
.php php
.js node
.r Rscript