本記事では、WordPressで KaTeX をMUプラグインとして導入し、$...$
/ $$...$$
/ \( \)
/ \[ \]
を記事内でそのまま使えるようにする方法を解説します。MU(Must-Use)プラグインは設置するだけで常時有効化され、テーマや通常プラグインに依存せず安定稼働します。
なぜKaTeX & MUプラグイン?
- 高速・軽量:MathJaxより描画が速く、フロント負荷が小さい
- 設置が簡単:MUは
wp-content/mu-plugins
に置くだけで有効 - 安全運用:ファイル所有者を
root
にすれば管理画面から改変されない - 軽量化内蔵:本文に数式があるページだけ KaTeXを読み込みます
- 退路つき:緊急停止用フラグ(
wp-config.php
でOFF)を搭載
インストール手順(MUプラグイン)
- 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);
- 権限を安全化(任意だが推奨)
# 例: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
- 動作確認
インライン:効率 $\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)」からバージョンのダウンロードやソース切替がボタンで完結。
導入と初期設定
- MUプラグインを設置
ファイル:
wp-content/mu-plugins/katex.php
本記事のコード(v1.6.0)に差し替えます。 - 管理画面で設定
WordPress管理画面 → 設定 → KaTeX (MU) を開き、以下を操作:
- 読み込みソース:自動(ローカル優先) を推奨(必要に応じて ローカル固定 / CDN固定 も選択可)。
- バージョン:例
0.16.11
(必要になったら数字だけ更新)。 - このバージョンをローカルにダウンロード:クリックで
uploads/katex/<version>/
にCSS/JSを保存。 - 設定を保存:保存後、以降はローカル資産が優先的に使われます。
日々の運用(トラブル時の復旧フロー)
- 数式が崩れた/出ないと気づいたら、設定 → KaTeX (MU) を開く。
- このバージョンをローカルにダウンロード → 設定を保存(ソースは 自動 または ローカル固定)。
- ページ再表示で復旧するはず。ダメならキャッシュ系プラグインを一度クリア。
メモ: ログイン中にフロントで未ロードを検出すると、画面右下に赤い通知が出ます(管理者のみ)。
仕組みの概要(技術メモ)
- ローカル格納先:
/wp-content/uploads/katex/<version>/
(katex.min.css
/katex.min.js
/auto-render.min.js
) - 読み込みポリシー:自動=ローカルがあればローカル、無ければCDN。300msで未ロードなら反対側へ自動フォールバック。
- 軽量化:本文に
$$
/$
/\(
/\[
/\begin{
があるページだけ読み込みます。 - 緊急停止:
wp-config.php
にdefine('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.net
をscript-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 記載方法のバグ修正
表示ラグ修正
<?php
/**
* Plugin Name: KaTeX Renderer (MU)
* Description: WordPressでKaTeXを安全・高速に。本文に数式があるページだけ読み込み。ローカル資産を“主”、CDNを“予備”にし、管理画面からバージョンDL/切替/再描画ができます。v1.6.4 は「縫い直し」+ラグ対策(早めフォールバック/長め待機/再試行/復帰イベント)を統合。
* Version: 1.6.4
* 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
// GETでの強制ON/デバッグ(例: ?katex_on=1&katex_debug=1)
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');
/* ===== uploads 配下(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);
}
/* ===== “本文に数式がある時だけ”読み込む判定 ===== */
// (1) ループの投稿をざっとスキャン
$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);
// (2) 実際に表示中の本文もチェック(抜粋/別テンプレ対策)
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);
/* ===== エンキュー(source: auto/local/cdn) ===== */
add_action('wp_enqueue_scripts', function () use ($KATEX_FORCE_ON, $KATEX_DEBUG) {
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';
$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);
}
if ($KATEX_DEBUG) wp_add_inline_style('katex-css', '/* KaTeX MU debug: '.($use_local?'local':'cdn').' '.$ver.' */');
}, 5);
/* ===== 初期化+自動フォールバック+“縫い直し”+ラグ対策 ===== */
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)) );
?>
<script>
(function(){
var DEBUG = <?php echo (isset($_GET['katex_debug']) && $_GET['katex_debug']) ? 'true' : 'false'; ?>;
function log(){ if(DEBUG && window.console){ try{ console.log.apply(console, ['[KaTeX MU]'].concat([].slice.call(arguments))); }catch(e){} } }
/* === “縫い直し”前処理 ===
1) <p>$$<br> ... <br>$$</p> → "$$...$$" 1テキストに
2) <p>$$</p><p> ... </p><p>$$</p> → 1つに結合
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; // → 1テキストノード
p.setAttribute('data-katex-stitched','br');
log('stitched br paragraph', left+right, p);
}
}
}
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,' ');
var joined = left + mid + right;
b.textContent = joined;
a.remove(); c.remove();
b.setAttribute('data-katex-stitched','triplet');
log('stitched triplet', left+right, b);
}
}
function normalizeMathBlocks(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);
})();
}
// 1) DOM準備 → 縫い直し → “長めに待って初期化”(12s)
function run(){
normalizeMathBlocks(document.body);
waitAndInit(12000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run, {once:true});
} else { run(); }
// 2) ユーザー操作で遅延解除されるタイプにも対応
['touchstart','scroll','keydown','pointerdown','click'].forEach(function(ev){
window.addEventListener(ev, function once(){ run(); window.removeEventListener(ev, once); }, {passive:true});
});
// 3) 後挿入にも対応(MutationObserver)
var mo = new MutationObserver(function(muts){
for (var i=0;i<muts.length;i++){
var m=muts[i];
if (m.addedNodes && m.addedNodes.length){ normalizeMathBlocks(document.body); waitAndInit(4000); break; }
}
});
try{ mo.observe(document.body, {childList:true, subtree:true}); }catch(e){}
// 4) bfcache/タブ復帰での再初期化
window.addEventListener('pageshow', function(){ try{ normalizeMathBlocks(document.body); }catch(e){} try{ waitAndInit(8000); }catch(e){} }, {passive:true});
document.addEventListener('visibilitychange', function(){ if (document.visibilityState === 'visible') { try{ normalizeMathBlocks(document.body); }catch(e){} try{ waitAndInit(8000); }catch(e){} } });
// 5) 早めフォールバック:100msで local⇄CDN を注入 → 12s待ち
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; ?>
normalizeMathBlocks(document.body);
waitAndInit(12000);
}, 100);
// 6) リトライ保険:3秒おきに最大30秒
(function retryGuard(){
var started = Date.now();
var tid = setInterval(function(){
if (window.katex && window.renderMathInElement) { clearInterval(tid); return; }
try { normalizeMathBlocks(document.body); } catch(e){}
try { waitAndInit(4000); } catch(e){}
if (Date.now() - started > 30000) clearInterval(tid);
}, 3000);
})();
})();
</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], '/') ) ); ?>" 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_rerender=1','_blank');
});}
})();
</script>
<h2>ヒント</h2>
<ul>
<li>文字化け/未描画時:この画面で「このバージョンをローカルにダウンロード」→「設定を保存(ローカル固定or自動)」で復旧。</li>
<li>キャッシュ/最適化系プラグインでは <code>katex.min.js</code> / <code>auto-render.min.js</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;
}
/* ===== ログイン時、未ロード検知の簡易通知(管理者のみ) ===== */
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) や ?katex_on=1&katex_debug=1 で確認を。';
document.body.appendChild(b);
setTimeout(function(){ try{ b.remove(); }catch(e){} }, 8000);
}
}, 800);
</script>
<?php
}, 99);
v1.6.1 携帯端末のアクセス中に変換されないを修正