diff --git a/frontend/src/components/shared/markdown-editor/editor-paste.ts b/frontend/src/components/shared/markdown-editor/editor-paste.ts new file mode 100644 index 00000000..3844d17a --- /dev/null +++ b/frontend/src/components/shared/markdown-editor/editor-paste.ts @@ -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', +}); diff --git a/frontend/src/components/shared/markdown-editor/markdown-editor-extensions.ts b/frontend/src/components/shared/markdown-editor/markdown-editor-extensions.ts index 36a291b4..0cf17ddf 100644 --- a/frontend/src/components/shared/markdown-editor/markdown-editor-extensions.ts +++ b/frontend/src/components/shared/markdown-editor/markdown-editor-extensions.ts @@ -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, ];