WTR Lab Uncensor

Replaces censored words with their uncensored counterparts on wtr-lab.com for a better reading experience, without breaking site functionality.

// ==UserScript==
// @name         WTR Lab Uncensor
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Replaces censored words with their uncensored counterparts on wtr-lab.com for a better reading experience, without breaking site functionality.
// @author       MasuRii
// @match        https://wtr-lab.com/en/novel/*/*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=wtr-lab.com
// @license      MIT
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION ---
    const CHAPTER_BODY_SELECTOR = '.chapter-body';
    const PROCESSED_MARKER = 'data-uncensor-processed';
    const LOGGING_STORAGE_KEY = 'wtrLabUncensor_loggingEnabled';

    // --- LOGGING SETUP ---
    // Get the saved logging preference, defaulting to false (disabled).
    let loggingEnabled = GM_getValue(LOGGING_STORAGE_KEY, false);

    // Function to toggle the logging preference.
    function toggleLogging() {
        loggingEnabled = !loggingEnabled; // Flip the current state
        GM_setValue(LOGGING_STORAGE_KEY, loggingEnabled); // Save the new state
        alert(`WTR Lab Uncensor logging is now ${loggingEnabled ? 'ENABLED' : 'DISABLED'}.`);
    }

    // Register the command in the Tampermonkey menu.
    GM_registerMenuCommand('Toggle Logging', toggleLogging);


    // --- CENSOR DATABASE ---
    const CENSOR_DATA = [
      { "censored": "B*stard", "uncensored": "Bastard" },
      { "censored": "Bulls**t", "uncensored": "Bullshit" },
      { "censored": "D*mn", "uncensored": "Damn" },
      { "censored": "F**K", "uncensored": "FUCK" },
      { "censored": "F*CK", "uncensored": "FUCK" },
      { "censored": "F*cking", "uncensored": "Fucking" },
      { "censored": "Motherf*cking", "uncensored": "Motherfucking" },
      { "censored": "P*ss", "uncensored": "Piss" },
      { "censored": "Scr*w", "uncensored": "Screw" },
      { "censored": "a**es", "uncensored": "asses" },
      { "censored": "assh*le", "uncensored": "asshole" },
      { "censored": "b**tards", "uncensored": "bastards" },
      { "censored": "b*stard", "uncensored": "bastard" },
      { "censored": "b*stards", "uncensored": "bastards" },
      { "censored": "b*tchy", "uncensored": "bitchy" },
      { "censored": "bulls**t", "uncensored": "bullshit" },
      { "censored": "bullsh*ting", "uncensored": "bullshitting" },
      { "censored": "d*mn", "uncensored": "damn" },
      { "censored": "dumb***es", "uncensored": "dumbasses" },
      { "censored": "dumb*ss", "uncensored": "dumbass" },
      { "censored": "f****r", "uncensored": "fucker" },
      { "censored": "f**ked", "uncensored": "fucked" },
      { "censored": "f**king", "uncensored": "fucking" },
      { "censored": "f*cker", "uncensored": "fucker" },
      { "censored": "idi*t", "uncensored": "idiot" },
      { "censored": "motherf**kers", "uncensored": "motherfuckers" },
      { "censored": "p*ss", "uncensored": "piss" },
      { "censored": "p*ssed", "uncensored": "pissed" },
      { "censored": "r*ped", "uncensored": "raped" },
      { "censored": "s***hole", "uncensored": "shithole" },
      { "censored": "s**t", "uncensored": "shit" },
      { "censored": "sh***y", "uncensored": "shitty" },
      { "censored": "sh*tless", "uncensored": "shitless" },
      { "censored": "sh*tty", "uncensored": "shitty" },
      { "censored": "A**hole", "uncensored": "Asshole" },
      { "censored": "B*****d", "uncensored": "Bastard" },
      { "censored": "B***h", "uncensored": "Bitch" },
      { "censored": "B*Tch", "uncensored": "BiTch" },
      { "censored": "B*tch", "uncensored": "Bitch" },
      { "censored": "Bullsh*t", "uncensored": "Bullshit" },
      { "censored": "D**n", "uncensored": "Damn" },
      { "censored": "F**k", "uncensored": "Fuck" },
      { "censored": "F*ck", "uncensored": "Fuck" },
      { "censored": "Motherf*cker", "uncensored": "Motherfucker" },
      { "censored": "Sh*t", "uncensored": "Shit" },
      { "censored": "a**hole", "uncensored": "asshole" },
      { "censored": "a**holes", "uncensored": "assholes" },
      { "censored": "a*s", "uncensored": "ass" },
      { "censored": "a*ses", "uncensored": "asses" },
      { "censored": "an*s", "uncensored": "anus" },
      { "censored": "an*ses", "uncensored": "anuses" },
      { "censored": "as*es", "uncensored": "asses" },
      { "censored": "as*ses", "uncensored": "asses" },
      { "censored": "b*****d", "uncensored": "bastardd" },
      { "censored": "b***h", "uncensored": "bitch" },
      { "censored": "b**bs", "uncensored": "boobs" },
      { "censored": "b*tch", "uncensored": "bitch" },
      { "censored": "b*tches", "uncensored": "bitches" },
      { "censored": "b*tt", "uncensored": "butt" },
      { "censored": "bl*wjob", "uncensored": "blowjob" },
      { "censored": "bo*bs", "uncensored": "boobs" },
      { "censored": "bullsh*t", "uncensored": "bullshit" },
      { "censored": "bullsh*tting", "uncensored": "bullshitting" },
      { "censored": "c**p", "uncensored": "crap" },
      { "censored": "c*ck", "uncensored": "cock" },
      { "censored": "c*cks", "uncensored": "cocks" },
      { "censored": "d**n", "uncensored": "damn" },
      { "censored": "d*ck", "uncensored": "dick" },
      { "censored": "d*cks", "uncensored": "dicks" },
      { "censored": "f**k", "uncensored": "fuck" },
      { "censored": "f*ck", "uncensored": "fuck" },
      { "censored": "f*cked", "uncensored": "fucked" },
      { "censored": "f*cking", "uncensored": "fucking" },
      { "censored": "h*rd", "uncensored": "hard" },
      { "censored": "m*sturbating", "uncensored": "masturbating" },
      { "censored": "motherf*cker", "uncensored": "motherfucker" },
      { "censored": "motherf*cking", "uncensored": "motherfucking" },
      { "censored": "org*sm", "uncensored": "orgasm" },
      { "censored": "p*bic", "uncensored": "pubic" },
      { "censored": "p*nes", "uncensored": "penes" },
      { "censored": "p*nile", "uncensored": "penile" },
      { "censored": "p*nis", "uncensored": "penes" },
      { "censored": "p*rn", "uncensored": "porn" },
      { "censored": "s*x", "uncensored": "sex" },
      { "censored": "s*xual", "uncensored": "sexual" },
      { "censored": "sh*t", "uncensored": "shit" },
      { "censored": "sl*t", "uncensored": "slut" },
      { "censored": "t*ts", "uncensored": "tits" },
      { "censored": "w*ener", "uncensored": "wiener" },
      { "censored": "wh*rehouses", "uncensored": "whorehouses" },
      { "censored": "Bull***", "uncensored": "Bullshit" },
      { "censored": "Bulls**", "uncensored": "Bullshit" },
      { "censored": "Bullsh*", "uncensored": "Bullshit" },
      { "censored": "Godd*", "uncensored": "Goddamn" },
      { "censored": "Motherf*", "uncensored": "Motherfucker" },
      { "censored": "Scr*", "uncensored": "Screw" },
      { "censored": "Sh*", "uncensored": "Shit" },
      { "censored": "assh*", "uncensored": "asshole" },
      { "censored": "bulls**", "uncensored": "bullshit" },
      { "censored": "bullsh*", "uncensored": "bullshit" },
      { "censored": "dumb***", "uncensored": "dumbass" },
      { "censored": "idi*", "uncensored": "idiot" },
      { "censored": "motherf*", "uncensored": "motherfucker" },
      { "censored": "motherf**", "uncensored": "motherfucker" },
      { "censored": "****r", "uncensored": "fucker" },
      { "censored": "***hole", "uncensored": "asshole" },
      { "censored": "**K", "uncensored": "FUCK" },
      { "censored": "**hole", "uncensored": "asshole" },
      { "censored": "**k", "uncensored": "fuck" },
      { "censored": "**ked", "uncensored": "fucked" },
      { "censored": "**kers", "uncensored": "fuckers" },
      { "censored": "**king", "uncensored": "fucking" },
      { "censored": "**tards", "uncensored": "bastards" },
      { "censored": "*CK", "uncensored": "FUCK" },
      { "censored": "*Ck", "uncensored": "FUCk" },
      { "censored": "*Tch", "uncensored": "BiTch" },
      { "censored": "*ck", "uncensored": "Fuck" },
      { "censored": "*cked", "uncensored": "fucked" },
      { "censored": "*cker", "uncensored": "fucker" },
      { "censored": "*cking", "uncensored": "fucking" },
      { "censored": "*cks", "uncensored": "fucks" },
      { "censored": "*mn", "uncensored": "damn" },
      { "censored": "*nis", "uncensored": "penis" },
      { "censored": "*ped", "uncensored": "raped" },
      { "censored": "*rn", "uncensored": "porn" },
      { "censored": "*ssed", "uncensored": "assed" },
      { "censored": "*stard", "uncensored": "bastard" },
      { "censored": "*stards", "uncensored": "bastards" },
      { "censored": "*tch", "uncensored": "bitch" },
      { "censored": "*tches", "uncensored": "bitches" },
      { "censored": "*tchy", "uncensored": "bitchy" },
      { "censored": "*tless", "uncensored": "shitless" },
      { "censored": "*tty", "uncensored": "shitty" },
      { "censored": "Godd*mn", "uncensored": "Goddamn" },
      { "censored": "f***", "uncensored": "fuck" },
      { "censored": "f******", "uncensored": "fucking" },
      { "censored": "F*Ck", "uncensored": "Fuck" },
      { "censored": "s***", "uncensored": "shit" },
      { "censored": "Bull***t", "uncensored": "Bullshit" },
      { "censored": "bada**", "uncensored": "badass" },
      { "censored": "smarta**", "uncensored": "smartass" },
      { "censored": "Bullsh**", "uncensored": "Bullshit" },
      { "censored": "F * ck", "uncensored": "Fuck" },
      { "censored": "d * ck", "uncensored": "dick" },
      { "censored": "*ss", "uncensored": "ass" },
      { "censored": "*sses", "uncensored": "asses" },
      { "censored": "*sshole", "uncensored": "asshole" },
      { "censored": "*ssholes", "uncensored": "assholes" },
      { "censored": "bada**.", "uncensored": "badass." },
      { "censored": "f***?", "uncensored": "fuck?" },
      { "censored": "s***,", "uncensored": "shit," },
      { "censored": "a**!", "uncensored": "ass!" },
      { "censored": "Bullsh**!", "uncensored": "Bullshit!" },
      { "censored": "bullsh*tting.", "uncensored": "bullshitting." },
      { "censored": "sh*tting", "uncensored": "shitting" },
      { "censored": "bullsh*tting!\"", "uncensored": "bullshitting!\"" },
      { "censored": "sh*ting", "uncensored": "shitting" },
      { "censored": "d * mned", "uncensored": "damned" },
      { "censored": "sh * t!", "uncensored": "shit!" },
      { "censored": "sh * t", "uncensored": "shit" },
      { "censored": "a * s!", "uncensored": "ass!" },
      { "censored": "a * s", "uncensored": "ass" },
      { "censored": "B * tch,\"", "uncensored": "Bitch,\"" },
      { "censored": "B * tch,", "uncensored": "Bitch," },
      { "censored": "B * tch", "uncensored": "Bitch" },
      { "censored": "F * cking", "uncensored": "Fucking" },
      { "censored": "B * stard,", "uncensored": "Bastard," },
      { "censored": "B * stard", "uncensored": "Bastard" },
      { "censored": "a**!\"", "uncensored": "ass!\"" },
      { "censored": "a**.", "uncensored": "ass." },
      { "censored": "B*llshit", "uncensored": "Bullshit" },
      { "censored": "F*cK", "uncensored": "FucK" },
      { "censored": "F*k", "uncensored": "Fuck" },
      { "censored": "F*uk", "uncensored": "Fuck" },
      { "censored": "r*tards", "uncensored": "retards" },
      { "censored": "D*MMIT", "uncensored": "DAMMIT" },
      { "censored": "D*MN", "uncensored": "DAMN" },
      { "censored": "D*MNIT", "uncensored": "DAMNIT" },
      { "censored": "D*mmit", "uncensored": "Damnit" },
      { "censored": "F***ING", "uncensored": "FUCKING" },
      { "censored": "SH*T", "uncensored": "SHIT" },
      { "censored": "b*tiches", "uncensored": "bitches" },
      { "censored": "d*mmit", "uncensored": "damnit" },
      { "censored": "f*king", "uncensored": "fucking" },
      { "censored": "h*ll", "uncensored": "hell" },
      { "censored": "F***!", "uncensored": "Fuck!" },
      { "censored": "B*stards", "uncensored": "Bastards" },
      { "censored": "bad*ss", "uncensored": "badass" },
      { "censored": "bad-*ss", "uncensored": "badass" },
      { "censored": "b****es", "uncensored": "bitches" },
      { "censored": "f*cked-up", "uncensored": "fucked-up" },
      { "censored": "Motherf***er", "uncensored": "Motherfucker" },
      { "censored": "b****,", "uncensored": "bitch," },
      { "censored": "son of a ****.", "uncensored": "son of a bitch." },
      { "censored": "****ing", "uncensored": "fucking" },
      { "censored": "Motherf***", "uncensored": "Motherfucker" },
      { "censored": "b****", "uncensored": "bitch" },
      { "censored": "Fuc*ing", "uncensored": "Fucking" },
      { "censored": "fu*k", "uncensored": "fuck" },
      { "censored": "Fuc*", "uncensored": "Fuck" },
      { "censored": "fu*", "uncensored": "fuck" },
      { "censored": "F*ckChens", "uncensored": "FuckChens" },
      { "censored": "Fu*k", "uncensored": "Fuck" },
      { "censored": "b**ch", "uncensored": "bitch" },
      { "censored": "motherf***er", "uncensored": "motherfucker" },
      { "censored": "as*holes", "uncensored": "assholes" },
      { "censored": "bast*rds", "uncensored": "bastards" },
      { "censored": "fu*ked", "uncensored": "fucked" },
      { "censored": "fu*ked-up", "uncensored": "fucked-up" },
      { "censored": "rottenbast*ards", "uncensored": "rottenbastards" },
      { "censored": "bast*rd", "uncensored": "bastard" },
      { "censored": "dr*g", "uncensored": "drug" },
      { "censored": "dr*gs", "uncensored": "drugs" },
      { "censored": "pr*n", "uncensored": "porn" },
      { "censored": "r*pe", "uncensored": "rape" },
      { "censored": "rap*d", "uncensored": "raped" },
      { "censored": "rap*st", "uncensored": "rapist" },
      { "censored": "r*ping", "uncensored": "raping" },
      { "censored": "F*ckers", "uncensored": "Fuckers" },
      { "censored": "b*lly", "uncensored": "bully" },
      { "censored": "p*rvert", "uncensored": "pervert" },
      { "censored": "s**cide", "uncensored": "suicide" },
      { "censored": "t*rturing", "uncensored": "torturing" }
    ];

    // --- REPLACEMENT ENGINE ---

    // Create a map for quick lookups: { "c*nsored": "uncensored" }
    const replacementMap = CENSOR_DATA.reduce((acc, item) => {
        acc[item.censored] = item.uncensored;
        return acc;
    }, {});

    // Function to escape special characters for use in a RegExp
    function escapeRegex(string) {
        return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    // Create a single, efficient RegExp to find all censored words.
    // We sort by length descending to match longer words first (e.g., "bullsh*t" before "sh*t").
    const censoredWordsPattern = CENSOR_DATA
        .map(item => escapeRegex(item.censored))
        .sort((a, b) => b.length - a.length)
        .join('|');
    const uncensorRegex = new RegExp(censoredWordsPattern, 'g');

    /**
     * Recursively traverses the DOM from a starting node, replacing text in text nodes.
     * This method preserves element nodes and their event listeners.
     * @param {Node} node The node to start traversing from.
     */
    function traverseAndReplace(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            const originalText = node.nodeValue;
            const newText = originalText.replace(uncensorRegex, (matched) => {
                return replacementMap[matched] || matched;
            });
            if (newText !== originalText) {
                node.nodeValue = newText;
            }
            return;
        }

        if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() !== 'script' && node.tagName.toLowerCase() !== 'style') {
            for (const child of node.childNodes) {
                traverseAndReplace(child);
            }
        }
    }

    /**
     * Replaces censored words within a target element using safe DOM traversal.
     * @param {HTMLElement} targetElement The container element to process.
     */
    function applyUncensor(targetElement) {
        if (!targetElement || targetElement.hasAttribute(PROCESSED_MARKER)) {
            return;
        }

        traverseAndReplace(targetElement);
        targetElement.setAttribute(PROCESSED_MARKER, 'true');

        // Conditionally log to the console based on the user's preference.
        if (loggingEnabled) {
            console.log('WTR Lab Uncensor script applied to chapter.', targetElement);
        }
    }

    /**
     * Finds and processes all chapter bodies currently in the DOM.
     */
    function processAllVisibleChapters() {
        const chapterBodies = document.querySelectorAll(CHAPTER_BODY_SELECTOR);
        chapterBodies.forEach(applyUncensor);
    }

    // --- EXECUTION LOGIC ---
    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches(CHAPTER_BODY_SELECTOR) || node.querySelector(CHAPTER_BODY_SELECTOR)) {
                            setTimeout(processAllVisibleChapters, 250);
                            return;
                        }
                    }
                }
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    setTimeout(processAllVisibleChapters, 500);

})();