blockquote内へのペーストで分割が発生するバグを修正

basecamp/lexxy

QuoteNode 内にHTMLをペーストすると、見出しやコードブロックなどのブロック要素がblockquoteの外側に分割されて挿入される問題を修正しました。NodeInsertersselection.insertNodes() ではなく専用の挿入パスを使うことで、ペースト先のノードが分割されなくなります。

背景

Lexicalの selection.insertNodes() は、ブロック要素を含むノード群を挿入する際に、現在のカーソル位置を起点にして受け取り側のノードを分割する挙動を持ちます。通常の段落では期待通りに動作しますが、QuoteNode 内にカーソルがある状態でヘッダー(<h1>)やコードブロック(<pre>)などのブロック要素をペーストすると、blockquoteが途中で分割され、一部のノードがquoteの外側に吐き出される問題が起きていました。

Fizzy card #4979 として報告されていたこの問題は、「引用テキストとしてのペーストが期待通りに動作しない」という形でユーザーに影響していました。

技術的な変更

insertDOM メソッドにおける挿入処理が、selection.insertNodes() の直接呼び出しから insertAtCursor() 経由に統一されました。これにより、QuoteNode 内へのペーストでは分割ではなくノードの単純な挿入が行われるようになります。

変更前:

insertDOM(doc, { tag } = {}) {
  this.#unwrapPlaceholderAnchors(doc)
  if (tag === PASTE_TAG) this.#stripTableCellColorStyles(doc)

  this.editor.update(() => {
    const selection = $getSelection()
    if (!$isRangeSelection(selection)) return

    const nodes = $generateNodesFromDOM(this.editor, doc)
    if (!this.#insertUploadNodes(nodes)) {
      selection.insertNodes(nodes)
    }
  }, { tag })
}

変更後:

insertDOM(doc, { tag } = {}) {
  this.#unwrapPlaceholderAnchors(doc)

  this.editor.update(() => {
    if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc)

    const nodes = $generateNodesFromDOM(this.editor, doc)
    if (!this.#insertUploadNodes(nodes)) {
      this.insertAtCursor(...nodes)
    }
  }, { tag })
}

変更に伴い、いくつかの重要な修正が加えられています。まず、$hasUpdateTag(PASTE_TAG) による判定がエディタの update() コールバック内に移動しています。これは tag オプションがコールバック内でのみ有効なため、より正確な判定が可能になります。また、$isRangeSelection(selection) による早期リターンが削除され、選択状態のチェックは insertAtCursor() の責務に委譲されています。

uploader.js でも同様に、forEach による個別呼び出しからスプレッド構文による一括渡しに統一されています。

変更前:

$insertUploadNodes() {
  this.nodes.forEach(this.contents.insertAtCursor)
}

変更後:

$insertUploadNodes() {
  this.contents.insertAtCursor(...this.nodes)
}

テストでは、<h1><pre><code><p> を含むHTMLをblockquote内にペーストした際、すべての要素がblockquote内に留まり、外側に <p><h1><pre> が1つも出力されないことを検証するケースが追加されました。

設計判断

selection.insertNodes() を迂回して insertAtCursor() に統一するアプローチが採用されました。Lexicalの insertNodes() はノードを分割して挿入する汎用的なAPIですが、QuoteNode のような「内容をまとめて保持する」コンテナノードとの相性が悪いケースがあります。insertAtCursor() は内部的に $getNearestNodeOfType$ensureForwardRangeSelection を活用し、挿入先のコンテキストに応じた振る舞いを実現できます(diffに $getNearestNodeOfType$ensureForwardRangeSelection の新規インポートが追加されていることからも、この方向性が読み取れます)。

また、PASTE_TAG の判定をコールバック外からコールバック内の $hasUpdateTag() に移したことで、Lexicalのupdateタグの仕組みと整合した、より正確な状態チェックが実現されています。

まとめ

この修正は、Lexicalの汎用的な insertNodes() APIの分割挙動が QuoteNode という特定のコンテキストで引き起こす問題を、挿入ロジックの統一によって解消しています。selection.insertNodes() の直接呼び出しをコードベースから排除することで、ペースト処理の経路が一本化され、コンテナノードの種類によらず一貫した挿入動作が保証されるようになりました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
d7c53fb4

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)の3部構成が明確に適用されており、模範的な構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(`言語:ファイルパス`)とPR番号のリンク記法(`#123`)が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Lexicalフレームワークの内部API(`QuoteNode`, `selection.insertNodes`等)に関する専門的な内容であり、対象読者であるエンジニアに適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のすべてのコードブロックは、提供されたDiffの内容(追加行・削除行)を正確に引用しています。ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

LexicalのAPI名(`insertNodes`, `insertAtCursor`)や定数名(`PASTE_TAG`)などの技術用語が、文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「`selection.insertNodes`の分割挙動」という問題の根本原因から、「`insertAtCursor`への統一」という解決策まで、技術的な説明が論理的かつ正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのTitle、Description、Diff内のコード変更によって裏付けられており、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#911)やIssue番号(Fizzy card #4979)がPR情報と一致しており、正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトル(Fix pasting into blockquote splitting on line breaks)の内容を的確に要約しており、主題が一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事にはPR情報に記載のないバージョン情報やリリース日程などの外部知識は含まれておらず、提供された情報に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「修正しました」「起きていました」といった時間表現が、完了したPRの内容を正しく反映しています。