はじめに

pandoc の --filter 機能を使ってMarkdown等にプリプロセスな処理を挟んで、
Markdown上に特定の記法で書いたら自動でコードを埋め込むようにするという話です。

pandocのfilter

pandocでは --filter [filter script] という感じで、フィルター処理をするスクリプトを変換時に入れることが出来ます。

詳しくは下記を参照して下さい

この--filter は以下のような処理のalias的な存在になってます。

変換前ファイル -> JSON -> フィルタースクリプト -> JSON -> 変換後ファイル

フィルタースクリプトには変換前のファイルをJSONにしたものが渡されます。

つまり、フィルタースクリプトは文章をJSON(Pandoc AST)を処理するようなスクリプトを書いていけばいいことになります。

インポートスクリプト

今回は次のようなものを変換するスクリプトを考えたいと思います。

Code Blockの中に $import(src/example.js) という書式で読み込みたいファイル名を指定する。

``` js
$import(src/example.js)
```

これをフィルタースクリプトで変換すると、src/example.js のファイルの中身がそのまま展開されるようなものを作っていきます。

``` js
example.jsの中身が入る
```

変換処理の流れ

先ほど大雑把にフィルターの流れを見ましたが、実際は下記のような事が行われています。

pandoc -f SOURCEFORMAT -t json | filter-script | pandoc -f json -t TARGETFORMAT

フィルタースクリプト(filter-script) にはパイプでpandoc ASTであるJSONが渡ってきて、変換した結果は標準出力に流すようなフィルタースクリプトを書く必要があります。

デバッグもこの流れで下記のようにパイプ渡しで実行しながら確認していけばいいことになります。

pandoc -f SOURCEFORMAT -t json | filter-script

pandoc AST

PandocのドキュメントにはどういうASTなのかかかれてないので実際にJSONに変換してみます。

# header

* list-1
* list-2

``` js
var a = 1;
```

上記のようなMarkdownを pandoc -f markdown -t json readme.md | jq "." という感じでJSONにしてみます。

[
  {
    "unMeta": {}
  },
  [
    {
      "c": [
        1,
        [
          "header",
          [],
          []
        ],
        [
          {
            "c": "header",
            "t": "Str"
          }
        ]
      ],
      "t": "Header"
    },
    {
      "c": [
        [
          {
            "c": [
              {
                "c": "list-1",
                "t": "Str"
              }
            ],
            "t": "Plain"
          }
        ],
        [
          {
            "c": [
              {
                "c": "list-2",
                "t": "Str"
              }
            ],
            "t": "Plain"
          }
        ]
      ],
      "t": "BulletList"
    },
    {
      "c": [
        [
          "",
          [
            "js"
          ],
          []
        ],
        "var a = 1;"
      ],
      "t": "CodeBlock"
    }
  ]
]

全体を見てると、何となく、 "t" が HeaderやCodeBlockといったタイプを表していて、"c" がそのタイプのコンテンツが入ってることがわかります。

今回のフィルタースクリプトに必要なのは CodeBlock の所だけなのでそこに集中します。

書いてみる

今回Node.jsで書いたフィルタースクリプトは以下に置いてあります

スクリプトはパイプでデータが受け取れて(nodeの場合はstreamで受け取る)、JSONが扱えれば何でもいいと思います。

import.js では "t" == "CodeBlock" なものをひたすら探索して、そのCodeBlockの中身である"c" をみて、先ほどの$import(src/example.js) というような書式で始まっていたら、そのファイルを読み込んで、"c" の中に展開するという事をしているだけです。

変換処理ができたら、stringify で文字列化してそれを標準出力に流すという感じです。

process.stdin
    .pipe(es.parse())
    .pipe(es.map(function (data, callback) {
        callback(null, main(data));
    }))
    .pipe(es.stringify())
    .pipe(process.stdout);

実行する

さきほど作成したimport.jsに実行属性をつければ、以下のようにフィルタースクリプトとして利用できます。

pandoc -f markdown -t markdown --filter ./import.js example.md

実際にやってみるとmarkdownファイルにあるCodeBlockの中身が展開されてることがわかります。

$ cat example.md
# Example

Embed source code.

``` js
$import(src/example.js)
```

Ya!
$ pandoc -f markdown -t markdown --filter ./import.js example.md --atx-headers
# Example

Embed source code.

``` {.js}
module.exports = function () {
    return "Hello World";
};
```

Ya!

今回は Markdown -> Markdown にfilterをしていますが、これはpandoc ASTを変換しているため、pandocがサポートしてる出力形式なら何でも適応出来ます。

例えば、 Markdown -> HTMLとした場合も同様にファイルが展開されていることがわかります。

$ pandoc -f markdown -t html --filter ./import.js example.md --atx-headers
<h1 id="example">Example</h1>
<p>Embed source code.</p>
<pre class="js"><code>module.exports = function () {
    return "Hello World";
};</code></pre>

おわりに

この記事ではpandocの--filter機能について説明しました。

filter機能を使うことで、Markdownに限らずpandocで変換出来るものに対して独自の処理を入れることができるので、Markdownの記法を拡張してみたり、独自のルールを追加できます。

pandoc ASTはただのJSONなので比較的扱いやすいので、簡単な処理を追加しやすいので使い道があるんじゃないかなと思います(拡張し過ぎは問題がありそうですが)