From 260daa7eb6d21f4a89d7afbeef9917c5bfc419b1 Mon Sep 17 00:00:00 2001
From: Maksym Plavinskyi
+ Hello
+
+ name@example.com
+
+ and
+
+ anothername@test.example.uk
+
+ !
+
+ Hello
+
+ name@example.com
+
+ and
+
+ anothername@test.example.uk
+
+ and
+
+ www.example.com
+
+ !
+
+ + Hello name@example.c name@example.1 + + + name-lastname@example.com + + + + name.lastname@meta.com + +
+ `, + undefined, + {ignoreClasses: true}, + ); + }); + test('Can convert url-like text with formatting into links', async ({ page, isPlainText, @@ -467,6 +571,47 @@ test.describe('Auto Links', () => { ); }); + test('Can convert URLs into email links', async ({page, isPlainText}) => { + const testUrls = [ + // Email usecases + 'email@domain.com', + 'firstname.lastname@domain.com', + 'email@subdomain.domain.com', + 'firstname+lastname@domain.com', + 'email@[123.123.123.123]', + '"email"@domain.com', + '1234567890@domain.com', + 'email@domain-one.com', + '_______@domain.com', + 'email@domain.name', + 'email@domain.co.uk', + 'firstname-lastname@domain.com', + ]; + + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type(testUrls.join(' ') + ' '); + + let expectedHTML = ''; + for (const url of testUrls) { + expectedHTML += ` + + ${url} + + + `; + } + + await assertHTML( + page, + html` +${expectedHTML}
+ `, + undefined, + {ignoreClasses: true}, + ); + }); + test(`Can not convert bad URLs into links`, async ({page, isPlainText}) => { const testUrls = [ // Missing Protocol @@ -515,6 +660,45 @@ test.describe('Auto Links', () => { ); }); + test(`Can not convert bad URLs into email links`, async ({ + page, + isPlainText, + }) => { + const testUrls = [ + '@domain.com', + '@subdomain.domain.com', + + // Invalid Characters + 'email@domain!.com', // Invalid character in domain + 'email@domain.c', // Invalid top level domain + + // Missing Domain + 'email@.com', + 'email@.org', + + // Incomplete URLs + 'email@', // Incomplete URL + + // Just Text + 'not_an_email', // Plain text + ]; + + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type(testUrls.join(' ')); + + await assertHTML( + page, + html` ++ ${testUrls.join(' ')} +
+ `, + undefined, + {ignoreClasses: true}, + ); + }); + test('Can unlink the autolink and then make it link again', async ({ page, isPlainText, diff --git a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts index 5eaa8e85300..bb859ca82c6 100644 --- a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts +++ b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts @@ -91,8 +91,19 @@ function startsWithSeparator(textContent: string): boolean { return isSeparator(textContent[0]); } -function startsWithFullStop(textContent: string): boolean { - return /^\.[a-zA-Z0-9]{1,}/.test(textContent); +/** + * Check if the text content starts with a fullstop followed by a top-level domain. + * Meaning if the text content can be a beginning of a top level domain. + * @param textContent + * @param isEmail + * @returns boolean + */ +function startsWithTLD(textContent: string, isEmail: boolean): boolean { + if (isEmail) { + return /^\.[a-zA-Z]{2,}/.test(textContent); + } else { + return /^\.[a-zA-Z0-9]{1,}/.test(textContent); + } } function isPreviousNodeValid(node: LexicalNode): boolean { @@ -385,7 +396,8 @@ function handleBadNeighbors( if ( $isAutoLinkNode(previousSibling) && !previousSibling.getIsUnlinked() && - (!startsWithSeparator(text) || startsWithFullStop(text)) + (!startsWithSeparator(text) || + startsWithTLD(text, previousSibling.isEmailURI())) ) { previousSibling.append(textNode); handleLinkEdit(previousSibling, matchers, onChange);