diff --git a/apps/client/src/features/editor/extensions/markdown-clipboard.test.ts b/apps/client/src/features/editor/extensions/markdown-clipboard.test.ts index 8c17a4f1..f2b332b6 100644 --- a/apps/client/src/features/editor/extensions/markdown-clipboard.test.ts +++ b/apps/client/src/features/editor/extensions/markdown-clipboard.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect } from "vitest"; -import { normalizeTableColumnWidths } from "./markdown-clipboard"; +import { + normalizeTableColumnWidths, + classifyClipboardSelection, +} from "./markdown-clipboard"; // normalizeTableColumnWidths mutates a DOM subtree (jsdom provides document). function root(html: string): HTMLElement { @@ -124,3 +127,47 @@ describe("normalizeTableColumnWidths", () => { ).toEqual([null, null]); }); }); + +describe("classifyClipboardSelection", () => { + it("serializes a list of 2+ items as markdown", () => { + expect( + classifyClipboardSelection([{ name: "bulletList", childCount: 2 }]), + ).toEqual({ asMarkdown: true, wrapBareRows: false }); + }); + + it("leaves a single-item list as plain text", () => { + expect( + classifyClipboardSelection([{ name: "bulletList", childCount: 1 }]), + ).toEqual({ asMarkdown: false, wrapBareRows: false }); + }); + + it("serializes a whole table without wrapping bare rows", () => { + expect( + classifyClipboardSelection([{ name: "table", childCount: 3 }]), + ).toEqual({ asMarkdown: true, wrapBareRows: false }); + }); + + it("serializes a partial cell selection (bare rows) and flags wrapping", () => { + expect( + classifyClipboardSelection([ + { name: "tableRow", childCount: 2 }, + { name: "tableRow", childCount: 2 }, + ]), + ).toEqual({ asMarkdown: true, wrapBareRows: true }); + }); + + it("leaves plain paragraphs as plain text", () => { + expect( + classifyClipboardSelection([{ name: "paragraph", childCount: 1 }]), + ).toEqual({ asMarkdown: false, wrapBareRows: false }); + }); + + it("does not wrap when rows are mixed with other block types", () => { + expect( + classifyClipboardSelection([ + { name: "tableRow", childCount: 2 }, + { name: "paragraph", childCount: 1 }, + ]), + ).toEqual({ asMarkdown: false, wrapBareRows: false }); + }); +}); diff --git a/apps/client/src/features/editor/extensions/markdown-clipboard.ts b/apps/client/src/features/editor/extensions/markdown-clipboard.ts index c8e36a1b..fa387569 100644 --- a/apps/client/src/features/editor/extensions/markdown-clipboard.ts +++ b/apps/client/src/features/editor/extensions/markdown-clipboard.ts @@ -27,24 +27,36 @@ export const MarkdownClipboard = Extension.create({ key: new PluginKey("markdownClipboard"), props: { clipboardTextSerializer: (slice) => { - const listTypes = ["bulletList", "orderedList", "taskList"]; - let topLevelCount = 0; - let hasList = false; + const topLevelNodes: { name: string; childCount: number }[] = []; slice.content.forEach((node) => { - if (listTypes.includes(node.type.name)) { - hasList = true; - topLevelCount += node.childCount; - } else { - topLevelCount++; - } + topLevelNodes.push({ + name: node.type.name, + childCount: node.childCount, + }); }); - if (!hasList || topLevelCount < 2) return null; + const { asMarkdown, wrapBareRows } = + classifyClipboardSelection(topLevelNodes); + if (!asMarkdown) return null; const div = document.createElement("div"); const serializer = DOMSerializer.fromSchema(this.editor.schema); const fragment = serializer.serializeFragment(slice.content); - div.appendChild(fragment); + + if (wrapBareRows) { + // A partial table cell-selection serializes to bare