4.20 Pandoc の Lua フィルタから操作する (*)

技術的にはこの節は少し発展的ですが, Markdown の内容が Pandoc 抽象構文木 (AST) にどのように翻訳されるかを一度学べば, Lua というプログラミング言語を使ってどのような Markdown の要素も操作する力を得ることになります.

基本として, Pandoc は Markdown ファイルを読み取り, その内容が AST にパースされます. Pandoc はこの AST をLua スクリプトを使って修正することを可能にします. AST の意味するものを示すため, 以下のような簡単な Markdown ファイル (ast.md) を使います.

## 第1節

Hello world!

このファイルは見出し1つとパラグラフ1つを持っています. Pandoc がこの内容をパースした後にファイルを JSON 形式に変換すれば, R ユーザーにとっては 結果として現れる AST を理解するよりも簡単でしょう.

pandoc -f markdown -t json -o ast.json ast.md

そして JSON ファイルを R に読み込み, データ構造を書き出します.

この操作をしたら, Markdown の内容は再帰的なリストで表現されていることが分かるでしょう. その構造を以下に表します. ラベル t は “type,” c は “content” を表します. 例として見出しを取り上げてみましょう. タイプは “Header” で, その中身は3つの要素が含まれています. 見出しのレベル (2), 属性 (例えば ID が section-one であること), そしてテキストの内容です.

xfun:::tree(
  jsonlite::fromJSON('ast.json', simplifyVector = FALSE)
)
List of 3
 |-pandoc-api-version:List of 2
 |  |-: int 1
 |  |-: int 22
 |-meta              : Named list()
 |-blocks            :List of 2
    |-:List of 2
    |  |-t: chr "Header"
    |  |-c:List of 3
    |     |-: int 2
    |     |-:List of 3
    |     |  |-: chr "第1節"
    |     |  |-: list()
    |     |  |-: list()
    |     |-:List of 1
    |        |-:List of 2
    |           |-t: chr "Str"
    |           |-c: chr "第1節"
    |-:List of 2
       |-t: chr "Para"
       |-c:List of 3
          |-:List of 2
          |  |-t: chr "Str"
          |  |-c: chr "Hello"
          |-:List of 1
          |  |-t: chr "Space"
          |-:List of 2
             |-t: chr "Str"
             |-c: chr "world!"

あなたが AST に気づけば, Lua によって修正することができます. Pandoc は組み込みの Lua インタプリタを持っているので, 追加でツールをインストールする必要はありません. Lua スクリプトは Pandoc では「Lua フィルタ」と呼ばれます. 次に見出しのレベルを1上げる, 例えばレベル3の見出しを2に変換する簡単な例を見せます. これは文書のトップレベルの見出しがレベル2で, 代わりにレベル1から始めたい場合に便利です.

最初に raise-header.lua という名前の Lua スクリプトファイルを作ります. これには Header という名前の関数が含まれており, “Header” タイプの要素を修正したいということを意味しています (一般に, あるタイプの要素を処理するためにタイプ名を関数名として使うことができます).

function Header(el)
  -- 見出しのレベルは要素の持つ 'level' 属性でアクセスできます.
  -- 後述の Pandoc ドキュメントを見てください.
  if (el.level <= 1) then
    error("h1 のレベルを上げる方法がわかりません")
  end
  el.level = el.level - 1
  return el
end

そしてこのスクリプト Pandoc の --lua-filter 引数に与えることができます. 例えばこうです.

pandoc -t markdown --atx-headers \
  --lua-filter=raise-header.lua ast.md
[WARNING] Deprecated: --atx-headers. Use --markdown-headings=atx instead.
# 第1節

Hello world!

## Section One# Section One へ変換することに成功したのがお分かりかと思います. この例は些細なものだと思うかも知れませんし, どうして次のように単に正規表現を使って ### に置き換えないのかと思うことでしょう.

gsub("^##", "#", readLines("ast.md"))

たいていの場合, 構造化された文書を操作するのに正規表現はロバストな手段ではありません. 例えば ## が R コード内でコメントに使われているというように, ほぼいつも例外があるためです. AST は構造化されたデータを与えてくれるので, 確実に意図した要素を修正していることが分かります.

Pandoc の Lua フィルタに関する追加ドキュメントが https://pandoc.org/lua-filters.html にあり, ここで多くの例を見つけることができます. GitHub リポジトリ https://github.com/pandoc/lua-filters のコミュニティで書かれたフィルタを見つけることもできます.

R Markdown の世界では Lua フィルタを活用しているパッケージの例の一部が以下になります (たいていは inst/ ディレクトリにあります).

  • rmarkdown パッケージ (https://github.com/rstudio/rmarkdown) は改行 (4.1節参照) を挿入するフィルタとカスタムブロック (9.6節参照)を生成するフィルタを含んでいます.

  • pagedown パッケージ (Xie et al. 2021) には脚注を実装するのを助けるフィルタと HTML ページに図のリストを表示するフィルタがあります.

  • govdown パッケージ (Garmonsway 2021) には Pandoc の Div による囲みを適切な HTML タグに変換するフィルタがあります.

本書の5.1.2節でも Lua フィルタでテキストの色を変更する方法を紹介する例を見ることができます.

Lua フィルタを (上記のパッケージのように) 導入するために R パッケージ を作りたくない R Markdown ユーザーは, これらの Lua スクリプトをコンピュータのどこかに保存し, R Markdown 出力フォーマットの pandoc_args オプションを次の例のように適用することもできます.

---
output:
  html_document:
    pandoc_args:
      - --lua-filter=raise-header.lua
---

参考文献

Garmonsway, Duncan. 2021. Govdown: GOV.UK Style Templates for r Markdown. https://ukgovdatascience.github.io/govdown/.
Xie, Yihui, Romain Lesur, Brent Thorne, and Xianying Tan. 2021. Pagedown: Paginate the HTML Output of r Markdown with CSS for Print. https://github.com/rstudio/pagedown.