import { Plugin } from 'prosemirror-state'; import { Slice, Fragment, Node as PMNode, Schema } from 'prosemirror-model'; import { markdownToHtml, looksLikeMarkdown } from '../markdown-parse'; import { htmlToSlice } from '../serializer'; /** * Builds a slice of plain paragraphs from text, splitting on newlines. Open * depth 1 on both ends lets the first/last block merge into the surrounding * paragraph, matching how a normal plain-text paste behaves. */ export function plainTextSlice(schema: Schema, text: string): Slice { const blocks = text.split(/\r\n?|\n/); const nodes: PMNode[] = blocks.map(line => schema.nodes.paragraph.create(null, line ? schema.text(line) : null) ); return new Slice(Fragment.from(nodes), 1, 1); } /** Reduces a pasted slice to unstyled text, preserving block breaks. */ export function stripSliceFormatting(schema: Schema, slice: Slice): Slice { const text = slice.content.textBetween(0, slice.content.size, '\n', '\n'); return plainTextSlice(schema, text); } export interface PasteOptions { /** * Read live: when it returns true, every paste drops formatting (the * persistent "paste as plain text" toggle). The Ctrl+Shift+V shortcut is a * separate one-shot handled in the keymap. */ getForcePlainText?: () => boolean; /** * Read live: when false, plain-text pastes are never parsed as Markdown. * Defaults to on. */ getMarkdownPaste?: () => boolean; /** Read live: whether pasted HTML is sanitized. Defaults to on. */ getSanitize?: () => boolean; } /** * Paste behavior: plain-text pastes that look like Markdown are parsed into * rich content (headings, lists, code fences, tables, inline marks), and all * pastes drop formatting while the host's plain-text toggle is on. */ export function pastePlugin(schema: Schema, options: PasteOptions = {}): Plugin { return new Plugin({ props: { handlePaste(view, event) { if (options.getForcePlainText?.()) return false; if (options.getMarkdownPaste && !options.getMarkdownPaste()) return false; // Only plain-text pastes qualify: an HTML flavor means the source // was already rich, and code blocks take text verbatim. const html = event.clipboardData?.getData('text/html') ?? ''; const text = event.clipboardData?.getData('text/plain') ?? ''; if (html.trim() || !text.trim()) return false; if (view.state.selection.$from.parent.type.spec.code) return false; if (!looksLikeMarkdown(text)) return false; const slice = htmlToSlice( schema, markdownToHtml(text), options.getSanitize?.() ?? true ); view.dispatch(view.state.tr.replaceSelection(slice).scrollIntoView()); return true; }, transformPasted(slice) { return options.getForcePlainText?.() ? stripSliceFormatting(schema, slice) : slice; }, }, }); }