// ==UserScript== // @name 让链接可点击 (精确匹配版) // @namespace https://viayoo.com/ // @version 3.0 // @description 精确识别URL并使其可点击,解决边界识别问题 // @author cumt-feng // @run-at document-idle // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; // 配置 const CONFIG = { blacklist: GM_getValue('blacklist', ['example.com', 'localhost']), openInNewTab: GM_getValue('openInNewTab', true), // 精确的URL匹配规则 urlPatterns: [ // 标准HTTP/HTTPS URL /https?:\/\/[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}(?:\/[^\s<>"'\u4e00-\u9fff,。!?;:]*)?/g, // www开头的URL /www\.[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}(?:\/[^\s<>"'\u4e00-\u9fff,。!?;:]*)?/g ] }; // 检查是否在黑名单中 if (CONFIG.blacklist.some(domain => location.hostname.includes(domain))) { return; } // 主函数 function init() { // 注册菜单命令 registerMenuCommands(); // 开始处理页面 processPage(); // 监听DOM变化 observeChanges(); } function registerMenuCommands() { GM_registerMenuCommand('显示黑名单', showBlacklist); GM_registerMenuCommand('添加当前域名到黑名单', addToBlacklist); GM_registerMenuCommand('切换新标签页打开', toggleNewTab); GM_registerMenuCommand('重新扫描', rescanPage); } // 精确的URL识别函数 function extractUrls(text) { const urls = []; // 遍历所有匹配模式 for (const pattern of CONFIG.urlPatterns) { let match; while ((match = pattern.exec(text)) !== null) { const url = match[0]; const startIndex = match.index; const endIndex = startIndex + url.length; // 验证URL的边界 if (isValidUrlBoundary(text, startIndex, endIndex)) { urls.push({ url: normalizeUrl(url), start: startIndex, end: endIndex, original: url }); } } } return urls; } // 验证URL边界是否合法 function isValidUrlBoundary(text, start, end) { // 检查URL前面不能是字母数字(避免匹配到单词中的部分) if (start > 0) { const prevChar = text.charAt(start - 1); if (/[a-zA-Z0-9]/.test(prevChar)) { return false; } } // 检查URL后面不能是字母数字或常见URL字符 if (end < text.length) { const nextChar = text.charAt(end); if (/[a-zA-Z0-9_\-~]/.test(nextChar)) { return false; } } return true; } // 标准化URL function normalizeUrl(url) { if (url.startsWith('www.')) { return 'https://' + url; } return url; } // 处理文本节点 function processTextNode(textNode) { const text = textNode.textContent; // 跳过已经处理过的节点或空文本 if (!text || textNode.processed || text.length < 10) { return; } // 查找URL const urls = extractUrls(text); if (urls.length === 0) { return; } // 按位置排序(从后往前处理,避免索引变化) urls.sort((a, b) => b.start - a.start); // 创建包含链接的HTML let newHtml = text; for (const urlInfo of urls) { const before = newHtml.substring(0, urlInfo.start); const after = newHtml.substring(urlInfo.end); const linkHtml = createLinkHtml(urlInfo.url, urlInfo.original); newHtml = before + linkHtml + after; } // 如果内容有变化,替换文本节点 if (newHtml !== text) { const span = document.createElement('span'); span.innerHTML = newHtml; textNode.parentNode.replaceChild(span, textNode); markAsProcessed(span); } } // 创建链接HTML function createLinkHtml(url, displayText) { const target = CONFIG.openInNewTab ? ' target="_blank"' : ''; // 特殊协议处理 if (url.startsWith('ed2k://') || url.startsWith('thunder://')) { return `${displayText}`; } return `${displayText}`; } // 标记已处理的节点 function markAsProcessed(element) { if (element.nodeType === Node.ELEMENT_NODE) { element.processed = true; // 为特殊链接添加点击事件 element.querySelectorAll('.special-link').forEach(link => { link.addEventListener('click', handleSpecialLinkClick); }); } } // 特殊链接点击处理 function handleSpecialLinkClick(event) { event.preventDefault(); const url = event.target.getAttribute('data-url'); if (confirm(`即将打开特殊链接:\n${url}\n\n是否继续?`)) { window.open(url, CONFIG.openInNewTab ? '_blank' : '_self'); } } // 处理整个页面 function processPage() { // 获取所有文本节点 const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { // 跳过特定标签内的文本 const excludedParents = ['A', 'SCRIPT', 'STYLE', 'TEXTAREA', 'PRE', 'CODE']; let parent = node.parentNode; while (parent && parent !== document.body) { if (excludedParents.includes(parent.nodeName)) { return NodeFilter.FILTER_REJECT; } parent = parent.parentNode; } return NodeFilter.FILTER_ACCEPT; } } ); const textNodes = []; let node; while ((node = walker.nextNode())) { textNodes.push(node); } // 处理文本节点 textNodes.forEach(processTextNode); } // 监听DOM变化 function observeChanges() { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.TEXT_NODE) { processTextNode(node); } else if (node.nodeType === Node.ELEMENT_NODE) { // 处理新添加的元素中的文本节点 const textNodes = getTextNodes(node); textNodes.forEach(processTextNode); } }); } } }); observer.observe(document.body, { childList: true, subtree: true }); } // 获取元素内的所有文本节点 function getTextNodes(element) { const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, null, false ); const textNodes = []; let node; while ((node = walker.nextNode())) { textNodes.push(node); } return textNodes; } // 菜单命令功能 function showBlacklist() { alert(`当前黑名单:\n${CONFIG.blacklist.join('\n')}`); } function addToBlacklist() { const domain = location.hostname; if (!CONFIG.blacklist.includes(domain)) { CONFIG.blacklist.push(domain); GM_setValue('blacklist', CONFIG.blacklist); alert(`已添加 ${domain} 到黑名单,页面将刷新`); location.reload(); } else { alert(`${domain} 已在黑名单中`); } } function toggleNewTab() { CONFIG.openInNewTab = !CONFIG.openInNewTab; GM_setValue('openInNewTab', CONFIG.openInNewTab); alert(`链接将在${CONFIG.openInNewTab ? '新标签页' : '当前标签页'}打开`); } function rescanPage() { processPage(); alert('页面已重新扫描'); } // 测试函数 - 验证URL识别 function testUrlRecognition() { const testCases = [ "点击https://tas.talebase.com/api/Mz6R7r,完成作答", "访问http://example.com/test,谢谢!", "网址是https://www.google.com/search?q=test。", "联系admin@example.com获取帮助" ]; console.log('=== URL识别测试 ==='); testCases.forEach((test, i) => { const urls = extractUrls(test); console.log(`测试 ${i+1}: "${test}"`); console.log(`识别到 ${urls.length} 个URL:`, urls.map(u => u.url)); }); } // 初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // 运行测试 setTimeout(testUrlRecognition, 1000); })();