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 }