コードブロックのコピー&ペースト修正:改行保持と平文ペースト強制

basecamp/lexxy

コードブロックをコピーして別の場所に貼り付けると改行が失われる問題と、コードブロックへのペーストでHTMLが混入する問題を同時に修正した。いずれも <pre> 要素の扱いとクリップボードデータの型に起因するバグである。

背景

2つの独立したバグが共存していた。一方はコードブロックからのコピー時に改行が失われる問題、もう一方はコードブロックへのペースト時にHTML形式のデータがそのまま適用される問題である。

コピー時の改行消失は、シンタックスハイライト処理の実装に起因していた。highlightCode() 内の highlightElement() 関数が <pre> 要素を <code> 要素に replaceWith() で置き換えていたため、ハイライト済みのDOM上に <pre> ラッパーが存在しなくなっていた。<code> はインライン要素であるため、ブラウザはクリップボードへのシリアライズ時に改行を含む空白文字を折り畳む。その結果、複数行のコードをコピーして貼り付けると1行に結合されてしまっていた。

ペースト時のHTML混入については、コードブロックへのペーストが未ハンドルのままLexicalのネイティブハンドラに委譲されていた。LexicalはHTMLクリップボードデータを解釈してブロックを分割する可能性があり、コードブロックの構造が壊れうる状態だった。

技術的な変更

2つの独立したバグに対し、それぞれ最小限の変更で対処している。

<pre> ラッパーの保持は、src/helpers/code_highlighting_helper.js の1行変更で解決した。replaceWith(codeElement)<pre> 要素自体を <code> で置き換えるのに対し、replaceChildren(codeElement)<pre> を残したまま子要素だけを差し替える。

変更前:

preElement.replaceWith(codeElement)

変更後:

preElement.replaceChildren(codeElement)

これにより、ハイライト後のDOMは <pre><code data-language="...">...</code></pre> の構造を維持する。<pre> のブロック要素としての性質によって、ブラウザはクリップボードシリアライズ時に改行を保持する。

コードブロックへの平文ペースト強制は、src/editor/clipboard.jspaste() メソッドと新たなプライベートメソッド #pastePlainTextIntoCodeBlock() の追加で実現した。

変更前:

if (!clipboardData || this.#isPastingIntoCodeBlock()) return false

変更後:

if (!clipboardData) return false

if (this.#isPastingIntoCodeBlock()) {
  this.#pastePlainTextIntoCodeBlock(clipboardData)
  event.preventDefault()
  return true
}

追加された #pastePlainTextIntoCodeBlock() は、クリップボードから text/plain のみを取得し、selection.insertRawText(text) でエディタに挿入する。event.preventDefault() でブラウザおよびLexicalのデフォルト処理を抑制し、HTMLデータが混入する経路を断っている。

#pastePlainTextIntoCodeBlock(clipboardData) {
  const text = clipboardData.getData("text/plain")
  if (!text) return

  this.editor.update(() => {
    const selection = $getSelection()
    if ($isRangeSelection(selection)) selection.insertRawText(text)
  }, { tag: PASTE_TAG })
}

テストは2層で追加されている。test/javascript/unit/helpers/code_highlighting_helper.test.js にユニットテストを追加し、highlightCode() 実行後に <code> の親要素が <pre> であることを検証する。また test/browser/tests/paste/code_block_copy.test.js にPlaywrightによるE2Eテストを追加し、ハイライト済みHTMLをクリップボード経由でペーストした際に改行が保持されることをエンドツーエンドで確認している。

設計判断

replaceWith から replaceChildren への変更は、既存のDOM構造の前提を修正する方向を選択している。<pre> の保持はブラウザのクリップボードシリアライズの挙動に依存した修正であり、HTML仕様における <pre> のブロック要素としての性質を明示的に活用している。テストコメントにもこの意図が記載されており、「<pre> ラッパーが空白文字を保存し、コピー&クリップボードのシリアライズで改行が生き残る」という設計の根拠が明文化されている。

コードブロックへのペーストを明示的にハンドルする判断は、Lexicalのデフォルト挙動への依存を意図的に断ち切るものだ。以前の実装では return false でイベントハンドラを終了させるだけで、実際には何も処理していなかった(Lexicalのネイティブハンドラに委譲される)。新実装では event.preventDefault()return true によってイベントを完全に引き取り、平文挿入のみを保証している。

まとめ

replaceWithreplaceChildren に変更して <pre> ラッパーを保持し、コードブロックへのペーストを平文専用ハンドラで引き取るという2点の修正により、コードブロックのコピー&ペースト動作が一貫して正しく機能するようになった。いずれも既存のブラウザ仕様とLexicalの動作特性を正確に把握した上で、最小限の変更で根本原因に対処している。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、設計判断、まとめという「総論→各論→結論」の構成が明確で、ガイドラインに準拠しています。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックのファイル名指定は正しいですが、フッターのPRリンクがガイドラインで推奨される`[#1010](URL)`形式ではなく`[PR #1010](URL)`形式になっています。

対象読者への適合性 ✓ PASS

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

DOM操作、クリップボードAPI、Lexicalフレームワークに関する専門用語が適切に使用されており、対象読者であるエンジニアに適した内容です。

パラグラフ・ライティング ⚠ WARNING

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

多くの段落でトピックセンテンスが先頭にあり読みやすいですが、「設計判断」セクションにセクション全体の要旨を述べる総論パラグラフが欠けています。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、`replaceWith`から`replaceChildren`への変更、`paste`メソッドのロジック変更など、提供されたDiffの内容と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「クリップボードシリアライズ」「インライン要素」「ネイティブハンドラ」など、PRの文脈で使われている技術用語を正確に使用しています。

説明の技術的正確性 ✓ PASS

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

「`<code>`がインライン要素であるため空白が折りたたまれる」という原因の説明や、「`replaceChildren`で`<pre>`を保持する」という解決策の説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張(改行が失われる原因、ペースト時にHTMLが混入する原因、それぞれの解決策)は、PRのDescriptionとDiffによって完全に裏付けられています。

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

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

PR番号(#1010)やファイルパス(`src/editor/clipboard.js`など)は正確に記載されています。

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

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

記事のタイトル「コードブロックのコピー&ペースト修正:改行保持と平文ペースト強制」は、PRのタイトル「Fix code block copy/paste: preserve newlines and always paste as plain text」の内容を正確に和訳・要約しています。

外部知識の正確性 ✓ PASS

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

PR情報に含まれないバージョン情報やリリース予定などの外部知識の捏造は見られません。

時間表現の正確性 ✓ PASS

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

「以前の実装では」や「新実装では」といった時間表現は、PRの文脈(`Previously`と`now`)と一致しており、歪曲はありません。