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.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

Advertisement:

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

Advertisement:

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