From a42ac3d45085ee85bc34fdbfaa6ed1b5b8407f81 Mon Sep 17 00:00:00 2001 From: Olivier Lambert Date: Sat, 28 Mar 2026 23:26:47 +0100 Subject: [PATCH] fix: strip trailing whitespace-only paragraphs from pasted content (#2050) --- .../editor/extensions/markdown-clipboard.ts | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/apps/client/src/features/editor/extensions/markdown-clipboard.ts b/apps/client/src/features/editor/extensions/markdown-clipboard.ts index e1e3707d..0d6ab263 100644 --- a/apps/client/src/features/editor/extensions/markdown-clipboard.ts +++ b/apps/client/src/features/editor/extensions/markdown-clipboard.ts @@ -1,7 +1,7 @@ // adapted from: https://github.com/aguingand/tiptap-markdown/blob/main/src/extensions/tiptap/clipboard.js - MIT import { Extension } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; -import { DOMParser } from "@tiptap/pm/model"; +import { DOMParser, Fragment, Slice } from "@tiptap/pm/model"; import { find } from "linkifyjs"; import { markdownToHtml } from "@docmost/editor-ext"; @@ -40,7 +40,7 @@ export const MarkdownClipboard = Extension.create({ const { tr } = view.state; const { from, to } = view.state.selection; - const html = markdownToHtml(text); + const html = markdownToHtml(text.replace(/\n+$/, "")); const contentNodes = DOMParser.fromSchema( this.editor.schema, @@ -53,6 +53,37 @@ export const MarkdownClipboard = Extension.create({ view.dispatch(tr); return true; }, + // Strip trailing whitespace-only paragraphs from pasted content. + // Terminals (GNOME Terminal, etc.) often include trailing + // whitespace in their HTML clipboard data, which ProseMirror + // parses as an extra paragraph. Inside a list item this creates + // an orphan empty line that breaks the list structure. + transformPasted: (slice) => { + let { content, openStart, openEnd } = slice; + + // Remove trailing paragraphs that contain only whitespace + while (content.childCount > 1) { + const lastChild = content.lastChild; + if ( + lastChild?.type.name === "paragraph" && + lastChild.textContent.trim() === "" + ) { + const children = []; + for (let i = 0; i < content.childCount - 1; i++) { + children.push(content.child(i)); + } + content = Fragment.from(children); + } else { + break; + } + } + + if (content !== slice.content) { + return new Slice(content, openStart, Math.max(openEnd, 1)); + } + + return slice; + }, clipboardTextParser: (text, context, plainText) => { const link = find(text, { defaultProtocol: "http", @@ -64,7 +95,7 @@ export const MarkdownClipboard = Extension.create({ return null; } - const parsed = markdownToHtml(text); + const parsed = markdownToHtml(text.replace(/\n+$/, "")); return DOMParser.fromSchema(this.editor.schema).parseSlice( elementFromString(parsed), {