// ==UserScript== // @name 让链接可点击 (精确版) // @namespace https://viayoo.com/ // @version 2.4 // @description 精确识别页面中的文本链接并使其可点击,修复URL识别问题。 // @homepageURL https://app.viayoo.com/addons/31 // @author cumt-feng // @run-at document-start // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // ==/UserScript== (function () { 'use strict'; const DEFAULT_BLACKLIST = ['example.com', 'localhost']; const DEFAULT_OPEN_IN_NEW_TAB = true; function getStoredValue(key, defaultValue) { try { return GM_getValue(key, defaultValue); } catch (e) { return defaultValue; } } function setStoredValue(key, value) { try { GM_setValue(key, value); } catch (e) { console.error(`Tampermonkey Script: Error setting GM_value for ${key}:`, e); } } const SCRIPT_RUNNING_KEY = encodeURIComponent('谷花泰:让链接可点击:执行判断'); if (window[SCRIPT_RUNNING_KEY]) return; window[SCRIPT_RUNNING_KEY] = true; class ClickLink { constructor() { // 精确的URL正则表达式 - 重点修复边界问题 this.url_regexp = /(https?:\/\/[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}(?:\/[^\s<>"',。!?;:]*[^\s<>"',。!?;:])?)/gi; this.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(","); this.xPath = "//text()[not(ancestor::" + this.excludedTags.join(') and not(ancestor::') + ")]"; this.blacklist = getStoredValue('blacklist', DEFAULT_BLACKLIST); this.openInNewTab = getStoredValue('openInNewTab', DEFAULT_OPEN_IN_NEW_TAB); this.init(); } init() { const hostname = window.location.hostname; const isBlacklisted = this.blacklist.some(keyword => hostname.includes(keyword)); if (isBlacklisted) return; document.addEventListener("mouseover", this.clearLink.bind(this)); this.setupMenuCommands(); this.startObserve(); setTimeout(this.linkMixInit.bind(this), 100); } setupMenuCommands() { GM_registerMenuCommand('显示/隐藏黑名单', this.toggleBlacklistDisplay.bind(this)); GM_registerMenuCommand('添加当前域名到黑名单', this.addCurrentDomainToBlacklist.bind(this)); GM_registerMenuCommand('从黑名单移除当前域名', this.removeCurrentDomainFromBlacklist.bind(this)); GM_registerMenuCommand(`切换链接新标签页打开 (${this.openInNewTab ? '是' : '否'})`, this.toggleOpenInNewTab.bind(this)); GM_registerMenuCommand('重新扫描页面', this.rescanPage.bind(this)); } // 其他菜单命令方法保持不变... // --- 核心修复:精确的URL处理 --- setLink(candidate) { if (!candidate || candidate.parentNode?.classList.contains("textToLink") || candidate.nodeName === "#cdata-section") { return; } const originalText = candidate.textContent; // 使用新的精确URL检测方法 const processedText = this.detectAndReplaceUrls(originalText); if (originalText === processedText) { return; } const span = document.createElement("span"); span.innerHTML = processedText; // 处理特殊链接 span.querySelectorAll('.special-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const specialUrl = link.getAttribute('data-special-url'); this.handleSpecialLinkClick(specialUrl); }); }); try { candidate.parentNode.replaceChild(span, candidate); } catch (e) { console.warn('Tampermonkey Script: Could not replace text node:', e); } } // 新的精确URL检测方法 detectAndReplaceUrls(text) { // 首先尝试精确匹配常见的URL模式 const preciseUrlRegex = /(https?:\/\/[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}(?:\/[^\s<>"',。!?;:]*)?)/gi; return text.replace(preciseUrlRegex, (match) => { // 进一步清理URL边界 const cleanUrl = this.cleanUrlBoundaries(match); if (!this.isValidUrl(cleanUrl)) { return match; // 如果不是有效URL,返回原文本 } return this.createLinkElement(cleanUrl, match); }); } // 清理URL边界 - 移除尾部的标点符号和中文 cleanUrlBoundaries(url) { // 移除URL尾部的中文标点和文字 let cleanUrl = url.replace(/[,。!?;:”“‘’()【】\s]+$/, ''); // 确保不以标点符号结尾 cleanUrl = cleanUrl.replace(/[.,;:!?)]+$/, ''); return cleanUrl; } // 验证URL是否有效 isValidUrl(url) { // 检查基本URL结构 if (!url.includes('://')) return false; // 检查域名格式 const domainMatch = url.match(/https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/); if (!domainMatch) return false; const domain = domainMatch[1]; // 验证域名格式(不能以点开头或结尾,不能有连续点) if (domain.startsWith('.') || domain.endsWith('.') || domain.includes('..')) { return false; } return true; } // 创建链接元素 createLinkElement(url, displayText) { const targetAttribute = this.openInNewTab ? ' target="_blank"' : ''; // 特殊协议处理 if (url.startsWith('ed2k://') || url.startsWith('thunder://')) { return `${displayText}`; } return `${displayText}`; } handleSpecialLinkClick(url) { this.showCustomModal( '确认打开特殊链接', `您正尝试打开一个特殊链接:\n${url}\n\n这可能会启动外部应用程序。您确定要继续吗?`, true, (confirm) => { if (confirm) { window.open(url, this.openInNewTab ? '_blank' : '_self'); } } ); } // 测试用例验证函数(可选) testUrlDetection() { const testCases = [ "点击https://tas.talebase.com/api/Mz6R7r,完成作答", "访问http://example.com/page,谢谢!", "网址是https://www.google.com/search?q=test。", "联系admin@example.com。" ]; testCases.forEach((testCase, index) => { const result = this.detectAndReplaceUrls(testCase); console.log(`Test ${index + 1}:`, { input: testCase, output: result, hasLink: result !== testCase }); }); } clearLink(event) { const link = event.originalTarget || event.target; if (!(link && link.localName === "a" && link.classList.contains("textToLink"))) { return; } let url = link.getAttribute("href"); if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('mailto:')) { if (url.includes('@')) { link.setAttribute("href", "mailto:" + url); } else { link.setAttribute("href", "http://" + url); } } } // 其他方法保持不变... linkPack(result, start) { const startTime = Date.now(); let i = start; const processChunk = () => { let chunkEnd = Math.min(i + 10000, result.snapshotLength); for (; i < chunkEnd; i++) { this.setLink(result.snapshotItem(i)); } if (i < result.snapshotLength && (Date.now() - startTime < 2500)) { requestAnimationFrame(processChunk); } else if (i < result.snapshotLength) { setTimeout(processChunk, 50); } }; requestAnimationFrame(processChunk); } linkify(node) { if (!node) return; const result = document.evaluate(this.xPath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); this.linkPack(result, 0); } linkFilter(node) { for (let i = 0; i < this.excludedTags.length; i++) { if (this.excludedTags[i] === node.parentNode?.localName?.toLowerCase()) { return NodeFilter.FILTER_REJECT; } } return NodeFilter.FILTER_ACCEPT; } observePage(root) { if (!root || root.nodeType !== Node.ELEMENT_NODE) return; const tW = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: this.linkFilter.bind(this) }, false); let nodesToProcess = []; while (tW.nextNode()) { nodesToProcess.push(tW.currentNode); } if (nodesToProcess.length > 0) { nodesToProcess.forEach(node => this.setLink(node)); } } startObserve() { this.observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === "childList" && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { this.observePage(node); } } } } }); } linkMixInit() { if (window !== window.top || document.title === "") return; if (document.body) { // 可选:运行测试用例 this.testUrlDetection(); this.linkify(document.body); this.observer.observe(document.body, { childList: true, subtree: true }); } } showCustomModal(title, message, isConfirm = false, onConfirm = null) { // 模态框实现保持不变... } } try { new ClickLink(); } catch (err) { console.error('Tampermonkey Script: Failed to load ClickLink functionality:', err); } })();