昨日までの記事では、Pandocフィルターの基本と少しだけ実用味がありそうな例を紹介しました。
Pandocフィルターは、Pandoc本体の開発言語と同じくHaskellで書けますが、Pandocの内部動作を変えられるわけではなく、pandoc
コマンドによってJSONとして出力したデータを操作する仕組みです。
内部に組み込まれたLua処理系で実行できる新しいフィルターの仕組みもありますが、いずれにしてもpandoc
というコマンドに対する補助的な機構です。
一方で、Haskellというプログラミング言語から見ると、Pandocはライブラリでもあります。
つまりpandoc
コマンドとしてでなく、自分が書くHaskellのプログラムで読み込んでそのMarkdownのパーサだけを使う、といったことも可能です。
今日はそのような事例を紹介します。
XML原稿にあるMarkdownの島
先週、このアドベントカレンダーでは、原稿にXMLを利用した組版の世界観について何回かに分けて紹介しました。 当社がやっているのはレイアウトの層にLaTeXを使う非標準的なXML組版ですが、XMLを原稿のフォーマットに使うことにはそれなりに意義があるのは伝わったと思います。
で、XMLが原稿の形式として至高であることはまあ認めるとして、専用のエディタなしに書いたり編集したりするのはやはり大変です。
そこで多くの人は、「<markdown>
みたいな要素を作って、そのなかではMarkdownを直接書けるようにすればいいんじゃない?」みたいな暗黒面に堕ちるわけです。
もちろん、暗黒面というのはXML組版をジェダイ教とみなした場合の話であって、XML組版のほうをシスとみなすなら、原稿にMarkdown島を入れ込むことは真のフォースを挫く愚行に見えるに違いありません。
いずれにせよ悪い文明だな。
<?xml version="1.0" encoding="UTF-8"?> <chapter> <title>chapter title</title> <markdown> Foo bar... ## Intro to Bar ## {#sec.foo} bar baz... ### About Baz ### baz baz... </markdown> </chapter>
どうやらDocbookっぽいスキーマのXMLの中に、<markdown>
という独自のタグを使って生のMarkdownを入れられるようにしたようですね。
Madkdownの中ではヘッダの属性記法が使われていることが見て取れます。
Pandocをライブラリとして使う手順
この原稿を手にしたときは絶望しましたが、Pandocライブラリを使ってMarkdown島の中身を正規のDocbookへと戻してあげれば、ふつうにDocbookのようなXMLとして扱えそうですね。 島の周囲については、XMLを扱うライブラリでまじめに処理してもいいのですが、ここでは手を抜いてテキストフィルター的に済ませましょう。
つまり、次のような戦略です。
まずはMarkdownをDocbookへ変換する部分から考えましょう。
Text.Pandoc.Readers.Markdown
モジュールを読み込むと、テキストをMarkdownとして読むreadMarkdown
という関数が使えるようになります。
これはpandoc
コマンドでMarkdownを読み込むときに使われるReaderそのものです。
Writerのほうは、この場合に読み込むべきはText.Pandoc.Writers.Docbook
です。
この中にwriteDocbook5
という関数があります。
これらを単純に使えれば楽なのですが、pandoc
コマンドに無数のオプションを指定できるようになっていることからわかるように、ライブラリ関数にもオプションを指定しなければなりません。
ここではヘッダ属性を扱いたいので、標準の設定にExt_header_attributes
を追加したものとしてオプションを用意します。
また、Readerの関数が返すPandoc構造はPandocMonadでくるまれているので、実行には専用の仕掛けが必要です。
この場合にはText.Pandoc.Class
のrunPure
を使えばいいでしょう。
結局、だいたいこんな感じに書けるはずです。
大げさに見えるかもですが、mdToDocbook
関数でやっていることを読み取るのはそんなに難しくないでしょう。
import Text.Pandoc.Writers.Docbook import Text.Pandoc.Readers.Markdown import Text.Pandoc.Extensions import Text.Pandoc.Options import Text.Pandoc.Class (runPure) mdToDocbook b = case runPure $ readMarkdown readOptions $ T.pack b of Right p -> case runPure $ writeDocbook5 writeOptions p of Right s -> T.unpack s Left err -> "" Left err -> "" readOptions = def{readerExtensions = extensionsFromList [ Ext_header_attributes]} writeOptions = def{writerExtensions = extensionsFromList [Ext_header_attributes]}
次は、全体からMarkdown島を探すパーサが必要です。
これはParsecで簡単に書けて、上記のmdToDocbook
を組み込めばこんな感じになります。
mdParser :: Parser String mdParser = spaces >> string "<markdown" >> conc <$> (manyTill (noneOf "/>") (try $ string ">")) <*> (manyTill anyChar (try $ string "</markdown>")) where conc _ = mdToDocbook
残るはファイル全体に対するパーサーです。 Markdown島以外はスルーするように定義します(定義が簡単になるように、ファイルの末尾に必ず空行があることを想定しました)。
fixDocbook :: String -> IO String fixDocbook filename = do h <- IO.openFile filename IO.ReadMode b <- IO.hGetContents h return $ fixer b fixer body = case parse (concat <$> manyTill block eof) "" body of Right str -> str Left err -> "" where block = choice [try $ mdParser, otherlines] otherlines :: Parser String otherlines = (++"\n") <$> manyTill anyChar (try newline)
以上を全部まとめてmain
をつけたものを以下に示します。
module Main where import System.Environment (getArgs) import qualified System.IO as IO import qualified Data.Text as T import Text.Pandoc.Writers.Docbook import Text.Pandoc.Readers.Markdown import Text.Pandoc.Extensions import Text.Pandoc.Options import Text.Pandoc.Class (runPure) import Text.ParserCombinators.Parsec hiding (many, optional, (<|>)) import Control.Applicative main :: IO () main = do srcfile:_ <- getArgs db <- fixDocbook srcfile putStrLn db return () fixDocbook :: String -> IO String fixDocbook filename = do h <- IO.openFile filename IO.ReadMode b <- IO.hGetContents h return $ fixer b fixer body = case parse (concat <$> manyTill block eof) "" body of Right str -> str Left err -> "" where block = choice [try $ mdParser, otherlines] otherlines :: Parser String otherlines = (++"\n") <$> manyTill anyChar (try newline) mdParser :: Parser String mdParser = spaces >> string "<markdown" >> conc <$> (manyTill (noneOf "/>") (try $ string ">")) <*> (manyTill anyChar (try $ string "</markdown>")) where conc _ = mdToDocbook mdToDocbook b = case runPure $ readMarkdown readOptions $ T.pack b of Right p -> case runPure $ writeDocbook5 writeOptions p of Right s -> T.unpack s Left err -> "" Left err -> "" readOptions = def{readerExtensions = extensionsFromList [ Ext_header_attributes]} writeOptions = def{writerExtensions = extensionsFromList [Ext_header_attributes]}
これをコンパイルして先のXMLっぽいファイルを処理してみると、こうなりました。
$ ghc fixer.hs [1 of 1] Compiling Main ( fixer.hs, fixer.o ) Linking fixer ... $ ./fixer input.xml <?xml version="1.0" encoding="UTF-8"?> <chapter> <title>chapter title</title> <para> Foo bar... </para> <section xml:id="sec.foo"> <title>Intro to Bar</title> <para> bar baz... </para> <section> <title>About Baz</title> <para> baz baz... </para> </section> </section> </chapter>
Pandocの豊富な機能をライブラリとして使うことで、わりとこざっぱりしたコードでMarkdown島を含むXMLを処理することができました。
ところで今回の話の影の主役は、実はPandocライブラリではなくParsecです。 Parsecを駆使することにより、未知の記法で書かれたドキュメントを自分が好きなHaskellの抽象データ型へと押し込めて、そこから他の扱いやすい形式へと変換するという道が切り拓けます。
Parsecを使って未知の記法から都合がいい構造を取り出す話をする前に、明日はちょっとTeX on LaTeX(お勧めしない)の回を挟むつもりです。