From 2c4db9ffe332c96544c6dad740b29ae170f02963 Mon Sep 17 00:00:00 2001 From: Rob Figueiredo Date: Sat, 21 Oct 2023 20:26:55 -0400 Subject: [PATCH 1/3] markdown: support formatted link text --- .../lexical-markdown/src/MarkdownImport.ts | 36 +++++++++++++++++++ .../src/MarkdownTransformers.ts | 11 +++--- .../__tests__/unit/LexicalMarkdown.test.ts | 8 +++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index 76e40e2ca5e..a47295ceae6 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -17,6 +17,8 @@ import type {LexicalNode, TextNode} from 'lexical'; import {$createCodeNode} from '@lexical/code'; import {$isListItemNode, $isListNode, ListItemNode} from '@lexical/list'; +import {$createLinkNode} from '@lexical/link'; +import {LINK} from '@lexical/markdown'; import {$isQuoteNode} from '@lexical/rich-text'; import {$findMatchingParent} from '@lexical/utils'; import { @@ -209,6 +211,40 @@ function importTextFormatTransformers( textMatchTransformers: Array, ) { const textContent = textNode.getTextContent(); + + // Look for links first. + const linkMatch = LINK.importRegExp.exec(textContent); + if (linkMatch) { + // If the link is the whole text node, then replace it and return. + // Else, split the match from previous and subsequent text nodes + // (if any) and recurse. + if (linkMatch.index === 0 && linkMatch[0].length === textContent.length) { + const [, linkText, linkUrl, linkTitle] = linkMatch; + const linkNode = $createLinkNode(linkUrl, {title: linkTitle}); + const linkTextNode = $createTextNode(linkText); + linkNode.append(linkTextNode); + textNode.replace(linkNode); + importTextFormatTransformers( + linkTextNode, + textFormatTransformersIndex, + textMatchTransformers, + ); + } else { + const nodes = textNode.splitText( + linkMatch.index, + linkMatch.index + linkMatch[0].length, + ); + for (const node of nodes) { + importTextFormatTransformers( + node, + textFormatTransformersIndex, + textMatchTransformers, + ); + } + } + return; + } + const match = findOutermostMatch(textContent, textFormatTransformersIndex); if (!match) { diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index ebc1b448dbc..438ca3c38f4 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -361,16 +361,17 @@ export const LINK: TextMatchTransformer = { return null; } const title = node.getTitle(); - const linkContent = title - ? `[${node.getTextContent()}](${node.getURL()} "${title}")` - : `[${node.getTextContent()}](${node.getURL()})`; + const linkHref = title ? `${node.getURL()} "${title}"` : node.getURL(); const firstChild = node.getFirstChild(); // Add text styles only if link has single text node inside. If it's more // then one we ignore it as markdown does not support nested styles for links if (node.getChildrenSize() === 1 && $isTextNode(firstChild)) { - return exportFormat(firstChild, linkContent); + return `[${exportFormat( + firstChild, + node.getTextContent(), + )}](${linkHref})`; } else { - return linkContent; + return `[${node.getTextContent()}](${linkHref})`; } }, importRegExp: diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index fcc3f20f075..2bc0891c0dc 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -115,6 +115,14 @@ describe('Markdown', () => { html: '

Hello world

', md: '`Hello` world', }, + { + html: '

XXX world

', + md: '[`XXX`](https://lexical.dev) world', + }, + { + html: '

Hello XXX world

', + md: 'Hello [`XXX`](https://lexical.dev) world', + }, { html: '

Hello world

', md: '~~Hello~~ world', From 1da7b6ab6b55c79513e80dd5a1dbd2e8dc9a468f Mon Sep 17 00:00:00 2001 From: Rob Figueiredo Date: Sat, 21 Oct 2023 20:56:22 -0400 Subject: [PATCH 2/3] markdown: support formatting that extends to sibling nodes --- packages/lexical-markdown/src/MarkdownExport.ts | 5 +++-- .../src/__tests__/unit/LexicalMarkdown.test.ts | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownExport.ts b/packages/lexical-markdown/src/MarkdownExport.ts index c0215cdc626..31f84ee178e 100644 --- a/packages/lexical-markdown/src/MarkdownExport.ts +++ b/packages/lexical-markdown/src/MarkdownExport.ts @@ -14,6 +14,7 @@ import type { } from '@lexical/markdown'; import type {ElementNode, LexicalNode, TextFormatType, TextNode} from 'lexical'; +import {$isLinkNode} from '@lexical/link'; import { $getRoot, $isDecoratorNode, @@ -176,7 +177,7 @@ function getTextSibling(node: TextNode, backward: boolean): TextNode | null { if (!sibling) { const parent = node.getParentOrThrow(); - if (parent.isInline()) { + if (parent.isInline() && !$isLinkNode(parent)) { sibling = backward ? parent.getPreviousSibling() : parent.getNextSibling(); @@ -185,7 +186,7 @@ function getTextSibling(node: TextNode, backward: boolean): TextNode | null { while (sibling) { if ($isElementNode(sibling)) { - if (!sibling.isInline()) { + if (!sibling.isInline() || $isLinkNode(sibling)) { break; } diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index 2bc0891c0dc..93661828249 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -123,6 +123,10 @@ describe('Markdown', () => { html: '

Hello XXX world

', md: 'Hello [`XXX`](https://lexical.dev) world', }, + { + html: '

Hello XXY

', + md: '`Hello` [`XXY`](https://lexical.dev)', + }, { html: '

Hello world

', md: '~~Hello~~ world', From d9bbeb707a9a13b19b98f821456508aa6a166699 Mon Sep 17 00:00:00 2001 From: Rob Figueiredo Date: Sat, 11 May 2024 20:22:14 -0400 Subject: [PATCH 3/3] fix lint checks --- packages/lexical-markdown/src/MarkdownImport.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index a47295ceae6..357920aa58b 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -16,8 +16,8 @@ import type { import type {LexicalNode, TextNode} from 'lexical'; import {$createCodeNode} from '@lexical/code'; -import {$isListItemNode, $isListNode, ListItemNode} from '@lexical/list'; import {$createLinkNode} from '@lexical/link'; +import {$isListItemNode, $isListNode, ListItemNode} from '@lexical/list'; import {LINK} from '@lexical/markdown'; import {$isQuoteNode} from '@lexical/rich-text'; import {$findMatchingParent} from '@lexical/utils'; @@ -131,7 +131,7 @@ function $importBlocks( } } - importTextFormatTransformers( + $importTextFormatTransformers( textNode, textFormatTransformersIndex, textMatchTransformers, @@ -205,7 +205,7 @@ function $importCodeBlock( // E.g. for "*Hello **world**!*" string it will create text node with // "Hello **world**!" content and italic format and run recursively over // its content to transform "**world**" part -function importTextFormatTransformers( +function $importTextFormatTransformers( textNode: TextNode, textFormatTransformersIndex: TextFormatTransformersIndex, textMatchTransformers: Array, @@ -224,7 +224,7 @@ function importTextFormatTransformers( const linkTextNode = $createTextNode(linkText); linkNode.append(linkTextNode); textNode.replace(linkNode); - importTextFormatTransformers( + $importTextFormatTransformers( linkTextNode, textFormatTransformersIndex, textMatchTransformers, @@ -235,7 +235,7 @@ function importTextFormatTransformers( linkMatch.index + linkMatch[0].length, ); for (const node of nodes) { - importTextFormatTransformers( + $importTextFormatTransformers( node, textFormatTransformersIndex, textMatchTransformers, @@ -288,7 +288,7 @@ function importTextFormatTransformers( // Recursively run over inner text if it's not inline code if (!currentNode.hasFormat('code')) { - importTextFormatTransformers( + $importTextFormatTransformers( currentNode, textFormatTransformersIndex, textMatchTransformers, @@ -297,7 +297,7 @@ function importTextFormatTransformers( // Run over leading/remaining text if any if (leadingNode) { - importTextFormatTransformers( + $importTextFormatTransformers( leadingNode, textFormatTransformersIndex, textMatchTransformers, @@ -305,7 +305,7 @@ function importTextFormatTransformers( } if (remainderNode) { - importTextFormatTransformers( + $importTextFormatTransformers( remainderNode, textFormatTransformersIndex, textMatchTransformers,