コードブロックのコピー&ペースト修正:改行保持と平文ペースト強制
コードブロックをコピーして別の場所に貼り付けると改行が失われる問題と、コードブロックへのペーストで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.js の paste() メソッドと新たなプライベートメソッド #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 によってイベントを完全に引き取り、平文挿入のみを保証している。
まとめ
replaceWith を replaceChildren に変更して <pre> ラッパーを保持し、コードブロックへのペーストを平文専用ハンドラで引き取るという2点の修正により、コードブロックのコピー&ペースト動作が一貫して正しく機能するようになった。いずれも既存のブラウザ仕様とLexicalの動作特性を正確に把握した上で、最小限の変更で根本原因に対処している。