Skip to content

Commit 7c2fbde

Browse files
AlessioGrkendelljoseph
authored andcommitted
feat(richtext-lexical): support escaping markdown characters (#11784)
Fixes #10289 and #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: facebook/lexical#7353
1 parent 7c0df4a commit 7c2fbde

File tree

5 files changed

+118
-20
lines changed

5 files changed

+118
-20
lines changed

packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownExport.ts

+6
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,12 @@ function exportTextFormat(
205205
// bring the whitespace back. So our returned string looks like this: " **foo** "
206206
const frozenString = textContent.trim()
207207
let output = frozenString
208+
209+
if (!node.hasFormat('code')) {
210+
// Escape any markdown characters in the text content
211+
output = output.replace(/([*_`~\\])/g, '\\$1')
212+
}
213+
208214
// the opening tags to be added to the result
209215
let openingTags = ''
210216
// the closing tags to be added to the result

packages/richtext-lexical/src/packages/@lexical/markdown/MarkdownImport.ts

+9-12
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import type {
2929
Transformer,
3030
} from './MarkdownTransformers.js'
3131

32-
import { IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI } from '../../../lexical/utils/environment.js'
3332
import { importTextTransformers } from './importTextTransformers.js'
3433
import { isEmptyParagraph, transformersByType } from './utils.js'
3534

@@ -255,28 +254,26 @@ function createTextFormatTransformersIndex(
255254
const tagRegExp = tag.replace(/([*^+])/g, '\\$1')
256255
openTagsRegExp.push(tagRegExp)
257256

258-
if (IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT) {
257+
// Single-char tag (e.g. "*"),
258+
if (tag.length === 1) {
259259
fullMatchRegExpByTag[tag] = new RegExp(
260-
`(${tagRegExp})(?![${tagRegExp}\\s])(.*?[^${tagRegExp}\\s])${tagRegExp}(?!${tagRegExp})`,
260+
`(?<![\\\\${tagRegExp}])(${tagRegExp})((\\\\${tagRegExp})?.*?[^${tagRegExp}\\s](\\\\${tagRegExp})?)((?<!\\\\)|(?<=\\\\\\\\))(${tagRegExp})(?![\\\\${tagRegExp}])`,
261261
)
262262
} else {
263+
// Multi‐char tags (e.g. "**")
263264
fullMatchRegExpByTag[tag] = new RegExp(
264-
`(?<![\\\\${tagRegExp}])(${tagRegExp})((\\\\${tagRegExp})?.*?[^${tagRegExp}\\s](\\\\${tagRegExp})?)((?<!\\\\)|(?<=\\\\\\\\))(${tagRegExp})(?![\\\\${tagRegExp}])`,
265+
`(?<!\\\\)(${tagRegExp})((\\\\${tagRegExp})?.*?[^\\s](\\\\${tagRegExp})?)((?<!\\\\)|(?<=\\\\\\\\))(${tagRegExp})(?!\\\\)`,
265266
)
266267
}
267268
}
268269

269270
return {
270271
// Reg exp to find open tag + content + close tag
271272
fullMatchRegExpByTag,
272-
// Reg exp to find opening tags
273-
openTagsRegExp: new RegExp(
274-
(IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT ? '' : `${escapeRegExp}`) +
275-
'(' +
276-
openTagsRegExp.join('|') +
277-
')',
278-
'g',
279-
),
273+
274+
// Regexp to locate *any* potential opening tag (longest first).
275+
// eslint-disable-next-line regexp/no-useless-character-class, regexp/no-empty-capturing-group, regexp/no-empty-group
276+
openTagsRegExp: new RegExp(`${escapeRegExp}(${openTagsRegExp.join('|')})`, 'g'),
280277
transformersByTag,
281278
}
282279
}

packages/richtext-lexical/src/packages/@lexical/markdown/importTextTransformers.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ export function importTextTransformers(
7878
textMatchTransformers,
7979
)
8080
}
81-
return
8281
} else if (foundTextMatch) {
8382
const result = importFoundTextMatchTransformer(
8483
textNode,
@@ -112,9 +111,9 @@ export function importTextTransformers(
112111
textMatchTransformers,
113112
)
114113
}
115-
return
116-
} else {
117-
// Done!
118-
return
119114
}
115+
// Handle escape characters
116+
const textContent = textNode.getTextContent()
117+
const escapedText = textContent.replace(/\\([*_`~])/g, '$1')
118+
textNode.setTextContent(escapedText)
120119
}

test/lexical-mdx/tableJson.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export const tableJson = {
137137
format: 0,
138138
mode: 'normal',
139139
style: '',
140-
text: ' ',
140+
text: ' * ',
141141
type: 'text',
142142
version: 1,
143143
},

test/lexical-mdx/tests/default.test.ts

+98-2
Original file line numberDiff line numberDiff line change
@@ -376,13 +376,13 @@ there4
376376
input: `
377377
| Option | Default route | Description |
378378
| ----------------- | ----------------------- | ----------------------------------------------- |
379-
| \`account\` | | The user's account page. |
379+
| \`account\` \\* | | The user's account page. |
380380
| \`createFirstUser\` | \`/create-first-user\` | The page to create the first user. |
381381
`,
382382
inputAfterConvertFromEditorJSON: `
383383
| Option | Default route | Description |
384384
|---|---|---|
385-
| \`account\` | | The user's account page. |
385+
| \`account\` \\* | | The user's account page. |
386386
| \`createFirstUser\` | \`/create-first-user\` | The page to create the first user. |
387387
`,
388388
rootChildren: [tableJson],
@@ -400,6 +400,19 @@ there4
400400
},
401401
},
402402
},
403+
{
404+
input: `
405+
<Banner>
406+
Escaped \\*
407+
</Banner>
408+
`,
409+
blockNode: {
410+
fields: {
411+
blockType: 'Banner',
412+
content: textToRichText('Escaped *'),
413+
},
414+
},
415+
},
403416
{
404417
input: `\`inline code\``,
405418
rootChildren: [
@@ -1286,6 +1299,89 @@ Some line [Start of link
12861299
},
12871300
},
12881301
},
1302+
{
1303+
input: `
1304+
<Banner>
1305+
Some line [Text **bold** \\*normal\\*](/some/link)
1306+
</Banner>
1307+
`,
1308+
blockNode: {
1309+
fields: {
1310+
blockType: 'Banner',
1311+
content: {
1312+
root: {
1313+
children: [
1314+
{
1315+
children: [
1316+
{
1317+
detail: 0,
1318+
format: 0,
1319+
mode: 'normal',
1320+
style: '',
1321+
text: 'Some line ',
1322+
type: 'text',
1323+
version: 1,
1324+
},
1325+
{
1326+
children: [
1327+
{
1328+
detail: 0,
1329+
format: 0,
1330+
mode: 'normal',
1331+
style: '',
1332+
text: 'Text ',
1333+
type: 'text',
1334+
version: 1,
1335+
},
1336+
{
1337+
detail: 0,
1338+
format: 1,
1339+
mode: 'normal',
1340+
style: '',
1341+
text: 'bold',
1342+
type: 'text',
1343+
version: 1,
1344+
},
1345+
{
1346+
detail: 0,
1347+
format: 0,
1348+
mode: 'normal',
1349+
style: '',
1350+
text: ' *normal*',
1351+
type: 'text',
1352+
version: 1,
1353+
},
1354+
],
1355+
fields: {
1356+
linkType: 'custom',
1357+
newTab: false,
1358+
url: '/some/link',
1359+
},
1360+
format: '',
1361+
indent: 0,
1362+
type: 'link',
1363+
version: 3,
1364+
},
1365+
],
1366+
direction: null,
1367+
format: '',
1368+
indent: 0,
1369+
textFormat: 0,
1370+
textStyle: '',
1371+
type: 'paragraph',
1372+
version: 1,
1373+
},
1374+
],
1375+
direction: null,
1376+
format: '',
1377+
indent: 0,
1378+
type: 'root',
1379+
version: 1,
1380+
},
1381+
},
1382+
},
1383+
},
1384+
},
12891385
{
12901386
inputAfterConvertFromEditorJSON: `
12911387
<Banner>

0 commit comments

Comments
 (0)