🔗 文本快链

智能识别网页中纯文本链接并转为可点击链接

// ==UserScript==

// @name         🔗 文本快链

// @namespace    https://gf.qytechs.cn/zh-CN/users/1454800

// @version      1.0.2

// @description  智能识别网页中纯文本链接并转为可点击链接

// @author       Aiccest

// @match        *://*/*

// @grant        none

// @license      MIT

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

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址