昨日の記事では、いわゆる行コメントっぽい振る舞いを例に、2種類のPandocフィルターについて説明しました。 今日は、もうちょっと非自明なPandocフィルターの例として、 昨日のPandocフィルターをもうちょっと進化させたバージョンと、コードブロックのLaTeX出力に非標準的なパッケージを使う事例を紹介します。 (ちなみにLuaフィルターはお仕事で使ったことがないので、いずれも古典的なPandocフィルターによる事例です)
行コメントPandocフィルター、バージョン2
機能の記事で例として挙げた行コメント用Pandocフィルターの実装は、段落の先頭が★
だった場合にはその段落を出力しない、という中途半端なものでした。
これをもうちょっと実用的に、Markdownで行の先頭が★
だった場合にはその行を出力しない、というふうにできないものでしょうか?
結論から言ってしまうと、これはPandocフィルターだけで完全にやるのは困難です。 たとえばMarkdownの原稿に次のような箇条書きがあったとします。
* temp1 * temp2 * temp3
これの2つめの項目の先頭に★
を付けてみましょう。
* temp1 ★* temp2 * temp3
これをPandocの標準的なMarkdown Readerで読み込むと、次のようなPandoc構造が得られます(フィルターをかけてない素の状態です)。
[BulletList [[Plain [Str "temp1",SoftBreak,Str "\9733*",Space,Str "temp2"]] ,[Plain [Str "temp3"]]]]
上記のPandoc構造は「2項目からなる箇条書き」であることが見て取れます。
また、「先頭が★
の行を出力しない」という処理に期待される構造について想像してみると、やはり「2項目からなる箇条書き」のはずです。
両者の構造は同じであることから、この処理はPandoc構造に対するアクション、つまりフィルターとして無理なく実装できそうです。
一方、次のように1つめの箇条書き項目に★
をつけると、Markdown ReaderはどんなPandoc構造を読み取るでしょうか?
★* temp1 * temp2 * temp3
この場合には次のようなPandoc構造が得られます。
[Para [Str "\9733*",Space,Str "temp1",SoftBreak,Str "*",Space,Str "temp2",SoftBreak,Str "*",Space,Str "temp3"]]
これは、もはや箇条書きではありません。ただの段落です。この構造から箇条書きを復元するのは、不可能ではありませんが、かなり厄介な相談です。
この例からわかるように、元の原稿の記法をPandocフィルターで拡張しようと思ったら、それがReaderでどんなPandoc構造に押し込められるかを常に意識して考える必要があります。
場合によっては、Text.Pandoc
以下のさまざまなモジュールを駆使することで、フィルターではなく「専用の変換器」をHaskellで書くほうが簡単かもしれません。
参考までに、上記のような「Readerで意図しない構造として読まれてしまう」場合やCodeBlock
、それにテーブルなどを除いて、おおむね行コメントっぽく機能するように拡張したPandocフィルターの例を貼り付けておきます。
見てのとおりぜんぜん自明じゃないので、このような機能が本当にほしかったら、sedとかrubyのワンライナーで先頭に★
がある行を削除してからPandocにかけるようにするほうが現実的かもしれません。というか、ふつうはそうすべきです。
import Text.Pandoc.JSON main :: IO () main = toJSONFilter block block :: Block -> Block block (Para (Str s:xs)) = commentOut Para (T.head s) s xs block (Plain (Str s:xs)) = commentOut Plain (T.head s) s xs block bs = bs commentOut t s str xs = case s of '★' -> skipBlock t xs _ -> t $ Str str:(nextInline xs) skipBlock :: ([Inline] -> Block) -> [Inline] -> Block skipBlock t (SoftBreak:xs) = block $ t xs skipBlock t (_:xs) = skipBlock t xs skipBlock _ _ = Null nextInline :: [Inline] -> [Inline] nextInline (SoftBreak:(Str s:xs)) = case (T.head s) of '★' -> SoftBreak:(skipInline xs) _ -> SoftBreak:Str s:(nextInline xs) nextInline (x:xs) = x:nextInline xs nextInline _ = [] skipInline :: [Inline] -> [Inline] skipInline (SoftBreak:xs) = xs skipInline (_:xs) = skipInline xs skipInline _ = []
そもそも、ある記法に対する行コメントというのはReaderで実現すべき機能なので、Pandocフィルターの例として使うのはアンチパターン的だったかもしれませんね。
コードブロックをLaTeXの好きな環境で出力する
もう1つだけ、非自明なPandocフィルターを紹介します。
PandocではLaTeXの出力に対応しています。
ただ残念なことに、標準ではコードブロックの出力にfancyvrb
が使われます。
--listings
オプションを付けることでlistings
パッケージが使われるようにもできますが、LaTeXでいまいちばんナウいVerbatim系のパッケージはfvextra
なので(異論は認める)、できればこれを使いたいものです。
- CTAN: Package fvextra
https://www.ctan.org/pkg/fvextra
こういう、特定の構造の出力方法をカスタマイズしたいときこそ、Pandocフィルターの出番でしょう。
もっとも単純なのは、こんな感じにCodeBlock
をRawBlock
へと変換する方法です。
main :: IO () main = toJSONFilter block block (CodeBlock _ cts) = RawBlock (Format "tex") $ "\\begin{Verbatim}[(fvextraのオプション)]\n" ++ cts ++ "\\end{Verbatim}\n"
fvextra
パッケージのオプションは、\\begin{Verbatim}
の後ろの部分に目的に合わせて追記してあげてください。
ただ、このナイーブな実装にはひとつ難点があって、コードハイライトが効かなくなります。
fvextra
でminted
を併用してもいいのでしょうが、たぶんうまくいかなくて大変なので、そこはPandocのほうでやってもらいたいものです。
現在のPandocでは、コードハイライトの仕掛けが本体から切り離されており、Skylighting
という別パッケージにまとめられています。
これはチュートリアル的なのはないので、Hackageのドキュメントとソースを見ながら使い方を解読していく感じになります。
- skylighting-core: syntax highlighting library
http://hackage.haskell.org/package/skylighting-core-0.8.3
最終的にはこんな感じのPandocフィルターになりました(最低限の部分だけを切り出してます)。
{-# LANGUAGE PackageImports #-} {-# LANGUAGE OverloadedStrings #-} import Text.Pandoc.JSON import Text.Pandoc.Highlighting import qualified Data.Text as T import Data.Char (isSpace) import Skylighting import Skylighting.Format.LaTeX import "regex-compat-tdfa" Text.Regex as R main :: IO () main = toJSONFilter block block (CodeBlock (i,c,attrs) cts) = RawBlock (Format "tex") $ "\\begin{Verbatim}[(fvextraのオプション)]\n" ++ (doCodeHighlight cts) ++ "\\end{Verbatim}" where doCodeHighlight s = case highlight defaultSyntaxMap formatLaTeXBlock ("",c,attrs) s of Left msg -> s Right h -> h formatLaTeXBlock :: FormatOptions -> [SourceLine] -> String formatLaTeXBlock opts ls = T.unpack $ T.unlines [formatLaTeX False ls] formatLaTeX :: Bool -> [SourceLine] -> T.Text formatLaTeX inline = T.intercalate (T.singleton '\n') . map (sourceLineToLaTeX inline) sourceLineToLaTeX :: Bool -> SourceLine -> T.Text sourceLineToLaTeX inline = mconcat . map (tokenToLaTeX inline) tokenToLaTeX :: Bool -> Token -> T.Text tokenToLaTeX inline (NormalTok, txt) | T.all isSpace txt = (T.pack . escapeTeX . T.unpack) txt tokenToLaTeX inline (toktype, txt) = T.cons '\\' (T.pack (show toktype) <> "{" <> (T.pack . escapeTeX . T.unpack) txt <> "}") block bs = bs escapeTeX :: String -> String escapeTeX s = let re = R.mkRegex "[(]symbol ([0-9]{1,3})[)]" in R.subRegex re (at $ sh $ bs $ lp $ rp $ us $ ps $ dl $ ad s) "{\\symbol{\\1}}" where bs s = R.subRegex (R.mkRegex "[\\]") s "(symbol 92)" lp s = R.subRegex (R.mkRegex "{") s "(symbol 123)" rp s = R.subRegex (R.mkRegex "}") s "(symbol 125)" at s = R.subRegex (R.mkRegex "[@]") s "(symbol 64)" sh s = R.subRegex (R.mkRegex "[#]") s "(symbol 35)" us s = R.subRegex (R.mkRegex "[_]") s "(symbol 95)" ps s = R.subRegex (R.mkRegex "[%]") s "(symbol 37)" dl s = R.subRegex (R.mkRegex "[$]") s "(symbol 36)" ad s = R.subRegex (R.mkRegex "[&]") s "(symbol 38)"
escapeTeX
は、いつかの記事で解説したTeX用の特殊文字のエスケープです。
なんとなくここで伏線を回収してみました。
$ cat temp.md ```ruby if true p "pandoc" end ``` $ pandoc --filter codeblock.hs -t latex temp.md \begin{Verbatim}[(fvextraのオプション)] \KeywordTok{if} \DecValTok{true}\NormalTok{ p }\StringTok{"pandoc"} \KeywordTok{end} \end{Verbatim}
コードブロックに与えた属性により、ブロック内のキーワードなどがコードハイライト用のコマンドで囲まれてますね。 あとはこれらに対する見せ方をLaTeXのスタイルで好きなように指定してあげるだけです。
明日は、Markdown原稿で索引のエントリを指定する記法について考えてみたいと思います。 そのうえでPandocフィルターを使ってその処理の実装例を紹介する予定です。