Convert Any links to Clickable Links

URLやドメイン名(先頭の "h" が抜けている場合も含む)をクリック可能なリンクに変換する。ただし、入力中のフォーム領域は除外する。

目前为 2025-02-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Convert Any links to Clickable Links
  3. // @namespace kdroidwin.hatenablog.com/
  4. // @version 1.4
  5. // @description URLやドメイン名(先頭の "h" が抜けている場合も含む)をクリック可能なリンクに変換する。ただし、入力中のフォーム領域は除外する。
  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 MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // 入力領域かどうかチェックする関数
  19. function isEditable(node) {
  20. if (!node) return false;
  21. // ノードが input または textarea なら true
  22. if (node.nodeName === 'INPUT' || node.nodeName === 'TEXTAREA') return true;
  23. // contenteditable 属性が設定されているかチェック
  24. if (node.hasAttribute && node.hasAttribute('contenteditable') && node.getAttribute('contenteditable') !== 'false') {
  25. return true;
  26. }
  27. return false;
  28. }
  29.  
  30. function convertTextToLinks() {
  31. // フォーム要素内は除外するため、walker で対象ノードを選ぶときに、親要素が入力可能な要素でないかもチェックする
  32. const pattern = /(h?ttps?:\/\/[^\s]+|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})/g;
  33. const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
  34. acceptNode: node => {
  35. // 親要素または祖先に input/textarea/contenteditable 要素があるかチェック
  36. let current = node.parentNode;
  37. while (current) {
  38. if (isEditable(current)) {
  39. return NodeFilter.FILTER_REJECT;
  40. }
  41. current = current.parentNode;
  42. }
  43. if (node.parentNode && node.parentNode.tagName !== 'A' && pattern.test(node.nodeValue)) {
  44. return NodeFilter.FILTER_ACCEPT;
  45. }
  46. return NodeFilter.FILTER_REJECT;
  47. }
  48. });
  49.  
  50. let nodes = [];
  51. while (walker.nextNode()) {
  52. nodes.push(walker.currentNode);
  53. }
  54.  
  55. nodes.forEach(node => {
  56. const fragment = document.createDocumentFragment();
  57. const parts = node.nodeValue.split(pattern);
  58. parts.forEach(part => {
  59. const text = part.trim();
  60. if (/^ttps:\/\//i.test(text)) {
  61. const link = document.createElement('a');
  62. link.href = 'h' + text;
  63. link.textContent = text;
  64. link.target = '_blank';
  65. fragment.appendChild(link);
  66. } else if (/^https?:\/\//i.test(text)) {
  67. const link = document.createElement('a');
  68. link.href = text;
  69. link.textContent = text;
  70. link.target = '_blank';
  71. fragment.appendChild(link);
  72. } else if (/(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}/.test(text)) {
  73. const link = document.createElement('a');
  74. link.href = 'https://' + text;
  75. link.textContent = text;
  76. link.target = '_blank';
  77. fragment.appendChild(link);
  78. } else {
  79. // ※ここでは元の部分(trim していない)をそのままテキストとして追加
  80. fragment.appendChild(document.createTextNode(part));
  81. }
  82. });
  83. node.parentNode.replaceChild(fragment, node);
  84. });
  85. }
  86.  
  87. // 初回実行
  88. convertTextToLinks();
  89. // DOM の変更がある場合に再度実行する
  90. new MutationObserver(convertTextToLinks).observe(document.body, { childList: true, subtree: true });
  91. })();