Skip to content

[lexical][lexical-html] Bug Fix: gate trailing <br> import drop on managed line breaks#8765

Draft
potatowagon wants to merge 2 commits into
facebook:mainfrom
potatowagon:jv-managed-linebreak-import
Draft

[lexical][lexical-html] Bug Fix: gate trailing <br> import drop on managed line breaks#8765
potatowagon wants to merge 2 commits into
facebook:mainfrom
potatowagon:jv-managed-linebreak-import

Conversation

@potatowagon

@potatowagon potatowagon commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Description

Follow-up to #8750, implementing @etrepum's marker approach from #7600 (review 2893090447): mark Lexical's reconciler-injected terminating <br> (data-lexical-managed-linebreak="true", already set in LexicalDOMSlot.setManagedLineBreak) so importDOM can drop that artifact while preserving a real, unmarked trailing break.

Decision: lossless round-trip is the priority for an unmarked trailing <br>, over matching the browser's non-rendering of a trailing block <br>. This PR therefore preserves unmarked trailing breaks and drops only the marked artifacts. The known cost is documented below.

The core conflict

A trailing <br> as the last child of a block is non-rendering in HTML (browsers show nothing for it). So the same DOM shape <p>2<br></p> means two different things depending on origin:

  • Browser/paste artifact → ideally dropped on import (match browser rendering; no phantom blank line).
  • Authored/intentional break → must be preserved on import (so export → import is lossless).

An ordinary authored <br> carries no marker to distinguish these, so an import rule must pick one policy for unmarked trailing breaks — you cannot have both for the same input. We choose preserve (round-trip fidelity).

Behavior of each approach on a trailing block <br>

Verified empirically (drop = <br> removed on import):

Input (last child <br> of a block) #6395 (original) #8750 (Apple-only) #8765 (this PR)
external / unmarked authored1<p>2<br /></p>3 drop → 1/2/3 keep → blank line keep → blank line
Lexical managed artifact<br data-lexical-managed-linebreak> drop keep → round-trip wrong drop → round-trip correct
Apple clipboard<br class="Apple-interchange-newline"> drop drop drop

Note: #8765 is strictly stronger than #8750 for round-trip#8750 keys only on the Apple class, so it does not drop Lexical's own data-lexical-managed-linebreak artifact and thus fails to fix the round-trip it targeted. #8765 drops that artifact correctly.

Why this is complete for round-trip (no exportDOM change needed)

LineBreakNode has no exportDOM override; the default emits a single unmarked <br>. With the "preserve unmarked" policy, both export paths round-trip losslessly:

Export: no change required

exportDOM is intentionally left untouched. The "preserve unmarked" import policy makes the export-side doubling that #7600 introduced (emitting N+1 <br> in exportDOM) unnecessary — a single unmarked <br> round-trips as-is:

Trailing LineBreakNodes exportDOM HTML re-import result
1 <p>2<br></p> 1 (last-child <br> unmarked → preserved)
2 <p>2<br><br></p> 2 (only the last is a "last child"; unmarked → preserved)
3 <p>2<br><br><br></p> 3
empty block <p><br></p> empty (only-child → dropped, unchanged)

Verified against the $generateHtmlFromNodes$generateNodesFromDOM path and the existing block-heavy snapshots (135 lexical-list + lexical-history tests pass, incl. list items / checkboxes / nested blocks). We deliberately do not stamp data-lexical-managed-linebreak on exportDOM output — that would be the #7600 export direction @etrepum preferred to avoid; round-trip works purely by preservation instead.

Known cost

External HTML with an unmarked trailing <br> (e.g. 1<p>2<br /></p>3) is now preserved and renders a blank line, where a browser would render nothing. This is the accepted trade for lossless round-trip. Matching browser rendering here would require reverting to #6395's drop-on-import and moving the round-trip fix to exportDOM (emit N+1 <br>, #7600-style) — the direction @etrepum preferred not to take.

What this PR does

Drop a trailing <br> on import only when it is a managed line break:

  • data-lexical-managed-linebreak="true" (Lexical's reconciler artifact), or
  • class="Apple-interchange-newline" (Safari clipboard artifact).

Applied to both LineBreakNode.importDOM and the @lexical/html coreImportRules LineBreakRule; isManagedLineBreak is exported from lexical so both share it. The pre-existing isManagedLineBreak in LexicalMutations (which detects an editor-managed live-DOM break) is renamed to isEditorManagedLineBreak to avoid the name collision.

cc @etrepum @levensta

Test plan

vitest --project unit passes for the affected files. Coverage in LexicalEventHelpers.test.tsx:

  • unmarked authored trailing <br> is preserved (1<p>2<br /></p>3) — documents the accepted cost
  • managed trailing <br> (data-lexical-managed-linebreak) is ignored
  • Apple-interchange trailing <br> is ignored
✓ packages/lexical-utils/.../LexicalEventHelpers.test.tsx
✓ packages/lexical-html (87 tests)

…naged line breaks

Follow-up to facebook#8750 implementing @etrepum's approach from facebook#7600.

facebook#6395 dropped every trailing <br> in a block on import to match HTML's
non-rendering behavior, but that loses authored trailing breaks on
round-trip serialization. facebook#8750 narrowed the drop to Safari's
Apple-interchange-newline clipboard artifact, but that gate doesn't fire
for ordinary structural/authored trailing breaks (or non-Safari pastes),
so the phantom-blank-line regression persists for those.

Instead, drop a trailing <br> only when it is a *managed* line break:
- data-lexical-managed-linebreak="true": the terminator Lexical's
  reconciler already injects via LexicalDOMSlot.setManagedLineBreak, so
  re-importing Lexical's own exported HTML stays idempotent.
- class="Apple-interchange-newline": Safari's clipboard transport artifact.

An ordinary authored trailing <br> carries neither marker and is now
preserved, so round-trip serialization to HTML is lossless while
rendering of external pasted HTML stays faithful. Browser-agnostic for
the Lexical-managed case.

Applies the same gate to both LineBreakNode.importDOM and the
@lexical/html coreImportRules LineBreakRule. Adds test coverage for the
preserved / managed / Apple-interchange cases.
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview Jul 3, 2026 7:58am
lexical-playground Ready Ready Preview Jul 3, 2026 7:58am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 30, 2026
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Jun 30, 2026
* An ordinary authored trailing `<br>` carries neither marker and is
* preserved so that round-trip serialization to HTML stays lossless.
*/
export function isManagedLineBreak(node: Node): boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A different name for this probably makes sense, or we should rename the one in LexicalMutations (maybe isEditorManagedLineBreak). They don't work the same way, so it may cause confusion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — renamed the existing LexicalMutations helper to isEditorManagedLineBreak (68c830d) to avoid the collision. Added cross-referencing comments on both so the distinction is clear: the mutations one identifies a break the editor is actively managing in the live DOM (via __lexicalLineBreak / node key), while the new one inspects serialized markup (data-lexical-managed-linebreak / Apple-interchange class) to decide whether a parsed <br> should be dropped on import.

Addresses review feedback: the new isManagedLineBreak in
LexicalLineBreakNode (inspects serialized markup — the
data-lexical-managed-linebreak attribute / Apple-interchange class to
decide whether to drop a parsed <br> on import) collided by name with
the pre-existing isManagedLineBreak in LexicalMutations (identifies a
line break the editor is actively managing in the live DOM via the
__lexicalLineBreak slot reference / node key). They work differently, so
the mutations one is renamed to isEditorManagedLineBreak and both now
carry cross-referencing comments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants