1// ==UserScript== 2// @name .lu Short Link Converter 3// @namespace http://tampermonkey.net/ 4// @version 1.3 5// @description .lu/xxxxx を https://d.kuku.lu/xxxxx へのリンクに変換 6// @match https://may.2chan.net/* 7// @grant none 8// @run-at document-end 9// ==/UserScript== 10 11(function () { 12 'use strict'; 13 14 const pattern = /\.lu\/([a-zA-Z0-9]+)/g; 15 16 function convertTextNode(node) { 17 const text = node.nodeValue; 18 19 pattern.lastIndex = 0; 20 21 if (!pattern.test(text)) { 22 pattern.lastIndex = 0; 23 return; 24 } 25 26 pattern.lastIndex = 0; 27 28 const fragment = document.createDocumentFragment(); 29 let lastIndex = 0; 30 let match; 31 32 while ((match = pattern.exec(text)) !== null) { 33 34 // マッチする前の文字 35 fragment.appendChild( 36 document.createTextNode( 37 text.slice(lastIndex, match.index) 38 ) 39 ); 40 41 // 実際に開くURL 42 const fullUrl = 'https://d.kuku.lu/' + match[1]; 43 44 // リンク作成 45 const link = document.createElement('a'); 46 47 // クリック時のURL 48 link.href = fullUrl; 49 50 // ★ 表示は元の .lu/XXXX のまま 51 link.textContent = match[0]; 52 53 link.target = '_blank'; 54 link.rel = 'noopener noreferrer'; 55 56 // 見た目 57 link.style.color = '#06c'; 58 link.style.textDecoration = 'underline'; 59 60 fragment.appendChild(link); 61 62 lastIndex = pattern.lastIndex; 63 } 64 65 // 残りの文字 66 fragment.appendChild( 67 document.createTextNode(text.slice(lastIndex)) 68 ); 69 70 node.parentNode.replaceChild(fragment, node); 71 } 72 73 function scan(root) { 74 const walker = document.createTreeWalker( 75 root, 76 NodeFilter.SHOW_TEXT, 77 { 78 acceptNode(node) { 79 const parent = node.parentElement; 80 81 if (!parent) { 82 return NodeFilter.FILTER_REJECT; 83 } 84 85 // 既にリンクになっているものなどは除外 86 if ( 87 parent.closest( 88 'a, script, style, textarea, input' 89 ) 90 ) { 91 return NodeFilter.FILTER_REJECT; 92 } 93 94 pattern.lastIndex = 0; 95 96 return pattern.test(node.nodeValue) 97 ? NodeFilter.FILTER_ACCEPT 98 : NodeFilter.FILTER_REJECT; 99 } 100 } 101 ); 102 103 const nodes = []; 104 105 while (walker.nextNode()) { 106 nodes.push(walker.currentNode); 107 } 108 109 nodes.forEach(convertTextNode); 110 } 111 112 // 初回処理 113 if (document.body) { 114 scan(document.body); 115 } 116 117 // 後から追加されたレスにも対応 118 const observer = new MutationObserver((mutations) => { 119 for (const mutation of mutations) { 120 for (const node of mutation.addedNodes) { 121 122 if (node.nodeType === Node.TEXT_NODE) { 123 convertTextNode(node); 124 125 } else if (node.nodeType === Node.ELEMENT_NODE) { 126 scan(node); 127 } 128 } 129 } 130 }); 131 132 observer.observe(document.body, { 133 childList: true, 134 subtree: true 135 }); 136 137})();