From 6f1a76a39cf4712ccd35039d44982ae78112d3c9 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 19 Mar 2025 15:04:47 -0600 Subject: [PATCH 1/3] feat(richtext-lexical): support escaping markdown characters --- .../@lexical/markdown/MarkdownExport.ts | 6 ++++ .../@lexical/markdown/MarkdownImport.ts | 30 +++++++++++-------- test/lexical-mdx/tableJson.ts | 2 +- test/lexical-mdx/tests/default.test.ts | 17 +++++++++-- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownExport.ts b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownExport.ts index 58d634cb610..edaf0b7900c 100644 --- a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownExport.ts +++ b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownExport.ts @@ -205,6 +205,12 @@ function exportTextFormat( // 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 + output = output.replace(/([*_`~\\])/g, '\\$1') + } + // the opening tags to be added to the result let openingTags = '' // the closing tags to be added to the result diff --git a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts index aa731b7e106..c31e5fb45ab 100644 --- a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts +++ b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts @@ -19,6 +19,7 @@ import { $getRoot, $getSelection, $isParagraphNode, + $isTextNode, } from 'lexical' import type { @@ -29,7 +30,6 @@ import type { Transformer, } from './MarkdownTransformers.js' -import { IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI } from '../../../lexical/utils/environment.js' import { importTextTransformers } from './importTextTransformers.js' import { isEmptyParagraph, transformersByType } from './utils.js' @@ -213,6 +213,15 @@ function $importBlocks( importTextTransformers(textNode, textFormatTransformersIndex, textMatchTransformers) + // Go through every text node in the element node and handle escape characters + for (const child of elementNode.getChildren()) { + if ($isTextNode(child)) { + const textContent = child.getTextContent() + const escapedText = textContent.replace(/\\([*_`~])/g, '$1') + child.setTextContent(escapedText) + } + } + // If no transformer found and we left with original paragraph node // can check if its content can be appended to the previous node // if it's a paragraph, quote or list @@ -255,13 +264,15 @@ function createTextFormatTransformersIndex( const tagRegExp = tag.replace(/([*^+])/g, '\\$1') openTagsRegExp.push(tagRegExp) - if (IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT) { + // Single-char tag (e.g. "*"), + if (tag.length === 1) { fullMatchRegExpByTag[tag] = new RegExp( - `(${tagRegExp})(?![${tagRegExp}\\s])(.*?[^${tagRegExp}\\s])${tagRegExp}(?!${tagRegExp})`, + `(? + Escaped \\* + +`, + blockNode: { + fields: { + blockType: 'Banner', + content: textToRichText('Escaped *'), + }, + }, + }, { input: `\`inline code\``, rootChildren: [ From d74b9191944b8d4c01d46ccfe37ae5068f0126af Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 19 Mar 2025 15:08:26 -0600 Subject: [PATCH 2/3] fix lint --- .../src/packages/@lexical/markdown/MarkdownImport.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts index c31e5fb45ab..1c10c365638 100644 --- a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts +++ b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts @@ -282,6 +282,7 @@ function createTextFormatTransformersIndex( fullMatchRegExpByTag, // Regexp to locate *any* potential opening tag (longest first). + // eslint-disable-next-line regexp/no-useless-character-class, regexp/no-empty-capturing-group, regexp/no-empty-group openTagsRegExp: new RegExp(`${escapeRegExp}(${openTagsRegExp.join('|')})`, 'g'), transformersByTag, } From fbdc6f60d91ab1bae9896b6b98fc9e9451c6dba0 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 19 Mar 2025 18:50:39 -0600 Subject: [PATCH 3/3] handle links --- .../@lexical/markdown/MarkdownImport.ts | 10 --- .../markdown/importTextTransformers.ts | 9 +- test/lexical-mdx/tests/default.test.ts | 83 +++++++++++++++++++ 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts index 1c10c365638..888d9310931 100644 --- a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts +++ b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts @@ -19,7 +19,6 @@ import { $getRoot, $getSelection, $isParagraphNode, - $isTextNode, } from 'lexical' import type { @@ -213,15 +212,6 @@ function $importBlocks( importTextTransformers(textNode, textFormatTransformersIndex, textMatchTransformers) - // Go through every text node in the element node and handle escape characters - for (const child of elementNode.getChildren()) { - if ($isTextNode(child)) { - const textContent = child.getTextContent() - const escapedText = textContent.replace(/\\([*_`~])/g, '$1') - child.setTextContent(escapedText) - } - } - // If no transformer found and we left with original paragraph node // can check if its content can be appended to the previous node // if it's a paragraph, quote or list diff --git a/packages/richtext-lexical/src/packages/@lexical/markdown/importTextTransformers.ts b/packages/richtext-lexical/src/packages/@lexical/markdown/importTextTransformers.ts index 4d872a0707b..c7c7e90938a 100644 --- a/packages/richtext-lexical/src/packages/@lexical/markdown/importTextTransformers.ts +++ b/packages/richtext-lexical/src/packages/@lexical/markdown/importTextTransformers.ts @@ -78,7 +78,6 @@ export function importTextTransformers( textMatchTransformers, ) } - return } else if (foundTextMatch) { const result = importFoundTextMatchTransformer( textNode, @@ -112,9 +111,9 @@ export function importTextTransformers( textMatchTransformers, ) } - return - } else { - // Done! - return } + // Handle escape characters + const textContent = textNode.getTextContent() + const escapedText = textContent.replace(/\\([*_`~])/g, '$1') + textNode.setTextContent(escapedText) } diff --git a/test/lexical-mdx/tests/default.test.ts b/test/lexical-mdx/tests/default.test.ts index 62212670a08..3ed30bdad1d 100644 --- a/test/lexical-mdx/tests/default.test.ts +++ b/test/lexical-mdx/tests/default.test.ts @@ -1299,6 +1299,89 @@ Some line [Start of link }, }, }, + { + input: ` + + Some line [Text **bold** \\*normal\\*](/some/link) + +`, + blockNode: { + fields: { + blockType: 'Banner', + content: { + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'Some line ', + type: 'text', + version: 1, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'Text ', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 1, + mode: 'normal', + style: '', + text: 'bold', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' *normal*', + type: 'text', + version: 1, + }, + ], + fields: { + linkType: 'custom', + newTab: false, + url: '/some/link', + }, + format: '', + indent: 0, + type: 'link', + version: 3, + }, + ], + direction: null, + format: '', + indent: 0, + textFormat: 0, + textStyle: '', + type: 'paragraph', + version: 1, + }, + ], + direction: null, + format: '', + indent: 0, + type: 'root', + version: 1, + }, + }, + }, + }, + }, { inputAfterConvertFromEditorJSON: `