Textarea Typograf

Replaces hyphens, quotation marks, uncanonic smiles and "yo" in some russian words.

目前為 2021-10-30 提交的版本,檢視 最新版本

// ==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-2021, Gleb Kemarsky (https://github.com/glebkema)
// @license      MIT
// @version      0.5.16
// @match        http://*/*
// @match        https://*/*
// @grant        none
// @run-at       context-menu
// ==/UserScript==

// ==OpenUserJS==
// @author glebkema
// ==/OpenUserJS==

'use strict';

const MODE_ANY = 'any';
const MODE_ANY_BEGINNING = 'anyBeginning';
const MODE_ANY_ENDING = 'anyEnding';
const MODE_EXCEPTIONS = 'exceptions';
const MODE_EXTRA_PREFIXES = 'extraPrefixes';
const MODE_NO_CAPITAL_LETTER = 'noCapitalLetter';
const MODE_NO_PREFIXES = 'noPrefixes';
const MODE_NO_SUFFIXES = 'noSuffixes';
const MODE_STANDARD = 'standard';

class Typograf {
	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) {
		text = text.replace(/([:;])[—oо]?([D)(|])/g, '$1-$2');
		return text;
	}

	improveYo(text) {
		// verbs - list of the cores (with a capital letter and yo)
		text = this.improveYoVerb(text, MODE_EXCEPTIONS,
			'Льё,Мнё,Рвё,Трё');
		text = this.improveYoVerb(text, MODE_EXTRA_PREFIXES,
			'Берё,Вернё,Даё,Живё,Несё,Орё,Плывё,Поё,Ревё,Смеё,Стаё');
		text = this.improveYoVerb(text, MODE_NO_CAPITAL_LETTER,
			'Йдё,Ймё');
		text = this.improveYoVerb(text, MODE_NO_PREFIXES,
			'Идё,Начнё,Обернё,Придё,Улыбнё');
		text = this.improveYoVerb(text, MODE_NO_PREFIXES,
			'Льнё,Прильнё');
		text = this.improveYoVerb(text, MODE_NO_SUFFIXES,
			'Шёл');
		text = this.improveYoVerb(text, MODE_STANDARD,
			'Бьё,Ведё,Везё,Врё,Вьё,Гнё,Дерё,Ждё,Жмё,Жрё,Прё,Пьё,Ткнё,Чтё,Шлё,Шьё');

		// verbs - unsystematic cases
		let lookBehind = '(?<![гж-нпру-я])'; // +абвдеост, -ы
		text = this.replaceYo(text, 'Дерг', 'Дёрг', lookBehind, '(?![б-яё])');    // +а, -у
		text = this.replaceYo(text, 'Дерн', 'Дёрн', lookBehind, '(?![б-джзй-нп-тф-ъь-яё])');  // +аеиоуы (сущ. или глагол)

		// verbs - fix the exceptions
		text = this.replaceException(text, 'Раздольём');
		text = this.replaceException(text, 'Расстаёт', '(?![а-дж-я])');
		text = this.replaceException(text, 'Шлём');

		// list of the words (with a capital letter and yo)
		text = this.improveYoWord(text, null,
			'Её,Ещё,Моё,Неё,Своё,Твоё');
		text = this.improveYoWord(text, null,
			'Вдвоём,Втроём,Объём,Остриём,Приём,Причём,Своём,Твоём');
		text = this.improveYoWord(text, null,
			'В моём,На моём,О моём'); // only with certain prepositions
		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,
			'Затёк,Натёк,Потёк');
		text = this.improveYoWord(text, null,
			'Грёза,Грёзы,Слёзы');
		text = this.improveYoWord(text, null,
			'Вперёд');
		text = this.improveYoWord(text, null,
			'Насчёт');
		text = this.improveYoWord(text, MODE_ANY,
			'Съёмк');
		text = this.improveYoWord(text, MODE_ANY_BEGINNING,
			'варём');
		text = this.improveYoWord(text, MODE_ANY_ENDING,
			'Актёр,Алён,Алёх,Алёш,Алфёр,Аматёр,Амёб,Анкетёр,Антрепренёр,Артём');
		text = this.improveYoWord(text, MODE_ANY_ENDING,
			'Вертолёт,Звездолёт,Налёт,Отлёт,Полёт,Пролёт,Самолёт');
		text = this.improveYoWord(text, MODE_ANY_ENDING,
			'Партнёр,Проём');
		text = this.improveYoWord(text, MODE_ANY_ENDING,
			'Зачёт,Звездочёт,Отчёт,Почёт,Расчёт,Счёт,Учёт');
		text = this.improveYoWord(text, MODE_ANY_ENDING,
			'Вёрстк,Расчёск,Чётк');

		return text;
	}

	improveYoVerb(text, mode, list) {
		return this.iterator(text, mode, list, this.replaceYoVerb.bind(this));
	}

	improveYoWord(text, mode, list) {
		return this.iterator(text, mode, list, this.replaceYoWord.bind(this));
	}

	iterator(text, mode, list, callback) {
		if ('string' === typeof list) {
			list = list.split(',');
		}
		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, lookAhead = '') {
		const replace = this.removeAllYo(exception);
		let regex = new RegExp(exception + lookAhead, 'g');
		text = text.replace(regex, replace);
		regex = new RegExp('(?<![А-Яa-я])' + 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, 'gi');
		text = text.replace(regex, replace.toLowerCase());
		return text;
	}

	replaceYoVerb(text, mode, find, replace) {
		if (MODE_EXCEPTIONS === mode) {
			return this.replaceYo(text, find, replace,
				'(?<![б-джзй-нп-тф-я]|зе|ко|фе)' ); // +аеиоу -"зельем" -"корвет" -"фельетон"
				// '(?=[мтш])(?!мо)(?!ть)'); // -"мнемо" -"треть"
		}
		if (MODE_EXTRA_PREFIXES === mode) {
			let lookBehind = '(?<![гжк-нпрф-я])'; // +аеиоу +бвдзст
			if ('Даё' === replace) {
				lookBehind = '(?<![гжк-нпрф-ъь-я])'; // +ы
			} else if ('Стаё' === replace) {
				lookBehind = '(?<![гжк-нпрф-я]|ра)'; // -"вы/за/от/подрастает"
			}
			return this.replaceYo(text, find, replace, lookBehind);
		}
		if (MODE_NO_CAPITAL_LETTER === mode) {
			return this.replaceYo(text, find.toLowerCase(), replace);
		}
		if (MODE_NO_PREFIXES === mode) {
			return this.replaceYo(text, find, replace,
				'(?<![А-Яа-яЁё])');
		}
		if (MODE_NO_SUFFIXES === mode) {
			return this.replaceYo(text, find, replace,
				'(?<![б-джзй-нп-тф-я])', // +аеиоу
				'(?![а-яё])');
		}
		// MODE_STANDARD
		return this.replaceYo(text, find, replace);
	}

	replaceYoWord(text, mode, find, replace) {
		if (MODE_ANY === mode) {
			return this.replaceYo(text, find, replace,
				'',
				'(?=[а-я])');
		}
		if (MODE_ANY_BEGINNING === mode) {
			return this.replaceYo(text, find, replace,
				'',
				'(?![а-яё])');
		}
		if (MODE_ANY_ENDING === mode) {
			return this.replaceYo(text, find, replace,
				'(?<![А-Яа-яЁё])',
				'');
		}
		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
}