// ==UserScript== // @name 网页文本转链接(Shadow DOM&动态) // @description 网页中文本转为可点击链接 添加颜色下划线 // @version 1.0 // @author WJ // @match *://*/* // @exclude https://*.bing.com/* // @exclude https://*.baidu.com/* // @license MIT // @grant none // @run-at document-idle // @namespace https://greasyfork.org/users/914996 // @downloadURL none // ==/UserScript== (() => { /* 1. 样式 */ const style = document.createElement('style'); style.textContent = '.url-link{color:#348A87;text-decoration:underline}'; document.head.appendChild(style); /* 2. 正则(与原脚本一致,仅编译一次) */ const tlds = [ 'app','aero','aer','art','asia','beer','biz','cat','cc','chat','ci','cloud', 'club','cn','com','cool','coop','co','dev','edu','email','fit','fun','gov', 'group','hk','host','icu','info','ink','int','io','jobs','kim','love','ltd', 'luxe','me','mil','mobi','moe','museum','name','net','nl','network','one', 'online','org','plus','post','press','pro','red','ren','run','ru','shop', 'site','si','space','store','tech','tel','top','travel','tv','tw','uk','us', 'video','vip','wang','website','wiki','wml','work','ws','xin','xyz','yoga','zone' ].join('|'); const urlRegex = new RegExp( String.raw`\b[\w.:/?=%&#-]{3,}\.(?:${tlds})(?!\w)[\w.:/?=%&#-]*|` + String.raw`(?:(?:https?:\/\/)|(?:www\.|wap\.))[\w.:/?=%&#-@+~=]{3,250}\.[\w]{2,6}\b[\w.:/?=%&#-@+~=]*`, 'gi' ); /* 3. 跳过标签与输入区域 */ const skipTags = new Set(['A','SCRIPT','STYLE','TEXTAREA','BUTTON','SELECT','OPTION','CODE','PRE','INPUT']); const shouldSkip = el => !el || skipTags.has(el.tagName) || el.isContentEditable || el.closest('[contenteditable="true"], input, textarea, select'); /* 4. 处理节点(打标记防重复) */ const PROCESSED = '_urlLinked'; function processNode(root) { if (root[PROCESSED] || shouldSkip(root)) return; root[PROCESSED] = true; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); const textNodes = []; let n; while (n = walker.nextNode()) { if (!shouldSkip(n.parentElement)) textNodes.push(n); } textNodes.forEach(tn => { const txt = tn.textContent; urlRegex.lastIndex = 0; if (!urlRegex.test(txt)) return; urlRegex.lastIndex = 0; const replaced = txt.replace(urlRegex, m => `${m}`); if (replaced !== txt) { const span = document.createElement('span'); span.innerHTML = replaced; tn.replaceWith(span); } }); } /* 5. 仅扫描可见区域(IntersectionObserver + 空闲回调) */ function processVisible() { const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { io.unobserve(e.target); requestIdleCallback(() => processNode(e.target), { timeout: 1000 }); } }); }); document.querySelectorAll('body *').forEach(el => io.observe(el)); } /* 6. 节流 MutationObserver */ let moPending = false; function onMutation() { if (moPending) return; moPending = true; requestIdleCallback(() => { document.querySelectorAll(`body *:not([${PROCESSED}])`).forEach(processNode); moPending = false; }, { timeout: 1000 }); } /* 7. 初始化 */ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', processVisible); } else { processVisible(); } new MutationObserver(onMutation).observe(document.body, { childList: true, subtree: true }); })();