R MarkdownでKaTeXを使う

カテゴリ: r

はじめに

今、Rmdから出力できるHTML5でJavaScript控え目で軽量で高速なHTML文書フォーマットとして、minidown::mini_documentを開発しています。割と実用段階に入ったと思うので、以下のサンプルページを見て見てください。 https://minidown-example.atusy.net/

このminidown::mini_documentではKaTeXを採用しています。 rmarkdown::html_documentが採用するMathJaxより軽量で高速な素敵な数式レンダリングエンジンです。 MathJaxもバージョン3で軽量化・高速化を図っていますが、まだKaTeXには及ばない。一応MathJaxにも対応するつもりでしたが、なぜか表示が崩れるのでお蔵入りしました。

KaTeXをRmdで使う試みは既にぞうさんがされています https://qiita.com/kazutan/items/c07d317dde68b90ef118。しかし、self_containedを有効にすると、KaTeX本体をダウンロードしてHTMLファイル内に取り込もうとするので、レンダリングに時間がかかってしまいます。

しかし、本気でフォーマットとしてKaTeXをサポートするなら、この問題は避けて通れません。

アイディア

ではどうするかということで、 rmarkdown::html_document のヘルプを参照すると、「MathJaxはself_containedを例外的に回避しているよ」とあります。

Note that even for self contained documents MathJax is still loaded externally (this is necessary because of its size).

しかし、ソースを読んでもどうやってるか理解するのが難しいので、オレオレな方法で挑みました。

やっていることはシンプルで、基本は

  • PandocにKaTeXを挿入したい場所にキーワードを仕込ませる
  • R Markdownのpost_processorでキーワードをKaTeXで置換する

これだけです。

実装

プレースホルダの作成

以下の一行だけのファイルをmath.htmlとして保存しました。

<!--math placeholder-->

KaTeXスクリプトの用意

以下の内容をkatex.htmlとして保存しました。

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" integrity="sha384-zB1R0rpPzHqg7Kpt0Aljp8JPLqbXI3bhnPWROx27a9N0Ll6ZP/+DiW/UqRcLbRjq" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js" integrity="sha384-y23I5Q6l+B6vatafAwxRu/0oK/79VlbSz7Q9aiSZUvyWYIYsd+qj+o24G5ZU2zJz" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js" integrity="sha384-kWPLUVMOks5AQFrykwIup5lo0m3iMkkHrD0uJ4H5cjeGihAutqP0yW0J6dpFiVkI" crossorigin="anonymous" onload="renderMathInElement(document.body);"></script>
<script>
  document.addEventListener("DOMContentLoaded", function() {
    renderMathInElement(document.body, {
      delimiters: [
        {left: "$$", right: "$$", display: true},
        {left: "$", right: "$$", display: false}
      ],
      displayMode: true
    });
  });
</script>

フォーマット関数の用意

心臓部を作っていきます。

ベースフォーマットの用意

1からフォーマットを作るのは大変なので、rmarkdown::html_documentを改変しましょう。 rmarkdown::html_documentに必要な引数を指定しておきます。

  • ヘッダにmath.htmlを挿入するため、list(in_header = 'math.html')を指定
  • MathJaxを使わないので、mathjax = NULLを指定
  • Pandocは、数式レンダリングエンジン未使用時に数式を可能な範囲で変換する素敵機能を持っているので、pandoc_args = '--mathjax'を指定して無効化。
    • 通常はこれだけでは無効化できませんが、rmarkdown::html_documentのPandoc用テンプレートファイルの仕様上、これが可能です。
base_format <- rmarkdown::html_document(
  includes = list(in_header = 'math.html'),
  mathjax = NULL,
  pandoc_args = '--mathjax'
)

ベースフォーマットを改変する関数の用意

今回はKaTeXを使うのでkatex_documentと仮称します。 R Markdownにはpost_processorという機能があり、 Pandoc実行後に、出力ファイルのパスなどの様々な引数を受け取る関数を用いて後処理を実行できます。こいつを使って出力に挿入されているmath.htmlの内容の直後にkatex.htmlを挿入します。 self_containedの処理は、Pandocの段階で済んでいるので、ここでkatex.htmlを挿入しても大丈夫です。やっていることはコードを見れば分かると思います。注意点は以下の3点です。

  • katex_documentの返り値を改変したフォーマットにすること
  • base_formatが持つpost_processorもちゃんと実行されるよにすること
  • math.htmlの内容はPandocによってインデントされてしまうので、正規表現で見つけてやること
katex_document <- function(...) {
  post <- base_format$post_processor
  format <- base_format
  format$post_processor <- function(metadata, input_file, output_file, clean, verbose) {
    output <- readLines(output_file)
    output <- append(
      output
      readLines('katex.html'),
      which(grepl(' *<!--math placeholder-->', output))
    )
    writeLines(output, output_file)
    post(metadata, input_file, output_file, clean, verbose)
  }
  format
}

レンダリング

rmarkdown::renderでレンダリングしてみましょう。上記の関数を.Rprofileに記述すればYAMLフロントマターからも使えるかも知れません。

rmarkdown::render(
  'index.Rmd',
  output_format = katex_document()
)

実用化に向けて

簡単に利用しようと思うと、パッケージ化が必須になります。そうすると、フォーマットを適切に設定できるように引数を調整するとか、プレースホルダはいらなくなるので除去してあげるとか、色々な工夫が必要になます。そのあたりはatusy/minidownの実装を参考にして頂ければと思います。

Enjoy!

私のOSS活動を支援して頂けるスポンサーを募集しています。
https://github.com/sponsors/atusy