Pandoc + minted でソースコード入りのきれいなPDFを作成した話

# TL;DR

$\LaTeX$ でレポート書くの面倒くさい！
↓
markdownで書いてPandocで$\LaTeX$経由でPDFにすれば $\LaTeX$ の数式も組み込めて神では？
↓
ソースコード含めたいけどPandocの装飾クソダサイな？！
↓
minted使えばきれいにソースコード出力できるんじゃね？
↓
え、ちょっと待って、設定ダルすぎません...?
↓
フィルター書いてああしてこうして...

# はじめに

これを書いてる人は典型的な情報系大学生なので、 $\LaTeX$ + ソースコードなんてレポートを書くこともあるのです(実際はないですけど)。でも $\LaTeX$ だらけのレポートは読みにくさの塊で、正直毎回簡単な記述ですら $\LaTeX$ で書くというのは避けたいものです。

そんなあなたにPandoc。しかしPandocの出力するソースコード部分のハイライトはQiitaの記事と比べると圧倒的に醜く、そのままにしておくのは許せないものがありました。

そこでPandocのほうの設定を色々いじり、ソースコードをいい感じにしてくれるmintedを導入したのですが、かなり苦戦したのでその記録です。

# 環境

- Ubuntu 18.04.3 LTS (Bionic Beaver)
- TeXlive はなんだかんだで結局フルインストールしました。 **古いパッケージの更新に苦戦するので `apt` でのインストールは非推奨です。 `install-tl` を使用しましょう。**

[TeX Live を Ubuntu に（APT を使わずに）導入する — しっぽのさきっちょ | text.Baldanders.info](https://text.baldanders.info/remark/2019/04/install-texlive-in-ubuntu/)

# Pandocの導入

Pandocはある形式の文書を別な形式の文書に変えてくれるパワフルなツールです。今回はmarkdownを $\LaTeX$ に変換するために使います。(PDFではないんです)

Pandocは普通に `apt` で入れましょう。

```bash:bash
$ sudo apt install pandoc
...
$ pandoc --version
pandoc 1.19.2.4
...
```

ここで躓くことはないかと思います。

ちなみにこれからたくさんPandocについて触れていくことになるのですが、親切丁寧なドキュメントがPandocにはあります。悩んだら読みましょう。

[Pandoc ユーザーズガイド 日本語版 - Japanese Pandoc User's Association](http://sky-y.github.io/site-pandoc-jp/users-guide/)

# mintedの導入

mintedを使用するためにはPython3とパッケージ `pygments` が必要です。次のページがとても参考になりました。

[mintedでLaTeXドキュメントにソースコードを載せる - Qiita](https://qiita.com/la_float/items/2884a4d80a54ffa89a34)

以降、mintedの設定もこの記事様に従っていきます。ただ、 `pip3` でインストールすると必要なコマンド `pygmentize` がないと言われるので、 `sudo pip3` でインストールしましょう[^1]

# さて、悪魔融合です...

Pandocとmintedは実はちょっと相性が悪いのです。というのも、mintedは中間ディレクトリに移動されたりすると死んでしまう一方、Pandocは容赦なく中間ディレクトリを生成し移動するためです。

[pandoc-minted · PyPI](https://pypi.org/project/pandoc-minted/)

このことに注意しつつ、自前でシェルスクリプトを用意する必要が出てきそうです。

また、mintedを差し込むために **Pandocフィルター** なるものをこれから作成していきます。

## Pandocフィルターって何さ

Pandocって変換前に一度構文解析的なことをしているそうなんですね...で、そこにフィルターを挟むとJSONファイルを介してちょこちょこっと内容をいじることができます。次のページが詳しかったです。

- [pandocフィルターおぼえがき（前編） - Qiita](https://qiita.com/i3vi3/items/205768ad10107d9f571f)
- [PythonでPandocのフィルターを書く - Qiita](https://qiita.com/takada-at/items/187923e57071c0aa73a7)

例えば今僕はこの記事を `pandoc_article.md` という名前にして書いてるんですけど(どうでもいい)、それを次のコマンドで出力して

```bash:bash
$ pandoc pandoc_article.md -f markdown -t json > p.json
```

一部抜粋するとこんなふうになってます。

```json:p.json一部抜粋
{
  "t": "CodeBlock",
  "c": [
    [
      "",
      [
        "bash:bash"
      ],
      []
    ],
    "$ pandoc -s pandoc_article.md -f markdown -t json > p.json"
  ]
},
```

`"t": "CodeBlock",` は解析の結果これがコードブロックであることを示しています。

`"bash:bash"` というのはQiitaの記事を書いたことがある人ならわかるかもしれませんが、

```
` ` `bash:bash
$ pandoc -s pandoc_article.md -f markdown -t json > p.json
` ` `
```

というコードブロックのシンタックスハイライト用の言語と左上につく表示名です。Pandoc的にはまとめて言語名として扱われているようです。

`"c"` の持つ2つめの要素に実際のコードが含まれていることがわかります。

で、このJSONをフィルターでいじるといい感じになるわけです。

Python3でいじりたいので、 `pip3` で `pandocfilters` パッケージを取ってきましょう。

```bash:bash
$ pip3 install pandocfilters
```

では早速フィルターを書いていきましょう。

実は先に挙げた [pandoc-minted · PyPI](https://pypi.org/project/pandoc-minted/) はこれから書こうとしてるフィルターと似たものなのですが、こちらではminted環境しか導入しておらずいい感じの表現になってくれないです。

自前で用意したほうがその後色々変更できて便利でしょうからがんばりましょう。

```python:minted_filter.py
#!/usr/bin/python3
import sys
assert sys.version_info[0] == 3

from pandocfilters import toJSONFilter, RawBlock
import re

def minted_filter(key, value, frmat, meta):
    """ Modifing codeblock

    Args:
        key   => type of pandoc object. In json its key is "t"
        value => contents of pandoc object. In json its key is "c"
        frmat => target output format
        meta  => document metadata
    """
    
    # if frmat != "latex" or key != "CodeBlock":
    if key != "CodeBlock":
        return

    [[_, classes, _], contents] = value
    
    if len(classes) > 0:
        splt_cls = classes[0].split(":")
        title    = splt_cls[1] if len(splt_cls) >= 2 else ""
        lang     = splt_cls[0]
    else:
        title    = ""
        lang     = "text"

    title = re.sub(r"_", "\\_", title)
    lang  = re.sub(r"_", "\\_", lang)
        
    replaced = """\
\\begin{{spacing}}{{1.0}} {title}\n\
\\begin{{mdframed}}\n\
\\begin{{minted}}[breaklines, linenos]{{{lang}}}\n\
{contents}\n\
\\end{{minted}}\n\
\\end{{mdframed}}\n\
\\end{{spacing}}""".format(title=title, lang=lang, contents=contents)
    
    return [RawBlock(frmat, replaced)]
    
if __name__ == "__main__":
    toJSONFilter(minted_filter)
```

[pandoc-minted · PyPI](https://pypi.org/project/pandoc-minted/) をもとに見様見真似で書きました。装飾を増やし、Qiita用に若干アレンジを加え、 `"Code"` に関しては置換しない設定としました。

また `markdown -> latex` 以外の変換をする方は `frmat` による条件分岐に気をつけてください。。。まぁこのフィルターを `markdown -> latex` 以外で使うこともないでしょうし、デバッグの容易さより自分は条件分岐していません。

最後に、フィルターに実行権限を与えるのを忘れないでください。(忘れるとShebangが無視されてしまいPython2が実行される可能性があります。)

```bash:bash
$ chmod u+x minted_filter.py
```

フィルターは次のように使います。

```bash:bash
$ pandoc --filter minted_filter.py pandoc_article.md -f markdown -t json > q.json
```

2020/7/30 追記: 面倒を避けるため `minted_filter.py` は予め `$PATH` 変数の通った場所に置いておいてください。 
`$HOME/bin/` 以下が一般的です。詳しくはググって()

```bash
$ mkdir -p ~/bin
$ mv minted_filter.py ~/bin/
```

フィルターを挟むと先程の出力が変わっていることが確認できます。[^2]

```json:q.json一部抜粋一部変形
{
  "t": "RawBlock",
  "c": [
    "json",
    "\\begin{spacing}{1.0} bash\n\\begin{mdframed}\n\\begin{minted}[breaklines, linenos]{bash}\n$ pandoc -s pandoc_article.md -f markdown -t json > p.json\n\\ end{minted}\n\\ end{mdframed}\n\\ end{spacing}"
  ]
},
```

## メタデータの付加

勘の鋭い読者(もとい $\LaTeX$ が書ける読者)ならこのままだとある問題に気づくでしょう。

**minted環境導入してなくないか？！？！**

仰るとおりです。その他にも色々と入れてないですね。このまま行くとコンパイルに掛ければもちろんエラーになります。

そこで、Markdownファイルの方にちょこちょこっと細工をしましょう[^3]。Markdownの最後に次のテキストを挿入してください。

```
---
urlcolor: cyan
header-includes:
    - \usepackage{minted}
    - \usepackage{setspace}
    - \usepackage{mdframed}
---
```

注意点として、markdown本文との間に空白行を入れておいてください。

これはPandoc用のメタデータで、Pandocで変換する際に様々な注文をつけることができます。メタデータはJSONからも最後のほうで確認できます。

今回はリンクの色を水色にする指定と、 $\LaTeX$ で変換する際に必要な各種パッケージをプリアンブル部に含めてもらうようにする指定を加えました。

以降、順番が前後して申し訳ありませんが、このメタデータを付加したファイルがPandocで変換されていたものとします。(もちろん毎回この処理を行うのは面倒ですから、後ほど自動化します。)

## LaTeXファイルのコンパイル

フィルターを通してできたJSONファイルから $\LaTeX$ ファイルを作成し、いよいよ文書をPDF化します。

Pandocの出力する $\LaTeX$ は少々特殊で、うまいことlualatexを使用する必要があります。次の記事がとても参考になりました。

[メモ: Pandoc+LaTeXで気軽に日本語PDFを出力する - Qiita](https://qiita.com/sky_y/items/15bf7737f4b37da50372)

先程作成した `q.json` をまたPandocで今度は $\LaTeX$ に変換します。なぜ一度にPDFに変換しないかは悪魔融合の冒頭で話したとおり、一時ディレクトリを作成しないようにするためです。

ここで記事の注意どおり変数を指定するオプションをつけましょう。

```bash:bash
$ pandoc -s q.json -o pandoc_article.tex -f json -t latex -V documentclass=bxjsarticle -V classoption=pandoc 
```

ここで注意してほしいのは `-s` ( `--standalone` ) オプションです。これは出力されるtexファイルを独立にコンパイル可能にするものです。ないとプリアンブル部が出力されずエラーになります。

最後にlualatexでコンパイルして完成です。ここで最後の注意ですが、mintedを使用するために `-shell-escape` オプションを使用する必要があります。

```bash:bash
$ lualatex -shell-escape pandoc_article.tex
```

# markdown -> PDFしてくれるコマンドの作成

ここまでのコマンドを一連のシェルスクリプトにしましょう。解説のためにJSONファイルを出力していましたが、今回は必要ありません。

2020/7/30 追記: 注意点ですが、先程も書いたように`minted_filter.py` は予め `$PATH` 変数の通った場所に置いておいてください。(マークダウンファイルがあるディレクトリにいちいちコピーするのも面倒ですよね？)

```bash:md2pdf_by_pandoc
#!/bin/bash

mkdir -p .markdown_preview_tmp
set -e
cd .markdown_preview_tmp
fname=`basename $1`
cp ../$fname .

cat <<EOF >> $fname

---
urlcolor: cyan
header-includes:
    - \usepackage{minted}
    - \usepackage{setspace}
    - \usepackage{mdframed}
---
EOF

pandoc -s --filter minted_filter.py $fname -o ${fname%%.md}.tex -f markdown -t latex -V documentclass=bxjsarticle -V classoption=pandoc
lualatex -shell-escape ${fname%%.md}.tex

cp ${fname%%.md}.pdf ../${fname%%.md}.pdf
cd ../
rm -r .markdown_preview_tmp
```

カレントディレクトリに一時ディレクトリを作成しmarkdownファイルをコピーしてきて、その中で

- メタデータの付加
- md -> latex
- latex -> PDF

という作業をおこなっています。最後に一時デイレクトリを消せばmarkdownとPDFだけが残りハッピーですね！(デバッグ時は `rm` をコメントアウトしましょう。)

このコマンドも実行権限を与えパスの通ったところに置いておきましょう。

```bash:bash
chmod u+x md2pdf_by_pandoc
```

各種ツールがインストールされ、フィルターとこのコマンドが適切な位置[^4]にあれば、最終的に次のコマンドで素敵なPDFが出来上がるはずです。

```bash:bash
$ mv md2pdf_by_pandoc ~/bin/
$ md2pdf_by_pandoc pandoc_article.md
```

で、この記事をPDF化したものが[こちら](https://drive.google.com/file/d/1fda9Ms8r-eCMm6uHaGz8kxSz7KxXu4Ek/view?usp=sharing)になります。

ここまで読んでいただき、ありがとうございました。至らない点があればコメントくださると幸いです。m(_ _)m

[^1]: `pip` に `sudo` をつけて実行するのはセキュリティ上の理由から非推奨です、、、本当はどうするべきなんでしょうね()
[^2]: `\end{minted}` あたりが混ざっているとエラーになってしまうので、バックスラッシュとendの間にスペースを入れています。
[^3]: さらに勘がいい読者はもしかしたら「フィルターでメタデータをぶっこめばいいじゃん」って思うかもしれません。。ただ記述が面倒になりそうだったので私は直接markdown側にぶち込むことにしました。こっちのほうが楽だしね。
[^4]: 要はフィルターとこのコマンドを `$HOME/bin/` に実行権限付きでぶち込んでおこうって意味です。