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

使用 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 是段落的基本元素。类型系统中 BlockInline 之间的区别使得无法表示例如一个链接 (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

一些注意事项

  1. 第一部分构建了一个转换管道:输入字符串传递给 readMarkdown,然后生成的 Pandoc AST (doc) 由 writeRST 渲染。转换管道由 runIO “运行”——更多内容见下文。

  2. result 的类型是 Either PandocError Text。我们可以手动进行模式匹配,但在这种情况下,使用 Text.Pandoc.Error 中的 handleError 函数更简单。如果值是 Left,它会以适当的错误代码和消息退出,如果值是 Right,则返回 Text

PandocMonad 类

让我们看看 readMarkdownwriteRST 的类型

readMarkdown :: (PandocMonad m, ToSources a)
             => ReaderOptions
             -> a
             -> m Pandoc
writeRST     :: PandocMonad m
             => WriterOptions
             -> Pandoc
             -> m Text

PandocMonad m => 部分是一个类型类约束。它表示 readMarkdownwriteRST 定义的计算可以在 PandocMonad 类型类的任何实例中使用。PandocMonad 定义在 Text.Pandoc.Class 模块中。

提供了 PandocMonad 的两个实例:PandocIOPandocPure。区别在于在 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

请注意,PandocIOMonadIO 的一个实例,因此您可以使用 liftIO 在 pandoc 转换链中执行任意 IO 操作。

readMarkdown 的第二个参数是多态的,可以是 ToSources 类型类的任何实例类型。您可以使用 Text,如上例所示。但是,如果输入来自多个文件并且您希望准确跟踪源位置,您也可以使用 [(FilePath, Text)]

选项

每个读取器或写入器的第一个参数是控制读取器或写入器行为的选项:读取器的 ReaderOptions 和写入器的 WriterOptions。这些定义在 Text.Pandoc.Options 中。建议研究这些选项以了解可以调整的内容。

def(来自 Data.Default)表示每种选项的默认值。(您也可以使用 defaultWriterOptionsdefaultReaderOptions。)通常您会想使用默认值并在需要时才修改它们,例如

    writeRST def{ writerReferenceLinks = True }

一些特别重要的选项

  1. writerTemplate:默认情况下,这是 Nothing,这意味着将生成文档片段。如果您想要一个完整的文档,您需要指定 Just template,其中 template 是一个来自 Text.Pandoc.TemplatesTemplate Text,包含模板的内容(而不是路径)。

  2. readerExtensionswriterExtensions:这些指定在解析和渲染中使用的扩展。扩展定义在 Text.Pandoc.Extensions 中。

构建器

有时以编程方式构建 Pandoc 文档很有用。为了简化这一点,我们在 Text.Pandoc.Builder pandoc-types 模块中提供了便利。

由于连接列表速度较慢,我们使用特殊的类型 InlinesBlocks,它们封装了 InlineBlock 元素的 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

处理错误和警告

runIOrunPure 返回 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 -> c

Walkable 实例为大多数 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 = x

walkMwalk 的单子版本;例如,当您需要转换执行 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 x

query 用于从 AST 收集信息。它的参数是一个查询函数,该函数以某种单子类型(例如列表)生成结果。结果被连接在一起。下面是一个返回文档中链接的所有 URL 列表的示例

listURLs :: Pandoc -> [Text]
listURLs = query urls
  where urls (Link _ _ (src, _)) = [src]
        urls _                   = []

创建前端

命令行程序 pandoc 的所有功能都在 Text.Pandoc.App 模块的 convertWithOpts 中被抽象出来。因此,为 pandoc 创建一个 GUI 前端只需填充 Opts 结构并调用此函数即可。

在 Web 应用程序中使用 pandoc 的注意事项

  1. Pandoc 的解析器在某些输入上可能会表现出病态行为。因此,始终建议将 pandoc 的使用包装在超时函数中(例如 base 中的 System.Timeout.timeout),以防止拒绝服务攻击。

  2. 如果 pandoc 从不受信任的用户输入生成 HTML,则始终建议通过清理器(例如 xss-sanitize)过滤生成的 HTML,以避免安全问题。

  3. 使用 runPure 而不是 runIO 将确保 pandoc 的函数不执行任何 IO 操作(例如写入文件)。如果需要提供某些资源,runPure 可用的状态中提供了一个“虚拟环境”(参见 Text.Pandoc.Class 中的 PureState 及其相关函数)。还可以编写 PandocMonad 的自定义实例,例如,将 Wiki 资源作为文件在虚拟环境中提供,同时将 pandoc 与系统的其余部分隔离。