Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
957 changes: 481 additions & 476 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import {
$createParagraphNode,
$createTextNode,
$getRoot,
$getSelection,
$isBlockElementNode,
$isParagraphNode,
$isRootNode,
$isTextNode,
CONTROLLED_TEXT_INSERTION_COMMAND,
FORMAT_TEXT_COMMAND,
} from 'lexical';
import { $createCodeNode, $isCodeNode } from '@lexical/code';
import { $createCodeNode } from '@lexical/code';
import { TOGGLE_LINK_COMMAND } from '@lexical/link';
import {
INSERT_CHECK_LIST_COMMAND,
Expand All @@ -20,20 +20,13 @@ import {
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
import { $createHeadingNode, $createQuoteNode, $isHeadingNode } from '@lexical/rich-text';
import { $dfs } from '@lexical/utils';

import { InsertingPayloadMap, useEditorPanelContext } from '../../../EditorPanel';
import { $getCursorNode } from '../../utils/selection';

import { INSERT_FILES_COMMAND } from '../Files/FilesPlugin';
import { $createImageNode } from '../Image/ImageNode';
import { $toggleFormatNode } from './utils/format';
import {
$canInsertElementsToNode,
$findCommonAncestor,
$getNearestSibling,
$wrapNodes,
} from './utils/tree';
import { $canInsertElementsToNode, $getNearestSibling, $wrapNodes } from './utils/tree';

/**
* Plugin to handle editor panel actions about formatting and nodes insertion
Expand All @@ -47,18 +40,7 @@ export const EditorPanelPlugin = () => {
const cleanupFormatting = onFormatting.watch((format) => {
// TODO: support formatting selected text slices
editor.update(() => {
const selection = $getSelection();
if (!selection) return;

const nodes = selection.getNodes();
if (nodes.length === 0) return;

const commonAncestor = $findCommonAncestor(nodes[0], nodes, (node) =>
$dfs(node).some(({ node }) => $isCodeNode(node)),
);
if (!commonAncestor) return;

$toggleFormatNode(commonAncestor, format);
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ import {
ElementNode,
KEY_ENTER_COMMAND,
KEY_SPACE_COMMAND,
TextNode,
} from 'lexical';
import { $isCodeNode } from '@lexical/code';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $isQuoteNode } from '@lexical/rich-text';
import { mergeRegister } from '@lexical/utils';

import { $isFormattingNode, FormattingNode } from '../Markdown/nodes/FormattingNode';
import { $convertTextNodeFormatting } from './utils';

const OUT_OF_BLOCK_NODE_COMMAND = createCommand<ElementNode>();

Expand Down Expand Up @@ -66,7 +64,7 @@ export const FormattingPlugin = () => {
useEffect(
() =>
mergeRegister(
editor.registerNodeTransform(TextNode, $convertTextNodeFormatting),
// editor.registerNodeTransform(TextNode, $convertTextNodeFormatting),
Comment thread
vitonsky marked this conversation as resolved.
Outdated
editor.registerNodeTransform(FormattingNode, (node: FormattingNode) => {
// Remove empty formatting nodes
const textContent = node.getTextContent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level >
"
`;

exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > List with formatting 1`] = `
"- *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~
- Plain text
- *Italic*
- **Bold**
- ~~Delete~~
"
`;

exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Mixed list 1`] = `
"- level 1-1
- [x] level 2-1
Expand Down Expand Up @@ -106,6 +115,20 @@ exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level >
"
`;

exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Quote with formatting 1`] = `
"> *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~
> Plain text
> *Italic*
> **Bold**
> ~~Delete~~
"
`;

exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Rich formatting 1`] = `
"*All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~***
"
`;

exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Simple code 1`] = `
"\`\`\`js
const x = 42;
Expand All @@ -114,6 +137,11 @@ console.log("Your number is", x ** x);
"
`;

exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Simple formatting 1`] = `
"Text may be **bold**, *italic*, ~~strikethrough~~.
"
`;

exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Simple post with headers 1`] = `
"# The title

Expand Down Expand Up @@ -153,6 +181,14 @@ exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level >
"
`;

exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Table with formatting 1`] = `
"| foo | bar |
| ---------------------------------------------------------------------------------------------------------------------- | -------------------------- |
| *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~ |
| **bold** | *italic* |
"
`;

exports[`Markdown-Lexical-Markdown round-trips must be consistent on AST level > Text with few empty lines 1`] = `
"foo

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ import {
$convertFromMarkdownString,
$convertToMarkdownString,
$serializeAsMarkdownAST,
markdownProcessor,
parseMarkdownToAST,
} from '../markdownParser';
import {
detailsWithSummary,
formattedLine,
formattingInList,
formattingInQuote,
formattingInTable,
mixedList,
nestedQuote,
postWithHeaders,
richFormatting,
simpleCode,
simpleFormatting,
simpleQuote,
simpleTable,
unsupportedFeatures,
Expand All @@ -25,12 +31,41 @@ import {
updateEditorState,
} from './utils';

test('Markdown parser round-trips', () => {
const ast = parseMarkdownToAST(richFormatting);
const out = markdownProcessor.stringify(ast);

expect(normalizeMarkdownTree(parseMarkdownToAST(out))).toEqual(
normalizeMarkdownTree(ast),
);
});

describe('Markdown-Lexical-Markdown round-trips must be consistent on AST level', () => {
const cases = [
{
title: 'Rich formatting',
markdown: richFormatting,
},
{
title: 'Plain list',
markdown: '- foo\n - bar\n - baz',
},
{
title: 'Simple formatting',
markdown: simpleFormatting,
},
{
title: 'List with formatting',
markdown: formattingInList,
},
{
title: 'Quote with formatting',
markdown: formattingInQuote,
},
{
title: 'Table with formatting',
markdown: formattingInTable,
},
{
title: 'List item with inline elements',
markdown: '- foo [bar](...) baz',
Expand Down Expand Up @@ -124,7 +159,15 @@ describe('Markdown-Lexical-Markdown round-trips must be consistent on AST level'
editor.read(() => normalizeMarkdownTree($serializeAsMarkdownAST())),
).toMatchObject(normalizeMarkdownTree(parseMarkdownToAST(sourceText)));

expect(editor.read(() => $convertToMarkdownString())).toMatchSnapshot();
const out = editor.read(() => $convertToMarkdownString());
expect(out).toMatchSnapshot();

// Parse markdown
await updateEditorState(editor, () => {
$convertFromMarkdownString(out);
});

expect(editor.read(() => $convertToMarkdownString())).toBe(out);
}),
);
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
export const simpleFormatting = `
Text may be **bold**, *italic*, ~~strikethrough~~.
`.trim();

export const richFormatting = `
*All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~***
`.trim();

export const formattingInList = `
- *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~
- Plain text
- *Italic*
- **Bold**
- ~~Delete~~
`.trim();

export const formattingInQuote = `
> *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~
> Plain text
> *Italic*
> **Bold**
> ~~Delete~~
`.trim();

export const formattingInTable = `
| foo | bar |
| ---- | ----- |
| *All text can be italic, something additionally can be **bold**, ~~strikethrough~~, or **~~bold AND strikethrough~~*** | plain text and ~~deleted~~ |
| **bold** | *italic* |
`.trim();

export const mixedList = `
- level 1-1
- [x] level 2-1
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { $isLineBreakNode, $isParagraphNode, $isTextNode, LexicalNode } from 'lexical';
import {
$isLineBreakNode,
$isParagraphNode,
$isTextNode,
LexicalNode,
TextFormatType,
} from 'lexical';
import {
Blockquote,
Break,
Expand Down Expand Up @@ -104,7 +110,26 @@ export const convertLexicalNodeToMarkdownNode = (node: LexicalNode): Content =>
}) satisfies InlineCode;
}

return u('text', { value: node.getTextContent() }) satisfies Text;
let wrappedNode: PhrasingContent = u('text', {
value: node.getTextContent(),
}) satisfies Text;

// TODO: support all formats like super/sub, etc
const formatsOrder = (
['bold', 'italic', 'strikethrough'] satisfies TextFormatType[]
).reverse();
const nodesMap = {
italic: 'emphasis',
bold: 'strong',
strikethrough: 'delete',
} satisfies Partial<Record<TextFormatType, PhrasingContent['type']>>;

formatsOrder.forEach((format) => {
if (!node.hasFormat(format)) return;
wrappedNode = u(nodesMap[format], { children: [wrappedNode] });
});

return wrappedNode;
}

if ($isCodeNode(node)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const createSyncContext = <T>(defaultValue: T) => {
let context: { current: T } | null = null;

const get = () => (context ? context.current : defaultValue);
const use = <R = void>(value: T, callback: () => R) => {
// Set context
const currentContext = context;
context = { current: value };
try {
return callback();
} finally {
// Restore previous context
context = currentContext;
}
};

return { get, use };
};
Loading
Loading