@terrible-lexical/markdown
Version:
This package contains Markdown helpers and functionality for Lexical.
250 lines (235 loc) • 8.45 kB
text/typescript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {CodeNode} from '@terrible-lexical/code/src';
import {createHeadlessEditor} from '@terrible-lexical/headless/src';
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@terrible-lexical/html/src';
import {LinkNode} from '@terrible-lexical/link/src';
import {ListItemNode, ListNode} from '@terrible-lexical/list/src';
import {HeadingNode, QuoteNode} from '@terrible-lexical/rich-text/src';
import {$getRoot, $insertNodes} from 'terrible-lexical';
import {
$convertFromMarkdownString,
$convertToMarkdownString,
LINK,
TextMatchTransformer,
TRANSFORMERS,
} from '../..';
describe('Markdown', () => {
type Input = Array<{
html: string;
md: string;
skipExport?: true;
skipImport?: true;
}>;
const URL = 'https://lexical.dev';
const IMPORT_AND_EXPORT: Input = [
{html: '<h1><span>Hello world</span></h1>', md: '# Hello world'},
{html: '<h2><span>Hello world</span></h2>', md: '## Hello world'},
{html: '<h3><span>Hello world</span></h3>', md: '### Hello world'},
{html: '<h4><span>Hello world</span></h4>', md: '#### Hello world'},
{html: '<h5><span>Hello world</span></h5>', md: '##### Hello world'},
{html: '<h6><span>Hello world</span></h6>', md: '###### Hello world'},
{
// Multiline paragraphs
html: '<p><span>Hello</span><br><span>world</span><br><span>!</span></p>',
md: ['Hello', 'world', '!'].join('\n'),
},
{
html: '<blockquote><span>Hello</span><br><span>world!</span></blockquote>',
md: '> Hello\n> world!',
},
{
// Miltiline list items
html: '<ul><li value="1"><span>Hello</span></li><li value="2"><span>world</span><br><span>!</span><br><span>!</span></li></ul>',
md: '- Hello\n- world\n!\n!',
},
{
html: '<ul><li value="1"><span>Hello</span></li><li value="2"><span>world</span></li></ul>',
md: '- Hello\n- world',
},
{
html: '<ul><li value="1"><span>Level 1</span></li><li value="2"><ul><li value="1"><span>Level 2</span></li><li value="2"><ul><li value="1"><span>Level 3</span></li></ul></li></ul></li></ul><p><span>Hello world</span></p>',
md: '- Level 1\n - Level 2\n - Level 3\n\nHello world',
},
{
// Import only: export will use "-" instead of "*"
html: '<ul><li value="1"><span>Level 1</span></li><li value="2"><ul><li value="1"><span>Level 2</span></li><li value="2"><ul><li value="1"><span>Level 3</span></li></ul></li></ul></li></ul><p><span>Hello world</span></p>',
md: '* Level 1\n * Level 2\n * Level 3\n\nHello world',
skipExport: true,
},
{
html: '<ol><li value="1"><span>Hello</span></li><li value="2"><span>world</span></li></ol>',
md: '1. Hello\n2. world',
},
{
html: '<ol start="25"><li value="25"><span>Hello</span></li><li value="26"><span>world</span></li></ol>',
md: '25. Hello\n26. world',
},
{
html: '<p><i><em>Hello</em></i><span> world</span></p>',
md: '*Hello* world',
},
{
html: '<p><b><strong>Hello</strong></b><span> world</span></p>',
md: '**Hello** world',
},
{
html: '<p><i><b><strong>Hello</strong></b></i><span> world</span></p>',
md: '***Hello*** world',
},
{
html: '<p><code><span>Hello</span></code><span> world</span></p>',
md: '`Hello` world',
},
{
html: '<p><s><span>Hello</span></s><span> world</span></p>',
md: '~~Hello~~ world',
},
{
html: '<p><a href="https://lexical.dev"><span>Hello</span></a><span> world</span></p>',
md: '[Hello](https://lexical.dev) world',
},
{
html: '<p><a href="https://lexical.dev" title="Hello world"><span>Hello</span></a><span> world</span></p>',
md: '[Hello](https://lexical.dev "Hello world") world',
},
{
html: '<p><a href="https://lexical.dev" title="Title with \\" escaped character"><span>Hello</span></a><span> world</span></p>',
md: '[Hello](https://lexical.dev "Title with \\" escaped character") world',
},
{
html: '<p><span>Hello </span><s><i><b><strong>world</strong></b></i></s><span>!</span></p>',
md: 'Hello ~~***world***~~!',
},
{
html: '<p><i><em>Hello </em></i><i><b><strong>world</strong></b></i><i><em>!</em></i></p>',
md: '*Hello **world**!*',
},
{
// Import only: export will use * instead of _ due to registered transformers order
html: '<p><i><em>Hello</em></i><span> world</span></p>',
md: '_Hello_ world',
skipExport: true,
},
{
// Import only: export will use * instead of _ due to registered transformers order
html: '<p><b><strong>Hello</strong></b><span> world</span></p>',
md: '__Hello__ world',
skipExport: true,
},
{
// Import only: export will use * instead of _ due to registered transformers order
html: '<p><i><b><strong>Hello</strong></b></i><span> world</span></p>',
md: '___Hello___ world',
skipExport: true,
},
{
// Import only: export will use * instead of _ due to registered transformers order
html: '<p><span>Hello </span><s><i><b><strong>world</strong></b></i></s><span>!</span></p>',
md: 'Hello ~~__*world*__~~!',
skipExport: true,
},
{
html: '<pre spellcheck="false"><span>Code</span></pre>',
md: '```\nCode\n```',
},
{
html: '<pre spellcheck="false"><span>Code</span></pre>',
md: '```\nCode\n```',
},
{
// Import only: extra empty lines will be removed for export
html: '<p><span>Hello</span></p><p><span>world</span></p>',
md: ['Hello', '', '', '', 'world'].join('\n'),
skipExport: true,
},
{
// Import only: multiline quote will be prefixed with ">" on each line during export
html: '<blockquote><span>Hello</span><br><span>world</span><br><span>!</span></blockquote>',
md: '> Hello\nworld\n!',
skipExport: true,
},
{
// Import only: ensures that left side of splitText is processed for text match transformers
html: '<p><span>Hello </span><a href="https://lexical.dev"><span>world</span></a><span>! Hello </span><mark><span>$world$</span></mark><span>! </span><a href="https://lexical.dev"><span>Hello</span></a><span> world! Hello </span><mark><span>$world$</span></mark><span>!</span></p>',
md: `Hello [world](${URL})! Hello $world$! [Hello](${URL}) world! Hello $world$!`,
skipExport: true,
},
];
const HIGHLIGHT_TEXT_MATCH_IMPORT: TextMatchTransformer = {
...LINK,
importRegExp: /\$([^$]+?)\$/,
replace: (textNode) => {
textNode.setFormat('highlight');
},
};
for (const {html, md, skipImport} of IMPORT_AND_EXPORT) {
if (skipImport) {
continue;
}
it(`can import "${md.replace(/\n/g, '\\n')}"`, () => {
const editor = createHeadlessEditor({
nodes: [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
LinkNode,
],
});
editor.update(
() =>
$convertFromMarkdownString(md, [
...TRANSFORMERS,
HIGHLIGHT_TEXT_MATCH_IMPORT,
]),
{
discrete: true,
},
);
expect(
editor.getEditorState().read(() => $generateHtmlFromNodes(editor)),
).toBe(html);
});
}
for (const {html, md, skipExport} of IMPORT_AND_EXPORT) {
if (skipExport) {
continue;
}
it(`can export "${md.replace(/\n/g, '\\n')}"`, () => {
const editor = createHeadlessEditor({
nodes: [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
LinkNode,
],
});
editor.update(
() => {
const parser = new DOMParser();
const dom = parser.parseFromString(html, 'text/html');
const nodes = $generateNodesFromDOM(editor, dom);
$getRoot().select();
$insertNodes(nodes);
},
{
discrete: true,
},
);
expect(
editor
.getEditorState()
.read(() => $convertToMarkdownString(TRANSFORMERS)),
).toBe(md);
});
}
});