Skip to content

Fix trailing hard breaks being dropped on paste - #7840

Open
haradahinata wants to merge 1 commit into
ueberdosis:mainfrom
haradahinata:codex/preserve-trailing-hard-break-paste
Open

Fix trailing hard breaks being dropped on paste#7840
haradahinata wants to merge 1 commit into
ueberdosis:mainfrom
haradahinata:codex/preserve-trailing-hard-break-paste

Conversation

@haradahinata

Copy link
Copy Markdown

Changes Overview

Preserve trailing hard breaks when HTML copied from a Tiptap editor is pasted back into a Tiptap editor.

Implementation Approach

The hard break extension now transforms ProseMirror clipboard HTML that contains data-pm-slice. When an actual trailing <br> would otherwise be treated as ProseMirror's disposable trailing break, the transform appends a comment sentinel after it so the clipboard parser keeps the hardBreak node. Existing .ProseMirror-trailingBreak hack nodes are still ignored.

Testing Done

  • corepack pnpm exec vitest run packages/extension-hard-break/__tests__/hard-break.spec.ts
  • corepack pnpm exec vitest run packages/extension-hard-break/__tests__/hard-break.spec.ts packages/core/src/__tests__/transformPastedHTML.test.ts
  • corepack pnpm exec prettier --check packages/extension-hard-break/src/hard-break.ts packages/extension-hard-break/__tests__/hard-break.spec.ts .changeset/clear-hard-break-paste.md
  • corepack pnpm exec eslint packages/extension-hard-break/src/hard-break.ts packages/extension-hard-break/__tests__/hard-break.spec.ts
  • corepack pnpm --filter @tiptap/extension-hard-break build

Verification Steps

  1. Type a paragraph that ends with multiple hard breaks.
  2. Copy the paragraph from a Tiptap editor.
  3. Paste it back into a Tiptap editor.
  4. Confirm the pasted paragraph keeps the same number of hard breaks.

Additional Notes

None.

AI Usage

  • I have used AI tools (e.g., ChatGPT, Claude, Copilot) in creating this PR.
  • If yes, describe which tools were used and how they contributed to this PR:
    Codex was used to inspect the ProseMirror clipboard behavior, draft the patch, and run local validation commands.

Checklist

  • I have created a changeset for this PR if necessary.
  • My changes do not break the library.
  • I have added tests where applicable.
  • I have followed the project guidelines.
  • I have fixed any lint issues.

Related Issues

Fixes #5418

@changeset-bot

changeset-bot Bot commented May 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f711838

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 72 packages
Name Type
@tiptap/extension-hard-break Patch
@tiptap/starter-kit Patch
@tiptap/core Patch
@tiptap/extension-audio Patch
@tiptap/extension-blockquote Patch
@tiptap/extension-bold Patch
@tiptap/extension-bubble-menu Patch
@tiptap/extension-bullet-list Patch
@tiptap/extension-code-block-lowlight Patch
@tiptap/extension-code-block Patch
@tiptap/extension-code Patch
@tiptap/extension-collaboration-caret Patch
@tiptap/extension-collaboration Patch
@tiptap/extension-color Patch
@tiptap/extension-details Patch
@tiptap/extension-document Patch
@tiptap/extension-drag-handle-react Patch
@tiptap/extension-drag-handle-vue-2 Patch
@tiptap/extension-drag-handle-vue-3 Patch
@tiptap/extension-drag-handle Patch
@tiptap/extension-emoji Patch
@tiptap/extension-file-handler Patch
@tiptap/extension-floating-menu Patch
@tiptap/extension-font-family Patch
@tiptap/extension-heading Patch
@tiptap/extension-highlight Patch
@tiptap/extension-horizontal-rule Patch
@tiptap/extension-image Patch
@tiptap/extension-invisible-characters Patch
@tiptap/extension-italic Patch
@tiptap/extension-link Patch
@tiptap/extension-list Patch
@tiptap/extension-mathematics Patch
@tiptap/extension-mention Patch
@tiptap/extension-node-range Patch
@tiptap/extension-ordered-list Patch
@tiptap/extension-paragraph Patch
@tiptap/extension-strike Patch
@tiptap/extension-subscript Patch
@tiptap/extension-superscript Patch
@tiptap/extension-table-of-contents Patch
@tiptap/extension-table Patch
@tiptap/extension-text-align Patch
@tiptap/extension-text-style Patch
@tiptap/extension-text Patch
@tiptap/extension-twitch Patch
@tiptap/extension-typography Patch
@tiptap/extension-underline Patch
@tiptap/extension-unique-id Patch
@tiptap/extension-youtube Patch
@tiptap/extensions Patch
@tiptap/html Patch
@tiptap/markdown Patch
@tiptap/pm Patch
@tiptap/react Patch
@tiptap/static-renderer Patch
@tiptap/suggestion Patch
@tiptap/vue-2 Patch
@tiptap/vue-3 Patch
@tiptap/extension-character-count Patch
@tiptap/extension-dropcursor Patch
@tiptap/extension-focus Patch
@tiptap/extension-gapcursor Patch
@tiptap/extension-history Patch
@tiptap/extension-list-item Patch
@tiptap/extension-list-keymap Patch
@tiptap/extension-placeholder Patch
@tiptap/extension-table-cell Patch
@tiptap/extension-table-header Patch
@tiptap/extension-table-row Patch
@tiptap/extension-task-item Patch
@tiptap/extension-task-list Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented May 17, 2026

Copy link
Copy Markdown

Deploy Preview for tiptap-embed ready!

Name Link
🔨 Latest commit f711838
🔍 Latest deploy log https://app.netlify.com/projects/tiptap-embed/deploys/6a0a0fdea31bc80008a37e75
😎 Deploy Preview https://deploy-preview-7840--tiptap-embed.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@bdbch

bdbch commented Jul 16, 2026

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary

  • Preserve trailing hard breaks when pasting HTML between Tiptap editors.
  • Add a clipboard HTML helper that keeps trailing <br> elements during parsing.
  • Add tests and a patch changeset for the hard break extension.

Walkthrough

The hard-break extension now preserves trailing hard breaks when pasting Tiptap clipboard HTML. A regression test verifies round-trip parsing for multiple trailing breaks, and a patch changeset documents the fix.

Changes

Hard-break paste preservation

Layer / File(s) Summary
Preserve trailing breaks during paste
packages/extension-hard-break/src/hard-break.ts
Pasted ProseMirror slice HTML is processed to retain trailing hard-break elements before parsing.
Validate and release the paste behavior
packages/extension-hard-break/__tests__/hard-break.spec.ts, .changeset/clear-hard-break-paste.md
A clipboard round-trip test covers multiple trailing hard breaks, and a patch changeset records the behavior update.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: complexity: medium, impact: medium

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: preserving trailing hard breaks on paste.
Description check ✅ Passed The PR description covers the fix, implementation, tests, verification, and linked issue.
Linked Issues check ✅ Passed The changes address #5418 by preserving trailing hard breaks during paste, including copied ProseMirror HTML.
Out of Scope Changes check ✅ Passed The changeset and tests are directly related to the hard-break paste fix and do not look out of scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/extension-hard-break/src/hard-break.ts`:
- Line 8: Update the HTML parsing in the hard-break helper around DOMParser
usage to remain Trusted Types-safe: reuse ProseMirror’s existing clipboard
parsing path, or allow the helper to accept and pass through TrustedHTML.
Preserve the current parsed-document behavior while ensuring paste works when
Trusted Types enforcement is enabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 733f9d59-0fae-4540-ad53-6e977169fed4

📥 Commits

Reviewing files that changed from the base of the PR and between ec1afc3 and f711838.

📒 Files selected for processing (3)
  • .changeset/clear-hard-break-paste.md
  • packages/extension-hard-break/__tests__/hard-break.spec.ts
  • packages/extension-hard-break/src/hard-break.ts

return html
}

const document = new window.DOMParser().parseFromString(html, 'text/html')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

rg -n "trustedTypes|maybeWrapTrusted|DOMParser|parseFromClipboard" packages

Repository: ueberdosis/tiptap

Length of output: 3025


🏁 Script executed:

sed -n '1,220p' packages/extension-hard-break/src/hard-break.ts
printf '\n--- tests ---\n'
sed -n '1,220p' packages/extension-hard-break/__tests__/hard-break.spec.ts
printf '\n--- core elementFromString ---\n'
sed -n '1,200p' packages/core/src/utilities/elementFromString.ts
printf '\n--- pm view parseFromClipboard references in repo? ---\n'
rg -n "__parseFromClipboard|parseFromClipboard|trustedTypes|maybeWrapTrusted" packages/pm packages -g '!**/dist/**'

Repository: ueberdosis/tiptap

Length of output: 6186


🏁 Script executed:

git ls-files 'packages/pm/**' 'packages/extension-hard-break/**' | sed -n '1,200p'

Repository: ueberdosis/tiptap

Length of output: 928


🏁 Script executed:

git ls-files | rg '^packages/pm/'

Repository: ueberdosis/tiptap

Length of output: 607


🌐 Web query:

ProseMirror parseFromClipboard Trusted Types DOMParser parseFromString source

💡 Result:

ProseMirror handles Trusted Types through its internal clipboard parsing logic within prosemirror-view [1]. When an environment enforces a strict Content Security Policy (CSP) with require-trusted-types-for 'script', standard browser APIs like innerHTML and DOMParser.parseFromString() will throw a TypeError if they receive a raw string instead of a TrustedHTML object [2][3][4]. To address this, ProseMirror uses a helper function (often named maybeWrapTrusted in prosemirror-view/src/clipboard.ts) to manage interaction with the browser's Trusted Types API [1][5]. Key points regarding ProseMirror's implementation: 1. Automatic Policy Usage: ProseMirror attempts to use trustedTypes.defaultPolicy if it is defined by the application [5][6]. This is the recommended approach, as it allows developers to define their own sanitization logic (e.g., using DOMPurify) and ensures that all HTML passed to ProseMirror's internal parsing sinks is properly sanitized [5]. 2. Fallback Policy: If no default policy exists, ProseMirror has historically created its own policy named "ProseMirrorClipboard" [5][3]. This policy performs no sanitization (it effectively acts as a pass-through) to allow pasting to function in restrictive environments [5]. Security researchers have noted that this "ProseMirrorClipboard" policy can bypass CSP protections if misused, as it provides a trusted sink for arbitrary HTML [5]. 3. DOMParser and Sinks: Both Element.innerHTML and DOMParser.parseFromString() are considered injection sinks under Trusted Types [4]. When ProseMirror parses HTML content—whether from the clipboard or via DOMParser—it must satisfy the browser's requirement that these sinks receive TrustedHTML [2][4]. 4. Best Practices: To ensure both security and functionality, you should define a trustedTypes.defaultPolicy in your application before the editor initializes [5]. This policy should ideally implement robust sanitization to prevent XSS attacks while allowing the necessary HTML structures required by your schema [5][7]. By defining a global defaultPolicy, you override the need for ProseMirror to create its own internal policy, satisfying the browser's security requirements while maintaining control over how HTML is processed [5][6].

Citations:


Keep this HTML parse Trusted Types-safe. DOMParser.parseFromString(html, 'text/html') is a Trusted Types sink, so paste can fail under require-trusted-types-for 'script'. Reuse the same clipboard parsing path ProseMirror uses, or make this helper accept TrustedHTML too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension-hard-break/src/hard-break.ts` at line 8, Update the HTML
parsing in the hard-break helper around DOMParser usage to remain Trusted
Types-safe: reuse ProseMirror’s existing clipboard parsing path, or allow the
helper to accept and pass through TrustedHTML. Preserve the current
parsed-document behavior while ensuring paste works when Trusted Types
enforcement is enabled.

Source: MCP tools

@bdbch
bdbch requested a review from a team as a code owner July 30, 2026 09:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Copied paragraph with several <br> pasting into editor without last <br>

2 participants