Converts URLs into clickable links on YouTube Shorts.
当前为
// ==UserScript==
// @name YouTube Shorts Linkify
// @namespace http://tampermonkey.net/
// @version 1.2
// @author UniverseDev
// @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);
}
function observeJSVoidAnchors(){
const io = new IntersectionObserver(entries=>{
entries.forEach(entry=>{
if(entry.isIntersecting){
if(entry.target.matches && entry.target.matches('a[href^="javascript:void"]')){
fixJSVoidAnchorsFor(entry.target);
}
io.unobserve(entry.target);
}
});
}, {threshold:0.1});
const mutationObserver = 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"]')){
io.observe(node);
} else if(node.querySelectorAll){
node.querySelectorAll('a[href^="javascript:void"]').forEach(anchor => io.observe(anchor));
}
}
});
});
});
mutationObserver.observe(document.body, {childList:true, subtree:true});
}
window.addEventListener('load', ()=>{
initLinkify();
});
})();