链接助手 - 三击自动转换

三击将文本URL转换为可点击链接

  1. // ==UserScript==
  2. // @name Link Helper - Triple Click Text to Link
  3. // @name:zh-CN 链接助手 - 三击自动转换
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.1
  6. // @description Convert text URLs to links on triple click
  7. // @description:zh-CN 三击将文本URL转换为可点击链接
  8. // @author Alex3236
  9. // @match *://*/*
  10. // @grant none
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const CLICK_TIMEOUT = 1000;
  18. const CLICK_THRESHOLD = 10;
  19. const URL_REGEX = /(https?:\/\/[^\s<]+|www\.[^\s<]+\.[^\s<]{2,})/gi;
  20. const STYLE = "color: #66CCFF; background: #163E64";
  21.  
  22. let clicks = [];
  23.  
  24. document.addEventListener('click', function(e) {
  25. const now = Date.now();
  26. clicks.push({
  27. time: now,
  28. x: e.clientX,
  29. y: e.clientY
  30. });
  31.  
  32. if (clicks.length > 3) clicks.shift();
  33.  
  34. if (clicks.length === 3) {
  35. const [first, second, third] = clicks;
  36.  
  37. if (third.time - first.time < CLICK_TIMEOUT &&
  38. distance(first, second) < CLICK_THRESHOLD &&
  39. distance(second, third) < CLICK_THRESHOLD) {
  40.  
  41. processTripleClick(e.clientX, e.clientY);
  42. clicks = []; // 重置点击记录
  43. }
  44. }
  45. });
  46.  
  47. function distance(a, b) {
  48. return Math.hypot(a.x - b.x, a.y - b.y);
  49. }
  50.  
  51. function processTripleClick(x, y) {
  52. const textNode = getTextNodeFromPoint(x, y);
  53. if (!textNode || isInsideLink(textNode)) return;
  54.  
  55. const text = textNode.nodeValue;
  56. const matches = findUrls(text);
  57.  
  58. if (matches.length > 0) {
  59. replaceTextWithLinks(textNode, matches);
  60. }
  61. }
  62.  
  63. function getTextNodeFromPoint(x, y) {
  64. let range;
  65. if (document.caretRangeFromPoint) {
  66. range = document.caretRangeFromPoint(x, y);
  67. } else if (document.caretPositionFromPoint) {
  68. const pos = document.caretPositionFromPoint(x, y);
  69. if (!pos) return null;
  70. range = document.createRange();
  71. range.setStart(pos.offsetNode, pos.offset);
  72. range.collapse(true);
  73. }
  74. return range?.startContainer?.nodeType === Node.TEXT_NODE ? range.startContainer : null;
  75. }
  76.  
  77. function isInsideLink(node) {
  78. let parent = node.parentNode;
  79. while (parent) {
  80. if (parent.tagName === 'A') return true;
  81. parent = parent.parentNode;
  82. }
  83. return false;
  84. }
  85.  
  86. function findUrls(text) {
  87. const matches = [];
  88. let match;
  89.  
  90. while ((match = URL_REGEX.exec(text)) !== null) {
  91. matches.push({
  92. start: match.index,
  93. end: match.index + match[0].length,
  94. url: match[0]
  95. });
  96. }
  97. return matches;
  98. }
  99.  
  100. function replaceTextWithLinks(textNode, matches) {
  101. const parent = textNode.parentNode;
  102. const docFrag = document.createDocumentFragment();
  103. let lastIndex = 0;
  104.  
  105. matches.forEach(match => {
  106. if (match.start > lastIndex) {
  107. docFrag.appendChild(document.createTextNode(
  108. textNode.nodeValue.slice(lastIndex, match.start)
  109. ));
  110. }
  111.  
  112. const a = document.createElement('a');
  113. a.style = STYLE;
  114. a.href = match.url.startsWith('http') ? match.url : `http://${match.url}`;
  115. a.textContent = match.url;
  116. a.target = '_blank';
  117. docFrag.appendChild(a);
  118.  
  119. lastIndex = match.end;
  120. });
  121.  
  122. if (lastIndex < textNode.nodeValue.length) {
  123. docFrag.appendChild(document.createTextNode(
  124. textNode.nodeValue.slice(lastIndex)
  125. ));
  126. }
  127.  
  128. parent.replaceChild(docFrag, textNode);
  129. }
  130. })();