diff --git a/apps/web/features/editor/content-editor.css b/apps/web/features/editor/content-editor.css index b6e7ed16..697771a7 100644 --- a/apps/web/features/editor/content-editor.css +++ b/apps/web/features/editor/content-editor.css @@ -125,6 +125,9 @@ color: color-mix(in srgb, var(--foreground) 75%, transparent); padding: 0.125rem 0.375rem; border-radius: var(--radius-sm); + box-decoration-break: clone; + -webkit-box-decoration-break: clone; + line-height: 2; } /* Code blocks */ diff --git a/apps/web/features/editor/extensions/markdown-paste.ts b/apps/web/features/editor/extensions/markdown-paste.ts index fc549e8f..7dde1fb9 100644 --- a/apps/web/features/editor/extensions/markdown-paste.ts +++ b/apps/web/features/editor/extensions/markdown-paste.ts @@ -11,17 +11,29 @@ export function createMarkdownPasteExtension() { new Plugin({ key: new PluginKey("markdownPaste"), props: { - clipboardTextParser(text, _context, plainText) { - if (!plainText && editor.markdown) { - const json = editor.markdown.parse(text); - const node = editor.schema.nodeFromJSON(json); - return Slice.maxOpen(node.content); - } - // Plain text fallback - const p = editor.schema.nodes.paragraph!; - const doc = editor.schema.nodes.doc!; - const paragraph = p.create(null, text ? editor.schema.text(text) : undefined); - return new Slice(doc.create(null, paragraph).content, 0, 0); + handlePaste(view, event) { + if (!editor.markdown) return false; + const clipboard = event.clipboardData; + if (!clipboard) return false; + + const text = clipboard.getData("text/plain"); + if (!text) return false; + + const html = clipboard.getData("text/html"); + + // If HTML contains data-pm-slice, the source is another + // ProseMirror editor — let ProseMirror use its native HTML + // clipboard path to preserve exact node structure. + if (html && html.includes("data-pm-slice")) return false; + + // Everything else (VS Code, text editors, .md files, terminals, + // web pages): parse text/plain as Markdown. + const json = editor.markdown.parse(text); + const node = editor.schema.nodeFromJSON(json); + const slice = Slice.maxOpen(node.content); + const tr = view.state.tr.replaceSelection(slice); + view.dispatch(tr); + return true; }, }, }),