クリップボード経由でリンクをペーストするとURLが消える問題を修正

basecamp/lexxy

Lexxy内でコピーしたリンクを貼り付けるとhrefが失われプレーンテキストになるバグを修正しました。Clipboardクラスがペースト処理を直接ハンドリングする構造に変更され、「シングルリンク」ペイロードを検知して適切にリンクを復元します。

背景

Lexicalエディタのクリップボードペイロードがシングルリンクとして構成されたとき、Lexicalのデフォルトリッチテキストペースト処理がリンクを失う問題がありました。application/x-lexical-editor形式でコピーされたリンクは、単一の LinkNode(または段落でラップされたLinkNode)としてシリアライズされます。しかし、LexicalのデフォルトのSELECTION_INSERT_CLIPBOARD_NODES_COMMANDハンドラはこの構造をフラット化し、hrefを保持せずリンクのテキストコンテンツのみを挿入していました。

既存の実装では、ペースト処理を CommandDispatcherPASTE_COMMANDCOMMAND_PRIORITY_LOW で登録し、clipboard.paste() に委譲する構造でした。このアーキテクチャでは、Lexicalが内部的に発火させる SELECTION_INSERT_CLIPBOARD_NODES_COMMAND を横断してリンクの特殊ケースを処理する経路がありませんでした。

この問題は、生のURLをペーストする既存のパスが正しく動作していたことと対照的です。URLペースト処理ではテキストを選択していればリンクでラップし、選択がなければカーソル位置にリンクを挿入する #insertSingleLinkAt が実装済みでした。今回の修正はその既存ロジックを再利用する形で解決を図っています。

技術的な変更

Clipboardクラス自身が PASTE_COMMANDSELECTION_INSERT_CLIPBOARD_NODES_COMMAND の両コマンドを登録する責務を持つように変更され、CommandDispatcher からペースト関連のコード一切が除去されました。

clipboard.js では #registerPasteCommands メソッドが追加され、コンストラクタから呼び出されます。また、リソース管理のために ListenerBin を使ったクリーンアップ機構と dispose() メソッドが追加されています。

#registerPasteCommands() {
  this.#listeners.track(
    this.editor.registerCommand(PASTE_COMMAND, this.paste.bind(this), COMMAND_PRIORITY_NORMAL),
    this.editor.registerCommand(
      SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
      (payload) => this.#handleParsedClipboardNodes(payload),
      COMMAND_PRIORITY_NORMAL
    )
  )
}

#handleParsedClipboardNodes({ nodes, selection }) {
  const url = $bareUrlFromSingleLink(nodes)
  if (!url) return false

  this.#insertSingleLinkAt(selection, url)

$bareUrlFromSingleLink ヘルパーが「シングルリンク」ペイロードを判定します。nodes配列がシングルリンクノードである場合、または単一の段落ノードにラップされた単一リンクノードである場合にURLを返し、それ以外は false を返してデフォルトのペースト処理に委ねます。$isLinkNode$isParagraphNode を使った判定により、段落でラップされたリンクにも対応しています。

command_dispatcher.js では PASTE_COMMAND の登録、dispatchPaste メソッド、this.clipboard への参照がすべて削除されました。editor.js では Clipboard インスタンスが this.#disposables に追加され、dispose() が確実に呼ばれるようになっています。

 this.clipboard = new Clipboard(this)
+this.#disposables.push(this.clipboard)

変更の要点を整理すると次のとおりです:

  • PASTE_COMMAND の優先度が COMMAND_PRIORITY_LOW から COMMAND_PRIORITY_NORMAL に昇格
  • SELECTION_INSERT_CLIPBOARD_NODES_COMMANDCOMMAND_PRIORITY_NORMAL でインターセプト
  • Clipboarddispose() を実装し #disposables に登録されるようになった
  • テストファイル test/browser/tests/paste/copy_link.test.js が追加(177行)

設計判断

ペースト処理の責務を CommandDispatcher から Clipboard クラスに移した設計が採用されました。

変更前の構造では CommandDispatcher がペーストのエントリポイントを持ち、Clipboard はそこから呼ばれるだけでした。今回の変更では Clipboard 自身がLexicalのコマンドバスに接続し、PASTE_COMMANDSELECTION_INSERT_CLIPBOARD_NODES_COMMAND の両方を直接ハンドルします。これにより、Lexicalがクリップボードノードを解析した後に発火する SELECTION_INSERT_CLIPBOARD_NODES_COMMAND をインターセプトし、ノード構造を検査してシングルリンクを検出するという処理が自然に書けるようになります。

シングルリンク判定が false を返した場合はハンドラが false を返すことで、Lexicalのデフォルト処理にフォールスルーします。これにより既存のペースト動作を壊さずに特定ケースのみを上書きできます。ListenerBin による登録解除の管理もこの変更で整備され、ライフサイクル管理の一貫性が向上しています。

まとめ

ペースト処理の責務を Clipboard クラスに集約し、Lexicalのコマンドパイプラインを適切なタイミングでインターセプトすることで、シングルリンクのペイロードが持つ構造情報を保持できるようになりました。既存のURLペーストロジックを再利用する設計は変更範囲を最小化しつつ、リンクの段落ラップなど複数の形式に対応する堅牢な解決策を実現しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
2b89a76c

この記事は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リンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:src/editor/clipboard.js)とPRリンク([PR #1022](...))の記法がガイドラインに準拠しています。

対象読者への適合性 ✓ PASS

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

Lexicalエディタの内部的なコマンド処理に関する内容であり、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクション、各パラグラフが要点から始まり、具体例で補強する構成になっており、非常に読みやすいです。段落の長さも適切です。

Diff内容との照合 ✓ PASS

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

記事で引用されている`clipboard.js`および`editor.js`のコードは、提供されたDiffの内容を正確に反映しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「LinkNode」「CommandDispatcher」「SELECTION_INSERT_CLIPBOARD_NODES_COMMAND」など、PRで使われている技術用語を正しく使用しています。

説明の技術的正確性 ✓ PASS

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

ペースト処理の責務を`CommandDispatcher`から`Clipboard`クラスに移したという説明は、Diffの内容と一致しており、技術的に正確です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionやDiffから裏付けが取れ、ハルシネーション(情報の創作)は見られません。

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

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

PR番号「#1022」や追加されたテストファイルの行数「177行」など、記事中の数値や固有名詞はすべて正確です。

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

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

記事タイトルはPRの「Preserve link when pasting a Lexxy single-link clipboard payload」という主題を的確に要約しており、内容と一致しています。

外部知識の正確性 ✓ PASS

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

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

時間表現の正確性 ✓ PASS

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

記事内には時間表現に関する記述はなく、PR情報との時間的な齟齬は発生していません。