// ==UserScript== // @name 让链接可点击 (Enhanced - Fixed) // @namespace https://viayoo.com/ // @version 2.3 // @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'; // --- Configuration Defaults --- const DEFAULT_BLACKLIST = ['example.com', 'localhost']; const DEFAULT_OPEN_IN_NEW_TAB = true; // --- Utility Functions --- function getStoredValue(key, defaultValue) { try { return GM_getValue(key, defaultValue); } catch (e) { console.error(`Tampermonkey Script: Error getting GM_value for ${key}:`, 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); } } // --- Script Execution Check --- const SCRIPT_RUNNING_KEY = encodeURIComponent('谷花泰:让链接可点击:执行判断'); if (window[SCRIPT_RUNNING_KEY]) { console.log('Tampermonkey Script: Already running or marked to prevent re-execution.'); return; } window[SCRIPT_RUNNING_KEY] = true; class ClickLink { constructor() { // 改进的URL正则表达式 - 更精确地匹配URL边界 this.url_regexp = /(https?:\/\/[^\s<>"']+(?:\.[^\s<>"']+)*[^\s<>"',;.!?]|www\.[^\s<>"']+\.[^\s<>"']+[^\s<>"',;.!?]|[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:\/[^\s<>"']*[^\s<>"',;.!?])?)/gi; this.urlPrefixes = ['http://', 'https://', 'ftp://', 'thunder://', 'ed2k://', 'mailto:']; 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() { console.log('Tampermonkey Script: Initializing ClickLink functionality.'); const hostname = window.location.hostname; const isBlacklisted = this.blacklist.some(keyword => hostname.includes(keyword)); if (isBlacklisted) { console.log(`Tampermonkey Script: Current domain '${hostname}' is blacklisted. Script will not execute.`); return; } document.addEventListener("mouseover", this.clearLink.bind(this)); this.setupMenuCommands(); this.startObserve(); setTimeout(this.linkMixInit.bind(this), 100); } // 其他方法保持不变... // --- 改进的链接处理逻辑 --- setLink(candidate) { if (!candidate || candidate.parentNode?.classList.contains("textToLink") || candidate.nodeName === "#cdata-section") { return; } const originalText = candidate.textContent; console.log('Tampermonkey Script: Processing text node:', originalText); // 使用更精确的URL检测和替换 const processedText = this.processTextWithUrls(originalText); if (originalText === processedText) { console.log('Tampermonkey Script: No valid links found in text node.'); 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处理函数 - 更精确地识别URL边界 processTextWithUrls(text) { return text.replace(this.url_regexp, (match) => { // 验证匹配的确实是有效的URL const cleanUrl = this.validateAndCleanUrl(match); if (!cleanUrl) return match; // 如果不是有效URL,返回原文本 return this.createLinkHtml(cleanUrl, match); }); } // 验证和清理URL validateAndCleanUrl(url) { // 移除URL后面可能附带的中文标点或文字 let cleanUrl = url.replace(/[,。!?;:”“‘’()【】\s]+$/, ''); // 检查是否以常见域名结尾 const domainRegex = /\.(com|cn|net|org|edu|gov|io|co|info|biz|me|tv|cc|hk|tw|jp|kr|de|fr|uk|us|ca|au|ru|br|in|eu)(?:\/|$)/i; if (!domainRegex.test(cleanUrl)) { return null; } // 确保URL格式正确 if (cleanUrl.startsWith('www.')) { cleanUrl = 'http://' + cleanUrl; } // 验证URL的基本格式 try { new URL(cleanUrl); return cleanUrl; } catch (e) { // 如果URL构造失败,尝试修复 if (cleanUrl.includes('://')) { return cleanUrl; } return null; } } // 创建链接HTML createLinkHtml(url, displayText) { const targetAttribute = this.openInNewTab ? ' target="_blank"' : ''; // 处理特殊协议 if (url.startsWith('ed2k://') || url.startsWith('thunder://')) { return `${displayText}`; } // 处理邮件链接 if (url.startsWith('mailto:')) { return `${displayText}`; } // 确保HTTP/HTTPS链接有协议 if (url.startsWith('http://') || url.startsWith('https://')) { return `${displayText}`; } // 默认添加https协议 return `${displayText}`; } handleSpecialLinkClick(url) { this.showCustomModal( '确认打开特殊链接', `您正尝试打开一个特殊链接:\n${url}\n\n这可能会启动外部应用程序。您确定要继续吗?`, true, (confirm) => { if (confirm) { window.open(url, this.openInNewTab ? '_blank' : '_self'); } } ); } // 其他方法保持不变... linkPack(result, start) { const startTime = Date.now(); let i = start; console.log(`Tampermonkey Script: Starting link processing for ${result.snapshotLength} nodes.`); 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) { console.log(`Tampermonkey Script: Yielding after processing ${i - start} nodes. Remaining: ${result.snapshotLength - i}`); setTimeout(processChunk, 50); } else { console.log('Tampermonkey Script: Finished initial link processing.'); } }; requestAnimationFrame(processChunk); } linkify(node) { if (!node) return; const result = document.evaluate(this.xPath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); console.log(`Tampermonkey Script: Found ${result.snapshotLength} text nodes for linkification in provided node.`); this.linkPack(result, 0); } // ... 其余方法保持不变 } try { new ClickLink(); } catch (err) { console.error('Tampermonkey Script: Failed to load ClickLink functionality:', err); } })();