1. ブロックエディタの構造と課題
WordPress のブロックエディタは、各要素が「ブロック」という単位でカプセル化されています。
- データの二重構造: エディタ上では React のステートとして管理されていますが、保存時には HTML コメント(“ など)を含む独自の形式でデータベースに格納されます。
- HTML エンティティ: セキュリティとデータ整合性のため、
&や<は&や<として内部処理されます。
従来のプラグインは HTML 文字列を力技で書き換えるものが多く、ブロックの構造を破壊したり、不要なタグを残してしまうリスクがありました。
2. ゼロ・タグ レンダリング (Zero-tag Rendering)
本プラグインの最大の特徴は、エディタの HTML を一切汚さない CSS Custom Highlight API の採用です。
- 従来の手法: 検索語を
<mark>タグで囲むため、DOM 構造が変わり、リアクティブなエディタの動作を阻害する。 - 本プラグインの手法:
Rangeオブジェクトを使ってテキストの位置情報だけをブラウザに伝え、仮想的なレイヤーで色を塗ります。これにより、ソースコードには 1 文字の変更も加えずに、高速なハイライトを実現しています。
3. リアルタイム・プレシジョン (Real-time Precision)
ユーザーがタイピングするたびに、エディタ全体のテキストをスキャンします。これを支えるのが以下の設計です。
TreeWalker による精密スキャン
エディタ内の膨大な HTML 要素の中から、テキストノードだけを効率的に抽出する TreeWalker を使用しています。
- ツールバーや設定パネルなどの「編集対象外」のエリアを排除し、純粋なコンテンツのみを対象にします。
- 正規表現の保護:
&などの HTML エンティティを 1 つの「塊」として認識する独自の走査ロジックを実装。これにより、正規表現置換で&を書き換えてもamp;が残ってしまうようなデータ破損を防いでいます。
4. プリシジョン・ナビゲーション (Precision Navigation)
見つけたマッチ箇所へ、迷わず辿り着くための設計です。
- スマート・スクロール:
getBoundingClientRect()を活用し、マッチした箇所が常に画面の中央に来るように、キャンバスのスクロール位置を計算・同期します。 - 双方向インタラクション: サイドバーからのナビゲーションだけでなく、エディタ上のハイライトを直接クリックすることで、その箇所を「現在のターゲット」として選択できる直感的な操作感を実現しました。
5. インテリジェント・データ更新
置換を実行する際、プラグインは単なる「画面上の書き換え」ではなく、WordPress 内部の データストア (core/block-editor) と直接対話します。
- 階層構造の再帰処理: ネストされた「グループブロック」や「カラムブロック」の奥深くにある文字列まで、再帰的な探索(Deep Search)で確実に捕捉します。
- エンティティ・セーフ置換: 置換後の文字列は
encodeForStorageを通じて適切にエンティティ化され、WordPress の標準仕様に則った形でデータストアへ書き戻されます。
まとめ:モダンなエディタに、モダンなツールを
LiveDraft Search & Replace は、WordPress エディタを「単なるテキストエリア」としてではなく、高度なオブジェクト構造として捉えることで完成しました。
「壊さない」「汚さない」「迷わせない」。 この 3 つの設計原則が、CSS Custom Highlight API や React の高度なステート管理と融合し、次世代の編集体験を提供します。
整理のポイント
- DOM = 「今、画面で見えている偽物(表示用のコピー)」
- データストア = 「ブラウザのメモリにある本物(保存される直前のデータ)」
- データベース = 「サーバーにある保存版(お店を閉めた後の記録)」
あなたが「DOMのスキャン」と「データストアの更新」を使い分けているのは、「画面で見た通りに探し、データの根本を安全に直す」ためです。これこそが、WordPressというシステムの上で正しく動くための唯一の方法なんです。
まとめ
- 検索: DOM(表示上の文字)を見て、ユーザーの期待通りに場所を特定する。
- 表示: DOM(Range)に色を塗って、ユーザーに場所を教える。
- 置換: 特定した場所を「データストア」への命令に変換して、保存可能なデータとして書き換える。
あなたのコードの役割分担
- TreeWalker: 「住所(ノード)」を特定する。
- Range + CSS Highlight: その「住所」に色を塗る。
- getBoundingClientRect: その「住所」が今、画面のどこにあるかを計算して、スクロール(ジャンプ)させる。
あなたのコードで行われている「検索のステップ」を、正しい順番で整理します。
検索の3ステップ
ステップ1:TreeWalkerで「器(うつわ)」を集める
まず、TreeWalkerを起動します。このときTreeWalkerに教えるのは「検索ワード」ではなく、「どんな種類のノードを探してほしいか」という条件だけです。
- あなたの設定:
NodeFilter.SHOW_TEXT(テキストノードだけを見つけてきて!)
TreeWalkerは、エディター内のテキストノードを一つずつ順番に差し出してくれるだけです。
ステップ2:差し出されたテキストを「正規表現」で調べる
TreeWalkerが持ってきたテキストノード(例:"Hello World")に対して、初めて検索ワード(正規表現)をぶつけます。
JavaScript
// TreeWalkerが持ってきたノード 'n' に対して、
// ここで初めて検索ワード 'regex' を使ってチェックする
const match = regex.exec(n.nodeValue);
ステップ3:マッチした場所を記録する
もし検索ワードが見つかったら、そのノードと、その中の開始位置・終了位置を記録します。これが「ヒットした場所」の特定になります。
なぜTreeWalkerに直接渡さないのか?
TreeWalker自体には「文字を探す」という高度な機能は備わっていません。
- TreeWalkerの仕事: 「次、3番目の段落のテキストノードです」「次、4番目のリストのテキストノードです」と案内すること。
- 正規表現(RegExp)の仕事: 「その中に『WordPress』という単語はありますか?」と判定すること。
この2つを組み合わせることで、「どのブロックの、どのテキストノードの、何文字目にあるか」という極めて精緻な検索結果が得られるわけです。
正確に言うと、「検索(マッチ箇所の特定)」はDOMから行い、「置換(データの書き換え)」はデータストアに対して行うという、役割の使い分けをしています。
なぜ検索をデータストアではなくDOMから行うのか、その技術的な理由と仕組みを解説します。
1. 検索にDOM(TreeWalker)を使う理由
データストアにある「生データ(文字列)」だけを見て検索位置を特定するのは、実は非常に困難です。なぜなら、データストアの文字と、画面に見えている文字の間には「エディターによる加工」が入るからです。
- HTMLエンティティ: データストアでは
&は&ですが、画面(DOM)では&と表示されます。 - 非表示要素: ブロックのメタ情報や隠しタグなど、ユーザーに見えない文字がデータストアには含まれています。
ユーザーは「画面に見えている文字」を基準に検索するため、DOMを直接スキャンするのが最も正確なのです。
2. 検索位置を特定する「2つの座標」
あなたのプラグインでは、マッチした場所を以下の2種類の情報で記録しています。
- 論理的な位置 (Range):
TreeWalkerで見つけた「どのテキストノード」の「何文字目から何文字目」かという情報です。これは CSS Custom Highlight API で色を塗るために使われます。 - ブロックとの紐付け (clientId): 見つけたテキストノードから親を辿り、
.wp-blockが持つdata-block(clientId)を取得します。これは 置換を行う際、どのデータを書き換えるか特定する ために使われます。
3. 検索から置換への橋渡し
ここがこのプラグインの最もテクニカルな部分です。
- DOMで発見: 「このDOMノード(clientId: A)の5文字目にマッチした!」
- ストアで置換: 「clientId: A のブロック属性をストアから取得。その中にある文字列を置換して、ストアを更新!」
このように、「DOMで見つけた場所を、データストアの更新に翻訳する」という処理を行っています。
4. 課題:DOMとストアの「不一致」
ただし、DOMから位置を得ることには一つ弱点があります。それは、「DOM上の5文字目が、データストア(属性)内の5文字目と必ずしも一致しない」という点です。
例えば、属性内ではHTMLタグが含まれていたり、エスケープされていたりすると、文字数がズレます。
あなたのコードでは、これを解決するために localIndex(そのブロック内で何番目のマッチか) という概念を導入しています。
- DOM側で「このブロックの3番目のマッチ」だと分かれば、データストア側でも「同じロジックで検索して3番目のマッチ」を置換すれば、位置がズレても正確に置換できるという仕組みです。
まとめ
- 検索: DOM(表示上の文字)を見て、ユーザーの期待通りに場所を特定する。
- 表示: DOM(Range)に色を塗って、ユーザーに場所を教える。
- 置換: 特定した場所を「データストア」への命令に変換して、保存可能なデータとして書き換える。
この「表示(DOM)と実体(ストア)の橋渡し」がうまくいっているからこそ、Undoをしても壊れず、正確な置換ができるツールになっています。
1. データストアの基本構造
データストアは、主に以下の3つの要素で構成されています。
- State (状態): 現在のブロック、選択範囲、編集履歴(Undo/Redo)などが保存されている生データ。
- Selectors (セレクター): Stateから必要な情報だけを取り出すための関数。
- あなたのコード:
select('core/block-editor').getBlocks()など。
- あなたのコード:
- Actions / Dispatch (アクション): Stateを更新するための命令。
- あなたのコード:
updateBlockAttributes(clientId, attributes)など。
- あなたのコード:
2. WordPressにおける階層構造
WordPressのデータストアは、役割ごとに「名前空間」という単位で分割されています。
| 名前空間 | 役割 | あなたのプラグインでの使用例 |
core/editor | エディター全体の基本設定 | ビジュアル/コードモードの判定(getEditorMode) |
core/block-editor | ブロックの配置や属性 | 全ブロックの取得、個別ブロックの文字置換 |
core/data | データストアの基盤 | useSelect や useDispatch の提供元 |
3. ブロックデータの持ち方(ツリー構造)
データストア内では、各ブロックは以下のようなオブジェクト(JSON)として管理されています。
JSON
{
"clientId": "uuid-1234",
"name": "core/paragraph",
"attributes": {
"content": "こんにちは、世界!",
"fontSize": "large"
},
"innerBlocks": []
}
- フラットではない: ブロックの中に
innerBlocks(子ブロック)が含まれるツリー構造です。 - あなたの実装の工夫: 置換処理の際に
flatten関数(allBlocks.flatMap(...))を使っているのは、このツリー構造を一旦バラバラにして、全てのテキストを漏れなくスキャンするためです。
4. DOMとデータストアの関係(ここが重要!)
あなたのプラグインが動くとき、2つの異なる世界が同期しています。
- データストア(真実のデータ):WordPressが「保存」したり「Undo」したりする際のマスターデータです。ここを書き換えないと、画面上で置換したように見えても保存されません。
- DOM(表示用のデータ):ブラウザが画面に描画している結果です。Custom Highlight APIはこの「表示用データ」の上に色を塗っています。
WordPressのデータストア内にあるブロックの属性(content など)は、基本的にHTMLエンティティ化された状態で収められています。
ただし、これは「エディターがどうデータを扱っているか」という非常に重要な部分に関わりますので、少し詳しく解説します。
1. データストア内での状態
WordPress(Gutenberg)のデータストア(core/block-editor)において、段落ブロックなどの content 属性は、ブラウザの innerHTML に近い形で保持されています。
- 特殊文字の扱い:
&→&<→<>→>"→"
- 具体例: データストア上の生データは以下のようになっています。
"content": "A & B < C > "D""
2. なぜエンティティ化されているのか?
これは「セキュリティ(XSS対策)」と「データの整合性」のためです。
- 保存時の衝突回避: WordPressは最終的にこれらを “ というコメント形式のHTMLとしてデータベースに保存します。もし属性値の中に生の
<や>が混じっていると、HTMLのタグ構造とバッティングしてデータが壊れる可能性があります。 - Reactの仕様: データストアの値を画面に反映する際、Reactはデフォルトでエスケープ処理を行います。ストア側ですでにHTMLセーフな形になっていることで、意図しないスクリプトの実行(XSS)を防いでいます。
3. 【重要】あなたのプラグインへの影響
ここが、先ほどお話しした「DOM(TreeWalker)とデータストアのズレ」の正体です。
- DOM(TreeWalker)が見ている世界: ブラウザがレンダリングした後の世界なので、
&は&(1文字) としてカウントされます。 - データストアが見ている世界:
&は&(5文字) としてカウントされます。
[!CAUTION] そのため、DOMで見つけた「5文字目から置換」という数値をそのままデータストアの
content文字列に適用すると、位置が大幅にズレて別の場所を破壊してしまうのです。
4. あなたのコードでの解決策(再確認)
あなたのプラグインで updateBlockAttributes を使って置換する際、単なる文字数(インデックス)指定ではなく、以下のような工夫が必要になります(あるいは既に実装されています)。
- RegExpによる再検索: データストアの文字列に対しても同じ正規表現をかけ、そのマッチした部分を置換する。
- サニタイズを考慮: 置換後の文字列も、WordPressの作法に合わせて適切にエンティティ化(またはデコード)された状態でストアに渡す。
まとめ
データストア内では 特殊文字はエスケープ(エンティティ化)されています。 このため、DOMで見つけた「見た目の文字」をデータストアの「生の文字」に反映させるには、この「エンティティ化による文字数の違い」を常に意識する必要があります。

コメント
テスト