// ==UserScript== // @name ふたば不可視文字 可視化+投稿クリーン // @namespace https://example.com/ // @version 1.1 // @description ゼロ幅スペースなどを可視化し、投稿時には可視化された文字列ごと削除 // @match *://*.2chan.net/* // @grant none // ==/UserScript== (function () { 'use strict'; // 不可視文字とその表示用ラベル const invisibleMap = { '\u200B': '[ZWSP]', '\u200C': '[ZWNJ]', '\u200D': '[ZWJ]', '\u2060': '[WJ]', '\uFEFF': '[BOM]', '\u00A0': '[NBSP]' }; const labelSet = new Set(Object.values(invisibleMap)); // 可視化ラベルのSet // 可視化処理:テキストノードを走査して不可視文字にラベルを挿入 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'; 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); } // 投稿前に textarea をクリーンアップ function cleanBeforeSubmit() { const invisibleCharRegex = new RegExp( '[' + Object.keys(invisibleMap).join('') + ']|' + [...labelSet].map(label => label.replace(/[\[\]\\]/g, '\\$&')).join('|'), 'g' ); const forms = document.querySelectorAll('form'); forms.forEach(form => { form.addEventListener('submit', e => { const textareas = form.querySelectorAll('textarea'); textareas.forEach(textarea => { textarea.value = textarea.value.replace(invisibleCharRegex, ''); }); }, true); }); } // 初期化 window.addEventListener('load', () => { scanAndVisualize(); cleanBeforeSubmit(); new MutationObserver(scanAndVisualize).observe(document.body, { childList: true, subtree: true }); }); })();