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,将每个级别 >= 2
的 Header
块替换为一个 Para
块,其内容包装在一个 Emph
内联元素中
#!/usr/bin/env runhaskell
-- behead.hs
import Text.Pandoc.JSON
main :: IO ()
= toJSONFilter behead
main
behead :: Block -> Block
Header n _ xs) | n >= 2 = Para [Emph xs]
behead (= x behead 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
= toJSONFilter wordpressify
main where wordpressify (Math x y) = Math x ("LaTeX " ++ y)
= x wordpressify x
任务完成。(我在此省略了类型签名,只是为了展示它可行。)
但我不想学习 Haskell!
虽然用 Haskell 编写 pandoc 过滤器最容易,但使用 pandocfilters
包在 Python 中编写它们也相当容易。该包在 PyPI 中,可以使用 pip install pandocfilters
或 easy_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
@(CodeBlock (id, classes, namevals) contents) =
doInclude cbcase lookup (T.pack "include") namevals of
Just f -> CodeBlock (id, classes, namevals) <$>
TIO.readFile (T.unpack f)Nothing -> return cb
= return x
doInclude x
main :: IO ()
= toJSONFilter doInclude main
请在以下内容上尝试:
Here's the pandoc README:
~~~~ {include="README"}
this will be replaced by contents of README
~~~~
删除链接
如果我们想从文档中删除所有链接,但保留链接的文本,该怎么办?
#!/usr/bin/env runhaskell
-- delink.hs
import Text.Pandoc.JSON
= toJSONFilter delink
main
delink :: Inline -> [Inline]
Link _ txt _) = txt
delink (= [x] delink x
请注意,delink
不能是类型为 Inline -> Inline
的函数,因为我们想用来替换链接的不是单个 Inline
元素,而是一个列表。因此,我们将 delink
设为一个从 Inline
元素到 Inline
元素列表的函数。toJSONFilter
仍然可以将此函数提升为类型为 Pandoc -> Pandoc
的转换。
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
Just format) x@(Link attr [Str ruby] (src,_)) =
handleRuby (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
_ = x
handleRuby _ x
main :: IO ()
= toJSONFilter handleRuby main
请注意,当使用 --filter
调用脚本时,pandoc 会将其目标格式作为第一个参数传递。当函数的第一个参数类型为 Maybe Format
时,toJSONFilter
会自动将其赋值为目标格式的 Just
或 Nothing
。
我们编译脚本:
# 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 中。
练习
将 Markdown 文档中的所有常规文本转换为大写(不触及 URL 或链接标题中的文本)。
从文档中删除所有水平线。
将所有编号列表重新编号为罗马数字。
将每个带有
dot
类的分隔代码块替换为通过对代码块内容运行dot -Tpng
(来自 graphviz)生成的图像。查找所有带有
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
- 输入是否是带有标题的独立文档;
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 |