Convert Any Links to Clickable Links

すべてのリンクをクリックできるリンクにかえる。

  1. // ==UserScript==
  2. // @name Convert Any Links to Clickable Links
  3. // @namespace kdroidwin.hatenablog.com
  4. // @version 3.1
  5. // @description すべてのリンクをクリックできるリンクにかえる。
  6. // @author Kdroidwin
  7. // @match *://*/*
  8. // @exclude *://github.com/*
  9. // @exclude *://chat.openai.com/*
  10. // @exclude *://blog.hatena.ne.jp/*
  11. // @grant none
  12. // @license GPL-3.0
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const urlPattern = /\b(?:h?ttps?:\/\/[^\s<>"]+|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s<>"]*)?)\b/g;
  19.  
  20. function convertTextToLinks(node) {
  21. if (node.nodeType !== 3 || !urlPattern.test(node.nodeValue)) return;
  22.  
  23. const parent = node.parentNode;
  24. if (parent.tagName === 'A' || parent.matches('input, textarea, [contenteditable]')) return;
  25.  
  26. const frag = document.createDocumentFragment();
  27. let lastIndex = 0;
  28.  
  29. node.nodeValue.replace(urlPattern, (match, offset) => {
  30. frag.appendChild(document.createTextNode(node.nodeValue.slice(lastIndex, offset)));
  31.  
  32. let url = match;
  33. if (url.startsWith('ttp')) {
  34. url = 'h' + url; // 'ttps://' → 'https://'
  35. } else if (!url.includes('://')) {
  36. url = 'https://' + url; // 'example.com' → 'https://example.com'
  37. }
  38.  
  39. const a = document.createElement('a');
  40. a.href = url;
  41. a.textContent = url;
  42. a.target = '_blank';
  43. a.style.display = 'inline'; // レイアウト崩れ防止
  44.  
  45. frag.appendChild(a);
  46. lastIndex = offset + match.length;
  47. });
  48.  
  49. frag.appendChild(document.createTextNode(node.nodeValue.slice(lastIndex)));
  50. parent.replaceChild(frag, node);
  51. }
  52.  
  53. function debounce(func, delay) {
  54. let timer;
  55. return (...args) => {
  56. clearTimeout(timer);
  57. timer = setTimeout(() => func(...args), delay);
  58. };
  59. }
  60.  
  61. const observer = new MutationObserver(debounce(mutations => {
  62. for (const mutation of mutations) {
  63. if (mutation.type === 'childList') {
  64. for (const node of mutation.addedNodes) {
  65. if (node.nodeType === 1) {
  66. node.querySelectorAll('*').forEach(el => {
  67. for (const textNode of el.childNodes) {
  68. convertTextToLinks(textNode);
  69. }
  70. });
  71. } else if (node.nodeType === 3) {
  72. convertTextToLinks(node);
  73. }
  74. }
  75. } else if (mutation.type === 'characterData') {
  76. convertTextToLinks(mutation.target);
  77. }
  78. }
  79. }, 500));
  80.  
  81. observer.observe(document.body, { childList: true, subtree: true, characterData: true });
  82.  
  83. document.body.querySelectorAll('*').forEach(el => {
  84. for (const node of el.childNodes) {
  85. convertTextToLinks(node);
  86. }
  87. });
  88. })();