スポンサーリンク

AMPでも動くMathJax-LaTeXのなんちゃってデコーダを作った件

2023年4月3日

注意:当記事は、AMPでは正しく表示されません。AMPではなく通常のページでご覧ください。

 こんにちは、その日暮らしです。

 突然ですが、MathJax-LaTeXってプラグイン、使ったことありますか?

 WordPressの記事の中に書いたLaTeXのコマンドを、MathJaxを使ってブラウザ上に数式として表示してくれるプラグインです。例えば、「\(y=f(x)\)」って書くと「\(y=f(x)\)」(AMPでは正しく表示されない)というふうに表示されます。

 Pythonのmatplotlib関連の記事でも数式を表示するのに使っているのですが、デメリットがひとつあって、それは、AMP(Accelerated Mobile Pages)では動作しないことです。

 LaTeXのコマンドを含んだ記事をAMPで表示すると、上の例でいえば「\(y=f(x)\)」(AMPでは正しく表示されない)の代わりにLaTeXのコマンドがそのまま「\(y=f(x)\)」って表示されてしまいます。なので、そんな記事ではAMPを無効にするしかありません。

 せっかく記事を書いたのにAMPを無効にするのはもったいないですよね。もしかしたら、AMPを無効にすることでSEOに影響を及ぼしてしまうかもしれません。

 そうなっては困るので、ごく簡単なコマンド限定でAMPでもLaTeXのコマンドを含んだ記事を表示できるようにする「なんちゃってデコーダ」を考えてみました。

 当記事では、そんな小ネタをシェアします。


なんちゃってデコーダの仕様

 まず、AMPで表示可能な数式を決めます。

 これまで投稿した記事で使った数式は、「\(\sin\)」(AMPでは正しく表示されない)と「\(\cos\)」(AMPでは正しく表示されない)と「\(\frac{m}{n}\)(分数)」(AMPでは正しく表示されない)と「\(\pi\)」(AMPでは正しく表示されない)と「\(\,\)(小さなスペース)」(AMPでは正しく表示されない)だけなので、とりあえずこれらの数式を表示することを目標に以下の表のような仕様にしたいと思います。

 つまり、LaTeXのコマンドを意味が損なわれない範囲でプレーンなテキストに置き換えてしまおうということです。

数式置換前のコマンド置換後のテキスト
\(\sin\)\sinsin
\(\cos\)\coscos
\(\frac{m}{n}\)\frac{m}{n}(m/n)
\(\pi\)\piπ
\(\,\)(小さなスペース)\,なし
\({}\)(コマンドの区切り){}なし

注意:上の表は、AMPでは正しく表示されません。当記事は、AMPではなく通常のページでご覧ください。

なんちゃってデコーダの実装

 では、実装です。処理の概要は、以下の通りです。これらをPHPで実装し、テーマのfunctions.phpに貼り付けます。

  • the_content関数にフィルターフックをかける。
  • フックさせた関数が呼び出されたら以下の処理を行う。
    • 投稿でなければ処理を終わる。
    • AMPでなければ処理を終わる。
    • MathJax-LaTeXのショートコードを取り除く。
    • ショートコードがなかったなら処理を終わる。
    • HTMLをDOMにロードする。
    • DOMのツリーをトラバースしながらLaTeXのコマンドを前の章の表のとおりに置換する。
    • DOMをHTMLにセーブする。

DOMロード用のヘルパー関数

 HTMLをDOMへロードする関数です。

 本デコーダでは、HTMLをDOMに変換していろいろな操作を行った後にまたHTMLへ戻すということを行っています。

 文字化け対策として関数mb_convert_encodingを使って文字コードをUTF-8からHTML-ENTITIESへ変換したものをDOMへロードしています。なお、4行目は、UTF-8→HTML-ENTITIES→UTF-8と変換したときに「&lt;」が「<」へ勝手に変換されないようにするためのおまじないで、本文中に書いたHTMLのタグがブラウザでHTMLと解釈されてしまうのを防止します。

 また、10行目で<div></div>で囲んだHTMLをロードし、その後12行目から18行目で<div></div>の囲みを削除しているのは、ロード時に<html>や<body>が追加されないようオプションLIBXML_HTML_NOIMPLIEDを指定したときに必要なおまじないです。詳細は、以下のページをご覧ください。

// HTMLをDOMにロードする。
function my_load_html_helper( $html ) {
    // HTMLの文字コードをUTF-8からHTML-ENTITIESに変換する。
    $html = str_replace( '&', '&amp;', $html );
    $html = mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' );
    // HTMLをDOMにロードする。
    // ロード時に<HTML>や<BODY>や<!DOCTYPE>が追加されないようにする。
    // その代わり一時的に<div></div>でラップする必要あり。
    $doc = new DOMDocument();
    @$doc->loadHTML( "<div>$html</div>", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
    // ラップした<div></div>をはがす。
    $cont = $doc->getElementsByTagName( 'div' )->item( 0 );
    while ( $doc->firstChild ) {
        $doc->removeChild( $doc->firstChild );
    }
    while ( $cont->firstChild ) {
        $doc->appendChild( $cont->firstChild );
    }
    return $doc;
}

HTMLセーブ用のヘルパー関数

 DOMをHTMLへセーブする関数です。

 ロード時の文字化け対策として行った文字コードの変換の逆変換として、DOMからセーブしたHTMLの文字コードを関数mb_convert_encodingを使ってHTML-ENTITIESからUTF-8へ変換しています。

// DOMをHTMLにセーブする。
function my_save_html_helper( $doc ) {
    // DOMをHTMLにセーブする。
    $html = $doc->saveHTML();
    // HTMLの文字コードをHTML-ENTITIESからUTF-8に変換する。
    $html = mb_convert_encoding( $html, 'UTF-8', 'HTML-ENTITIES' );
    return $html;
}

コールバック関数

 フィルターフックで呼び出される関数です。

 今回の処理のメインルーチン的な関数です。実装の概要で述べた処理を行っています。

注意:実際にコードを使う場合は、以下のコードの42行目にある「[math-jax]」の部分から「-」を削除した上で使うようにしてください。なぜわざわざハイフンを挿入しているかというと、ハイフンを挿入しないとこの部分がMathJax-LaTeXを起動するためのショートコードとみなされその結果下のコードからこの部分が削除され表示されてしまうからです。

// AMPの場合にMathJax-LaTeXのコードをデコードする。
function the_content_latex_decoder( $content ) {
    // 投稿以外は何もしない。
    if ( !is_singular( 'post' ) )
        return $content;
    // AMP以外は何もしない。
    $current_url = get_pagenum_link( get_query_var( 'paged' ) );
    if ( substr( $current_url, -5 ) !== '/amp/' )
        return $content;
    
    // 記事冒頭にあるMathJax-LaTeXのショートコードを取り除く。
    $html = str_replace( '<p>[math-jax]</p>', '', $content, $count );
    if ( $count === 0 )
        // MathJax-LaTeXが使われていなければ何もしない。
        return $content;
    
    // HTMLをDOMにロードする。
    $doc = my_load_html_helper( $html );
    // MathJax-LaTeXのコードをデコードする。
    dom_tree_traverser( 'my_latex_decoder', $doc );
    // DOMをHTMLにセーブする。
    $html = my_save_html_helper( $doc );
    
    return $html;
}
add_filter( 'the_content', 'the_content_latex_decoder' );

トラバース用のヘルパー関数

 DOMのツリーをたどる関数です。

 各ノードをたどりながら第1引数で与えられた関数をそのノードの子ノードを引数にして呼び出します。自身を再帰的に呼び出すことでツリーの全ノードをたどるようになっています。

// DOMツリーをトラバースしながら渡された関数を呼び出す。
function dom_tree_traverser($callback, $node) {
    // 渡された関数を呼び出す。
    call_user_func($callback, $node);
    // 子ノードがなければ何もしない。
    if ($node->hasChildNodes()) {
        // すべての子ノードについて・・・。
        foreach ($node->childNodes as $childNode) {
            // 再帰呼び出しをする。
            dom_tree_traverser($callback, $childNode);
        }
    }
}

なんちゃってデコード関数

 デコードを行う関数です。

 まず、引数で与えられたノードがテキストノードでない場合や<pre>や<code>内のテキストの場合、あるいは、空のテキストの場合には何もせず処理を終わります。そして、「\(」と「\)」で囲まれているLaTeXのコマンドを見つけたら、それらを前の章の表の通りに置換します。置換対象となる文字列を正規表現を使って検索していますが、そんなに難しくないと思います。

 少し説明します。

 関数preg_match_allを使って「\(」と「\)」で囲まれているLaTeXのコマンドを見つけたら、そのLaTeXのコマンドについて表にある変換を行い、その結果を「\(」と「\)」で囲まれていた部分と置き換える、ということをやっています。

 分数のコマンドも同様です。関数preg_match_allを使って「\frac{m}{m}」というコマンドを見つけたら、「(m/n)」という文字列に変換し、もとのコマンド「\frac{m}{m}」と置き換える、ということをやっています。

// MathJax-LaTeXのコードをなんちゃってデコードする。
function my_latex_decoder( $node ) {
    // DOMTextでなければ何もしない。
    if ( $node->nodeType !== XML_TEXT_NODE )
        return;
    // preタグ内とcodeタグ内の場合は何もしない。
    if ( preg_match( '/pre|code/', $node->parentNode->nodeName ) )
        return;
    // 中身が空の場合は何もしない。
    $text = $node->textContent;
    if ( empty( $text ) )
        return;
    
    // 「\(xxx\)」の部分(MathJax-LaTeXのコード)をデコードする。
    preg_match_all( '/\\\\\\((.+?)\\\\\\)/', $text, $match, PREG_SET_ORDER );
    for ( $i = 0; $i < count( $match ); $i++ ) {
        $subject = $match[$i][1];
        // 「\,」を「」(空文字)に置き換える。
        $subject = str_replace( '\\,', '', $subject );
        // 「\pi」を「π」に置き換える。
        $subject = str_replace( '\\pi', 'π', $subject );
        // 「\sin」を「sin」に置き換える。
        $subject = str_replace( '\\sin', 'sin', $subject );
        // 「\cos」を「cos」に置き換える。
        $subject = str_replace( '\\cos', 'cos', $subject );
        // 「{}」を「」(空文字)に置き換える。
        $subject = str_replace( '{}', '', $subject );
        // 「\frac{xxx}{xxx}」の部分(分数)をデコードする。
        preg_match_all( '/(\\\\frac{.+?}{.+?})/', $subject, $match_frac, PREG_SET_ORDER );
        for ( $j = 0; $j < count( $match_frac ); $j++ ) {
            $subject_frac = $match_frac[$j][1];
            // 「\frac{」を「(」に置き換える。
            $subject_frac = str_replace( '\\frac{', '(', $subject_frac );
            // 「}{」を「/」に置き換える。
            $subject_frac = str_replace( '}{', '/', $subject_frac );
            // 「}」を「)」に置き換える。
            $subject_frac = str_replace( '}', ')', $subject_frac );
            // 置き換えた結果を元の文字列に反映する。
            $subject = str_replace( $match_frac[$j][0], $subject_frac, $subject );
        }
        // 置き換えた結果を元の文字列に反映する。
        $text = str_replace( $match[$i][0], $subject, $text );
    }
    
    // 結果を反映する。
    $node->textContent = $text;
}

 どうでしょうか。やっぱ、なんちゃってですよね。でもまあ、複雑な数式(例えば定積分とか)なんかはもともと普通のプレーンなテキストだけでは表現できないので、対応できるLaTeXのコマンドとしてはこんなもんかなあと思います。


以上、「AMPでも動くMathJax-LaTeXのなんちゃってデコーダを作った件」でした。


この記事を書いた人

プロフィール

 その日暮らし

 こんにちは、その日暮らしです/地方国立大理系院卒→大手大企業就職→ソフト開発二十年超→メンタル壊して退職→ちょっと回復→資格取得頑張る(簿記3級と応用情報は合格でデスペはギブアップ)→コロナ禍で再就職無理→離婚orz→実家へ出戻ってこどおじ化(笑)→WordPressの勉強のためブログに挑戦/そんな訳でブログは始めたばかりですが日々いろんなことを試して得た知識を投稿していこうと思ってます/以上