From 0f7c78b24dec932c9e1629623bedaf4485eacb08 Mon Sep 17 00:00:00 2001 From: Chinmay Disale Date: Fri, 24 Nov 2023 02:20:01 +0530 Subject: [PATCH] Fix: Autolink plugin URL recognition failures (#5275) --- .../__tests__/e2e/AutoLinks.spec.mjs | 96 +++++++++++++++++++ .../src/plugins/AutoLinkPlugin/index.tsx | 3 +- .../src/LexicalAutoLinkPlugin.ts | 2 +- 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs b/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs index f02109bb3a9..f5e045ae6c4 100644 --- a/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs @@ -307,4 +307,100 @@ test.describe('Auto Links', () => { {ignoreClasses: true}, ); }); + + test(`Does not convert bad URLs into links`, async ({page, isPlainText}) => { + const badUrls = [ + 'http://', + 'http://.', + 'http://..', + 'http://../', + 'http://?', + 'http://??', + 'http://??/', + 'http://#', + 'http://##', + 'http://##/', + '//', + '//a', + '///a', + '///', + 'http:///a', + 'rdar://1234', + 'h://test', + ':// should fail', + 'http://foo.bar/foo(bar)baz quux', + 'http://-error-.invalid/', + 'http://-a.b.co', + 'http://a.b-.co', + 'http://ex..ample.com', + 'http://example..com', + 'http://example-.com', + 'http://-example.com', + ]; + + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type(badUrls.join(' ')); + + await assertHTML( + page, + html` +

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

+ `, + undefined, + {ignoreClasses: true}, + ); + }); + + test('Does convert good complex URLs into links', async ({ + page, + isPlainText, + }) => { + const goodUrls = [ + 'http://foo.com/blah_blah', + 'http://foo.com/blah_blah/', + 'http://www.example.com/wpstyle/?p=364', + 'https://www.example.com/foo/?bar=baz&inga=42&quux', + 'http://foo.com/something?after=parens', + 'http://jlo.mp', + 'http://1337.net', + 'http://a.b-c.de', + // // Include IPs and localhost + 'http://localhost', + 'http://localhost:3000', + 'http://192.168.1.1', + 'http://192.168.1.1:3000', + 'http://example.com', + 'http://example.com/path/to/resource?query=string#fragment', + 'https://username:password@example.com', + 'http://example.com/path/to/page.html?query=string#fragment', + 'https://example.com#anchor', + 'http://abcdefghij.com', + ]; + + test.skip(isPlainText); + await focusEditor(page); + await page.keyboard.type(goodUrls.join(' ') + ' '); + + let expectedHTML = ''; + for (const url of goodUrls) { + expectedHTML += ` + + ${url.replace(/&/g, '&')} + + + `; + } + + await assertHTML( + page, + html` +

${expectedHTML}

+ `, + undefined, + {ignoreClasses: true}, + ); + }); }); diff --git a/packages/lexical-playground/src/plugins/AutoLinkPlugin/index.tsx b/packages/lexical-playground/src/plugins/AutoLinkPlugin/index.tsx index 509c3defd50..e822e7fa13b 100644 --- a/packages/lexical-playground/src/plugins/AutoLinkPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/AutoLinkPlugin/index.tsx @@ -13,8 +13,7 @@ import { import * as React from 'react'; const URL_REGEX = - /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/; - + /(https?:\/\/)?((\w+:\w+@)?(([a-zA-Z\d]([a-zA-Z\d-]*[a-zA-Z\d])*)\.)+[a-zA-Z]{2,}|localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?(\/[-a-zA-Z\d%_.~+]*)*(\?[;&a-zA-Z\d%_.~+=-]*)?(#[-a-zA-Z\d_]*)?/; const EMAIL_REGEX = /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/; diff --git a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts index b28917ddb0c..4d67dc15e67 100644 --- a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts +++ b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts @@ -73,7 +73,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);