WordPress小ネタ集⑤:KaTeX Renderer (MU) 導入ガイド:WordPressで数式を高速・軽量に表示する最短手順

パソコン

本記事では、WordPressで KaTeXMUプラグインとして導入し、$...$ / $$...$$ / \( \) / \[ \] を記事内でそのまま使えるようにする方法を解説します。MU(Must-Use)プラグインは設置するだけで常時有効化され、テーマや通常プラグインに依存せず安定稼働します。

スポンサーリンク

なぜKaTeX & MUプラグイン?

  • 高速・軽量:MathJaxより描画が速く、フロント負荷が小さい
  • 設置が簡単:MUは wp-content/mu-plugins に置くだけで有効
  • 安全運用:ファイル所有者を root にすれば管理画面から改変されない
  • 軽量化内蔵本文に数式があるページだけ KaTeXを読み込みます
  • 退路つき:緊急停止用フラグ(wp-config.php でOFF)を搭載

インストール手順(MUプラグイン)

  1. MUプラグインを配置

    以下を wp-content/mu-plugins/katex.php として保存(全文コピペOK)。バージョンは v1.6.0(ローカル主・CDN予備・管理ボタン付き)です。

    <?php
    /** * Plugin Name: KaTeX Renderer (MU) * Description: WordPressでKaTeXを安全・高速に。本文に数式があるページだけ読み込み。ローカル資産を“主”、CDNを“予備”にし、管理画面からバージョンDL/切替/再描画ができます。 * Version: 1.6.0 * Author: MASA * Update URI: false */ if (defined('KATEX_MU_OFF') && KATEX_MU_OFF) return; /* ===== 基本設定 ===== */
    if (!defined('KATEX_DEFAULT_VER')) define('KATEX_DEFAULT_VER', '0.16.11');
    if (!defined('KATEX_CDN_BASE')) define('KATEX_CDN_BASE', 'https://cdn.jsdelivr.net/npm/katex@%s/dist'); // %s=ver
    // アップロード先(WPが書ける場所)
    function katex_mu_upload_dir() { $up = wp_get_upload_dir(); return trailingslashit($up['basedir']).'katex/';
    }
    function katex_mu_upload_url() { $up = wp_get_upload_dir(); return trailingslashit($up['baseurl']).'katex/';
    }
    // オプション(source: auto/local/cdn, version: 0.16.11 など)
    function katex_mu_opt($key = null) { $opt = get_option('katex_mu_opt', ['source'=>'auto','version'=>KATEX_DEFAULT_VER]); if (!$opt || !is_array($opt)) $opt = ['source'=>'auto','version'=>KATEX_DEFAULT_VER]; return $key ? ($opt[$key] ?? null) : $opt;
    }
    function katex_mu_update_opt($arr) { $opt = katex_mu_opt(); update_option('katex_mu_opt', array_merge($opt, $arr), false);
    } /* ===== “本文に数式がある時だけ”読み込む判定 ===== */
    $GLOBALS['katex_mu_should_load'] = false;
    add_filter('the_posts', function(array $posts){ foreach ($posts as $p) { $c = is_object($p) ? (get_post_field('post_content', $p) ?? '') : ''; if ($c && preg_match('/\$\$|(?<!\\\\)\$|\\\\\(|\\\\\[|\\\\begin\{/', $c)) { $GLOBALS['katex_mu_should_load'] = true; break; } } return $posts;
    }, 0); /* ===== 資産のURLと存在チェック ===== */
    function katex_mu_local_paths($ver) { $dir = trailingslashit(katex_mu_upload_dir()).$ver.'/'; $url = trailingslashit(katex_mu_upload_url()).$ver.'/'; return [ 'dir'=>$dir,'url'=>$url, 'css'=>['path'=>$dir.'katex.min.css','url'=>$url.'katex.min.css'], 'js' =>['path'=>$dir.'katex.min.js','url'=>$url.'katex.min.js'], 'auto'=>['path'=>$dir.'auto-render.min.js','url'=>$url.'auto-render.min.js'], ];
    }
    function katex_mu_local_exists($ver) { $p = katex_mu_local_paths($ver); return file_exists($p['css']['path']) && file_exists($p['js']['path']) && file_exists($p['auto']['path']);
    } /* ===== エンキュー(source: auto/local/cdn) ===== */
    add_action('wp_enqueue_scripts', function () { if (is_admin() || empty($GLOBALS['katex_mu_should_load'])) return; $opt = katex_mu_opt(); $ver = preg_match('/^\d+\.\d+(\.\d+)?$/', $opt['version'] ?? '') ? $opt['version'] : KATEX_DEFAULT_VER; $src = in_array($opt['source'] ?? 'auto', ['auto','local','cdn'], true) ? $opt['source'] : 'auto'; $local = katex_mu_local_paths($ver); $has_local = katex_mu_local_exists($ver); $use_local = ($src === 'local') || ($src === 'auto' && $has_local); if ($use_local) { wp_enqueue_style ('katex-css', $local['css']['url'], [], null); wp_enqueue_script('katex-js', $local['js']['url'], [], null, true); wp_enqueue_script('katex-auto', $local['auto']['url'], ['katex-js'], null, true); } else { $cdn = sprintf(KATEX_CDN_BASE, $ver); wp_enqueue_style ('katex-css', $cdn.'/katex.min.css', [], null); wp_enqueue_script('katex-js', $cdn.'/katex.min.js', [], null, true); wp_enqueue_script('katex-auto', $cdn.'/contrib/auto-render.min.js', ['katex-js'], null, true); }
    }, 5); /* ===== 初期化+自動フォールバック(local⇄CDN) ===== */
    add_action('wp_footer', function () { if (is_admin() || empty($GLOBALS['katex_mu_should_load'])) return; $opt = katex_mu_opt(); $ver = $opt['version'] ?? KATEX_DEFAULT_VER; $local = katex_mu_local_paths($ver); $cdn = sprintf(KATEX_CDN_BASE, $ver); $using_local = ( ( $opt['source']==='local') || ($opt['source']==='auto' && katex_mu_local_exists($ver)) ); ?> <script> (function(){ function inject(tag, attrs){var el=document.createElement(tag);for(var k in attrs){el.setAttribute(k, attrs[k]);}document.head.appendChild(el);} function ensureLoaded(cb){ var t=0, h=setInterval(function(){ if (window.renderMathInElement && window.katex) {clearInterval(h); cb();} if (++t>30) clearInterval(h); }, 50);} function init(){ if (typeof renderMathInElement!=='function') return; renderMathInElement(document.body,{ delimiters:[ {left:"$$", right:"$$", display:true}, {left:"$", right:"$", display:false}, {left:"\\[", right:"\\]", display:true}, {left:"\\(", right:"\\)", display:false} ], throwOnError:false, strict:"ignore", ignoredTags:["script","noscript","style","textarea","pre","code","kbd","samp"] }); } // 300ms待って未ロードならフォールバック:local→CDN or CDN→local setTimeout(function(){ var loaded = !!(window.katex && window.renderMathInElement); if (loaded) return;  inject('link',{rel:'stylesheet',href:'/katex.min.css'}); inject('script',{src:'/katex.min.js',defer:''}); inject('script',{src:'/contrib/auto-render.min.js',defer:''});  inject('link',{rel:'stylesheet',href:''}); inject('script',{src:'',defer:''}); inject('script',{src:'',defer:''});  ensureLoaded(init); }, 300); // 通常経路でも初期化 if (document.readyState==='loading') { document.addEventListener('DOMContentLoaded', function(){ setTimeout(init, 0); }, {once:true}); } else { setTimeout(init, 0); } })(); </script> <style>.katex-display{margin:.9em 0}.katex{font-size:1.05em}</style> <?php
    }, 20); /* ===== 管理画面:設定/ダウンロード/切替/再描画 ===== */
    add_action('admin_menu', function () { add_options_page('KaTeX (MU)', 'KaTeX (MU)', 'manage_options', 'katex-mu', function () { if (!current_user_can('manage_options')) return; // POST処理 if (isset($_POST['katex_mu_action']) && check_admin_referer('katex_mu_save','katex_mu_nonce')) { $act = sanitize_text_field($_POST['katex_mu_action']); $ver = sanitize_text_field($_POST['katex_mu_version'] ?? KATEX_DEFAULT_VER); if (!preg_match('/^\d+\.\d+(\.\d+)?$/', $ver)) $ver = KATEX_DEFAULT_VER; if ($act === 'save') { $src = sanitize_text_field($_POST['katex_mu_source'] ?? 'auto'); if (!in_array($src, ['auto','local','cdn'], true)) $src = 'auto'; katex_mu_update_opt(['source'=>$src,'version'=>$ver]); echo '<div class="updated"><p>設定を保存しました。</p></div>'; } if ($act === 'download') { $ok = katex_mu_download_version($ver); if ($ok === true) { katex_mu_update_opt(['version'=>$ver, 'source'=>'local']); echo '<div class="updated"><p>KaTeX '.$ver.' をローカルに保存しました(ソース=local)。</p></div>'; } else { echo '<div class="error"><p>ダウンロード失敗:'.esc_html($ok).'</p></div>'; } } } $opt = katex_mu_opt(); $ver = esc_attr($opt['version']); $src = esc_attr($opt['source']); $has = katex_mu_local_exists($opt['version']); $dir = katex_mu_local_paths($opt['version'])['dir']; ?> <div class="wrap"> <h1>KaTeX (MU) 設定</h1> <form method="post"> <?php wp_nonce_field('katex_mu_save','katex_mu_nonce'); ?> <table class="form-table" role="presentation"> <tr> <th scope="row">読み込みソース</th> <td> <label><input type="radio" name="katex_mu_source" value="auto" <?php checked($src,'auto'); ?>> 自動(ローカル優先)</label><br> <label><input type="radio" name="katex_mu_source" value="local" <?php checked($src,'local'); ?>> ローカル固定</label><br> <label><input type="radio" name="katex_mu_source" value="cdn" <?php checked($src,'cdn'); ?>> CDN固定(jsDelivr)</label> </td> </tr> <tr> <th scope="row">バージョン</th> <td> <input type="text" name="katex_mu_version" value="<?php echo $ver; ?>" class="regular-text" placeholder="例: 0.16.11"> <p class="description">例)0.16.11。将来変更したいときにここを書き換え。</p> <p><strong>ローカル状態:</strong> <?php if ($has): ?> あり(<?php echo esc_html($dir); ?>) <?php else: ?> なし <?php endif; ?> </p> </td> </tr> </table> <p class="submit"> <button type="submit" name="katex_mu_action" value="save" class="button button-primary">設定を保存</button> <button type="submit" name="katex_mu_action" value="download" class="button">このバージョンをローカルにダウンロード</button> </p> </form> <h2>クイック操作</h2> <p> <a href="#" class="button" id="katex-mu-rerender">フロントで再描画を試す(要ログイン)</a> </p> <script> (function(){ document.getElementById('katex-mu-rerender').addEventListener('click', function(e){ e.preventDefault(); window.open('<?php echo esc_url(home_url('/')); ?>?katex_rerender=1','_blank'); }); })(); </script> <h2>ヒント</h2> <ul> <li>文字化け/未描画時:この画面で「このバージョンをローカルにダウンロード」→「設定を保存(ローカル固定or自動)」で復旧。</li> <li>テーマやWP本体更新後は、キャッシュ/最適化プラグインの「結合/遅延」除外に <code>katex.min.js</code> / <code>auto-render.min.js</code> を入れてください。</li> </ul> </div> <?php });
    }); /* ===== ダウンロード実装(jsDelivr固定・検証付き) ===== */
    function katex_mu_download_version($ver) { if (!preg_match('/^\d+\.\d+(\.\d+)?$/', $ver)) return 'version format'; if (!function_exists('wp_remote_get')) return 'http API unavailable'; $base = sprintf(KATEX_CDN_BASE, $ver); $files = [ 'katex.min.css' => $base.'/katex.min.css', 'katex.min.js' => $base.'/katex.min.js', 'auto-render.min.js' => $base.'/contrib/auto-render.min.js', ]; $dir = trailingslashit(katex_mu_upload_dir()).$ver.'/'; if (!wp_mkdir_p($dir)) return 'mkdir failed: '.$dir; foreach ($files as $name=>$url) { $res = wp_remote_get($url, ['timeout'=>20, 'redirection'=>3, 'user-agent'=>'KaTeX-MU']); if (is_wp_error($res)) return $res->get_error_message(); $code = wp_remote_retrieve_response_code($res); $body = wp_remote_retrieve_body($res); if ($code !== 200 || !$body) return 'http '.$code.' for '.$url; // 簡易検証(サイズと先頭チェック) if (strlen($body) < 1024) return 'download too small: '.$name; $dest = $dir.$name; if (false === file_put_contents($dest, $body)) return 'write failed: '.$dest; @chmod($dest, 0644); } return true;
    } /* ===== ログイン時、未ロード検知の簡易通知(管理者のみ) ===== */
    add_action('wp_footer', function(){ if (!current_user_can('manage_options')) return; ?> <script> setTimeout(function(){ if (!(window.katex && window.renderMathInElement)) { var b=document.createElement('div'); b.style.cssText='position:fixed;bottom:10px;right:10px;background:#c00;color:#fff;padding:8px 12px;border-radius:4px;z-index:99999'; b.textContent='[KaTeX] 資産が読み込まれていません。設定>KaTeX(MU)で修復できます。'; document.body.appendChild(b); setTimeout(function(){ b.remove(); }, 8000); } }, 800); </script> <?php
    }, 99);
    
  2. 権限を安全化(任意だが推奨)
    # 例:WPが /var/www/html/wpm の場合は適宜置換
    sudo chown root:root /var/www/html/wp-content/mu-plugins/katex.php
    sudo chmod 644 /var/www/html/wp-content/mu-plugins/katex.php
    # SELinux 使用時
    sudo restorecon -Rv /var/www/html/wp-content/mu-plugins
    # PHP-FPM を再読み込み(OPcache対策 / 環境により不要)
    sudo systemctl reload php-fpm
  3. 動作確認
    インライン:効率 $\eta = \dfrac{T\,\omega}{V\,I}$ ディスプレイ:$$ \eta = \frac{P_{\text{out}}}{P_{\text{in}}} = \frac{T\,\omega}{V\,I} $$

    ※ ディスプレイ数式は 同じ段落(Paragraph)ブロック内で $$ ... $$ を完結させてください。改行は Shift+Enter(ソフト改行)が安全です。

ブロック(ディスプレイ)数式の書き方

基本(1行)

$$ \eta = \frac{P_{\text{out}}}{P_{\text{in}}} = \frac{T\,\omega}{V\,I} $$

複数行(同じ段落内で Shift+Enter)

$$
\eta = \frac{P_{\text{out}}}{P_{\text{in}}} = \frac{T\,\omega}{V\,I}
$$

ポイント: 開き $$ と閉じ $$同じ段落ブロック内に収める。改行は Shift+Enter を使う。

整列表示(aligned)

$$
\begin{aligned}
\eta &= \frac{P_{\text{out}}}{P_{\text{in}}} \\ &= \frac{T\,\omega}{V\,I}
\end{aligned}
$$

代替デリミタ

\[
\eta = \frac{P_{\text{out}}}{P_{\text{in}}} = \frac{T\,\omega}{V\,I}
\]

式番号を付ける(手動)

$$
\eta = \frac{T\,\omega}{V\,I} \tag{1}
$$

ローカル主・CDN予備・管理ボタン付きの安全運用

WordPressやCDNの更新で数式が表示崩れするリスクを最小化するため、本MUプラグインはローカル資産を“主”CDNを“予備(自動フォールバック)”とする構成に対応しています。さらに管理画面からワンクリックで「ダウンロード/切替/再描画」が可能です。

なにが嬉しい?(要点)

  • WP更新に強い:ローカル(/wp-content/uploads/katex/<version>/)を使うので、WP本体更新でも消えません。
  • CDN障害に強い:ロード失敗時は自動で local ⇄ CDN を切替えて復旧を試みます。
  • 運用が楽:管理画面「設定 → KaTeX (MU)」からバージョンのダウンロードやソース切替がボタンで完結。

導入と初期設定

  1. MUプラグインを設置

    ファイル:wp-content/mu-plugins/katex.php
    本記事のコード(v1.6.0)に差し替えます。

  2. 管理画面で設定

    WordPress管理画面 → 設定KaTeX (MU) を開き、以下を操作:

    • 読み込みソース自動(ローカル優先) を推奨(必要に応じて ローカル固定 / CDN固定 も選択可)。
    • バージョン:例 0.16.11(必要になったら数字だけ更新)。
    • このバージョンをローカルにダウンロード:クリックで uploads/katex/<version>/ にCSS/JSを保存。
    • 設定を保存:保存後、以降はローカル資産が優先的に使われます。

日々の運用(トラブル時の復旧フロー)

  1. 数式が崩れた/出ないと気づいたら、設定 → KaTeX (MU) を開く。
  2. このバージョンをローカルにダウンロード設定を保存(ソースは 自動 または ローカル固定)。
  3. ページ再表示で復旧するはず。ダメならキャッシュ系プラグインを一度クリア。

メモ: ログイン中にフロントで未ロードを検出すると、画面右下に赤い通知が出ます(管理者のみ)。

仕組みの概要(技術メモ)

  • ローカル格納先/wp-content/uploads/katex/<version>/katex.min.css / katex.min.js / auto-render.min.js
  • 読み込みポリシー自動=ローカルがあればローカル、無ければCDN。300msで未ロードなら反対側へ自動フォールバック。
  • 軽量化:本文に $$ / $ / \( / \[ / \begin{ があるページだけ読み込みます。
  • 緊急停止wp-config.phpdefine('KATEX_MU_OFF', true); で即時OFF。

トラブルシューティング

  • 数式が出ない:ページソースに katex.min.js / auto-render.min.js が無い → MU配置パスやファイル名を確認(mu-plugins直下が原則)。
  • 最適化プラグインと競合:JS結合/遅延の除外に katex.min.js / auto-render.min.js / 可能なら cdn.jsdelivr.net を追加。
  • ブロックの種類pre/code ブロックは無視対象。段落(Paragraph)ブロックで入力。
  • サブディレクトリ運用:WPが /wpm 等なら、.../wpm/wp-content/mu-plugins/katex.php に設置。
  • CSPでブロック:厳格CSPなら https://cdn.jsdelivr.netscript-src / style-src に許可。

緊急停止(退路)

# wp-config.php に追記
define('KATEX_MU_OFF', true);

まとめ

  • KaTeXは速くて軽い、数式表示の定番
  • MUプラグイン化で置くだけ常時有効&テーマ非依存
  • v1.6.0ローカル主・CDN予備・管理ボタン付きで実運用に最適
  • ブロック数式は同じ段落内で完結(改行は Shift+Enter)がコツ

修正版を掲示

v1.6.7 診断パネル(?katex_diag=1)とキャッシュバスター(?katex_nocache=1)、恒久のver付与を追加。

<?php
/** * Plugin Name: KaTeX Renderer (MU) * Description: WordPressでKaTeXを安全・高速に。本文に数式があるページだけ読み込み。ローカル資産を“主”、CDNを“予備”。v1.6.7 は診断パネル(?katex_diag=1)とキャッシュバスター(?katex_nocache=1)、恒久のver付与を追加。 * Version: 1.6.7 * Author: MASA * Update URI: false */ if (defined('KATEX_MU_OFF') && KATEX_MU_OFF) return;
if (!defined('KATEX_MU_PLUGIN_VER')) define('KATEX_MU_PLUGIN_VER', '1.6.7'); /* ===== 基本設定 ===== */
if (!defined('KATEX_DEFAULT_VER')) define('KATEX_DEFAULT_VER', '0.16.11');
if (!defined('KATEX_CDN_BASE')) define('KATEX_CDN_BASE', 'https://cdn.jsdelivr.net/npm/katex@%s/dist'); // %s=ver function katex_mu_get_bool($key){ return isset($_GET[$key]) && ($_GET[$key]==='1' || $_GET[$key]==='true'); }
$KATEX_FORCE_ON = katex_mu_get_bool('katex_on');
$KATEX_DEBUG = katex_mu_get_bool('katex_debug');
$KATEX_NOCACHE = katex_mu_get_bool('katex_nocache');
$KATEX_DIAG = katex_mu_get_bool('katex_diag'); /* ===== uploads 配下 ===== */
function katex_mu_upload_dir() { $up = wp_get_upload_dir(); return trailingslashit($up['basedir']).'katex/'; }
function katex_mu_upload_url() { $up = wp_get_upload_dir(); return trailingslashit($up['baseurl']).'katex/'; } /* ===== オプション ===== */
function katex_mu_opt($key = null) { $opt = get_option('katex_mu_opt', ['source'=>'auto','version'=>KATEX_DEFAULT_VER]); if (!$opt || !is_array($opt)) $opt = ['source'=>'auto','version'=>KATEX_DEFAULT_VER]; return $key ? ($opt[$key] ?? null) : $opt;
}
function katex_mu_update_opt($arr) { $opt = katex_mu_opt(); update_option('katex_mu_opt', array_merge($opt, $arr), false);
} /* ===== “本文に数式がある時だけ”読み込む判定 ===== */
$GLOBALS['katex_mu_should_load'] = false;
add_filter('the_posts', function(array $posts){ foreach ($posts as $p) { $c = is_object($p) ? (get_post_field('post_content', $p) ?? '') : ''; if ($c && preg_match('/\$\$|(?<!\\\\)\$|\\\\\(|\\\\\[|\\\\begin\{/', $c)) { $GLOBALS['katex_mu_should_load'] = true; break; } } return $posts;
}, 0); add_action('wp', function () use ($KATEX_FORCE_ON) { if ($KATEX_FORCE_ON) { $GLOBALS['katex_mu_should_load'] = true; return; } if (!empty($GLOBALS['katex_mu_should_load'])) return; if (function_exists('amp_is_request') && amp_is_request()) return; // AMPは任意JS不可 if (is_singular()) { $p = get_queried_object(); if ($p && !empty($p->post_content)) { if (preg_match('/\$\$|(?<!\\\\)\$|\\\\\(|\\\\\[|\\\\begin\{/', $p->post_content)) { $GLOBALS['katex_mu_should_load'] = true; } } }
}, 0); /* ===== 資産のURLと存在チェック ===== */
function katex_mu_local_paths($ver) { $dir = trailingslashit(katex_mu_upload_dir()).$ver.'/'; $url = trailingslashit(katex_mu_upload_url()).$ver.'/'; return [ 'dir'=>$dir,'url'=>$url, 'css'=>['path'=>$dir.'katex.min.css','url'=>$url.'katex.min.css'], 'js' =>['path'=>$dir.'katex.min.js','url'=>$url.'katex.min.js'], 'auto'=>['path'=>$dir.'auto-render.min.js','url'=>$url.'auto-render.min.js'], ];
}
function katex_mu_local_exists($ver) { $p = katex_mu_local_paths($ver); return file_exists($p['css']['path']) && file_exists($p['js']['path']) && file_exists($p['auto']['path']);
} /* ===== 先読み(軽いお守り) ===== */
add_action('wp_head', function(){ if (is_admin() || empty($GLOBALS['katex_mu_should_load'])) return; $opt = katex_mu_opt(); $ver = $opt['version'] ?? KATEX_DEFAULT_VER; $local = katex_mu_local_paths($ver); $has_local = katex_mu_local_exists($ver); $cdn = sprintf(KATEX_CDN_BASE, $ver); if ($has_local) { echo '<link rel="preload" as="style" href="'.esc_url($local['css']['url']).'">'."\n"; echo '<link rel="preload" as="script" href="'.esc_url($local['js']['url']).'">'."\n"; echo '<link rel="preload" as="script" href="'.esc_url($local['auto']['url']).'">'."\n"; } else { echo '<link rel="preload" as="style" href="'.esc_url($cdn.'/katex.min.css').'">'."\n"; echo '<link rel="preload" as="script" href="'.esc_url($cdn.'/katex.min.js').'">'."\n"; echo '<link rel="preload" as="script" href="'.esc_url($cdn.'/contrib/auto-render.min.js').'">'."\n"; }
}, 3); /* ===== エンキュー(ver付与/キャッシュバスター対応) ===== */
add_action('wp_enqueue_scripts', function () use ($KATEX_FORCE_ON, $KATEX_DEBUG, $KATEX_NOCACHE) { if (is_admin()) return; if (empty($GLOBALS['katex_mu_should_load']) && !$KATEX_FORCE_ON) return; $opt = katex_mu_opt(); $ver = preg_match('/^\d+\.\d+(\.\d+)?$/', $opt['version'] ?? '') ? $opt['version'] : KATEX_DEFAULT_VER; $src = in_array($opt['source'] ?? 'auto', ['auto','local','cdn'], true) ? $opt['source'] : 'auto'; // nocacheなら一時ver $tmpVer = $KATEX_NOCACHE ? (string) time() : null; $local = katex_mu_local_paths($ver); $has_local = katex_mu_local_exists($ver); $use_local = ($src === 'local') || ($src === 'auto' && $has_local); if ($use_local) { // 恒久バスティング:filemtime、nocacheなら time() $v_css = $tmpVer ?: (file_exists($local['css']['path']) ? (string)filemtime($local['css']['path']) : KATEX_MU_PLUGIN_VER); $v_js = $tmpVer ?: (file_exists($local['js']['path']) ? (string)filemtime($local['js']['path']) : KATEX_MU_PLUGIN_VER); $v_auto = $tmpVer ?: (file_exists($local['auto']['path']) ? (string)filemtime($local['auto']['path']) : KATEX_MU_PLUGIN_VER); wp_enqueue_style ('katex-css', $local['css']['url'], [], $v_css); wp_enqueue_script('katex-js', $local['js']['url'], [], $v_js, true); wp_enqueue_script('katex-auto', $local['auto']['url'], ['katex-js'], $v_auto, true); } else { $cdn = sprintf(KATEX_CDN_BASE, $ver); $v = $tmpVer ?: $ver; // CDNはKaTeX版をverに wp_enqueue_style ('katex-css', $cdn.'/katex.min.css', [], $v); wp_enqueue_script('katex-js', $cdn.'/katex.min.js', [], $v, true); wp_enqueue_script('katex-auto', $cdn.'/contrib/auto-render.min.js', ['katex-js'], $v, true); } if ($KATEX_DEBUG) wp_add_inline_style('katex-css', '/* KaTeX MU debug: '.($use_local?'local':'cdn').' '.$ver.' */');
}, 5); /* ===== 初期化+フォールバック+“縫い直し”+IMEセーフ+ラグ対策+診断パネル ===== */
function katex_mu_print_init_script() { if (is_admin()) return; $should = !empty($GLOBALS['katex_mu_should_load']) || (isset($_GET['katex_on']) && $_GET['katex_on']); if (!$should) return; $opt = katex_mu_opt(); $ver = $opt['version'] ?? KATEX_DEFAULT_VER; $local = katex_mu_local_paths($ver); $cdn = sprintf(KATEX_CDN_BASE, $ver); $using_local = ( ( $opt['source']==='local') || ($opt['source']==='auto' && katex_mu_local_exists($ver)) ); $diag = current_user_can('manage_options') && isset($_GET['katex_diag']) && $_GET['katex_diag']; ?> <script> (function(){ var DEBUG = <?php echo (isset($_GET['katex_debug']) && $_GET['katex_debug']) ? 'true' : 'false'; ?>; var USING_LOCAL = <?php echo $using_local ? 'true':'false'; ?>; var KATEX_VER = "<?php echo esc_js($ver); ?>"; var DIAG_MODE = <?php echo $diag ? 'true':'false'; ?>; function log(){ if(DEBUG && window.console){ try{ console.log.apply(console, ['[KaTeX MU]'].concat([].slice.call(arguments))); }catch(e){} } } /* === IMEセーフ(全角記号→ASCII) === */ function walkTextNodes(root, fn){ var w=document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); var n; while(n=w.nextNode()){ fn(n); } } function normalizeFullwidthMathDelims(root){ walkTextNodes(root, function(node){ var t=node.nodeValue; if(!t) return; if (t.indexOf('\\(')!==-1 || t.indexOf('\\[')!==-1 || t.indexOf('$$')!==-1 || t.indexOf('$')!==-1){ var nt = t.replace(/[\u00A5\uFFE5\uFF3C]/g, '\\').replace(/\uFF08/g, '(').replace(/\uFF09/g, ')').replace(/\uFF3B/g, '[').replace(/\uFF3D/g, ']'); if (nt!==t) { node.nodeValue = nt; log('normalized fullwidth in math node'); } } }); } /* === “縫い直し”(<br>/3段落/インライン割れ) === */ function flattenWithBRToText(p){ var buf=''; for (var i=0;i<p.childNodes.length;i++){ var n=p.childNodes[i]; if (n.nodeType===3){ buf += n.textContent; } else if (n.nodeName==='BR'){ buf += '\n'; } else { buf += (n.textContent||''); } } return buf; } function stitchParagraphBR(root, left, right){ var ps = root.querySelectorAll('p'); for (var i=0; i<ps.length; i++){ var p = ps[i]; if (!p) continue; if (!p.querySelector('br')) continue; var t = flattenWithBRToText(p).replace(/\u00a0/g,' ').trim(); var lre = new RegExp('^\\s*'+left.replace(/[-/\\^$*+?.()|[\]{}]/g,'\\$&')); var rre = new RegExp(right.replace(/[-/\\^$*+?.()|[\]{}]/g,'\\$&')+'\\s*$'); if (lre.test(t) && rre.test(t)) { p.textContent = t; p.setAttribute('data-katex-stitched','br'); log('stitched br paragraph', left+right); } } } function stitchParagraphTriplet(root, left, right){ var ps = root.querySelectorAll('p'); for (var i=0; i<ps.length-2; i++){ var a=ps[i], b=ps[i+1], c=ps[i+2]; if (!a || !b || !c) continue; var at=(a.textContent||'').trim(), ct=(c.textContent||'').trim(); if (at!==left || ct!==right) continue; var mid = flattenWithBRToText(b).replace(/\u00a0/g,' '); b.textContent = left + mid + right; a.remove(); c.remove(); b.setAttribute('data-katex-stitched','triplet'); log('stitched triplet', left+right); } } function normalizeMathBlocks(root){ stitchParagraphBR(root, '$$', '$$'); stitchParagraphTriplet(root, '$$', '$$'); stitchParagraphBR(root, '\\[', '\\]'); stitchParagraphTriplet(root, '\\[', '\\]'); stitchParagraphBR(root, '\\(', '\\)'); stitchParagraphTriplet(root, '\\(', '\\)'); } function inject(tag, attrs){ var el=document.createElement(tag); for(var k in attrs){ if(attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); } document.head.appendChild(el); return el; } function initIfReady(){ if (typeof renderMathInElement !== 'function') return false; try { renderMathInElement(document.body,{ delimiters:[ {left:"$$", right:"$$", display:true}, {left:"$", right:"$", display:false}, {left:"\\[", right:"\\]", display:true}, {left:"\\(", right:"\\)", display:false} ], throwOnError:false, strict:"ignore", ignoredTags:["script","noscript","style","textarea","pre","code","kbd","samp"] }); log('rendered'); } catch(e) { log('render error', e); } return true; } function waitAndInit(timeoutMs){ var start=Date.now(); (function tick(){ if (initIfReady()) return; if (Date.now()-start > timeoutMs) { log('timeout waiting'); return; } setTimeout(tick, 80); })(); } // 実行シーケンス function run(){ normalizeFullwidthMathDelims(document.body); normalizeMathBlocks(document.body); waitAndInit(12000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run, {once:true}); } else { run(); } ['touchstart','scroll','keydown','pointerdown','click'].forEach(function(ev){ window.addEventListener(ev, function once(){ run(); window.removeEventListener(ev, once); }, {passive:true}); }); var mo = new MutationObserver(function(muts){ for (var i=0;i<muts.length;i++){ var m=muts[i]; if (m.addedNodes && m.addedNodes.length){ normalizeFullwidthMathDelims(document.body); normalizeMathBlocks(document.body); waitAndInit(4000); break; } } }); try{ mo.observe(document.body, {childList:true, subtree:true}); }catch(e){} window.addEventListener('pageshow', function(){ try{ normalizeFullwidthMathDelims(document.body); normalizeMathBlocks(document.body); }catch(e){} try{ waitAndInit(8000); }catch(e){} }, {passive:true}); document.addEventListener('visibilitychange', function(){ if (document.visibilityState === 'visible') { try{ normalizeFullwidthMathDelims(document.body); normalizeMathBlocks(document.body); }catch(e){} try{ waitAndInit(8000); }catch(e){} } }); // 早めフォールバック:100msで local⇄CDN 注入 setTimeout(function(){ var loaded = !!(window.katex && window.renderMathInElement); if (loaded) { log('ready without fallback'); return; } log('injecting fallback (<?php echo $using_local?'cdn':'local'; ?>)'); <?php if ($using_local): ?> inject('link',{rel:'stylesheet',href:'<?php echo esc_js($cdn); ?>/katex.min.css'}); inject('script',{src:'<?php echo esc_js($cdn); ?>/katex.min.js',defer:''}); inject('script',{src:'<?php echo esc_js($cdn); ?>/contrib/auto-render.min.js',defer:''}); <?php else: ?> inject('link',{rel:'stylesheet',href:'<?php echo esc_js($local['css']['url']); ?>'}); inject('script',{src:'<?php echo esc_js($local['js']['url']); ?>',defer:''}); inject('script',{src:'<?php echo esc_js($local['auto']['url']); ?>',defer:''}); <?php endif; ?> normalizeFullwidthMathDelims(document.body); normalizeMathBlocks(document.body); waitAndInit(12000); }, 100); // リトライ保険:3秒おきに最大30秒 (function retryGuard(){ var started = Date.now(); var tid = setInterval(function(){ if (window.katex && window.renderMathInElement) { clearInterval(tid); return; } try { normalizeFullwidthMathDelims(document.body); normalizeMathBlocks(document.body); } catch(e){} try { waitAndInit(4000); } catch(e){} if (Date.now() - started > 30000) clearInterval(tid); }, 3000); })(); // === 診断パネル(管理者+?katex_diag=1) === if (DIAG_MODE) { var box=document.createElement('div'); box.style.cssText='position:fixed;right:10px;bottom:10px;background:#111;color:#fff;font:12px/1.3 system-ui, sans-serif;padding:10px 12px;border-radius:8px;z-index:999999;opacity:.95;max-width:320px'; function row(k,v){ var p=document.createElement('div'); p.innerHTML='<b>'+k+':</b> '+v; return p; } var bodyText = (document.body.innerText||''); var counts = { '$$': (bodyText.match(/\$\$/g)||[]).length, '\\(': (bodyText.match(/\\\(/g)||[]).length, '\\[': (bodyText.match(/\\\[/g)||[]).length }; box.appendChild(row('should_load', 'yes')); box.appendChild(row('source', USING_LOCAL?'local':'cdn')); box.appendChild(row('katex_ver', KATEX_VER)); box.appendChild(row('delims', '$$:'+counts['$$']+' \\(:'+counts['\\(']+' \\[:'+counts['\\['])); var btns=document.createElement('div'); btns.style.marginTop='6px'; function mkbtn(txt,fn){ var b=document.createElement('button'); b.textContent=txt; b.style.cssText='margin-right:6px;margin-top:4px;border:0;border-radius:6px;padding:6px 8px;background:#2a7;color:#fff;cursor:pointer'; b.onclick=fn; return b; } btns.appendChild(mkbtn('再描画', function(){ run(); })); btns.appendChild(mkbtn('ハード注入(CDN)', function(){ var c = "<?php echo esc_js($cdn); ?>"; inject('link',{rel:'stylesheet',href:c+'/katex.min.css'}); inject('script',{src:c+'/katex.min.js',defer:''}); inject('script',{src:c+'/contrib/auto-render.min.js',defer:''}); setTimeout(function(){ run(); }, 200); })); btns.appendChild(mkbtn('閉じる', function(){ box.remove(); })); box.appendChild(btns); document.body.appendChild(box); } })(); </script> <style>.katex-display{margin:.9em 0}.katex{font-size:1.05em}</style> <?php
}
// footerが無いテーマ対策:body_open と footer の両方で出す(重複防止)
function katex_mu_once_init_script(){ static $done=false; if($done) return; $done=true; katex_mu_print_init_script();
}
add_action('wp_body_open', 'katex_mu_once_init_script', 5);
add_action('wp_footer', 'katex_mu_once_init_script', 20); /* ===== 管理画面:設定/ダウンロード/切替/再描画 ===== */
add_action('admin_menu', function () { add_options_page('KaTeX (MU)', 'KaTeX (MU)', 'manage_options', 'katex-mu', function () { if (!current_user_can('manage_options')) return; if (isset($_POST['katex_mu_action']) && check_admin_referer('katex_mu_save','katex_mu_nonce')) { $act = sanitize_text_field($_POST['katex_mu_action']); $ver = sanitize_text_field($_POST['katex_mu_version'] ?? KATEX_DEFAULT_VER); if (!preg_match('/^\d+\.\d+(\.\d+)?$/', $ver)) $ver = KATEX_DEFAULT_VER; if ($act === 'save') { $src = sanitize_text_field($_POST['katex_mu_source'] ?? 'auto'); if (!in_array($src, ['auto','local','cdn'], true)) $src = 'auto'; katex_mu_update_opt(['source'=>$src,'version'=>$ver]); echo '<div class="updated"><p>設定を保存しました。</p></div>'; } if ($act === 'download') { $ok = katex_mu_download_version($ver); if ($ok === true) { katex_mu_update_opt(['version'=>$ver, 'source'=>'local']); echo '<div class="updated"><p>KaTeX '.$ver.' をローカルに保存しました(ソース=local)。</p></div>'; } else { echo '<div class="error"><p>ダウンロード失敗:'.esc_html($ok).'</p></div>'; } } } $opt = katex_mu_opt(); $ver = esc_attr($opt['version']); $src = esc_attr($opt['source']); $has = katex_mu_local_exists($opt['version']); $dir = katex_mu_local_paths($opt['version'])['dir']; ?> <div class="wrap"> <h1>KaTeX (MU) 設定</h1> <form method="post"> <?php wp_nonce_field('katex_mu_save','katex_mu_nonce'); ?> <table class="form-table" role="presentation"> <tr> <th scope="row">読み込みソース</th> <td> <label><input type="radio" name="katex_mu_source" value="auto" <?php checked($src,'auto'); ?>> 自動(ローカル優先)</label><br> <label><input type="radio" name="katex_mu_source" value="local" <?php checked($src,'local'); ?>> ローカル固定</label><br> <label><input type="radio" name="katex_mu_source" value="cdn" <?php checked($src,'cdn'); ?>> CDN固定(jsDelivr)</label> </td> </tr> <tr> <th scope="row">バージョン</th> <td> <input type="text" name="katex_mu_version" value="<?php echo $ver; ?>" class="regular-text" placeholder="例: 0.16.11"> <p class="description">例)0.16.11。将来変更したいときにここを書き換え。</p> <p><strong>ローカル状態:</strong> <?php if ($has): ?> あり(<?php echo esc_html($dir); ?>) <?php else: ?> なし <?php endif; ?> </p> </td> </tr> </table> <p class="submit"> <button type="submit" name="katex_mu_action" value="save" class="button button-primary">設定を保存</button> <button type="submit" name="katex_mu_action" value="download" class="button">このバージョンをローカルにダウンロード</button> </p> </form> <h2>クイック操作</h2> <p> <a href="#" class="button" id="katex-mu-rerender">フロントで再描画を試す(要ログイン)</a> <a href="<?php echo esc_url( home_url( add_query_arg(['katex_on'=>1,'katex_debug'=>1,'katex_nocache'=>1,'katex_diag'=>1], '/') ) ); ?>" target="_blank" class="button">強制ON+デバッグ+診断でトップ表示</a> </p> <script> (function(){ var a=document.getElementById('katex-mu-rerender'); if(a){ a.addEventListener('click', function(e){ e.preventDefault(); window.open('<?php echo esc_url(home_url('/')); ?>?katex_on=1&katex_debug=1&katex_nocache=1&katex_diag=1','_blank'); });} })(); </script> <h2>ヒント</h2> <ul> <li>文字化け/未描画時:この画面で「このバージョンをローカルにダウンロード」→「設定を保存(ローカル固定or自動)」で復旧。</li> <li>キャッシュ/最適化系プラグインでは <code>katex.min.js</code> / <code>auto-render.min.js</code> / <code>/wp-content/uploads/katex/</code> を<strong>結合/遅延の除外</strong>に。</li> </ul> </div> <?php });
}); /* ===== ダウンロード実装(jsDelivr固定・検証付き) ===== */
function katex_mu_download_version($ver) { if (!preg_match('/^\d+\.\d+(\.\d+)?$/', $ver)) return 'version format'; if (!function_exists('wp_remote_get')) return 'http API unavailable'; $base = sprintf(KATEX_CDN_BASE, $ver); $files = [ 'katex.min.css' => $base.'/katex.min.css', 'katex.min.js' => $base.'/katex.min.js', 'auto-render.min.js' => $base.'/contrib/auto-render.min.js', ]; $dir = trailingslashit(katex_mu_upload_dir()).$ver.'/'; if (!wp_mkdir_p($dir)) return 'mkdir failed: '.$dir; foreach ($files as $name=>$url) { $res = wp_remote_get($url, ['timeout'=>20, 'redirection'=>3, 'user-agent'=>'KaTeX-MU']); if (is_wp_error($res)) return $res->get_error_message(); $code = wp_remote_retrieve_response_code($res); $body = wp_remote_retrieve_body($res); if ($code !== 200 || !$body) return 'http '.$code.' for '.$url; if (strlen($body) < 1024) return 'download too small: '.$name; $dest = $dir.$name; if (false === file_put_contents($dest, $body)) return 'write failed: '.$dest; @chmod($dest, 0644); } return true;
} /* ===== ログイン時、未ロード検知の簡易通知(管理者のみ・should_load時だけ) ===== */
add_action('wp_footer', function(){ if (!current_user_can('manage_options')) return; $should = !empty($GLOBALS['katex_mu_should_load']) || (isset($_GET['katex_on']) && $_GET['katex_on']); if (!$should) return; ?> <script> setTimeout(function(){ if (!(window.katex && window.renderMathInElement)) { var b=document.createElement('div'); b.style.cssText='position:fixed;bottom:10px;right:10px;background:#c00;color:#fff;padding:8px 12px;border-radius:4px;z-index:99999'; b.textContent='[KaTeX] 資産が読み込まれていません。?katex_on=1&katex_debug=1&katex_nocache=1&katex_diag=1 で確認を。'; document.body.appendChild(b); setTimeout(function(){ try{ b.remove(); }catch(e){} }, 8000); } }, 800); </script> <?php
}, 99);

v1.6.6 警告ぶの修正

v1.6.4 記載方法のバグ修正
    表示ラグ修正

v1.6.1 携帯端末のアクセス中に変換されないを修正