// ==UserScript== // @name ふたば不可視文字 可視化+投稿クリーン(完全対応版) // @namespace https://example.com/ // @version 1.4 // @description 不可視文字を可視化し、入力・投稿時に確実に除去 // @match *://*.2chan.net/* // @grant none // ==/UserScript== (function () { 'use strict'; // 可視化対象の不可視文字とそのラベル const invisibleMap = { '\u200B': '[ZWSP]', '\u200C': '[ZWNJ]', '\u200D': '[ZWJ]', '\u2060': '[WJ]', '\uFEFF': '[BOM]', '\u00A0': '[NBSP]', '\u2028': '[LS]', // Line Separator '\u2029': '[PS]' // Paragraph Separator }; const labelSet = new Set(Object.values(invisibleMap)); // テキストノードを可視化 function visualizeInvisibleChars(node) { if (node.nodeType === Node.TEXT_NODE) { let original = node.nodeValue; let modified = original; let matched = false; for (const [char, label] of Object.entries(invisibleMap)) { if (modified.includes(char)) { matched = true; modified = modified.split(char).join(label); } } if (matched) { const span = document.createElement('span'); span.textContent = modified; span.style.backgroundColor = 'yellow'; span.title = '不可視文字検出済み'; node.parentNode.replaceChild(span, node); } } else if (node.nodeType === Node.ELEMENT_NODE && !node.closest('textarea')) { for (const child of Array.from(node.childNodes)) { visualizeInvisibleChars(child); } } } // 全文書をスキャンして可視化 function scanAndVisualize() { visualizeInvisibleChars(document.body); } // 投稿前・入力時に不可視文字+可視化ラベルを完全削除 function cleanBeforeSubmit() { const invisibleCharRegex = new RegExp( '[' + Object.keys(invisibleMap).join('') + ']', 'g' ); const labelRegex = new RegExp( [...labelSet].map(label => label.replace(/[\[\]\\]/g, '\\$&')).join('|'), 'g' ); const textareas = document.querySelectorAll('textarea'); textareas.forEach(textarea => { const clean = () => { textarea.value = textarea.value .replace(invisibleCharRegex, '') .replace(labelRegex, ''); }; // 入力監視(ペーストや編集で入る場合) textarea.addEventListener('input', clean); // 送信時の最終クリーン処理 textarea.form?.addEventListener('submit', () => { clean(); }, true); }); } // 初期化 window.addEventListener('load', () => { scanAndVisualize(); cleanBeforeSubmit(); new MutationObserver(scanAndVisualize).observe(document.body, { childList: true, subtree: true }); }); })();