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