Textarea Typograf

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

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

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