URLやドメイン名(先頭の "h" が抜けている場合も含む)をクリック可能なリンクに変換する。ただし、入力中のフォーム領域は除外する。
当前为
// ==UserScript==
// @name Convert Any links to Clickable Links
// @namespace kdroidwin.hatenablog.com
// @version 1.9
// @description URLやドメイン名(先頭の "h" が抜けている場合も含む)をクリック可能なリンクに変換する。ただし、入力中のフォーム領域は除外する。
// @author K
// @match *://*/*
// @exclude *://github.com/*
// @exclude *://chat.openai.com/*
// @exclude *://blog.hatena.ne.jp/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
function isEditable(node) {
if (!node) return false;
if (node.nodeName === 'INPUT' || node.nodeName === 'TEXTAREA') return true;
if (node.hasAttribute && node.hasAttribute('contenteditable') && node.getAttribute('contenteditable') !== 'false') {
return true;
}
return false;
}
function convertTextToLinks(root = document.body) {
const pattern = /(h?ttps?:\/\/[^\s]+|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?)/g;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode: node => {
let current = node.parentNode;
while (current) {
if (isEditable(current)) return NodeFilter.FILTER_REJECT;
current = current.parentNode;
}
if (node.parentNode && node.parentNode.tagName !== 'A' && pattern.test(node.nodeValue)) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_REJECT;
}
});
let nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
nodes.forEach(node => {
const fragment = document.createDocumentFragment();
const matches = node.nodeValue.match(pattern);
if (!matches) return;
let lastIndex = 0;
matches.forEach(match => {
const matchIndex = node.nodeValue.indexOf(match, lastIndex);
if (matchIndex > lastIndex) {
fragment.appendChild(document.createTextNode(node.nodeValue.substring(lastIndex, matchIndex)));
}
const link = document.createElement('a');
if (/^ttps:\/\//i.test(match)) {
link.href = 'h' + match;
} else if (/^https?:\/\//i.test(match)) {
link.href = match;
} else {
link.href = 'https://' + match;
}
link.textContent = match;
link.target = '_blank';
fragment.appendChild(link);
lastIndex = matchIndex + match.length;
});
if (lastIndex < node.nodeValue.length) {
fragment.appendChild(document.createTextNode(node.nodeValue.substring(lastIndex)));
}
node.parentNode.replaceChild(fragment, node);
});
}
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// 初回実行
convertTextToLinks();
// DOM の変更を監視(変更があったら 300ms 後に実行)
const observer = new MutationObserver(debounce(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
convertTextToLinks(node);
}
}
}
}, 300));
observer.observe(document.body, { childList: true, subtree: true });
})();