您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Converts URLs into clickable links in descriptions and comments on YouTube Shorts.
当前为
// ==UserScript== // @name YouTube Shorts Linkify // @namespace http://tampermonkey.net/ // @version 1.5 // @license GPL-3.0-or-later // @description Converts URLs into clickable links in descriptions and comments 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'; const DEBUG = false; function log(...args) { if (DEBUG) console.log('[YT Shorts Linkify]', ...args); } function addStyles() { const style = document.createElement('style'); style.textContent = ` a.yt-short-linkify { color: inherit !important; /* Matches surrounding text color */ text-decoration: none !important; cursor: pointer; } a.yt-short-linkify:hover, a.yt-short-linkify:focus, a.yt-short-linkify:active { color: inherit !important; text-decoration: underline !important; /* Underline on hover */ outline: none !important; } `; document.head.appendChild(style); log("Styles added."); } function initLinkify() { const policy = window.trustedTypes ? trustedTypes.createPolicy('ytShortsLinkify', { createHTML: input => input }) : null; } const urlRegex = /(?<=\s|^|\()((?:https?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))\b(?!\.)/g; function isSafeToLinkify(node) { let parent = node.parentNode; while (parent && parent !== document.body) { if (parent.nodeName === 'A' || parent.nodeName === 'SCRIPT' || parent.nodeName === 'STYLE') { return false; } parent = parent.parentNode; } return true; } function createLinkElement(url) { const a = document.createElement('a'); a.className = 'yt-short-linkify'; a.href = /^https?:\/\//i.test(url) ? url : 'https://' + url; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = /^https?:\/\//i.test(url) ? url : 'https://' + url; return a; } function linkifyTextNode(textNode) { const text = textNode.nodeValue; if (!text || !/\.[a-zA-Z]{2}/.test(text)) return; urlRegex.lastIndex = 0; if (!urlRegex.test(text)) return; log("Linkifying text node:", text.substring(0, 50) + "..."); const fragment = document.createDocumentFragment(); let lastIndex = 0; let match; urlRegex.lastIndex = 0; while ((match = urlRegex.exec(text)) !== null) { const url = match[1]; const index = match.index; if (index > lastIndex) { fragment.appendChild(document.createTextNode(text.substring(lastIndex, index))); } const link = createLinkElement(url); fragment.appendChild(link); lastIndex = index + match[0].length; } if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.substring(lastIndex))); } textNode.parentNode.replaceChild(fragment, textNode); log("Text node replaced."); } function linkifyElement(element) { if (!element || element.nodeType !== Node.ELEMENT_NODE || !element.isConnected) { log("Skipping linkifyElement for invalid/disconnected element:", element); return; } log("Scanning element for text nodes:", element.tagName); const treeWalker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (node.nodeValue.trim() !== '' && isSafeToLinkify(node)) { urlRegex.lastIndex = 0; if (urlRegex.test(node.nodeValue)) { return NodeFilter.FILTER_ACCEPT; } } return NodeFilter.FILTER_REJECT; } } ); const nodesToProcess = []; let currentNode; while ((currentNode = treeWalker.nextNode())) { nodesToProcess.push(currentNode); } if (nodesToProcess.length > 0) { log(`Found ${nodesToProcess.length} text nodes to process in`, element.tagName); nodesToProcess.forEach(linkifyTextNode); } else { log("No relevant text nodes found in", element.tagName); } } function setupObservers() { log("Setting up observers..."); const observer = new MutationObserver(mutations => { requestAnimationFrame(() => { let addedNodes = new Set(); let charDataNodes = new Set(); mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.isConnected) { addedNodes.add(node); } else if (node.nodeType === Node.TEXT_NODE && node.parentNode?.isConnected) { charDataNodes.add(node.parentNode); } }); } else if (mutation.type === 'characterData') { if (mutation.target.parentNode?.isConnected) { charDataNodes.add(mutation.target.parentNode); } } }); addedNodes.forEach(node => { if (node.isConnected) { linkifyElement(node); } }); charDataNodes.forEach(node => { if (node.isConnected && node !== document.body && node !== document.documentElement) { linkifyElement(node); } }); }); }); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); log("MutationObserver attached to body."); document.querySelectorAll('body *').forEach(el => { if (el.isConnected && el.textContent?.trim() && /\.[a-zA-Z]{2}/.test(el.textContent)) { if (!el.matches('a') && !el.closest('a')) { linkifyElement(el); } } }); log("Initial scan complete."); } function init() { log("Initializing script..."); initLinkify(); addStyles(); setupObservers(); log("Script initialized."); } if (document.readyState === 'loading') { window.addEventListener('load', init); } else { init(); } })();