// ==UserScript== // @name 🔗 文本快链 // @namespace https://greasyfork.org/zh-CN/users/1454800 // @version 1.0.2 // @description 智能识别网页中纯文本链接并转为可点击链接 // @author Aiccest // @match *://*/* // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/532397/%F0%9F%94%97%20%E6%96%87%E6%9C%AC%E5%BF%AB%E9%93%BE.user.js // @updateURL https://update.greasyfork.icu/scripts/532397/%F0%9F%94%97%20%E6%96%87%E6%9C%AC%E5%BF%AB%E9%93%BE.meta.js // ==/UserScript== (function() { 'use strict'; const linkPrefixes = [ 'http://', 'https://', 'ftp://', 'thunder://', 'ed2k://', 'magnet:', 'mailto:', 'tel:', 'sms:' ]; const fileExtensions = [ '.zip', '.rar', '.7z', '.exe', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.mp4', '.mp3', '.jpg', '.png', '.gif', '.txt', '.js', '.css' ]; // 只保留中文标点,不加英文符号 const punctuations = ',。!?、;:”“‘’()【】《》…'; const linkRegex = new RegExp( `(${linkPrefixes.map(p => p.replace(/[:\\/]/g, '\\$&')).join('|')})[^\\s<>"'${punctuations}]*`, 'gi' ); const markdownRegex = /.*?(https?:\/\/[^\s)]+)/gi; const ignoredTags = new Set(['A', 'SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'BUTTON']); function findExtensionEnd(url) { const lowerUrl = url.toLowerCase(); for (const ext of fileExtensions) { const idx = lowerUrl.indexOf(ext); if (idx !== -1) { return idx + ext.length; } } return -1; } function createLinkElement(url) { const a = document.createElement('a'); a.href = url; a.textContent = url; a.style.textDecoration = 'none'; a.target = '_blank'; a.rel = 'noopener noreferrer'; return a; } function cleanUrlEnd(url) { // 去除末尾孤立的英文标点 return url.replace(/[.,!?]+$/, ''); } function processTextNode(textNode) { let text = textNode.nodeValue; // 先处理markdown格式,转成普通链接 text = text.replace(markdownRegex, (full, url) => url); const frag = document.createDocumentFragment(); let lastIndex = 0; let match; linkRegex.lastIndex = 0; while ((match = linkRegex.exec(text)) !== null) { const matchStart = match.index; const rawUrl = match[0]; let realUrl = rawUrl; let overflowText = ''; const extEnd = findExtensionEnd(rawUrl); if (extEnd !== -1 && extEnd < rawUrl.length) { realUrl = rawUrl.slice(0, extEnd); overflowText = rawUrl.slice(extEnd); } else { realUrl = cleanUrlEnd(rawUrl); // 重点:剥掉末尾标点 overflowText = rawUrl.slice(realUrl.length); } if (matchStart > lastIndex) { frag.appendChild(document.createTextNode(text.slice(lastIndex, matchStart))); } frag.appendChild(createLinkElement(realUrl)); if (overflowText) { frag.appendChild(document.createTextNode(overflowText)); } lastIndex = matchStart + rawUrl.length; } if (lastIndex < text.length) { frag.appendChild(document.createTextNode(text.slice(lastIndex))); } if (frag.childNodes.length > 0) { textNode.parentNode.replaceChild(frag, textNode); } } function walkAndProcess(root) { const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (!node.parentNode) return NodeFilter.FILTER_REJECT; const parentTag = node.parentNode.tagName; if (ignoredTags.has(parentTag)) return NodeFilter.FILTER_REJECT; const text = node.nodeValue; if (!text || (!linkRegex.test(text) && !markdownRegex.test(text))) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }, false ); const nodes = []; let node; while ((node = walker.nextNode())) { nodes.push(node); } for (const n of nodes) { processTextNode(n); } } function debounce(fn, delay) { let timer = null; return function() { clearTimeout(timer); timer = setTimeout(fn, delay); }; } const observer = new MutationObserver(debounce(() => { walkAndProcess(document.body); }, 300)); // 300ms 响应速度 observer.observe(document.body, { childList: true, subtree: true }); walkAndProcess(document.body); })();