// ==UserScript==
// @name 网页文本转链接
// @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 https://update.greasyfork.icu/scripts/541057/%E7%BD%91%E9%A1%B5%E6%96%87%E6%9C%AC%E8%BD%AC%E9%93%BE%E6%8E%A5.user.js
// @updateURL https://update.greasyfork.icu/scripts/541057/%E7%BD%91%E9%A1%B5%E6%96%87%E6%9C%AC%E8%BD%AC%E9%93%BE%E6%8E%A5.meta.js
// ==/UserScript==
(() => {
// 1. 注入样式
document.head.insertAdjacentHTML(
'beforeend',
''
);
// 2. URL 正则与常量
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 skipSelectors = [
'[contenteditable]', 'code', 'pre', '.WJ_modal',
'.modal', '.popup', '.dialog', '[role="dialog"]',
'.ace_editor', '.CodeMirror', '.monaco-editor', '.cm-editor'
].join(',');
const PROCESSED = 'data-url-processed';
const shouldSkip = el => !el || skipTags.has(el.tagName) || el.isContentEditable || !!el.closest?.(skipSelectors);
// 4. 节点处理函数
const processNode = root => {
if (!root || root.hasAttribute?.(PROCESSED) || shouldSkip(root)) return;
root.setAttribute(PROCESSED, 'true');
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
{ acceptNode: n => shouldSkip(n.parentElement) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT }
);
const tasks = [];
for (let node; (node = walker.nextNode());) {
const o = node.textContent ?? '';
const r = o.replace(urlRegex, m => {
const h = /^\w+:\/\//.test(m) ? m : `https://${m}`;
return `${m}`;
});
r !== o && tasks.push({ node, replaced: r });
}
for (const { node, replaced } of tasks) {
node.replaceWith(document.createRange().createContextualFragment(replaced));
}
};
// 5. 观察器
const io = new IntersectionObserver(entries => {
for (const { isIntersecting, target } of entries) {
if (isIntersecting) {
io.unobserve(target);
requestIdleCallback?.(() => processNode(target), { timeout: 1000 });
}
}
});
const mo = new MutationObserver(mutations => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
if (node.nodeType === 1 && !shouldSkip(node) && !node.hasAttribute?.(PROCESSED)) {
io.observe(node);
}
}
}
});
// 7. 初始化
Array.from(document.body.children).forEach(child => !shouldSkip(child) && io.observe(child));
mo.observe(document.body, { childList: true, subtree: true });
})();