您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
识别并直接跳转普通文本链接
// ==UserScript== // @name 直接跳转文本链接 // @namespace http://tampermonkey.net/ // @version 0.7 // @description 识别并直接跳转普通文本链接 // @author wcbblll // @match *://*/* // @exclude http://greasyfork.icu/* // @run-at document-idle // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // 移除重复项 function removeDuplicates(arr) { return arr.filter((item, index, self) => self.indexOf(item) === index); } // 从后往前遍历数组 function traverseArrayBackward(arr, callback) { for (let i = arr.length - 1; i >= 0; i--) { callback(arr[i], i, i === 0); } } let linkRegex = /((https?:\/\/)?|(\/\/))?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; // 文本全局匹配链接返回数组,数组里面是字符串 function getTextLinks(text) { // const linkRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; // const linkRegex = /((https?:\/\/)?|(\/\/))?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; const links = text.match(linkRegex); return filterLinks(links || []); } // 文本全局匹配链接返回数组,数组里面是对象 // 先用url匹配,匹配之后把匹配到的字符串替换为空符串在匹配host,解决url和host匹配两次的问题 function getTextLinksList(text) { // const linkRegex = /((https?:\/\/)?|(\/\/))?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; // const linkRegex = /((https?:\/\/)?|(\/\/))?(www\.)?[a-zA-Z0-9#]{1,256}[-@:%._\+~=]*\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; const hostRegex = /\b(?!\/\/)((?:www\.)?[a-zA-Z0-9_.-]+(?:\.[a-zA-Z0-9_.-]+)*\.[a-zA-Z]{2,})\b/g let newText = text // debugger function matchFunc(reg, type) { const matches = newText.matchAll(reg); const matchArr = [] for (const match of matches) { const matchStr = match[0] const len = matchStr.length newText = newText.replace(matchStr, ' '.repeat(len)) match.type = type matchArr.push(match) } return matchArr } return filterLinks([...matchFunc(linkRegex, 'url'), ...matchFunc(hostRegex, 'host')]) } // 传参为一维数组或二维数组,二维数组取第一项,对传参进行过滤,链接必须包含顶级域名,ip必须是合法的ipv4地址。过滤,减少误判情况 function filterLinks(links) { if (!links) return links // 常用的顶级域名列表,通过过滤,减少误判情况 // eslint-disable-next-line const yuMingList = ['com', 'net', 'org', 'edu', 'gov', 'mil', 'info', 'biz', 'app', 'shop', 'store', 'xyz', 'top', 'live', 'cn', 'us', 'uk', 'jp', 'de', 'fr', 'ca', 'hk', 'tw', 'mo', 'eu', 'in', 'tv', 'cc', 'cloud'] function isLink(link) { const find = yuMingList.find(item => link.includes(`.${item}`)) if (find) return true const ipv4List = link.split('.') // ipv4必须包含4位数字 const ipv4 = ipv4List.length >= 4 && ipv4List.reduce((pre, cur) => { if (Number(cur) === Number(cur)) pre++ return pre }, 0) >= 4 if (ipv4) return true return false } return links.filter((item) => { if (Array.isArray(item)) { // 二维数组 return isLink(item[0]) } else { // 普通数组 return isLink(item) } }) } /** * Splits a given text into an array of objects, where each object represents a segment of the text. * * @param {string} text - The text to be split. * @param {Array<Object>} arr - An array of objects, where each object contains a link and its index in the text. * @return {Array<Object>} An array of objects, where each object represents a segment of the text. */ function splitText(text, arr) { if (!arr.length) return [{ text, type: 'text' }] let lastIndex = 0 let returnArr = [] arr.forEach((item, i) => { const link = item[0] const textObj = { text: text.slice(lastIndex, item.index), type: 'text' } const linkObj = { text: link, type: 'link' } returnArr.push(textObj, linkObj) lastIndex = item.index + link.length if (i === arr.length - 1 && lastIndex < text.length) { returnArr.push({ text: text.slice(lastIndex), type: 'text' }) } }) return returnArr } // 根据链接地址创建a标签,都当做绝对路径使用 function createLink(link) { const a = document.createElement("a"); a.href = link.indexOf('//') > -1 ? link : '//' + link; a.textContent = link; a.target = "_blank"; // 可选:在新标签页打开链接 a.rel = 'noopener noreferrer nofollow' a.setAttribute('text-link', true) return a; } // 根据文字创建文本标签 function createTextNode(text) { const textNode = document.createTextNode(text); return textNode; } // 根据类型返回不同的创建标签方法 function createNode(type, text) { switch (type) { case 'link': return createLink(text); case 'text': return createTextNode(text); default: throw new Error('Invalid type'); } } /* { match: { node:[] } } */ // 排除列表,这些里面内容不需要处理 const excludeList = ['A', 'SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT'] function createTextNodeTree(matchObj = {}, node, parent) { if (parent && excludeList.includes(parent.nodeName)) return matchObj if (node.nodeType === 3) { const textContent = node.textContent; let matches = getTextLinks(textContent) matches.forEach((match) => { if (matchObj[match] && matchObj[match].length > 0) { matchObj[match].push(node) } else { matchObj[match] = [node] } }); } if (node.shadowRoot) { const shadowRoot = node.shadowRoot for (const shadowRootChild of shadowRoot.childNodes) { createTextNodeTree(matchObj, shadowRootChild, shadowRoot) } } for (let child of node.childNodes) { createTextNodeTree(matchObj, child, node) } return matchObj; } function debounce(func, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, delay); }; } function callback(mutationsList, observer) { const matchObj = createTextNodeTree({}, document.body, null); for (const match in matchObj) { if (Object.hasOwnProperty.call(matchObj, match)) { const nodeList = removeDuplicates(matchObj[match]); nodeList.forEach((node) => { const generateNodeList = splitText(node.textContent, getTextLinksList(node.textContent)); traverseArrayBackward(generateNodeList, ({ type, text }, i, isLast) => { try { isLast ? node.parentNode.replaceChild(createNode(type, text), node) : node.nextSibling ? node.parentNode.insertBefore(createNode(type, text), node.nextSibling) : node.parentNode.appendChild(createNode(type, text)); } catch (error) { } }) }) } } } let observer = new MutationObserver(debounce(callback, 1000)); observer.observe(document.body, { childList: true, attributes: true }); })();