Paste another site's text without replaying it forever
view.pasteText(text, event) re-asks every paste handler with the event it was given, so the composer's own handler replayed the paste until the stack overflowed (any rich paste from VS Code, Slack or a browser page). It now passes no event, and a ref keeps it from re-entering either way. The plain-text decision moves to shouldPastePlainText with its own tests.
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
} from "../(data)/composer-mention-kinds";
|
||||
import ComposerMentionChip from "../(partials)/composer-mention-chip";
|
||||
import { MentionMarkdown, mentionRef } from "../(utils)/mention-markdown";
|
||||
import shouldPastePlainText from "../(utils)/should-paste-plain-text";
|
||||
import searchMentionItems, { type ComposerMentionItem } from "../(utils)/search-mention-items";
|
||||
|
||||
/**
|
||||
@@ -63,6 +64,7 @@ export default function useComposerEditor({
|
||||
|
||||
const [menu, setMenu] = useState<ComposerMentionMenu | null>(null);
|
||||
const menu_ref = useRef<ComposerMentionMenu | null>(null);
|
||||
const replaying_paste = useRef(false);
|
||||
|
||||
const showMenu = (next: ComposerMentionMenu | null) => {
|
||||
menu_ref.current = next;
|
||||
@@ -177,6 +179,9 @@ export default function useComposerEditor({
|
||||
class: ComposerEditorClassName,
|
||||
},
|
||||
handlePaste: (view, event) => {
|
||||
/** `pasteText` re-runs this handler, so it must not send it round again. */
|
||||
if (replaying_paste.current) return false;
|
||||
|
||||
const files = [...(event.clipboardData?.files || [])];
|
||||
|
||||
if (files.length) {
|
||||
@@ -188,9 +193,22 @@ export default function useComposerEditor({
|
||||
const text = event.clipboardData?.getData("text/plain") || "";
|
||||
|
||||
/** Our own copies keep their formatting; other sites' don't. */
|
||||
if (!html || html.includes("data-pm-slice") || !text) return false;
|
||||
if (!shouldPastePlainText({ html, text })) return false;
|
||||
|
||||
return view.pasteText(text, event);
|
||||
replaying_paste.current = true;
|
||||
|
||||
try {
|
||||
/**
|
||||
* Without the event: `pasteText` re-asks every paste
|
||||
* handler with the one it's given, and this one would
|
||||
* replay the paste forever (a stack overflow), while a
|
||||
* fresh event reads no clipboard data and falls through
|
||||
* to the plain-text parse.
|
||||
*/
|
||||
return view.pasteText(text);
|
||||
} finally {
|
||||
replaying_paste.current = false;
|
||||
}
|
||||
},
|
||||
handleDrop: (_view, event) => {
|
||||
const files = [...(event.dataTransfer?.files || [])];
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import shouldPastePlainText from "./should-paste-plain-text";
|
||||
|
||||
describe("should paste plain text", () => {
|
||||
test("replays another site's rich paste as plain text", () => {
|
||||
expect(
|
||||
shouldPastePlainText({
|
||||
html: '<meta charset="utf-8"><span>GROK_BOT_MAILBOX_PASSWORD</span>',
|
||||
text: "GROK_BOT_MAILBOX_PASSWORD",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(shouldPastePlainText({ html: "<p>line one</p><p>line two</p>", text: "one\ntwo" })).toBe(true);
|
||||
});
|
||||
|
||||
test("keeps our own copies formatted", () => {
|
||||
expect(
|
||||
shouldPastePlainText({
|
||||
html: '<p data-pm-slice="1 1 []"><strong>bold</strong></p>',
|
||||
text: "**bold**",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("leaves a plain paste and a file paste alone", () => {
|
||||
expect(shouldPastePlainText({ html: "", text: "just text" })).toBe(false);
|
||||
expect(shouldPastePlainText({ html: '<img src="blob:x">', text: "" })).toBe(false);
|
||||
expect(shouldPastePlainText({ html: "", text: "" })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
type Params = {
|
||||
/** The clipboard's `text/html`. */
|
||||
html: string;
|
||||
/** The clipboard's `text/plain`. */
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a paste is replayed as plain text instead of parsed as HTML: a
|
||||
* rich paste from another site drops its styling (its markup and CSS),
|
||||
* while our own copies (`data-pm-slice`) keep their formatting and a paste
|
||||
* with no text (files, an image) is none of this handler's business.
|
||||
*/
|
||||
export default function shouldPastePlainText({ html, text }: Params) {
|
||||
if (!text) return false;
|
||||
if (!html) return false;
|
||||
|
||||
return !html.includes("data-pm-slice");
|
||||
}
|
||||
Reference in New Issue
Block a user