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 記載方法のバグ修正
    表示ラグ修正

<?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 携帯端末のアクセス中に変換されないを修正