From 310ad650947676a4a7b93aa65a4ff8b920dfd151 Mon Sep 17 00:00:00 2001 From: Jack Duvall <10897431+duvallj@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:29:27 -0500 Subject: [PATCH 1/3] wip: add new test case --- .../src/__tests__/unit/HTMLCopyAndPaste.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts b/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts index 8f5364de494..f91110b9d77 100644 --- a/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts +++ b/packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts @@ -104,6 +104,17 @@ describe('HTMLCopyAndPaste tests', () => { pastedHTML: `hello`, plainTextInsert: ' world', }, + { + expectedHTML: `
`, + name: 'invalid list node correction', + pastedHTML: ` +
+
  • Item A
  • +
  • Item B
  • +
  • Item C
  • +
    + `, + }, ]; HTML_COPY_PASTING_TESTS.forEach((testCase, i) => { From a44ca847e40805bc5a5211c0b58603870c4395a2 Mon Sep 17 00:00:00 2001 From: Jack Duvall <10897431+duvallj@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:29:47 -0500 Subject: [PATCH 2/3] wip: add new invariant --- packages/lexical/src/LexicalNode.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/lexical/src/LexicalNode.ts b/packages/lexical/src/LexicalNode.ts index 8d5ea7e143f..1f45b5f124e 100644 --- a/packages/lexical/src/LexicalNode.ts +++ b/packages/lexical/src/LexicalNode.ts @@ -1328,6 +1328,18 @@ export class LexicalNode { writableNodeToInsert.__next = nextKey; writableNodeToInsert.__prev = writableSelf.__key; writableNodeToInsert.__parent = writableSelf.__parent; + // Check invariant: if node requires a parent, verify it has the correct parent type + if (writableNodeToInsert.isParentRequired()) { + const requiredParentType = + writableNodeToInsert.createParentElementNode().constructor; + invariant( + writableParent instanceof requiredParentType, + 'insertAfter: node %s requires a parent of type %s, but got %s', + writableNodeToInsert.constructor.name, + requiredParentType.name, + writableParent.constructor.name, + ); + } if (restoreSelection && $isRangeSelection(selection)) { const index = this.getIndexWithinParent(); $updateElementSelectionOnCreateDeleteNode( @@ -1379,6 +1391,18 @@ export class LexicalNode { writableNodeToInsert.__prev = prevKey; writableNodeToInsert.__next = writableSelf.__key; writableNodeToInsert.__parent = writableSelf.__parent; + // Check invariant: if node requires a parent, verify it has the correct parent type + if (writableNodeToInsert.isParentRequired()) { + const requiredParentType = + writableNodeToInsert.createParentElementNode().constructor; + invariant( + writableParent instanceof requiredParentType, + 'insertAfter: node %s requires a parent of type %s, but got %s', + writableNodeToInsert.constructor.name, + requiredParentType.name, + writableParent.constructor.name, + ); + } const selection = $getSelection(); if (restoreSelection && $isRangeSelection(selection)) { const parent = this.getParentOrThrow(); From 12644c5cfe8594e2ea8ec50b10afa40ff0796ec0 Mon Sep 17 00:00:00 2001 From: Jack Duvall <10897431+duvallj@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:54:04 -0500 Subject: [PATCH 3/3] pass test with new logic --- packages/lexical-html/src/index.ts | 43 +++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/lexical-html/src/index.ts b/packages/lexical-html/src/index.ts index fc31fbb8021..f24f4e5cfaf 100644 --- a/packages/lexical-html/src/index.ts +++ b/packages/lexical-html/src/index.ts @@ -309,9 +309,46 @@ function $createNodesFromDOM( } } else { if ($isElementNode(currentLexicalNode)) { - // If the current node is a ElementNode after conversion, - // we can append all the children to it. - currentLexicalNode.append(...childLexicalNodes); + // If the current node is a ElementNode after conversion, we should be able to append all the + // children to it. However, we still need to ensure that children requiring specific parents + // have the correct parent type. Group children by their required parent type. + let requiredParent: ElementNode | null = null; + for (const child of childLexicalNodes) { + // console.log(requiredParent, child, child.isParentRequired()); + const desiredParent = child.createParentElementNode(); + if ( + child.isParentRequired() && + !(currentLexicalNode instanceof desiredParent.constructor) + ) { + if ( + requiredParent === null || + !(requiredParent instanceof desiredParent.constructor) + ) { + // CASE 1: We have a child that needs a parent, but the existing parent either doesn't + // exist or can't house this child. So, create a new parent to house this child. + requiredParent = desiredParent; + requiredParent.append(child); + // Typescript gets a little confused here, so help it along + (currentLexicalNode as ElementNode).append(requiredParent); + } else { + // CASE 2: We have a child in need of a parent, and the parent we've already been + // adding to can do the job. + requiredParent.append(child); + } + } else { + if (requiredParent === null) { + // CASE 3: We have a child that doesn't need a parent, and no existing parent, so we can + // add like normal. + currentLexicalNode.append(child); + } else { + // CASE 4: We have a child that doesn't need a parent, but there is an existing parent. + // To make it so that children always appear in the correct order, we should remove the + // parent before continuing. + requiredParent = null; + currentLexicalNode.append(child); + } + } + } } }