mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-24 19:41:14 +02:00
ProseMirror's default clipboardTextSerializer uses Slice.textBetween, which flattens every node to its inner text. Copying `## 你好` from the editor only put `你好` on the clipboard's text/plain channel, so pasting into VS Code, terminals, or messaging apps lost all Markdown markers. Add a markdown-copy extension symmetric to the existing markdown-paste: on copy/cut/drag, route the selected Slice through editor.markdown.serialize to write the Markdown source. The text/html channel is left at ProseMirror's default so pasting back into another ProseMirror editor still preserves exact node structure via data-pm-slice. Registered for both editable and readonly modes — users frequently copy from rendered comments/issue descriptions. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
2.3 KiB
TypeScript
63 lines
2.3 KiB
TypeScript
/**
|
|
* Markdown copy extension — make the clipboard's text/plain channel carry
|
|
* Markdown source instead of plain textContent.
|
|
*
|
|
* Symmetric to markdown-paste.ts:
|
|
* paste: text/plain → editor.markdown.parse → doc
|
|
* copy: slice → editor.markdown.serialize → text/plain
|
|
*
|
|
* Why: ProseMirror's default clipboardTextSerializer calls Slice.textBetween,
|
|
* which flattens every node to its inner text. Headings, lists, code blocks,
|
|
* mentions, file cards — all lose their Markdown markers. Pasting into VS
|
|
* Code, terminals, or messaging apps then sees only naked text.
|
|
*
|
|
* The text/html channel is left at ProseMirror's default so pasting back
|
|
* into another ProseMirror editor still preserves exact node structure via
|
|
* data-pm-slice.
|
|
*/
|
|
import { Extension } from "@tiptap/core";
|
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
import type { Slice } from "@tiptap/pm/model";
|
|
|
|
// Blob URLs (blob:http://…) are process-local; never let them leave the page.
|
|
const BLOB_IMAGE_RE = /!\[[^\]]*\]\(blob:[^)]*\)\n?/g;
|
|
|
|
export function createMarkdownCopyExtension() {
|
|
return Extension.create({
|
|
name: "markdownCopy",
|
|
addProseMirrorPlugins() {
|
|
const { editor } = this;
|
|
|
|
const fallback = (slice: Slice) =>
|
|
slice.content.textBetween(0, slice.content.size, "\n\n");
|
|
|
|
return [
|
|
new Plugin({
|
|
key: new PluginKey("markdownCopy"),
|
|
props: {
|
|
clipboardTextSerializer(slice: Slice) {
|
|
if (!editor.markdown) return fallback(slice);
|
|
try {
|
|
// Wrap slice content in a temp doc so the serializer walks
|
|
// it like a real document. Inline-only slices auto-wrap
|
|
// into doc → paragraph; block slices pass through.
|
|
const doc = editor.schema.topNodeType.create(
|
|
null,
|
|
slice.content,
|
|
);
|
|
const md = editor.markdown.serialize(doc.toJSON());
|
|
return md.replace(BLOB_IMAGE_RE, "").replace(/\n+$/, "");
|
|
} catch {
|
|
// Special selections (e.g. table cellSelection) may fail
|
|
// schema validation when wrapped in a doc node. Fall back
|
|
// so copy never breaks.
|
|
return fallback(slice);
|
|
}
|
|
},
|
|
},
|
|
}),
|
|
];
|
|
},
|
|
});
|
|
}
|