Wordpress

Shikiでシンタックスハイライトブロック

ということで前回記事のPrismでシンタックスハイライトブロックの進化版、Shikiでシンタックスハイライトブロックにゃー

人物のアイコン素材 その5

ところでShikiってあまり聞いたことがないのですがなんでしょうか?

Prismと同じシンタックスハイライトしてくれるヤツにゃー
こういうやつはいくつかあるけどShikiは新しめのプロジェクトらしい
Macの定番テキストエディタらしいTextMateの文法とテーマをベースにして高度なカスタマイズが可能なものらしい

人物のアイコン素材 その5

そうなんですね
らしいばかりで怪しいのですけど

VS Codeも同じ仕組みで動いているみたいだしNode.jsのWebサイトでも採用されているみたいなので品質や将来性は問題ないだろう

人物のアイコン素材 その5

もう作ってしまったようですし
まあいいでしょう

コードのポイント解説

<div useBlockProps
  class="pt-shiki"
  data-lang="{{language}}"
  data-theme="{{theme}}"
  data-line-numbers="{{line_numbers}}"
  aria-label="Source code ({{language}})"
  role="region"
>
  <!--
    NOTE:
    - JS 無効時でも表示される安全なプレーン構造
    - Shiki はこの <pre> を丸ごと置き換える
  -->
  <pre><code class="language-{{language}}">{{code}}</code></pre>
</div>


<style>
/* ======================================================
   Shiki Code Block — Complete Stable CSS
   LazyBlocks + Gutenberg + Frontend 対応
====================================================== */

/* ---------- ルート変数 ---------- */
.pt-shiki {
  /* ========= フォント管理(ここだけ変更すれば全体反映)========= */
  --pt-shiki-font-size: 16px;

  /* 行高はここだけを変更すれば全体が同期 */
  --pt-shiki-line-height: calc(var(--pt-shiki-font-size) * 1.6);

  /* フォールバック配色(JS失敗時) */
  --pt-shiki-bg: #0d1117;
  --pt-shiki-fg: #c9d1d9;

  /* =========================================================
     ブロック間余白(重要)
     ========================================================= */
  margin-block: var(--wp--style--block-gap, 1.5em);
}

/* ======================================================
   初期状態(JS未実行・Shiki未適用)
   - 可読性確保
   - CLS抑制
====================================================== */
.pt-shiki pre {
  /*margin: 0;*/
  padding: 14px;
  border-radius: 6px;
  overflow: auto;

  font-size: var(--pt-shiki-font-size);
  line-height: var(--pt-shiki-line-height);
  font-family:
    ui-monospace,
    SFMono-Regular,
    Menlo,
    Consolas,
    "Liberation Mono",
    monospace;

  white-space: pre;

  background: var(--pt-shiki-bg);
  color: var(--pt-shiki-fg);

  content-visibility: auto;
  contain-intrinsic-size: 1px 240px;
}

/* Gutenberg / theme の code リセット打ち消し */
.pt-shiki pre code {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  display: block;
  margin: 0;
  padding: 0;
}

/* ======================================================
   Shiki適用後
====================================================== */

.pt-shiki pre.shiki {
  /*margin: 0;*/
  line-height: normal; /* ← 親のline-heightを無効化 */
  content-visibility: visible;
}

/* &#x2757;重要:行ボックス二重化防止 */
.pt-shiki pre.shiki code {
  display: block !important;
  line-height: 0 !important;
}

/* ======================================================
   各行(Shiki生成)
====================================================== */

.pt-shiki pre.shiki .line {
  display: block;
  position: relative;
  white-space: pre;

  /* 行番号用余白 */
  padding-left: 3.5em;

  /* 行高をここだけに集中 */
  line-height: var(--pt-shiki-line-height) !important;
  min-height: var(--pt-shiki-line-height);

  margin: 0;
}

/* 空行でも高さを持たせる */
.pt-shiki pre.shiki .line:empty::before {
  content: "\200B";
}

/* ======================================================
   行番号
====================================================== */

.pt-shiki .line-number {
  position: absolute;
  left: 0;
  width: 3em;
  text-align: right;

  opacity: .45;
  user-select: none;
  pointer-events: none;
  font-variant-numeric: tabular-nums;

  line-height: var(--pt-shiki-line-height);
}

/* ======================================================
   Gutenberg Editor 専用(LazyBlocks previewのみ)
====================================================== */

/*
  - エディタ専用
  - フロントには一切影響しない
*/
.editor-styles-wrapper
.lzb-preview-server
.pt-shiki
pre {
  height: 10em;
  overflow-y: auto;
  font-size: 13px;
}

/* Gutenberg の余計なマージンを殺す */
.wp-block-lazyblock-shiki-code pre {
  /*margin-top: 0 !important;*/
  /*margin-bottom: 0 !important;*/
}
</style>


<script type="module">
/*
  NOTE:
  - Frontend のみで Shiki を初期化
  - Editor / 再初期化は行わない(安全優先)
*/
if (
  !window.__LB_SHIKI_INIT__ &&
  !document.body.classList.contains('block-editor-page')
) {
  window.__LB_SHIKI_INIT__ = true;

  // NOTE: CDN バージョンを単一管理
  const SHIKI_VER = '3.21.0';

  async function initShiki() {
    const blocks = Array.from(document.querySelectorAll('.pt-shiki'));
    if (!blocks.length) return;

    // NOTE: ESM import(失敗時は catch される)
    const shiki = await import(
      `https://cdn.jsdelivr.net/npm/shiki@${SHIKI_VER}/+esm`
    );

    // 使用言語・テーマを事前に収集
    const langs = new Set();
    const themes = new Set();

    blocks.forEach(b => {
      langs.add(b.dataset.lang || 'plaintext');
      themes.add(b.dataset.theme || 'nord');
    });

    const highlighter = await shiki.createHighlighter({
      langs: [...langs],
      themes: [...themes],
    });

    blocks.forEach(block => {
      if (block.dataset.shikiDone) return;
      block.dataset.shikiDone = '1';

      const pre = block.querySelector('pre');
      const codeEl = block.querySelector('code');
      if (!pre || !codeEl) return;

      const code = codeEl.textContent;
      const lang = block.dataset.lang || 'plaintext';
      const theme = block.dataset.theme || 'nord';
      const showLines =
        block.dataset.lineNumbers === '1' ||
        block.dataset.lineNumbers === 'true';

      let html;
      try {
        html = highlighter.codeToHtml(code, {
          lang,
          theme,
          transformers: showLines ? [lineNumberTransformer()] : []
        });
      } catch {
        // NOTE: 言語未対応時はプレーン表示にフォールバック
        return;
      }

      pre.outerHTML = html;
    });
  }

  function lineNumberTransformer() {
    let line = 0;
    return {
      line(node) {
        line++;
        node.children.unshift({
          type: 'element',
          tagName: 'span',
          properties: {
            className: ['line-number'],
            'aria-hidden': 'true'
          },
          children: [{ type: 'text', value: String(line) }]
        });
      }
    };
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initShiki);
  } else {
    initShiki();
  }
}
</script>

今回はチャッピーさんことChatGPTでヴァイブコーディングにゃー

人物のアイコン素材 その5

ヴァイブコーディングという名の開発丸投げだろう

基本方針はLazyBlocks内完結、二重ローディング防止、Shikiが動かない場合のフォールバック処理、その他SEO対策にゃー

人物のアイコン素材 その5

自分で開発しないからって要求を詰め込むな

ポイントのひとつめ
二重ローディング防止はwp_enqueue_scriptおよびwp_enqueue_style関数を使ってやるのが定番らしいのだけどLazyBlocks内で使えないらしい
PHPのグローバル変数やdefineとかで判定しようとしたけど動作状況によってうまくいかなかったにゃー
JSで判定するのが今のところ一番安定しているにゃー

人物のアイコン素材 その5

Pro版ならそこは苦労はしなくて済むのですけどね

CSSはそんなにサイズもないしこのブロックを大量に使うようなこともないだろうから重複OKにしているにゃー

人物のアイコン素材 その5

テーマも含んでいるのでShiki本体のサイズのほうがおそらく大きいでしょうね

あとから気づいたけどFine-grained Bundleというのがあるらしい
これをやると必要な分だけローディングされるからJS本体のサイズを減らせそう

人物のアイコン素材 その5

調べておけよ!
最初から!

Fullだと6.4 MBか
まあそれほどこのブロックを使わなさそうなので今後のアップデート目標ということにしておくか

人物のアイコン素材 その5

めちゃくちゃでかいじゃあないか!

一応ブラウザで確認したけど6.4 MBダウンロードはしていなかったにゃー
元々サーバサイトレンダリング向けに開発されているぽいのでクライアント側で動かすのはできないことはないけどあまりよくないのかも
だから<pre><code>のouterHTMLに注入する動作になっているにゃー

人物のアイコン素材 その5

無理やり感はありますが公式もCDN経由の紹介をしているので想定範囲ないですかね

minifyされているのでよくわからんが多分必要なものだけダウンロードするようになっているぽい

人物のアイコン素材 その5

まあいいでしょう
まずは動くのが重要ですからね

ポイントのふたつめ
JS実行できなかった場合、とりあえずハイライトしないけどそれらしく出力するようにした点にゃー
実行前が<pre>、実行後は<pre class=”shiki”>になる点がポイントにゃー

人物のアイコン素材 その5

CLS対策も含んでいるのですね

うむ
やたら面倒だったにゃー
テーマCSSとかの競合とかもあるのでめんどい
font-familyを統一しておくといい感じになったにゃー

人物のアイコン素材 その5

なるほど

aria-labelとrole=”region”とかやるとアクセシビリティがあがるらしい
この辺りはよくわかってないにゃー

人物のアイコン素材 その5

行番号はカスタマイズしてますよね

うむ
これはチャッピーさんにお願いしたら一発で出来上がったにゃー
流石にゃー

人物のアイコン素材 その5

この辺りはShikiの高度なカスタマイズの恩恵もありますね

実はHighlight APIとShikiを組み合わせたshiki-highlight-apiというヤツも見つけていたのだけど
こういうやつを使って前回失敗したので今回は見送ったのにゃー
もしかしたらこっちは言語によらずちゃんと動くのかも

人物のアイコン素材 その5

書かれていない制限に引っかかると厄介ですからね
まあ妥当な判断か

そんなところかにゃー
Shikiを使ってテーマも対応言語も増えて満足にゃー

人物のアイコン素材 その5

ほとんどなにもしてないけどな

何を言っている!
ShikiみたいなヤツはWordPressの思想と合わない、だとか言ってくるチャッピーさんをなだめたり
二重ローディングは最初PHPバージョンだったけど全然動かなくてJSバージョンもなかなか安定しなくて
それでも自信満々なコードを提示してくるチャッピーさんにダメ出ししたり
なかなか大変だったのにゃー

人物のアイコン素材 その5

なんというか
チャッピーさん、お疲れ様です。

しかし成果は良かったので過去のコードとかチャッピーさんを使って改善してみるかな

人物のアイコン素材 その5

まだ酷使する気か!?

それでは今回はこの辺で
Aloha

人物のアイコン素材 その5

何の脈絡もなくハワイ語!?

,