152 lines
3.9 KiB
TypeScript
152 lines
3.9 KiB
TypeScript
const FILE_LIKE_EXTENSION_PATTERN =
|
|
/\.(?:md|markdown|txt|ya?ml|json|toml|csv|tsv|pdf|png|jpe?g|gif|svg|webp|avif|mp3|wav|ogg|mp4|mov|zip|tar|gz|tsx?|jsx?|cjs|mjs|rs|py|sh|css|html?)$/i
|
|
|
|
const EXPLICIT_PROTOCOL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i
|
|
const MAYBE_PROTOCOL_PATTERN = /^[a-z][a-z0-9+.-]*:/i
|
|
const LOCAL_PATH_PREFIX_PATTERN = /^(?:\.{1,2}\/|~\/|\/)/
|
|
const WINDOWS_PATH_SEPARATOR = '\\'
|
|
const WWW_PREFIX = 'www.'
|
|
|
|
export type LinkValue = {
|
|
raw: string
|
|
}
|
|
|
|
export type LinkMarkCandidate = {
|
|
href: LinkValue
|
|
text: LinkValue
|
|
}
|
|
|
|
function withRaw(raw: string): LinkValue {
|
|
return { raw }
|
|
}
|
|
|
|
function stripUrlDecorators(value: LinkValue) {
|
|
return withRaw(value.raw.split(/[?#]/, 1)[0] ?? value.raw)
|
|
}
|
|
|
|
function stripProtocol(value: LinkValue) {
|
|
return withRaw(value.raw.replace(/^[a-z][a-z0-9+.-]*:\/\//i, ''))
|
|
}
|
|
|
|
function isExplicitProtocolUrl(value: LinkValue) {
|
|
return EXPLICIT_PROTOCOL_PATTERN.test(value.raw)
|
|
}
|
|
|
|
function isMaybeProtocolUrl(value: LinkValue) {
|
|
return MAYBE_PROTOCOL_PATTERN.test(value.raw)
|
|
}
|
|
|
|
function hasWindowsPathSeparator(value: LinkValue) {
|
|
return value.raw.includes(WINDOWS_PATH_SEPARATOR)
|
|
}
|
|
|
|
function hasPathSeparator(value: LinkValue) {
|
|
return value.raw.includes('/')
|
|
}
|
|
|
|
function startsWithWebHostPrefix(value: LinkValue) {
|
|
return value.raw.startsWith(WWW_PREFIX)
|
|
}
|
|
|
|
function firstPathSegment(value: LinkValue) {
|
|
return value.raw.split('/', 1)[0] ?? value.raw
|
|
}
|
|
|
|
function isDomainLikePath(value: LinkValue) {
|
|
return firstPathSegment(value).includes('.')
|
|
}
|
|
|
|
function hostnameFromUrlLikeValue(value: LinkValue) {
|
|
const withoutUserinfo = value.raw.includes('@') ? value.raw.split('@').pop() ?? value.raw : value.raw
|
|
return withoutUserinfo.split(/[/?#:]/, 1)[0] ?? withoutUserinfo
|
|
}
|
|
|
|
function isExplicitWebUrl(value: LinkValue) {
|
|
return isExplicitProtocolUrl(value) || startsWithWebHostPrefix(value)
|
|
}
|
|
|
|
function isSingleSegmentValue(value: LinkValue) {
|
|
return !hasPathSeparator(value)
|
|
}
|
|
|
|
function isLocalPathPrefix(value: LinkValue) {
|
|
return LOCAL_PATH_PREFIX_PATTERN.test(value.raw)
|
|
}
|
|
|
|
function isPathLikeFileReference(value: LinkValue) {
|
|
return hasPathSeparator(value) && !isDomainLikePath(value)
|
|
}
|
|
|
|
function hasBareProtocolPrefix(value: LinkValue) {
|
|
return isMaybeProtocolUrl(value) && !value.raw.includes('@')
|
|
}
|
|
|
|
function hostnameHasTld(hostname: string) {
|
|
return hostname.includes('.')
|
|
}
|
|
|
|
function isIpv4Host(hostname: string) {
|
|
const parts = hostname.split('.')
|
|
return parts.length === 4 && parts.every((part) => {
|
|
if (part.length === 0 || part.length > 3) return false
|
|
if ([...part].some((char) => char < '0' || char > '9')) return false
|
|
return Number(part) <= 255
|
|
})
|
|
}
|
|
|
|
function normalizeInput(value: LinkValue) {
|
|
return stripUrlDecorators(withRaw(value.raw.trim()))
|
|
}
|
|
|
|
export function looksLikeLocalFileReference(value: LinkValue) {
|
|
const normalized = normalizeInput(value)
|
|
if (!normalized.raw) return false
|
|
|
|
if (isLocalPathPrefix(normalized) || hasWindowsPathSeparator(normalized)) {
|
|
return true
|
|
}
|
|
|
|
if (!FILE_LIKE_EXTENSION_PATTERN.test(normalized.raw)) {
|
|
return false
|
|
}
|
|
|
|
if (isExplicitWebUrl(normalized)) {
|
|
return false
|
|
}
|
|
|
|
if (isSingleSegmentValue(normalized)) {
|
|
return true
|
|
}
|
|
|
|
return isPathLikeFileReference(normalized)
|
|
}
|
|
|
|
export function shouldAutoLinkTolariaHref(url: LinkValue) {
|
|
if (looksLikeLocalFileReference(url)) {
|
|
return false
|
|
}
|
|
|
|
if (isExplicitProtocolUrl(url) || hasBareProtocolPrefix(url)) {
|
|
return true
|
|
}
|
|
|
|
const hostname = hostnameFromUrlLikeValue(url)
|
|
if (isIpv4Host(hostname)) {
|
|
return false
|
|
}
|
|
|
|
return hostnameHasTld(hostname)
|
|
}
|
|
|
|
export function shouldStripAutoLinkedLocalFileMark(mark: LinkMarkCandidate) {
|
|
const normalizedText = normalizeInput(mark.text)
|
|
if (!looksLikeLocalFileReference(normalizedText)) {
|
|
return false
|
|
}
|
|
|
|
const normalizedHref = stripUrlDecorators(
|
|
stripProtocol(normalizeInput(mark.href)),
|
|
)
|
|
return normalizedHref.raw === normalizedText.raw
|
|
}
|