From 43cdccdef06f7b4df0ef42bb880e3dd9ca32aca4 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Thu, 20 Mar 2025 10:29:45 -0600 Subject: [PATCH] feat(richtext-lexical): support escaping markdown characters (#11784) Fixes https://github.com/payloadcms/payload/issues/10289 and https://github.com/payloadcms/payload/issues/11772 This adds support for escaping markdown characters. For example,` \*` is supposed to be imported as `*` and exported back to `\*`. Equivalent PR in lexical repo: https://github.com/facebook/lexical/pull/7353 --- .../@lexical/markdown/MarkdownExport.ts | 6 ++ .../@lexical/markdown/MarkdownImport.ts | 25 ++--- .../markdown/importTextTransformers.ts | 9 +- test/lexical-mdx/tableJson.ts | 2 +- test/lexical-mdx/tests/default.test.ts | 100 +++++++++++++++++- 5 files changed, 120 insertions(+), 22 deletions(-) diff --git a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownExport.ts b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownExport.ts index 58d634cb6..edaf0b790 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 aa731b7e1..888d93109 100644 --- a/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts +++ b/packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts @@ -29,7 +29,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' @@ -255,28 +254,26 @@ function createTextFormatTransformersIndex( const tagRegExp = tag.replace(/([*^+])/g, '\\$1') openTagsRegExp.push(tagRegExp) - if (IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT) { - fullMatchRegExpByTag[tag] = new RegExp( - `(${tagRegExp})(?![${tagRegExp}\\s])(.*?[^${tagRegExp}\\s])${tagRegExp}(?!${tagRegExp})`, - ) - } else { + // Single-char tag (e.g. "*"), + if (tag.length === 1) { fullMatchRegExpByTag[tag] = new RegExp( `(? + Escaped \\* + +`, + blockNode: { + fields: { + blockType: 'Banner', + content: textToRichText('Escaped *'), + }, + }, + }, { input: `\`inline code\``, rootChildren: [ @@ -1286,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: `