From 2c1a8f10c84444560c0982784ce3c14242f82340 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Tue, 5 Nov 2024 10:35:12 -0700 Subject: [PATCH] [lexical-markdown] Feature: add ability to control finding the end of a node matched by TextMatchTransformer (#6681) --- .../lexical-markdown/src/MarkdownImport.ts | 9 +++- .../src/MarkdownTransformers.ts | 9 ++++ .../__tests__/unit/LexicalMarkdown.test.ts | 53 ++++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index 2f7dc27324b..47f56dd3b7e 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -383,7 +383,14 @@ function importTextMatchTransformers( } const startIndex = match.index || 0; - const endIndex = startIndex + match[0].length; + const endIndex = transformer.getEndIndex + ? 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 2a335156213..2a4fa5d5cf0 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -174,6 +174,15 @@ 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. + * + * @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 | 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. 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 = {