diff --git a/packages/richtext-lexical/src/lexical/utils/url.spec.ts b/packages/richtext-lexical/src/lexical/utils/url.spec.ts new file mode 100644 index 00000000000..40abcf98f9b --- /dev/null +++ b/packages/richtext-lexical/src/lexical/utils/url.spec.ts @@ -0,0 +1,88 @@ +import { jest } from '@jest/globals' +import { absoluteRegExp, relativeOrAnchorRegExp } from './url.js' + +describe('Lexical URL Regex Matchers', () => { + describe('relative URLs', () => { + it('validation for links it should match', async () => { + const shouldMatch = [ + '/path/to/resource', + '/file-name.html', + '/', + '/dir/', + '/path.with.dots/', + '#anchor', + '#section-title', + '/path#fragment', + ] + + shouldMatch.forEach((testCase) => { + expect(relativeOrAnchorRegExp.test(testCase)).toBe(true) + }) + }) + + it('validation for links it should not match', async () => { + const shouldNotMatch = [ + 'match', + 'http://example.com', + 'relative/path', + 'file.html', + 'some#fragment', + '#', + '/#', + '/path/with spaces', + '', + 'ftp://example.com', + ] + + shouldNotMatch.forEach((testCase) => { + expect(relativeOrAnchorRegExp.test(testCase)).not.toBe(true) + }) + }) + }) + + describe('absolute URLs', () => { + it('validation for links it should match', async () => { + const shouldMatch = [ + 'http://example.com', + 'https://example.com', + 'ftp://files.example.com', + 'http://example.com/resource', + 'https://example.com/resource?key=value', + 'http://example.com/resource#anchor', + 'http://www.example.com', + 'https://sub.example.com/path/file', + 'mailto:email@example.com', + 'tel:+1234567890', + 'http://user:pass@example.com', + 'www.example.com', + 'www.example.com/resource', + 'www.example.com/resource?query=1', + 'www.example.com#fragment', + ] + + shouldMatch.forEach((testCase) => { + expect(absoluteRegExp.test(testCase)).toBe(true) + }) + }) + + it('validation for links it should not match', async () => { + const shouldNotMatch = [ + '/relative/path', + '#anchor', + 'example.com', + '://missing.scheme', + 'http://', + 'http:/example.com', + 'ftp://example .com', + 'http://example', + 'not-a-url', + 'http//example.com', + 'https://example.com/ spaces', + ] + + shouldNotMatch.forEach((testCase) => { + expect(absoluteRegExp.test(testCase)).not.toBe(true) + }) + }) + }) +}) diff --git a/packages/richtext-lexical/src/lexical/utils/url.ts b/packages/richtext-lexical/src/lexical/utils/url.ts index 57cdb1e543d..0719f030602 100644 --- a/packages/richtext-lexical/src/lexical/utils/url.ts +++ b/packages/richtext-lexical/src/lexical/utils/url.ts @@ -15,9 +15,20 @@ export function sanitizeUrl(url: string): string { return 'https://' } -// Source: https://stackoverflow.com/a/8234912/2013580 -const absoluteRegExp = - /(?:[A-Za-z]{3,9}:(?:\/\/)?(?:[-;:&=+$,\w]+@)?[A-Za-z\d.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z\d.-]+)(?:\/[+~%/.\w-]*)?\??[-+=&;%@.\w]*#?\w*/ +/** + * This regex checks for absolute URLs in a string. Tested for the following use cases: + * - http://example.com + * - https://example.com + * - ftp://files.example.com + * - http://example.com/resource + * - https://example.com/resource?key=value + * - http://example.com/resource#anchor + * - http://www.example.com + * - https://sub.example.com/path/file + * - mailto: + */ +export const absoluteRegExp = + /^(?:[a-zA-Z][a-zA-Z\d+.-]*:(?:\/\/)?(?:[-;:&=+$,\w]+@)?[A-Za-z\d]+(?:\.[A-Za-z\d]+)+|www\.[A-Za-z\d]+(?:\.[A-Za-z\d]+)+|(?:tel|mailto):[\w+.-]+)(?:\/[+~%/\w-]*)?(?:\?[-;&=%\w]*)?(?:#\w+)?$/ /** * This regex checks for relative URLs starting with / or anchor links starting with # in a string. Tested for the following use cases: @@ -25,7 +36,7 @@ const absoluteRegExp = * - /privacy-policy#primary-terms * - #primary-terms * */ -const relativeOrAnchorRegExp = /^[\w\-./]*(?:#\w[\w-]*)?$/ +export const relativeOrAnchorRegExp = /^(?:\/[\w\-./]*(?:#\w[\w-]*)?|#[\w\-]+)$/ /** * Prevents unreasonable URLs from being inserted into the editor.