Skip to content

Commit a82b1dc

Browse files
authored
[lexical-markdown] Add test to keep code language (facebook#6259)
1 parent 66052cc commit a82b1dc

File tree

11 files changed

+89
-25
lines changed

11 files changed

+89
-25
lines changed

packages/lexical-code/src/CodeNode.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ export type SerializedCodeNode = Spread<
4848
SerializedElementNode
4949
>;
5050

51-
const mapToPrismLanguage = (
51+
const isLanguageSupportedByPrism = (
5252
language: string | null | undefined,
53-
): string | null | undefined => {
54-
// eslint-disable-next-line no-prototype-builtins
55-
return language != null && window.Prism.languages.hasOwnProperty(language)
56-
? language
57-
: undefined;
53+
): boolean => {
54+
try {
55+
// eslint-disable-next-line no-prototype-builtins
56+
return language ? window.Prism.languages.hasOwnProperty(language) : false;
57+
} catch {
58+
return false;
59+
}
5860
};
5961

6062
function hasChildDOMNodeTag(node: Node, tagName: string) {
@@ -67,12 +69,15 @@ function hasChildDOMNodeTag(node: Node, tagName: string) {
6769
return false;
6870
}
6971

70-
const LANGUAGE_DATA_ATTRIBUTE = 'data-highlight-language';
72+
const LANGUAGE_DATA_ATTRIBUTE = 'data-language';
73+
const HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE = 'data-highlight-language';
7174

7275
/** @noInheritDoc */
7376
export class CodeNode extends ElementNode {
7477
/** @internal */
7578
__language: string | null | undefined;
79+
/** @internal */
80+
__isSyntaxHighlightSupported: boolean;
7681

7782
static getType(): string {
7883
return 'code';
@@ -84,7 +89,8 @@ export class CodeNode extends ElementNode {
8489

8590
constructor(language?: string | null | undefined, key?: NodeKey) {
8691
super(key);
87-
this.__language = mapToPrismLanguage(language);
92+
this.__language = language;
93+
this.__isSyntaxHighlightSupported = isLanguageSupportedByPrism(language);
8894
}
8995

9096
// View
@@ -95,6 +101,10 @@ export class CodeNode extends ElementNode {
95101
const language = this.getLanguage();
96102
if (language) {
97103
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
104+
105+
if (this.getIsSyntaxHighlightSupported()) {
106+
element.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
107+
}
98108
}
99109
return element;
100110
}
@@ -109,9 +119,17 @@ export class CodeNode extends ElementNode {
109119
if (language) {
110120
if (language !== prevLanguage) {
111121
dom.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
122+
123+
if (this.__isSyntaxHighlightSupported) {
124+
dom.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
125+
}
112126
}
113127
} else if (prevLanguage) {
114128
dom.removeAttribute(LANGUAGE_DATA_ATTRIBUTE);
129+
130+
if (prevNode.__isSyntaxHighlightSupported) {
131+
dom.removeAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE);
132+
}
115133
}
116134
return false;
117135
}
@@ -123,6 +141,10 @@ export class CodeNode extends ElementNode {
123141
const language = this.getLanguage();
124142
if (language) {
125143
element.setAttribute(LANGUAGE_DATA_ATTRIBUTE, language);
144+
145+
if (this.getIsSyntaxHighlightSupported()) {
146+
element.setAttribute(HIGHLIGHT_LANGUAGE_DATA_ATTRIBUTE, language);
147+
}
126148
}
127149
return {element};
128150
}
@@ -304,12 +326,18 @@ export class CodeNode extends ElementNode {
304326

305327
setLanguage(language: string): void {
306328
const writable = this.getWritable();
307-
writable.__language = mapToPrismLanguage(language);
329+
writable.__language = language;
330+
writable.__isSyntaxHighlightSupported =
331+
isLanguageSupportedByPrism(language);
308332
}
309333

310334
getLanguage(): string | null | undefined {
311335
return this.getLatest().__language;
312336
}
337+
338+
getIsSyntaxHighlightSupported(): boolean {
339+
return this.getLatest().__isSyntaxHighlightSupported;
340+
}
313341
}
314342

315343
export function $createCodeNode(

packages/lexical-code/src/__tests__/unit/LexicalCodeNode.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ describe('LexicalCodeNode tests', () => {
182182
});
183183
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
184184
expect(testEnv.innerHTML).toBe(
185-
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span><span data-lexical-text="true">\t</span></code>',
185+
'<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span><span data-lexical-text="true">\t</span></code>',
186186
);
187187

188188
// CodeNode should only render diffs, make sure that the TabNode is not cloned when
@@ -200,7 +200,7 @@ describe('LexicalCodeNode tests', () => {
200200
}),
201201
);
202202
expect(testEnv.innerHTML).toBe(
203-
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">foo</span></code>',
203+
'<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">foo</span></code>',
204204
);
205205
});
206206

@@ -224,7 +224,7 @@ describe('LexicalCodeNode tests', () => {
224224
});
225225
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
226226
expect(testEnv.innerHTML).toBe(
227-
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">f</span><span data-lexical-text="true">\t</span></code>',
227+
'<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">f</span><span data-lexical-text="true">\t</span></code>',
228228
);
229229
});
230230

@@ -248,7 +248,7 @@ describe('LexicalCodeNode tests', () => {
248248
});
249249
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
250250
expect(testEnv.innerHTML).toBe(
251-
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">\t</span><span data-lexical-text="true">function</span></code>',
251+
'<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">\t</span><span data-lexical-text="true">function</span></code>',
252252
);
253253

254254
await editor.update(() => {
@@ -261,7 +261,7 @@ describe('LexicalCodeNode tests', () => {
261261
});
262262
await editor.dispatchCommand(KEY_TAB_COMMAND, shiftTabKeyboardEvent());
263263
expect(testEnv.innerHTML).toBe(
264-
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span></code>',
264+
'<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span></code>',
265265
);
266266
});
267267

@@ -285,7 +285,7 @@ describe('LexicalCodeNode tests', () => {
285285
});
286286
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
287287
expect(testEnv.innerHTML).toBe(
288-
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">\t</span><span data-lexical-text="true">function</span></code>',
288+
'<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">\t</span><span data-lexical-text="true">function</span></code>',
289289
);
290290

291291
await editor.update(() => {
@@ -299,7 +299,7 @@ describe('LexicalCodeNode tests', () => {
299299
});
300300
await editor.dispatchCommand(KEY_TAB_COMMAND, shiftTabKeyboardEvent());
301301
expect(testEnv.innerHTML).toBe(
302-
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span></code>',
302+
'<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span></code>',
303303
);
304304
});
305305

@@ -326,13 +326,13 @@ describe('LexicalCodeNode tests', () => {
326326
});
327327
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
328328
expect(testEnv.innerHTML).toBe(
329-
`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
329+
`<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1
330330
2"><span data-lexical-text="true">\t</span><span data-lexical-text="true">hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span><br><span data-lexical-text="true">\t</span><span data-lexical-text="true">hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span></code>`,
331331
);
332332

333333
await editor.dispatchCommand(KEY_TAB_COMMAND, shiftTabKeyboardEvent());
334334
expect(testEnv.innerHTML).toBe(
335-
`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
335+
`<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1
336336
2"><span data-lexical-text="true">hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span><br><span data-lexical-text="true">hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span></code>`,
337337
);
338338
});
@@ -351,7 +351,7 @@ describe('LexicalCodeNode tests', () => {
351351
});
352352
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
353353
expect(testEnv.innerHTML)
354-
.toBe(`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
354+
.toBe(`<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1
355355
2"><span data-lexical-text="true">hello</span><br><span data-lexical-text="true">\t</span></code>`);
356356
});
357357

@@ -375,7 +375,7 @@ describe('LexicalCodeNode tests', () => {
375375
});
376376
await editor.dispatchCommand(KEY_TAB_COMMAND, shiftTabKeyboardEvent());
377377
expect(testEnv.innerHTML).toBe(
378-
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">hello</span></code>',
378+
'<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">hello</span></code>',
379379
);
380380
});
381381

@@ -395,7 +395,7 @@ describe('LexicalCodeNode tests', () => {
395395
keyEvent.altKey = true;
396396
await editor.dispatchCommand(KEY_ARROW_UP_COMMAND, keyEvent);
397397
expect(testEnv.innerHTML)
398-
.toBe(`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
398+
.toBe(`<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1
399399
2"><span data-lexical-text="true">ghi</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">jkl</span><br><span data-lexical-text="true">abc</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">def</span></code>`);
400400
});
401401

@@ -428,7 +428,7 @@ describe('LexicalCodeNode tests', () => {
428428
keyEvent.altKey = true;
429429
await editor.dispatchCommand(KEY_ARROW_DOWN_COMMAND, keyEvent);
430430
expect(testEnv.innerHTML)
431-
.toBe(`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
431+
.toBe(`<code spellcheck="false" data-language="javascript" data-highlight-language="javascript" dir="ltr" data-gutter="1
432432
2
433433
3"><span data-lexical-text="true">mno</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">pqr</span><br><span data-lexical-text="true">abc</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">def</span><br><span data-lexical-text="true">ghi</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">jkl</span></code>`);
434434
});

packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@ describe('Markdown', () => {
186186
html: '<pre spellcheck="false"><span style="white-space: pre-wrap;">Code</span></pre>',
187187
md: '```\nCode\n```',
188188
},
189+
{
190+
html: '<pre spellcheck="false" data-language="javascript" data-highlight-language="javascript"><span style="white-space: pre-wrap;">Code</span></pre>',
191+
md: '```javascript\nCode\n```',
192+
},
193+
{
194+
// Should always preserve language in md but keep data-highlight-language only for supported languages
195+
html: '<pre spellcheck="false" data-language="unknown"><span style="white-space: pre-wrap;">Code</span></pre>',
196+
md: '```unknown\nCode\n```',
197+
},
189198
{
190199
// Import only: prefix tabs will be removed for export
191200
html: '<pre spellcheck="false"><span style="white-space: pre-wrap;">Code</span></pre>',

packages/lexical-playground/__tests__/e2e/CodeActionMenu.spec.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ test.describe('CodeActionMenu', () => {
197197
dir="ltr"
198198
spellcheck="false"
199199
data-gutter="1"
200+
data-language="javascript"
200201
data-highlight-language="javascript">
201202
<span data-lexical-text="true"></span>
202203
<span class="PlaygroundEditorTheme__tokenAttr" data-lexical-text="true">
@@ -227,6 +228,7 @@ test.describe('CodeActionMenu', () => {
227228
dir="ltr"
228229
spellcheck="false"
229230
data-gutter="12"
231+
data-language="javascript"
230232
data-highlight-language="javascript">
231233
<span class="PlaygroundEditorTheme__tokenAttr" data-lexical-text="true">
232234
const
@@ -271,6 +273,7 @@ test.describe('CodeActionMenu', () => {
271273
dir="ltr"
272274
spellcheck="false"
273275
data-gutter="1"
276+
data-language="javascript"
274277
data-highlight-language="javascript">
275278
<span data-lexical-text="true">cons luci</span>
276279
<span class="PlaygroundEditorTheme__tokenOperator" data-lexical-text="true">

packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ test.describe('CodeBlock', () => {
4848
spellcheck="false"
4949
dir="ltr"
5050
data-gutter="1"
51+
data-language="javascript"
5152
data-highlight-language="javascript">
5253
<span
5354
class="PlaygroundEditorTheme__tokenFunction"
@@ -128,6 +129,7 @@ test.describe('CodeBlock', () => {
128129
spellcheck="false"
129130
dir="ltr"
130131
data-gutter="1"
132+
data-language="javascript"
131133
data-highlight-language="javascript">
132134
<span
133135
class="PlaygroundEditorTheme__tokenFunction"
@@ -225,6 +227,7 @@ test.describe('CodeBlock', () => {
225227
dir="ltr"
226228
spellcheck="false"
227229
data-gutter="12345"
230+
data-language="javascript"
228231
data-highlight-language="javascript">
229232
<span data-lexical-text="true">foo</span>
230233
<br />
@@ -254,6 +257,7 @@ test.describe('CodeBlock', () => {
254257
spellcheck="false"
255258
dir="ltr"
256259
data-gutter="1"
260+
data-language="javascript"
257261
data-highlight-language="javascript">
258262
<span data-lexical-text="true">select</span>
259263
<span
@@ -275,6 +279,7 @@ test.describe('CodeBlock', () => {
275279
spellcheck="false"
276280
dir="ltr"
277281
data-gutter="1"
282+
data-language="sql"
278283
data-highlight-language="sql">
279284
<span
280285
class="PlaygroundEditorTheme__tokenAttr"
@@ -333,6 +338,7 @@ test.describe('CodeBlock', () => {
333338
spellcheck="false"
334339
dir="ltr"
335340
data-gutter="123"
341+
data-language="javascript"
336342
data-highlight-language="javascript">
337343
<span
338344
class="PlaygroundEditorTheme__tokenFunction"
@@ -421,6 +427,7 @@ test.describe('CodeBlock', () => {
421427
spellcheck="false"
422428
dir="ltr"
423429
data-gutter="123"
430+
data-language="javascript"
424431
data-highlight-language="javascript">
425432
<span
426433
class="PlaygroundEditorTheme__tokenAttr"
@@ -492,6 +499,7 @@ test.describe('CodeBlock', () => {
492499
dir="ltr"
493500
spellcheck="false"
494501
data-gutter="123"
502+
data-language="javascript"
495503
data-highlight-language="javascript">
496504
<span data-lexical-text="true"></span>
497505
<span data-lexical-text="true"></span>
@@ -565,6 +573,7 @@ test.describe('CodeBlock', () => {
565573
dir="ltr"
566574
spellcheck="false"
567575
data-gutter="123"
576+
data-language="javascript"
568577
data-highlight-language="javascript">
569578
<span data-lexical-text="true"></span>
570579
<span
@@ -635,6 +644,7 @@ test.describe('CodeBlock', () => {
635644
spellcheck="false"
636645
dir="ltr"
637646
data-gutter="123"
647+
data-language="javascript"
638648
data-highlight-language="javascript">
639649
<span
640650
class="PlaygroundEditorTheme__tokenAttr"
@@ -702,6 +712,7 @@ test.describe('CodeBlock', () => {
702712
spellcheck="false"
703713
dir="ltr"
704714
data-gutter="123"
715+
data-language="javascript"
705716
data-highlight-language="javascript">
706717
<span
707718
class="PlaygroundEditorTheme__tokenFunction"
@@ -773,6 +784,7 @@ test.describe('CodeBlock', () => {
773784
spellcheck="false"
774785
dir="ltr"
775786
data-gutter="123"
787+
data-language="javascript"
776788
data-highlight-language="javascript">
777789
<span
778790
class="PlaygroundEditorTheme__tokenFunction"
@@ -899,6 +911,7 @@ test.describe('CodeBlock', () => {
899911
dir="ltr"
900912
spellcheck="false"
901913
data-gutter="12"
914+
data-language="javascript"
902915
data-highlight-language="javascript">
903916
<span data-lexical-text="true"></span>
904917
<span data-lexical-text="true">a b</span>

packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/HTMLCopyAndPaste.spec.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ test.describe('HTML CopyAndPaste', () => {
5656
await focusEditor(page);
5757

5858
const clipboard = {
59-
'text/html': `<meta charset='utf-8'><p class="x1f6kntn x1fcty0u x16h55sf x12nagc xdj266r" dir="ltr"><span>Code block</span></p><code class="x1f6kntn x1fcty0u x16h55sf x1xmf6yo x1e56ztr x1q8sqs3 xeq4nuv x1lliihq xz9dl7a xn6708d xsag5q8 x1ye3gou" spellcheck="false" data-highlight-language="javascript"><span class="xuc5kci">function</span><span> </span><span class="xu88d7e">foo</span><span class="x1noocy9">(</span><span class="x1noocy9">)</span><span> </span><span class="x1noocy9">{</span><br><span> </span><span class="xuc5kci">return</span><span> </span><span class="x180nigk">'Hey there'</span><span class="x1noocy9">;</span><br><span class="x1noocy9">}</span></code><p class="x1f6kntn x1fcty0u x16h55sf x12nagc xdj266r" dir="ltr"><span>--end--</span></p>`,
59+
'text/html': `<meta charset='utf-8'><p class="x1f6kntn x1fcty0u x16h55sf x12nagc xdj266r" dir="ltr"><span>Code block</span></p><code class="x1f6kntn x1fcty0u x16h55sf x1xmf6yo x1e56ztr x1q8sqs3 xeq4nuv x1lliihq xz9dl7a xn6708d xsag5q8 x1ye3gou" spellcheck="false" data-language="javascript" data-highlight-language="javascript"><span class="xuc5kci">function</span><span> </span><span class="xu88d7e">foo</span><span class="x1noocy9">(</span><span class="x1noocy9">)</span><span> </span><span class="x1noocy9">{</span><br><span> </span><span class="xuc5kci">return</span><span> </span><span class="x180nigk">'Hey there'</span><span class="x1noocy9">;</span><br><span class="x1noocy9">}</span></code><p class="x1f6kntn x1fcty0u x16h55sf x12nagc xdj266r" dir="ltr"><span>--end--</span></p>`,
6060
};
6161

6262
await pasteFromClipboard(page, clipboard);
@@ -74,6 +74,7 @@ test.describe('HTML CopyAndPaste', () => {
7474
dir="ltr"
7575
spellcheck="false"
7676
data-gutter="123"
77+
data-language="javascript"
7778
data-highlight-language="javascript">
7879
<span
7980
class="PlaygroundEditorTheme__tokenAttr"

0 commit comments

Comments
 (0)