Repository navigation
Bug: A tab in inline code leaves the code span, and a paragraph of only whitespace code is dropped #9409
Description
Lexical version: 0.52.0 (main at 98e936d gives the same results)
Steps to reproduce
Import each document below with $convertFromMarkdownString(markdown, TRANSFORMERS), then export it with $convertToMarkdownString(TRANSFORMERS). ⇥ is a tab:
| Markdown | Imported | Exported Markdown | Expected |
|---|---|---|---|
`a⇥b` |
code a, a tab node (no format), code b |
`a`⇥`b` |
code a⇥b |
a `⇥` b |
a , a tab node (no format), b |
a ⇥ b |
code ⇥ between a and b |
`⇥` alone |
a tab node (no format) | ⇥, then an empty document on the next round trip |
code ⇥ |
x, blank line, ` `, blank line, y |
paragraphs x and y only |
x, blank line, y |
three paragraphs, the middle one code |
a⇥b (no code) |
a, tab node, b ✅ |
unchanged ✅ |
Current behaviour
- A tab inside a code span is imported as a
TabNodewithout the code format (or any other format of the span), so it drops out of the code. The span is split around it, and a span holding only tabs stops being code. - A paragraph whose only content is inline code of up to three whitespace characters, such as
` `or`⇥`, counts as an empty paragraph. Import removes it, and export treats it as empty when separating blocks. Code is content even when it's whitespace.
Cause
In packages/lexical-markdown/src/:
$normalizeMarkdownTextNode()inMarkdownImport.tsreplaces each tab in every imported text node with$createTabNode(), and only the surrounding text keeps the node's format and style.isEmptyParagraph()inutils.tstests the text of a paragraph's only text node against/^\s{0,3}$/, without checking the code format.$importMarkdownNodes()inMarkdownImport.tsuses it to drop paragraphs, andcreateMarkdownExport()uses it to decide block separators.
Suggested fix
--- a/packages/lexical-markdown/src/MarkdownImport.ts
+++ b/packages/lexical-markdown/src/MarkdownImport.ts
@@ -292,7 +292,8 @@ function $importBlocks(
function $normalizeMarkdownTextNode(textNode: TextNode): void {
// A TabNode is a TextNode whose content is a tab, so without this guard the
// rebuild below would destroy it and create an equivalent one in its place.
- if ($isTabNode(textNode)) {
+ // Inline code keeps its tabs as text, so they stay part of the code span.
+ if ($isTabNode(textNode) || textNode.hasFormat('code')) {
return;
}
const text = textNode.getTextContent();
--- a/packages/lexical-markdown/src/utils.ts
+++ b/packages/lexical-markdown/src/utils.ts
@@ -72,6 +72,8 @@ export function isEmptyParagraph(node: LexicalNode): boolean {
firstChild == null ||
(node.getChildrenSize() === 1 &&
$isTextNode(firstChild) &&
+ // Inline code of only whitespace is content.
+ !firstChild.hasFormat('code') &&
MARKDOWN_EMPTY_LINE_REG_EXP.test(firstChild.getTextContent()))
);
}I kept the tab as text in the code node rather than giving the TabNode the code format. Separate code nodes on each side of a tab would be exported as adjacent code spans ( `a⇥b` ), which read back as one span with backticks in it.
With this change:
`a⇥b`round-trips unchanged.`⇥`anda `⇥` bstay code and are stable. They're exported with padding (` ⇥ `), which a reader strips back to the tab.- The paragraph of only
` `is kept. Its export still grows by two spaces on every save; that's a separate padding bug ingetCodeSpanDelimiter()reported in Bug: A code span of only spaces grows by two spaces on every save, and loses its bold or italic #9405. - Tabs outside code (
a⇥b) and in code blocks are unchanged. A truly empty paragraph is still dropped.
Tested with headless Lexical against main at 98e936d. The unit tests for lexical-markdown, lexical-mdast and lexical-playground/__tests__/unit pass (1175), along with tsc, ESLint and Prettier. No new unit tests are added. I didn't check how the editor handles a text node containing a tab when typing in a browser. Code blocks already hold tabs as TabNodes and aren't affected.
No existing issue found.
Activity