YouTube Shorts Linkify

Converts URLs into clickable links on YouTube Shorts.

目前为 2025-02-11 提交的版本。查看 最新版本

// ==UserScript==
// @name         YouTube Shorts Linkify
// @namespace    http://tampermonkey.net/
// @version      1.2
// @license      GPL-3.0-or-later
// @description  Converts URLs into clickable links on YouTube Shorts.
// @match        https://www.youtube.com/shorts/*
// @icon         https://www.google.com/s2/favicons?domain=www.youtube.com&sz=64
// @grant        none
// ==/UserScript==

(function(){
    'use strict';
    function initLinkify(){
        const policy = window.trustedTypes ? trustedTypes.createPolicy('ytShortsLinkify',{createHTML: input=>input}) : null;
        const style = document.createElement('style');
        style.textContent = "a.yt-short-linkify { color: inherit; text-decoration: underline; cursor: pointer; } .custom-tooltip { position: absolute; background: #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; pointer-events: none; z-index: 10000; opacity: 0; white-space: nowrap; transition: opacity 0.2s ease; }";
        document.head.appendChild(style);
        const urlRegex = /(?<=(?:\s|^|[(]))(?<![!@#$%^&*()_+\-=\[\]{};:'"\\|,\.<>\/?`~])((?:https?:\/\/)?(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}(?:\/\S*)?)\b/g;
        function linkifyTextNode(textNode){
            if(!textNode.nodeValue || !urlRegex.test(textNode.nodeValue)) return;
            const span = document.createElement('span');
            let text = textNode.nodeValue, lastIndex = 0;
            urlRegex.lastIndex = 0;
            let match;
            while((match = urlRegex.exec(text)) !== null){
                const url = match[0], index = match.index;
                span.appendChild(document.createTextNode(text.substring(lastIndex,index)));
                const a = document.createElement('a');
                a.className = 'yt-short-linkify';
                a.style.color = 'inherit';
                a.href = /^https?:\/\//i.test(url) ? url : 'https://' + url;
                a.target = '_blank';
                a.rel = 'noopener noreferrer';
                a.textContent = url.replace(/^https?:\/\//i,'');
                a.addEventListener('mouseenter', function(){
                    const tooltip = document.createElement('div');
                    tooltip.className = 'custom-tooltip';
                    tooltip.textContent = a.href;
                    document.body.appendChild(tooltip);
                    tooltip.offsetWidth;
                    const rect = a.getBoundingClientRect();
                    const tooltipRect = tooltip.getBoundingClientRect();
                    tooltip.style.left = (rect.left+window.pageXOffset+rect.width/2-tooltipRect.width/2)+'px';
                    tooltip.style.top = (rect.top+window.pageYOffset-tooltipRect.height-5)+'px';
                    tooltip.style.opacity = "1";
                    a._tooltip = tooltip;
                });
                a.addEventListener('mouseleave', function(){
                    if(a._tooltip){
                        a._tooltip.style.opacity = "0";
                        setTimeout(()=>{ a._tooltip.remove(); a._tooltip = null; },200);
                    }
                });
                span.appendChild(a);
                lastIndex = index + url.length;
            }
            span.appendChild(document.createTextNode(text.substring(lastIndex)));
            textNode.parentNode.replaceChild(span,textNode);
        }
        function linkifyElement(element){
            const treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
                acceptNode: function(node){
                    return (node.parentNode && node.parentNode.nodeName==='A') ? NodeFilter.FILTER_REJECT : (node.nodeValue && urlRegex.test(node.nodeValue)) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
                }
            });
            const nodes = [];
            while(treeWalker.nextNode()){
                nodes.push(treeWalker.currentNode);
            }
            nodes.forEach(linkifyTextNode);
        }
        const io = new IntersectionObserver(entries=>{
            entries.forEach(entry=>{
                if(entry.isIntersecting){
                    linkifyElement(entry.target);
                    io.unobserve(entry.target);
                }
            });
        },{threshold:0.1});
        document.querySelectorAll('body *').forEach(el=>io.observe(el));
        const mutationObserver = new MutationObserver(mutations=>{
            mutations.forEach(mutation=>{
                if(mutation.type==='childList'){
                    mutation.addedNodes.forEach(node=>{
                        if(node.nodeType===Node.ELEMENT_NODE){
                            io.observe(node);
                        }
                    });
                } else if(mutation.type==='characterData'){
                    if(mutation.target.parentNode){
                        io.observe(mutation.target.parentNode);
                    }
                } else if(mutation.type==='attributes'){
                    if(mutation.target){
                        io.observe(mutation.target);
                    }
                }
            });
        });
        mutationObserver.observe(document.body,{childList:true, subtree:true, characterData:true, attributes:true, attributeFilter:['class','style']});
        function fixJSVoidAnchorsFor(anchor){
            let urlText = anchor.textContent.trim();
            if(!urlText)return;
            if(!/^https?:\/\//i.test(urlText)){
                urlText = 'https://' + urlText;
            }
            const newAnchor = document.createElement('a');
            newAnchor.className = 'yt-short-linkify';
            newAnchor.style.color = 'inherit';
            newAnchor.href = urlText;
            newAnchor.target = '_blank';
            newAnchor.rel = 'noopener noreferrer';
            newAnchor.textContent = urlText.replace(/^https?:\/\//i,'');
            newAnchor.addEventListener('mouseenter', function(){
                const tooltip = document.createElement('div');
                tooltip.className = 'custom-tooltip';
                tooltip.textContent = newAnchor.href;
                document.body.appendChild(tooltip);
                tooltip.offsetWidth;
                const rect = newAnchor.getBoundingClientRect();
                const tooltipRect = tooltip.getBoundingClientRect();
                tooltip.style.left = (rect.left+window.pageXOffset+rect.width/2-tooltipRect.width/2)+'px';
                tooltip.style.top = (rect.top+window.pageYOffset-tooltipRect.height-5)+'px';
                tooltip.style.opacity = "1";
                newAnchor._tooltip = tooltip;
            });
            newAnchor.addEventListener('mouseleave', function(){
                if(newAnchor._tooltip){
                    newAnchor._tooltip.style.opacity = "0";
                    setTimeout(()=>{ newAnchor._tooltip.remove(); newAnchor._tooltip = null; },200);
                }
            });
            anchor.parentNode.replaceChild(newAnchor,anchor);
        }
        const ioJS = new IntersectionObserver(entries=>{
            entries.forEach(entry=>{
                if(entry.isIntersecting){
                    if(entry.target.matches && entry.target.matches('a[href^="javascript:void"]')){
                        fixJSVoidAnchorsFor(entry.target);
                    }
                    ioJS.unobserve(entry.target);
                }
            });
        },{threshold:0.1});
        const mutationObserverJS = new MutationObserver(mutations=>{
            mutations.forEach(mutation=>{
                mutation.addedNodes.forEach(node=>{
                    if(node.nodeType===Node.ELEMENT_NODE){
                        if(node.matches && node.matches('a[href^="javascript:void"]')){
                            ioJS.observe(node);
                        } else if(node.querySelectorAll){
                            node.querySelectorAll('a[href^="javascript:void"]').forEach(anchor=>ioJS.observe(anchor));
                        }
                    }
                });
            });
        });
        mutationObserverJS.observe(document.body,{childList:true, subtree:true});
    }
    window.addEventListener('load', ()=>{
        initLinkify();
    });
})();