feat(editor): parse markdown on paste so paste matches load

@tiptap/markdown only parses markdown for initial content / insertContent,
never for the clipboard — so pasting block markdown (headings, lists,
tables, quotes, fences) landed as literal text while only StarterKit's
inline mark paste-rules fired, and `_`/`__` formatted on paste even though
load keeps them literal. Add a MarkdownPaste extension that routes
plain-text pastes through the same faithful markdown layer as load, so the
two are consistent. Rich sources keep ProseMirror's own fidelity: an
in-editor copy (data-pm-slice) and web/Office HTML (block tags) fall
through to the default path, and pastes inside code stay literal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-01 15:45:51 +07:00
co-authored by Claude Opus 4.8
parent 4e11fd96a4
commit 3b3c4cf7bd
2 changed files with 45 additions and 0 deletions
@@ -0,0 +1,43 @@
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from '@tiptap/pm/state';
// @tiptap/markdown parses markdown for load/insertContent but never for the clipboard, so a paste of block
// markdown (# ## - 1. | >) would land as literal text (only StarterKit's inline mark paste-rules fire).
// Route plain-text pastes through the same markdown layer as load; defer to ProseMirror's own path for rich
// sources — an in-editor copy (carries `data-pm-slice`) or web/Office HTML (block tags) — so their fidelity
// survives, and keep pastes inside code literal.
const RICH_HTML_BLOCK = /<(?:h[1-6]|ul|ol|li|table|thead|tbody|tr|td|th|blockquote|pre|img|hr)\b/i;
export const MarkdownPaste = Extension.create({
addProseMirrorPlugins() {
const { editor } = this;
return [
new Plugin({
key: new PluginKey('markdownPaste'),
props: {
handlePaste(view, event) {
const text = event.clipboardData?.getData('text/plain') ?? '';
if (!text.trim()) {
return false;
}
const html = event.clipboardData?.getData('text/html') ?? '';
if (html.includes('data-pm-slice') || RICH_HTML_BLOCK.test(html)) {
return false;
}
if (view.state.selection.$from.parent.type.spec.code || editor.isActive('code')) {
return false;
}
return editor.commands.insertContent(text, { contentType: 'markdown' });
},
},
}),
];
},
name: 'markdownPaste',
});
@@ -5,6 +5,7 @@ import { Placeholder } from '@tiptap/extensions';
import StarterKit from '@tiptap/starter-kit';
import { createMarkdownLayer, MarkdownTable } from './editor-markdown';
import { MarkdownPaste } from './editor-paste';
import { TagHighlight } from './editor-tag-highlight';
import { VariableHighlight } from './editor-variable-highlight';
@@ -25,4 +26,5 @@ export const createMarkdownExtensions = (placeholder?: string) => [
TagHighlight,
Placeholder.configure({ emptyEditorClass: 'is-editor-empty', placeholder }),
...createMarkdownLayer(),
MarkdownPaste,
];