Skip to content

[lexical-markdown] Bug Fix: don't emit a dangling closing tag when a selection slices a format to whitespace - #8961

Merged
etrepum merged 2 commits into
facebook:mainfrom
luantaraschi:fix/markdown-selection-dangling-tag
Aug 8, 2026
Merged

[lexical-markdown] Bug Fix: don't emit a dangling closing tag when a selection slices a format to whitespace#8961
etrepum merged 2 commits into
facebook:mainfrom
luantaraschi:fix/markdown-selection-dangling-tag

Conversation

@luantaraschi

Copy link
Copy Markdown
Contributor

Description

exportTextFormat decides two different things from two different strings, and a selection export can make them disagree.

Whether the node carries a format comes from checkHasFormat, which reads the node's whole text and rejects nodes that are entirely whitespace:

if (n && /^\s*$/.test(n.getTextContent())) {
  return false;
}

Whether the tags are actually emitted comes from isWhitespaceOnly, computed from the textContent argument, which on the selection path is the node sliced down to the selected range:

if (isWhitespaceOnly && !node.hasFormat('code')) {
  return closingTagsBefore + output;
}

When a selection slices a formatted node down to whitespace, the first check passes and the second one fires. The tag is pushed onto unclosedTags but the opening tag is thrown away by the early return, so the next node closes a tag that was never opened.

Two bold text nodes, 'a ' and 'b', with the selection running from offset 1 of the first through the second:

$convertSelectionToMarkdownString(TRANSFORMERS, $getSelection())
// '  b**', expected '  **b**'

The ** at the end has no opener, so the output is not valid markdown.

The opening-tag registration is now skipped when the exported text is whitespace-only. The early return already discards those tags, so registering them had no effect other than corrupting the shared unclosedTags list. The following node then sees no pending **, opens its own, and closes it.

This does not change the full-document path: there textContent is the node's whole text, so isWhitespaceOnly can only be true when checkHasFormat already returned false for every format.

Test plan

Before

FAIL packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts
  > $convertSelectionToMarkdownString whitespace slices
    > does not emit a dangling closing tag when the selection slices a format down to whitespace

AssertionError: expected '  b**' to be '  **b**'

After

Test Files  1 passed (1)
     Tests  417 passed (417)

Full vitest run: 5251 passed, 2 skipped. The two failures are in scripts/__tests__/unit/childProcess.test.ts and are about shell env forwarding on Windows. They reproduce on a clean main with this change stashed, so they are not related to it.

pnpm run prettier and pnpm run tsc are clean.

…selection slices a format to whitespace

## Description

`exportTextFormat` decides whether a node carries a format from the node's
whole text, via `checkHasFormat`, but decides whether to emit the tags from
the `textContent` argument, which on the selection path is the node sliced
down to the selected range.

When a selection slices a formatted node down to whitespace the two
disagree: the tag is pushed onto `unclosedTags` while the opening tag is
thrown away by the whitespace early return, so the next node closes a tag
that was never opened.

Opening-tag registration is now skipped when the exported text is
whitespace-only. The full-document path is unaffected, since there
`checkHasFormat` already rejects whitespace-only nodes.

## Test plan

### Before

expected '  b**' to be '  **b**'

### After

417 passed in the markdown suite; 5251 passed in the full unit run. The two
remaining failures are pre-existing Windows shell failures in
scripts/__tests__/unit/childProcess.test.ts.
@luantaraschi
luantaraschi requested a review from zurfyx as a code owner August 7, 2026 16:49
Copilot AI lite review requested due to automatic review settings August 7, 2026 16:49
@vercel

vercel Bot commented Aug 7, 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 Aug 7, 2026 4:50pm
lexical-playground Ready Ready Preview Aug 7, 2026 4:50pm

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 Aug 7, 2026

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

@etrepum is attempting to deploy a commit to the Meta Open Source Team on Vercel.

A member of the Team first needs to authorize it.

@etrepum
etrepum added this pull request to the merge queue Aug 8, 2026
Merged via the queue into facebook:main with commit 6adcca6 Aug 8, 2026
40 of 42 checks passed
@etrepum etrepum mentioned this pull request Aug 31, 2026
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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants