Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lexical][auto-link] Fix auto link node escapes on second "." #9

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, '&');
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