使用 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 ()
= do
main <- runIO $ do
result <- readMarkdown def (T.pack "[testing](url)")
doc
writeRST def doc<- handleError result
rst 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 Text
PandocMonad 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]
= reverse <$> getsCommonState stLog
getLog
-- | 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 ()
如果我们在上一节定义的转换过程中需要更详细的信息消息,我们可以这样做
<- runIO $ do
result INFO
setVerbosity <- readMarkdown def (T.pack "[testing](url)")
doc writeRST def doc
请注意,PandocIO
是 MonadIO
的一个实例,因此您可以使用 liftIO
在 pandoc 转换链中执行任意 IO 操作。
readMarkdown
的第二个参数是多态的,可以是 ToSources
类型类的任何实例类型。您可以使用 Text
,如上例所示。但是,如果输入来自多个文件并且您希望准确跟踪源位置,您也可以使用 [(FilePath, Text)]
。
选项
每个读取器或写入器的第一个参数是控制读取器或写入器行为的选项:读取器的 ReaderOptions
和写入器的 WriterOptions
。这些定义在 Text.Pandoc.Options 中。建议研究这些选项以了解可以调整的内容。
def
(来自 Data.Default)表示每种选项的默认值。(您也可以使用 defaultWriterOptions
和 defaultReaderOptions
。)通常您会想使用默认值并在需要时才修改它们,例如
= True } writeRST def{ writerReferenceLinks
一些特别重要的选项
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
= doc $ header 1 (text (T.pack "Hello!"))
mydoc <> para (emph (text (T.pack "hello world")) <> text (T.pack "."))
main :: IO ()
= print mydoc main
如果您使用 OverloadedStrings
编译指示,您可以进一步简化
= doc $ header 1 "Hello!"
mydoc <> 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
Object v) = Station <$>
parseJSON (.: "street_address" <*>
v .: "station_name" <*>
v <$> (v .:? "cards_accepted" .!= ""))
(T.words = mzero
parseJSON _
createLetter :: [Station] -> Pandoc
= doc $
createLetter stations "Dear Boss:" <>
para "Here are the CNG stations that accept Voyager cards:" <>
para "Station", plain "Address", plain "Cards accepted"]
simpleTable [plain map stationToRow stations) <>
("Your loyal servant," <>
para "JohnHancock.png" "" mempty)
plain (image where
=
stationToRow station $ name station)
[ plain (text $ address station)
, plain (text mconcat $ intersperse linebreak
, plain ($ map text $ cardsAccepted station)
]
main :: IO ()
= do
main <- BL.readFile "fuel.json"
json let letter = case decode json of
Just stations -> createLetter [s | s <- stations,
"Voyager" `elem` cardsAccepted s]
Nothing -> error "Could not decode JSON"
<- runIO (writeDocx def letter) >>= handleError
docx "letter.docx" docx
BL.writeFile 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
= runIdentity . walkM (return . f)
walk 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 -> c
Walkable
实例为大多数 Pandoc 类型组合定义。例如,Walkable Inline Block
实例允许您获取一个函数 Inline -> Inline
并将其应用于 Block
中的每个内联。而 Walkable [Inline] Pandoc
允许您获取一个函数 [Inline] -> [Inline]
并将其应用于 Pandoc
中每个最大的 Inline
列表。
这是一个提升标题级别函数的简单示例
promoteHeaderLevels :: Pandoc -> Pandoc
= walk promote
promoteHeaderLevels where promote :: Block -> Block
Header lev attr ils) = Header (lev + 1) attr ils
promote (= x promote x
walkM
是 walk
的单子版本;例如,当您需要转换执行 IO 操作、使用 PandocMonad 操作或更新内部状态时,可以使用它。下面是一个使用 State 单子为每个代码块添加唯一标识符的示例
addCodeIdentifiers :: Pandoc -> Pandoc
= evalState (walkM addCodeId doc) 1
addCodeIdentifiers doc where addCodeId :: Block -> State Int Block
CodeBlock (_,classes,kvs) code) = do
addCodeId (<- get
curId + 1)
put (curId return $ CodeBlock (show curId,classes,kvs) code
= return x addCodeId x
query
用于从 AST 收集信息。它的参数是一个查询函数,该函数以某种单子类型(例如列表)生成结果。结果被连接在一起。下面是一个返回文档中链接的所有 URL 列表的示例
listURLs :: Pandoc -> [Text]
= query urls
listURLs 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 与系统的其余部分隔离。