Skip to content

[lexical-mdast] Bug Fix: A block shortcut no longer strips the lines after its marker when the text holds a newline - #9776

Merged
zurfyx merged 1 commit into
mainfrom
zurfyx/mdast-shortcut-multiline-text
Nov 4, 2026
Merged

zurfyx merged 1 commit into
mainfrom
zurfyx/mdast-shortcut-multiline-text

Conversation

@zurfyx

@zurfyx zurfyx commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Description

A text node can hold a newline when app code inserts text that contains one (for example selection.insertText('---\nnotes'); typing and pasting split lines into line breaks or paragraphs instead). A space or Enter typed at the end of such a node is scanned by MarkdownStreamScanner.scanBlock as markdown, and an empty construct on its first line (a thematic break ---, ***, ___, an empty fence, or a heading, list item or quote with no content) reported the whole text as its marker. The shortcut then turned the paragraph into a horizontal rule or an empty heading, list item, quote or code block and deleted the lines after the marker. The thematic-break case on Space is new with #9484; the Enter cases and the other markers on Space are older.

Now scanBlock does not report a construct whose marker, from the construct's own start, would span a line break (\n or \r), so such text stays as it is. A marker typed on its own line (also after blank lines in the same text node, which are not part of the construct), and Enter with content after the marker in a later text node, are unchanged.

Changeset

.changeset/mdast-shortcut-multiline-text.md (@lexical/mdast patch, [fix]).

Test plan

  • packages/lexical-mdast/src/__tests__/unit/MdastShortcutsMultilineText.test.ts (new, 25 tests): Space and Enter after ---, *** and ___ followed by a line in the same text node; Enter and Space after an empty #, -, > and 1. (Enter also after an empty fence) followed by a line; \r and \r\n; a quote marker spanning two lines with content in the next text node; markers alone on their line still convert, also after a blank line; Enter with a newline only in a later text node still converts as before.

Before

$ ./node_modules/.bin/vitest --run --project unit packages/lexical-mdast/src/__tests__/unit/MdastShortcutsMultilineText.test.ts
AssertionError: expected '\n' to be '---\nvaluable ' // Object.is equality
AssertionError: expected '\n' to contain '---\nvaluable'
AssertionError: expected '' to contain '>\nvaluable'
      Tests  18 failed | 7 passed (25)

After

$ ./node_modules/.bin/vitest --run --project unit packages/lexical-mdast/src/__tests__/unit/MdastShortcutsMultilineText.test.ts
      Tests  25 passed (25)

The @lexical/mdast and @lexical/markdown unit tests pass with no change from main. A differential of 1,386 cases (33 markers, including headings, quotes, every list marker, task items, fences and thematic breaks, in 14 layouts: one line, a newline inside the text node after or before the marker, one or two blank lines or \r\n before it, a line break node after or before it, text after or before it on the same line, typed character by character; then Space, Enter or nothing) gives the same editor state as main in every case except the 136 whose text node holds a newline after the marker, where main deleted the following line in 88. The playground does not use @lexical/mdast, so no e2e test covers it; no browser was run.

Review

Independent review with gpt-6.1-sol over three rounds (the last clean); the first found that a newline after the caret, in a later text node, changed Enter, which this keeps as before.

@vercel

vercel Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
lexical Ready Ready Preview Nov 4, 2026 2:47am UTC
lexical-playground Ready Ready Preview Nov 4, 2026 2:47am UTC

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 Nov 4, 2026
@meta-code-review

meta-code-review Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 4, 2026, 2:59 AM UTC

@pkg-pr-new

pkg-pr-new Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9776

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9776

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9776

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9776

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9776

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9776

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9776

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9776

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9776

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9776

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9776

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9776

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9776

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9776

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9776

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9776

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9776

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9776

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9776

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9776

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9776

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9776

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9776

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9776

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9776

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9776

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9776

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9776

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9776

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9776

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9776

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9776

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9776

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9776

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9776

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9776

commit: 289aa20

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks good to me. The guard in MarkdownStreamScanner.scanBlock stops the block shortcuts from deleting the lines after a marker when a text node holds a newline, and the new tests reproduce the lost text on main and pass here. A few optional, non-blocking suggestions:

  1. A line break before the marker (packages/lexical-mdast/src/MdastStream.ts:124). The marker is measured from the start of the text, so a marker typed after a leading line break in the same text node no longer converts, where main converted it. Examples are \n followed by a typed # , or #\n followed by Enter; CommonMark 4.9 ignores leading blank lines, so \n# is a heading. The test at MdastShortcutsMultilineText.test.ts:159 asserts the new behavior, but the changeset (.changeset/mdast-shortcut-multiline-text.md:7) says typing a marker on its own line is unchanged. Either measure the line break from the construct's own start (first.position.start.offset), which keeps main's behavior there, or add a sentence to the changeset that describes it; .changeset/README.md asks each entry to describe the change from the user's side.
  2. Space for the other markers (MdastShortcutsMultilineText.test.ts:114). On main, a Space typed after #\nvaluable (or -, >, 1.) also deleted valuable, as the changeset says, but only Enter is tested for those markers. Adding Space to that it.each would cover the Space branch in MdastShortcuts.ts.
  3. The - [ ] case passes on main too: - [ ]\nvaluable parses as a task item whose paragraph continues lazily into valuable. It guards against a regression but doesn't show the fix.
  4. Test environment header. AGENTS.md (Testing Strategy) asks for // @vitest-environment node after the copyright header in tests that only exercise editor state, as the sibling tests in packages/lexical-mdast/src/__tests__/unit/ have it.

// first line (a rule, a fence, a heading or list item with no content)
// span the whole text as its marker, and the lines after it would be
// stripped with it.
return match !== null && /[\n\r]/.test(line.slice(0, match.markerLength))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: markerLength is measured from offset 0, so a line break before the construct counts too. A marker typed after a leading \n in the same text node (\n then # , or #\n + Enter) no longer converts, where main converted it. Checking only from first.position.start.offset would keep main's behavior there. Otherwise, the changeset's line 7 ("Typing a marker on its own line … unchanged") could mention it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 289aa20: the line break is now looked for only from the construct's own start (position.start.offset) to the end of its marker, so a marker after leading blank lines in the same text node converts as on main. The changeset says so, and a test covers it.

});
});

it.each(['#', '-', '>', '```\n```', '1.', '- [ ]'])(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: on main, a Space typed after #\nvaluable (and -, >, 1.) also deleted valuable, but these markers are tested with Enter only. A Space case here would cover the Space branch in MdastShortcuts.ts. The - [ ] entry passes on main too: it parses as a task item whose paragraph continues lazily into valuable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 289aa20: Space is now tested after #, -, > and 1. followed by a line, and the - [ ] case, which passes on main, is removed.

* LICENSE file in the root directory of this source tree.
*
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: AGENTS.md (Testing Strategy) asks for // @vitest-environment node after the copyright header in tests that only exercise editor state, as the other shortcut tests in this directory have it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 289aa20: the file now has // @vitest-environment node.

…after its marker when the text holds a newline

## Description

A text node can hold a newline when app code inserts text that contains one (for example `selection.insertText('---\nnotes')`; typing and pasting split lines into line breaks or paragraphs instead). A space or Enter typed at the end of such a node is scanned by `MarkdownStreamScanner.scanBlock` as markdown, and an empty construct on its first line (a thematic break `---`, `***`, `___`, an empty fence, or a heading, list item or quote with no content) reported the whole text as its marker. The shortcut then turned the paragraph into a horizontal rule or an empty heading, list item, quote or code block and deleted the lines after the marker. The thematic-break case on Space is new with #9484; the Enter cases and the other markers on Space are older.

Now `scanBlock` does not report a construct whose marker, from the construct's own start, would span a line break (`\n` or `\r`), so such text stays as it is. A marker typed on its own line (also after blank lines in the same text node, which are not part of the construct), and Enter with content after the marker in a later text node, are unchanged.

## Changeset

`.changeset/mdast-shortcut-multiline-text.md` (`@lexical/mdast` patch, `[fix]`).

## Test plan

- `packages/lexical-mdast/src/__tests__/unit/MdastShortcutsMultilineText.test.ts` (new, 25 tests): Space and Enter after `---`, `***` and `___` followed by a line in the same text node; Enter and Space after an empty `#`, `-`, `>` and `1.` (Enter also after an empty fence) followed by a line; `\r` and `\r\n`; a quote marker spanning two lines with content in the next text node; markers alone on their line still convert, also after a blank line; Enter with a newline only in a later text node still converts as before.

### Before

```
$ ./node_modules/.bin/vitest --run --project unit packages/lexical-mdast/src/__tests__/unit/MdastShortcutsMultilineText.test.ts
AssertionError: expected '\n' to be '---\nvaluable ' // Object.is equality
AssertionError: expected '\n' to contain '---\nvaluable'
AssertionError: expected '' to contain '>\nvaluable'
      Tests  18 failed | 7 passed (25)
```

### After

```
$ ./node_modules/.bin/vitest --run --project unit packages/lexical-mdast/src/__tests__/unit/MdastShortcutsMultilineText.test.ts
      Tests  25 passed (25)
```

The `@lexical/mdast` and `@lexical/markdown` unit tests pass with no change from main. A differential of 1,386 cases (33 markers, including headings, quotes, every list marker, task items, fences and thematic breaks, in 14 layouts: one line, a newline inside the text node after or before the marker, one or two blank lines or `\r\n` before it, a line break node after or before it, text after or before it on the same line, typed character by character; then Space, Enter or nothing) gives the same editor state as main in every case except the 136 whose text node holds a newline after the marker, where main deleted the following line in 88. The playground does not use `@lexical/mdast`, so no e2e test covers it; no browser was run.

## Review

Independent review with gpt-6.1-sol over three rounds (the last clean); the first found that a newline after the caret, in a later text node, changed Enter, which this keeps as before.

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks good to me. Looking for a line break only between the construct's own start and the end of its marker in MarkdownStreamScanner.scanBlock stops the Space and Enter shortcuts from deleting the lines after a marker, and markers after leading blank lines still convert as they do on main. The new tests reproduce the lost text on main (18 of 25 fail there) and pass here, and the changeset follows .changeset/README.md, Verify: paragraph included.

One optional, non-blocking suggestion:

  • Test name (packages/lexical-mdast/src/__tests__/unit/MdastShortcutsMultilineText.test.ts:199). On main, >\n> followed by valuable in the next text node became a quote holding valuable, so nothing after the marker was lost there; this test pins the new decision not to convert a marker that spans two lines. A name such as "a quote marker spanning two lines no longer converts on Enter" would say what it checks.

);
});

it('a marker spanning a line break keeps the text after it on Enter', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: on main this case already kept valuable (it became a quote holding valuable, losing only the >\n> marker), so the test pins the new decision not to convert a marker that spans two lines rather than a lost line. A name such as a quote marker spanning two lines no longer converts on Enter would say what it checks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right: on main this case kept valuable and dropped only the two-line quote marker and its line break; the test pins the decision that a marker spanning a line break does not convert. Not renamed here, to keep the head that was verified; the name will say that if the file changes again.

@zurfyx
zurfyx marked this pull request as ready for review November 4, 2026 03:15
@zurfyx
zurfyx added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 402818d Nov 4, 2026
73 checks passed

This branch was successfully deployed

2 active deployments
Preview – lexical — 289aa206 Deployed Nov 4, 2026 by vercel[bot]
Preview – lexical-playground — 289aa206 Deployed Nov 4, 2026 by vercel[bot]
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.

1 participant