Files
AFFiNE-Mirror/blocksuite/affine/shared/src/utils/url.ts
T
fundon fd838d4e2d fix(editor): should add HTTP protocol into link automatically (#11934)
Closes: [BS-3291](https://linear.app/affine-design/issue/BS-3291/工具栏展开时报错,链接无法点击打开)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - URLs entered without a protocol (e.g., "github.com/...") are now automatically normalized to use "https://", ensuring links are secure and consistently formatted.

- **Bug Fixes**
  - Improved handling and validation of links to prevent issues with missing or invalid protocols in bookmarks and inline links.
  - Simplified URL validation logic by leveraging native URL parsing, removing complex regex and email-specific checks.
  - Streamlined toolbar link actions to operate only on valid normalized URLs.
  - Refined URL detection in markdown preprocessing to exclude lines containing spaces from being treated as URLs.

- **Tests**
  - Added tests to verify that links without a protocol are correctly normalized and displayed across different views.
  - Updated URL validation tests to better reflect valid and invalid URL formats, including IP addresses and domain variants.

- **Style**
  - Updated snapshots to reflect the use of "https://" in links.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-19 17:05:06 +00:00

150 lines
2.7 KiB
TypeScript

// https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
const ALLOWED_SCHEMES = new Set([
'http',
'https',
'ftp',
'sftp',
'mailto',
'tel',
]);
// https://publicsuffix.org/
const TLD_REGEXP = /(?:\.[a-zA-Z]+)?(\.[a-zA-Z]{2,})$/;
const toURL = (str: string) => {
try {
if (!URL.canParse(str)) return null;
return new URL(str);
} catch {
return null;
}
};
function resolveURL(str: string) {
const url = toURL(str);
if (!url) return null;
const protocol = url.protocol.substring(0, url.protocol.length - 1);
const hostname = url.hostname;
let allowed = ALLOWED_SCHEMES.has(protocol);
if (allowed && hostname.includes('.')) {
allowed = TLD_REGEXP.test(hostname);
}
return { url, allowed };
}
export function normalizeUrl(str: string) {
str = str.trim();
let url = toURL(str);
if (!url) {
const hasScheme = str.match(/^https?:\/\//);
if (!hasScheme) {
const dotIdx = str.indexOf('.');
if (dotIdx > 0 && dotIdx < str.length - 1) {
url = toURL(`https://${str}`);
}
}
}
// Formatted
if (url) {
if (!str.endsWith('/') && url.href.endsWith('/')) {
return url.href.substring(0, url.href.length - 1);
}
return url.href;
}
return str;
}
/**
* Assume user will input a url, we just need to check if it is valid.
*
* For more detail see https://www.ietf.org/rfc/rfc1738.txt
*/
export function isValidUrl(str: string) {
str = str.trim();
let result = resolveURL(str);
if (result && !result.allowed) return false;
if (!result) {
const hasScheme = str.match(/^https?:\/\//);
if (!hasScheme) {
const dotIdx = str.indexOf('.');
if (dotIdx > 0 && dotIdx < str.length - 1) {
result = resolveURL(`https://${str}`);
}
}
}
return result?.allowed ?? false;
}
// https://en.wikipedia.org/wiki/Top-level_domain
const COMMON_TLDS = new Set([
'com',
'org',
'net',
'edu',
'gov',
'co',
'io',
'me',
'moe',
'mil',
'top',
'dev',
'xyz',
'info',
'cat',
'ru',
'de',
'jp',
'uk',
'pro',
]);
function isCommonTLD(url: URL) {
const tld = url.hostname.split('.').pop() ?? '';
return COMMON_TLDS.has(tld);
}
/**
* Assuming the user will input anything, we need to check rigorously.
*/
export function isStrictUrl(str: string) {
try {
if (!isValidUrl(str)) {
return false;
}
const url = new URL(normalizeUrl(str));
return isCommonTLD(url);
} catch {
return false;
}
}
export function isUrlInClipboard(clipboardData: DataTransfer) {
const url = clipboardData.getData('text/plain');
return isValidUrl(url);
}
export function getHostName(link: string) {
try {
const url = new URL(link);
return url.hostname || url.pathname;
} catch {
return link;
}
}