import { DOMUtils, utils } from "@/env"; /** * 链接文本转超链接 */ export const MTIdentifyLinks = () => { /*TEXT link to Clickable Hyperlink*/ const HANDLER_CLASS_NAME = "texttolink"; /** * URL匹配正则表达式 */ const url_regexp = /((https?:\/\/|www\.)[\x21-\x7e]+[\w\/]|(\w[\w._-]+\.(com|cn|org|net|info|tv|cc))(\/[\x21-\x7e]*[\w\/])?|ed2k:\/\/[\x21-\x7e]+\|\/|thunder:\/\/[\x21-\x7e]+=)/gi; /** * 处理链接点击事件,确保链接有正确的协议前缀 */ const handleClearLink = function (event: MouseEvent) { // @ts-ignore let targetElement: HTMLElement | null = event.originalTarget ?? event.target; let url: string; if ( null != targetElement && "a" === targetElement.localName && -1 !== targetElement.className.indexOf(HANDLER_CLASS_NAME) && ((url = targetElement.getAttribute("href")!), typeof url === "string" && 0 !== url.indexOf("http") && 0 !== url.indexOf("ed2k://") && 0 !== url.indexOf("thunder://")) ) { return targetElement.setAttribute("href", "http://" + targetElement); } }; /** * 将文本中的URL转换为可点击的超链接 */ const setLink = function (textNode: Node | null) { // 增加类型检查避免报错 if (typeof textNode != "object" || textNode == null) { return; } const textContent = textNode?.textContent; const $parent = textNode?.parentNode as ParentNode | null; if ($parent == null) { return; } if ( // @ts-ignore -1 === $parent?.className?.indexOf?.(HANDLER_CLASS_NAME) && "#cdata-section" !== textNode.nodeName && typeof textContent === "string" ) { // 修改后的文本 const modifiedContent = textContent.replace( url_regexp, `$1` ); // 纯本文和修改后的链接文本长度对比 // 如果长度一致,则说明没有修改,不需要处理 // 如果长度不一致,则说明有修改,需要处理替换 if (textContent.length !== modifiedContent.length) { const spanElement = document.createElement("span"); DOMUtils.html(spanElement, modifiedContent); const $url = spanElement.querySelector("a")!; const url = $url.href!; console.log(`识别: ${url}`); const isSpanParent = $parent.nodeName.toLowerCase() === "span"; if (isSpanParent) { return $parent.replaceChild($url, textNode); } else { return $parent.replaceChild(spanElement, textNode); } } } }; /** * 排除不需要处理的标签 */ const excludedTags = "a svg canvas applet input button area pre embed frame frameset head iframe img option map meta noscript object script style textarea code".split( " " ); const xpath = `//text()[not(ancestor::${excludedTags.join(") and not(ancestor::")})]`; const filter = new RegExp(`^(${excludedTags.join("|")})$`, "i"); /** * 分批处理链接以避免阻塞UI */ const processLinksInBatches = function (textNodesSnapshot: XPathResult, startIndex: number) { let currentIndex, endIndex; if (startIndex + 10000 < textNodesSnapshot.snapshotLength) { let start = (currentIndex = startIndex); for ( endIndex = startIndex + 10000; startIndex <= endIndex ? currentIndex <= endIndex : currentIndex >= endIndex; start = startIndex <= endIndex ? ++currentIndex : --currentIndex ) { setLink(textNodesSnapshot.snapshotItem(start)); } setTimeout(function () { return processLinksInBatches(textNodesSnapshot, startIndex + 10000); }, 15); } else { let start; for ( start = currentIndex = startIndex, endIndex = textNodesSnapshot.snapshotLength; startIndex <= endIndex ? currentIndex <= endIndex : currentIndex >= endIndex; start = startIndex <= endIndex ? ++currentIndex : --currentIndex ) { setLink(textNodesSnapshot.snapshotItem(start)); } } }; /** * 处理指定元素内的文本链接 */ const linkifyText = function (element: HTMLElement) { const textNodesSnapshot = document.evaluate(xpath, element, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); return processLinksInBatches(textNodesSnapshot, 0); }; /** * 观察页面变化并处理新添加的内容 */ const observePageChanges = function (rootElement: Node) { for ( const treeWalker = document.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { const localName = (node?.parentNode)?.localName; if (!filter.test(localName)) { return NodeFilter.FILTER_ACCEPT; } else { return NodeFilter.FILTER_SKIP; } }, }); treeWalker.nextNode(); ) { setLink(treeWalker.currentNode); } }; let lockFn = new utils.LockFunction((mutations: MutationRecord[]) => { for (const mutation of mutations) { if ("childList" === mutation.type) { const addedNodes = mutation.addedNodes; for (let nodeIndex = 0; nodeIndex < addedNodes.length; nodeIndex++) { const node = addedNodes[nodeIndex]; observePageChanges(node); } } } }); /** * 初始化链接处理 */ const initLinkProcessing = function () { linkifyText(document.body); /** * 监听DOM变化 */ const mutationObserver = utils.mutationObserver(document.body, { config: { subtree: true, childList: true, }, callback: (mutations) => { lockFn.run(mutations); }, }); return mutationObserver; }; /** * 清理链接函数 */ const clearLinkHelper = function (linkElement: HTMLElement) { const url = linkElement.getAttribute("href"); if ( typeof url === "string" && 0 !== url.indexOf("http") && 0 !== url.indexOf("ed2k://") && 0 !== url.indexOf("thunder://") ) { return linkElement.setAttribute("href", "http://" + url); } }; /** * 清理所有链接 */ const clearAllLinks = function () { const linkElements = Array.from(document.getElementsByClassName(HANDLER_CLASS_NAME)); for (const $link of linkElements) { clearLinkHelper($link as HTMLElement); } }; document.addEventListener("mouseover", handleClearLink); setTimeout(clearAllLinks, 1500); setTimeout(initLinkProcessing, 100); };