NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Textarea Typograf // @namespace https://github.com/glebkema/tampermonkey-textarea-typograf // @description Replaces hyphens, quotation marks, uncanonic smiles and "yo" in some russian words. // @author glebkema // @copyright 2020-2024, Gleb Kemarsky (https://github.com/glebkema) // @license MIT // @version 0.7.12 // @match http://*/* // @match https://*/* // @grant none // @run-at context-menu // ==/UserScript== // ==OpenUserJS== // @author glebkema // ==/OpenUserJS== 'use strict'; class Typograf { MODE_ANY = 'any'; MODE_ANY_BEGINNING = 'anyBeginning'; MODE_ANY_BEGINNING_EXCEPT_O_AND_Y = 'anyBeginningExceptOAndY'; MODE_ANY_BEGINNING_EXCEPT_Y = 'anyBeginningExceptY'; MODE_ANY_ENDING = 'anyEnding'; MODE_ANY_ENDING_EXCEPT_D = 'anyEndingExceptD'; MODE_ANY_ENDING_EXCEPT_I_AND_SOFT_SIGN = 'anyEndingExceptIAndSoftSign'; MODE_ANY_ENDING_EXCEPT_L = 'anyEndingExceptL'; MODE_ANY_ENDING_EXCEPT_N = 'anyEndingExceptN'; MODE_ANY_EXCEPT_I = 'anyExceptI'; MODE_ANY_EXCEPT_K = 'anyExceptK'; MODE_ANY_EXCEPT_R = 'anyExceptR'; MODE_AS_IS = 'asIs'; MODE_ENDINGS_1 = 'endings1'; MODE_ENDINGS_2 = 'endings2'; MODE_ENDINGS_3 = 'endings3'; MODE_EXCEPTIONS = 'exceptions'; MODE_EXTRA_PREFIXES = 'extraPrefixes'; MODE_NO_CAPITAL_LETTER = 'noCapitalLetter'; MODE_NO_PREFIXES = 'noPrefixes'; MODE_NO_SUFFIXES = 'noSuffixes'; MODE_STANDARD = 'standard'; verbCores = { [this.MODE_EXCEPTIONS]: 'Льё,Мнё,Рвё,Трё', [this.MODE_EXTRA_PREFIXES]: 'Берё,Боднё,Вернё,Даё,Живё,Несё,Орё,Пасё,Плывё,Поё,Ревё,Смеё,Стаё', [this.MODE_NO_CAPITAL_LETTER]: 'Йдё,Ймё', [this.MODE_NO_PREFIXES]: 'Идё,Начнё,Обернё,Придаё,Придё,Улыбнё', [this.MODE_NO_SUFFIXES]: 'Берёгся,Шёл', [this.MODE_STANDARD]: 'Бережё,Блеснё,Блюдё,Блюё,Бьё,Ведё,Везё,Врё,Вьё,Гнё,Дерё,Ждё,Жмё,Жрё,Льнё,Прё,Пьё,Ткнё,Чтё,Шлё,Шьё', }; words = { [this.MODE_AS_IS]: // alphabetically 'Бёдер,Белёк,Бельём,Бобёр,Бобылём,' + 'Взахлёб,Вперёд,' + 'Запёк,' + 'Копьё,Копьём,' + 'Отстранён,' // MODE_ENDINGS_3 для других форм этого слова + 'Предпочёл,Прочёл,' + 'Рулём,' + 'Твёрже,' // groups + 'Василёк,Мотылёк,Огонёк,Пенёк,Поперёк,Ручеёк,' + 'Вдвоём,Втроём,Объём,Остриём,Причём,Своём,Твоём,' + 'Грёза,Грёзы,Слёзы,' + 'Её,Ещё,Моё,Неё,Своё,Твоё,' + 'Журавлём,Кораблём,Королём,Снегирём,Соловьём,' + 'Затёк,Натёк,Потёк,' + 'Трёх,Четырём,Четырёх,', // "Трём" уже есть как глагол [this.MODE_ANY]: 'ёхонек,ёхоньк,ёшенек,ёшеньк,' + 'бомбёж,гиллёз,надёг,ощёк,счётн,уёмн,шёрстн,циллёз,ъёмкост,' // стёгивал,стёгнут, + 'Пролёт,Самолёт,' + 'Отчёт,Расчёт,' + 'Веретён,Гнёзд,Звёздн,Лёгочн,Лётчи,Надёжн,Налёт,Разъём,Съёмк,' // adjectives + 'бережённ,ворённ,мягчённ,ретённ,таённ,теплённ', [this.MODE_ANY_BEGINNING]: 'атырёв,атырём,варём,' + 'арьё,арьём,ерьё,ерьём,ырьё,ырьём,' + 'берёг', // NB: except as is: "берег моря" [this.MODE_ANY_BEGINNING_EXCEPT_O_AND_Y]: // adjectives 'Точён', // - сосредоточено [this.MODE_ANY_BEGINNING_EXCEPT_Y]: // adjectives 'несённ,' + 'тёкш,Тёрт,тёрш,' + 'Шёрстн', [this.MODE_ANY_ENDING]: // alphabetically 'Актёр,Алён,Алёх,Алёш,Алфёр,Аматёр,Амёб,Анкетёр,Антрепренёр,Артём,' + 'Бабёнк,Бабёф,Балансёр,Балдёж,Банкомёт,Баталёр,Бёдра,Бельёвщиц,Бережён,Берёз,Бесён,Бесслёзн,Бечёвк,Бечёво,Билетёр,Бирюлёв,Благословлён,Блёстк,Бобрён,Боксёр,Бородён,Боронён,Бочкарёв,' + 'Вёрстк,' + 'Ворьё,' // NB: ворьё,ворьём но подворье,подспорье + 'Жёстк,' + 'Лёгки,' + 'Партнёр,Проём,' + 'Расчёск,Ребён,' + 'Серьёз,' + 'Трёш,' + 'Чётк,' // cognate words + 'Вертолёт,Звездолёт,Отлёт,Перелёт,Полёт,' + 'Запёкш,Запечён,Испечён,' + 'Заём,Наём,' + 'Зачёт,Звездочёт,Почёт,Счёт,Учёт', [this.MODE_ANY_ENDING_EXCEPT_D]: 'Одёж', [this.MODE_ANY_ENDING_EXCEPT_I_AND_SOFT_SIGN]: 'Твёрд', [this.MODE_ANY_ENDING_EXCEPT_L]: 'Приём', [this.MODE_ANY_ENDING_EXCEPT_N]: 'Трёх', [this.MODE_ENDINGS_1]: 'Зелён', // [аоуык] [this.MODE_ENDINGS_2]: 'Учён', // [аоуы] [this.MODE_ENDINGS_3]: 'Включён,Остранён', // [н] [this.MODE_ANY_EXCEPT_I]: 'бретён,скажён,творён', [this.MODE_ANY_EXCEPT_K]: 'бъё', [this.MODE_ANY_EXCEPT_R]: 'омёт', } run(element) { if (element && 'textarea' === element.tagName.toLowerCase() && element.value) { const start = element.selectionStart; const end = element.selectionEnd; if (start === end) { element.value = this.improve(element.value); } else { const selected = element.value.substring(start, end); const theLength = element.value.length; element.value = element.value.substring(0, start) + this.improve(selected) + element.value.substring(end, theLength); } } else { // console.info('Start editing a non-empty textarea before calling the script'); } } improve(text) { if (text) { text = this.improveDash(text); text = this.improveQuotes(text); text = this.improveSmile(text); text = this.improveYo(text); } return text; } improveDash(text) { text = text.replace(/ - /g, ' — '); return text; } improveQuotes(text) { // use only one type + only external if two stand together // text = text.replace(/(?<=^|[(\s])["„“]/g, '«'); // text = text.replace(/["„“](?=$|[.,;:!?)\s])/g, '»'); // use only one type text = text.replace(/["„“”](?=["„“”«]*[\wа-яё(])/gi, '«'); text = text.replace(/(?<=[\wа-яё).!?]["„“”»]*)["„“”]/gi, '»'); // nested quotes // (?:«[^»]*)([«"])([^"»]*)(["»]) // (?=(?:(?<!\w)["«](\w.*?)["»](?!\w))) https://stackoverflow.com/a/39706568/6263942 // («([^«»]|(?R))*») https://stackoverflow.com/a/14952740/6263942 // «((?>[^«»]+|(?R))*)» https://stackoverflow.com/a/26386070/6263942 // «([^«»]*+(?:(?R)[^«»]*)*+)» https://stackoverflow.com/a/26386070/6263942 // «[^»]*(?:(«)[^«»]*+(»)[^«]*)+» do { var old = text; text = text.replace(/(?<=«[^»]*)«(.*?)»/g, '„$1“'); } while ( old !== text ); return text; } improveSmile(text) { // fix uncanonical smiles text = text.replace(/([:;])[—oо]?([D)(|])/g, '$1-$2'); // remove the dot before the smile text = text.replace(/(?<=[А-ЯЁа-яё])\.\s*(?=[:;]-[D)(|])/g, ' '); return text; } improveYo(text) { // verbs - cores for (let mode in this.verbCores) { text = this.improveverbCores(text, mode, this.verbCores[mode]); } // verbs - unsystematic cases let lookBehind = '(?<![гж-нпру-я])'; // +абвдеост, -ы text = this.replaceYo(text, 'Дерг', 'Дёрг', lookBehind, '(?![б-яё])'); // +а, -у text = this.replaceYo(text, 'Дерн', 'Дёрн', lookBehind, '(?![б-джзй-нп-тф-ъь-яё])'); // +аеиоуы (сущ. или глагол) lookBehind = '(?<![бвге-зй-ру-я])'; // +адист text = this.replaceYo(text, 'Стег', 'Стёг', lookBehind, '(?!ал|ать|ну)'); text = this.replaceYo(text, 'Стегнут', 'Стёгнут', lookBehind, '(?!ь)'); // NB: расстёгнутый text = this.replaceYo(text, 'черкива', 'чёркива', '(?<=[адты])', '(?=[елт])'); // verbs - fix the exceptions lookBehind = '(?<![А-Яa-я])'; text = this.replaceException(text, 'Раздольём', lookBehind); text = this.replaceException(text, 'Расстаёт', lookBehind, '(?![а-дж-я])'); text = this.replaceException(text, 'Шлём', lookBehind); // words with a certain preposition // NB: before `words` for combinations like `Всё чётко` text = this.improveYoWord(text, null, 'В моём,На моём,О моём'); text = this.improveYoWord(text, null, 'В нём,О нём,При нём'); text = this.improveYoWord(text, null, 'Всё верно,Всё напрасно,Всё очень просто,Всё понятно,Всё правильно,Всё просто,Всё путём,Всё равно,Всё так же,Всё то же,Всё точно,Всё чётко,Всё ясно'); text = this.improveYoWord(text, null, 'Всё, на чём/Всё, о чём/Всё, про что/Всё, с чем/Всё, что/Всё-таки', '/'); text = this.improveYoWord(text, null, 'Ни на чём/Ни о чём/Ни при чём', '/'); // words for (let mode in this.words) { text = this.improveYoWord(text, mode, this.words[mode]); } return text; } improveverbCores(text, mode, list, divider = ',') { return this.iterator(text, mode, list, divider, this.replaceverbCores.bind(this)); } improveYoWord(text, mode, list, divider = ',') { return this.iterator(text, mode, list, divider, this.replaceYoWord.bind(this)); } iterator(text, mode, list, divider, callback) { if ('string' === typeof list) { list = list.split(divider); } for (let i = 0; i < list.length; i++) { const replace = list[i].trim(); if (replace) { const find = this.removeAllYo(replace); text = callback(text, mode, find, replace); } } return text; } removeAllYo(text) { return text.replace(/ё/g, 'е').replace(/Ё/g, 'Е'); } // restore the `e` instead of `yo` replaceException(text, exception, lookBehind = '', lookAhead = '') { const replace = this.removeAllYo(exception); let regex = new RegExp(exception + lookAhead, 'g'); text = text.replace(regex, replace); regex = new RegExp(lookBehind + exception.toLowerCase() + lookAhead, 'g'); text = text.replace(regex, replace.toLowerCase()); return text; } replaceYo(text, find, replace, lookBehind = '(?<![б-джзй-нп-тф-я])', // +аеиоу // lookAhead = '(?=[мтш])' lookAhead = '(?=(?:м|мся|т|те|тесь|тся|шь|шься)(?:[^а-яё]|$))' ) { let regex; let findLowerCase = find.toLowerCase(); // NB: \b doesn't work for russian words // 1) starts with a capital letter = just a begining of the word if (find !== findLowerCase) { regex = new RegExp(find + lookAhead, 'g'); text = text.replace(regex, replace); } // 2) in lowercase = with a prefix ahead or without it regex = new RegExp(lookBehind + findLowerCase + lookAhead, 'g' + ('' === lookBehind ? '' : 'i')); text = text.replace(regex, replace.toLowerCase()); return text; } replaceverbCores(text, mode, find, replace) { if (this.MODE_EXCEPTIONS === mode) { return this.replaceYo(text, find, replace, '(?<![б-джзй-нп-тф-я]|зе|ко|фе)' ); // +аеиоу -"зельем" -"корвет" -"фельетон" // '(?=[мтш])(?!мо)(?!ть)'); // -"мнемо" -"треть" } if (this.MODE_EXTRA_PREFIXES === mode) { let lookBehind = '(?<![гжк-нпрф-я])'; // +аеиоу +бвдзст if ('Даё' === replace) { lookBehind = '(?<![гжик-нпрф-ъь-я]|ла|па)'; // -и +ы >>> +"Придаёт" -"Обладает" -"Попадает" } else if ('Пасё' === replace) { lookBehind = '(?<![б-зй-нпртф-я])'; // "напасёшься" } else if ('Стаё' === replace) { lookBehind = '(?<![гжк-нпрф-я]|ра)'; // -"вы/за/от/подрастает" } return this.replaceYo(text, find, replace, lookBehind); } if (this.MODE_NO_CAPITAL_LETTER === mode) { return this.replaceYo(text, find.toLowerCase(), replace); } if (this.MODE_NO_PREFIXES === mode) { return this.replaceYo(text, find, replace, '(?<![А-Яа-яЁё])'); } if (this.MODE_NO_SUFFIXES === mode) { return this.replaceYo(text, find, replace, '(?<![б-джзй-нпртф-я])', // +аеиоу +с '(?![а-яё])'); } // MODE_STANDARD return this.replaceYo(text, find, replace); } replaceYoWord(text, mode, find, replace) { if (this.MODE_ANY === mode) { return this.replaceYo(text, find, replace, '', ''); } if (this.MODE_ANY_BEGINNING === mode) { return this.replaceYo(text, find, replace, '', '(?![а-яё])'); } if (this.MODE_ANY_BEGINNING_EXCEPT_O_AND_Y === mode) { return this.replaceYo(text, find, replace, '(?<![оы])', ''); } if (this.MODE_ANY_BEGINNING_EXCEPT_Y === mode) { return this.replaceYo(text, find, replace, '(?<![ы])', ''); } if (this.MODE_ANY_ENDING === mode) { return this.replaceYo(text, find, replace, '(?<![А-Яа-яЁё])', ''); } if (this.MODE_ANY_ENDING_EXCEPT_D === mode) { return this.replaceYo(text, find, replace, '(?<![А-Яа-яЁё])', '(?![д])'); } if (this.MODE_ANY_ENDING_EXCEPT_I_AND_SOFT_SIGN === mode) { return this.replaceYo(text, find, replace, '(?<![А-Яа-яЁё])', '(?![иь])'); } if (this.MODE_ANY_ENDING_EXCEPT_L === mode) { return this.replaceYo(text, find, replace, '(?<![А-Яа-яЁё])', '(?![л])'); } if (this.MODE_ANY_ENDING_EXCEPT_N === mode) { return this.replaceYo(text, find, replace, '(?<![А-Яа-яЁё])', '(?![н])'); } if (this.MODE_ANY_EXCEPT_I === mode) { return this.replaceYo(text, find, replace, '', '(?![и])'); } if (this.MODE_ANY_EXCEPT_K === mode) { return this.replaceYo(text, find, replace, '', '(?![к])'); } if (this.MODE_ANY_EXCEPT_R === mode) { return this.replaceYo(text, find, replace, '', '(?![р])'); } if (this.MODE_ENDINGS_1 === mode) { return this.replaceYo(text, find, replace, '', '(?=[аоуык])'); } if (this.MODE_ENDINGS_2 === mode) { return this.replaceYo(text, find, replace, '', '(?=[аоуы])'); } if (this.MODE_ENDINGS_3 === mode) { return this.replaceYo(text, find, replace, '', '(?=н)'); } // MODE_AS_IS return this.replaceYo(text, find, replace, '(?<![А-Яа-яЁё])', '(?![а-яё])'); } } // if it's a browser, not a test if ('undefined' !== typeof document) { let typograf = new Typograf(); typograf.run(document.activeElement); } // if it's a test by Node.js if (module) { module.exports = { Typograf: Typograf, }; } else { var module; // hack for Tampermonkey's eslint }