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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
`(?<![\\\\${tagRegExp}])(${tagRegExp})((\\\\${tagRegExp})?.*?[^${tagRegExp}\\s](\\\\${tagRegExp})?)((?<!\\\\)|(?<=\\\\\\\\))(${tagRegExp})(?![\\\\${tagRegExp}])`,
|
||||
)
|
||||
} else {
|
||||
// Multi‐char tags (e.g. "**")
|
||||
fullMatchRegExpByTag[tag] = new RegExp(
|
||||
`(?<!\\\\)(${tagRegExp})((\\\\${tagRegExp})?.*?[^\\s](\\\\${tagRegExp})?)((?<!\\\\)|(?<=\\\\\\\\))(${tagRegExp})(?!\\\\)`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Reg exp to find open tag + content + close tag
|
||||
fullMatchRegExpByTag,
|
||||
// Reg exp to find opening tags
|
||||
openTagsRegExp: new RegExp(
|
||||
(IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT ? '' : `${escapeRegExp}`) +
|
||||
'(' +
|
||||
openTagsRegExp.join('|') +
|
||||
')',
|
||||
'g',
|
||||
),
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export const tableJson = {
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' ',
|
||||
text: ' * ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
|
||||
@@ -376,13 +376,13 @@ there4
|
||||
input: `
|
||||
| Option | Default route | Description |
|
||||
| ----------------- | ----------------------- | ----------------------------------------------- |
|
||||
| \`account\` | | The user's account page. |
|
||||
| \`account\` \\* | | The user's account page. |
|
||||
| \`createFirstUser\` | \`/create-first-user\` | The page to create the first user. |
|
||||
`,
|
||||
inputAfterConvertFromEditorJSON: `
|
||||
| Option | Default route | Description |
|
||||
|---|---|---|
|
||||
| \`account\` | | The user's account page. |
|
||||
| \`account\` \\* | | The user's account page. |
|
||||
| \`createFirstUser\` | \`/create-first-user\` | The page to create the first user. |
|
||||
`,
|
||||
rootChildren: [tableJson],
|
||||
@@ -400,6 +400,19 @@ there4
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `
|
||||
<Banner>
|
||||
Escaped \\*
|
||||
</Banner>
|
||||
`,
|
||||
blockNode: {
|
||||
fields: {
|
||||
blockType: 'Banner',
|
||||
content: textToRichText('Escaped *'),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `\`inline code\``,
|
||||
rootChildren: [
|
||||
@@ -1286,6 +1299,89 @@ Some line [Start of link
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `
|
||||
<Banner>
|
||||
Some line [Text **bold** \\*normal\\*](/some/link)
|
||||
</Banner>
|
||||
`,
|
||||
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: `
|
||||
<Banner>
|
||||
|
||||
Reference in New Issue
Block a user