4chanX String Replacement

Replaces text in 4chan posts. Supports literal strings and simplified regex with automatic case-insensitivity. Fully compatible with 4chanX and Oneechan themes.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        4chanX String Replacement
// @version     2.1
// @description Replaces text in 4chan posts. Supports literal strings and simplified regex with automatic case-insensitivity. Fully compatible with 4chanX and Oneechan themes.
// @author      kpganon
// @license     MIT
// @namespace   https://github.com/kpg-anon/scripts
// @include     /^https?://boards\.4chan(nel)?\.org/\w+/thread/\d+/
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_download
// @grant       GM_xmlhttpRequest
// @run-at      document-idle
// ==/UserScript==

(function() {

    function applyStaticStyles() {
        GM_addStyle(`
            #wordReplacerMenu {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                z-index: 9999;
                background-color: #282a36;
                color: #f8f8f2;
                border: 1px solid #6272a4;
                padding: 10px;
                width: auto;
                max-height: 800px;
                overflow-y: auto;
                display: none;
            }
            #wordReplacerMenu #rulesContainer {
                margin-top: 10px;
            }
            #wordReplacerMenu #rulesContainer div {
                display: flex;
                justify-content: space-between;
                margin-top: 5px;
            }
            #wordReplacerMenu input {
                flex: 1;
                margin-right: 10px;
                max-width: 45%;
                text-align: center;
                position: relative;
                border: 1px solid #6272a4;
                color: #c5c8c6;
            }
            #wordReplacerMenu #buttonContainer {
                text-align: center;
                width: 100%;
                margin-top: 12px;
            }
            #wordReplacerMenu button {
                margin: 0 2px;
                display: inline-block;
                cursor: pointer;
                border-radius: 2px;
            }
            #wordReplacerMenu button:hover {
                filter: brightness(1.2);
            }

            #wordReplacerMenu button:active {
                transform: scale(0.95);
            }
        `);
    }

    function applyDynamicStyles() {
        const nameElementColor = window.getComputedStyle(document.querySelector('.name')).color;
        const rgbaBorderColor = nameElementColor.replace('rgb', 'rgba').replace(')', ', 0.75)');
        GM_addStyle(`#wordReplacerMenu { border: 1px solid ${nameElementColor}; }`);

        const replyElement = document.querySelector('.reply');
        if (replyElement) {
            const computedReplyBgColor = window.getComputedStyle(replyElement).backgroundColor;
            const replyRgbaColor = computedReplyBgColor.replace('rgb', 'rgba').replace(')', ', 0.9)');
            GM_addStyle(`#wordReplacerMenu { background-color: ${replyRgbaColor} !important; }`);
        }

        const textareaElement = document.querySelector('textarea');
        if (textareaElement) {
            const computedTextareaBgColor = window.getComputedStyle(textareaElement).backgroundColor;
            const textareaRgbMatch = computedTextareaBgColor.match(/\d+, \d+, \d+/);
            if (textareaRgbMatch) {
                const textareaRgbaColor = `rgba(${textareaRgbMatch[0]}, 0.9)`;
                GM_addStyle(`#wordReplacerMenu input { background-color: ${textareaRgbaColor}; }`);
            }
        }

        const submitButton = document.querySelector('input[type="submit"]');
        const bodyColor = window.getComputedStyle(document.body).color;
        let buttonTextColor;

        if (bodyColor === 'rgb(197, 200, 198)') {
            buttonTextColor = 'black';
        } else {
            buttonTextColor = bodyColor;
        }

        if (submitButton) {
            const buttonBgColor = window.getComputedStyle(submitButton).backgroundColor;
            GM_addStyle(`
                #wordReplacerMenu button {
                    background-color: ${buttonBgColor};
                    color: ${buttonTextColor};
                    border-radius: 2px;
                    border: 1px solid ${rgbaBorderColor};
                }
            `);
        }
    }

    function observeStyleChanges() {
        const styleElement = document.getElementById('ch4SS'); 
        if (!styleElement) {
            return;
        }

        const observerOptions = {
            childList: true,
            subtree: true
        };

        const styleChangeObserver = new MutationObserver((mutations) => {
            for (let mutation of mutations) {
                if (mutation.type === 'childList') {
                    applyDynamicStyles();
                }
            }
        });

        styleChangeObserver.observe(styleElement, observerOptions);
    }

    function createMenu() {
        const menu = document.createElement('div');
        menu.id = 'wordReplacerMenu';

        const rulesContainer = document.createElement('div');
        rulesContainer.id = 'rulesContainer';
        menu.appendChild(rulesContainer);

        const buttonContainer = document.createElement('div');
        buttonContainer.id = 'buttonContainer';
        menu.appendChild(buttonContainer);

        document.body.appendChild(menu);

        addRuleRow();
        addButtons(buttonContainer);
        applyDynamicStyles();
    }

    function addButtons(container) {
        const buttons = ['Add Rule', 'Save and Apply', 'Import', 'Export', 'Close'];
        const functions = [() => addRuleRow(), saveRules, importRules, exportRules, toggleMenu];

        buttons.forEach((text, index) => {
            const button = document.createElement('button');
            button.textContent = text;
            button.addEventListener('click', functions[index]);
            container.appendChild(button);
        });
    }

    function addRuleRow(pattern = '', replacement = '') {
        const inputContainer = document.createElement('div');
        const patternInput = createInput('Text/pattern to replace', pattern);
        const replacementInput = createInput('Replacement text', replacement);

        inputContainer.appendChild(patternInput);
        inputContainer.appendChild(replacementInput);

        const rulesContainer = document.getElementById('rulesContainer');
        rulesContainer.appendChild(inputContainer);
    }

    function createInput(placeholder, value) {
        const input = document.createElement('input');
        input.placeholder = placeholder;
        input.value = value;
        return input;
    }

    function saveRules() {
        const rulesContainer = document.getElementById('rulesContainer');
        const inputRows = Array.from(rulesContainer.children);
        const rules = inputRows.reduce((acc, row) => {
            const pattern = row.children[0].value;
            const replacement = row.children[1].value;
            if (pattern || replacement) {
                acc.push({ pattern, replacement });
            }
            return acc;
        }, []);
        GM_setValue('wordReplacerRules', JSON.stringify(rules));
    }

    function importRules() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.onchange = (event) => {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const data = JSON.parse(e.target.result);
                    clearRules();
                    data.forEach(rule => {
                        addRuleRow(rule.pattern, rule.replacement);
                    });
                    if (data.length >= 4) {
                        addRuleRow();
                    }
                };
                reader.readAsText(file);
            }
        };
        input.click();
    }

    function exportRules() {
        const savedRules = GM_getValue('wordReplacerRules');
        const blob = new Blob([savedRules], {type: "application/json"});
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.style.display = 'none';
        a.href = url;
        a.download = `${Date.now()}-replacer-config.json`;

        document.body.appendChild(a);
        a.click();

        URL.revokeObjectURL(url);
        document.body.removeChild(a);
    }

    function clearRules() {
        const rulesContainer = document.getElementById('rulesContainer');
        rulesContainer.innerHTML = '';
    }

    function loadRules() {
        clearRules();
        const savedRules = GM_getValue('wordReplacerRules');
        if (savedRules) {
            const rules = JSON.parse(savedRules);
            rules.forEach(rule => {
                addRuleRow(rule.pattern, rule.replacement);
            });
            if (rules.length >= 4) {
                addRuleRow();
            } else {
                for (let i = rules.length; i < 4; i++) {
                    addRuleRow();
                }
            }
        } else {
            for (let i = 0; i < 4; i++) {
                addRuleRow();
            }
        }
    }

    function toggleMenu() {
        const menu = document.getElementById('wordReplacerMenu');
        const toggleButtonIcon = document.querySelector('.fa-pencil');

        if (menu.style.display === 'none') {
            loadRules();
            menu.style.display = 'block';
            toggleButtonIcon.classList.remove('disabled');
            applyDynamicStyles();
        } else {
            menu.style.display = 'none';
            toggleButtonIcon.classList.add('disabled');
        }
    }

    function createToggleButton() {
        const toggleButtonIcon = document.createElement('a');
        toggleButtonIcon.className = 'fa fa-pencil';
        toggleButtonIcon.title = 'Toggle Replacer';
        toggleButtonIcon.href = 'javascript:;';
        toggleButtonIcon.addEventListener('click', toggleMenu);

        toggleButtonIcon.classList.add('disabled');

        const toggleButtonContainer = document.createElement('span');
        toggleButtonContainer.className = 'shortcut brackets-wrap';
        toggleButtonContainer.appendChild(toggleButtonIcon);

        const waitForHeader = setInterval(() => {
            const shortcutsContainer = document.getElementById('shortcuts');
            const statsShortcut = document.getElementById('shortcut-stats');
            if (shortcutsContainer && statsShortcut) {
                clearInterval(waitForHeader);
                shortcutsContainer.insertBefore(toggleButtonContainer, statsShortcut.nextSibling);
            }
        }, 100);
    }


    applyStaticStyles();
    createMenu();
    createToggleButton();
    observeStyleChanges();

    function reconnectObserver() {
        if (observer && !document.body.contains(observer.target)) {
            observer.disconnect();
            observer.observe(document.body, observerConfig);
        }
    }

    function handleNewPosts() {
        const newPosts = document.querySelectorAll('.postMessage:not(.processed)');
        newPosts.forEach(post => {
            replaceTextInPost(post);
            post.classList.add('processed');
        });
    }

    function handleVisibilityChange() {
        if (!document.hidden) {
            reconnectObserver();
            posts.forEach(replaceTextInPost);
        }
    }

    function updateAndProcessPosts() {
        const posts = document.querySelectorAll('.postMessage');
        posts.forEach(replaceTextInPost);
    }

    function replaceTextInNode(node) {
    if (node.parentElement && node.parentElement.tagName === 'A') {
        return;
    }
        const savedRules = GM_getValue('wordReplacerRules');
        if (savedRules) {
            const rules = JSON.parse(savedRules);
            rules.forEach(rule => {
                if (rule.pattern) {
                    const pattern = new RegExp(rule.pattern, 'gi');
                    if (node.nodeType === 3) {
                        node.nodeValue = node.nodeValue.replace(pattern, rule.replacement);
                    }
                }
            });
        }
    }

    function replaceTextInPost(postElement) {
        Array.from(postElement.childNodes).forEach(replaceTextInNode);

        let greentexts = postElement.querySelectorAll('.quote');
        greentexts.forEach(quoteElement => {
            Array.from(quoteElement.childNodes).forEach(replaceTextInNode);
        });
    }

    function processMutations(mutations) {
        for (let mutation of mutations) {
            if (mutation.addedNodes && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        let postNodes = node.querySelectorAll('.postMessage');
                        if (postNodes.length > 0) {
                            postNodes.forEach(replaceTextInPost);
                        } else if (node.classList && node.classList.contains('postMessage')) {
                            replaceTextInPost(node);
                        }
                    }
                });
            }
        }
    }

    const observerConfig = {
        childList: true,
        subtree: true
    };

    const observer = new MutationObserver((mutations) => {
        processMutations(mutations);
        reconnectObserver();
    });
    observer.observe(document.body, observerConfig);

    const posts = document.querySelectorAll('.postMessage');
    posts.forEach(replaceTextInPost);

    updateAndProcessPosts();

    document.addEventListener('visibilitychange', handleVisibilityChange);

    document.addEventListener('ThreadUpdate', handleNewPosts);

    document.addEventListener('click', (e) => {
        if (e.target && e.target.innerText === 'Save and Apply') {
            setTimeout(updateAndProcessPosts, 500);
        }
    });
})();