Skip to content

[lexical-markdown][lexical-playground] Bug Fix: Convert tabs in TabNode at import#8211

Merged
etrepum merged 5 commits intofacebook:mainfrom
lytion:lytion/markdown/tabnode-import/1
Mar 15, 2026
Merged

[lexical-markdown][lexical-playground] Bug Fix: Convert tabs in TabNode at import#8211
etrepum merged 5 commits intofacebook:mainfrom
lytion:lytion/markdown/tabnode-import/1

Conversation

@lytion
Copy link
Copy Markdown
Contributor

@lytion lytion commented Mar 11, 2026

This is a follow up of #7845. I made a new PR to make it clean.

Reported issue: #7820
To summarize, if you use indent button and then switch to markdown, indent will be lost.

In this fix, once all transformers have been applied, loop on nodes to convert all '\t' in TabNodes.
I decided to do it here and not in $importBlocks as it was gonna create several nodes and break the function flow which only use one TextNode.

Closes #7820

Signed-off-by: Simon Bauchet <simon@vivaldi.com>
Signed-off-by: Simon Bauchet <simon@vivaldi.com>
@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 Mar 11, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 11, 2026

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

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Mar 13, 2026 8:36am
lexical-playground Ready Ready Preview, Comment Mar 13, 2026 8:36am

Request Review

Comment on lines 103 to 107
if ($isParagraphNode(child) && child.getTextContent().includes('\t')) {
for (const paragraphChild of child.getChildren()) {
$createTabInNode(paragraphChild);
}
}
Copy link
Copy Markdown
Collaborator

@etrepum etrepum Mar 12, 2026

Choose a reason for hiding this comment

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

This would probably be better written by traversing for all of the TextNodes first, computing paragraphNode.getTextContent() is doing a whole extra traversal of that subtree

      if ($isElementNode(child)) {
        for (const textNode of child.getAllTextNodes()) {
          $normalizeMarkdownTextNode(textNode);
        }
      }

For the simplest correct implementation that would preserve formats and such, the tab replacing code should find the appropriate offsets, use textNode.splitText(), and then replace the nodes that are just tabs with $createTabNode().

Signed-off-by: Simon Bauchet <simon@vivaldi.com>
Comment on lines +311 to +319
while (tabOffset !== -1) {
const splitNodes = currNode.splitText(tabOffset, tabOffset + 1);
splitNodes.forEach((node) => {
if (node.getTextContent() === '\t') {
node.replace($createTabNode());
}
});
currNode = splitNodes[splitNodes.length - 1];
tabOffset = currNode.getTextContent().indexOf('\t');
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.

I don't think this logic is correct if the node ends with a tab? It looks like it will loop infinitely. It could also be done a little more clearly if all the tabs are found first and splitText is called at most once

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.

Could have yes, but it seems there's a trim before because I have no '\t' or space at the end of a string when calling getTextContent().

…replace

Signed-off-by: Simon Bauchet <simon@vivaldi.com>
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.

I think the array approach here can end up with tab offsets like [0,1,1,2] when you have adjacent tabs which will create and then GC empty TextNodes since the current splitText algorithm doesn't de-dupe the indices.


// Look in node for '\t' and create a TabNode for each occurrence.
function $normalizeMarkdownTextNode(textNode: TextNode): void {
const tabOffsets = [];
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.

Suggested change
const tabOffsets = [];
const tabOffsets: Set<number> = new Set();


// Find all tab occurrences
while (index !== -1) {
tabOffsets.push(index, index + 1);
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.

Suggested change
tabOffsets.push(index, index + 1);
tabOffsets.add(index);
tabOffsets.add(index + 1);

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.

Indeed, I thought I covered this case in unit test but this test had '\t(space)\t', my bad.
I refactored to use a Set and added a test

Signed-off-by: Simon Bauchet <simon@vivaldi.com>
@etrepum etrepum added this pull request to the merge queue Mar 15, 2026
Merged via the queue into facebook:main with commit fb104d7 Mar 15, 2026
40 checks passed
@lytion lytion deleted the lytion/markdown/tabnode-import/1 branch March 16, 2026 09:21
@etrepum etrepum mentioned this pull request Mar 19, 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. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Indent lost when switching to Markdown

3 participants