Skip to content

[lexical-list][lexical-markdown] Feature: Keep list marker for markdown round-trip#7892

Merged
etrepum merged 15 commits intofacebook:mainfrom
lytion:lytion/keep-list-marker
Oct 3, 2025
Merged

[lexical-list][lexical-markdown] Feature: Keep list marker for markdown round-trip#7892
etrepum merged 15 commits intofacebook:mainfrom
lytion:lytion/keep-list-marker

Conversation

@lytion
Copy link
Copy Markdown
Contributor

@lytion lytion commented Sep 30, 2025

Description

I'm submitting a proposal so list markers are not loss anymore when switching from/to markdown.
This is a continuation in the idea that switching from/to markdown should not modify typed text.

These changes add a state to NodeList that save list marker between markdown import/export.

Test plan

In lexical playground
In view mode:

  1. Type * hello
  2. Switch to markdown

Before

-> markdown is - hello

After

-> markdown is * hello, it kept marker
Switching again and again keep marker.

Unit tests

Added unit tests in LexicalMarkdown.test.ts that verify a marker is kept between import/export
Added e2e tests in Markdown.spec.mjs that verify markers are kept when typing in lexical view and when copy/pasting.

@vercel
Copy link
Copy Markdown

vercel bot commented Sep 30, 2025

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

Project Deployment Preview Comments Updated (UTC)
lexical Ready Ready Preview Comment Oct 3, 2025 7:28am
lexical-playground Ready Ready Preview Comment Oct 3, 2025 7:28am

@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 Sep 30, 2025
@lytion lytion closed this Sep 30, 2025
@lytion lytion reopened this Sep 30, 2025
@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Sep 30, 2025

I don't think this really belongs in ListItemNode, lexical is not really a markdown native editor so it doesn't make sense to always try and store markdown related things even when you are not using it.

One thing you could do is to write custom markdown transformers that use NodeState to attach this metadata to the nodes it creates so that they can be used on markdown serialization (and that data would end up in the JSON for those nodes as well). https://lexical.dev/docs/concepts/node-state

The issues you had writing tests probably had something to do with how the editors are configured in the specific places you were trying to write tests. There is no check list transformer in the markdown package, but there is one in the playground, so you'd have to copy that into the tests to use it. A primary reason for that is that such a transformer would fail if the check list plugin is not installed. Extensions will solve this once someone implements a markdown extension, but that's why it only ships with default transformers that are likely to be enabled in a basic rich text editor.

Signed-off-by: Simon Bauchet <simon@vivaldi.com>
const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/;
const TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-*:? ?)+\|\s?$/;

export const listMarkerState = createState('listMarker', {
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.

might be worth coming up with a more globally unique name for this, since it does only apply to markdown and it's not allowed for two different states on the same node to have the same name

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.

What about mdListMarkerState ?

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.

mdListMarker seems reasonable, "state" is redundant

);
if (listType === 'bullet' || listType === 'check') {
const marker = match[0].trim()[0];
$setState(listItem, listMarkerState, marker);
Copy link
Copy Markdown
Collaborator

@etrepum etrepum Oct 1, 2025

Choose a reason for hiding this comment

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

This state should really be on the list rather than the list item since it's not really permitted to mix different markers in the same unordered list. It's also a lot more compact as far as serialization goes.

if you put it on the ListItemNode what will happen is that you deserialize from markdown, then edit in lexical, you will end up with your custom markers and then any new items will be the default marker. You'll also run into the same sort of issues when copy and paste because the list items might have been created with different markers than their siblings.

Would be a good idea to have some test coverage that checks that the lists have consistent markers after such operations

Signed-off-by: Simon Bauchet <simon@vivaldi.com>
- marker is kept when using indented list (indented list should have default marker)
- marker is kept when copy/pasting list

Signed-off-by: Simon Bauchet <simon@vivaldi.com>
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Oct 2, 2025
const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/;
const TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-*:? ?)+\|\s?$/;

export const listMarkerState = createState('listMarker', {
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.

mdListMarker seems reasonable, "state" is redundant

Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

This all seemed to work fairly well in some brief testing in the playground, and the additional unit and e2e tests are nice

etrepum
etrepum previously approved these changes Oct 2, 2025
@etrepum etrepum changed the title [lexical-list][lexical-markdown] Bug Fix: Keep list marker [lexical-list][lexical-markdown] Feature: Keep list marker for markdown round-trip Oct 2, 2025
@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Oct 2, 2025

I think this is good to merge but could you fix up the PR description to match the current state of the PR (e.g. there are tests now)? This will help when putting together the release notes.

@lytion
Copy link
Copy Markdown
Contributor Author

lytion commented Oct 3, 2025

Done, I refactored the Unit Test part by:

Added unit tests in LexicalMarkdown.test.ts that verify a marker is kept between import/export
Added e2e tests in Markdown.spec.mjs that verify markers are kept when typing in lexical view and when copy/pasting.

And added a small description of what the PR does

@lytion
Copy link
Copy Markdown
Contributor Author

lytion commented Oct 3, 2025

It seems I'm missing something to merge it, I don't have the merge button.
I'm sorry I only started using GitHub PR system to work on Lexical, I'm not familiar with it. I can't find what's preventing me from merging

@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Oct 3, 2025

The merge button is only there for maintainers

@etrepum etrepum added this pull request to the merge queue Oct 3, 2025
Merged via the queue into facebook:main with commit a9e1fa8 Oct 3, 2025
36 checks passed
@etrepum etrepum mentioned this pull request Oct 11, 2025
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.

3 participants