Textarea Typograf

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

当前为 2021-11-23 提交的版本,查看 最新版本

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