Skip to content

Commit

Permalink
[lexical][auto-link] Fix auto link node escapes on second "." (#6146)
Browse files Browse the repository at this point in the history
Co-authored-by: Maksym Plavinskyi <Maksym_Plavinskyi@epam.com>
  • Loading branch information
MaxPlav and Maksym Plavinskyi authored Jun 12, 2024
1 parent a82b1dc commit 1f2471f
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 2 deletions.
131 changes: 131 additions & 0 deletions packages/lexical-playground/__tests__/e2e/AutoLinks.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, '&amp;');
const rawUrl = url;

if (!url.startsWith('http')) {
url = `https://${url}`;
}

expectedHTML += `
<a href="${url}" dir="ltr">
<span data-lexical-text="true">${rawUrl}</span>
</a>
<span data-lexical-text="true"></span>
`;
}

await assertHTML(
page,
html`
<p dir="ltr">${expectedHTML}</p>
`,
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`
<p dir="ltr">
<span data-lexical-text="true">${testUrls.join(' ')}</span>
</p>
`,
undefined,
{ignoreClasses: true},
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +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?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)(?<![-.+():%])/;

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,}))/;
Expand Down
9 changes: 8 additions & 1 deletion packages/lexical-react/src/LexicalAutoLinkPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ function startsWithSeparator(textContent: string): boolean {
return isSeparator(textContent[0]);
}

function startsWithFullStop(textContent: string): boolean {
return /^\.[a-zA-Z0-9]{1,}/.test(textContent);
}

function isPreviousNodeValid(node: LexicalNode): boolean {
let previousNode = node.getPreviousSibling();
if ($isElementNode(previousNode)) {
Expand Down Expand Up @@ -376,7 +380,10 @@ function handleBadNeighbors(
const nextSibling = textNode.getNextSibling();
const text = textNode.getTextContent();

if ($isAutoLinkNode(previousSibling) && !startsWithSeparator(text)) {
if (
$isAutoLinkNode(previousSibling) &&
(!startsWithSeparator(text) || startsWithFullStop(text))
) {
previousSibling.append(textNode);
handleLinkEdit(previousSibling, matchers, onChange);
onChange(null, previousSibling.getURL());
Expand Down

0 comments on commit 1f2471f

Please sign in to comment.