貼り付け時のリストから不正子ノードを除去して番号ずれを防止
Lexxy エディタでは、HelpScout などからコピーした番号付きリストを貼り付けた際に、余分な先頭の "1." が生成されて番号がずれる問題を修正しました。 stray <br> やテキストノードが <ol>/<ul> の直接子として残ることが原因です。今回の変更により、貼り付け HTML のクリーンアップが標準化され、リストの正しい番号付けが保証されます。
背景
貼り付けた HTML に <ol> の直下に <br> や空白テキストが混在すると、Lexical の $generateNodesFromDOM がそれらを空の <li value="1"><br></li> に変換し、余分なリスト項目が生成されます。これによりユーザーは本来の「1.」が二重に表示され、文書の可読性が低下します。問題は HelpScout など外部ツールが生成する HTML の構造に起因し、エディタ側での対策が必要でした。
技術的な変更
Contents#formatPastedDOM は従来の内部ヘルパー (#unwrapPlaceholderAnchors、#stripTableCellColorStyles) の呼び出しをやめ、PastedContentFormatter に処理を委譲するよう変更されました。新しいインスタンスを生成し format() を実行するだけで、貼り付け DOM の正規化が完結します。
@@
- this.#unwrapPlaceholderAnchors(doc)
- this.#stripTableCellColorStyles(doc)
+ new PastedContentFormatter(doc).format()
PastedContentFormatter は 3 つのプライベートメソッドを持ち、順にプレースホルダーアンカーの展開、テーブルセルのインラインカラー除去、リストの stray 子ノード除去を行います。各メソッドは対象要素を走査し、条件に合致しないノードを安全に削除します。特に #stripStrayListChildren は <ol>/<ul> の子ノードを走査し、<li> 以外をすべて removeChild で除去します。
@@
- #stripStrayListChildren() {
- for (const list of this.doc.querySelectorAll("ol, ul")) {
- for (const child of Array.from(list.childNodes)) {
- if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "LI") continue
- list.removeChild(child)
- }
- }
- }
+ #stripStrayListChildren() {
+ for (const list of this.doc.querySelectorAll("ol, ul")) {
+ for (const child of Array.from(list.childNodes)) {
+ if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "LI") continue
+ list.removeChild(child)
+ }
+ }
+ }
追加されたテスト clean_invalid_pasted_list_html.test.js では、<ol> と <ul> に対し stray <br> とテキストノードが混在した HTML を貼り付け、結果のエディタ HTML が余分な <li> を含まないことを検証しています。テストは失敗していた main ブランチの挙動と、修正後の期待結果を明示的に比較しています。
@@
- await assertEditorHtml(
- editor,
- '<ol><li value="1">First item</li><li value="2">Second item</li><li value="3">Third item</li></ol>',
- )
+ await assertEditorHtml(
+ editor,
+ '<ol><li value="1">First item</li><li value="2">Second item</li><li value="3">Third item</li></ol>',
+ )
設計判断
処理の集中化 を目的に新たに PastedContentFormatter クラスを導入した点が本変更の核となります。従来は Contents に散在していたクリーニングロジックを一箇所に集約することで、将来的な拡張やテストの追加が容易になる設計です。クラス化によりプライベートメソッドがカプセル化され、外部から直接触れられない安全な実装となります。
また、既存機能との互換性 を保つために元のアンカー展開やテーブルカラー除去ロジックをそのまま移植し、#stripStrayListChildren だけを新規に加えました。これにより、現行エディタの挙動は変わらず、リスト番号ずれの問題のみが解消されます。変更範囲が限定的であることは、リグレッションリスクの低減につながります。
まとめ
今回の PR は、貼り付け時に生成される不正なリスト子ノードを除去することで、番号ずれを防止する重要なバグ修正です。処理を PastedContentFormatter に統合したことでコードの可読性と保守性が向上し、テストで期待通りの動作が確認されています。エディタ利用者はヘルプデスクツールからのコピー貼り付けでも正しいリスト番号を得られるようになりました。