Textarea Typograf

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

当前为 2022-04-30 提交的版本,查看 最新版本

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