このサイトでは,MathJaxを使って数式を表示している.軽い気持ちでv2.7からv3.2に移行しようとしてハマったので,このときの対処法と理由をメモする.

これはgithub-pages (jekyll) のkmarkdownで変換しているときに起こる問題のよう.


MathJaxとは

ブラウザで動作するJavaScriptの数学表示エンジン.

公式サイトはこちら

表示例

たとえば,こんな感じで表示される.

インラインスタイル

例文: 「ここで,$n$は自然数とする.」

ここで,$n$は自然数とする.

$で囲った部分に$\LaTeX$の記法を記すことで数式を表すことができる.

ディスプレイスタイル

\[\lim_{n \to \infty} \frac{1}{n} \sum_{k=1}^n \left( \frac{k}{n} \right) = \int_0^1 f(x) \mathrm{d}x\]

$$で囲った部分に$\LaTeX$の記法を記すことで数式を表すことができる.

$$
\lim_{n \to \infty} \frac{1}{n} \sum_{k=1}^n \left( \frac{k}{n} \right) = \int_0^1 f(x) \mathrm{d}x
$$

問題が起きた状況

公式にUpgrading from v2 to v3なるドキュメントがあり,これを見ながら移行していた.ここにconversion toolが紹介されていて,2系の設定から3系のコードを生成できるよ,と書いてあった.

このとき私は,公式にあるものだからすぐ移行できるものと舐めていた…

2系で使用していたコードはこちら.

<!-- 2系 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-AMS_CHTML"></script>
<!-- 設定の変更 -->
<script type="text/x-mathjax-config">
    MathJax.Hub.Config({
    CommonHTML:{
        scale: 100
    },
    tex2jax: {
        inlineMath: [ ['$','$'], ["\\(","\\)"] ],
        displayMath: [ ['$$','$$'], ["\\[","\\]"] ]
    }
    });
</script>
<!-- /2系 -->

conversion toolは次の画像のようになっていて,そのまま入力できる場所がある.

2021-12-11-mathjax-conversion-tool-before-enter.png

これにそのままコピペできるのでしてあげるとこんな感じ.

2021-12-11-mathjax-conversion-tool-after-enter.png

そしてConvertボタンを押してあげると次のコードが生成された.

<script>
window.MathJax = {
  chtml: {
    scale: 1
  },
  tex: {
    inlineMath: [ ['$','$'], ["\\(","\\)"] ],
    displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
    autoload: {
      color: [],
      colorv2: ['color']
    },
    packages: {'[+]': ['noerrors']}
  },
  options: {
    ignoreHtmlClass: 'tex2jax_ignore',
    processHtmlClass: 'tex2jax_process'
  },
  loader: {
    load: ['[tex]/noerrors']
  }
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" id="MathJax-script"></script>

よくわからないオプションがいっぱい追加されているけどまあ公式の作ったやつだしいけるでしょ!ってことでそのままheaderにコピペした.
その結果,inline style(インラインスタイル)の数式は正しく表示されたが,display style(ディスプレイスタイル)の数式が表示されなかった.

原因究明のためにしたこと

その数式が表示されるはずだった場所

開発者ツールでその数式が表示されるはずだった場所になにがあるかを探した.

そうしたところ次のコードが生成されていた.

<script type="math/tex; mode=display">\lim_{n \to \infty} \frac{1}{n} \sum_{k=1}^n \left( \frac{k}{n} \right) = \int_0^1 f(x) \mathrm{d}x</script>

当然scriptタグなので表示はされない.

ググった

これを手がかりに,同じ症状が出ている人がいないか調べていると公式のgithubのあるissueが見つかった.

最初の問題提起を訳してみるとこんな感じ.

Mathjaxのバージョン3 (v3) では,バージョン2 (v2) でサポートされていた<script type="math/tex">のレンダリングができません.

Github Pagesを使って,$$...$$のmarkdownを変換したときに見つけました.markdownはGithubのサーバーで処理されましたが,Mathjaxのv3では何もレンダリングされませんでした(v2では問題ありませんでした).また,localでJekyllを使って確認したところ,markdown内の$$...$$は、Mathjaxで処理される前に<script type="math/tex; mode=display">...</script>に変換されていました.

これがv3で想定されている動作なのかどうか気になっています.もしそうなのであれば,<script type="math/tex">のレンダリングをオンにするoptionはありますか?
Does version 3 support <script type=”math/tex”>?

これに対する回答がこちら.

これは,v3で想定されている動作です.v3へのアップグレードのドキュメントのAPIへの変更に関する項目の最後の箇条書きを参照してください.これにより,あなたが使用しているスクリプトを処理する設定が得られます.
Does version 3 support <script type=”math/tex”>?

その「最後の箇条書き」が,以下

v2では,MathJaxによって位置が特定された数式は,ページから取り除かれ,ページ内の特別な<script>タグに格納される.このタグは読者には見えないが,ページ上の数式の位置と内容を示すものである.
v2では,プログラム自身がこれらの<script>タグを作成することが可能だったため,MathJaxが数式の区切りを探す必要がなく,また,ページ作成者が<、>、&などのHTML特殊文字を数式にエンコードする必要もありませんでした.しかし,v3では,このようにドキュメントを変更せず,ページ内のタグで見つけた数式を保存しません.その代わりに,(MathItemクラスの)数式オブジェクトの外部リストを保持します.そのため,このようなスクリプトを使用してページ内の数学を初期状態で保存したい場合は,renderActionsリストのfindアクションを,スクリプトの位置を特定し,必要なMathItemオブジェクトを作成する関数を使用するように置き換えることができます.

たとえば,このように書けばv2が作成していたコード (<script type="math/tex">ってやつ; 筆者注) を見つけることができます.

MathJax = {
 options: {
    renderActions: {
      find: [10, function (doc) {
        for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
          const display = !!node.type.match(/; *mode=display/);
          const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
          const text = document.createTextNode('');
          node.parentNode.replaceChild(text, node);
          math.start = {node: text, delim: '', n: 0};
          math.end = {node: text, delim: '', n: 0};
          doc.math.push(math);
        }
      }, '']
    }
  }
};

これは、数学の区切り文字を探す標準的な検索アクションを,代わりにv2のscriptタグを探すアクションに置き換えることに注意してください.本来の区切り記号の検索とscriptタグの検索の両方を行いたい場合は,上記のfind:findScript:に変更し、デフォルトの検索アクションを置き換えないようにします.そうすれば,両方のアクションが発生します.
Changes in the MathJax API

要するに

つまり,v2までは中間生成物 (scriptタグのやつ) を変換する機構をとっていて,外部のソフト (今回で言えばjekyllのkmarkdown) はそれを見越して先にそこまで変換させる機能が備わっている.しかし,mathjaxのv3へのバージョンアップに伴う仕様変更でその中間生成物を使わなくなったのでただただ読み込めなくなってしまった,という感じと思われる.

対策

先ほど引用したドキュメントに書いてあった通り以下の設定を加える.

MathJax = {
 options: {
    renderActions: {
      find: [10, function (doc) {
        for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
          const display = !!node.type.match(/; *mode=display/);
          const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
          const text = document.createTextNode('');
          node.parentNode.replaceChild(text, node);
          math.start = {node: text, delim: '', n: 0};
          math.end = {node: text, delim: '', n: 0};
          doc.math.push(math);
        }
      }, '']
    }
  }
};

つまり,先ほど生成したv2からの移行用と合わせて,以下をheaderに貼り付ければ良い.

<script>
window.MathJax = {
  chtml: {
    scale: 1
  },
  tex: {
    inlineMath: [ ['$','$'], ["\\(","\\)"] ],
    displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
    autoload: {
      color: [],
      colorv2: ['color']
    },
    packages: {'[+]': ['noerrors']}
  },
  options: {
    ignoreHtmlClass: 'tex2jax_ignore',
    processHtmlClass: 'tex2jax_process',
    renderActions: {
      find: [10, function (doc) {
        for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
          const display = !!node.type.match(/; *mode=display/);
          const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
          const text = document.createTextNode('');
          node.parentNode.replaceChild(text, node);
          math.start = {node: text, delim: '', n: 0};
          math.end = {node: text, delim: '', n: 0};
          doc.math.push(math);
        }
      }, '']
    }
  },
  loader: {
    load: ['[tex]/noerrors']
  }
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" id="MathJax-script"></script>

自分は,バージョンを指定しないのがなんだか不安だったので最後の行を置き換えて以下のようにした.

<script>
window.MathJax = {
  chtml: {
    scale: 1
  },
  tex: {
    inlineMath: [ ['$','$'], ["\\(","\\)"] ],
    displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
    autoload: {
      color: [],
      colorv2: ['color']
    },
    packages: {'[+]': ['noerrors']}
  },
  options: {
    ignoreHtmlClass: 'tex2jax_ignore',
    processHtmlClass: 'tex2jax_process',
    renderActions: {
      find: [10, function (doc) {
        for (const node of document.querySelectorAll('script[type^="math/tex"]')) {
          const display = !!node.type.match(/; *mode=display/);
          const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
          const text = document.createTextNode('');
          node.parentNode.replaceChild(text, node);
          math.start = {node: text, delim: '', n: 0};
          math.end = {node: text, delim: '', n: 0};
          doc.math.push(math);
        }
      }, '']
    }
  },
  loader: {
    load: ['[tex]/noerrors']
  }
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.0/es5/tex-chtml.min.js" id="MathJax-script"></script>

これで無事表示されるようになった.

コメント

書き終わってからこんなサイトを見つけた.

まさしく自分がハマったところを解説してくれていた.自分の記事か,このサイトが見つかって解決できる人が1人でもいると嬉しい.

参考