- WordPressのGutenbergエディタ用プラグインで、貼り付け時にLaTeXがMathMLに変換されない不具合が発生した。
- コンソールログを確認すると
context-unresolvedが原因で変換処理が呼ばれていなかった。 core/paragraphのattributes.contentが通常の文字列ではなく特殊なobjectになることがあり、型判定で処理が弾かれていた。- テストで失敗を確認してから修正する手順を踏み、ログで実態を把握することが問題解決を早めると示した。
1. 途中で変換処理が止まっている?
WordPress の Gutenberg に Markdown + LaTeX を貼り付けると MathML に変換するプラグインを作っています1。
コードを貼り付けると 途中から LaTeX が変換されないことに気づきました。
貼り付け後の出力を見ると、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/paragraph の attributes.content の型判定でした。
2. なぜ content が object になるのか
Gutenberg の core/paragraph は、通常 attributes.content が string です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 を起点に貼り付けると、実機では content が null ではなく特殊な 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 } が出ました。before と after がともに 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.content も8、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.content が string である保証はありません。
空 paragraph 起点の paste では null や特殊 object になることがあります。pasteHandler 後の block tree も同様で、typeof === 'string' だけで判定すると後処理変換は無効化されます。
3.1. ログで最初に確認すべき項目
Gutenberg プラグインで同種の問題に当たった場合、以下を実機で確認すると原因が絞れます。
reason(なぜ context が解決できなかったか)contentType(attributes.contentの実際の型)blockName(どのブロックで止まっているか)pasteHandler後 block tree の attribute shape(constructorNameやownPropertyNames)
実機でしか見えない block shape の差異があります。
モックで再現できないと感じたら、ログを取りながら進めるのが結果的に早いです。
- MathML(Mathematical Markup Language)は数式を HTML の中で表現するための W3C 標準マークアップ言語です。ブラウザのネイティブレンダリングで数式を表示できるため、MathJax などの JavaScript ライブラリに依存しない表示が可能です。 – MathML – MDN
- display 数式は
$$...$$や\[...\]で囲まれブロック表示される数式で、inline 数式は$...$で囲まれ文章中にインラインで配置されます。今回のプラグインはlatex-segments.js内のtokenizeLatexSegments()でこの両形式を判別しています。 wp.data.select('core/block-editor')は Gutenberg の Redux ライクなデータストアにアクセスする API です。getSelectionStart()・getSelectionEnd()はカーソル位置を{ clientId, attributeKey, offset }の形で返します。公式ドキュメント: – core/block-editor データリファレンス – Block Editor Handbook- Chrome DevTools のコンソールでは、オブジェクトは参照として記録されます。
console.log(obj)で出力した時点では中身が表示されていても、後から展開すると「記録時点」ではなく「展開時点」のスナップショットが見えることがあります。デバッグ時はJSON.stringify(obj)や個別プロパティのログ出力で、値を確定させることが確実です。 - Gutenberg の RichText コンポーネントは内部で
@wordpress/rich-textパッケージのRichTextValueオブジェクトを使いますが、block の属性として保存・取得される値は通常 HTML 文字列です。ただしエディタのインメモリ状態では、バージョンや状況によって内部オブジェクト形式になることがあります。 – @wordpress/rich-text – Block Editor Handbook - Gutenberg のブロックエディタは WordPress の更新とともに内部実装が変わります。同じ
core/paragraphでも WordPress のバージョンや環境によってattributes.contentの形状が異なる場合があります。ユニットテストのモックでは再現しにくい差異です。 - このプラグインは
node:testとnode:assert/strictを使っています。node:testは Node.js 18 から組み込みで利用可能になり、Node.js 20 で安定版となったビルトインのテストランナーです。Jest などの外部パッケージなしにnpm testでテストを実行できます。 – Test runner – Node.js Documentation pasteHandlerは@wordpress/blocksパッケージが提供する関数で、クリップボードのテキストや HTML を受け取り、Gutenberg のブロック配列に変換します。Markdown テキストを貼り付けると、見出しや段落などのブロックに自動変換されます。 – @wordpress/blocks – Block Editor HandbookconstructorName: "j"はビルドツールによるミニファイの結果です。本来のクラス名が短縮されているため、外部プラグインから型を特定しにくくなっています。ownPropertyNames: ["originalHTML"]が見えることから、これは Gutenberg 内部の RichText 関連オブジェクトと推測できます。