ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

使用 Pandoc Haskell API 构建文档转换工具:从 PandocMonad 到 AST 遍历的完整指南

使用 Pandoc Haskell API 构建文档转换工具:从 PandocMonad 到 AST 遍历的完整指南 使用 Pandoc Haskell API 构建文档转换工具从 PandocMonad 到 AST 遍历的完整指南【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc导读Pandoc 不仅是一个命令行转换器pandoc更是一套以 Haskell 库形式提供的通用标记语言转换框架。本文基于仓库中的 doc/using-the-pandoc-api.md 官方文档结合 src/Text/Pandoc 下的源码实现系统讲解如何将 Pandoc 作为 Haskell 库使用理解其读取器—AST—写入器的架构、掌握PandocMonad类型类与PandocIO/PandocPure两种运行环境、熟练使用Text.Pandoc.Builder程序化构建文档、遍历并变换 Pandoc AST以及安全地在 Web 应用等场景中集成 Pandoc 转换能力。读完本文你将具备编写自定义文档转换工具、为 Pandoc 创建图形化前端、以及利用纯函数式运行环境实现沙箱化转换的实战能力。Pandoc 的架构读取器、AST 与写入器Pandoc 的核心架构非常简洁一组**读取器readers将各种输入格式翻译成一颗抽象语法树即Pandoc AST表示一份结构化文档然后一组写入器writers**将这棵 AST 渲染为各种输出格式。用官方文档中的图示表达[input format] reader [Pandoc AST] writer [output format]这一架构的最大优势在于只需实现 M 个读取器和 N 个写入器就能完成M × N种格式转换组合而无需为每对格式单独编写转换代码。从仓库源码可以印证这一点src/Text/Pandoc/Readers 目录下提供了 Markdown、LaTeX、HTML、Docx、Org、RST、JATS、MediaWiki、Typst 等数十种读取器src/Text/Pandoc/Writers 目录下则提供了相应的写入器例如 writeDocx、writeRST 等。Pandoc AST 的数据结构Pandoc AST 定义在独立的pandoc-types包中核心模块为Text.Pandoc.Definition。一个Pandoc文档由**元数据Meta**和一组Block构成Block是块级元素包括Para段落、Header章节标题、BlockQuote引用块等部分Block如BlockQuote内部嵌套的是Block列表另一部分Block如Para内部包含的是Inline行内元素列表还有的Block如CodeBlock只包含纯文本或什么都不包含。Inline是段落的基本构成元素。类型系统对Block与Inline的区分使得某些非法结构在编译期就无法表达——例如一个链接Inline的链接文字不可能是引用块Block。这种表达上的限制在多数情况下反而是帮助因为 Pandoc 所支持的许多格式本身就存在类似的层级限制。用pandoc -t native探索 AST官方文档推荐用pandoc -t native来直观观察 AST。在仓库中构建好 Pandoc 后将 Markdown 输入管道给该命令即可看到对应的 AST 文本表示% echo -e 1. *foo*\n2. bar | pandoc -t native [OrderedList (1,Decimal,Period) [[Plain [Emph [Str foo]]] ,[Plain [Str bar]]]]可以看到一个有序列表被解析为OrderedList其编号样式为(1,Decimal,Period)每个列表项是Plain块其中*foo*被表示为Emph [Str foo]。这是调试读取器行为、理解 AST 结构最直接的手段。第一个转换示例读取 Markdown输出 RST下面是官方文档给出的最小可用示例——把一段 Markdown 字符串转换为 reStructuredTextimport 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) writeRST def doc rst - handleError result TIO.putStrLn rst这里有三个关键点需要理解转换管线的组装do块内部构成了一个完整的转换管线——输入字符串先交给readMarkdown得到的 Pandoc ASTdoc再交给writeRST渲染。整个管线最后由runIO运行详见下一节关于PandocMonad的讨论。结果的错误处理result的类型是Either PandocError Text。虽然可以手动模式匹配但更简洁的做法是使用Text.Pandoc.Error模块中的handleError函数——若值是Left它将以合适的错误码和错误信息退出程序若值是Right则直接取出其中的Text。def即默认选项readMarkdown与writeRST的第一个参数分别是ReaderOptions与WriterOptionsdef来自Data.Default表示取默认值。关于选项的定制本文后面的Reader 与 Writer 的选项一节会详细展开。从源码看readMarkdown 与writeRST都被设计为通用的PandocMonad计算这正是指向下文主题的线索。PandocMonad 类型类一切转换的抽象底座观察readMarkdown与writeRST的类型签名readMarkdown :: (PandocMonad m, ToSources a) ReaderOptions - a - m Pandoc writeRST :: PandocMonad m WriterOptions - Pandoc - m TextPandocMonad m 是一个类型类约束它表明这两个函数定义的计算可以在PandocMonad类型类的任意实例中运行。PandocMonad定义于 src/Text/Pandoc/Class.hs 及其子模块 src/Text/Pandoc/Class/PandocMonad.hs 中其类头为class (Functor m, Applicative m, Monad m, MonadError PandocError m) PandocMonad m where也就是说任何PandocMonad实例都自带MonadError PandocError能力这为统一的错误传播机制奠定了基础。两个内置实例PandocIO 与 PandocPurePandoc 提供了两个开箱即用的PandocMonad实例实例运行函数副作用典型场景PandocIOrunIO :: PandocIO a - IO (Either PandocError a)允许执行 IO如读取文件、访问网络常规命令行工具、桌面应用PandocPurerunPure :: PandocPure a - Either PandocError a完全无副作用沙箱环境、Web 服务、防止恶意输入两者在 src/Text/Pandoc/Class/PandocIO.hs 与 src/Text/Pandoc/Class/PandocPure.hs 中分别实现instance PandocMonad PandocIO与instance PandocMonad PandocPure。官方文档特别强调PandocPure适合需要阻止用户做任何恶意操作的沙箱化场景——它内部用状态模拟了文件系统、时间等环境因此读取文件、获取当前时间这类IO操作在其中都以纯函数方式完成。另外PandocIO是MonadIO的实例因此在转换链中可以直接用liftIO执行任意的 IO 操作这为在转换过程中穿插自定义副作用提供了便利。PandocMonad 提供的辅助函数Text.Pandoc.Class.PandocMonad 导出了一大批可在任意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 ()这些函数在源码中均有对应实现。例如 report 的实现逻辑是先获取当前stVerbosity仅当消息级别不高于当前冗余度时才调用logOutput输出但无论冗余度如何消息都会被追加进日志列表stLog供getLog稍后检索——这一点对调试非常有价值。getVerbosity/setVerbosity通过modifyCommonState读写CommonState中的stVerbosity字段fetchItem则按stResourcePath设置的路径列表搜索本地文件系统必要时还会发起网络请求PandocIO实例中。如果想让前面那个转换示例输出更详细的信息消息只需在管线开头加上setVerbosityresult - runIO $ do setVerbosity INFO doc - readMarkdown def (T.pack testing) writeRST def docToSources输入类型的多态readMarkdown的第二个参数是多态的它可以是任何ToSources类型类的实例。既可以直接用Text如上面的示例也可以用[(FilePath, Text)]——当输入来自多个文件、且希望精确跟踪各部分的源码位置source positions时元组列表形式尤为有用。Reader 与 Writer 的选项精确控制行为每个读取器/写入器的第一个参数都是选项类型读取器用ReaderOptions写入器用WriterOptions二者定义于 src/Text/Pandoc/Options.hs。官方文档建议仔细研究这些选项因为它们几乎涵盖了所有可调行为。def来自Data.Default表示每种选项的默认值也可以显式使用defaultWriterOptions与defaultReaderOptions。通常的做法是先用默认值仅在需要时通过记录更新语法修改个别字段例如writeRST def{ writerReferenceLinks True }几个尤其重要的选项官方文档重点提示了以下两类选项writerTemplate默认值为Nothing此时输出的是文档片段fragment而非完整文档。若要生成完整文档需要传入Just template其中template是类型为Template Text来自Text.Pandoc.Templates的模板内容本身不是模板文件路径。readerExtensions与writerExtensions分别指定解析与渲染时启用的扩展。所有扩展都定义在 src/Text/Pandoc/Extensions.hs 中。借助它们可以精确控制诸如表格语法风格、脚注、智能标点等 Markdown 变体行为——这与命令行pandoc -f markdownfootnotes的扩展开关机制完全对应。Builder程序化构建 Pandoc 文档有时我们不是转换文档而是要从零开始构造文档。为此pandoc-types提供了 Text.Pandoc.Builder 模块。由于直接拼接列表很慢Builder 使用了特殊的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 .)实战案例用 JSON 数据生成 Word 信件官方文档给出一个完整的实战例子老板要求写一封 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解析 JSON、pandoc构建文档、writeDocx写出 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这个示例集中展示了 Builder 的常用组合子para段落、plain无段落间距的纯文本块、simpleTable简单表格表头与行数据均以块列表表达、linebreak换行、image图片这里以空文字mempty作标题以及doc将Blocks包装成完整Pandoc文档。数据侧则以 aeson 的FromJSON实例完成 JSON 到Station的映射并用列表推导过滤出接受 Voyager 卡的站点——整个过程完全不依赖 Word 软件也不用人眼翻阅数据。对应的 DOCX 写出函数 writeDocx 在仓库中定义于Text.Pandoc.Writers.Docx模块。数据文件Data FilesPandoc 自带一批数据文件存放于仓库的 data 子目录下——其中包括各格式的默认模板data/templates、本地化翻译data/translations、默认 CSL 引用样式data/default.csl、EPUB 样式表data/epub.css等。这些文件随 Pandoc 一起安装若 Pandoc 以embed_data_files编译标志构建它们会被直接嵌入二进制文件。程序可通过Text.Pandoc.Class中的readDataFile获取数据文件。其查找逻辑实现于 src/Text/Pandoc/Data.hs为先在用户数据目录由setUserDataDir/getUserDataDir管理中查找若未找到则返回系统默认安装的版本。若想强制始终使用默认文件调用setUserDataDir Nothing即可。元数据文件Metadata FilesPandoc 可以按用户指南MANUAL.txt所述向文档添加元数据。与数据文件类似元数据 YAML 文件可用Text.Pandoc.Class中的readMetadataFile读取其查找顺序见 src/Text/Pandoc/Class/PandocMonad.hs 中的实现为先在工作目录查找若未找到再到用户数据目录的metadata子目录中查找。这对在多文档项目间共享 YAML 元数据如作者、机构、参考文献元数据非常实用。模板系统Pandoc 拥有自己的模板系统细节记载于用户指南MANUAL.txt。模板以纯文本编写内含变量占位符渲染时由上下文context填充。获取默认模板使用Text.Pandoc.Templates模块的getDefaultTemplate。注意它优先在用户数据目录的templates子目录中查找从而允许用户覆盖系统默认模板若想禁用此行为同样调用setUserDataDir Nothing。仓库中每种输出格式的默认模板可见于 data/templates如default.html5、default.latex、default.docx等。渲染模板renderTemplate接受两个参数——模板Text与上下文任意ToJSON实例。若要从 Pandoc 文档的元数据部分创建上下文使用Text.Pandoc.Writers.Shared中的metaToJSON若还需要并入变量variables的值则改用metaToJSON并确保WriterOptions中设置了writerVariables。版本说明以上函数名出自官方文档。在当前仓库源码中模板渲染相关函数如renderTemplate与上下文构造函数如 metaToContext的签名已随pandoc-types的演进有所调整具体以你所用版本的 Haddock 文档为准。错误与警告处理runIO与runPure都返回Either PandocError a。PandocMonad计算中抛出的所有错误都会被捕获并以Left值返回给调用方处理要查看PandocError的所有构造子可查阅 src/Text/Pandoc/Error.hs其中定义了PandocIOError、PandocParseError、PandocCouldNotFindDataFileError、PandocUnknownReaderError等数十种错误。若要在PandocMonad计算内部主动抛出PandocError使用throwError来自MonadError这也是类头约束的一部分。除了会中断转换管线的错误还可以生成信息性消息使用Text.Pandoc.Class中的report发出一个LogMessageLogMessage的全部构造子定义于 src/Text/Pandoc/Logging.hs包括SkippedContent、DuplicateLinkReference、ReferenceNotFound、CouldNotFetchResource、Fetching等冗余度级别定义于同一模块data Verbosity ERROR | WARNING | INFOsetVerbosity/getVerbosity决定report的消息是否会打印到 stderr在PandocIO下运行时的行为但无论冗余度如何所有被报告的消息都会内部存储可随时用getLog取出按时间顺序。这一设计让向用户展示警告与程序化收集日志两条路径彼此独立非常便于在库调用层做细粒度控制。遍历 AST提取信息与变换文档经常需要遍历 Pandoc AST目的不外乎两类提取信息例如这份文档里链接了哪些 URL所有代码示例能否编译变换文档例如把每个章节标题的层级加一去掉强调把特定标记的代码块替换为图片。为高效完成这些操作pandoc-types提供了 Text.Pandoc.Walk 模块其核心是Walkable类型类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中的每一个Inline上Walkable [Inline] Pandoc则允许你把[Inline] - [Inline]函数应用到整个Pandoc文档中每一段最大的Inline列表上。用 walk 提升标题层级下面这个函数把文档中所有标题的层级提升一级Header 1变Header 2依此类推promoteHeaderLevels :: Pandoc - Pandoc promoteHeaderLevels walk promote where promote :: Block - Block promote (Header lev attr ils) Header (lev 1) attr ils promote x x用 walkM 在变换中携带状态walkM是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 x用 query 收集信息query用于从 AST 中收集信息其参数是一个查询函数产生某个**幺半群Monoid**类型的值如列表所有结果会被拼接在一起。下面的例子返回文档中所有被链接的 URLlistURLs :: Pandoc - [Text] listURLs query urls where urls (Link _ _ (src, _)) [src] urls _ []walk的自底向上语义、walkM的单子化能力与query的聚合语义共同构成了 Pandoc 过滤器filters与 Lua 脚本之外、最底层的 AST 编程接口。创建前端convertWithOpts 与 Opts命令行程序pandoc的全部功能都被抽象进了 src/Text/Pandoc/App.hs 模块的convertWithOpts函数其签名为convertWithOpts :: ScriptingEngine - Opt - IO ()其中Opt即选项记录。因此为 Pandoc 创建 GUI 前端只需要填充Opts结构体并调用该函数——官方文档强调所有功能都已被抽象。这意味着无论是桌面图形界面、Web 服务还是其他宿主程序只要能把用户的意图映射为Opts字段输入/输出文件、读取器、写入器、选项开关等就能复用与pandoc命令完全一致的转换逻辑而无需自行拼装读取器与写入器管线。命令行的实际入口 pandoc-cli/src/pandoc.hs 正是这一模式的调用方。在 Web 应用中使用 Pandoc 的注意事项官方文档针对 Web 场景给出了三条明确的安全与稳定性建议必须加超时保护Pandoc 的解析器在某些输入上可能表现出病态pathological行为。因此始终建议用超时函数包裹 Pandoc 调用例如base库中的System.Timeout.timeout以防止 DoS 攻击拖垮服务进程。HTML 输出必须消毒如果 Pandoc 从不可信用户输入生成 HTML应始终将生成的 HTML 通过消毒器sanitizer如xss-sanitize过滤以避免 XSS 等安全问题。优先使用runPure实现沙箱使用runPure而非runIO可确保 Pandoc 的函数不执行任何 IO 操作如写文件。如果需要为转换提供某些资源可以在runPure内部的状态中准备一个伪环境参见Text.Pandoc.Class中的PureState及其相关函数。更进一步也可以编写自定义的PandocMonad实例——例如让 wiki 资源以伪环境中的文件形式暴露给 Pandoc同时将 Pandoc 与系统其余部分完全隔离。这三条建议分别从可用性超时、输出安全消毒与运行时隔离纯环境/自定义实例三个维度为把 Pandoc 嵌入多租户 Web 服务提供了可落地的工程基线。小结本文从 Pandoc 的读取器—AST—写入器架构出发完整走通了以 Haskell 库方式使用 Pandoc 的路径先用readMarkdown/writeRST完成最小转换再通过PandocMonad类型类理解PandocIO与PandocPure两种运行环境及其在沙箱化场景中的价值接着掌握了ReaderOptions/WriterOptions的关键选项、用Text.Pandoc.Builder程序化构造文档包括 JSON 数据直出 Word 的完整案例并了解了数据文件、元数据文件与模板系统的检索与覆盖机制最后深入walk/walkM/query三种 AST 遍历原语并梳理了借助convertWithOpts创建前端及在 Web 应用中安全集成 Pandoc 的实践要点。若想继续深入推荐直接阅读仓库中的核心源码src/Text/Pandoc/Class/PandocMonad.hs类型类与辅助函数、src/Text/Pandoc/Class/PandocIO.hs 与 src/Text/Pandoc/Class/PandocPure.hs两个实例、src/Text/Pandoc/App.hs前端抽象以及 doc/lua-filters.md、doc/custom-readers.md 等文档了解更上层的过滤器与自定义读写器机制。【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表