Preserve plain text spacing and backslashes on paste

basecamp/lexxy

Lexxy now preserves the exact whitespace and backslashes of plain‑text paths when they are pasted, eliminating the silent corruption caused by the previous markdown conversion path.

背景

Problem: Windows/UNC file paths that contain consecutive spaces or leading backslashes were corrupted on paste because the plain‑text input was forced through the markdown conversion pipeline (Clipboard#pasteMarkdown). The markdown renderer collapses runs of whitespace according to HTML rules and unescapes backslashes, which rewrites the user's original string. This issue manifested as paths like \\Arina-alvand\alvand\03 - ENGINEERING\... being pasted as \Arina-alvand\alvand\03 - ENGINEERING\... with spaces collapsed.

The bug was reported in Basecamp card 10006219528 and reproduced by pasting a UNC path with multiple spaces and backslashes.

技術的な変更

新しい判定ロジック: Clipboard#pasteMarkdown now invokes a private helper #isPlainTextWithoutMarkdown after the markdown conversion. If the resulting document consists of a single <p> element whose children are all text nodes, the method returns true and the original text is inserted verbatim.

@@ -165,14 +165,31 @@ export default class Clipboard {
   #pasteMarkdown(text) {
     const html = marked(text, { breaks: true })
     const doc = parseHtml(html)
-    const detail = Object.freeze({
-      markdown: text,
-      document: doc,
-      addBlockSpacing: () => addBlockSpacing(doc)
-    })
-
-    dispatch(this.editorElement, "lexxy:insert-markdown", detail)
-    this.contents.insertDOM(doc, { tag: PASTE_TAG })
+    if (this.#isPlainTextWithoutMarkdown(doc)) {
+      this.contents.insertText(text, { tag: PASTE_TAG })
+    } else {
+      const detail = Object.freeze({
+        markdown: text,
+        document: doc,
+        addBlockSpacing: () => addBlockSpacing(doc)
+      })
+
+      dispatch(this.editorElement, "lexxy:insert-markdown", detail)
+      this.contents.insertDOM(doc, { tag: PASTE_TAG })
+    }
+  }
+
+  // Markdown conversion collapses runs of whitespace and unescapes backslashes,
+  // silently corrupting plain text such as Windows/UNC file paths. When the text
+  // carries no Markdown structure, paste it verbatim instead.
+  #isPlainTextWithoutMarkdown(doc) {
+    const elements = Array.from(doc.body.children)
+    if (elements.length !== 1) return false
+
+    const paragraph = elements[0]
+    return paragraph.nodeName === "P"
+      && Array.from(paragraph.childNodes).every((node) => node.nodeType === Node.TEXT_NODE)
   }

新しい挿入メソッド: Contents#insertText creates a paragraph node, appends a text node with the raw string, and delegates to the existing insertAtCursor pipeline. This reuses NodeInserter, ensuring that edge cases such as pasting inside a blockquote (addressed in PR #1109) remain safe.

@@ -49,6 +49,13 @@ export default class Contents {
     }, { tag })
   }

+  insertText(text, { tag } = {}) {
+    this.editor.update(() => {
+      const paragraph = $createParagraphNode().append($createTextNode(text))
+      this.insertAtCursor(paragraph)
+    }, { tag })
+  }
+
   insertAtCursor(...nodes) {
     const selection = $getSelection() ?? $getRoot().selectEnd()
     const inserter = NodeInserter.for(selection)

テスト追加: Playwright tests under test/browser/tests/paste/plain_text_paths.test.js verify that UNC paths, consecutive spaces, and ordinary plain text survive paste unchanged, while genuine markdown (Hello **there**) still renders as expected.

設計判断

既存インフラの再利用: The fix opts to route plain‑text insertion through the same NodeInserter machinery rather than a low‑level insertRawText. This preserves the quote‑element‑point handling introduced in PR [#1109] and avoids code duplication. The detection method is deliberately lightweight—checking element count and node types—so the common markdown path remains unchanged.

マークダウン判定の境界: By limiting the verbatim path to documents that consist of a single text‑only paragraph, the implementation guarantees that any genuine markdown structure (headings, lists, links) continues to be processed by the full markdown pipeline. This design maintains backward compatibility for existing markdown‑centric workflows while fixing a specific plain‑text regression.

まとめ

The changes ensure that pasting Windows/UNC paths or any plain text with multiple spaces and backslashes retains its exact characters, addressing the silent corruption described in the original bug report. At the same time, the markdown conversion flow remains untouched for real markdown input, and the existing insertion infrastructure continues to handle complex cursor positions safely.

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
1b9086e9

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文・背景・技術的な変更・設計判断・まとめの全てが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは正しいdiff形式で記載され、GitHubリンクは[#番号](URL)の形式で正しくリンク化されています。ファイル名付きハイライトは使用していませんが、要件を満たしています。

対象読者への適合性 ✓ PASS

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

専門エンジニア向けの技術的詳細が中心で、初心者向けの余計な説明はありません。

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

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

各セクション・パラグラフがトピックセンテンスで始まり、1段落1トピックで構成され、長さも適切です。空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事内のdiffブロックは提供されたPRのdiffと完全に一致しており、コードの抜粋や変更点に不整合はありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語はPRで使用されているものと一致し、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

記事の技術的主張はPRの説明およびdiffで裏付けられており、論理的に正確です。

事実の突合 ✓ PASS

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

すべての事実(問題の概要、BasecampカードID、PR番号、関連PR #1109 など)はPR情報に根拠があります。

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

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

PR番号やカードID、コード行数などの数値は正確です。

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

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

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

外部知識の正確性 ✓ PASS

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

記事はPRに記載された情報以外の外部知識を含んでいません。

時間表現の正確性 ✓ PASS

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

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