// ==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);
}
})();