YouTube Shorts Linkify

Converts URLs into clickable links on YouTube Shorts.

当前为 2025-02-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Shorts Linkify
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @license GPL-3.0-or-later
  6. // @description Converts URLs into clickable links on YouTube Shorts.
  7. // @match https://www.youtube.com/shorts/*
  8. // @icon https://www.google.com/s2/favicons?domain=www.youtube.com&sz=64
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14. function ensureProtocol(url) {
  15. url = url.trim();
  16. if (url.startsWith("javascript:void")) return url;
  17. if (url.startsWith("http://") || url.startsWith("https://")) return url;
  18. return "https://" + url;
  19. }
  20. function initLinkify() {
  21. const policy = window.trustedTypes ? trustedTypes.createPolicy('ytShortsLinkify', { createHTML: input => input }) : null;
  22. const style = document.createElement('style');
  23. style.textContent = "a.yt-short-linkify, a.yt-short-linkify:link, a.yt-short-linkify:visited, a.yt-short-linkify:hover, a.yt-short-linkify:active, a.yt-short-linkify[href^='javascript:void'], a.yt-core-attributed-string__link, a.yt-core-attributed-string__link--call-to-action-color { color: inherit !important; text-decoration: underline !important; cursor: pointer !important; background-color: inherit !important; } .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; }";
  24. document.head.appendChild(style);
  25. const urlRegex = /(?<![@,;:'"?!])\b((?:javascript:void(?:\([^)]+\))?(?:[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]*)?)|(?:(?:https?:\/\/)?(?:www\.)?[a-zA-Z0-9][a-zA-Z0-9-]*(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}(?:\/[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]*)?))\b/g;
  26. function linkifyTextNode(textNode) {
  27. if (!textNode.nodeValue || !urlRegex.test(textNode.nodeValue)) return;
  28. const span = document.createElement('span');
  29. let text = textNode.nodeValue, lastIndex = 0;
  30. urlRegex.lastIndex = 0;
  31. let match;
  32. while ((match = urlRegex.exec(text)) !== null) {
  33. const url = match[0], index = match.index;
  34. span.appendChild(document.createTextNode(text.substring(lastIndex, index)));
  35. const a = document.createElement('a');
  36. a.className = 'yt-short-linkify';
  37. a.href = ensureProtocol(url);
  38. if (!url.toLowerCase().startsWith("javascript:void")) {
  39. a.target = '_blank';
  40. a.rel = 'noopener noreferrer';
  41. a.textContent = url.replace(/^https?:\/\//i, '');
  42. } else {
  43. a.textContent = url;
  44. }
  45. function removeTooltip() {
  46. if (a._tooltip) {
  47. a._tooltip.remove();
  48. a._tooltip = null;
  49. }
  50. }
  51. a.addEventListener('mouseenter', function() {
  52. removeTooltip();
  53. const tooltip = document.createElement('div');
  54. tooltip.className = 'custom-tooltip';
  55. if (!a.href.toLowerCase().startsWith("javascript:void")) {
  56. tooltip.textContent = a.href;
  57. } else {
  58. tooltip.textContent = a.textContent;
  59. }
  60. document.body.appendChild(tooltip);
  61. const rect = a.getBoundingClientRect();
  62. const tooltipRect = tooltip.getBoundingClientRect();
  63. tooltip.style.left = (rect.left + window.pageXOffset) + 'px';
  64. tooltip.style.top = (rect.top + window.pageYOffset - tooltipRect.height - 5) + 'px';
  65. a._tooltip = tooltip;
  66. setTimeout(removeTooltip, 2000);
  67. });
  68. a.addEventListener('mouseleave', removeTooltip);
  69. a.addEventListener('mouseout', removeTooltip);
  70. span.appendChild(a);
  71. lastIndex = index + url.length;
  72. }
  73. span.appendChild(document.createTextNode(text.substring(lastIndex)));
  74. textNode.parentNode.replaceChild(span, textNode);
  75. }
  76. function linkifyElement(element) {
  77. const treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
  78. acceptNode: function(node) {
  79. if (node.parentNode && node.parentNode.nodeName === 'A') {
  80. const href = node.parentNode.getAttribute('href');
  81. if (href && href.startsWith('javascript:void')) return NodeFilter.FILTER_ACCEPT;
  82. return NodeFilter.FILTER_REJECT;
  83. }
  84. return (node.nodeValue && urlRegex.test(node.nodeValue)) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
  85. }
  86. });
  87. const nodes = [];
  88. while (treeWalker.nextNode()) nodes.push(treeWalker.currentNode);
  89. nodes.forEach(linkifyTextNode);
  90. }
  91. const io = new IntersectionObserver(entries => {
  92. entries.forEach(entry => {
  93. if (entry.isIntersecting) {
  94. linkifyElement(entry.target);
  95. io.unobserve(entry.target);
  96. }
  97. });
  98. }, { threshold: 0.1 });
  99. document.querySelectorAll('body *').forEach(el => io.observe(el));
  100. const mutationObserver = new MutationObserver(mutations => {
  101. mutations.forEach(mutation => {
  102. if (mutation.type === 'childList') {
  103. mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) io.observe(node); });
  104. } else if (mutation.type === 'characterData') {
  105. if (mutation.target.parentNode) io.observe(mutation.target.parentNode);
  106. } else if (mutation.type === 'attributes' && mutation.target) {
  107. io.observe(mutation.target);
  108. }
  109. });
  110. });
  111. mutationObserver.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class', 'style'] });
  112. document.addEventListener('click', function() {
  113. document.querySelectorAll('.custom-tooltip').forEach(function(tooltip) { tooltip.remove(); });
  114. });
  115. function attachOuterLinkRedirect() {
  116. document.querySelectorAll('a.yt-core-attributed-string__link--call-to-action-color').forEach(outerLink => {
  117. outerLink.removeEventListener('click', outerLink._redirectHandler, true);
  118. outerLink._redirectHandler = function(event) {
  119. event.preventDefault();
  120. event.stopPropagation();
  121. event.stopImmediatePropagation();
  122. const innerLink = outerLink.querySelector('a.yt-short-linkify');
  123. if (innerLink && innerLink.href && innerLink.href !== "javascript:void(0);") {
  124. setTimeout(() => { window.open(innerLink.href, '_blank'); }, 0);
  125. }
  126. };
  127. outerLink.addEventListener('click', outerLink._redirectHandler, true);
  128. });
  129. }
  130. attachOuterLinkRedirect();
  131. const outerLinkObserver = new MutationObserver(attachOuterLinkRedirect);
  132. outerLinkObserver.observe(document.body, { childList: true, subtree: true, attributes: false });
  133. }
  134. window.addEventListener('load', initLinkify);
  135. })();