文本链接可点击

通过点击将网页中的文本URL转换为可点击的A标签,所有代码均为AI提供,可能存在严重BUG。

// ==UserScript==
// @name        文本链接可点击
// @namespace   http://tampermonkey.net/
// @version     1.3
// @description 通过点击将网页中的文本URL转换为可点击的A标签,所有代码均为AI提供,可能存在严重BUG。
// @author      cangming99
// @match       *://*/*
// @run-at      document-end
// @grant       none
// @license     MIT
// ==/UserScript==

(function () {
    'use strict';

    const urlRegex = /\b((https?|ftp|file):\/\/|www\.)?[-A-Za-z0-9+&@#/%?=~_|!:,.;]+\.[A-Za-z]{2,}([-A-Za-z0-9+&@#/%=~_|]*)\b/g;

    // 添加自定义样式
    const style = document.createElement('style');
    style.textContent = `
        .custom-link {
            color: #007bff;
            text-decoration: underline;
        }
        .custom-link:hover {
            color: #0056b3;
        }
    `;
    document.head.appendChild(style);

    // 节流函数,防止频繁点击处理
    function throttle(fn, delay) {
        let lastExecution = 0;
        return function (...args) {
            const now = Date.now();
            if (now - lastExecution >= delay) {
                lastExecution = now;
                fn(...args);
            }
        };
    }

    // URL 转换函数:检查文本节点中是否存在 URL,并进行转换
    function convertTextToLink(element) {
        if (element.hasAttribute('data-url-processed')) return; // 避免重复处理

        // 跳过已包含A标签的元素
        if (element.querySelector('a')) return;

        let hasUrl = false;
        const childNodes = Array.from(element.childNodes);

        childNodes.forEach(node => {
            if (node.nodeType === Node.TEXT_NODE) {
                let currentNode = node.nodeValue;
                let resultNode = document.createDocumentFragment();
                let lastIndex = 0;

                // 重置 lastIndex 以确保每次匹配从头开始
                urlRegex.lastIndex = 0;

                while (true) {
                    const match = urlRegex.exec(currentNode);
                    if (!match) break;

                    hasUrl = true;

                    if (match.index > lastIndex) {
                        resultNode.appendChild(document.createTextNode(currentNode.slice(lastIndex, match.index)));
                    }

                    const url = match[0];
                    const a = document.createElement('a');
                    a.href = url.startsWith('http') ? url : 'http://' + url; // 自动为没有协议的URL加上 'http://'
                    a.target = '_blank';
                    a.className = 'custom-link';
                    a.textContent = url;
                    resultNode.appendChild(a);

                    lastIndex = urlRegex.lastIndex;
                }

                if (lastIndex < currentNode.length) {
                    resultNode.appendChild(document.createTextNode(currentNode.slice(lastIndex)));
                }

                if (hasUrl) {
                    node.replaceWith(resultNode);
                }
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                // 递归处理子元素,避免复杂结构的元素丢失
                convertTextToLink(node);
            }
        });

        if (hasUrl) {
            element.setAttribute('data-url-processed', 'true');
        }
    }

    // 递归查找包含文本的元素,向上查找直到找到文本容器
    function findTextContainer(node) {
        while (node && node.nodeType !== Node.TEXT_NODE && node.nodeType !== Node.ELEMENT_NODE) {
            node = node.parentNode;
        }
        return node;
    }

    // 点击事件的处理函数,判断并处理元素是否包含可转化为链接的文本
    function processClickEvent(event) {
        let target = event.target;

        // 获取最接近的文本节点
        let textNode = findTextContainer(target);

        if (textNode && textNode.nodeType === Node.ELEMENT_NODE && !textNode.hasAttribute('data-url-processed')) {
            convertTextToLink(textNode);
        }
    }

    // 主点击监听函数,使用节流优化点击事件
    document.body.addEventListener('click', throttle(processClickEvent, 200));
})();