Skip to content

Commit 1b95886

Browse files
authored
fix(message-editor): stop pasted CSS and escapes leaking into composer
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
1 parent 7b32529 commit 1b95886

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

packages/ui/src/features/message-editor/utils/htmlToMarkdown.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,42 @@ describe("htmlToMarkdown", () => {
3636
expect(htmlToMarkdown("")).toBeNull();
3737
expect(htmlToMarkdown("<p></p>")).toBeNull();
3838
});
39+
40+
it.each([
41+
["ordered-list-style numbers", "1. First 2. Second"],
42+
["underscores in identifiers", "call snake_case_name here"],
43+
["square brackets", "an array like arr[0] and [x]"],
44+
["leading hash and dash", "# not a heading - not a bullet"],
45+
])(
46+
"does not backslash-escape plain text (%s), so it defers to native paste",
47+
(_, text) => {
48+
// Plain punctuation must not be mangled into "1\\.", "snake\\_case", etc.
49+
// When it stays intact it equals the plain-text fallback and returns null.
50+
expect(htmlToMarkdown(`<span>${text}</span>`, text)).toBeNull();
51+
},
52+
);
53+
54+
it("strips macOS <style> clipboard blocks instead of leaking CSS as text", () => {
55+
// Shape of the text/html macOS puts on the clipboard when copying rich
56+
// text from native apps. The CSS must not survive into the paste.
57+
const html = [
58+
'<meta charset="utf-8">',
59+
"<style>",
60+
"<!--",
61+
"p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 18.0px Helvetica}",
62+
"-->",
63+
"</style>",
64+
'<p class="p1">Yo dude</p>',
65+
].join("\n");
66+
// No formatting beyond the plain text once the CSS is gone, so it defers.
67+
expect(htmlToMarkdown(html, "Yo dude")).toBeNull();
68+
expect(htmlToMarkdown(html)).toBe("Yo dude");
69+
});
70+
71+
it("preserves real formatting without escaping surrounding punctuation", () => {
72+
const html = "<p>See <strong>item_1.</strong> in arr[0]</p>";
73+
expect(htmlToMarkdown(html, "See item_1. in arr[0]")).toBe(
74+
"See **item_1.** in arr[0]",
75+
);
76+
});
3977
});

packages/ui/src/features/message-editor/utils/htmlToMarkdown.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ function getTurndown(): TurndownService {
1313
emDelimiter: "*",
1414
});
1515
turndown.use(gfm); // tables, strikethrough, task lists
16+
// Drop non-content elements outright. macOS puts a <style> block in the
17+
// clipboard HTML when copying rich text from native apps (Notes, Mail,
18+
// Slack), and Turndown would otherwise emit its CSS as text, e.g.
19+
// "p.p1 {margin: 0.0px ...; font: 18.0px Helvetica}" before the real text.
20+
turndown.remove(["style", "script", "head", "title", "meta", "link"]);
21+
// The composer is plain-text, so we only want structural formatting
22+
// (headings, lists, links, tables, code) preserved as Markdown. Turndown's
23+
// default escaping is meant for round-tripping Markdown and mangles ordinary
24+
// text — "1." -> "1\.", "snake_case" -> "snake\_case", "[x]" -> "\[x\]".
25+
// Disabling it keeps plain text intact so it also stays equal to the
26+
// plain-text fallback below and defers to the editor's native paste.
27+
turndown.escape = (text) => text;
1628
return turndown;
1729
}
1830

0 commit comments

Comments
 (0)