Pandoc 过滤器
概述
Pandoc 提供了一个接口,让用户能够编写程序(称为过滤器),这些程序可以在 Pandoc 的抽象语法树(AST)上进行操作。
Pandoc 包含了一系列的读取器和写入器。当将文档从一种格式转换到另一种格式时,文本首先由读取器解析成 Pandoc 的中间表示形式——即一个 “抽象语法树” 或简称 AST —— 然后由写入器将其转换为目标格式。Pandoc 的 AST 格式定义在模块 Text.Pandoc.Definition
中,该模块位于 pandoc-types 包 内。
“过滤器” 是在读取器和写入器之间修改 AST 的程序。
Pandoc 支持两种类型的过滤器:
- Lua 过滤器 使用 Lua 语言定义对 Pandoc AST 的转换。它们在 单独的文档 中描述。
- JSON 过滤器,在这里描述,是管道程序,它们从标准输入读取并写入标准输出,消费和产生 Pandoc AST 的 JSON 表示形式:
Lua 过滤器有几个优点。它们使用嵌入在 Pandoc 内部的 Lua 解释器,因此您无需安装任何外部软件。而且它们通常比 JSON 过滤器更快。但是如果您希望用 Lua 以外的语言编写您的过滤器,您可能会倾向于使用 JSON 过滤器。JSON 过滤器可以用任何编程语言编写。
您可以直接在管道中使用 JSON 过滤器:
但使用 --filter
选项更为方便,它可以自动处理管道连接:
要了解如何编写自己的过滤器,请继续阅读本指南。此外,在 wiki 页面 上还有一个第三方过滤器的列表。
一个简单的例子
假设你想要将 Markdown 文档中所有的二级及以上的标题替换为普通的段落,并且其中的文字用斜体表示。你会如何实现这个需求呢?
首先想到的方法可能是使用正则表达式。比如这样做:
这在大多数情况下应该是可行的。但是别忘了 ATX 风格的标题可以以不是标题文本一部分的 #
符号序列结尾:
如果文档中含有 HTML 注释或限定代码块中以 ##
开头的行又会怎样呢?
A third level heading in standard markdown
我们不希望这些行被修改。另外,对于 Setext 风格的二级标题又该如何处理?
我们也需要处理这种情况。最后,我们能确定在字符串两侧添加星号就能使其变为斜体吗?如果字符串本身已经包含了星号怎么办?这样可能会导致文字变为粗体,这不是我们想要的结果。如果它包含了一个普通的未转义的星号又会怎样?
你将如何修改你的正则表达式来处理这些情况?这将会非常复杂。
一个更好的方法是让 Pandoc 处理解析工作,然后在文档被写入之前修改抽象语法树(AST)。为此我们可以使用过滤器。
为了查看当 Pandoc 解析我们的文本时会产生什么样的 AST,我们可以使用 Pandoc 的 native
输出格式:
一个 Pandoc
文档由一个 Meta
块(包含诸如标题、作者和日期等元数据)和一系列 Block
元素组成。在这个例子中,我们有两个 Block
,一个是 Header
,另一个是 Para
。每个 Block
的内容都是一系列 Inline
元素。关于 Pandoc 的 AST 更多细节,请参阅 Hackage 上 Text.Pandoc.Definition
的文档。
我们可以使用 Haskell 创建一个 JSON 过滤器来转换这个 AST,将每个等级大于等于 2
的 Header
块替换为一个其内容被包裹在 Emph
内联元素中的 Para
:
toJSONFilter
函数做了两件事。首先,它将 behead
函数(该函数映射 Block -> Block
)提升为对整个 Pandoc
AST 的转换,遍历 AST 并转换每一个块。其次,它将这个 Pandoc -> Pandoc
的转换包装上必要的 JSON 序列化和反序列化,产生一个从标准输入消费 JSON 并向标准输出产生 JSON 的可执行程序。
要使用这个过滤器,先让它具有可执行权限:
然后运行:
(还需要安装 pandoc-types
在本地包仓库中。使用 cabal-install 可以通过命令 cabal v2-update && cabal v2-install --lib pandoc-types --package-env .
完成。)
或者我们可以编译这个过滤器:
需要注意的是,如果过滤器位于系统路径(PATH)中,则不需要初始的
./
。同样要注意的是命令行可以包含多个--filter
实例:这些过滤器将按顺序应用。
针对 WordPress 的 LaTeX
另一个简单的例子。WordPress 博客要求 LaTeX 数学公式使用特殊的格式。与直接使用 $e=mc^2$
不同,你需要使用 $LaTeX e=mc^2$
。我们如何相应地转换一个 Markdown 文档呢?
再次,使用正则表达式很难可靠地完成这项工作。一个 $
可能是一个常规的货币指示符,也可能出现在注释、代码块或内联代码范围内。我们只想找到那些开始 LaTeX 数学公式的 $
。如果我们有一个解析器就好了……
我们确实有。Pandoc 已经能够提取 LaTeX 数学公式,因此我们可以这样操作:
任务完成。(这里省略了类型签名,只是为了展示它可以做到这一点。)
但是我不想学习 Haskell!
虽然使用 Haskell 编写 Pandoc 过滤器最为方便,但使用 Python 和 pandocfilters
包也相当容易。该包已在 PyPI 中,可以通过 pip install pandocfilters
或 easy_install pandocfilters
安装。
以下是用 Python 编写的 “去头” 过滤器:
toJSONFilter(behead)
遍历 AST 并将 behead
动作应用于每个元素。如果 behead
返回空值,节点保持不变;如果返回对象,则替换节点;如果返回列表,则将新列表插入。
请注意,尽管在这个示例中没有使用这些参数,format
提供了访问目标格式的途径,而 meta
提供了访问文档元数据的途径。
在 pandocfilters 存储库 中有许多 Python 过滤器的例子。
对于更符合 Python 风格的替代方案,可以考虑使用 panflute 库。
不喜欢 Python?还有其他语言版本的 pandocfilters
:
从 Pandoc 2.0 开始,Pandoc 内置了使用 Lua 编写过滤器的支持。Lua 解释器已内置到 Pandoc 中,因此 Lua 过滤器无需额外软件即可运行。请参阅 Lua 过滤器文档。
包含文件
目前为止,我们所做的转换都没有涉及到输入输出(IO)。那么,有没有一种脚本可以读取一个 Markdown 文档,找出所有具有 include
属性的内联代码块,并用指定文件的实际内容替换它们的内容呢?
在以下操作中尝试此操作:
移除链接
如果我们想要从文档中移除每一个链接,但保留链接的文本内容,应该怎么做呢?
请注意,delink
不能是一个类型为 Inline -> Inline
的函数,因为我们想要用来替换链接的不是一个单一的 Inline
元素,而是一个这样的元素列表。因此我们将 delink
设计为从一个 Inline
元素到一个 Inline
元素列表的函数。toJSONFilter
仍然可以将这个函数提升为类型为 Pandoc -> Pandoc
的转换。
这段话的意思是,在处理 Pandoc 的抽象语法树时,如果我们要将链接(Link)替换为其文本内容,由于链接的文本内容可能包含多个 Inline
元素(例如单词间可能有空格或其他标记),我们需要返回一个 Inline
元素的列表而不是单个 Inline
元素。即使这样,toJSONFilter
依然可以将这样的函数提升为在整个文档级别进行转换的功能。
以下是去除 HTML 标签并翻译成中文的内容:
一个用于 ruby 文本的过滤器
最后,这里有一个很好的实际应用例子,是在 pandoc-discuss 列表中开发的。Qubyte 写道:
我对使用 pandoc 将我的日语 Markdown 笔记转换成美观的 HTML 和(Xe)LaTeX 格式很感兴趣。随着 HTML5 的出现,ruby(通常用于给汉字注音,通过在汉字上方或旁边放置文本)成为标准,并且浏览器的支持正在逐渐增强(基于 WebKit 的浏览器似乎已经完全支持它)。对于那些尚未支持它的浏览器(特别是 Firefox),该特性会以一种优雅的方式退化,即在每个汉字旁边用括号包含注音,这对于其他输出格式也很适用。(Xe)LaTeX 中的 ruby 不是问题。
目前,我在转换成 HTML 时使用内联 HTML 来实现这个效果,但这看起来很丑陋并且需要很多按键操作,例如:
这样设置 “ご飯”(gohan),其中 “はん” 被注音在第二个字符之上,或者如果浏览器不支持 ruby,则显示在字符旁边的括号内。我希望有一种更简洁的方式来表示:
或者任何节省按键的操作都会受到欢迎。
我们提出了以下脚本,它使用了这样的约定:Markdown 链接中 URL 以破折号开头的被视为 ruby:
注意,当使用 --filter
调用脚本时,pandoc 会传递目标格式作为第一个参数。当函数的第一个参数类型为 Maybe Format
时,toJSONFilter
会自动为其分配 Just
目标格式或 Nothing
。
我们编译我们的脚本:
然后运行它:
注意:要通过 LaTeX 生成 PDF,你需要使用
--pdf-engine=xelatex
,指定一个包含日文字符的主字体(例如 “Noto Sans CJK JP” ),并在模板或 header-includes 中添加\usepackage{ruby}
。
练习
- 将 Markdown 文档中的所有常规文本转换为全大写(不改变 URL 或链接标题中的文本)。
- 从文档中移除所有的水平分割线。
- 将所有编号列表重新编号为罗马数字。
- 将所有带有
dot
类别的分隔代码块替换为由dot -Tpng
(来自 graphviz)在代码块内容上运行生成的图像。 - 找出所有带有
python
类别的代码块,并使用 Python 解释器运行它们,将结果打印到控制台。
JSON 过滤器的技术细节
JSON 过滤器是指任何能够消费和产生有效的 Pandoc JSON 文档表示的程序。本节描述了与调用过滤器相关的技术细节。
参数
程序总是会被调用,并且目标格式作为唯一的参数。例如,一个像这样的 Pandoc 命令:
将会导致 Pandoc 调用程序 demo
并传入参数 html
。
环境变量
Pandoc 在调用过滤器之前会设置额外的环境变量。
PANDOC_VERSION
: 使用来处理文档的 Pandoc 版本号。例如:2.11.1
。PANDOC_READER_OPTIONS
: 传递给输入解析器选项的 JSON 对象表示。对象字段包括:
abbreviations
: 已知缩写的集合(字符串数组)。columns
: 终端中的列数;一个整数。default-image-extension
: 图像的默认扩展名;一个字符串。extensions
: 语法扩展位字段的整数表示。indented-code-classes
: 缩进代码块的默认类别;字符串数组。standalone
: 输入是否为包含头部的独立文档;布尔值true
或false
。strip-comments
: HTML 注释被剥离而不是作为原始 HTML 解析;布尔值true
或false
。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 |