docs/solutions/best-practices/2026-05-09-lexical-htmlcopy-harvest-rows-need-whitespace-and-empty-target-boundaries.md
Lexical's HTMLCopyAndPaste.spec.mjs looks like one clipboard file, but the
portable rows split into separate owners: normal HTML whitespace import,
code-source import, and HR/block-void insertion policy.
<p> nodes became empty paragraphs.tx.fragment.insert removed the empty paste target but
regressed source-code and nested-list paste behavior.tx.nodes.insert with tx.fragment.insert for the whole example
importer. That broke existing accepted rows for source-code HTML and nested
list import.Keep the insertion path that preserves the existing corpus, and normalize the HTML importer at the narrow boundary:
if (el.nodeType === 3) {
return normalizeTextNode(el)
}
normalizeTextNode strips raw newline characters from normal HTML text nodes
while preserving explicit code/pre whitespace. normalizeBodyFragment drops
top-level whitespace-only text nodes. After tx.nodes.insert(fragment), the
example removes only a leading empty text block when the pasted fragment
contains top-level blocks.
The browser proof should cover both accepted rows:
<b> / <i>;<code data-language>... ...</code> imports as a code block through the
existing source-code HTML corpus.The copied invariant is not "replace Slate paste with Lexical's paste model." It is "normal HTML source newlines are layout noise unless they come from an explicit break or preserved-whitespace context."
Keeping tx.nodes.insert avoids destabilizing list/table/code import behavior.
The targeted empty-target cleanup fixes native block-fragment paste without
claiming a new generic fragment insertion law.
, code/pre whitespace, and
decorator/block-void insertion.paste-html-import.ts, run the focused row and the full
paste-html.test.ts browser file.tx.fragment.insert as a separate policy change that must pass the
whole paste-html corpus before it can replace tx.nodes.insert.