From bfcfee758a7bf48be3564e2cf38e6136163c48c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20Andr=C3=A9=20Reinseth?= Date: Fri, 7 Nov 2025 10:13:55 +0100 Subject: [PATCH 1/3] Revert "[RFC][lexical-markdown] Replace whitespace with code point when the string has leading and trailing whitespaces (#7400)" This reverts commit 4d459e3f47e20901c444247005725780fb88134e. --- .../lexical-markdown/src/MarkdownExport.ts | 19 +++------- .../__tests__/unit/LexicalMarkdown.test.ts | 37 +++++++------------ .../src/importTextTransformers.ts | 6 +-- 3 files changed, 19 insertions(+), 43 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownExport.ts b/packages/lexical-markdown/src/MarkdownExport.ts index b61b4532a98..9aea01bc0da 100644 --- a/packages/lexical-markdown/src/MarkdownExport.ts +++ b/packages/lexical-markdown/src/MarkdownExport.ts @@ -203,13 +203,10 @@ function exportTextFormat( ): string { // This function handles the case of a string looking like this: " foo " // Where it would be invalid markdown to generate: "** foo **" - // If the node has no format, we use the original text. - // Otherwise, we escape leading and trailing whitespaces to their corresponding code points, - // ensuring the returned string maintains its original formatting, e.g., "** foo **". - let output = - node.getFormat() === 0 - ? textContent - : escapeLeadingAndTrailingWhitespaces(textContent); + // We instead want to trim the whitespace out, apply formatting, and then + // bring the whitespace back. So our returned string looks like this: " **foo** " + const frozenString = textContent.trim(); + let output = frozenString; if (!node.hasFormat('code')) { // Escape any markdown characters in the text content @@ -290,7 +287,7 @@ function exportTextFormat( output = openingTags + output + closingTagsAfter; // Replace trimmed version of textContent ensuring surrounding whitespace is not modified - return closingTagsBefore + output; + return closingTagsBefore + textContent.replace(frozenString, () => output); } function getTextSibling(node: TextNode, backward: boolean): TextNode | null { @@ -309,9 +306,3 @@ function hasFormat( ): boolean { return $isTextNode(node) && node.hasFormat(format); } - -function escapeLeadingAndTrailingWhitespaces(textContent: string) { - return textContent.replace(/^\s+|\s+$/g, (match) => { - return [...match].map((char) => '&#' + char.codePointAt(0) + ';').join(''); - }); -} diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index a5fc85f973a..a0fe514963f 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -409,17 +409,14 @@ describe('Markdown', () => { { html: '

Hello world!

', md: '**Hello ~~world~~**!', - mdAfterExport: '**Hello ~~world~~**!', }, { html: '

Hello world!

', md: '**~~Hello *world*~~**~~!~~', - mdAfterExport: '**~~Hello *world*~~**~~!~~', }, { html: '

Hello world!

', md: '*Hello **world**!*', - mdAfterExport: '*Hello **world**!*', }, { html: '

hello world

', @@ -602,28 +599,24 @@ describe('Markdown', () => { md: '**Bold** `[text](https://lexical.dev)` **Bold 3**', }, { - html: '

Text boldstart text boldend text

', - md: 'Text **boldstart [text](https://lexical.dev) boldend** text', - mdAfterExport: - 'Text **boldstart **[**text**](https://lexical.dev)** boldend** text', + html: '

Text boldstart text boldend text

', + md: 'Text **boldstart** [**text**](https://lexical.dev) **boldend** text', }, { - html: '

Text boldstart text boldend text

', - md: 'Text **boldstart [`text`](https://lexical.dev) boldend** text', - mdAfterExport: - 'Text **boldstart **[**`text`**](https://lexical.dev)** boldend** text', + html: '

Text boldstart text boldend text

', + md: 'Text **boldstart** [**`text`**](https://lexical.dev) **boldend** text', }, { - html: '

It works with links too

', - md: 'It ~~___works [with links](https://lexical.io)___~~ too', + html: '

It works with links too

', + md: 'It ~~___works___~~ [~~___with links___~~](https://lexical.io) too', mdAfterExport: - 'It ***~~works ~~***[***~~with links~~***](https://lexical.io) too', + 'It ***~~works~~*** [***~~with links~~***](https://lexical.io) too', }, { - html: '

It works with links too!

', - md: 'It ~~___works [with links](https://lexical.io) too___~~!', + html: '

It works with links too!

', + md: 'It ~~___works___~~ [~~___with links___~~](https://lexical.io) ~~___too___~~!', mdAfterExport: - 'It ***~~works ~~***[***~~with links~~***](https://lexical.io)***~~ too~~***!', + 'It ***~~works~~*** [***~~with links~~***](https://lexical.io) ***~~too~~***!', }, { html: '

linklink2

', @@ -688,10 +681,6 @@ describe('Markdown', () => { html: '

*Hello* world

', md: '\\*Hello\\* world', }, - { - html: '

 

', - md: '** **', - }, { html: '

[h]elloh[e]llo

', md: '[[h]ello](https://lexical.dev)[h[e]llo](https://lexical.dev)', @@ -720,7 +709,7 @@ describe('Markdown', () => { { html: '

text link

', md: '**text [link](https://lexical.dev)**', - mdAfterExport: '**text **[**link**](https://lexical.dev)', + mdAfterExport: '**text** [**link**](https://lexical.dev)', }, { html: '

texttext

', @@ -734,7 +723,7 @@ describe('Markdown', () => { { html: '

foo bar

', md: '**foo [*bar*](/url)**', - mdAfterExport: '**foo **[***bar***](/url)', + mdAfterExport: '**foo** [***bar***](/url)', }, { html: '

*foo bar baz

', @@ -744,7 +733,7 @@ describe('Markdown', () => { { html: '

a * b x

', md: '*a `*` b `x`*', - mdAfterExport: '*a `*` b `x`*', + mdAfterExport: '*a `*` b `x`*', }, { html: '

_foo_bar

', diff --git a/packages/lexical-markdown/src/importTextTransformers.ts b/packages/lexical-markdown/src/importTextTransformers.ts index b066fec5261..91f5d3147b5 100644 --- a/packages/lexical-markdown/src/importTextTransformers.ts +++ b/packages/lexical-markdown/src/importTextTransformers.ts @@ -135,10 +135,6 @@ export function importTextTransformers( // Handle escape characters const textContent = textNode.getTextContent(); - const escapedText = textContent - .replace(/\\([*_`~\\])/g, '$1') - .replace(/&#(\d+);/g, (_, codePoint) => { - return String.fromCodePoint(codePoint); - }); + const escapedText = textContent.replace(/\\([*_`~\\])/g, '$1'); textNode.setTextContent(escapedText); } From 0eca25d76a00d5d8bb70ee8bb50208905f85f8d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20Andr=C3=A9=20Reinseth?= Date: Fri, 7 Nov 2025 11:00:04 +0100 Subject: [PATCH 2/3] [lexical-markdown] skip format of the empty string on export Leading and trailing whitespaces inside formatted text are already moved outside the formatted region to ensure correct markdown. But if the formatted text only contain whitespace, then we end up with formatting for the empty string, e.g. `**** ` for a bold whitespace. So we need to skip the formatting in these cases. --- packages/lexical-markdown/src/MarkdownExport.ts | 3 ++- .../src/__tests__/unit/LexicalMarkdown.test.ts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/lexical-markdown/src/MarkdownExport.ts b/packages/lexical-markdown/src/MarkdownExport.ts index 9aea01bc0da..207c99e9f7d 100644 --- a/packages/lexical-markdown/src/MarkdownExport.ts +++ b/packages/lexical-markdown/src/MarkdownExport.ts @@ -205,6 +205,7 @@ function exportTextFormat( // Where it would be invalid markdown to generate: "** foo **" // We instead want to trim the whitespace out, apply formatting, and then // bring the whitespace back. So our returned string looks like this: " **foo** " + // However, we do not want to export any formatting if the string is just whitespace: " " const frozenString = textContent.trim(); let output = frozenString; @@ -285,7 +286,7 @@ function exportTextFormat( break; } - output = openingTags + output + closingTagsAfter; + output = output ? openingTags + output + closingTagsAfter : output; // Replace trimmed version of textContent ensuring surrounding whitespace is not modified return closingTagsBefore + textContent.replace(frozenString, () => output); } diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index a0fe514963f..6f02d7cf53a 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -740,6 +740,11 @@ describe('Markdown', () => { md: '_foo_bar', mdAfterExport: '\\_foo\\_bar', }, + { + html: '

', + md: ' ', + skipImport: true, + }, ]; for (const { From 17ecbf4b3e46e070f072365ef198594d0ada40c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnar=20Andr=C3=A9=20Reinseth?= Date: Thu, 26 Feb 2026 08:37:10 +0100 Subject: [PATCH 3/3] Restore import of escaped code points from 4d459e3f47e20901c444247005725780fb88134e --- packages/lexical-markdown/src/importTextTransformers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/lexical-markdown/src/importTextTransformers.ts b/packages/lexical-markdown/src/importTextTransformers.ts index 91f5d3147b5..b066fec5261 100644 --- a/packages/lexical-markdown/src/importTextTransformers.ts +++ b/packages/lexical-markdown/src/importTextTransformers.ts @@ -135,6 +135,10 @@ export function importTextTransformers( // Handle escape characters const textContent = textNode.getTextContent(); - const escapedText = textContent.replace(/\\([*_`~\\])/g, '$1'); + const escapedText = textContent + .replace(/\\([*_`~\\])/g, '$1') + .replace(/&#(\d+);/g, (_, codePoint) => { + return String.fromCodePoint(codePoint); + }); textNode.setTextContent(escapedText); }