Greasy Fork is available in English.
网页中文本转为可点击链接 添加颜色下划线
当前为
// ==UserScript==
// @name 网页文本转链接(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 http://greasyfork.icu/users/914996
// ==/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 =>
`<a class="url-link" target="_blank" href="${/^\w+:\/\//.test(m) ? m : 'https://' + m}">${m}</a>`);
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 });
})();