Skip to content

fix(message-editor): stop pasted CSS and escapes leaking into composer#3089

Merged
jonathanlab merged 1 commit into
mainfrom
posthog-code/fix-paste-css-leak
Jul 2, 2026
Merged

fix(message-editor): stop pasted CSS and escapes leaking into composer#3089
jonathanlab merged 1 commit into
mainfrom
posthog-code/fix-paste-css-leak

Conversation

@jonathanlab

Copy link
Copy Markdown
Contributor

Problem

Pasting rich text into the chat composer inserted garbage before the intended text. Reported case: pasting produced

p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 18.0px Helvetica}

Yo dude

Root cause

Both bugs live in the HTML→Markdown paste conversion (htmlToMarkdown / Turndown) in packages/ui/src/features/message-editor/utils/htmlToMarkdown.ts, used by the Tiptap handlePaste handler.

  1. macOS <style> leak. When you copy rich text from a native macOS app (Notes, Mail, Slack), the clipboard text/html contains a <style> block. Turndown has no rule for <style>, so it emitted the CSS as text — the style block, then the real text.
  2. Escape mangling. Turndown's default escaping is meant for round-tripping Markdown, so it backslash-escapes ordinary punctuation: 1.1\., snake_casesnake\_case, [x]\[x\]. Worse, the escaped output no longer equalled the plain-text fallback, so the equality guard failed and ordinary pastes were forced down the inline-Markdown path instead of the editor's native paste.

Fix

  • turndown.remove(["style", "script", "head", "title", "meta", "link"]) — drop non-content elements so their text never reaches the paste.
  • turndown.escape = (text) => text — the composer is plain-text; disabling escaping keeps ordinary text intact, so it equals the plain-text fallback → returns null → defers to native paste. Only genuinely structural HTML (bold, lists, links, tables, code) takes the Markdown path.

Testing

  • Added 6 regression tests (macOS style-block leak, escaping cases, and formatting-still-preserved). All 12 tests in htmlToMarkdown.test.ts pass; the new ones fail if either fix is reverted.
  • biome lint clean on both files.
  • Verified against the running Electron app over CDP by executing its actual bundled htmlToMarkdown module:
    • macOS <style>…p.p1 {…} + fallback → null (defers → clean Yo dude); no fallback → "Yo dude".
    • snake_case_name and 1. item [x]null (no mangling).
    • See <strong>bold</strong> … <a>link</a>See **bold** and [link](https://x.com) (formatting preserved).

🤖 Generated with Claude Code

Pasting rich text into the chat composer inserted garbage before the real
text. Two causes in the HTML->Markdown paste conversion (Turndown):

- macOS puts a `<style>` block on the clipboard when copying from native
  apps (Notes, Mail, Slack). Turndown emitted its CSS as text, so pasting
  "Yo dude" produced
  "p.p1 {margin: 0.0px ...; font: 18.0px Helvetica}\n\nYo dude".
  Fixed by removing non-content elements (style/script/head/title/meta/link).

- Turndown's default escaping backslash-mangled ordinary punctuation
  ("1." -> "1\.", "snake_case" -> "snake\_case", "[x]" -> "\[x\]"). Because
  the escaped output no longer equalled the plain-text fallback, plain
  pastes were also forced down the inline-Markdown path instead of the
  editor's native paste. Disabling escaping keeps plain text intact so it
  equals the fallback and defers to native paste; only genuinely formatted
  content (bold, lists, links, tables, code) takes the Markdown path.

Added regression tests covering the macOS style-block leak, the escaping
cases, and that real formatting is still preserved. Verified against the
running app's bundled module over CDP.

Generated-By: PostHog Code
Task-Id: 0f5214ac-2b22-4769-92cb-39967a9b67ed
@trunk-io

trunk-io Bot commented Jul 2, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 1b95886.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(message-editor): stop pasted CSS and..." | Re-trigger Greptile

// text — "1." -> "1\.", "snake_case" -> "snake\_case", "[x]" -> "\[x\]".
// Disabling it keeps plain text intact so it also stays equal to the
// plain-text fallback below and defers to the editor's native paste.
turndown.escape = (text) => text;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unescaped metacharacters can corrupt rendering in formatted pastes

Setting escape to a no-op prevents the backslash-mangling fix for plain text, but it also means *, _, [, and ] are never escaped in text nodes that sit alongside real Markdown formatting. When the result isn't equal to the plain-text fallback it gets returned as Markdown and rendered by Tiptap. A paste like <p>formula a*b and <strong>c</strong></p> produces formula a*b and **c**; if the *b and **c span looks like a valid CommonMark emphasis run to the parser, the rendered output will differ from the intended text. The risk is narrow (requires Markdown-special chars next to real HTML formatting) but it's a real edge case that no test currently covers.

Comment on lines +71 to +76
it("preserves real formatting without escaping surrounding punctuation", () => {
const html = "<p>See <strong>item_1.</strong> in arr[0]</p>";
expect(htmlToMarkdown(html, "See item_1. in arr[0]")).toBe(
"See **item_1.** in arr[0]",
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Standalone it where a parameterised case would be more consistent

The project prefers parameterised tests. This scenario (HTML with formatting → expected Markdown output) has the same shape as the existing it.each block at the top of the file, and could be expressed as another [label, html, plainFallback, expected] row there. Keeping it as a one-off it means future similar cases also grow as individual tests rather than rows.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@jonathanlab jonathanlab merged commit 47412d7 into main Jul 2, 2026
20 checks passed
@jonathanlab jonathanlab deleted the posthog-code/fix-paste-css-leak branch July 2, 2026 12:17
adboio added a commit that referenced this pull request Jul 2, 2026
… text

VS Code & co put syntax-highlighted text/html on the clipboard whose only
structure is a white-space:pre wrapper with one <div> per line. Neither
paste path handles that shape faithfully: Turndown converts the line divs
into paragraphs, so the inserted Markdown gains a blank line between every
code line, and ProseMirror's native HTML paste splits the lines into one
paragraph each, which the composer serializes back with blank lines too.
(#3089 fixed the backslash-escaping half of this; multi-line copies were
still mangled.)

Detect the code-editor clipboard shape (single white-space:pre block or
bare <pre>, div/span/br content only) and insert the clipboard's plain
text verbatim instead. Web code blocks (<pre><code>) keep the existing
fenced-Markdown conversion.

Generated-By: PostHog Code
Task-Id: ad31c8f6-d523-4079-b13e-1f8aa419d8d9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants