Skip to content

[lexical][lexical-html] Bug Fix: Preserve the user's last <br> in the block#8750

Open
levensta wants to merge 1 commit into
facebook:mainfrom
levensta:preserve-last-br
Open

[lexical][lexical-html] Bug Fix: Preserve the user's last <br> in the block#8750
levensta wants to merge 1 commit into
facebook:mainfrom
levensta:preserve-last-br

Conversation

@levensta

Copy link
Copy Markdown
Contributor

Description

This PR partially reverts the change made in that #6395

The problem is that if a user intentionally inserts a line break at the end of a block, it must be preserved for round-trip serialization to HTML to work correctly. Otherwise, the br tag is lost, and it is impossible to insert an additional “indent” between two blocks. The only exception is the special line break <br class="Apple-interchange-newline"> which Safari inserts when copying to the clipboard

Test plan

Before

1<p>2<br /></p>3

<p><span>1</span></p>
<p><span>2</span></p>
<p><span>3</span></p>

After

1<p>2<br /></p>3

<p><span>1</span></p>
<p><span>2</span><br></p>
<p><span>3</span></p>
@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 26, 2026
@vercel

vercel Bot commented Jun 26, 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 Jun 26, 2026 1:40am
lexical-playground Ready Ready Preview Jun 26, 2026 1:40am

Request Review

@levensta

Copy link
Copy Markdown
Contributor Author

It seems the recommendation to ignore the last br tag has been taken too literally. In standard HTML, a br tag at the end of the content is indeed meaningless, since it isn’t even visible. But if there’s another block after the br, then the br needs to be preserved so that it’s correctly imported into the LineBreakNode in the lexical layer, since a user might want to intentionally create spacing between blocks.

@potatowagon I might be missing something, so I’d like to know your opinion since you’re the author of the original PR #6395

@potatowagon potatowagon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed by Navi (Tater Thoughts Bobblehead) on behalf of @potatowagon.

Summary

This PR partially reverts #6395 to preserve intentional trailing <br> tags in block nodes during HTML import, while still stripping Safari's <br class="Apple-interchange-newline"> artifacts. The logic change is narrow and well-reasoned.

What I checked

  1. Logic correctness: The condition change from isLastChildInBlockNode(el)isAppleInterchangeNewline(el) && isLastChildInBlockNode(el) is correct. Only Apple-interchange newlines (Safari clipboard artifacts) are now stripped when they're the last child in a block. Regular trailing <br> is preserved for round-trip fidelity.

  2. New export (isAppleInterchangeNewline): Clean implementation — checks node.nodeName === "BR", confirms it's an HTMLElement, then matches the class attribute. No risk of false positives.

  3. Consistency: The change is applied in both LineBreakNode.importDOM() and coreImportRules.ts (the LineBreakRule), which is correct since both paths handle <br> import.

  4. Test coverage: Test updated to expect the new behavior (last br in a block node is preserved). The expected HTML output now includes the <br> followed by <br data-lexical-managed-linebreak="true">, which is the correct Lexical representation.

  5. CI status: All green — core tests (Node 22 + 24), browser tests, integrity checks, e2e canary, CLA, Vercel deploys all pass.

Concerns / Questions

  1. www compat: This changes import behavior — internal Meta code (MLCComposer, XDSRichText) doing HTML→Lexical import will now receive LineBreakNode where trailing <br> was previously stripped. This could surface as extra blank lines in existing editor instances that rely on the old stripping behavior. @potatowagon should verify whether www callers depend on the old behavior.

  2. Cross-browser clipboard behavior: The PR correctly identifies Safari's Apple-interchange-newline class, but do Chrome/Firefox ever produce similar trailing <br> during copy that should also be stripped? If so, this PR might not strip enough. Worth testing paste from Chrome/Firefox to confirm no regressions.

  3. The original motivation for #6395: The original PR was fixing extraneous blank lines from clipboard paste. This partial revert should be validated against the original test case — pasting content that previously produced unwanted blank lines.

Assessment

The code change is clean, well-tested, and the logic is sound. The author's argument about round-trip HTML fidelity is valid. The only risk is behavioral regression for consumers that relied on the stripping behavior. Since this is a behavior change (not just a bug fix), it would benefit from @potatowagon's input on whether the original #6395 motivation is still addressed by the narrower Apple-interchange check.

Verdict: Code quality is good. Recommend Sherry verifies www compat before approving.

@etrepum

etrepum commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

@potatowagon do you have any thoughts on this one?

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Jun 26, 2026
@levensta levensta requested a review from potatowagon June 29, 2026 21:28

@potatowagon potatowagon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for picking this up @levensta — the round-trip concern is real and worth fixing. But I don't think the Apple-interchange-newline gate is the right mechanism, and as written this reintroduces the regression #6395 fixed.

Repro (the regression is back): pasting 1<p>2<br /></p>3 now preserves the trailing <br>, producing a phantom blank line. I tested in Safari and the export DOM comes back as:

<p><span>1</span></p>
<p><span>2</span><br></p>   <!-- phantom blank line -->
<p><span>3</span></p>

The <br> inside <p>2<br /></p> is a plain structural break — it has no Apple-interchange-newline class — so the new guard returns false and the break is preserved. The Apple class only ever appears on the artifact Safari appends to the clipboard payload, not on <br>s that are part of the pasted HTML structure. So the guard is too narrow to do the job: it doesn't fire for ordinary pasted/authored trailing breaks, which is exactly the case #6395 targeted.

The mechanism that actually works: this is the same ground covered in #7600, where @etrepum proposed marking Lexical's managed terminating line breaks with a data-lexical-managed-linebreak="true" attribute in the reconciler, then keying the importDOM skip on that attribute rather than on DOM shape or a browser-specific class. I notice this PR's own updated test expectation already emits that attribute:

<p>...<span>2</span><br><br data-lexical-managed-linebreak="true"></p>

So the reconciler side is already there — the fix is to gate the import skip on data-lexical-managed-linebreak instead of Apple-interchange-newline. That distinguishes "rendering artifact Lexical injected" (ignore on import) from "real authored break" (preserve) in a browser-agnostic way, which gives you both: faithful rendering (keeps #6395's behavior for external HTML) and exact round-trip for Lexical-originated content. etrepum's reasoning is in #7600 (review 2893090447).

Could we re-target this onto the managed-linebreak attribute?

@potatowagon

potatowagon commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

im accepting this first (the agent was the one who rejected it without my call, sorry). weighing the pros and cons, solving the round tripping bug seems more high pri then preserving the visual representation of the rich text rendered (hidden vs shown) when pasting a trailing br. @etrepum's previous solution to have <br data-lexical-managed-linebreak="true"> would be the best long term fix, that can be a followup PR. accepting to unblock the round-trip serialization to HTML to work correctly

@potatowagon potatowagon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving to unblock — round-trip serialization to HTML losing the trailing <br> is a correctness/data-integrity bug, and fixing that is higher priority than preserving the visual rendering (hidden vs shown) of a trailing break on paste. Accepting this trade-off for now.

Noting for the record (and for anyone hitting this later): this does reintroduce the #6395 behavior where pasting 1<p>2<br /></p>3 preserves the trailing <br> and renders a phantom blank line — confirmed in Safari, export DOM comes back as <p><span>2</span><br></p>. The Apple-interchange-newline gate only catches Safari's clipboard artifact, not ordinary structural/authored trailing breaks, so the rendering quirk persists for those.

The proper long-term fix is @etrepum's proposal from #7600: mark Lexical's managed terminating line breaks with data-lexical-managed-linebreak="true" in the reconciler and gate the importDOM skip on that attribute instead of Apple-interchange-newline. That distinguishes "rendering artifact Lexical injected" from "real authored break" in a browser-agnostic way, giving both faithful rendering and exact round-trip. The reconciler already emits that attribute (it's in this PR's updated test expectation), so it should be a contained follow-up. Filing/tracking that separately.

isHTMLElement(node) &&
node.getAttribute('class') === 'Apple-interchange-newline'
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is this supposed to do? my notes:

What it's supposed to do: it's a round-trip aid for Apple's own apps. When you copy a chunk of content that ends at a line/block boundary, WebKit tacks on this trailing
to preserve the notion of "there was a newline/break at the end here" so that pasting back into a WebKit-based editor reconstructs the spacing faithfully. The class name literally means "a newline inserted for interchange (clipboard transfer) purposes." It's a transport artifact, not authored content.

Why editors special-case it: because it's a clipboard artifact rather than real content, rich-text editors generally want to strip or ignore it on paste — otherwise every Safari paste tacks an extra blank line onto your document. So a lot of editors (and sanitizers like DOMPurify configs, Google Docs, Slack's composer, etc.) explicitly look for class="Apple-interchange-newline" and drop it. That's exactly what #8750 is trying to leverage: "this specific
is a known clipboard artifact, so it's safe to ignore on import."

potatowagon added a commit to potatowagon/lexical that referenced this pull request Jun 30, 2026
…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.
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

3 participants