Textarea Typograf

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

  1. // ==UserScript==
  2. // @name Textarea Typograf
  3. // @namespace https://github.com/glebkema/tampermonkey-textarea-typograf
  4. // @description Replaces hyphens, quotation marks, uncanonic smiles and "yo" in some russian words.
  5. // @author glebkema
  6. // @copyright 2020-2024, Gleb Kemarsky (https://github.com/glebkema)
  7. // @license MIT
  8. // @version 0.7.12
  9. // @match http://*/*
  10. // @match https://*/*
  11. // @grant none
  12. // @run-at context-menu
  13. // ==/UserScript==
  14.  
  15. // ==OpenUserJS==
  16. // @author glebkema
  17. // ==/OpenUserJS==
  18.  
  19. 'use strict';
  20.  
  21. class Typograf {
  22. MODE_ANY = 'any';
  23. MODE_ANY_BEGINNING = 'anyBeginning';
  24. MODE_ANY_BEGINNING_EXCEPT_O_AND_Y = 'anyBeginningExceptOAndY';
  25. MODE_ANY_BEGINNING_EXCEPT_Y = 'anyBeginningExceptY';
  26. MODE_ANY_ENDING = 'anyEnding';
  27. MODE_ANY_ENDING_EXCEPT_D = 'anyEndingExceptD';
  28. MODE_ANY_ENDING_EXCEPT_I_AND_SOFT_SIGN = 'anyEndingExceptIAndSoftSign';
  29. MODE_ANY_ENDING_EXCEPT_L = 'anyEndingExceptL';
  30. MODE_ANY_ENDING_EXCEPT_N = 'anyEndingExceptN';
  31. MODE_ANY_EXCEPT_I = 'anyExceptI';
  32. MODE_ANY_EXCEPT_K = 'anyExceptK';
  33. MODE_ANY_EXCEPT_R = 'anyExceptR';
  34. MODE_AS_IS = 'asIs';
  35. MODE_ENDINGS_1 = 'endings1';
  36. MODE_ENDINGS_2 = 'endings2';
  37. MODE_ENDINGS_3 = 'endings3';
  38. MODE_EXCEPTIONS = 'exceptions';
  39. MODE_EXTRA_PREFIXES = 'extraPrefixes';
  40. MODE_NO_CAPITAL_LETTER = 'noCapitalLetter';
  41. MODE_NO_PREFIXES = 'noPrefixes';
  42. MODE_NO_SUFFIXES = 'noSuffixes';
  43. MODE_STANDARD = 'standard';
  44.  
  45. verbCores = {
  46. [this.MODE_EXCEPTIONS]: 'Льё,Мнё,Рвё,Трё',
  47. [this.MODE_EXTRA_PREFIXES]: 'Берё,Боднё,Вернё,Даё,Живё,Несё,Орё,Пасё,Плывё,Поё,Ревё,Смеё,Стаё',
  48. [this.MODE_NO_CAPITAL_LETTER]: 'Йдё,Ймё',
  49. [this.MODE_NO_PREFIXES]: 'Идё,Начнё,Обернё,Придаё,Придё,Улыбнё',
  50. [this.MODE_NO_SUFFIXES]: 'Берёгся,Шёл',
  51. [this.MODE_STANDARD]: 'Бережё,Блеснё,Блюдё,Блюё,Бьё,Ведё,Везё,Врё,Вьё,Гнё,Дерё,Ждё,Жмё,Жрё,Льнё,Прё,Пьё,Ткнё,Чтё,Шлё,Шьё',
  52. };
  53.  
  54. words = {
  55. [this.MODE_AS_IS]:
  56. // alphabetically
  57. 'Бёдер,Белёк,Бельём,Бобёр,Бобылём,'
  58. + 'Взахлёб,Вперёд,'
  59. + 'Запёк,'
  60. + 'Копьё,Копьём,'
  61. + 'Отстранён,' // MODE_ENDINGS_3 для других форм этого слова
  62. + 'Предпочёл,Прочёл,'
  63. + 'Рулём,'
  64. + 'Твёрже,'
  65.  
  66. // groups
  67. + 'Василёк,Мотылёк,Огонёк,Пенёк,Поперёк,Ручеёк,'
  68. + 'Вдвоём,Втроём,Объём,Остриём,Причём,Своём,Твоём,'
  69. + 'Грёза,Грёзы,Слёзы,'
  70. + 'Её,Ещё,Моё,Неё,Своё,Твоё,'
  71. + 'Журавлём,Кораблём,Королём,Снегирём,Соловьём,'
  72. + 'Затёк,Натёк,Потёк,'
  73. + 'Трёх,Четырём,Четырёх,', // "Трём" уже есть как глагол
  74.  
  75. [this.MODE_ANY]: 'ёхонек,ёхоньк,ёшенек,ёшеньк,'
  76. + 'бомбёж,гиллёз,надёг,ощёк,счётн,уёмн,шёрстн,циллёз,ъёмкост,' // стёгивал,стёгнут,
  77. + 'Пролёт,Самолёт,'
  78. + 'Отчёт,Расчёт,'
  79. + 'Веретён,Гнёзд,Звёздн,Лёгочн,Лётчи,Надёжн,Налёт,Разъём,Съёмк,'
  80.  
  81. // adjectives
  82. + 'бережённ,ворённ,мягчённ,ретённ,таённ,теплённ',
  83.  
  84. [this.MODE_ANY_BEGINNING]: 'атырёв,атырём,варём,'
  85. + 'арьё,арьём,ерьё,ерьём,ырьё,ырьём,'
  86. + 'берёг', // NB: except as is: "берег моря"
  87.  
  88. [this.MODE_ANY_BEGINNING_EXCEPT_O_AND_Y]:
  89.  
  90. // adjectives
  91. 'Точён', // - сосредоточено
  92.  
  93. [this.MODE_ANY_BEGINNING_EXCEPT_Y]:
  94.  
  95. // adjectives
  96. 'несённ,'
  97. + 'тёкш,Тёрт,тёрш,'
  98. + 'Шёрстн',
  99.  
  100. [this.MODE_ANY_ENDING]:
  101. // alphabetically
  102. 'Актёр,Алён,Алёх,Алёш,Алфёр,Аматёр,Амёб,Анкетёр,Антрепренёр,Артём,'
  103. + 'Бабёнк,Бабёф,Балансёр,Балдёж,Банкомёт,Баталёр,Бёдра,Бельёвщиц,Бережён,Берёз,Бесён,Бесслёзн,Бечёвк,Бечёво,Билетёр,Бирюлёв,Благословлён,Блёстк,Бобрён,Боксёр,Бородён,Боронён,Бочкарёв,'
  104. + 'Вёрстк,'
  105. + 'Ворьё,' // NB: ворьё,ворьём но подворье,подспорье
  106. + 'Жёстк,'
  107. + 'Лёгки,'
  108. + 'Партнёр,Проём,'
  109. + 'Расчёск,Ребён,'
  110. + 'Серьёз,'
  111. + 'Трёш,'
  112. + 'Чётк,'
  113.  
  114. // cognate words
  115. + 'Вертолёт,Звездолёт,Отлёт,Перелёт,Полёт,'
  116. + 'Запёкш,Запечён,Испечён,'
  117. + 'Заём,Наём,'
  118. + 'Зачёт,Звездочёт,Почёт,Счёт,Учёт',
  119.  
  120. [this.MODE_ANY_ENDING_EXCEPT_D]: 'Одёж',
  121.  
  122. [this.MODE_ANY_ENDING_EXCEPT_I_AND_SOFT_SIGN]: 'Твёрд',
  123.  
  124. [this.MODE_ANY_ENDING_EXCEPT_L]: 'Приём',
  125.  
  126. [this.MODE_ANY_ENDING_EXCEPT_N]: 'Трёх',
  127.  
  128. [this.MODE_ENDINGS_1]: 'Зелён', // [аоуык]
  129.  
  130. [this.MODE_ENDINGS_2]: 'Учён', // [аоуы]
  131.  
  132. [this.MODE_ENDINGS_3]: 'Включён,Остранён', // [н]
  133.  
  134.  
  135. [this.MODE_ANY_EXCEPT_I]: 'бретён,скажён,творён',
  136.  
  137. [this.MODE_ANY_EXCEPT_K]: 'бъё',
  138.  
  139. [this.MODE_ANY_EXCEPT_R]: 'омёт',
  140. }
  141.  
  142. run(element) {
  143. if (element && 'textarea' === element.tagName.toLowerCase() && element.value) {
  144. const start = element.selectionStart;
  145. const end = element.selectionEnd;
  146. if (start === end) {
  147. element.value = this.improve(element.value);
  148. } else {
  149. const selected = element.value.substring(start, end);
  150. const theLength = element.value.length;
  151. element.value = element.value.substring(0, start)
  152. + this.improve(selected) + element.value.substring(end, theLength);
  153. }
  154. } else {
  155. // console.info('Start editing a non-empty textarea before calling the script');
  156. }
  157. }
  158.  
  159. improve(text) {
  160. if (text) {
  161. text = this.improveDash(text);
  162. text = this.improveQuotes(text);
  163. text = this.improveSmile(text);
  164. text = this.improveYo(text);
  165. }
  166. return text;
  167. }
  168.  
  169. improveDash(text) {
  170. text = text.replace(/ - /g, ' — ');
  171. return text;
  172. }
  173.  
  174. improveQuotes(text) {
  175. // use only one type + only external if two stand together
  176. // text = text.replace(/(?<=^|[(\s])["„“]/g, '«');
  177. // text = text.replace(/["„“](?=$|[.,;:!?)\s])/g, '»');
  178.  
  179. // use only one type
  180. text = text.replace(/["„“”](?=["„“”«]*[\wа-яё(])/gi, '«');
  181. text = text.replace(/(?<=[\wа-яё).!?]["„“”»]*)["„“”]/gi, '»');
  182.  
  183. // nested quotes
  184. // (?:«[^»]*)([«"])([^"»]*)(["»])
  185. // (?=(?:(?<!\w)["«](\w.*?)["»](?!\w))) https://stackoverflow.com/a/39706568/6263942
  186. // («([^«»]|(?R))*») https://stackoverflow.com/a/14952740/6263942
  187. // «((?>[^«»]+|(?R))*)» https://stackoverflow.com/a/26386070/6263942
  188. // «([^«»]*+(?:(?R)[^«»]*)*+)» https://stackoverflow.com/a/26386070/6263942
  189. // «[^»]*(?:(«)[^«»]*+(»)[^«]*)+»
  190. do {
  191. var old = text;
  192. text = text.replace(/(?<=«[^»]*)«(.*?)»/g, '„$1“');
  193. } while ( old !== text );
  194. return text;
  195. }
  196.  
  197. improveSmile(text) {
  198. // fix uncanonical smiles
  199. text = text.replace(/([:;])[—oо]?([D)(|])/g, '$1-$2');
  200.  
  201. // remove the dot before the smile
  202. text = text.replace(/(?<=[А-ЯЁа-яё])\.\s*(?=[:;]-[D)(|])/g, ' ');
  203.  
  204. return text;
  205. }
  206.  
  207. improveYo(text) {
  208. // verbs - cores
  209. for (let mode in this.verbCores) {
  210. text = this.improveverbCores(text, mode, this.verbCores[mode]);
  211. }
  212.  
  213. // verbs - unsystematic cases
  214. let lookBehind = '(?<![гж-нпру-я])'; // +абвдеост, -ы
  215. text = this.replaceYo(text, 'Дерг', 'Дёрг', lookBehind, '(?![б-яё])'); // +а, -у
  216. text = this.replaceYo(text, 'Дерн', 'Дёрн', lookBehind, '(?![б-джзй-нп-тф-ъь-яё])'); // +аеиоуы (сущ. или глагол)
  217.  
  218. lookBehind = '(?<![бвге-зй-ру-я])'; // +адист
  219. text = this.replaceYo(text, 'Стег', 'Стёг', lookBehind, '(?!ал|ать|ну)');
  220. text = this.replaceYo(text, 'Стегнут', 'Стёгнут', lookBehind, '(?!ь)'); // NB: расстёгнутый
  221.  
  222. text = this.replaceYo(text, 'черкива', 'чёркива', '(?<=[адты])', '(?=[елт])');
  223.  
  224. // verbs - fix the exceptions
  225. lookBehind = '(?<![А-Яa-я])';
  226. text = this.replaceException(text, 'Раздольём', lookBehind);
  227. text = this.replaceException(text, 'Расстаёт', lookBehind, '(?![а-дж-я])');
  228. text = this.replaceException(text, 'Шлём', lookBehind);
  229.  
  230. // words with a certain preposition
  231. // NB: before `words` for combinations like `Всё чётко`
  232. text = this.improveYoWord(text, null, 'В моём,На моём,О моём');
  233. text = this.improveYoWord(text, null, 'В нём,О нём,При нём');
  234. text = this.improveYoWord(text, null, 'Всё верно,Всё напрасно,Всё очень просто,Всё понятно,Всё правильно,Всё просто,Всё путём,Всё равно,Всё так же,Всё то же,Всё точно,Всё чётко,Всё ясно');
  235. text = this.improveYoWord(text, null, 'Всё, на чём/Всё, о чём/Всё, про что/Всё, с чем/Всё, что/Всё-таки', '/');
  236. text = this.improveYoWord(text, null, 'Ни на чём/Ни о чём/Ни при чём', '/');
  237.  
  238. // words
  239. for (let mode in this.words) {
  240. text = this.improveYoWord(text, mode, this.words[mode]);
  241. }
  242.  
  243. return text;
  244. }
  245.  
  246. improveverbCores(text, mode, list, divider = ',') {
  247. return this.iterator(text, mode, list, divider, this.replaceverbCores.bind(this));
  248. }
  249.  
  250. improveYoWord(text, mode, list, divider = ',') {
  251. return this.iterator(text, mode, list, divider, this.replaceYoWord.bind(this));
  252. }
  253.  
  254. iterator(text, mode, list, divider, callback) {
  255. if ('string' === typeof list) {
  256. list = list.split(divider);
  257. }
  258. for (let i = 0; i < list.length; i++) {
  259. const replace = list[i].trim();
  260. if (replace) {
  261. const find = this.removeAllYo(replace);
  262. text = callback(text, mode, find, replace);
  263. }
  264. }
  265. return text;
  266. }
  267.  
  268. removeAllYo(text) {
  269. return text.replace(/ё/g, 'е').replace(/Ё/g, 'Е');
  270. }
  271.  
  272. // restore the `e` instead of `yo`
  273. replaceException(text, exception, lookBehind = '', lookAhead = '') {
  274. const replace = this.removeAllYo(exception);
  275. let regex = new RegExp(exception + lookAhead, 'g');
  276. text = text.replace(regex, replace);
  277. regex = new RegExp(lookBehind + exception.toLowerCase() + lookAhead, 'g');
  278. text = text.replace(regex, replace.toLowerCase());
  279. return text;
  280. }
  281.  
  282. replaceYo(text, find, replace,
  283. lookBehind = '(?<![б-джзй-нп-тф-я])', // +аеиоу
  284. // lookAhead = '(?=[мтш])'
  285. lookAhead = '(?=(?:м|мся|т|те|тесь|тся|шь|шься)(?:[^а-яё]|$))'
  286. ) {
  287. let regex;
  288. let findLowerCase = find.toLowerCase();
  289. // NB: \b doesn't work for russian words
  290. // 1) starts with a capital letter = just a begining of the word
  291. if (find !== findLowerCase) {
  292. regex = new RegExp(find + lookAhead, 'g');
  293. text = text.replace(regex, replace);
  294. }
  295. // 2) in lowercase = with a prefix ahead or without it
  296. regex = new RegExp(lookBehind + findLowerCase + lookAhead, 'g' + ('' === lookBehind ? '' : 'i'));
  297. text = text.replace(regex, replace.toLowerCase());
  298. return text;
  299. }
  300.  
  301. replaceverbCores(text, mode, find, replace) {
  302. if (this.MODE_EXCEPTIONS === mode) {
  303. return this.replaceYo(text, find, replace,
  304. '(?<![б-джзй-нп-тф-я]|зе|ко|фе)' ); // +аеиоу -"зельем" -"корвет" -"фельетон"
  305. // '(?=[мтш])(?!мо)(?!ть)'); // -"мнемо" -"треть"
  306. }
  307. if (this.MODE_EXTRA_PREFIXES === mode) {
  308. let lookBehind = '(?<![гжк-нпрф-я])'; // +аеиоу +бвдзст
  309. if ('Даё' === replace) {
  310. lookBehind = '(?<![гжик-нпрф-ъь-я]|ла|па)'; // -и +ы >>> +"Придаёт" -"Обладает" -"Попадает"
  311. } else if ('Пасё' === replace) {
  312. lookBehind = '(?<![б-зй-нпртф-я])'; // "напасёшься"
  313. } else if ('Стаё' === replace) {
  314. lookBehind = '(?<![гжк-нпрф-я]|ра)'; // -"вы/за/от/подрастает"
  315. }
  316. return this.replaceYo(text, find, replace, lookBehind);
  317. }
  318. if (this.MODE_NO_CAPITAL_LETTER === mode) {
  319. return this.replaceYo(text, find.toLowerCase(), replace);
  320. }
  321. if (this.MODE_NO_PREFIXES === mode) {
  322. return this.replaceYo(text, find, replace,
  323. '(?<![А-Яа-яЁё])');
  324. }
  325. if (this.MODE_NO_SUFFIXES === mode) {
  326. return this.replaceYo(text, find, replace,
  327. '(?<![б-джзй-нпртф-я])', // +аеиоу +с
  328. '(?![а-яё])');
  329. }
  330. // MODE_STANDARD
  331. return this.replaceYo(text, find, replace);
  332. }
  333.  
  334. replaceYoWord(text, mode, find, replace) {
  335. if (this.MODE_ANY === mode) {
  336. return this.replaceYo(text, find, replace,
  337. '',
  338. '');
  339. }
  340. if (this.MODE_ANY_BEGINNING === mode) {
  341. return this.replaceYo(text, find, replace,
  342. '',
  343. '(?![а-яё])');
  344. }
  345. if (this.MODE_ANY_BEGINNING_EXCEPT_O_AND_Y === mode) {
  346. return this.replaceYo(text, find, replace,
  347. '(?<![оы])',
  348. '');
  349. }
  350. if (this.MODE_ANY_BEGINNING_EXCEPT_Y === mode) {
  351. return this.replaceYo(text, find, replace,
  352. '(?<![ы])',
  353. '');
  354. }
  355. if (this.MODE_ANY_ENDING === mode) {
  356. return this.replaceYo(text, find, replace,
  357. '(?<![А-Яа-яЁё])',
  358. '');
  359. }
  360. if (this.MODE_ANY_ENDING_EXCEPT_D === mode) {
  361. return this.replaceYo(text, find, replace,
  362. '(?<![А-Яа-яЁё])',
  363. '(?![д])');
  364. }
  365. if (this.MODE_ANY_ENDING_EXCEPT_I_AND_SOFT_SIGN === mode) {
  366. return this.replaceYo(text, find, replace,
  367. '(?<![А-Яа-яЁё])',
  368. '(?![иь])');
  369. }
  370. if (this.MODE_ANY_ENDING_EXCEPT_L === mode) {
  371. return this.replaceYo(text, find, replace,
  372. '(?<![А-Яа-яЁё])',
  373. '(?![л])');
  374. }
  375. if (this.MODE_ANY_ENDING_EXCEPT_N === mode) {
  376. return this.replaceYo(text, find, replace,
  377. '(?<![А-Яа-яЁё])',
  378. '(?![н])');
  379. }
  380. if (this.MODE_ANY_EXCEPT_I === mode) {
  381. return this.replaceYo(text, find, replace,
  382. '',
  383. '(?![и])');
  384. }
  385. if (this.MODE_ANY_EXCEPT_K === mode) {
  386. return this.replaceYo(text, find, replace,
  387. '',
  388. '(?![к])');
  389. }
  390. if (this.MODE_ANY_EXCEPT_R === mode) {
  391. return this.replaceYo(text, find, replace,
  392. '',
  393. '(?![р])');
  394. }
  395. if (this.MODE_ENDINGS_1 === mode) {
  396. return this.replaceYo(text, find, replace,
  397. '',
  398. '(?=[аоуык])');
  399. }
  400. if (this.MODE_ENDINGS_2 === mode) {
  401. return this.replaceYo(text, find, replace,
  402. '',
  403. '(?=[аоуы])');
  404. }
  405. if (this.MODE_ENDINGS_3 === mode) {
  406. return this.replaceYo(text, find, replace,
  407. '',
  408. '(?=н)');
  409. }
  410. // MODE_AS_IS
  411. return this.replaceYo(text, find, replace,
  412. '(?<![А-Яа-яЁё])',
  413. '(?![а-яё])');
  414. }
  415. }
  416.  
  417. // if it's a browser, not a test
  418. if ('undefined' !== typeof document) {
  419. let typograf = new Typograf();
  420. typograf.run(document.activeElement);
  421. }
  422.  
  423. // if it's a test by Node.js
  424. if (module) {
  425. module.exports = {
  426. Typograf: Typograf,
  427. };
  428. } else {
  429. var module; // hack for Tampermonkey's eslint
  430. }