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})();