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