使用 pandoc API
Pandoc 可以作为 Haskell 库使用,以编写您自己的转换工具或为 Web 应用程序提供支持。本文档介绍了如何使用 pandoc API。
详细的 API 文档(函数和类型级别)可在 https://hackage.haskell.org/package/pandoc 获取。
Pandoc 的架构
Pandoc 的结构是一组读取器,它们将各种输入格式转换为表示结构化文档的抽象语法树 (Pandoc AST),以及一组写入器,它们将此 AST 渲染为各种输出格式。示意图如下
[input format] ==reader==> [Pandoc AST] ==writer==> [output format]
这种架构允许 pandoc 通过 M 个读取器和 N 个写入器执行 M × N 种转换。
Pandoc AST 在 pandoc-types 包中定义。您应该首先查看 Text.Pandoc.Definition 的 Haddock 文档。正如您将看到的,一个 Pandoc 由一些元数据和一组 Block 组成。Block 有多种类型,包括 Para(段落)、Header(节标题)和 BlockQuote。一些 Block(如 BlockQuote)包含 Block 列表,而另一些(如 Para)包含 Inline 列表,还有一些(如 CodeBlock)包含纯文本或无内容。Inline 是段落的基本元素。类型系统中 Block 和 Inline 之间的区别使得无法表示例如一个链接 (Inline) 的链接文本是引用块 (Block)。这种表达上的限制在很大程度上是一种帮助而不是障碍,因为 pandoc 支持的许多格式都有类似的限制。
探索 pandoc AST 的最佳方式是使用 pandoc -t native,它将显示与某些 Markdown 输入对应的 AST
% echo -e "1. *foo*\n2. bar" | pandoc -t native
[OrderedList (1,Decimal,Period)
[[Plain [Emph [Str "foo"]]]
,[Plain [Str "bar"]]]]
一个简单的例子
下面是一个使用 pandoc 读取器和写入器执行转换的简单示例
import Text.Pandoc
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
main :: IO ()
main = do
result <- runIO $ do
doc <- readMarkdown def (T.pack "[testing](url)")
writeRST def doc
rst <- handleError result
TIO.putStrLn rst一些注意事项
第一部分构建了一个转换管道:输入字符串传递给
readMarkdown,然后生成的 Pandoc AST (doc) 由writeRST渲染。转换管道由runIO“运行”——更多内容见下文。result的类型是Either PandocError Text。我们可以手动进行模式匹配,但在这种情况下,使用 Text.Pandoc.Error 中的handleError函数更简单。如果值是Left,它会以适当的错误代码和消息退出,如果值是Right,则返回Text。
PandocMonad 类
让我们看看 readMarkdown 和 writeRST 的类型
readMarkdown :: (PandocMonad m, ToSources a)
=> ReaderOptions
-> a
-> m Pandoc
writeRST :: PandocMonad m
=> WriterOptions
-> Pandoc
-> m TextPandocMonad m => 部分是一个类型类约束。它表示 readMarkdown 和 writeRST 定义的计算可以在 PandocMonad 类型类的任何实例中使用。PandocMonad 定义在 Text.Pandoc.Class 模块中。
提供了 PandocMonad 的两个实例:PandocIO 和 PandocPure。区别在于在 PandocIO 中运行的计算允许执行 IO(例如,读取文件),而在 PandocPure 中运行的计算则没有任何副作用。PandocPure 对于沙盒环境很有用,当您希望防止用户执行任何恶意操作时。要在 PandocIO 中运行转换,请使用 runIO(如上所述)。要在 PandocPure 中运行,请使用 runPure。
正如您从 Haddocks 中看到的,Text.Pandoc.Class 导出了许多可以在 PandocMonad 的任何实例中使用的辅助函数。例如
-- | Get the verbosity level.
getVerbosity :: PandocMonad m => m Verbosity
-- | Set the verbosity level.
setVerbosity :: PandocMonad m => Verbosity -> m ()
-- Get the accumulated log messages (in temporal order).
getLog :: PandocMonad m => m [LogMessage]
getLog = reverse <$> getsCommonState stLog
-- | Log a message using 'logOutput'. Note that 'logOutput' is
-- called only if the verbosity level exceeds the level of the
-- message, but the message is added to the list of log messages
-- that will be retrieved by 'getLog' regardless of its verbosity level.
report :: PandocMonad m => LogMessage -> m ()
-- | Fetch an image or other item from the local filesystem or the net.
-- Returns raw content and maybe mime type.
fetchItem :: PandocMonad m
=> Text
-> m (B.ByteString, Maybe MimeType)
-- Set the resource path searched by 'fetchItem'.
setResourcePath :: PandocMonad m => [FilePath] -> m ()如果我们在上一节定义的转换过程中需要更详细的信息消息,我们可以这样做
result <- runIO $ do
setVerbosity INFO
doc <- readMarkdown def (T.pack "[testing](url)")
writeRST def doc请注意,PandocIO 是 MonadIO 的一个实例,因此您可以使用 liftIO 在 pandoc 转换链中执行任意 IO 操作。
readMarkdown 的第二个参数是多态的,可以是 ToSources 类型类的任何实例类型。您可以使用 Text,如上例所示。但是,如果输入来自多个文件并且您希望准确跟踪源位置,您也可以使用 [(FilePath, Text)]。
选项
每个读取器或写入器的第一个参数是控制读取器或写入器行为的选项:读取器的 ReaderOptions 和写入器的 WriterOptions。这些定义在 Text.Pandoc.Options 中。建议研究这些选项以了解可以调整的内容。
def(来自 Data.Default)表示每种选项的默认值。(您也可以使用 defaultWriterOptions 和 defaultReaderOptions。)通常您会想使用默认值并在需要时才修改它们,例如
writeRST def{ writerReferenceLinks = True }一些特别重要的选项
writerTemplate:默认情况下,这是Nothing,这意味着将生成文档片段。如果您想要一个完整的文档,您需要指定Just template,其中template是一个来自 Text.Pandoc.Templates 的Template Text,包含模板的内容(而不是路径)。readerExtensions和writerExtensions:这些指定在解析和渲染中使用的扩展。扩展定义在 Text.Pandoc.Extensions 中。
构建器
有时以编程方式构建 Pandoc 文档很有用。为了简化这一点,我们在 Text.Pandoc.Builder pandoc-types 模块中提供了便利。
由于连接列表速度较慢,我们使用特殊的类型 Inlines 和 Blocks,它们封装了 Inline 和 Block 元素的 Sequence。它们是 Monoid 类型类的实例,可以轻松连接
import Text.Pandoc.Builder
mydoc :: Pandoc
mydoc = doc $ header 1 (text (T.pack "Hello!"))
<> para (emph (text (T.pack "hello world")) <> text (T.pack "."))
main :: IO ()
main = print mydoc如果您使用 OverloadedStrings 编译指示,您可以进一步简化
mydoc = doc $ header 1 "Hello!"
<> para (emph "hello world" <> ".")这是一个更实际的例子。假设您的老板说:给我写一封 Word 信,列出芝加哥所有接受 Voyager 卡的加油站。您找到了一些 JSON 数据,格式如下 (fuel.json)
[ {
"state" : "IL",
"city" : "Chicago",
"fuel_type_code" : "CNG",
"zip" : "60607",
"station_name" : "Clean Energy - Yellow Cab",
"cards_accepted" : "A D M V Voyager Wright_Exp CleanEnergy",
"street_address" : "540 W Grenshaw"
}, ...然后使用 aeson 和 pandoc 解析 JSON 并创建 Word 文档
{-# LANGUAGE OverloadedStrings #-}
import Text.Pandoc.Builder
import Text.Pandoc
import Data.Monoid ((<>), mempty, mconcat)
import Data.Aeson
import Control.Applicative
import Control.Monad (mzero)
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import Data.List (intersperse)
data Station = Station{
address :: T.Text
, name :: T.Text
, cardsAccepted :: [T.Text]
} deriving Show
instance FromJSON Station where
parseJSON (Object v) = Station <$>
v .: "street_address" <*>
v .: "station_name" <*>
(T.words <$> (v .:? "cards_accepted" .!= ""))
parseJSON _ = mzero
createLetter :: [Station] -> Pandoc
createLetter stations = doc $
para "Dear Boss:" <>
para "Here are the CNG stations that accept Voyager cards:" <>
simpleTable [plain "Station", plain "Address", plain "Cards accepted"]
(map stationToRow stations) <>
para "Your loyal servant," <>
plain (image "JohnHancock.png" "" mempty)
where
stationToRow station =
[ plain (text $ name station)
, plain (text $ address station)
, plain (mconcat $ intersperse linebreak
$ map text $ cardsAccepted station)
]
main :: IO ()
main = do
json <- BL.readFile "fuel.json"
let letter = case decode json of
Just stations -> createLetter [s | s <- stations,
"Voyager" `elem` cardsAccepted s]
Nothing -> error "Could not decode JSON"
docx <- runIO (writeDocx def letter) >>= handleError
BL.writeFile "letter.docx" docx
putStrLn "Created letter.docx"瞧!您无需使用 Word,也无需查看数据就写好了这封信。
数据文件
Pandoc 有许多数据文件,可以在仓库的 data/ 子目录中找到。这些文件随 pandoc 一起安装(或者,如果 pandoc 是用 embed_data_files 标志编译的,则它们嵌入在二进制文件中)。您可以使用 Text.Pandoc.Class 中的 readDataFile 检索数据文件。readDataFile 将首先在“用户数据目录”(setUserDataDir, getUserDataDir)中查找文件,如果未找到,它将返回系统默认安装的文件。要强制使用默认文件,请使用 setUserDataDir Nothing。
元数据文件
Pandoc 可以向文档添加元数据,如用户指南所述。与数据文件类似,可以使用 Text.Pandoc.Class 中的 readMetadataFile 检索元数据 YAML 文件。readMetadataFile 将首先在工作目录中查找文件,如果未找到,它将在用户数据目录(setUserDataDir, getUserDataDir)的 metadata 子目录中查找。
模板
Pandoc 有自己的模板系统,如用户指南所述。要检索系统的默认模板,请使用 Text.Pandoc.Templates 中的 getDefaultTemplate。请注意,这会首先在用户数据目录的 templates 子目录中查找,允许用户覆盖系统默认值。如果您想禁用此行为,请使用 setUserDataDir Nothing。
要渲染模板,请使用 renderTemplate',它接受两个参数:一个模板 (Text) 和一个上下文 (任何 ToJSON 的实例)。如果您想从 Pandoc 文档的元数据部分创建上下文,请使用 Text.Pandoc.Writers.Shared 中的 metaToJSON'。如果您还想合并变量中的值,请改用 metaToJSON,并确保在 WriterOptions 中设置了 writerVariables。
处理错误和警告
runIO 和 runPure 返回 Either PandocError a。所有在运行 PandocMonad 计算时引发的错误都将被捕获并作为 Left 值返回,以便调用程序可以处理它们。要查看 PandocError 的构造函数,请参阅 Text.Pandoc.Error 的文档。
要在 PandocMonad 计算中引发 PandocError,请使用 throwError。
除了会停止转换管道执行的错误之外,还可以生成信息性消息。使用 Text.Pandoc.Class 中的 report 来发出 LogMessage。有关 LogMessage 构造函数的列表,请参阅 Text.Pandoc.Logging。请注意,每种日志消息类型都与一个详细程度级别相关联。详细程度级别(setVerbosity/getVerbosity)决定报告是否会打印到标准错误(在 PandocIO 中运行时),但无论详细程度级别如何,所有报告的消息都内部存储,并可以使用 getLog 检索。
遍历 AST
遍历 Pandoc AST 通常很有用,无论是为了提取信息(例如,文档中链接的所有 URL 是什么?所有代码示例都编译了吗?)还是为了转换文档(例如,提高每个章节标题的级别,删除强调,或用图片替换特殊标记的代码块)。为了使这更容易、更高效,pandoc-types 包含一个模块 Text.Pandoc.Walk。
这是重要的文档
class Walkable a b where
-- | @walk f x@ walks the structure @x@ (bottom up) and replaces every
-- occurrence of an @a@ with the result of applying @f@ to it.
walk :: (a -> a) -> b -> b
walk f = runIdentity . walkM (return . f)
-- | A monadic version of 'walk'.
walkM :: (Monad m, Functor m) => (a -> m a) -> b -> m b
-- | @query f x@ walks the structure @x@ (bottom up) and applies @f@
-- to every @a@, appending the results.
query :: Monoid c => (a -> c) -> b -> cWalkable 实例为大多数 Pandoc 类型组合定义。例如,Walkable Inline Block 实例允许您获取一个函数 Inline -> Inline 并将其应用于 Block 中的每个内联。而 Walkable [Inline] Pandoc 允许您获取一个函数 [Inline] -> [Inline] 并将其应用于 Pandoc 中每个最大的 Inline 列表。
这是一个提升标题级别函数的简单示例
promoteHeaderLevels :: Pandoc -> Pandoc
promoteHeaderLevels = walk promote
where promote :: Block -> Block
promote (Header lev attr ils) = Header (lev + 1) attr ils
promote x = xwalkM 是 walk 的单子版本;例如,当您需要转换执行 IO 操作、使用 PandocMonad 操作或更新内部状态时,可以使用它。下面是一个使用 State 单子为每个代码块添加唯一标识符的示例
addCodeIdentifiers :: Pandoc -> Pandoc
addCodeIdentifiers doc = evalState (walkM addCodeId doc) 1
where addCodeId :: Block -> State Int Block
addCodeId (CodeBlock (_,classes,kvs) code) = do
curId <- get
put (curId + 1)
return $ CodeBlock (show curId,classes,kvs) code
addCodeId x = return xquery 用于从 AST 收集信息。它的参数是一个查询函数,该函数以某种单子类型(例如列表)生成结果。结果被连接在一起。下面是一个返回文档中链接的所有 URL 列表的示例
listURLs :: Pandoc -> [Text]
listURLs = query urls
where urls (Link _ _ (src, _)) = [src]
urls _ = []创建前端
命令行程序 pandoc 的所有功能都在 Text.Pandoc.App 模块的 convertWithOpts 中被抽象出来。因此,为 pandoc 创建一个 GUI 前端只需填充 Opts 结构并调用此函数即可。
在 Web 应用程序中使用 pandoc 的注意事项
Pandoc 的解析器在某些输入上可能会表现出病态行为。因此,始终建议将 pandoc 的使用包装在超时函数中(例如
base中的System.Timeout.timeout),以防止拒绝服务攻击。如果 pandoc 从不受信任的用户输入生成 HTML,则始终建议通过清理器(例如
xss-sanitize)过滤生成的 HTML,以避免安全问题。使用
runPure而不是runIO将确保 pandoc 的函数不执行任何 IO 操作(例如写入文件)。如果需要提供某些资源,runPure可用的状态中提供了一个“虚拟环境”(参见 Text.Pandoc.Class 中的PureState及其相关函数)。还可以编写PandocMonad的自定义实例,例如,将 Wiki 资源作为文件在虚拟环境中提供,同时将 pandoc 与系统的其余部分隔离。