【WordPressプラグイン】
貼り付けで LaTeX が
変換されないデバッグをした

  • WordPressのGutenbergエディタ用プラグインで、貼り付け時にLaTeXがMathMLに変換されない不具合が発生した。
  • コンソールログを確認するとcontext-unresolvedが原因で変換処理が呼ばれていなかった。
  • core/paragraphattributes.contentが通常の文字列ではなく特殊なobjectになることがあり、型判定で処理が弾かれていた。
  • テストで失敗を確認してから修正する手順を踏み、ログで実態を把握することが問題解決を早めると示した。

関連記事

1. 途中で変換処理が止まっている?

WordPress の Gutenberg に Markdown + LaTeX を貼り付けると MathML に変換するプラグインを作っています1
コードを貼り付けると 途中から LaTeX が変換されないことに気づきました。

context-unresolved で処理が止まっていた 貼り付け時 select(‘core/block-editor’) 呼び出し paste context 解決 (カーソル位置のblock取得) ⚠ 文脈解決に失敗 変換処理が呼ばれない inline / display 数式 どちらも raw のまま残る コンソールログ skip: context-unresolved reason: block-content-unavailable blockName: core/paragraph contentType: object selection は壊れていない 失敗箇所 → attributes.content の型判定 ログ確認で「処理未呼び出し」と確定 推測で修正に向かわない

貼り付け後の出力を見ると、display 数式も inline 数式も raw のまま残っていました2
最初は「途中の inline だけ失敗している」と見えたのですが、display 数式まで残っているなら話が違います。

display 数式が残っている = プラグインの変換処理が呼ばれていない可能性が高い

変換ロジックを疑う前に、まず「処理が呼ばれているか」を確認する必要がありました。

1.1. context-unresolved で処理が止まっていた

コンソールには、 skip:context-unresolved が出ていました。

プラグインは貼り付け時に select('core/block-editor') でカーソル位置の block 情報を取得し3、paste context を解決してから変換を走らせます。
その文脈解決が失敗していたわけです。

ただし、ログは最初 Object 表示のままで、中身が見えません4
content が空なのでは」と推測で修正に向かうのではなく、実機でログを展開して確認しました。

返ってきた事実がこれです。

reason: block-content-unavailable
blockName: core/paragraph
contentType: objectCode language: HTTP (http)

selection 自体は壊れていない。
失敗しているのは core/paragraphattributes.content の型判定でした。

2. なぜ contentobject になるのか

Gutenberg の core/paragraph は、通常 attributes.contentstring です5
プラグインの insertion-context.js はそれを前提に書いていました。

function normalizeBlockContent(block) {
  if (typeof block.attributes.content === 'string') {
    return block.attributes.content;
  }
  // null や undefined は空文字に変換
  if (block.name === 'core/paragraph' &&
      block.attributes.content === null) {
    return '';
  }
  return undefined; // ← ここで undefined が返り、context-unresolved になる
}Code language: JavaScript (javascript)

空の paragraph を起点に貼り付けると、実機では contentnull ではなく特殊な object になることがあります6
typeof'object' を返すため、上の関数は undefined を返してしまいます。

テストはこう書きました7

// insertion-context_test.js より
test('resolveParagraphPasteContext treats paragraph with object content at offset zero as empty', () => {
  const selectors = {
    getSelectionStart: () => ({ clientId: 'block-7', attributeKey: 'content', offset: 0 }),
    getSelectionEnd:   () => ({ clientId: 'block-7', attributeKey: 'content', offset: 0 }),
    getBlock: () => ({
      name: 'core/paragraph',
      attributes: { content: {}, dropCap: false },
    }),
    getBlockRootClientId: () => '',
    getBlockIndex: () => 0,
  };

  // 空 object は空 paragraph として扱われるべき
  const result = resolveParagraphPasteContext(createSelectFn(selectors));
  assert.equal(result.isEmptyParagraph, true);
});Code language: JavaScript (javascript)

fail を確認してから修正し、isEmptyParagraphContentObject() で空 object を空段落として吸収するようにしました。
これで context-unresolved は解消しました。

2.1. Markdown 経路後の inline LaTeX が動いていない

context は解決されたのに、次は inline-convert:markdown-path { before: 0, after: 0 } が出ました。
beforeafter がともに 0 というのは、トークナイザが LaTeX を検出できていないか、後処理が弾いているかのどちらかです。

追加したログがこちらです。

constructorName: "j"
ownPropertyNames: ["originalHTML"]
String(value): "見出し\n\n$E=mc^2$\n\n..."
JSON.stringify(value): "見出し\n\n$E=mc^2$\n..."Code language: JavaScript (javascript)

pasteHandler が返す block tree の attributes.content8、plain string ではなく Gutenberg 独自の特殊 object でした。
String()JSON.stringify() では本文を取り出せますが、typeof === 'string' ではじくと変換ロジックには一切渡りません9

3. Gutenberg の block attributes は plain object とは限らない

「途中の inline だけ壊れている」という最初の印象は間違いでした。
やはりログを出してはじめて「処理が呼ばれていない」と確定できます。

contentType: object というログが出たら、content: {} のテストケースを書いて fail を確認してから修正します。
推測で型チェックを緩めると、想定外の条件を取り逃がします。

select('core/block-editor') が返す paragraph の attributes.contentstring である保証はありません。
空 paragraph 起点の paste では null や特殊 object になることがあります。
pasteHandler 後の block tree も同様で、typeof === 'string' だけで判定すると後処理変換は無効化されます。

3.1. ログで最初に確認すべき項目

Gutenberg プラグインで同種の問題に当たった場合、以下を実機で確認すると原因が絞れます。

  • reason(なぜ context が解決できなかったか)
  • contentTypeattributes.content の実際の型)
  • blockName(どのブロックで止まっているか)
  • pasteHandler 後 block tree の attribute shape(constructorNameownPropertyNames

実機でしか見えない block shape の差異があります。
モックで再現できないと感じたら、ログを取りながら進めるのが結果的に早いです。

  1. MathML(Mathematical Markup Language)は数式を HTML の中で表現するための W3C 標準マークアップ言語です。ブラウザのネイティブレンダリングで数式を表示できるため、MathJax などの JavaScript ライブラリに依存しない表示が可能です。 – MathML – MDN
  2. display 数式は $$...$$\[...\] で囲まれブロック表示される数式で、inline 数式は $...$ で囲まれ文章中にインラインで配置されます。今回のプラグインは latex-segments.js 内の tokenizeLatexSegments() でこの両形式を判別しています。
  3. wp.data.select('core/block-editor') は Gutenberg の Redux ライクなデータストアにアクセスする API です。 getSelectionStart()getSelectionEnd() はカーソル位置を { clientId, attributeKey, offset } の形で返します。公式ドキュメント: – core/block-editor データリファレンス – Block Editor Handbook
  4. Chrome DevTools のコンソールでは、オブジェクトは参照として記録されます。console.log(obj) で出力した時点では中身が表示されていても、後から展開すると「記録時点」ではなく「展開時点」のスナップショットが見えることがあります。デバッグ時は JSON.stringify(obj) や個別プロパティのログ出力で、値を確定させることが確実です。
  5. Gutenberg の RichText コンポーネントは内部で @wordpress/rich-text パッケージの RichTextValue オブジェクトを使いますが、block の属性として保存・取得される値は通常 HTML 文字列です。ただしエディタのインメモリ状態では、バージョンや状況によって内部オブジェクト形式になることがあります。 – @wordpress/rich-text – Block Editor Handbook
  6. Gutenberg のブロックエディタは WordPress の更新とともに内部実装が変わります。同じ core/paragraph でも WordPress のバージョンや環境によって attributes.content の形状が異なる場合があります。ユニットテストのモックでは再現しにくい差異です。
  7. このプラグインは node:testnode:assert/strict を使っています。node:test は Node.js 18 から組み込みで利用可能になり、Node.js 20 で安定版となったビルトインのテストランナーです。Jest などの外部パッケージなしに npm test でテストを実行できます。 – Test runner – Node.js Documentation
  8. pasteHandler@wordpress/blocks パッケージが提供する関数で、クリップボードのテキストや HTML を受け取り、Gutenberg のブロック配列に変換します。Markdown テキストを貼り付けると、見出しや段落などのブロックに自動変換されます。 – @wordpress/blocks – Block Editor Handbook
  9. constructorName: "j" はビルドツールによるミニファイの結果です。本来のクラス名が短縮されているため、外部プラグインから型を特定しにくくなっています。ownPropertyNames: ["originalHTML"] が見えることから、これは Gutenberg 内部の RichText 関連オブジェクトと推測できます。