mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
fd838d4e2d
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 -->
150 lines
2.7 KiB
TypeScript
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;
|
|
}
|
|
}
|