Textarea Typograf

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

当前为 2021-10-31 提交的版本,查看 最新版本

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