Please change the rules for pasting plain text

In prosemirror, if there is one newline, it is made into a paragraph.

If there is only one newline, then <br> If there are two line breaks, then <p></p>

However, I think I need to add some processing such as clipboardTextparser to do this. I am considering implementing clipboardTextparser myself if necessary, but if possible, I would like the official library to change the logic for inserting line breaks and paragraphs. If you don’t plan to change it, could you please tell me why the logic is like this?

I’m sorry for my bad English, and I’m new to this kind of community, so I apologize if I’m bothering you.

1 Like

This isn’t going to change in the core library—that would cause trouble for existing setups, and not everyone will consider your changes an improvement (there’s no guarantee that the pasted text will actually use the double-newline-for-paragraph-break convention).

So yes, you’ll have to implement a clipboardTextParser function yourself.

As a side comment: HTMLmirror vs ProseMirror:

Being an editor for HTML pages, I think what end users and developers want is a HTMLmirror editor instead of a ProseMirror editor (that focuses just on prose).

That is, a HTML editor that gives control over HTML content in a WYSIWYG way and can also handle pasting of complex HTML content (retaining the fidelity of the original HTML and then be able to edit it). That’s a lot to ask for … but that’s what users really want for HTML pages…

I just give this opinion to give input on your future direction for the project and especially the future scope of the project (prose vs more expansive HTML). ProseMirror is a great contribution and seems to be by far the leading editor for mobile browsers. Mobile is dominating the web, so ProseMirror is well positioned to be the basis of a powerhouse on the web…

Some situations do indeed need an HTML editor. They’ll want to use a different project because ProseMirror isn’t that editor.

I understand the compatibility concern. Plain text newlines are ambiguous, so changing the existing default globally would be risky.

But I think there is still a narrower issue here: the current default paste and copy policies are not inverses of each other.

Pasting:

a\nb\n\nc\nd

currently becomes four textblocks:

p("a"), p("b"), p("c"), p("d")

That loses the distinction between a single newline and a blank line. Then copying those four textblocks serializes them with “\n\n” between each block:

a\n\nb\n\nc\n\nd

So paste+copy changes the structure of a common plaintext fragment.

Would you be open to an opt-in, schema-aware way to make plaintext clipboard handling round-trip? For schemas/editors that want the common “single newline = line break, blank line = paragraph break” convention, paste could convert single newlines to a configured inline linebreak node and blank lines to textblock boundaries, while copy would serialize that linebreak node as “\n” and textblock boundaries as “\n\n”.

That would avoid changing behavior for existing setups, but give applications a standard core-supported path for a convention that ProseMirror schemas can already represent.

(related thread)

clipboard.ts

@@ -33,8 +33,10 @@ export function serializeForClipboard(view: EditorView, slice: Slice) {
     (firstChild as HTMLElement).setAttribute(
       "data-pm-slice", `${openStart} ${openEnd}${wrappers ? ` -${wrappers}` : ""} ${JSON.stringify(context)}`)
 
+  let linebreak = view.state.schema.linebreakReplacement
   let text = view.someProp("clipboardTextSerializer", f => f(slice, view)) ||
-      slice.content.textBetween(0, slice.content.size, "\n\n")
+      slice.content.textBetween(0, slice.content.size, "\n\n", linebreak ? node =>
+        node.type == linebreak ? "\n" : node.type.spec.leafText ? node.type.spec.leafText(node) : "" : undefined)
 
   return {dom: wrap, text, slice}
 }
@@ -56,10 +58,10 @@ export function parseFromClipboard(view: EditorView, text: string, html: string
     if (parsed) {
       slice = parsed
     } else {
-      let marks = $context.marks()
-      let {schema} = view.state, serializer = DOMSerializer.fromSchema(schema)
+      let marks = $context.marks(), {schema} = view.state
+      let serializer = DOMSerializer.fromSchema(schema)
       dom = document.createElement("div")
-      text.split(/(?:\r\n?|\n)+/).forEach(block => {
+      text.replace(/\r\n?/g, "\n").split(schema.linebreakReplacement ? /\n\n/ : /\n+/).forEach(block => {
         let p = dom!.appendChild(document.createElement("p"))
         if (block) p.appendChild(serializer.serializeNode(schema.text(block, marks)))
       })

index.ts

@@ -710,7 +710,11 @@ export interface EditorProps<P = any> {
   /// The default behavior is to split the text into lines, wrap them
   /// in `<p>` tags, and call
   /// [`clipboardParser`](#view.EditorProps.clipboardParser) on it.
-  /// The `plain` flag will be true when the text is pasted as plain text.
+  /// When the schema defines a
+  /// [`linebreakReplacement`](#model.NodeSpec.linebreakReplacement),
+  /// single line breaks are converted to that node and pairs of line
+  /// breaks separate paragraphs. The `plain` flag will be true when
+  /// the text is pasted as plain text.
   clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice
 
   /// Can be used to transform pasted or dragged-and-dropped content
@@ -768,7 +772,9 @@ export interface EditorProps<P = any> {
   /// A function that will be called to get the text for the current
   /// selection when copying text to the clipboard. By default, the
   /// editor will use [`textBetween`](#model.Node.textBetween) on the
-  /// selected range.
+  /// selected range. When the schema defines a
+  /// [`linebreakReplacement`](#model.NodeSpec.linebreakReplacement),
+  /// that node is serialized as a newline.
   clipboardTextSerializer?: (this: P, content: Slice, view: EditorView) => string
 
   /// A set of [document decorations](#view.Decoration) to show in the