From 558383de11643e8da7f298db4f485b2fb294fd16 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Sat, 28 Sep 2024 00:46:10 +0200 Subject: [PATCH 1/4] feat(lexical-markdown): add getEndIndex to TextMatchTransformer --- packages/lexical-markdown/src/MarkdownImport.ts | 5 ++++- packages/lexical-markdown/src/MarkdownTransformers.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index 99b7900b144..9b2c0dcfe9e 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -369,7 +369,10 @@ function importTextMatchTransformers( } const startIndex = match.index || 0; - const endIndex = startIndex + match[0].length; + const endIndex = + transformer && transformer.getEndIndex + ? transformer.getEndIndex(textNode, match) + : startIndex + match[0].length; let replaceNode, newTextNode; if (startIndex === 0) { diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index b724b3ba0ca..ac1c406c578 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -161,6 +161,14 @@ export type TextMatchTransformer = Readonly<{ * Determines how the matched markdown text should be transformed into a node during the markdown import process */ replace?: (node: TextNode, match: RegExpMatchArray) => void; + /** + * + * For import operations, this function can be used to determine the end index of the match, after `importRegExp` has matched. + * Without this function, the end index will be determined by the length of the match from `importRegExp`. Manually determining the end index can be useful if + * the match from `importRegExp` is not the entire text content of the node. That way, `importRegExp` can be used to match only the start of the node, and `getEndIndex` + * can be used to match the end of the node. + */ + getEndIndex?: (node: TextNode, match: RegExpMatchArray) => number; /** * Single character that allows the transformer to trigger when typed in the editor. This does not affect markdown imports outside of the markdown shortcut plugin. * If the trigger is matched, the `regExp` will be used to match the text in the second step. From ce86444e061e991be8a2099c3a861282922354a9 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Sat, 28 Sep 2024 00:57:52 +0200 Subject: [PATCH 2/4] simplify --- packages/lexical-markdown/src/MarkdownImport.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index 9b2c0dcfe9e..273001b427d 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -369,10 +369,10 @@ function importTextMatchTransformers( } const startIndex = match.index || 0; - const endIndex = - transformer && transformer.getEndIndex - ? transformer.getEndIndex(textNode, match) - : startIndex + match[0].length; + const endIndex = transformer.getEndIndex + ? transformer.getEndIndex(textNode, match) + : startIndex + match[0].length; + let replaceNode, newTextNode; if (startIndex === 0) { From 3438c045991fc4ab9f1918197f15dff7a2dfee5f Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Mon, 14 Oct 2024 12:03:02 -0600 Subject: [PATCH 3/4] add ability to not match --- packages/lexical-markdown/src/MarkdownImport.ts | 4 ++++ packages/lexical-markdown/src/MarkdownTransformers.ts | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index 273001b427d..7458e2b46a1 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -373,6 +373,10 @@ function importTextMatchTransformers( ? transformer.getEndIndex(textNode, match) : startIndex + match[0].length; + if (endIndex === false) { + continue; + } + let replaceNode, newTextNode; if (startIndex === 0) { diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index ac1c406c578..6b054183f2a 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -162,13 +162,14 @@ export type TextMatchTransformer = Readonly<{ */ replace?: (node: TextNode, match: RegExpMatchArray) => void; /** - * * For import operations, this function can be used to determine the end index of the match, after `importRegExp` has matched. * Without this function, the end index will be determined by the length of the match from `importRegExp`. Manually determining the end index can be useful if * the match from `importRegExp` is not the entire text content of the node. That way, `importRegExp` can be used to match only the start of the node, and `getEndIndex` * can be used to match the end of the node. + * + * @returns The end index of the match, or false if the match was unsuccessful and a different transformer should be tried. */ - getEndIndex?: (node: TextNode, match: RegExpMatchArray) => number; + getEndIndex?: (node: TextNode, match: RegExpMatchArray) => number | false; /** * Single character that allows the transformer to trigger when typed in the editor. This does not affect markdown imports outside of the markdown shortcut plugin. * If the trigger is matched, the `regExp` will be used to match the text in the second step. From 9ae0ef95bd73e68b323747071bbc281380867d56 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Tue, 5 Nov 2024 00:07:33 -0700 Subject: [PATCH 4/4] add test --- .../__tests__/unit/LexicalMarkdown.test.ts | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index be7199eefab..f78fc4a3056 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -9,7 +9,7 @@ import {$createCodeNode, CodeNode} from '@lexical/code'; import {createHeadlessEditor} from '@lexical/headless'; import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html'; -import {LinkNode} from '@lexical/link'; +import {$createLinkNode, LinkNode} from '@lexical/link'; import {ListItemNode, ListNode} from '@lexical/list'; import {HeadingNode, QuoteNode} from '@lexical/rich-text'; import {$createTextNode, $getRoot, $insertNodes} from 'lexical'; @@ -28,6 +28,51 @@ import { normalizeMarkdown, } from '../../MarkdownTransformers'; +const SIMPLE_INLINE_JSX_MATCHER: TextMatchTransformer = { + dependencies: [LinkNode], + getEndIndex(node, match) { + // Find the closing tag. Count the number of opening and closing tags to find the correct closing tag. + // For simplicity, this will only count the opening and closing tags without checking for "MyTag" specifically. + let openedSubStartMatches = 0; + const start = (match.index ?? 0) + match[0].length; + let endIndex = start; + const line = node.getTextContent(); + + for (let i = start; i < line.length; i++) { + const char = line[i]; + if (char === '<') { + const nextChar = line[i + 1]; + if (nextChar === '/') { + if (openedSubStartMatches === 0) { + endIndex = i + ''.length; + break; + } + openedSubStartMatches--; + } else { + openedSubStartMatches++; + } + } + } + return endIndex; + }, + importRegExp: /<(MyTag)\s*>/, + regExp: /__ignore__/, + replace: (textNode, match) => { + const linkNode = $createLinkNode('simple-jsx'); + + const textStart = match[0].length + (match.index ?? 0); + const textEnd = + (match.index ?? 0) + textNode.getTextContent().length - ''.length; + const text = match.input?.slice(textStart, textEnd); + + const linkTextNode = $createTextNode(text); + linkTextNode.setFormat(textNode.getFormat()); + linkNode.append(linkTextNode); + textNode.replace(linkNode); + }, + type: 'text-match', +}; + // Matches html within a mdx file const MDX_HTML_TRANSFORMER: MultilineElementTransformer = { dependencies: [CodeNode], @@ -461,6 +506,12 @@ describe('Markdown', () => { md: '```ts\nCode\n```ts\nSub Code\n```\n```', skipExport: true, }, + { + customTransformers: [SIMPLE_INLINE_JSX_MATCHER], + html: '

Hello One <MyTag>Two</MyTag> there

', + md: 'Hello One Two there', + skipExport: true, + }, ]; const HIGHLIGHT_TEXT_MATCH_IMPORT: TextMatchTransformer = {