Textarea Typograf

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

当前为 2022-08-11 提交的版本,查看 最新版本

  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-2022, Gleb Kemarsky (https://github.com/glebkema)
  7. // @license MIT
  8. // @version 0.6.17
  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_ENDING = 'anyEnding';
  25. MODE_ANY_ENDING_EXCEPT_L = 'anyEndingExceptL';
  26. MODE_ANY_EXCEPT_I = 'anyExceptI';
  27. MODE_ANY_EXCEPT_K = 'anyExceptK';
  28. MODE_ANY_EXCEPT_R = 'anyExceptR';
  29. MODE_AS_IS = 'asIs';
  30. MODE_EXCEPTIONS = 'exceptions';
  31. MODE_EXTRA_PREFIXES = 'extraPrefixes';
  32. MODE_NO_CAPITAL_LETTER = 'noCapitalLetter';
  33. MODE_NO_PREFIXES = 'noPrefixes';
  34. MODE_NO_SUFFIXES = 'noSuffixes';
  35. MODE_STANDARD = 'standard';
  36.  
  37. verbCores = {
  38. [this.MODE_EXCEPTIONS]: 'Льё,Мнё,Рвё,Трё',
  39. [this.MODE_EXTRA_PREFIXES]: 'Берё,Боднё,Вернё,Даё,Живё,Несё,Орё,Плывё,Поё,Ревё,Смеё,Стаё',
  40. [this.MODE_NO_CAPITAL_LETTER]: 'Йдё,Ймё',
  41. [this.MODE_NO_PREFIXES]: 'Идё,Начнё,Обернё,Придаё,Придё,Улыбнё',
  42. [this.MODE_NO_SUFFIXES]: 'берёг,Берёгся,Шёл', // NB: the first starts with a small letter to prevent of changing the form without prefixes
  43. [this.MODE_STANDARD]: 'Бережё,Блеснё,Блюдё,Блюё,Бьё,Ведё,Везё,Врё,Вьё,Гнё,Дерё,Ждё,Жмё,Жрё,Льнё,Прё,Пьё,Ткнё,Чтё,Шлё,Шьё',
  44. };
  45.  
  46. words = {
  47. [this.MODE_AS_IS]: 'Её,Ещё,Моё,Неё,Своё,Твоё,'
  48. + 'Вдвоём,Втроём,Объём,Остриём,Причём,Своём,Твоём,'
  49. + 'Грёза,Грёзы,Слёзы,'
  50. + 'Затёк,Натёк,Потёк,'
  51. + 'Василёк,Мотылёк,Огонёк,Пенёк,Поперёк,Ручеёк,'
  52. + 'Журавлём,Кораблём,Королём,Снегирём,Соловьём,'
  53. + 'Копьё,Копьём,'
  54. + 'Трёх,Четырём,Четырёх,' // "Трём" уже есть как глагол
  55. + 'Вперёд,'
  56. + 'Запёк,Предпочёл,Прочёл,'
  57. + 'Вперёд,'
  58. + 'Бёдер,Белёк,Бельём,Бобёр,Бобылём,'
  59. + 'Рулём',
  60.  
  61. [this.MODE_ANY]: 'ёхонек,ёхоньк,ёшенек,ёшеньк,'
  62. + 'ворённ,ретённ,'
  63. + 'творён,бретён,'
  64. + 'бомбёж,гиллёз,надёг,ощёк,счётн,уёмн,шёрстн,циллёз,ъёмкост,' // стёгивал,стёгнут,
  65. + 'Пролёт,Самолёт,'
  66. + 'Отчёт,Расчёт,'
  67. + 'Веретён,Гнёзд,Звёздн,Лёгочн,Лётчи,Надёжн,Налёт,Разъём,Съёмк,',
  68.  
  69. [this.MODE_ANY_BEGINNING]: 'атырёв,атырём,варём,'
  70. + 'арьё,арьём,ерьё,ерьём,ырьё,ырьём',
  71.  
  72. [this.MODE_ANY_ENDING]: 'Актёр,Алён,Алёх,Алёш,Алфёр,Аматёр,Амёб,Анкетёр,Антрепренёр,Артём,'
  73. + 'Бабёнк,Бабёф,Балансёр,Балдёж,Банкомёт,Баталёр,Бёдра,Бельёвщиц,Бережён,Берёз,Бесён,Бесслёзн,Бечёвк,Бечёво,Билетёр,Бирюлёв,Благословлён,Блёстк,Бобрён,Боксёр,Бородён,Боронён,Бочкарёв,'
  74. + 'Вёрстк,'
  75. + 'Ворьё,' // NB: ворьё,ворьём но подворье,подспорье
  76. + 'Запёкш,Запечён,Испечён,'
  77. + 'Лёгки,'
  78. + 'Партнёр,Проём,'
  79. + 'Расчёск,'
  80. + 'Чётк,'
  81. + 'Вертолёт,Звездолёт,Отлёт,Перелёт,Полёт,'
  82. + 'Заём,Наём,'
  83. + 'Зачёт,Звездочёт,Почёт,Счёт,Учёт',
  84.  
  85. [this.MODE_ANY_ENDING_EXCEPT_L]: 'Приём',
  86.  
  87.  
  88. [this.MODE_ANY_EXCEPT_I]: 'скажён',
  89.  
  90. [this.MODE_ANY_EXCEPT_K]: 'бъё',
  91.  
  92. [this.MODE_ANY_EXCEPT_R]: 'омёт',
  93. }
  94.  
  95. run(element) {
  96. if (element && 'textarea' === element.tagName.toLowerCase() && element.value) {
  97. const start = element.selectionStart;
  98. const end = element.selectionEnd;
  99. if (start === end) {
  100. element.value = this.improve(element.value);
  101. } else {
  102. const selected = element.value.substring(start, end);
  103. const theLength = element.value.length;
  104. element.value = element.value.substring(0, start)
  105. + this.improve(selected) + element.value.substring(end, theLength);
  106. }
  107. } else {
  108. // console.info('Start editing a non-empty textarea before calling the script');
  109. }
  110. }
  111.  
  112. improve(text) {
  113. if (text) {
  114. text = this.improveDash(text);
  115. text = this.improveQuotes(text);
  116. text = this.improveSmile(text);
  117. text = this.improveYo(text);
  118. }
  119. return text;
  120. }
  121.  
  122. improveDash(text) {
  123. text = text.replace(/ - /g, ' — ');
  124. return text;
  125. }
  126.  
  127. improveQuotes(text) {
  128. // use only one type + only external if two stand together
  129. // text = text.replace(/(?<=^|[(\s])["„“]/g, '«');
  130. // text = text.replace(/["„“](?=$|[.,;:!?)\s])/g, '»');
  131.  
  132. // use only one type
  133. text = text.replace(/["„“”](?=["„“”«]*[\wа-яё(])/gi, '«');
  134. text = text.replace(/(?<=[\wа-яё).!?]["„“”»]*)["„“”]/gi, '»');
  135.  
  136. // nested quotes
  137. // (?:«[^»]*)([«"])([^"»]*)(["»])
  138. // (?=(?:(?<!\w)["«](\w.*?)["»](?!\w))) https://stackoverflow.com/a/39706568/6263942
  139. // («([^«»]|(?R))*») https://stackoverflow.com/a/14952740/6263942
  140. // «((?>[^«»]+|(?R))*)» https://stackoverflow.com/a/26386070/6263942
  141. // «([^«»]*+(?:(?R)[^«»]*)*+)» https://stackoverflow.com/a/26386070/6263942
  142. // «[^»]*(?:(«)[^«»]*+(»)[^«]*)+»
  143. do {
  144. var old = text;
  145. text = text.replace(/(?<=«[^»]*)«(.*?)»/g, '„$1“');
  146. } while ( old !== text );
  147. return text;
  148. }
  149.  
  150. improveSmile(text) {
  151. // fix uncanonical smiles
  152. text = text.replace(/([:;])[—oо]?([D)(|])/g, '$1-$2');
  153.  
  154. // remove the dot before the smile
  155. text = text.replace(/(?<=[А-ЯЁа-яё])\.\s*(?=[:;]-[D)(|])/g, ' ');
  156.  
  157. return text;
  158. }
  159.  
  160. improveYo(text) {
  161. // verbs - cores
  162. for (let mode in this.verbCores) {
  163. text = this.improveverbCores(text, mode, this.verbCores[mode]);
  164. }
  165.  
  166. // verbs - unsystematic cases
  167. let lookBehind = '(?<![гж-нпру-я])'; // +абвдеост, -ы
  168. text = this.replaceYo(text, 'Дерг', 'Дёрг', lookBehind, '(?![б-яё])'); // +а, -у
  169. text = this.replaceYo(text, 'Дерн', 'Дёрн', lookBehind, '(?![б-джзй-нп-тф-ъь-яё])'); // +аеиоуы (сущ. или глагол)
  170.  
  171. lookBehind = '(?<![бвге-зй-ру-я])'; // +адист
  172. text = this.replaceYo(text, 'Стег', 'Стёг', lookBehind, '(?!ал|ать|ну)');
  173. text = this.replaceYo(text, 'Стегнут', 'Стёгнут', lookBehind, '(?!ь)'); // NB: расстёгнутый
  174.  
  175. // verbs - fix the exceptions
  176. text = this.replaceException(text, 'Раздольём');
  177. text = this.replaceException(text, 'Расстаёт', '(?![а-дж-я])');
  178. text = this.replaceException(text, 'Шлём');
  179.  
  180. // words
  181. for (let mode in this.words) {
  182. text = this.improveYoWord(text, mode, this.words[mode]);
  183. }
  184.  
  185. // words with a certain preposition
  186. text = this.improveYoWord(text, null, 'В моём,На моём,О моём');
  187. text = this.improveYoWord(text, null, 'Всё, на чём/Всё, о чём/Всё, про что/Всё, с чем/Всё, что/Всё-таки', '/');
  188.  
  189. return text;
  190. }
  191.  
  192. improveverbCores(text, mode, list, divider = ',') {
  193. return this.iterator(text, mode, list, divider, this.replaceverbCores.bind(this));
  194. }
  195.  
  196. improveYoWord(text, mode, list, divider = ',') {
  197. return this.iterator(text, mode, list, divider, this.replaceYoWord.bind(this));
  198. }
  199.  
  200. iterator(text, mode, list, divider, callback) {
  201. if ('string' === typeof list) {
  202. list = list.split(divider);
  203. }
  204. for (let i = 0; i < list.length; i++) {
  205. const replace = list[i].trim();
  206. if (replace) {
  207. const find = this.removeAllYo(replace);
  208. text = callback(text, mode, find, replace);
  209. }
  210. }
  211. return text;
  212. }
  213.  
  214. removeAllYo(text) {
  215. return text.replace(/ё/g, 'е').replace(/Ё/g, 'Е');
  216. }
  217.  
  218. // restore the `e` instead of `yo`
  219. replaceException(text, exception, lookAhead = '') {
  220. const replace = this.removeAllYo(exception);
  221. let regex = new RegExp(exception + lookAhead, 'g');
  222. text = text.replace(regex, replace);
  223. regex = new RegExp('(?<![А-Яa-я])' + exception.toLowerCase() + lookAhead, 'g');
  224. text = text.replace(regex, replace.toLowerCase());
  225. return text;
  226. }
  227.  
  228. replaceYo(text, find, replace,
  229. lookBehind = '(?<![б-джзй-нп-тф-я])', // +аеиоу
  230. // lookAhead = '(?=[мтш])'
  231. lookAhead = '(?=(?:м|мся|т|те|тесь|тся|шь|шься)(?:[^а-яё]|$))'
  232. ) {
  233. let regex;
  234. let findLowerCase = find.toLowerCase();
  235. // NB: \b doesn't work for russian words
  236. // 1) starts with a capital letter = just a begining of the word
  237. if (find !== findLowerCase) {
  238. regex = new RegExp(find + lookAhead, 'g');
  239. text = text.replace(regex, replace);
  240. }
  241. // 2) in lowercase = with a prefix ahead or without it
  242. regex = new RegExp(lookBehind + findLowerCase + lookAhead, 'g' + ('' === lookBehind ? '' : 'i'));
  243. text = text.replace(regex, replace.toLowerCase());
  244. return text;
  245. }
  246.  
  247. replaceverbCores(text, mode, find, replace) {
  248. if (this.MODE_EXCEPTIONS === mode) {
  249. return this.replaceYo(text, find, replace,
  250. '(?<![б-джзй-нп-тф-я]|зе|ко|фе)' ); // +аеиоу -"зельем" -"корвет" -"фельетон"
  251. // '(?=[мтш])(?!мо)(?!ть)'); // -"мнемо" -"треть"
  252. }
  253. if (this.MODE_EXTRA_PREFIXES === mode) {
  254. let lookBehind = '(?<![гжк-нпрф-я])'; // +аеиоу +бвдзст
  255. if ('Даё' === replace) {
  256. lookBehind = '(?<![гжик-нпрф-ъь-я]|па)'; // -и +ы >>> +'Придаёт', -"Попадает"
  257. } else if ('Стаё' === replace) {
  258. lookBehind = '(?<![гжк-нпрф-я]|ра)'; // -"вы/за/от/подрастает"
  259. }
  260. return this.replaceYo(text, find, replace, lookBehind);
  261. }
  262. if (this.MODE_NO_CAPITAL_LETTER === mode) {
  263. return this.replaceYo(text, find.toLowerCase(), replace);
  264. }
  265. if (this.MODE_NO_PREFIXES === mode) {
  266. return this.replaceYo(text, find, replace,
  267. '(?<![А-Яа-яЁё])');
  268. }
  269. if (this.MODE_NO_SUFFIXES === mode) {
  270. return this.replaceYo(text, find, replace,
  271. '(?<![б-джзй-нпртф-я])', // +аеиоу +с
  272. '(?![а-яё])');
  273. }
  274. // MODE_STANDARD
  275. return this.replaceYo(text, find, replace);
  276. }
  277.  
  278. replaceYoWord(text, mode, find, replace) {
  279. if (this.MODE_ANY === mode) {
  280. return this.replaceYo(text, find, replace,
  281. '',
  282. '');
  283. }
  284. if (this.MODE_ANY_BEGINNING === mode) {
  285. return this.replaceYo(text, find, replace,
  286. '',
  287. '(?![а-яё])');
  288. }
  289. if (this.MODE_ANY_ENDING === mode) {
  290. return this.replaceYo(text, find, replace,
  291. '(?<![А-Яа-яЁё])',
  292. '');
  293. }
  294. if (this.MODE_ANY_ENDING_EXCEPT_L === mode) {
  295. return this.replaceYo(text, find, replace,
  296. '(?<![А-Яа-яЁё])',
  297. '(?![л])');
  298. }
  299. if (this.MODE_ANY_EXCEPT_I === mode) {
  300. return this.replaceYo(text, find, replace,
  301. '',
  302. '(?![и])');
  303. }
  304. if (this.MODE_ANY_EXCEPT_K === mode) {
  305. return this.replaceYo(text, find, replace,
  306. '',
  307. '(?![к])');
  308. }
  309. if (this.MODE_ANY_EXCEPT_R === mode) {
  310. return this.replaceYo(text, find, replace,
  311. '',
  312. '(?![р])');
  313. }
  314. // MODE_AS_IS
  315. return this.replaceYo(text, find, replace,
  316. '(?<![А-Яа-яЁё])',
  317. '(?![а-яё])');
  318. }
  319. }
  320.  
  321. // if it's a browser, not a test
  322. if ('undefined' !== typeof document) {
  323. let typograf = new Typograf();
  324. typograf.run(document.activeElement);
  325. }
  326.  
  327. // if it's a test by Node.js
  328. if (module) {
  329. module.exports = {
  330. Typograf: Typograf,
  331. };
  332. } else {
  333. var module; // hack for Tampermonkey's eslint
  334. }