Autolink bare URLs pasted with an HTML companion

basecamp/lexxy

Pasting a URL copied directly from the address bar in Chromium‑based browsers on macOS now automatically creates a link. The editor detects the clipboard shape where text/plain holds a bare URL and an optional matching text/html, and treats it as a URL paste.

背景

Clipboard type variance across browsers caused the regression. Safari supplies [ text/plain, text/uri-list ], the App ShareSheet provides [ text/uri-list ], while Chromium on macOS emits [ text/plain, text/html, (?:text/link-preview) ]. The original #isOnlyURLPasted method only returned true when text/uri-list was present, so Chromium‑derived pastes were excluded.

A second issue stemmed from #pastePlainText reading the first entry in clipboardData.items. Chromium lists the text/html entry before text/plain, causing the wrong fragment to be consumed even when a URL was detected.

技術的な変更

The paste flow now invokes #pastePlainTextOrURL instead of the former #pastePlainText. This method explicitly extracts the URL from text/plain or text/uri-list, bypassing the order‑dependent items[0] logic while preserving the existing call site.

@@ -40,7 +40,7 @@ export default class Clipboard {
     }

     if (this.#isPlainTextOrURLPasted(clipboardData)) {
-      this.#pastePlainText(clipboardData)
+      this.#pastePlainTextOrURL(clipboardData)
       event.preventDefault()
       return true
     }

The #isOnlyURLPasted implementation was overhauled. It now rejects Lexical‑specific clipboard data, handles pure text/uri-list sets, and detects a solitary URL in text/plain. When text/html is present, the new #htmlIsBareLinkToURL helper verifies that the HTML contains only that bare link before treating the paste as a URL.

@@ -78,14 +78,34 @@ export default class Clipboard {
     return types.length === 1 && types[0] === "text/plain"
   }

+  // Browsers expose a copied URL in several shapes:
+  //   Safari          [ text/plain, text/uri-list ]
+  //   App ShareSheet  [ text/uri-list ]
+  //   Chromium macOS  [ text/plain, text/html, (?:text/link-preview) ]
   #isOnlyURLPasted(clipboardData) {
-    // Safari URLs are copied as a text/plain + text/uri-list object
-    // App ShareSheet URLs are copied as solo text/uri-list object
+    if (this.#isLexicalClipboardData(clipboardData)) return false

     const types = Array.from(clipboardData.types)
-    return types.length
-      && types.length <= 2
-      && types.includes("text/uri-list")
-      && (types.length < 2 || types.includes("text/plain"))
+    if (types.includes("text/uri-list")) {
+      return types.every(type => type === "text/plain" || type === "text/uri-list")
+    }
+
+    if (clipboardData.files.length) return false
+
+    const text = clipboardData.getData("text/plain").trim()
+    if (!isAutolinkableURL(text)) return false
+
+    const html = clipboardData.getData("text/html")
+    return !html || this.#htmlIsBareLinkToURL(html, text)
   }
+
+  #htmlIsBareLinkToURL(html, url) {
+    const doc = parseHtml(html)
+    if (doc.body.textContent.trim() !== url) return false
+
+    const links = doc.body.querySelectorAll("a")
+    if (links.length === 0) return true
+    return links.length === 1 && links[0].getAttribute("href") === url
+  }

Two new browser tests were added to verify the corrected behaviour. One test confirms that a clipboard containing text/plain + matching text/html autolinks the URL on Chromium macOS, and another ensures that richer HTML content falls back to the normal HTML insertion path.

設計判断

The primary design decision was to treat a clipboard where text/plain holds a lone autolinkable URL and an optional matching text/html as a URL paste. This guard keeps genuine rich pastes—where the HTML contains additional markup or a different link target—on the HTML path, preserving existing user expectations.

Rather than relying on the ordering of clipboardData.items, the implementation now explicitly queries text/plain and text/uri-list, removing browser‑specific ordering dependencies. The HTML‑equality check introduces a minimal code path while maintaining backward compatibility; Safari and ShareSheet behaviours remain unchanged.

まとめ

By recognizing the Chromium‑macOS clipboard shape and reading the URL from the appropriate data type, the editor now autolinks bare URLs consistently across browsers. The change adds a small HTML‑validation helper, retains legacy behaviours, and is verified by 2 new browser tests while the full paste suite remains green (345 tests).

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
96a654ff

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的変更、設計判断(任意)、まとめの5要素が明確に配置されており、"総論→各論→結論"の構成が守られています。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しいdiffハイライトで記載されています。GitHubリンクは [PR #1172](URL) という形で # が含まれていますが、仕様通りの "[#123](URL)" 形式ではないため軽微な違いとして警告します。

対象読者への適合性 ✓ PASS

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

専門的な開発者向けの内容で、余計な基礎解説はなく適切です。

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

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

各セクションが総論・各論・結論のパラグラフで構成され、トピックセンテンスが先頭にあり、段落は短く空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中のdiffブロックは提供されたPRのdiffと一致しており、変更箇所・ファイル名・コード内容が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

用語はPR説明と一致し、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

技術的な説明はPRの根拠に基づき、因果関係も正しく記述されています。

事実の突合 ✓ PASS

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

全ての主張はPRのタイトル・説明・diff・テストコードで裏付けられており、捏造や推測はありません。

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

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

テスト数(2件)や全体テスト数(345件)など数値はPRと一致しています。

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

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

記事タイトルはPRタイトルと完全に一致しています。

外部知識の正確性 ✓ PASS

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

バージョンサポートやリリース日程といった外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PRの記述と一致しています。