GutenbergにSVGをコピペすると、コードブロックや段落に分解されてしまうことがあります。
SVGの構造を壊さずに貼り付けたい場面は意外と多く、毎回手作業で修正するのは手間です。
そこで、SVGをペーストすると自動でカスタムHTMLブロックに変換するプラグインを作りました。
1. GutenbergにSVGをペーストすると何が起きるか
GutenbergにSVGのコードをペーストすると、ブロックエディターは貼り付け内容を自動解釈します。
このとき、SVGが段落ブロックやカスタムコードブロックに分解されることがあります。
そこで、このプラグインでは、SVGのコードをペーストしたときに自動でカスタムHTMLブロックとして挿入されるようにします。
SVG単体をペーストするとカスタムHTMLブロックが1つ生成されるだけでなく、テキストとSVGが混在した内容をペーストすると、ちゃんとテキスト部分は段落などのブロックに、SVG部分はカスタムHTMLブロックに分割して順番通り挿入されます。
1.1. なぜ壊れるのか(raw transform)
WordPressのブロックエディターには、ペーストデータを処理する経路が2つあります。
1つ目はHTML経路で、クリップボードの text/html データを使います。
Gutenbergが提供するraw transformという仕組みが <svg> 要素を検出してカスタムHTMLブロックに変換します1。
ただし、raw transformが発火するのは「HTMLノードとして解釈されたトップレベル要素」だけです。
2つ目はプレーンテキスト経路で、text/plain データを使います。
XML宣言付きのSVGはこちらに落ちやすく、raw transformは発火しないため、SVGが段落やコードブロックに分解されます。
「raw transformだけ実装すれば十分」という判断が、XML宣言付きSVGを取りこぼす原因でした。
2. 2経路を最小介入で補う設計
既存のraw transformはそのまま維持しています。
追加したのは、プレーンテキスト経路向けの処理だけです。
ペーストイベントを監視し、クリップボードの内容に <svg> が含まれていれば、SVG部分を抽出してカスタムHTMLブロックとして挿入します。
前後にテキストがあれば、そのテキストも既存の pasteHandler2 でブロック化してから連結挿入します。
SVG抽出処理は svg-plain-text.js に分離していて、XML宣言やBOM3を除去した上で <svg>...</svg> ブロックを検出します。
raw transformを登録する際には、blocks.registerBlockTypeフィルター4を使っています。
このフックはすべてのブロックの登録時に発火するため、core/html以外のブロックはそのまま素通りさせています。
PHPファイル側ではenqueue_block_editor_assetsアクション5を使って、ビルド済みのJavaScriptをエディターにのみ読み込んでいます。
2.1. テスト先行で仕様を固めた
実装前にユニットテストを書いて、期待する挙動を先に固定しました6。
実運用で渡ってくるデータ(XML宣言、改行、コメント)をテストケースにしておくことで、修正のたびに同じ確認が自動で走ります。
「実運用フォーマットでテストを書く」という習慣が、今回のような取りこぼしを早期に発見できる体制につながりました。
3. ハマったポイントと対処
3.1. iframeキャンバス問題
Gutenbergの編集エリアはiframeで実装されています7。
親の document にペーストイベントを登録しても、iframe内のペーストには発火しません。
iframe内の contentDocument にも個別にリスナーを登録する必要がありました。
Gutenbergはエディターの初期化後にiframeを動的に挿入することもあります。MutationObserver8で新しいiframeの追加を監視し、挿入のたびにリスナーを追加することで対応しました。
3.2. 複数SVG混在の取りこぼし
また、最初の実装では「前部分・SVG・後部分」の3分割しか想定していなかったため、複数のSVGが混在する場合に後続のSVGが欠落していました。extractSvgSegments を導入してテキストとSVGが交互に並ぶ構造に対応しています。
3.3. 発火範囲を絞り込んだ——コンテキスト制御の追加
リリース後、想定外の場所でも変換が走るという問題が出ました。
文中にインラインで書いた <svg> や、Markdownのコードフェンス(```)内に書いた説明用のSVGコードまで変換対象になっていました。
また、タイトル欄など本文以外の入力欄でも発火していました。
「<svg> が含まれていれば変換する」という判定だけでは範囲が広すぎました。
対応は3層に分けています。
- まず、
handlePasteの冒頭に本文領域の判定を追加しました。
ペーストが発生した要素が本文ブロックの編集エリアでなければ即returnします。タイトル欄やtextareaはここで弾かれます。 - 次に、
extractSvgSegmentsにコードフェンス除外の判定を加えました。```や~~~で囲まれた範囲内の<svg>は抽出対象から外します。説明用のコードをそのまま貼り付けても変換が走らなくなります。 - 最後に、行内に埋め込まれたインラインSVGも除外しました。
前後に非SVGテキストが同じ行に存在する場合は、そのSVGを独立した変換対象と見なしません。
領域判定は paste-context.js に分離してユニットテストを追加しています。
「本文領域ならtrue、タイトル欄・textarea・対象要素が取得できない場合はfalse」という境界をテストで明文化しました。
- raw transformはGutenbergのBlock Transforms APIが提供する変換タイプの一つで、
selectorに指定したHTMLタグにマッチするDOMノードを対象に発火します。priorityの値が低いほど優先度が高く、デフォルト値は10です。 – Block Transforms | Block Editor Handbook pasteHandlerは@wordpress/blocksパッケージが提供する関数で、HTMLやプレーンテキストをブロックの配列に変換します。Gutenberg内部のペースト処理でも使われています。 – @wordpress/blocks | Block Editor Handbook- BOMはByte Order Markの略で、UTF-8ファイルの先頭に付く3バイトの符号(
\uFEFF)です。Windowsの一部テキストエディタが自動的に付加します。ブラウザのclipboardDataに混入することがあり、除去しないとSVGの検出が失敗する場合があります。 blocks.registerBlockTypeはGutenbergのJavaScript側フックで、@wordpress/hooksパッケージのaddFilter関数を通じて登録します。PHPのadd_filter()に相当し、ブロック名と設定オブジェクトを受け取って変更後の設定を返します。 – Block Filters | Block Editor Handbookenqueue_block_editor_assetsはエディター専用のスクリプト・スタイルを読み込むためのWordPressアクションフックです。フロントエンドには読み込まれません。 – enqueue_block_editor_assets | Developer.WordPress.org@wordpress/scriptsはGutenbergチームが提供するゼロコンフィグのビルド・テストツールで、内部でJestを使います。npx wp-scripts test-unit-jsコマンドでユニットテストを実行でき、@wordpress/*パッケージのモックも同梱されています。 – Testing Overview | Block Editor Handbook- GutenbergのiframeキャンバスはWordPress 5.8のテンプレートエディターで初めて採用されました。管理画面のCSSがコンテンツに干渉しないよう分離するのが主な目的です。投稿エディターへのiframe完全移行はWordPress 7.0で計画されています。 – Blocks in an iframed (template) editor – Make WordPress Core
MutationObserverはDOMの変更(要素の追加・削除・属性変化など)を非同期で監視するブラウザ標準APIです。observe()で監視対象と監視オプション(childList、subtreeなど)を指定します。 – MutationObserver | MDN Web Docs