40 lines
1.3 KiB
TypeScript
40 lines
1.3 KiB
TypeScript
const WEB_URL = /^https?:\/\/[^\s\\/?#]+(?:[/?#]\S*)?$/i
|
|
const MAIL_URL = /^mailto:[^\s@/?#]+@[^\s@]+$/i
|
|
const SITE_PATH = /^\/(?![/\\])\S*$/
|
|
const ANCHOR = /^#\S+$/
|
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/g
|
|
const HAS_CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/
|
|
const SCHEME = /^([a-z][a-z0-9+.-]*):/i
|
|
const STORED_SCHEMES = ['http', 'https', 'mailto', 'tel', 'ftp']
|
|
|
|
export function normalizeLinkUrl(raw: string): string | null {
|
|
const url = raw.trim()
|
|
if (HAS_CONTROL_CHARACTER.test(url)) {
|
|
return null
|
|
}
|
|
if (WEB_URL.test(url) || MAIL_URL.test(url) || SITE_PATH.test(url) || ANCHOR.test(url)) {
|
|
return url
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function isStoredHrefSafe(raw: string): boolean {
|
|
const visible = raw.replace(CONTROL_CHARACTERS, '').trim().replace(/\s/g, '')
|
|
const scheme = SCHEME.exec(visible)
|
|
if (scheme === null) {
|
|
return visible !== ''
|
|
}
|
|
return STORED_SCHEMES.includes(scheme[1]!.toLowerCase())
|
|
}
|
|
|
|
export function neutralizeUnsafeLinks(html: string): string {
|
|
const document = new DOMParser().parseFromString(html, 'text/html')
|
|
for (const anchor of document.body.querySelectorAll('a')) {
|
|
if (isStoredHrefSafe(anchor.getAttribute('href') ?? '')) {
|
|
continue
|
|
}
|
|
anchor.replaceWith(...anchor.childNodes)
|
|
}
|
|
return document.body.innerHTML
|
|
}
|