pandoc-crossrefを使ったらソースコードがページを突き抜けたので自作フィルタでどうにかした

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271956/dea9c613-1a7b-baf5-b3c6-617026eab092.png)

# はじめに

　Pandocで図表番号を参照するときに[pandoc-crossref](https://github.com/lierdakil/pandoc-crossref)を使う方は多いと思います．ところが先日，MarkdownファイルをPDFファイルに変換したときに不都合が生じました．1ページを超える長さのソースコードを記載したときに，ソースコードがページを突き抜けて読めなくなってしまうのです（図1）．

　この記事では

- pandoc-crossrefでソースコードが突き抜ける原因の調査
- ソースコードが突き抜けないようにするフィルタ[^1]の開発

の方法を紹介します．

![突き抜けているソースコード](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271956/7a2e3379-722f-9814-1880-da968ec176fe.png)
図1 突き抜けているソースコード

# 原因を調査する

## MarkdownファイルはどのようにPDFファイルになっているのか

　Pandocはファイルを`様々なフォーマット`から`様々なフォーマット`に変換します[^2]．より詳しく説明すると，まずPandocが入力ファイルをAST（Abstract Syntax Tree; 抽象構文木）に変換し，ASTを好みの内容に変換するフィルタを通り，最後にASTを出力ファイルに変換します（図2）．ただしPDFに関しては一旦LaTeXファイルを出力してからlualatexでコンパイルします．

![Pandocの詳細](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271956/2dec8ddd-363f-7dd0-81bf-f25949049e69.png)
図2 Pandocの詳細

　今回の問題は，pandoc-crossrefを使用した場合にソースコードがページを突き抜けてしまうことでした．つまりpandoc-crossrefが出力したASTに原因があると予想されます．そこでまずはPandocが出力するLaTeXファイルから原因を探し出し，次にASTの対応する部分を見つけましょう（図3）．

![原因の探し方](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271956/0bef5262-be8f-6cc4-4a6c-109da1adb40d.png)
図3 原因の探し方

## LaTeXファイルから原因を探す

　ソースコードを載せたMarkdownファイルをLaTeXに変換します．ただしpandoc-crossrefを使う場合と使わない場合でそれぞれLaTeXファイルを出力します．二つのファイルの差分を取ることで，pandoc-crossrefによって追加，削除及び変換されたLaTeXコマンドを見つけます．

　diffコマンドで見つけた差分は以下の通りです．両者を比較したところ，ソースコードを記載している部分が`codelisting`ブロックで囲まれています．この`\begin{codelisting}`と`\end{codelisting}`（とcaption）を消せば複数ページにまたがってソースコードを表示できるようです．

```shell-session
$ diff without-filter.tex with-filter.tex
4c4,8
< {[}@lst:source\_code{]} shows source code.
---
> コード.~\ref{lst:source_code} shows source code.
> 
> \begin{codelisting}
> 
> \caption{sample}
74a79,80
> 
> \end{codelisting}
```

## ASTから原因を探す

　LaTeXファイルの`codelisting`ブロックに該当する部分をASTから探します．図4はASTのファイルです．インデントも改行もなく，おぞましい内容ですね．

![JSON-formatted ASTの内容](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271956/275be8bd-3afc-ee0f-72d1-7fde84ec85b2.png)
図4 JSON-formatted ASTの内容

　このファイルはJSON形式なので，論理的にはツリー状に構造化されています．そこでJSON形式に従ってもう少し見やすくしましょう．以下のコマンドでASTのファイルを見やすくします．`filename`の部分は見たいJSONファイルに置き換えてください．ファイルの内容が構造化されて見やすくなります（図5）．

```shell_session
$ python3 -c "import json, pprint; pprint.pprint(json.loads(open(\"filename\").read()))"
```

![見やすくなったAST](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271956/ad929e30-7e17-58ef-3b04-d66637fae9ea.png)
図5 見やすくなったAST

　見やすくしたJSONファイルの差分を下に示します．ソースコードを含む箇所のみを抜粋しています．フィルタを使わない場合はキー`c`に対応する内容がソースコードとそれに関連する情報のようです．キー`t`に対応する内容は`CodeBlock`なので，まさにこのハッシュテーブルはコードブロックのためのものでしょう[^3]．
　とどのつまり，フィルタを使った場合の出力結果（下部分）をフィルタを使わない場合の出力結果（上部分）に変換する新しいフィルタを作ることで，ソースコードが突き抜けてしまう問題を解決できるでしょう．

```shell-session
$ diff without-filter-formatted.json with-filter-formatted.json
22,93c15,92
<             {'c': [['lst:source_code',
<                     ['c', 'numberLines'],
<                     [['startFrom', '1'], ['caption', 'sample']]],
<                    '#include <stdio.h>\n'
<                    （ソースコードの内容なので省略）
<                    '}'],
<              't': 'CodeBlock'}],
---
>             {'c': [['lst:source_code', ['listing', 'c', 'numberLines'], []],
>                    [{'c': [{'c': 'コード', 't': 'Str'},
>                            {'t': 'Space'},
>                            {'c': '1:', 't': 'Str'},
>                            {'t': 'Space'},
>                            {'c': 'sample', 't': 'Str'}],
>                      't': 'Para'},
>                     {'c': [['', ['c', 'numberLines'], [['startFrom', '1']]],
>                            '#include <stdio.h>\n'
>                            （ソースコードの内容なので省略）
>                            '}'],
>                      't': 'CodeBlock'}]],
>              't': 'Div'}],
```

# フィルタで問題のトークンを処理する

## フィルタの作り方

　今回は使い慣れたPythonでフィルタを開発することとします．フィルタを作る方法はQiitaにいくつもの記事があります[^4][^5]．Pythonでフィルタを作る場合は`pandocfilters`モジュール[^6]を使うと楽なようです．このモジュールでページを突き抜けないPDFファイルを作ります．
　前節で述べた通り，原因のASTトークンは`t`がDivで，かつ`t`がCodeBlockのトークンを内包したものでした．このトークンを`t`がCodeBlockのトークンの内容に置き換えるフィルタを作ってみます．
　作ったフィルタのコードを下に示します．たった22行で作れたのは驚きです．実行権限を付けて，早速使ってみましょう

```python:pandoc_remove_codelisting_filter.py
#!/usr/bin/env python3                                                                                                                          
# -*- coding: utf-8 -*-

from pandocfilters import toJSONFilter, CodeBlock

def myfilter(key, value, format_, meta):
    if key != 'Div':
        return

    for content in value:
        if type(content[0]) != dict:
            continue

        for sub_content in content:
            if sub_content['t'] == 'CodeBlock':
                return CodeBlock(*sub_content['c'])

def main():
    toJSONFilter(myfilter)

if __name__ == "__main__":
    main()
```

## フィルタを使う

　作ったフィルタを使ってMarkdownファイルをPDFファイルに変換します．もちろん作ったフィルタはpandoc-crossrefの後に適用します．コマンドラインでのフィルタの適用方法を下に示します．

```shell-session
$ pandoc --version
pandoc 2.9.2.1
Compiled with pandoc-types 1.20, texmath 0.12.0.2, skylighting 0.8.3.4
Default user data directory: /home/kenta/.local/share/pandoc or /home/kenta/.pandoc
Copyright (C) 2006-2020 John MacFarlane
Web:  https://pandoc.org
This is free software; see the source for copying conditions.
There is no warranty, not even for merchantability or fitness
for a particular purpose.
$ pandoc-crossref --version
pandoc-crossref v0.3.6.2 git commit UNKNOWN (UNKNOWN) built with Pandoc v2.9.2.1, pandoc-types v1.20 and GHC 8.8.3
$ pandoc sample.md -o sample.pdf\
         --pdf-engine=lualatex\
         --filter=pandoc-crossref --filter=./pandoc_remove_codelisting_filter.py
```
　出力されたPDFファイルを図6に示します．ページをまたがってソースコードを表示できるようになりました．これで長いソースコードも安心して載せられそうです．

![出力したPDFファイル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271956/59d62799-0e27-a2d9-79de-ab6515d71984.png)
図6 出力したPDFファイル

# まとめ

　`pandoc-crossref`を使った場合に，PDFファイルに記載したソースコードがページを突き抜けてしまいました．そこで本稿ではまずPandocが処理するASTから原因を調査し，それから原因を含むASTトークンを変換するフィルタを開発しました．開発したフィルタ`pandoc_remove_codelisting_filter`を使うことで，ソースコードを複数ページにまたがって載せられるようになりました．同様の問題でお困りの方は是非お使い下さい．
　また開発したフィルタはGitHubに置いておきました．pipでインストールするとフィルタをコマンドとして使えるようにしていますので，ご活用下さい．

# リンク

pandoc_remove_codelisting_filter: [https://github.com/Kenta11/pandoc_remove_codelisting_filter](https://github.com/Kenta11/pandoc_remove_codelisting_filter)
- `pip install https://github.com/Kenta11/pandoc_remove_codelisting_filter`でインストールできます！

# CHANGELOG
- 5/13: Before & Afterを追加，注釈[^3]を修正．

[^1]: フィルタはPandocでドキュメントを変換するためのプログラムです．『原因を調査する』で詳しく述べています．
[^2]: すごい数あります[https://pandoc.org/](https://pandoc.org/)
[^3]: 『ものでしょう』と述べているのは，つまり本当の仕様を知らないためです．~~このASTのドキュメントは無いようです．[AST Documentation? #3262](https://github.com/jgm/pandoc/issues/3262)~~Haskellのデータ構造で表現した仕様がありました．[Text.Pandoc.Definition](https://hackage.haskell.org/package/scholdoc-types-0.1.3.1/docs/Text-Pandoc-Definition.html)
[^4]: [@takada_at](https://qiita.com/takada-at), "PythonでPandocのフィルターを書く," [https://qiita.com/takada-at/items/187923e57071c0aa73a7](https://qiita.com/takada-at/items/187923e57071c0aa73a7).
[^5]: [@irisTa56](https://qiita.com/irisTa56), "Pandoc の filter として panflute を使ってみる," [https://qiita.com/irisTa56/items/34c78c92b0b0e0be90a6](https://qiita.com/irisTa56/items/34c78c92b0b0e0be90a6).
[^6]: jgm, "pandocfilters," [https://pypi.org/project/pandocfilters/](https://pypi.org/project/pandocfilters/)