Raw Source
adisloom / Rufuker 2ch

// ==UserScript==
// @name         Rufuker 2ch
// @name:ru      Руфакер для Двач 2ch
// @namespace    https://2ch.hk/
// @version      0.54
// @description  Culturally enriches the pidorussian lingamus on 2ch
// @description:ru  Культурна облагарожывает росейскую языку на Дваче 2ch
// @author       Anon
// @copyright    2021-2022, Anon
// @match        *://2ch.hk/*
// @match        *://2ch.pm/*
// @match        *://2ch.life/*
// @license      GPL-3.0-only
// @homepageURL  https://github.com/adisloom/rufuker/blob/main/README.md
// @updateURL    https://raw.githubusercontent.com/adisloom/rufuker/main/rufuker.js
// @downloadURL  https://raw.githubusercontent.com/adisloom/rufuker/main/rufuker.js
// @supportURL   https://github.com/adisloom/rufuker/issues
// @icon         https://www.google.com/s2/favicons?domain=2ch.life
// @defaulticon  https://www.google.com/s2/favicons?domain=2ch.life
// @icon64       https://www.google.com/s2/favicons?domain=2ch.life&sz=64
// @grant        none
// ==/UserScript==

/**********************************************************************************
*
*                           NOTICE
*
* The script requires the browser plugin "Tampermonkey".
* Set "Run only in top frame" to "No" in plugin's settings for the script.
*
* It may also work in other usercript manager plugins:
* GreaseMonkey, ViolentMonkey, FireMonkey.
*
***********************************************************************************/

(function() {
    'use strict';

    if (!document.getElementById('posts-form')) return 1;


     /* 
     *  Converts a string of text according to the rules.
     *  Optionial argument (bool) to disable uppercase 
     *  text conversion. Default - enabled 
     */
    class Rufuker {
        rufuker_replacement_rules = [
            // xx -> yy
            ['ий народ', 'ай на рот'],
            ['осси', 'абсе'],
            ['сски', 'зке'],
            ['еще', 'есчо'],
            ['когда', 'када'],
            ['деньг', 'тэньг'],
            ['денег', 'дынек'],
            ['денеж', 'дыняш'],
            ['ого([ \\s,\\.\\-:])', 'ава$1'],
            ['о([влрт])о', 'а$1а'],
            ['[иы]й([ \\s,\\.\\-:])', 'ы$1'],
            ['([^ \\s,\\.\\-:])ие([ \\s,\\.\\-:])', '$1$1е$2'],
            ['ри', 'ґы'],
            ['ре', 'ґе'],
            ['ря', 'ґя'],
            ['рь', 'гх'],
            ['ти', 'це'],
            ['те', 'ця'],
            ['тя', 'ца'],
            ['ди', 'дэ'],
            ['де', 'ды'],
            ['ши', 'шэ'],
            ['ше', 'ша'],
            ['жи', 'жэ'],
            ['же', 'жа'],
            ['си', 'се'],
            ['ио', 'её'],
            ['иа', 'ея'],
            ['иу', 'ею'],
            ['ие', 'ее'],
            ['ться', 'ца'],
            ['тся', 'тсо'],
            ['дь', 'ц'],
            ['ть', 'ц'],
            ['ли', 'ле'],
            ['че', 'це'],
            ['([жшч])ь', '$1'],
            ['щ', 'ш'],
            ['([^ \\s,\\.\\-:])и([ \\s,\\.\\-:])', '$1е$2'],
            ['ъе', 'йэ'],
            ['ъё', 'йо'],
            ['ъю', 'йу'],
            ['ъя', 'йа'],
            [' и([ \\s,\\.\\-:])', ' ды$1'],
            ['и', 'ы'] ];
        addUpperCase = true;
        aReplacement = [];

        constructor(uppercaseOption){
            if (typeof uppercaseOption === 'boolean') this.addUpperCase = uppercaseOption;
            this.compileRegex();
            this.rufukString = this.covertText.bind(this);
        }
        compileRegex() {
            this.aReplacement = this.rufuker_replacement_rules.map( c => ({ sRegex: new RegExp(c[0],'g'), sSubst: c[1] }) );
            if (!this.addUpperCase) return;
            var aUpcasedReplacement = this.rufuker_replacement_rules.map( function(c) {
                let rgx = c[0];
                let upRgx = '';
                for (let i = 0; i < rgx.length; i++){
                    let res = rgx[i].match(/[а-я]/);
                    if (res) upRgx = upRgx + res[0].toString().toUpperCase();
                    else upRgx = upRgx + rgx[i];
                }
                let substitute = c[1].at(0).toUpperCase() + c[1].slice(1); //capitalize the first letter
                return { sRegex: new RegExp(upRgx,'g'), sSubst: substitute };
            });
            this.aReplacement = this.aReplacement.concat(aUpcasedReplacement);
        }
        covertText(txt) {
            var flagAllCaps = this.detectAllCaps(txt);
            for (let r of this.aReplacement) {
                let substitute = r.sSubst.toString();
                if (flagAllCaps) substitute = substitute.toUpperCase();
                txt = txt.toString().replace(r.sRegex, substitute);
            }
            return txt;
        }
        detectAllCaps(str){
           let part = str.slice(-200);
           let res;
           if (res = part.match(/[А-Я]/g))
               if (res.length / part.length > 0.30) return true;
           else return false;
        }
    } //class


    /* 
     *  Can traverse 2ch and replace text in all the posts
     *  including popups and dynamically loaded messages.
     *  Argument - a function for text conversion.
     */
    class TextReplacer2ch {
        workingElement;
        #flagObserveNewPosts = true;
        #flagObserveScrollAndPopup = true;
        delayPopup = 100; //ms

        constructor (txtConverter) {
            this.txtConverter = txtConverter;

            if (this.workingElement = document.getElementById('posts-form')); else return 1;
            const aThreads = this.workingElement.querySelectorAll('div.thread');
            if (aThreads.length === 0) return 2; //wrong page
            this.replaceAllDecendantArticles(this.workingElement);
            if (this.#flagObserveScrollAndPopup) {
                const board_observer = new MutationObserver(this.replaceScrollAndPopup.bind(this));
                board_observer.observe(this.workingElement, {childList:true});
            }
            //single thread page needs one more observer for added posts
            if (this.#flagObserveNewPosts && aThreads.length === 1) {
                const thread_observer = new MutationObserver(this.replaceNewPosts.bind(this));
                thread_observer.observe(aThreads[0], {childList:true});
            }
        } //constructor

        replaceAllDecendantArticles(pe) {
            const articles = pe.querySelectorAll('article.post__message');
            articles.forEach(a => a.innerHTML = this.txtConverter(a.innerHTML));
        }

        replaceArticleByNum(idNum) {
            const id_article = 'm' + idNum;
            const el = document.getElementById(id_article);
            el.innerHTML = this.txtConverter(el.innerHTML);
        }

        replaceScrollAndPopup(mutationsList, observer) {
            let postClasses = ['post', 'post_type_reply', 'post_preview'];
            setTimeout( () => {
                for(const mutation of mutationsList) {
                    if (mutation.type !== 'childList' || mutation.addedNodes.length === 0) continue;
                    mutation.addedNodes.forEach( n => {
                        if (postClasses.every(name => n.classList.contains(name))) {
                            for (const idx in n.children) {
                                if (n.children[idx].nodeName === 'ARTICLE') {
                                    n.children[idx].innerHTML = this.txtConverter(n.children[idx].innerHTML);
                                }
                            }
                        } else if (n.className === 'thread') {
                            const thread = document.getElementById(n.id);
                            this.replaceAllDecendantArticles(thread);
                        }
                    });
                }
            }, this.delayPopup);
        }

        replaceNewPosts (mutationsList, observer) {
            for(const mutation of mutationsList) {
                if (mutation.type !== 'childList' || mutation.addedNodes.length === 0) continue;
                for (const n of mutation.addedNodes[0].children) {
                    if (n.nodeName !== 'DIV' || ! n.hasAttribute('id')) continue;
                    let idNum = n.id.match( /\d{3,}/g).pop(); //last 3+ digits
                    this.replaceArticleByNum(idNum);
                }
            } //for
        }
    } //class

    var txtConverter = new Rufuker();
    new TextReplacer2ch(txtConverter.rufukString);

})();