WordPress プラグインで GitHub Flavored Markdown(GFM)をレンダリングする際に、Rust のソースコードが編集画面と投稿画面でシンタックスハイライトされない問題が発生していました。

症状

  • Gutenberg エディタ: Rust コードが最初はハイライトされない
  • 投稿画面(フロントエンド): Rust コードがハイライトされない
  • 管理画面プレビュー: 同様にハイライトされない
  • コンソール出力: Inferred as Rust language ではなく C++ や他の言語として誤検出

原因

複数の層で言語リスト管理が不統一だったことが原因でした。

  1. エディタ側(blocks/markdown/edit.js): Shiki の言語リストに rust が含まれていなかった
  2. 管理画面(assets/js/gfmr-core.js): Shiki ハイライター設定に rust がなかった
  3. 言語検出ロジック(assets/js/gfmr-language-detection.js): Rust 固有のパターン(fn, let mut, use std:: など)が定義されていなかった
  4. 再レンダリング制御(assets/js/gfmr-main.js): SSR で別の言語でハイライト済みの場合、言語チェックなしでテーマ一致だけで再レンダリングをスキップしていた

直し方

1. エディタ側の修正(blocks/markdown/edit.js)

// 修正前
langs: ['javascript', 'typescript', 'python', 'ruby', 'go', 'bash', 'css', 'html', 'json', 'yaml', 'xml', 'sql', 'php', 'java', 'csharp', 'cpp']

// 修正後
langs: ['javascript', 'typescript', 'python', 'ruby', 'go', 'rust', 'bash', 'css', 'html', 'json', 'yaml', 'xml', 'sql', 'php', 'java', 'csharp', 'cpp']

2. 管理画面の修正(assets/js/gfmr-core.js)

// 修正前
langs: ['javascript', 'typescript', 'python', 'php', 'html', 'css']

// 修正後
langs: ['javascript', 'typescript', 'python', 'php', 'html', 'css', 'rust']

3. 言語検出パターンの追加(assets/js/gfmr-language-detection.js)

Rust 固有のパターンを Java / C++ 検出の前に追加します。

// Rust patterns (check before Java/C++)
if (
  /\b(fn\s+\w+|let\s+mut|impl\s+|pub\s+fn|trait\s+|match\s+\{|&mut\s|&self|Option<|Result<|\.unwrap\(\)|\.expect\(|use\s+std::|use\s+\w+::\{|mod\s+\w+|#\[derive)/i.test(text)
) {
  console.log('[DEBUG] Inferred as Rust language');
  return 'rust';
}

検出パターン:

  • fn — 関数定義
  • let mut — 可変変数宣言
  • use std:: — 標準ライブラリインポート
  • impl — 実装ブロック
  • pub fn — 公開関数
  • trait — トレイト定義
  • match { — パターンマッチング
  • &mut, &self — 参照
  • Option<, Result< — 一般的な型
  • .unwrap(), .expect( — よく使われるメソッド
  • #[derive — derive マクロ

4. C++ 検出パターンの厳密化(assets/js/gfmr-language-detection.js)

// 修正前
} else if (/#include|std::/.test(text)) {

// 修正後
} else if (/#include/.test(text) || /std::(cout|cin|endl|string|vector|map|set)/.test(text)) {

Rust の use std:: と C++ の std::cout を区別します。

5. 再レンダリングロジックの修正(assets/js/gfmr-main.js)

SSR で既にハイライトされている場合、言語も確認してから再レンダリングをスキップします。

// 修正前
if (savedTheme && savedTheme === currentTheme) {
  console.log(`Theme match, skipping re-render for: ${language}`);
  return;
}

// 修正後
const savedLanguage = codeElement.getAttribute('data-gfmr-language') ||
  codeElement.className.match(/language-(\w+)/)?.[1] || '';

if (savedTheme && savedTheme === currentTheme && savedLanguage === language) {
  console.log(`Theme and language match, skipping re-render for: ${language}`);
  return;
}

学び

  1. マルチレイヤーシステムでは一貫性が重要: 言語リストが複数箇所にハードコードされていると、保守が困難で不具合が生じやすい。
  2. 言語検出は複雑: 言語パターンは曖昧で、誤検出を避けるには優先順位が重要。Rust の std:: が C++ と混同されるなど、共通パターンを避ける必要がある。
  3. SSR とクライアントの同期: 既存コンテンツが SSR でハイライト済みの場合、言語検出の信頼度によっては再レンダリング判定が必要。
  4. テスト駆動の重要性: 既存テストに「Rust from fn keyword」「Rust from let mut」が記載されていたが実装がなかった。テストが先行していたことで実装漏れに気付きやすかった。

確認手順

  1. npm run test:quick で全テストが成功することを確認
  2. npm run lint で全 Lint チェックが成功することを確認
  3. Gutenberg エディタでブロック追加後、Rust コードがプレビューでハイライトされることを確認
  4. ブラウザコンソールで Inferred as Rust language が表示されることを確認
  5. 投稿画面で Rust コードが Shiki のスタイルで表示されることを確認

これは Markdown Renderer for GitHub に実装されています。