sukhoi191 / Majoryzator

// Merged from __tampermonkey__\HeaderStart.tampermonkey
// ==UserScript==

// Merged from __tampermonkey__\HeaderName.tampermonkey
// @name         Majoryzator

// Merged from __tampermonkey__\HeaderEnd.tampermonkey
// @namespace    https://gitlab.com/sukhoi191/majoryzator
// @version      1.4.6-beta
// @license      MIT
// @description  Spraw, aby dowolna strona internetowa stała się wypowiedzią Majora Suchodolskiego.
// @author       sukhoi191
// @match        *://*/*
// @updateURL    https://openuserjs.org/meta/sukhoi191/Majoryzator.meta.js
// @downloadURL  https://openuserjs.org/install/sukhoi191/Majoryzator.user.js
// @copyright    2019, sukhoi191 (https://openuserjs.org/users/sukhoi191)
// @resource     css https://gitlab.com/sukhoi191/majoryzator/raw/master/Majoryzator.css
// @resource     knownAbbreviationsJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/KnownAbbreviations.json
// @resource     conjunctionAfterBlacklistJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/ConjunctionAfterBlacklist.json
// @resource     conjunctionBeforeBlacklistJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/ConjunctionBeforeBlacklist.json
// @resource     tentativeEndsJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/TentativeEnds.json
// @resource     kononSentencesJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Konon_Sentences.json
// @resource     kononWordsJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Konon_Words.json
// @resource     kononFarewellsJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Konon_Farewells.json
// @resource     kononWelcomesJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Konon_Welcomes.json
// @resource     kononTranslationsJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Konon_Translations.json
// @resource     majorSentencesJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Major_Sentences.json
// @resource     majorWordsJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Major_Words.json
// @resource     majorFarewellsJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Major_Farewells.json
// @resource     majorWelcomesJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Major_Welcomes.json
// @resource     majorTranslationsJson https://gitlab.com/sukhoi191/majoryzator/raw/master/__data__/Major_Translations.json
// @require      https://raw.githubusercontent.com/lodash/lodash/4.17.15-npm/lodash.js
// @grant        GM_getResourceText
// @run-at       document-end
// ==/UserScript==
(function() {

// Merged from __modules__\UseStrict.js
'use strict';

// Merged from __modules__\TranslationType.js
/**
 * Enumeracja dla typu tłumaczenia.
 */
const TranslationType = {
    ANY: 1,
    BEGIN_ONLY: 2,
    INSIDE_ONLY: 3,
    END_ONLY: 4
};

// Merged from __modules__\TranslationCapitalizationType.js
/**
 * Enumeracja dla typu indeksu tłumaczenia.
 */
const TranslationCapitalizationType = {
    UPPERCASE: 1,
    LOWERCASE: 2,
    TITLECASE: 3,
    UNKNOWN: 4,
    NOT_FOUND: 5
};

// Merged from __modules__\SplittedTextPart.js
class SplittedTextPart {
    constructor(text, splitter) {
        this.text = text;
        this.splitter = splitter;
    }

    getText() {
        return this.text;
    }

    setText(text) {
        this.text = text;
    }

    getSplitter() {
        return this.splitter;
    }

    setSplitter(splitter) {
        this.splitter = splitter;
    }

    removeSplitter() {
        this.setSplitter(null);
    }

    getJoined() {
        return this.getText() + (this.getSplitter() != null ? this.getSplitter() : "");
    }

    toString() {
        return 'text: "' + this.getText() + '" | splitter: "' + this.getSplitter() + '"';
    }
}

// Merged from __modules__\Placeholder.js
class Placeholder {
    constructor(content, func) {
        this.content = content;
        this.func = func;
    }

    swap(textArray) {
        for (let i = 0; i < textArray.length; i++) {
            textArray[i] = textArray[i].replace(this.content, this.func());
        }

        return textArray;
    }
}

// Merged from __modules__\Person.js

/**
 * Reprezentuje osobę.
 */
class Person {
    constructor(name) {
        this.name = name;

        this.welcomeSwaps = [
            new Placeholder("{PrzywitaniePoraDnia}", () => this.getCorrectWelcomeForTime(new Date())),
            new Placeholder("{DzienTygodnia}", () => this.getDayOfWeek(new Date())),
            new Placeholder("{Data}", () => this.getDateAsString(new Date())),
            new Placeholder("{Godzina}", () => this.getTimeAsString(new Date()))
        ];
    }

    /**
     * Imię i nazwisko osoby.
     */
    getName() {
        return this.name;
    }

    /**
     * Słowa ("spójniki") do wstawienia.
     */
    getWords() {
        throw new Error('Method is not implemented.');
    }

    /**
     * Pełne zdania do wstawienia.
     */
    getFullSentences() {
        throw new Error('Method is not implemented.');
    }

    /**
     * Powitania.
     */
    getWelcomes() {
        throw new Error('Method is not implemented.');
    }

    /**
     * Pożegnania.
     */
    getFarewells() {
        throw new Error('Method is not implemented.');
    }

    /**
     * Prawdopodobieństwo, że po dotarciu do miejsca, w którym można dodać nowe słowo,
     * zostanie ono wstawione.
     * 0 oznacza 0% szansy, 100 oznacza 100% szansy.
     */
    getProbabilityOfAddingNewWord() {
        throw new Error('Method is not implemented.');
    }

    /**
     * Prawdopodobieństwo, że po dotarciu do miejsca, w którym można dodać nowe zdanie,
     * zostanie ono wstawione.
     * 0 oznacza 0% szansy, 100 oznacza 100% szansy.
     */
    getProbabilityOfAddingNewSentence() {
        throw new Error('Method is not implemented.');
    }

    /**
     * Lista tłumaczeń.
     * Każdy element listy składa się z kilku informacji:
     *
     * [słowo do znalezienia, funkcja zamieniająca słowo]
     *
     * Funkcja zamieniająca słowo przyjmuje jako parametr tekst do przetłumaczenia
     * oraz instancję translatora.
     *
     * Standardową implementacją funkcji jest zamiana poszczególnych liter w słowie.
     * Dla przykładu, jeśli element poniższej listy będzie wyglądał tak:
     *
     * ["Cześć", function(str, instane) {
     *   return instance.replaceWithCasing(str, [["ś", "si"], ["ć", "ci"]])
     * }]
     *
     * W tej sytuacji każde wystąpienie litery "ś" zostanie zmienione na "si", a każde
     * wystąpienie litery "ć" zostanie zmienione na "ci", co da nam jako wynik "czesici"
     * (tak, to cudny przykład).
     *
     * Użycie metody replaceWithCasing() pozwala na uporanie się z możliwością otrzymania
     * stringa z różnymi wielkościami liter. Dzięki temu przetłumaczone zostaną wszelkie
     * możliwe opcje:
     * - Cześć
     * - cześć
     * - cZeŚĆ
     * - CZEŚĆ
     * - itd.
     */
    getTranslations() {
        throw new Error('Method is not implemented.');
    }

    getCorrectWelcomeForTime(date) {
        if (date.getHours() >= 19 || (date.getHours() >= 0 && date.getHours() <= 4)) {
            return "dobry wieczór";
        } else {
            return "dzień dobry";
        }
    }

    swapPlaceholders(textArray) {
        for (let i = 0; i < this.welcomeSwaps.length; i++) {
            textArray = this.welcomeSwaps[i].swap(textArray);
        }

        return textArray;
    }

    getTimeAsString(date) {
        // https://stackoverflow.com/a/12230363/3470545
        return ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
    }

    getDateAsString(date) {
        let month = '';
        switch (date.getMonth()) {
            case 0:
                month = "stycznia";
                break;
            case 1:
                month = "lutego";
                break;
            case 2:
                month = "marca";
                break;
            case 3:
                month = "kwietnia";
                break;
            case 4:
                month = "maja";
                break;
            case 5:
                month = "czerwca";
                break;
            case 6:
                month = "lipca";
                break;
            case 7:
                month = "sierpnia";
                break;
            case 8:
                month = "września";
                break;
            case 9:
                month = "października";
                break;
            case 10:
                month = "listopada";
                break;
            case 11:
                month = "grudnia";
                break;
        }
        return date.getDate() + " " + month;
    }

    getDayOfWeek(date) {
        switch (date.getDay()) {
            case 0:
                return "niedzielę";
            case 1:
                return "poniedziałek";
            case 2:
                return "wtorek";
            case 3:
                return "środę";
            case 4:
                return "czwartek";
            case 5:
                return "piątek";
            case 6:
                return "sobotę";
            default:
                return "chuj wie co";
        }
    }
}

// Merged from __modules__\PersonMajor.js

class PersonMajor extends Person {
    constructor() {
        super('Major Wojciech Suchodoski');
    }

    getWords() {
        return this.swapPlaceholders(JSON.parse(GM_getResourceText("majorWordsJson")).words);
    }

    getFullSentences() {
        return this.swapPlaceholders(JSON.parse(GM_getResourceText("majorSentencesJson")).sentences);
    }

    getWelcomes() {
        return this.swapPlaceholders(JSON.parse(GM_getResourceText("majorWelcomesJson")).welcomes);
    }

    getFarewells() {
        return this.swapPlaceholders(JSON.parse(GM_getResourceText("majorFarewellsJson")).farewells);
    }

    getTranslations() {
        return JSON.parse(GM_getResourceText("majorTranslationsJson")).translations;
    }

    getProbabilityOfAddingNewWord() {
        return 35;
    }

    getProbabilityOfAddingNewSentence() {
        return 30;
    }
}

// Merged from __modules__\PersonKonon.js

class PersonKonon extends Person {
    constructor() {
        super('Krzysztof Kononowicz');
    }

    getWords() {
        return this.swapPlaceholders(JSON.parse(GM_getResourceText("kononWordsJson")).words);
    }

    getFullSentences() {
        return this.swapPlaceholders(JSON.parse(GM_getResourceText("kononSentencesJson")).sentences);
    }

    getWelcomes() {
        return this.swapPlaceholders(JSON.parse(GM_getResourceText("kononWelcomesJson")).welcomes);
    }

    getFarewells() {
        return this.swapPlaceholders(JSON.parse(GM_getResourceText("kononFarewellsJson")).farewells);
    }

    getTranslations() {
        return JSON.parse(GM_getResourceText("kononTranslationsJson")).translations;
    }

    getProbabilityOfAddingNewWord() {
        return 1;
    }

    getProbabilityOfAddingNewSentence() {
        return 40;
    }

    getCorrectWelcomeForTime(date) {
        let welcome = super.getCorrectWelcomeForTime(date);
        welcome = welcome[0].toUpperCase() + welcome.substr(1);
        return welcome + " państwu";
    }
}

// Merged from __modules__\Configuration.Functional.js
/**
 * Tłumaczymy?
 */
const TRANSLATE = true;

/**
 * Wstawiamy pełne zdania?
 */
const ADD_SENTENCES = true;

/**
 * Wstawiamy spójniki?
 */
const ADD_WORDS = true;

/**
 * Wstawiamy powitanie?
 */
const ADD_WELCOME = true;

/**
 * Wstawiamy pożegnanie?
 */
const ADD_FAREWELL = true;

/**
 * Fragment wyrażenia regularnego dla wyszukiwania liter.
 */
const REGEX_LETTERS = "a-zA-ZąćęłńóśźżĄĆĘŁŃÓŚŹŻ";

// Merged from __modules__\Translator.js
/**
 * Translator.
 */
class Translator {
    constructor(translations) {
        this.translations = translations;
    }

    /**
     * Podmienia podane stringi w tekście, dodatkowo podmieniając wersje lowercase
     * i uppercase podanych stringów.
     * @param {string} str String do zmiany.
     * @param {string} originalWord Oryginalne słowo do zamiany.
     * @param {string} findReplace Dane zmiany.
     * @param {TranslationType} translationType Określa typ tłumaczenia.
     * @param {boolean} matchWholeWord Czy wykonać tłumaczenie tylko w przypadku znalezienia całego słowa?
     */
    replaceWithCasing(str, originalWord, findReplace, translationType = TranslationType.ANY,
        matchWholeWord = false) {
        if (matchWholeWord && str.toLowerCase() != originalWord.toLowerCase()) {
            return str;
        }

        let indexSearchResult = this.findIndex(str, originalWord);
        let index = indexSearchResult[0];

        if (index == -1) {
            return str;
        }

        if (translationType == TranslationType.BEGIN_ONLY && index > 0) {
            return str;
        }

        if (translationType == TranslationType.INSIDE_ONLY && (index == 0 || index + originalWord.length == str.length)) {
            return str;
        }

        if (translationType == TranslationType.END_ONLY && index + originalWord.length != str.length) {
            return str;
        }

        let swapped = originalWord;
        let find = findReplace[0];
        let replace = findReplace[1];

        swapped = this.replaceExact(swapped, find, replace);
        swapped = this.replaceExact(swapped, find.toLowerCase(), replace.toLowerCase());
        swapped = this.replaceExact(swapped, find.toUpperCase(), replace.toUpperCase());

        if (indexSearchResult[1] == TranslationCapitalizationType.LOWERCASE) {
            swapped = swapped.toLowerCase();
        } else if (indexSearchResult[1] == TranslationCapitalizationType.UPPERCASE) {
            swapped = swapped.toUpperCase();
        } else if (indexSearchResult[1] == TranslationCapitalizationType.TITLECASE) {
            swapped = this.capitalizeFirstLetter(swapped);
        }

        return str.substring(0, index) + swapped + str.substring(index + originalWord.length);
    }

    /**
     * Wyszukuje indeks wystąpienia podanego tekstu w innym tekście.
     * Sprawdza różne możliwości wielkości liter.
     * @param {string} str Tekst do przeszukania.
     * @param {string} originalWord Tekst do znalezienia.
     * @returns Lista elementów - pierwszy to indeks, drugi określa sposób,
     * w jaki znaleziono tekst: N = bez zmian, L = lowercase, U = uppercase,
     * ? = nie znaleziono.
     */
    findIndex(str, originalWord) {
        let index = str.indexOf(originalWord);

        if (index != -1) {
            return [index, TranslationCapitalizationType.NOT_FOUND];
        }

        index = str.indexOf(originalWord.toLowerCase());

        if (index != -1) {
            return [index, TranslationCapitalizationType.LOWERCASE];
        }

        index = str.indexOf(originalWord.toUpperCase());

        if (index != -1) {
            return [index, TranslationCapitalizationType.UPPERCASE];
        }

        index = str.indexOf(this.capitalizeFirstLetter(originalWord));

        if (index != -1) {
            return [index, TranslationCapitalizationType.TITLECASE];
        }

        return [index, TranslationCapitalizationType.UNKNOWN];
    }

    /**
     * Zmienia pierwszą literę tekstu na wielką.
     * @param {string} str Tekst do zmiany.
     */
    capitalizeFirstLetter(str) {
        return str[0].toLocaleUpperCase() + str.substring(1);
    }

    /**
     * Dokonuje dokładną zamianę, bez rozróżnienia na wielkość liter.
     * @param {string} str String do zmiany.
     * @param {string} find String do znalezienia.
     * @param {string} replaceWord Tekst do wstawienia.
     */
    replaceExact(str, find, replaceWord) {
        return str.replace(new RegExp(find, "g"), replaceWord);
    }

    /**
     * Wykonuje tłumaczenie.
     * @param {array} textArray Tablica tekstu do przetłumaczenia.
     */
    translate(textArray) {
        textArray.forEach(function (text) {
            this.translations.forEach(function (translation) {
                let sourceWord = translation["sourceWord"];

                if (!new RegExp(translation["sourceWord"], "i").test(text.getText())) {
                    return;
                }

                let translatedWord = sourceWord;

                translation["operations"].forEach(function (operation) {
                    if (operation["method"] == "replaceWithCasing") {
                        let stringToReplace = operation["stringToReplace"];
                        let replacementString = operation["replacementString"];
                        let type = TranslationType.ANY;
                        let matchWholeWord = false;

                        if ("type" in operation) {
                            switch (operation["type"]) {
                                case "any":
                                    type = TranslationType.ANY;
                                    break;
                                case "begin_only":
                                    type = TranslationType.BEGIN_ONLY;
                                    break;
                                case "inside_only":
                                    type = TranslationType.INSIDE_ONLY;
                                    break;
                                case "end_only":
                                    type = TranslationType.END_ONLY;
                                    break;
                            }
                        }

                        if ("matchWholeWord" in operation) {
                            matchWholeWord = operation["matchWholeWord"];
                        }

                        text.setText(this.replaceWithCasing(text.getText(), translatedWord, [stringToReplace, replacementString], type, matchWholeWord));
                        translatedWord = text.getText();
                    }
                }.bind(this));
            }.bind(this));
        }.bind(this));

        return textArray;
    }
}

// Merged from __modules__\Generator.js


/**
 * Generator majorowych wstawek w tekście.
 */
class Generator {

    /**
     * Tworzy nową instancję.
     * @param {Person} person Osoba.
     * @param {string} knownAbbreviationsJson Znane skróty.
     * @param {string} tentativeEndsJson Niepewne końcówki.
     */
    constructor(person, knownAbbreviationsJson, tentativeEndsJson) {
        this.person = person;
        this.wordsArray = person.getWords();
        this.sentencesArray = person.getFullSentences();
        this.textSplitters = [' ', '\n'];
        this.knownAbbreviations = JSON.parse(knownAbbreviationsJson).abbreviations;
        this.tentativeEnds = JSON.parse(tentativeEndsJson).ends;

        this.createWordsQueue();
        this.createSentencesQueue();
    }

    /**
     * Tworzy kolejkę spójników.
     */
    createWordsQueue() {
        this.wordsQueue = this.wordsArray.slice(0);
        this.shuffleArray(this.wordsQueue);
    }

    /**
     * Tworzy kolejkę zdań.
     */
    createSentencesQueue() {
        this.sentencesQueue = this.sentencesArray.slice(0);
        this.shuffleArray(this.sentencesQueue);
    }

    /**
     * Miesza elementy w tablicy (generuje losową permutację).
     * Kod z https://stackoverflow.com/a/12646864/3470545 (a co, ja też jestem
     * czasem leniwy).
     * @param {array} array Tablica do wymieszania.
     */
    shuffleArray(array) {
        for (let i = array.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
    }

    /**
     * Losowo wstawia słowa / zdania do tekstu.
     * @param {string} text Tekst do zmiany.
     * @param {boolean} addWelcomeNow Czy wstawiamy powitanie na początku podanego tekstu?
     * @param {boolean} addFarewellNow Czy wstawiamy pożegnanie na końcu podanego tekstu?
     * @param {string} conjunctionBeforeBlacklist Czarna lista słów, przed którymi nie można wstawić spójnika.
     * @param {string} conjunctionAfterBlacklistJson Czarna lista słów, po których nie można wstawić spójnika.
     */
    generate(text, addWelcomeNow, addFarewellNow, conjunctionBeforeBlacklistJson, conjunctionAfterBlacklistJson) {
        let splittedText = this.splitText(text, this.textSplitters);

        let conjunctionBeforeBlacklist = JSON.parse(conjunctionBeforeBlacklistJson).blacklist;
        let conjunctionAfterBlacklist = JSON.parse(conjunctionAfterBlacklistJson).blacklist;

        this.joinNonSplittableParts(splittedText);
        this.majorizeTextArray(splittedText, conjunctionBeforeBlacklist, conjunctionAfterBlacklist);

        if (addWelcomeNow && ADD_WELCOME) {
            this.addWelcome(splittedText, this.person.getWelcomes());
        }

        if (addFarewellNow && ADD_FAREWELL) {
            this.addFarewell(splittedText, this.person.getFarewells());
        }

        if (TRANSLATE) {
            this.translate(splittedText);
        }

        return this.joinStringArray(splittedText);
    }

    /**
     * Tłumaczy podaną tablicę tekstu.
     * @param {array} textArray Tablica ze słowami do przetłumaczenia.
     */
    translate(textArray) {
        textArray = new Translator(this.person.getTranslations()).translate(textArray);
    }

    /**
     * Dzieli tekst w miejcach wystąpienia podanych znaków.
     * @param {string} text Tekst do podzielenia.
     * @param {array} splittersArray Tablica z tekstami dzielącymi.
     */
    splitText(text, splittersArray) {
        if (text == null) {
            return [];
        }

        let result = [new SplittedTextPart(text, '')];

        splittersArray.forEach(function (splitter) {
            result = this.splitTextWith(result, splitter);
        }.bind(this));

        return result;
    }

    /**
     * Dzieli elementy na tablicy tekstowej w miejscach wystąpienia podanego tekstu.
     * @param {string} textArray Tablica z tekstem.
     * @param {string} splitter Tekst dzielący.
     */
    splitTextWith(textArray, splitter) {
        let newArray = [];

        textArray.forEach(splittedTextPart => {
            let splitted = splittedTextPart.getText().split(splitter);

            if (splitted.length <= 1) {
                newArray.push(splittedTextPart);
            } else {
                splitted.forEach(element => newArray.push(new SplittedTextPart(element, splitter)));
                newArray[newArray.length - 1].setSplitter(splittedTextPart.getSplitter());
            }
        });

        if (newArray.length > 0) {
            newArray[newArray.length - 1].removeSplitter();
        }

        return newArray;
    }

    /**
     * Dokonuje majoryzacji tekstu.
     * @param {array} textArray Tablica ze słowami do majoryzacji.
     * @param {array} conjunctionBeforeBlacklist Czarna lista słów, przed którymi nie można wstawić spójnika.
     * @param {array} conjunctionAfterBlacklist Czarna lista słów, po których nie można wstawić spójnika.
     */
    majorizeTextArray(textArray, conjunctionBeforeBlacklist, conjunctionAfterBlacklist) {
        for (let i = 1; i <= textArray.length; i++) {
            if (i < textArray.length &&
                (this.isWhitespaceOnly(textArray[i].getText()) ||
                    this.isEndingWithTentativeText(textArray[i].getText(), this.tentativeEnds))) {
                continue;
            }

            let addNewSentence = false;
            let addNewWord = false;

            addNewSentence = this.shouldAddNewSentence(this.person.getProbabilityOfAddingNewSentence()) &&
                this.isEndOfSentence(textArray[i - 1].getText());

            if (!addNewSentence) {
                addNewWord = this.shouldAddNewWord(this.person.getProbabilityOfAddingNewWord());
            }

            if (ADD_SENTENCES && addNewSentence) {
                if (this.sentencesQueue.length == 0) {
                    this.createSentencesQueue();
                }

                let addedWordsCount = this.addNewSentence(textArray, i, this.sentencesQueue);
                i += addedWordsCount;
            } else if (ADD_WORDS && addNewWord) {
                if (this.wordsQueue.length == 0) {
                    this.createWordsQueue();
                }

                if (!this.canAddWordBetweenWords(textArray, i, conjunctionAfterBlacklist, conjunctionBeforeBlacklist)) {
                    continue;
                }

                let addedWordsCount = this.addNewWord(textArray, i, this.wordsQueue);
                this.addCommaBeforePositionIfNecessary(textArray, i);
                i += addedWordsCount;
            }
        }
    }

    /**
     * Sprawdza, czy w podanym miejscu można wstawić nowe słowo, biorąc pod uwagę
     * słowa znajdujące się bezpośrednio po i przed obecnie wstawianym.
     * @param {array} textArray Tablica ze słowami do majoryzacji.
     * @param {number} position Pozycja nowego słowa.
     * @param {array} conjunctionBeforeBlacklist Czarna lista słów, przed którymi nie można wstawić spójnika.
     * @param {array} conjunctionAfterBlacklist Czarna lista słów, po których nie można wstawić spójnika.
     */
    canAddWordBetweenWords(textArray, position, conjunctionAfterBlacklist, conjunctionBeforeBlacklist) {
        if (position == 0 || position >= textArray.length) {
            return false;
        }

        return this.canAddConjunctionAfter(textArray[position - 1].getText(), conjunctionAfterBlacklist) &&
            this.canAddConjunctionBefore(textArray[position].getText(), conjunctionBeforeBlacklist);
    }

    /**
     * Sprawdza, czy podany tekst składa się tylko z białych znaków.
     * @param {string} str Tekst do sprawdzenia.
     */
    isWhitespaceOnly(str) {
        return str == "" || /^\s*$/.test(str);
    }

    /**
     * Biorąc pod uwagę prawdopodobieństwo dodania nowego słowa, generuje liczbę informującą o tym,
     * czy zostanie ono dodane.
     * @param {number} probabilityOfNewWord Prawdopodobieństwo dodania nowego słowa.
     */
    shouldAddNewWord(probabilityOfNewWord) {
        return _.random(0, 100, false) < probabilityOfNewWord;
    }

    /**
     * Biorąc pod uwagę prawdopodobieństwo dodania nowego zdania, generuje liczbę informującą o tym,
     * czy zostanie ono dodane.
     * @param {number} probabilityOfNewSentence Prawdopodobieństwo dodania nowego zdania.
     */
    shouldAddNewSentence(probabilityOfNewSentence) {
        return _.random(0, 100, false) < probabilityOfNewSentence;
    }

    /**
     * Biorąc pod uwagę prawdopodobieństwo dodania nowej czynności, generuje liczbę informującą o tym,
     * czy zostanie ona dodana.
     * @param {number} probabilityOfNewAction Prawdopodobieństwo dodania nowej czynności.
     */
    shouldAddNewAction(probabilityOfNewAction) {
        return _.random(0, 100, false) < probabilityOfNewAction;
    }

    /**
     * Dodaje losowe powitanie.
     * @param {array} textArray Tablica ze słowami.
     * @param {array} welcomes Tablica z dostępnymi powitaniami.
     */
    addWelcome(textArray, welcomes) {
        let splittedWelcome = this.splitText(_.sample(welcomes), this.textSplitters);
        splittedWelcome[splittedWelcome.length - 1].setSplitter(' ');
        Array.prototype.unshift.apply(textArray, [].concat(splittedWelcome));
    }

    /**
     * Dodaje losowe pożegnanie.
     * @param {array} textArray Tablica ze słowami.
     * @param {array} farewells Tablica z dostępnymi pożegnaniami.
     */
    addFarewell(textArray, farewells) {
        textArray[textArray.length - 1].setSplitter(' ');

        let splittedFarewell = this.splitText(_.sample(farewells), this.textSplitters);
        splittedFarewell[splittedFarewell.length - 1].setSplitter(' ');
        Array.prototype.push.apply(textArray, [].concat(splittedFarewell));
    }

    /**
     * Wstawia losowe zdanie na podanej pozycji.
     * @param {array} textArray Tablica ze słowami.
     * @param {number} position Pozycja do wstawienia nowego zdania.
     * @param {array} sentencesQueue Kolejka z dostępnymi zdaniami.
     * @returns Ilość słów w dodanej tablicy.
     */
    addNewSentence(textArray, position, sentencesQueue) {
        if (position == textArray.length) {
            textArray[position - 1].setSplitter(' ');
        }

        let splittedSentence = this.splitText(sentencesQueue.shift(), this.textSplitters);
        splittedSentence[splittedSentence.length - 1].setSplitter(' ');
        Array.prototype.splice.apply(textArray, [position, 0].concat(splittedSentence));
        return splittedSentence.length;
    }

    /**
     * Wstawia nowe losowe słowo na podanej pozycji i dopisuje przecinek.
     * @param {array} textArray Tablica ze słowami.
     * @param {number} position Pozycja do wstawienia nowego zdania.
     * @param {array} wordsQueue Kolejka z dostępnymi słowami.
     * @returns Ilość słów w dodanej tablicy.
     */
    addNewWord(textArray, position, wordsQueue) {
        let splittedWord = this.splitText(wordsQueue.shift() + ",", this.textSplitters);
        splittedWord[splittedWord.length - 1].setSplitter(' ');
        Array.prototype.splice.apply(textArray, [position, 0].concat(splittedWord));
        return splittedWord.length;
    }

    /**
     * Sprawdza, czy można wstawić spójnik przed podanym tekstem.
     * @param {string} text Tekst do sprawdzenia.
     * @param {array} conjunctionBeforeBlacklist Czarna lista słów, przed którymi nie można wstawić spójnika.
     */
    canAddConjunctionBefore(text, conjunctionBeforeBlacklist = []) {
        return !/[\[|\(|\{|\-|–].*/.test(text) &&
            !this.isConjunctionOnBlacklist(text, conjunctionBeforeBlacklist);
    }

    /**
     * Sprawdza, czy można wstawić spójnik za podanym tekstem.
     * @param {string} text Tekst do sprawdzenia.
     * @param {array} conjunctionBeforeBlacklist Czarna lista słów, po których nie można wstawić spójnika.
     */
    canAddConjunctionAfter(text, conjunctionAfterBlacklist = []) {
        return !this.isWhitespaceOnly(text) &&
            !this.isEndOfSentence(text) &&
            !/.*[;|:|\-|–|\)\]\}]$/.test(text) &&
            !this.isConjunctionOnBlacklist(text, conjunctionAfterBlacklist) &&
            !this.isEndingWithTentativeText(text, this.tentativeEnds);
    }

    /**
     * Sprawdza, czy podany tekst jest na czarnej liście słów.
     * @param {string} text Tekst do sprawdzenia.
     */
    isConjunctionOnBlacklist(text, blacklist) {
        for (let i = 0; i < blacklist.length; i++) {
            if (new RegExp('^[ "\'\\(\\[\\{]*(' + blacklist[i].toLowerCase() + ')$').test(text.toLowerCase())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Dodaje przecinek przed podaną pozycję, o ile jest to potrzebne.
     * @param {array} textArray Tablica ze słowami.
     * @param {number} position Pozycja do sprawdzenia.
     */
    addCommaBeforePositionIfNecessary(textArray, position) {
        if (position > 0 && position <= textArray.length && !textArray[position - 1].getText().endsWith(",")) {
            textArray[position - 1].setText(textArray[position - 1].getText() + ",");
        }
    }

    addSpaceIfNecessary(textArray, position) {
        if (position > 0 && position <= textArray.length && textArray[position - 1].getSplitter() == null) {
            textArray[position - 1].setText(textArray[position - 1].getText() + " ");
        }
    }

    /**
     * Łączy te części tekstu, które nie powinny być podzielone.
     * @param {array} textArray Tablica z tekstem.
     */
    joinNonSplittableParts(textArray) {
        let nonSplitabbleParts = [
            "-", "–", ""
        ];

        for (let i = 0; i < textArray.length; i++) {
            let startIndex = -1;

            if (nonSplitabbleParts.includes(textArray[i].getText())) {
                startIndex = i;
            }

            while (i < textArray.length && nonSplitabbleParts.includes(textArray[i].getText())) {
                // Znajdujemy następny dozwolony znak.
                i++;
            }

            if (i == textArray.length) {
                i--;
            }

            if (startIndex != -1) {
                let slice = textArray.slice(startIndex, i + 1);
                let newElement = '';
                let lastSplitter = '';

                slice.forEach(function (element) {
                    newElement += element.getJoined();
                    lastSplitter = element.getSplitter();
                });

                if (lastSplitter != null) {
                    newElement = newElement.slice(0, -lastSplitter.length);
                }

                textArray.splice(startIndex, i - startIndex);
                textArray[startIndex].setText(newElement);
                textArray[startIndex].setSplitter(lastSplitter);
                i -= (i - startIndex);
            }
        }
    }

    /**
     * Łączy tablicę tekstu do stringa, używając podanego znaku lub tekstu do połączenia
     * kolejnych elementów.
     * @param {array} textArray Tablica ze słowami.
     */
    joinStringArray(textArray) {
        if (textArray == null) {
            return '';
        }

        let str = "";
        let lastSplitter = '';

        textArray.forEach(element => {
            str += element.getJoined();
            lastSplitter = element.getSplitter();
        });

        if (lastSplitter == null) {
            return str;
        }

        return str.length > 0 && lastSplitter.length > 0 ? str.slice(0, -lastSplitter.length) : str;
    }

    /**
     * Określa, czy podany string jest końcem zdania.
     * @param {string} str Tekst do sprawdzenia.
     */
    isEndOfSentence(str) {
        if (this.isAbbreviation(str, this.knownAbbreviations)) {
            return false;
        }

        let endOfSentence = new RegExp('[' + REGEX_LETTERS + '0-9 \\[\\{\\(\\]\\}\\)"\']+(\\.|[!|?]+)$').test(str);

        if (endOfSentence) {
            return true;
        }

        if (this.isPunctuationMarksOnlyEndOfSentence(str)) {
            return true;
        }

        return false;
    }

    /**
     * Sprawdza, czy podany tekst zawiera tylko znaki interpunkcyjne, które określają koniec zdania,
     * np. kropka, wykrzyknik (lub więcej) itd.
     * @param {string} str Tekst do sprawdzenia.
     */
    isPunctuationMarksOnlyEndOfSentence(str) {
        if (str.length == 0) {
            return false;
        }

        if (str.length == 1 && str[0] == '.') {
            return true;
        }

        for (let i = 0; i < str.length; i++) {
            if (str[i] != '!' && str[i] != '?') {
                return false;
            }
        }

        return true;
    }

    /**
     * Sprawdza, czy podany tekst jest znanym skrótem.
     * @param {string} str Tekst do sprawdzenia.
     * @param {array} abbreviations Tablica znanych skrótów.
     */
    isAbbreviation(str, abbreviations) {
        for (let i = 0; i < abbreviations.length; i++) {
            let abbreviation = abbreviations[i];

            if (new RegExp('^[ "\'\\(\\[\\{]*(' + abbreviation.toLowerCase() + ')$').test(str.toLowerCase())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Sprawdza, czy podany tekst kończy się znakami, które nie dają pewności
     * pod względem tego, czy mamy do czynienia z końcem zdania, czy nie.
     * @param {string} str Tekst do sprawdzenia.
     * @param {array} tentativeEndsArray Tablica niepewnych zakończeń.
     */
    isEndingWithTentativeText(str, tentativeEndsArray) {
        for (let i = 0; i < tentativeEndsArray.length; i++) {
            if (str.toLowerCase().endsWith(tentativeEndsArray[i])) {
                return true;
            }
        }

        return false;
    }
}

// Merged from __modules__\WebsiteMajorizator.js

/**
 * Majoryzator stron internetowych.
 */
class WebsiteMajorizator {
    /**
     * Tworzy nową instancję.
     * @param {Person} person Osoba.
     */
    constructor(person) {
        this.nodesToMajorize = [];
        this.person = person;
    }

    /**
     *  Sprawdza, czy dzieci podanego węzła mogą
     * zostać poddane edycji.
     * @param {Node} node Węzeł do sprawdzenia.
     */
    areNodeChildrenMajorizable(node) {
        let tagsToOmmit = [
            "SCRIPT",
            "STYLE"
        ];

        return node != null && !tagsToOmmit.includes(node.tagName) && node.nodeType == 1;
    }

    /**
     * Sprawdza, czy węzeł może zostać poddany modyfikacji.
     * @param {Node} node Węzeł do sprawdzenia.
     */
    isNodeEditable(node) {
        if (node == null || typeof node != "object") {
            return false;
        }

        if (node.data === undefined || node.data == null || node.data == "" || !new RegExp('[' + REGEX_LETTERS + ']+').test(node.data)) {
            return false;
        }

        if (node.nodeType === undefined || node.nodeType != 3) {
            return false;
        }

        if (node.childNodes === undefined || typeof node.childNodes != "object") {
            return false;
        }

        return true;
    }

    /**
     * Dokonuje rekursywnej majoryzacji podanego węzła ze strony internetowej.
     * @param {Node} node Węzeł, dla którego wykonujemy majoryzację.
     */
    findNodesToMajorize(node) {
        if (this.isNodeEditable(node)) {
            this.nodesToMajorize.push(node);
        }

        if (this.areNodeChildrenMajorizable(node)) {
            node.childNodes.forEach(function (child) {
                this.findNodesToMajorize(child);
            }.bind(this));
        }
    }

    /**
     * Majoryzuje znalezione węzły.
     * @param {Generator} generator Instanacja Generatora.
     * @param {string} conjunctionBeforeBlacklistJson Czarna lista słów, po których nie można wstawić spójnika.
     * @param {string} conjunctionAfterBlacklistJson Czarna lista słów, przed którymi nie można wstawić spójnika.
     */
    majorizeNodes(generator, conjunctionBeforeBlacklistJson, conjunctionAfterBlacklistJson) {
        for (let i = 0; i < this.nodesToMajorize.length; i++) {
            let node = this.nodesToMajorize[i];
            node.data = generator.generate(node.data, i == 0, i == this.nodesToMajorize.length - 1,
                conjunctionBeforeBlacklistJson,
                conjunctionAfterBlacklistJson);
        }
    }

    /**
     * Uruchamia majoryzację całej strony.
     */
    majorizeWholeWebsite(conjunctionBeforeBlacklistJson, conjunctionAfterBlacklistJson,
        knownAbbreviationsJson, tentativeEndsJson) {
        this.findNodesToMajorize(document.body);
        this.majorizeNodes(new Generator(this.person,
            knownAbbreviationsJson,
            tentativeEndsJson),
            conjunctionBeforeBlacklistJson,
            conjunctionAfterBlacklistJson);
    }
}

// Merged from __modules__\Launcher.js

class Launcher {
    addCss() {
        let style = document.createElement("style");
        style.innerHTML = GM_getResourceText("css");
        document.body.appendChild(style);
    }

    createButton(person, text, buttonId) {
        let button = document.createElement("input");
        button.type = "button";
        button.value = text;
        button.classList.add("majoryzator-button");
        button.id = buttonId;
        button.onclick = () => new WebsiteMajorizator(person).majorizeWholeWebsite(
            GM_getResourceText("conjunctionBeforeBlacklistJson"),
            GM_getResourceText("conjunctionAfterBlacklistJson"),
            GM_getResourceText("knownAbbreviationsJson"),
            GM_getResourceText("tentativeEndsJson")
        );
        document.body.appendChild(button);
    }

    launch() {
        this.addCss();
        this.createButton(new PersonMajor(), "M", "majoryzator-suchodolski");
        this.createButton(new PersonKonon(), "K", "majoryzator-kononowicz");
    }
}

new Launcher().launch();

// Merged from __tampermonkey__\Footer.tampermonkey
})();