Skip to content

Commit e624218

Browse files
committed
fix(core): preserve list item type when pasting into empty inline-content 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.
1 parent 38743b4 commit e624218

5 files changed

Lines changed: 9 additions & 104 deletions

File tree

packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`Lift nested lists > Does not merge <li> nodes separated by non-whitespace text 1`] = `"<ul><li>a</li></ul>text<ul><li>b</li></ul>"`;
4-
5-
exports[`Lift nested lists > Leaves <li>s already inside a <ul> alone 1`] = `"<ul><li>existing</li></ul><ul><li>orphan</li></ul>"`;
6-
73
exports[`Lift nested lists > Lifts multiple bullet lists 1`] = `
84
"<ul>
95
<div><li>
@@ -146,11 +142,3 @@ exports[`Lift nested lists > Lifts nested numbered lists 1`] = `
146142
</li>
147143
</ol>"
148144
`;
149-
150-
exports[`Lift nested lists > Wraps a single bare <li> in a <ul> 1`] = `"<ul><li>only</li></ul>"`;
151-
152-
exports[`Lift nested lists > Wraps bare <li>s mixed with other top-level content 1`] = `"<p>before</p><ul><li>x</li><li>y</li></ul><p>after</p>"`;
153-
154-
exports[`Lift nested lists > Wraps consecutive bare <li> elements in a <ul> 1`] = `"<ul><li>a</li><li>b</li></ul>"`;
155-
156-
exports[`Lift nested lists > Wraps nested orphan <li>s as inner lists 1`] = `"<ul><li>outer</li><li>inner</li></ul>"`;

packages/core/src/api/parsers/html/util/nestedLists.test.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -143,36 +143,6 @@ describe("Lift nested lists", () => {
143143
await testHTML(html);
144144
});
145145

146-
it("Wraps consecutive bare <li> elements in a <ul>", async () => {
147-
const html = `<li>a</li><li>b</li>`;
148-
await testHTML(html);
149-
});
150-
151-
it("Wraps a single bare <li> in a <ul>", async () => {
152-
const html = `<li>only</li>`;
153-
await testHTML(html);
154-
});
155-
156-
it("Wraps bare <li>s mixed with other top-level content", async () => {
157-
const html = `<p>before</p><li>x</li><li>y</li><p>after</p>`;
158-
await testHTML(html);
159-
});
160-
161-
it("Leaves <li>s already inside a <ul> alone", async () => {
162-
const html = `<ul><li>existing</li></ul><li>orphan</li>`;
163-
await testHTML(html);
164-
});
165-
166-
it("Does not merge <li> nodes separated by non-whitespace text", async () => {
167-
const html = `<li>a</li>text<li>b</li>`;
168-
await testHTML(html);
169-
});
170-
171-
it("Wraps nested orphan <li>s as inner lists", async () => {
172-
const html = `<li>outer<li>inner</li></li>`;
173-
await testHTML(html);
174-
});
175-
176146
it("Lifts nested mixed lists", async () => {
177147
const html = `<ol>
178148
<li>

packages/core/src/api/parsers/html/util/nestedLists.ts

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,6 @@ function isWhitespaceNode(node: Node) {
66
return node.nodeType === 3 && !/\S/.test(node.nodeValue || "");
77
}
88

9-
/**
10-
* Step 0, wraps any `<li>` element that is not inside a `<ul>`/`<ol>` in a
11-
* fresh `<ul>` so the existing parse rules (which require an `<ul>`/`<ol>`
12-
* parent) match. Consecutive orphan `<li>` siblings are grouped under a
13-
* single `<ul>`.
14-
*
15-
* Without this, pasting bare `<li>a</li><li>b</li>` HTML would parse as two
16-
* paragraphs because the BulletListItem parse rule only matches `<li>`
17-
* whose parent is `<ul>`.
18-
*/
19-
function wrapOrphanListItems(element: HTMLElement) {
20-
const orphans = Array.from(element.querySelectorAll("li")).filter(
21-
(li) => li.closest("ul, ol") === null,
22-
);
23-
const orphanSet: Set<Element> = new Set(orphans);
24-
const handled = new Set<Element>();
25-
26-
for (const orphan of orphans) {
27-
if (handled.has(orphan)) {
28-
continue;
29-
}
30-
31-
const group: Element[] = [orphan];
32-
handled.add(orphan);
33-
34-
// Walk siblings via nextSibling (not nextElementSibling) so we can stop
35-
// at meaningful text between orphans — only whitespace text is allowed
36-
// to bridge two orphan <li>s into the same <ul>.
37-
let next: Node | null = orphan.nextSibling;
38-
while (next) {
39-
if (isWhitespaceNode(next)) {
40-
next = next.nextSibling;
41-
continue;
42-
}
43-
if (
44-
next.nodeType === 1 &&
45-
(next as Element).tagName === "LI" &&
46-
orphanSet.has(next as Element)
47-
) {
48-
group.push(next as Element);
49-
handled.add(next as Element);
50-
next = next.nextSibling;
51-
continue;
52-
}
53-
break;
54-
}
55-
56-
const ul = orphan.ownerDocument.createElement("ul");
57-
orphan.parentNode!.insertBefore(ul, orphan);
58-
for (const li of group) {
59-
ul.appendChild(li);
60-
}
61-
}
62-
}
63-
649
/**
6510
* Step 1, Turns:
6611
*
@@ -172,7 +117,6 @@ export function nestedListsToBlockNoteStructure(
172117
element.innerHTML = elementOrHTML;
173118
elementOrHTML = element;
174119
}
175-
wrapOrphanListItems(elementOrHTML);
176120
liftNestedListsToParent(elementOrHTML);
177121
createGroups(elementOrHTML);
178122
return elementOrHTML;

packages/core/src/blocks/ListItem/BulletListItem/block.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,20 @@ export const createBulletListItemBlockSpec = createBlockSpec(
3737

3838
const parent = element.parentElement;
3939

40-
if (parent === null) {
41-
return undefined;
42-
}
43-
4440
if (
41+
parent === null ||
4542
parent.tagName === "UL" ||
4643
(parent.tagName === "DIV" && parent.parentElement?.tagName === "UL")
4744
) {
4845
return parseDefaultProps(element);
4946
}
5047

48+
// Orphan `<li>` (no <ul>/<ol> ancestor) — match as bulletListItem so
49+
// pasting bare `<li>` HTML doesn't fall back to a paragraph.
50+
if (!element.closest("ul, ol")) {
51+
return parseDefaultProps(element);
52+
}
53+
5154
return undefined;
5255
},
5356
// As `li` elements can contain multiple paragraphs, we need to merge their contents

tests/src/unit/core/clipboard/paste/__snapshots__/text/html/pasteHTMLWithMultipleCheckboxesInTableCell.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
{
1717
"styles": {},
1818
"text": "Cell 1ABC
19-
Unit tests covering the new feature have been added.
20-
All existing tests pass.",
19+
Unit tests covering the new feature have been added.
20+
All existing tests pass.",
2121
"type": "text",
2222
},
2323
],

0 commit comments

Comments
 (0)