diff --git a/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs b/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs index b13e2e4a70a8..e7d0688fc2b5 100644 --- a/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs @@ -382,4 +382,135 @@ test.describe('Auto Links', () => { {ignoreClasses: true}, ); }); + + test('Can convert URLs into links', async ({page, isPlainText}) => { + const testUrls = [ + // Basic URLs + 'http://example.com', // Standard HTTP URL + 'https://example.com', // Standard HTTPS URL + 'http://www.example.com', // HTTP URL with www + 'https://www.example.com', // HTTPS URL with www + 'www.example.com', // Missing HTTPS Protocol + + // With Different TLDs + 'http://example.org', // URL with .org TLD + 'https://example.net', // URL with .net TLD + 'http://example.co.uk', // URL with country code TLD + 'https://example.xyz', // URL with generic TLD + + // With Paths + 'http://example.com/path/to/resource', // URL with path + 'https://www.example.com/path/to/resource', // URL with www and path + + // With Query Parameters + 'http://example.com/path?name=value', // URL with query parameters + 'https://www.example.com/path?name=value&another=value2', // URL with multiple query parameters + + // With Fragments + 'http://example.com/path#section', // URL with fragment + 'https://www.example.com/path/to/resource#fragment', // URL with path and fragment + + // With Port Numbers + 'http://example.com:8080', // URL with port number + 'https://www.example.com:443/path', // URL with port number and path + + // IP Addresses + 'http://192.168.0.1', // URL with IPv4 address + 'https://127.0.0.1', // URL with localhost IPv4 address + + // With Special Characters in Path and Query + 'http://example.com/path/to/res+ource', // URL with plus in path + 'https://example.com/path/to/res%20ource', // URL with encoded space in path + 'http://example.com/path?name=va@lue', // URL with special character in query + 'https://example.com/path?name=value&another=val%20ue', // URL with encoded space in query + + // Subdomains and Uncommon TLDs + 'http://subdomain.example.com', // URL with subdomain + 'https://sub.subdomain.example.com', // URL with multiple subdomains + 'http://example.museum', // URL with uncommon TLD + 'https://example.travel', // URL with uncommon TLD + + // Edge Cases + 'http://foo.bar', // Minimal URL with uncommon TLD + 'https://foo.bar', // HTTPS minimal URL with uncommon TLD + ]; + + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type(testUrls.join(' ') + ' '); + + let expectedHTML = ''; + for (let url of testUrls) { + url = url.replaceAll(/&/g, '&'); + const rawUrl = url; + + if (!url.startsWith('http')) { + url = `https://${url}`; + } + + expectedHTML += ` + + ${rawUrl} + + + `; + } + + await assertHTML( + page, + html` +

${expectedHTML}

+ `, + undefined, + {ignoreClasses: true}, + ); + }); + + test(`Can not convert bad URLs into links`, async ({page, isPlainText}) => { + const testUrls = [ + // Missing Protocol + 'example.com', // Missing HTTPS and www + + // Invalid Protocol + 'htp://example.com', // Typo in protocol + 'htps://example.com', // Typo in protocol + + // Invalid TLDs + 'http://example.abcdefg', // TLD too long + + // Spaces and Invalid Characters + 'http://exa mple.com', // Space in domain + 'https://example .com', // Space in domain + 'http://example!.com', // Invalid character in domain + + // Missing Domain + 'http://.com', // Missing domain name + 'https://.org', // Missing domain name + + // Incomplete URLs + 'http://', // Incomplete URL + 'https://', // Incomplete URL + + // Just Text + 'not_a_url', // Plain text + 'this is not a url', // Sentence + 'example', // Single word + 'ftp://example.com', // Unsupported protocol (assuming only HTTP/HTTPS is supported) + ]; + + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type(testUrls.join(' ')); + + await assertHTML( + page, + html` +

+ ${testUrls.join(' ')} +

+ `, + undefined, + {ignoreClasses: true}, + ); + }); }); diff --git a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts index 63e7212b5b29..71cf021fad5f 100644 --- a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts +++ b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts @@ -75,7 +75,7 @@ function findFirstMatch( return null; } -const PUNCTUATION_OR_SPACE = /[.,;\s]/; +const PUNCTUATION_OR_SPACE = /[,;\s]/; function isSeparator(char: string): boolean { return PUNCTUATION_OR_SPACE.test(char);