Skip to content

fix(core): preserve list item type when pasting into empty list items#2722

Merged
nperez0111 merged 4 commits into
mainfrom
feat/fix-issue-2330
May 13, 2026
Merged

fix(core): preserve list item type when pasting into empty list items#2722
nperez0111 merged 4 commits into
mainfrom
feat/fix-issue-2330

Conversation

@nperez0111
Copy link
Copy Markdown
Contributor

@nperez0111 nperez0111 commented May 7, 2026

Summary

Fixes #2330. Pasting plain text (or a paragraph) into an empty bullet/numbered/check list item used to replace the list item with a paragraph; it now keeps its type and absorbs the pasted inline content. Also fixes bare <li>a</li><li>b</li> HTML pastes that previously produced a list item followed by a paragraph instead of two list items.

Rationale

BlockNote's editor.pasteHTML round-trips through the BlockNote HTML serializer, producing a closed slice (blockGroup > blockContainer > paragraph) that ProseMirror inserts as a new block — walking up the document and replacing the surrounding container. That's surprising when the user is just pasting text into an empty list item. Separately, the BulletListItem parse rule requires a <ul>/<ol> parent, so orphan <li> HTML fell back to paragraph parsing.

Changes

  • packages/core/src/editor/transformPasted.tsretypeLeadingParagraphForEmptyTarget retypes the slice's leading paragraph to match an empty, non-paragraph, inline-content target block (list items, headings, custom inline-content blocks). Subsequent blocks are kept as-is and become siblings. Non-paragraph leading blocks (heading, list item) keep the existing replace behavior.
  • packages/core/src/api/parsers/html/util/nestedLists.ts — new wrapOrphanListItems step wraps consecutive <li> siblings with no <ul>/<ol> ancestor in a fresh <ul> before list-lifting runs.

Impact

Scoped: only fires when the target is an empty inline-content block whose type isn't paragraph and the slice's leading block is a paragraph. Pasting into non-empty blocks, into paragraphs, or pasting non-paragraph leading blocks (heading, list item, table) is unchanged. The orphan-<li> wrap only affects HTML where <li> has no list ancestor.

Testing

  • New packages/core/src/editor/transformPasted.test.ts (15 tests) drives the actual paste path via editor.pasteHTML for: paragraph into empty bullet/numbered/check list items, paragraphs with marks, multi-paragraph paste, heading paste, heading + paragraph paste, list item paste, nested list paste, bare <li> paste, two list items into empty/non-empty list items, and regression cases for non-empty list items and paragraphs.
  • New snapshot tests in packages/core/src/api/parsers/html/util/nestedLists.test.ts cover orphan <li> wrapping and confirm <li>s already inside <ul> are left alone.
  • All existing core (441) and tests-package (844) tests pass.

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved pasting behavior for list items and other content into empty list items, with better inline content preservation and merging.
    • Enhanced parsing of standalone list items pasted without a containing list wrapper.
  • Tests

    • Added comprehensive test coverage for paste transformation across various content types and scenarios.

Review Change Stack

…tent blocks

Fixes #2330

Pasting plain text or a paragraph into an empty bullet/numbered/check list
item replaced the list item with a paragraph because BlockNote's serializer
wraps content in `blockGroup > blockContainer > paragraph`, producing a
closed slice that ProseMirror inserts as a new block rather than splicing
inline. `transformPasted` now retypes the leading paragraph in such a slice
to match the empty target block, so the list item keeps its type and any
trailing blocks become siblings.

Also fixes bare `<li>a</li><li>b</li>` HTML parsing: the BulletListItem
parse rule requires a `<ul>`/`<ol>` parent, so orphan `<li>`s used to fall
back to paragraphs. `nestedListsToBlockNoteStructure` now wraps consecutive
orphan `<li>` siblings in a fresh `<ul>` before parsing.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview May 12, 2026 5:26pm
blocknote-website Ready Ready Preview May 12, 2026 5:26pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49170771-1c98-4032-b183-72acaad3b41d

📥 Commits

Reviewing files that changed from the base of the PR and between 38743b4 and e624218.

⛔ Files ignored due to path filters (1)
  • tests/src/unit/core/clipboard/paste/__snapshots__/text/html/pasteHTMLWithMultipleCheckboxesInTableCell.json is excluded by !**/__snapshots__/**
📒 Files selected for processing (1)
  • packages/core/src/blocks/ListItem/BulletListItem/block.ts

📝 Walkthrough

Walkthrough

The pull request fixes a paste handling issue where empty list items were incorrectly replaced with paragraphs when pasting content. The fix comprises orphan <li> element parsing support, a new retyping mechanism in the paste handler to preserve target block types, and extensive test coverage for paste scenarios.

Changes

Empty List Item Paste Preservation

Layer / File(s) Summary
Orphan list item parsing
packages/core/src/blocks/ListItem/BulletListItem/block.ts
BulletListItem HTML parse logic now recognizes <li> elements lacking <ul>/<ol> ancestors, treating them as list items instead of rejecting them.
Paste retyping integration
packages/core/src/editor/transformPasted.ts
New retypeLeadingParagraphForEmptyTarget helper is invoked early in transformPasted to replace a pasted leading paragraph with the target block's type when the target is empty, non-paragraph, and inline-content, preserving inline markup.
Paste behavior test suite
packages/core/src/editor/transformPasted.test.ts
Mount-and-paste tests covering empty and non-empty list items, headings, bare <li> elements, nested lists, inline-mark preservation, multi-paragraph insertion, and paragraph-paste regression checks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through pasted lists,
No more vanishing acts in empty gists!
Orphans find homes, types stay true,
From <li> tags to items shiny new. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main fix: preserving list item type when pasting into empty list items, which is the core objective of the pull request.
Description check ✅ Passed The description follows the template structure with Summary, Rationale, Changes, Impact, Testing, and Checklist sections. All major sections are completed with sufficient detail.
Linked Issues check ✅ Passed The PR successfully addresses the primary objective from #2330: preserving list item type when pasting text/paragraphs into empty list items. The implementation through retypeLeadingParagraphForEmptyTarget matches orphan <li> HTML to parse as list items, meeting the issue's requirements.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue #2330: transformPasted.ts adds the retyping logic, BulletListItem parse rule handles orphan <li> elements, and new tests validate the changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fix-issue-2330

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/core/src/api/parsers/html/util/nestedLists.test.ts (1)

146-169: ⚡ Quick win

Add a regression test for non-whitespace separators between orphan <li> nodes.

Current cases miss <li>a</li>text<li>b</li>, which should not be merged into one wrapped list.

🤖 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/core/src/api/parsers/html/util/nestedLists.test.ts` around lines 146
- 169, Add a regression test in nestedLists.test.ts that ensures non-whitespace
separators prevent orphan <li> nodes from being merged: create an it(...) (e.g.,
"Does not merge <li> nodes separated by non-whitespace text") that calls
testHTML with the HTML string `<li>a</li>text<li>b</li>` and asserts the output
does NOT wrap both <li> elements into one <ul>; use the existing test pattern
(the testHTML helper) and mirror surrounding test style so the new case
integrates with other cases like "Wraps consecutive bare <li> elements in a
<ul>" and "Wraps bare <li>s mixed with other top-level content".
🤖 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/core/src/api/parsers/html/util/nestedLists.ts`:
- Around line 34-38: The grouping loop uses nextElementSibling which skips text
nodes and therefore merges <li> across meaningful text; change the iteration to
use nextSibling (start from orphan.nextSibling) and in the while loop stop if
you encounter a text node with non-whitespace content (nodeType ===
Node.TEXT_NODE && node.textContent.trim() !== '') or any node that is not an LI
element; only treat nodes as continued list items when the node is an Element
with tagName "LI" and orphanSet.has(node as HTMLElement). Update variables
referenced (next, orphan, orphanSet, group, handled) accordingly so grouping
only proceeds across whitespace text nodes but not across meaningful text.

In `@packages/core/src/editor/transformPasted.ts`:
- Around line 198-242: The helper retypeLeadingParagraphForEmptyTarget is called
unconditionally from transformPasted using
getBlockInfoFromSelection(view.state), which is unsafe for drop operations whose
insertion target may differ from the selection; update transformPasted to only
call retypeLeadingParagraphForEmptyTarget for paste flows (check transaction
metadata similar to shouldApplyFix) or add a target-equivalence check before
invoking retypeLeadingParagraphForEmptyTarget (compare drop insertion point
target vs selection-derived target), and add a regression test covering drops
into empty list-item targets to ensure correctness.

---

Nitpick comments:
In `@packages/core/src/api/parsers/html/util/nestedLists.test.ts`:
- Around line 146-169: Add a regression test in nestedLists.test.ts that ensures
non-whitespace separators prevent orphan <li> nodes from being merged: create an
it(...) (e.g., "Does not merge <li> nodes separated by non-whitespace text")
that calls testHTML with the HTML string `<li>a</li>text<li>b</li>` and asserts
the output does NOT wrap both <li> elements into one <ul>; use the existing test
pattern (the testHTML helper) and mirror surrounding test style so the new case
integrates with other cases like "Wraps consecutive bare <li> elements in a
<ul>" and "Wraps bare <li>s mixed with other top-level content".
🪄 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

Run ID: 421fd044-3488-4c9a-9d23-9a5f085b1b7e

📥 Commits

Reviewing files that changed from the base of the PR and between 1b53232 and 73e7a02.

⛔ Files ignored due to path filters (1)
  • packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
📒 Files selected for processing (4)
  • packages/core/src/api/parsers/html/util/nestedLists.test.ts
  • packages/core/src/api/parsers/html/util/nestedLists.ts
  • packages/core/src/editor/transformPasted.test.ts
  • packages/core/src/editor/transformPasted.ts

Comment thread packages/core/src/api/parsers/html/util/nestedLists.ts Outdated
Comment thread packages/core/src/editor/transformPasted.ts
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 7, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2722

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2722

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2722

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2722

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2722

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2722

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2722

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2722

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2722

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2722

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2722

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2722

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2722

commit: e624218

- nestedLists: walk siblings via nextSibling so meaningful (non-whitespace)
  text between bare <li>s prevents them from being merged into one <ul>.
  Whitespace text nodes still bridge consecutive orphans.
- transformPasted: bail out of retypeLeadingParagraphForEmptyTarget during
  drop events (view.dragging is set), since the slice is inserted at the
  drop point rather than the current selection.
@matthewlipski
Copy link
Copy Markdown
Collaborator

My main question regarding this is why not set the isolating attribute to true for list items instead? This is something we do by default in createBlockSpec and explicitly disable for default blocks, and has the effect of retaining the existing block type regardless of what content is pasted into it. I assume it's because:

  1. When pasting a non-paragraph (heading, quote, etc) into an empty list item, we actually do want to override the list item type to the type of the pasted content. We only don't want to do that for paragraphs because they are the "vanilla" block.
  2. That doesn't address the other issue fixed in this PR, i.e. handling pasting of consecutive bar <li> elements.

Could you comment on this?

@nperez0111
Copy link
Copy Markdown
Contributor Author

My main question regarding this is why not set the isolating attribute to true for list items instead? This is something we do by default in createBlockSpec and explicitly disable for default blocks, and has the effect of retaining the existing block type regardless of what content is pasted into it. I assume it's because:

1. When pasting a non-paragraph (heading, quote, etc) into an empty list item, we actually do want to override the list item type to the type of the pasted content. We only don't want to do that for paragraphs because they are the "vanilla" block.

2. That doesn't address the other issue fixed in this PR, i.e. handling pasting of consecutive bar `<li>` elements.

Could you comment on this?

Good question. Your hunch is right that we kind of diverge from prosemirror semantics (because our HTML structure is different) so a <p> being pasted into an <li> wouldn't just work right away. As for 2, I found that this behave unexpectedly when pasting something that was just <li>A</li><li>B</li>as opposed to being wrapped in a <ul> or <ol>. They weren't being matched as a list item even though they were.

Now that I mention it though, maybe I can just have the bullet list item parse match if it is an li without a parent? I'll give that a try

…tent blocks

Match orphan `<li>` (no `<ul>`/`<ol>` ancestor) as a `bulletListItem` in
the parse rule, so pasting bare `<li>a</li><li>b</li>` HTML produces two
list items instead of falling through to paragraphs. Replaces the
previous `wrapOrphanListItems` HTML preprocessing step.
@nperez0111
Copy link
Copy Markdown
Contributor Author

Yep, that worked better I think!

@nperez0111 nperez0111 merged commit 932e3ab into main May 13, 2026
22 of 23 checks passed
@nperez0111 nperez0111 deleted the feat/fix-issue-2330 branch May 13, 2026 08:46
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.

pasteHandler doesn't work correctly for list items when their content is empty prior to pasting text.

2 participants