YouTube Shorts Linkify

Converts URLs into clickable links on YouTube Shorts.

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

// ==UserScript==
// @name         YouTube Shorts Linkify
// @namespace    http://tampermonkey.net/
// @author       UniverseDev
// @license      GPL-3.0-or-later
// @version      1.2
// @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';
  let tooltipElement = document.createElement('div');
  tooltipElement.className = 'custom-tooltip';
  tooltipElement.style.display = 'none';
  const urlRegex = /\b((?:https?:\/\/)?(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}(?:\/\S*?))(?=\s|$)/g;
  function isInsideTooltip(node) {
    let current = node.parentNode;
    while (current) {
      if (current.classList && current.classList.contains('custom-tooltip')) {
        return true;
      }
      current = current.parentNode;
    }
    return false;
  }
  function linkifyTextNode(textNode) {
    if (!textNode.nodeValue) return;
    urlRegex.lastIndex = 0;
    if (!urlRegex.test(textNode.nodeValue)) return;
    const span = document.createElement('span');
    const text = textNode.nodeValue;
    let lastIndex = 0;
    urlRegex.lastIndex = 0;
    let match;
    while ((match = urlRegex.exec(text)) !== null) {
      const url = match[1];
      const index = match.index;
      span.appendChild(document.createTextNode(text.substring(lastIndex, index)));
      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 = url;
      a.addEventListener('mouseenter', function() {
        tooltipElement.textContent = url;
        tooltipElement.style.display = 'block';
        const rect = a.getBoundingClientRect();
        const tooltipRect = tooltipElement.getBoundingClientRect();
        tooltipElement.style.left = (rect.left + window.pageXOffset) + 'px';
        tooltipElement.style.top = (rect.top + window.pageYOffset - tooltipRect.height - 5) + 'px';
      });
      a.addEventListener('mouseleave', function() {
        tooltipElement.style.display = 'none';
      });
      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) {
        if (node.parentNode && node.parentNode.nodeName === 'A') return NodeFilter.FILTER_REJECT;
        if (isInsideTooltip(node)) return NodeFilter.FILTER_REJECT;
        urlRegex.lastIndex = 0;
        return (node.nodeValue && urlRegex.test(node.nodeValue))
          ? NodeFilter.FILTER_ACCEPT
          : NodeFilter.FILTER_REJECT;
      }
    });
    const nodes = [];
    while (treeWalker.nextNode()) {
      nodes.push(treeWalker.currentNode);
    }
    nodes.forEach(linkifyTextNode);
  }
  function initLinkify() {
    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.9; white-space: nowrap; }";
    document.head.appendChild(style);
    document.body.appendChild(tooltipElement);
    const io = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          linkifyElement(entry.target);
          io.unobserve(entry.target);
        }
      });
    }, { threshold: 0.1 });
    document.querySelectorAll('body *:not(.custom-tooltip)').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 && !node.classList.contains('custom-tooltip')) {
              io.observe(node);
            }
          });
        } else if (mutation.type === 'characterData') {
          if (mutation.target.parentNode && !isInsideTooltip(mutation.target)) {
            io.observe(mutation.target.parentNode);
          }
        } else if (mutation.type === 'attributes') {
          if (mutation.target && !mutation.target.classList.contains('custom-tooltip')) {
            io.observe(mutation.target);
          }
        }
      });
    });
    mutationObserver.observe(document.body, {
      childList: true,
      subtree: true,
      characterData: true,
      attributes: true,
      attributeFilter: ['class', 'style']
    });
  }
  window.addEventListener('load', initLinkify);
})();