Skip to content

Commit

Permalink
[lexical-markdown][lexical-playground] Feature: Option to include bla…
Browse files Browse the repository at this point in the history
…nklines in markdown render (#6020)
  • Loading branch information
potatowagon committed May 17, 2024
1 parent 04ecbb4 commit f02e163
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 39 deletions.
23 changes: 18 additions & 5 deletions packages/lexical-markdown/src/MarkdownExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ import {
$isTextNode,
} from 'lexical';

import {transformersByType} from './utils';
import {isEmptyParagraph, transformersByType} from './utils';

export function createMarkdownExport(
transformers: Array<Transformer>,
shouldPreserveNewLines: boolean = false,
): (node?: ElementNode) => string {
const byType = transformersByType(transformers);
const isNewlineDelimited = !shouldPreserveNewLines;

// Export only uses text formats that are responsible for single format
// e.g. it will filter out *** (bold, italic) and instead use separate ** and *
Expand All @@ -39,7 +41,8 @@ export function createMarkdownExport(
const output = [];
const children = (node || $getRoot()).getChildren();

for (const child of children) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
const result = exportTopLevelElements(
child,
byType.element,
Expand All @@ -48,11 +51,20 @@ export function createMarkdownExport(
);

if (result != null) {
output.push(result);
output.push(
// seperate consecutive group of texts with a line break: eg. ["hello", "world"] -> ["hello", "/nworld"]
isNewlineDelimited &&
i > 0 &&
!isEmptyParagraph(child) &&
!isEmptyParagraph(children[i - 1])
? '\n'.concat(result)
: result,
);
}
}

return output.join('\n\n');
// Ensure consecutive groups of texts are atleast \n\n apart while each empty paragraph render as a newline.
// Eg. ["hello", "", "", "hi", "\nworld"] -> "hello\n\n\nhi\n\nworld"
return output.join('\n');
};
}

Expand Down Expand Up @@ -116,6 +128,7 @@ function exportChildren(
exportTextFormat(child, child.getTextContent(), textTransformersIndex),
);
} else if ($isElementNode(child)) {
// empty paragraph returns ""
output.push(
exportChildren(child, textTransformersIndex, textMatchTransformers),
);
Expand Down
36 changes: 15 additions & 21 deletions packages/lexical-markdown/src/MarkdownImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
TextMatchTransformer,
Transformer,
} from '@lexical/markdown';
import type {LexicalNode, TextNode} from 'lexical';
import type {TextNode} from 'lexical';

import {$createCodeNode} from '@lexical/code';
import {$isListItemNode, $isListNode, ListItemNode} from '@lexical/list';
Expand All @@ -26,14 +26,16 @@ import {
$getRoot,
$getSelection,
$isParagraphNode,
$isTextNode,
ElementNode,
} from 'lexical';
import {IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI} from 'shared/environment';

import {PUNCTUATION_OR_SPACE, transformersByType} from './utils';
import {
isEmptyParagraph,
PUNCTUATION_OR_SPACE,
transformersByType,
} from './utils';

const MARKDOWN_EMPTY_LINE_REG_EXP = /^\s{0,3}$/;
const CODE_BLOCK_REG_EXP = /^[ \t]*```(\w{1,10})?\s?$/;
type TextFormatTransformersIndex = Readonly<{
fullMatchRegExpByTag: Readonly<Record<string, RegExp>>;
Expand All @@ -43,6 +45,7 @@ type TextFormatTransformersIndex = Readonly<{

export function createMarkdownImport(
transformers: Array<Transformer>,
shouldPreserveNewLines = false,
): (markdownString: string, node?: ElementNode) => void {
const byType = transformersByType(transformers);
const textFormatTransformersIndex = createTextFormatTransformersIndex(
Expand Down Expand Up @@ -77,11 +80,16 @@ export function createMarkdownImport(
);
}

// Removing empty paragraphs as md does not really
// allow empty lines and uses them as delimiter
// By default, removing empty paragraphs as md does not really
// allow empty lines and uses them as delimiter.
// If you need empty lines set shouldPreserveNewLines = true.
const children = root.getChildren();
for (const child of children) {
if (isEmptyParagraph(child) && root.getChildrenSize() > 1) {
if (
!shouldPreserveNewLines &&
isEmptyParagraph(child) &&
root.getChildrenSize() > 1
) {
child.remove();
}
}
Expand All @@ -92,20 +100,6 @@ export function createMarkdownImport(
};
}

function isEmptyParagraph(node: LexicalNode): boolean {
if (!$isParagraphNode(node)) {
return false;
}

const firstChild = node.getFirstChild();
return (
firstChild == null ||
(node.getChildrenSize() === 1 &&
$isTextNode(firstChild) &&
MARKDOWN_EMPTY_LINE_REG_EXP.test(firstChild.getTextContent()))
);
}

function $importBlocks(
lineText: string,
rootNode: ElementNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('Markdown', () => {
md: string;
skipExport?: true;
skipImport?: true;
shouldPreserveNewLines?: true;
}>;

const URL = 'https://lexical.dev';
Expand Down Expand Up @@ -147,6 +148,16 @@ describe('Markdown', () => {
html: '<p><i><em style="white-space: pre-wrap;">Hello </em></i><i><b><strong style="white-space: pre-wrap;">world</strong></b></i><i><em style="white-space: pre-wrap;">!</em></i></p>',
md: '*Hello **world**!*',
},
{
html: '<h1><span style="white-space: pre-wrap;">Hello</span></h1><p><br></p><p><br></p><p><br></p><p><b><strong style="white-space: pre-wrap;">world</strong></b><span style="white-space: pre-wrap;">!</span></p>',
md: '# Hello\n\n\n\n**world**!',
shouldPreserveNewLines: true,
},
{
html: '<h1><span style="white-space: pre-wrap;">Hello</span></h1><p><span style="white-space: pre-wrap;">hi</span></p><p><br></p><p><b><strong style="white-space: pre-wrap;">world</strong></b></p><p><br></p><p><span style="white-space: pre-wrap;">hi</span></p><blockquote><span style="white-space: pre-wrap;">hello</span><br><span style="white-space: pre-wrap;">hello</span></blockquote><p><br></p><h1><span style="white-space: pre-wrap;">hi</span></h1><p><br></p><p><span style="white-space: pre-wrap;">hi</span></p>',
md: '# Hello\nhi\n\n**world**\n\nhi\n> hello\n> hello\n\n# hi\n\nhi',
shouldPreserveNewLines: true,
},
{
// Import only: export will use * instead of _ due to registered transformers order
html: '<p><i><em style="white-space: pre-wrap;">Hello</em></i><span style="white-space: pre-wrap;"> world</span></p>',
Expand Down Expand Up @@ -221,7 +232,12 @@ describe('Markdown', () => {
},
};

for (const {html, md, skipImport} of IMPORT_AND_EXPORT) {
for (const {
html,
md,
skipImport,
shouldPreserveNewLines,
} of IMPORT_AND_EXPORT) {
if (skipImport) {
continue;
}
Expand All @@ -240,10 +256,12 @@ describe('Markdown', () => {

editor.update(
() =>
$convertFromMarkdownString(md, [
...TRANSFORMERS,
HIGHLIGHT_TEXT_MATCH_IMPORT,
]),
$convertFromMarkdownString(
md,
[...TRANSFORMERS, HIGHLIGHT_TEXT_MATCH_IMPORT],
undefined,
shouldPreserveNewLines,
),
{
discrete: true,
},
Expand All @@ -255,7 +273,12 @@ describe('Markdown', () => {
});
}

for (const {html, md, skipExport} of IMPORT_AND_EXPORT) {
for (const {
html,
md,
skipExport,
shouldPreserveNewLines,
} of IMPORT_AND_EXPORT) {
if (skipExport) {
continue;
}
Expand Down Expand Up @@ -288,7 +311,13 @@ describe('Markdown', () => {
expect(
editor
.getEditorState()
.read(() => $convertToMarkdownString(TRANSFORMERS)),
.read(() =>
$convertToMarkdownString(
TRANSFORMERS,
undefined,
shouldPreserveNewLines,
),
),
).toBe(md);
});
}
Expand Down
12 changes: 10 additions & 2 deletions packages/lexical-markdown/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,24 @@ function $convertFromMarkdownString(
markdown: string,
transformers: Array<Transformer> = TRANSFORMERS,
node?: ElementNode,
shouldPreserveNewLines = false,
): void {
const importMarkdown = createMarkdownImport(transformers);
const importMarkdown = createMarkdownImport(
transformers,
shouldPreserveNewLines,
);
return importMarkdown(markdown, node);
}

function $convertToMarkdownString(
transformers: Array<Transformer> = TRANSFORMERS,
node?: ElementNode,
shouldPreserveNewLines: boolean = false,
): string {
const exportMarkdown = createMarkdownExport(transformers);
const exportMarkdown = createMarkdownExport(
transformers,
shouldPreserveNewLines,
);
return exportMarkdown(node);
}

Expand Down
24 changes: 23 additions & 1 deletion packages/lexical-markdown/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ import type {
TextMatchTransformer,
Transformer,
} from '@lexical/markdown';
import type {ElementNode, LexicalNode, TextFormatType} from 'lexical';

import {$isCodeNode} from '@lexical/code';
import {$isListItemNode, $isListNode} from '@lexical/list';
import {$isHeadingNode, $isQuoteNode} from '@lexical/rich-text';
import {
$isParagraphNode,
$isTextNode,
type ElementNode,
type LexicalNode,
type TextFormatType,
} from 'lexical';

type MarkdownFormatKind =
| 'noTransformation'
Expand Down Expand Up @@ -429,3 +435,19 @@ export function transformersByType(transformers: Array<Transformer>): Readonly<{
}

export const PUNCTUATION_OR_SPACE = /[!-/:-@[-`{-~\s]/;

const MARKDOWN_EMPTY_LINE_REG_EXP = /^\s{0,3}$/;

export function isEmptyParagraph(node: LexicalNode): boolean {
if (!$isParagraphNode(node)) {
return false;
}

const firstChild = node.getFirstChild();
return (
firstChild == null ||
(node.getChildrenSize() === 1 &&
$isTextNode(firstChild) &&
MARKDOWN_EMPTY_LINE_REG_EXP.test(firstChild.getTextContent()))
);
}
6 changes: 5 additions & 1 deletion packages/lexical-playground/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default function Editor(): JSX.Element {
showTreeView,
showTableOfContents,
shouldUseLexicalContextMenu,
shouldPreserveNewLinesInMarkdown,
tableCellMerge,
tableCellBackgroundColor,
},
Expand Down Expand Up @@ -239,7 +240,10 @@ export default function Editor(): JSX.Element {
{isAutocomplete && <AutocompletePlugin />}
<div>{showTableOfContents && <TableOfContentsPlugin />}</div>
{shouldUseLexicalContextMenu && <ContextMenuPlugin />}
<ActionsPlugin isRichText={isRichText} />
<ActionsPlugin
isRichText={isRichText}
shouldPreserveNewLinesInMarkdown={shouldPreserveNewLinesInMarkdown}
/>
</div>
{showTreeView && <TreeViewPlugin />}
</>
Expand Down
11 changes: 11 additions & 0 deletions packages/lexical-playground/src/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function Settings(): JSX.Element {
disableBeforeInput,
showTableOfContents,
shouldUseLexicalContextMenu,
shouldPreserveNewLinesInMarkdown,
},
} = useSettings();
useEffect(() => {
Expand Down Expand Up @@ -150,6 +151,16 @@ export default function Settings(): JSX.Element {
checked={shouldUseLexicalContextMenu}
text="Use Lexical Context Menu"
/>
<Switch
onClick={() => {
setOption(
'shouldPreserveNewLinesInMarkdown',
!shouldPreserveNewLinesInMarkdown,
);
}}
checked={shouldPreserveNewLinesInMarkdown}
text="Preserve newlines in Markdown"
/>
</div>
) : null}
</>
Expand Down
1 change: 1 addition & 0 deletions packages/lexical-playground/src/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const DEFAULT_SETTINGS = {
isMaxLength: false,
isRichText: true,
measureTypingPerf: false,
shouldPreserveNewLinesInMarkdown: false,
shouldUseLexicalContextMenu: false,
showNestedEditorTreeView: false,
showTableOfContents: false,
Expand Down
12 changes: 10 additions & 2 deletions packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@ async function shareDoc(doc: SerializedDocument): Promise<void> {

export default function ActionsPlugin({
isRichText,
shouldPreserveNewLinesInMarkdown,
}: {
isRichText: boolean;
shouldPreserveNewLinesInMarkdown: boolean;
}): JSX.Element {
const [editor] = useLexicalComposerContext();
const [isEditable, setIsEditable] = useState(() => editor.isEditable());
Expand Down Expand Up @@ -172,9 +174,15 @@ export default function ActionsPlugin({
$convertFromMarkdownString(
firstChild.getTextContent(),
PLAYGROUND_TRANSFORMERS,
undefined, // node
shouldPreserveNewLinesInMarkdown,
);
} else {
const markdown = $convertToMarkdownString(PLAYGROUND_TRANSFORMERS);
const markdown = $convertToMarkdownString(
PLAYGROUND_TRANSFORMERS,
undefined, //node
shouldPreserveNewLinesInMarkdown,
);
root
.clear()
.append(
Expand All @@ -183,7 +191,7 @@ export default function ActionsPlugin({
}
root.selectEnd();
});
}, [editor]);
}, [editor, shouldPreserveNewLinesInMarkdown]);

return (
<div className="actions">
Expand Down

0 comments on commit f02e163

Please sign in to comment.