├── .npmignore ├── test ├── texts │ ├── example_with_yo.txt │ ├── example_without_yo.txt │ ├── win1251.txt │ └── unknow_charset ├── url.test.mjs └── file.test.mjs ├── images ├── logo.png └── screenshot.png ├── .gitignore ├── .gitattributes ├── jest.config.mjs ├── lib ├── exit-codes.mjs ├── symbols.mjs └── utils.mjs ├── eslint.config.mjs ├── .github └── workflows │ └── nodejs.yml ├── LICENSE ├── package.json ├── CHANGELOG.md ├── bin └── eyo.mjs └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | .* 3 | -------------------------------------------------------------------------------- /test/texts/example_with_yo.txt: -------------------------------------------------------------------------------- 1 | Елочка! 2 | -------------------------------------------------------------------------------- /test/texts/example_without_yo.txt: -------------------------------------------------------------------------------- 1 | Привет, мир! -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2yo/eyo/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2yo/eyo/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /test/texts/win1251.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2yo/eyo/HEAD/test/texts/win1251.txt -------------------------------------------------------------------------------- /test/texts/unknow_charset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2yo/eyo/HEAD/test/texts/unknow_charset -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tools/Yoficator.dic.dat 2 | node_modules/ 3 | coverage/ 4 | .nyc_output/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html eol=lf 2 | *.htm eol=lf 3 | *.xml eol=lf 4 | *.css eol=lf 5 | *.js eol=lf 6 | *.jst eol=lf 7 | *.json eol=lf 8 | *.md eol=lf 9 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | testEnvironment: 'node', 3 | moduleFileExtensions: ['mjs', 'js', 'json'], 4 | transform: {} 5 | }; 6 | -------------------------------------------------------------------------------- /lib/exit-codes.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | SUCCESS: 0, 3 | NOT_UTF8: 21, 4 | HAS_REPLACEMENT: 22, 5 | NO_SUCH_FILE: 23, 6 | UNKNOWN_CHARSET: 24, 7 | CANT_WRITE: 25 8 | }; 9 | -------------------------------------------------------------------------------- /lib/symbols.mjs: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const isWin = process.platform === 'win32'; 4 | export const okSymbol = isWin ? '[OK]' : '✓'; 5 | export const errorSymbol = isWin ? '[ERR]' : '✗'; 6 | export const warningSymbol = isWin ? '[!]' : '⚠'; 7 | export const diffColor = isWin ? chalk.underline : chalk.bold; 8 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | 4 | const keys = { 5 | ...globals.jest, 6 | ...globals.node 7 | }; 8 | 9 | export default [ 10 | { 11 | ignores: [ 12 | '.*', 13 | 'dist/**', 14 | 'node_modules/**', 15 | '*.config.js' 16 | ] 17 | }, 18 | { 19 | files: ['**/*.{js,mjs,cjs,ts}'] 20 | }, 21 | { 22 | languageOptions: { globals: keys } 23 | }, 24 | pluginJs.configs.recommended, 25 | ]; -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [25.x] 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: npm install, build, and test 18 | run: | 19 | npm ci 20 | npm test 21 | env: 22 | CI: true 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Denis Seleznev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eyo", 3 | "description": "CLI for restoring the letter «ё» (yo) in russian texts", 4 | "version": "6.0.1", 5 | "author": { 6 | "name": "Denis Seleznev", 7 | "email": "hcodes@yandex.ru", 8 | "url": "https://github.com/e2yo/eyo" 9 | }, 10 | "type": "module", 11 | "bin": { 12 | "eyo": "./bin/eyo.mjs" 13 | }, 14 | "homepage": "https://github.com/e2yo/eyo", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/e2yo/eyo.git" 19 | }, 20 | "keywords": [ 21 | "yo", 22 | "yoficator", 23 | "ёфикатор", 24 | "ёфикация" 25 | ], 26 | "engines": { 27 | "node": ">= 18.0" 28 | }, 29 | "dependencies": { 30 | "chalk": "^5.6.2", 31 | "commander": "^14.0.2", 32 | "eyo-kernel": "^4.0.1", 33 | "glob": "^12.0.0", 34 | "isutf8": "^4.0.1" 35 | }, 36 | "devDependencies": { 37 | "eslint": "^9.39.1", 38 | "jest": "^30.2.0" 39 | }, 40 | "files": [ 41 | "bin", 42 | "lib", 43 | "README.md", 44 | "CHANGELOG.md", 45 | "LICENSE" 46 | ], 47 | "scripts": { 48 | "test": "npm run-script eslint && npm run-script unit-test", 49 | "eslint": "eslint .", 50 | "unit-test": "NODE_OPTIONS=--experimental-vm-modules jest ." 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/url.test.mjs: -------------------------------------------------------------------------------- 1 | import { jest, describe, expect, beforeEach, afterEach, afterAll } from '@jest/globals'; 2 | import { program } from 'commander'; 3 | import { processUrl } from '../lib/utils.mjs'; 4 | import exitCodes from '../lib/exit-codes.mjs'; 5 | 6 | jest.setTimeout(15000); 7 | 8 | describe('processUrl', function() { 9 | const oldExitCode = process.exitCode; 10 | 11 | const spy = jest.spyOn(program, 'opts').mockImplementation(() => ({ lint: true })); 12 | 13 | afterAll(() => { 14 | spy.mockRestore(); 15 | }); 16 | 17 | beforeEach(() => { 18 | process.exitCode = oldExitCode; 19 | }); 20 | 21 | afterEach(() => { 22 | process.exitCode = oldExitCode; 23 | }); 24 | 25 | it('should set exit code ERROR_LOADING', done => { 26 | processUrl('https://raw.githubusercontent.com/hcodes/utils/master/test/texts/unknown.txt').then(() => { 27 | expect(process.exitCode).toEqual(exitCodes.ERROR_LOADING); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('should set exit code HAS_REPLACEMENT', done => { 33 | processUrl('https://raw.githubusercontent.com/hcodes/eyo/master/test/texts/example_with_yo.txt').then(() => { 34 | expect(process.exitCode).toEqual(exitCodes.HAS_REPLACEMENT); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should not set exit code', done => { 40 | processUrl('https://raw.githubusercontent.com/hcodes/eyo/master/test/texts/example_without_yo.txt').then(() => { 41 | expect(process.exitCode).toEqual(oldExitCode); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/file.test.mjs: -------------------------------------------------------------------------------- 1 | import { jest, describe, expect, beforeEach, afterEach, afterAll } from '@jest/globals'; 2 | import { program } from 'commander'; 3 | import { processFile } from '../lib/utils.mjs'; 4 | import exitCodes from '../lib/exit-codes.mjs'; 5 | 6 | jest.setTimeout(15000); 7 | 8 | describe('processFile', function() { 9 | const oldExitCode = process.exitCode; 10 | 11 | const spy = jest.spyOn(program, 'opts').mockImplementation(() => ({ lint: true })); 12 | 13 | afterAll(() => { 14 | spy.mockRestore(); 15 | }); 16 | 17 | beforeEach(function() { 18 | process.exitCode = oldExitCode; 19 | }); 20 | 21 | afterEach(function() { 22 | process.exitCode = oldExitCode; 23 | }); 24 | 25 | it('should set exit code NO_SUCH_FILE', done => { 26 | processFile('unknown.txt').then(() => { 27 | expect(process.exitCode).toEqual(exitCodes.NO_SUCH_FILE); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('should set exit code NOT_UTF8', done => { 33 | processFile('test/texts/win1251.txt').then(() => { 34 | expect(process.exitCode).toEqual(exitCodes.NOT_UTF8); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should set exit code HAS_REPLACEMENT', done => { 40 | processFile('test/texts/example_with_yo.txt').then(() => { 41 | expect(process.exitCode).toEqual(exitCodes.HAS_REPLACEMENT); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should not set exit code', done => { 47 | processFile('test/texts/example_without_yo.txt').then(() => { 48 | expect(process.exitCode).toEqual(oldExitCode); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | 54 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v6.0.1 4 | Мелкий фикс. 5 | 6 | ## v6.0.0 7 | - Из зависимостей удалены пакеты `node-fetch` и `iconv-lite`. Ёфикатор теперь обрабатывает только файлы с кодировкой `UTF-8`. 8 | - Пакет переведён на ESM. 9 | - Удалена поддержка старых версий Node.js < 18. 10 | - Обновление версии зависимостей. 11 | - Мелкие доработки. 12 | 13 | ## v5.0.0 14 | - Удалена поддержка старых версий Node.js < 10. 15 | - Ошибки стали выводится через `console.error`. 16 | - CLI-опция `--only-safe` теперь не выводит проверку успешных ресурсов. 17 | - Безопасные замены (явные ошибки) теперь выводятся красным цветом, небезопасные замены (предупреждения) - жёлтым. 18 | - Обновлены зависимости в `package.json`. 19 | - Пакет `request` заменён на `node-fetch`. 20 | 21 | ## v4.2.2 22 | Обновлены зависимости в package.json. 23 | 24 | ## v4.2.1 25 | Обновлены зависимости в package.json. 26 | 27 | ## v4.2.0 28 | - Поддержка glob-аргументов для Windows, `eyo "*.md"`. 29 | - Добавлен параметр `--in-place`, в файлах производится ёфикация и перезапись, `eyo -i "*.md"` #15, #21 30 | 31 | ## v4.1.1 32 | Обновлены зависимости в package.json. 33 | 34 | ## v4.1.0 35 | - Вывод только безопасных замен с помощью CLI-параметра `--only-safe` вместе с `--lint`. 36 | - Обновлены зависимости в package.json. 37 | 38 | ## v4.0.1 39 | Обновлены зависимости в package.json. 40 | 41 | ## v4.0.0 42 | - Теперь для работы с STDIN необходимо явно указывать опцию `--stdin`. 43 | - Обновлены зависимости в package.json. 44 | 45 | ## v3.1.1 46 | - Обновлены зависимости в package.json. 47 | 48 | ## v3.1.0 49 | - Обновлены зависимости в package.json. 50 | - Перевод пакета `eyo-kernel` на API v2. 51 | 52 | ## v3.0.2 53 | - Мелкие правки в README.md 54 | 55 | ## v3.0.1 56 | - Добавлен отсутствующий модуль `exit` в package.json #10 57 | 58 | ## v3.0.0 59 | - Удалён параметр `--show-position`, номер строки и колонки показывается всегда 60 | - Удалена поддержка старых версий Node.js 61 | - Исправлена ошибка, связанная с обрезанием длинных файлов #9 62 | - Обновлены зависимости в package.json 63 | 64 | ## v2.0.2 65 | - Обновлены зависимости в package.json 66 | 67 | ## v2.0.1 68 | - Обновлены зависимости в package.json 69 | 70 | ## v2.0.0 71 | - Ядро вынесено в отдельный пакет `eyo-kernel` #7 72 | 73 | ## v1.2.0 74 | - Выводить номер строки и номер столбца при проверке #3 75 | - Обновлены зависимости в package.json 76 | 77 | ## v1.1.0 78 | - Возможность указывать несколько файлов при проверке #1 79 | - Обновлены зависимости в package.json 80 | 81 | ## v1.0.0 82 | -------------------------------------------------------------------------------- /bin/eyo.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import chalk from'chalk'; 4 | import { program } from 'commander'; 5 | import { 6 | expandGlobArgs, 7 | isUrl, 8 | processUrl, 9 | processFile, 10 | processText, 11 | } from '../lib/utils.mjs'; 12 | 13 | import { createRequire } from 'node:module'; 14 | const require = createRequire(import.meta.url); 15 | const { version } = require('../package.json'); 16 | 17 | program.configureHelp({ 18 | styleTitle: (str) => chalk.bold(str), 19 | styleCommandText: (str) => chalk.cyan(str), 20 | styleCommandDescription: (str) => chalk.magenta(str), 21 | styleDescriptionText: (str) => chalk.italic(str), 22 | styleOptionText: (str) => chalk.green(str), 23 | styleArgumentText: (str) => chalk.yellow(str), 24 | styleSubcommandText: (str) => chalk.blue(str), 25 | }); 26 | 27 | program 28 | .name('eyo') 29 | .version(version, '-V, --version', 'Output the version number.') 30 | .helpOption('-h, --help', 'Display help for command.') 31 | .usage('[options] \n\n Restoring the letter “ё” (yo) in russian texts.') 32 | .argument('[file-or-url...]', 'Files or URLs to process.') 33 | .option('-l, --lint', 'Search of safe and unsafe replacements.') 34 | .option('-i, --in-place', 'Write files in place.') 35 | .option('-s, --sort', 'Sorting results by words with grouping.') 36 | .option('--only-safe', 'Output only safe replacements.') 37 | .option('--no-colors', 'Clean output without colors.') 38 | .option('--stdin', 'Process text provided on .') 39 | .option('--stdin-filename ', 'Specify filename to process STDIN as.') 40 | .action((files, options) => { 41 | chalk.enabled = options.colors; 42 | 43 | if (!options.stdin && !process.argv.slice(2).length) { 44 | program.help(); 45 | } 46 | 47 | if (options.stdin) { 48 | let text = ''; 49 | 50 | process.stdin 51 | .setEncoding('utf8') 52 | .on('readable', () => { 53 | const chunk = process.stdin.read(); 54 | if (chunk !== null) { 55 | text += chunk; 56 | } 57 | }) 58 | .on('end', () => { 59 | processText(text, options.stdinFilename || 'stdin'); 60 | process.exit(process.exitCode); 61 | }); 62 | } else { 63 | Promise.all(expandGlobArgs(program.args).map(resource => 64 | isUrl(resource) ? 65 | processUrl(resource) : 66 | processFile(resource) 67 | )).then(() => { 68 | process.exit(process.exitCode); 69 | }).catch((e) => { 70 | console.error(chalk.red(e)); 71 | process.exit(process.exitCode); 72 | }); 73 | } 74 | }) 75 | .parse(process.argv); 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Восстановление буквы «ё» в русских текстах 2 | === 3 | [![NPM version](https://img.shields.io/npm/v/eyo.svg?style=flat)](https://www.npmjs.com/package/eyo) 4 | [![NPM downloads](https://img.shields.io/npm/dm/eyo.svg?style=flat)](https://www.npmjs.com/package/eyo) 5 | [![install size](https://packagephobia.com/badge?p=eyo)](https://packagephobia.com/result?p=eyo) 6 | 7 | 8 | ## Особенности 9 | eyo 10 | 11 | + проверка и восстановление буквы «ё» в русских текстах, вместо написанной «е»; 12 | + замена «е» на «ё» только в бесспорных случаях; 13 | + исправление в словах нескольких букв «е», «ё»; 14 | + корректная обработка сокращений («мед. училище», но не «мёд. училище»); 15 | + аббревиатуры не обрабатываются. 16 | 17 | ![eyo](https://raw.githubusercontent.com/hcodes/eyo/master/images/screenshot.png) 18 | 19 | 20 | ## Установка 21 | `npm install eyo -g` 22 | 23 | ## Командная строка 24 | ``` 25 | Usage: eyo [options] 26 | Restoring the letter «ё» (yo) in russian texts. 27 | 28 | Options: 29 | -h, --help Output usage information 30 | -V, --version Output the version number 31 | -l, --lint Search of safe and unsafe replacements 32 | -i, --in-place Write files in place. 33 | -s, --sort Sort results 34 | --only-safe Output only safe replacements 35 | --stdin Process text provided on 36 | --stdin-filename Specify filename to process STDIN as 37 | --no-colors Clean output without colors 38 | ``` 39 | 40 | ### Примеры использования 41 | `eyo file.txt > file.out.txt` — безопасная замена «е» на «ё». 42 | `eyo https://example.com/index.html > file.out.html` — безопасная замена «е» на «ё» на странице сайта. 43 | 44 | `eyo -i README.md` — файл `README.md` будет перезаписан с безопасной заменой «е» на «ё». 45 | `eyo -i "**/*.md"` — файлы с расширением `.md` будут перезаписаны с безопасной заменой «е» на «ё». 46 | `find . -name "*.md" | xargs eyo --lint` 47 | `eyo --lint file1.txt file2.txt` — вывод слов для файлов, где необходима или возможна замена. 48 | `eyo --lint http://habrahabr.ru` — вывод слов для страницы сайта, где необходима или возможна замена. 49 | 50 | `cat file1.txt file2.txt file3.txt | eyo --stdin > output.txt` 51 | `cat file1.txt | eyo --stdin --stdin-filename file1.txt` 52 | 53 | ## Node.js 54 | 55 | Используйте отдельный пакет [`eyo-kernel`](https://www.npmjs.com/package/eyo-kernel) без зависимостей. 56 | 57 | `npm install eyo-kernel` 58 | 59 | ## Ссылки 60 | + [Ёфикация в браузере](https://e2yo.github.io/eyo-browser/) 61 | + [eyo-kernel](https://www.npmjs.com/package/eyo-kernel) — отдельный пакет для ёфикации без зависимостей 62 | + [https://ru.wikipedia.org/wiki/Ёфикатор](https://ru.wikipedia.org/wiki/%D0%81%D1%84%D0%B8%D0%BA%D0%B0%D1%82%D0%BE%D1%80) 63 | + [Про букву ё](http://www.gramota.ru/class/istiny/istiny_7_jo/) 64 | + [Поиск опечаток в тексте](https://github.com/hcodes/yaspeller) 65 | 66 | ## [Лицензия](./LICENSE) 67 | MIT License 68 | -------------------------------------------------------------------------------- /lib/utils.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | import chalk from 'chalk'; 4 | import { glob } from 'glob'; 5 | import isutf8 from 'isutf8'; 6 | import { program } from 'commander'; 7 | import { Eyo, notSafeDictionary, safeDictionary } from 'eyo-kernel'; 8 | 9 | import exitCodes from './exit-codes.mjs'; 10 | import { 11 | diffColor, 12 | okSymbol, 13 | errorSymbol, 14 | warningSymbol, 15 | } from './symbols.mjs'; 16 | 17 | const safeEyo = new Eyo(); 18 | safeEyo.dictionary.set(safeDictionary) 19 | 20 | const notSafeEyo = new Eyo(); 21 | notSafeEyo.dictionary.set(notSafeDictionary); 22 | 23 | export function printItem(item, i, isError) { 24 | const before = item.before; 25 | const after = item.after; 26 | const newBefore = []; 27 | const newAfter = []; 28 | const info = []; 29 | const pos = Array.isArray(item.position) ? item.position[0] : item.position; 30 | 31 | // Diff by letters 32 | for (let n = 0; n < before.length; n++) { 33 | if (before[n] !== after[n]) { 34 | newBefore[n] = diffColor(before[n]); 35 | newAfter[n] = diffColor(after[n]); 36 | } else { 37 | newBefore[n] = before[n]; 38 | newAfter[n] = after[n]; 39 | } 40 | } 41 | 42 | info.push(pos.line + ':' + pos.column); 43 | 44 | if (Array.isArray(item.position) && item.position.length > 1) { 45 | info.push('count: ' + item.position.length); 46 | } 47 | 48 | const text = 49 | (i + 1) + '. ' + 50 | newBefore.join('') + ' → ' + 51 | newAfter.join('') + 52 | (info.length ? ' (' + info.join(', ') + ')' : ''); 53 | 54 | if (isError) { 55 | console.error(text); 56 | } else { 57 | console.warn(text); 58 | } 59 | } 60 | 61 | export function printErrorItem(item, i) { 62 | printItem(item, i, true); 63 | } 64 | 65 | export function printWarningItem(item, i) { 66 | printItem(item, i, false); 67 | } 68 | 69 | /** 70 | * Это ссылка? 71 | * 72 | * @param {string} path 73 | * @returns {boolean} 74 | */ 75 | export function isUrl(path) { 76 | return path.search(/^https?:/) > -1; 77 | } 78 | 79 | /** 80 | * Развернуть glob-аргументы. 81 | * 82 | * @param {string[]} args 83 | * @returns {string[]} 84 | */ 85 | export function expandGlobArgs(args) { 86 | let result = []; 87 | 88 | for (const value of args) { 89 | if (isUrl(value)) { 90 | result.push(value); 91 | } else { 92 | const files = glob.sync(value); 93 | if (files) { 94 | result = result.concat(files); 95 | } 96 | } 97 | } 98 | 99 | return result; 100 | } 101 | 102 | /** 103 | * Ёфицировать текст и вывести в консоль. 104 | * 105 | * @param {string} text 106 | * @param {string} resource 107 | */ 108 | export function processText(text, resource) { 109 | const opts = program.opts(); 110 | if (opts.lint) { 111 | lintText(text, resource); 112 | } else { 113 | if (opts.inPlace) { 114 | try { 115 | const result = safeEyo.restore(text); 116 | fs.writeFileSync(resource, result); 117 | } catch(e) { 118 | process.exitCode = exitCodes.CANT_WRITE; 119 | console.error(e); 120 | } 121 | } else { 122 | process.stdout.write(safeEyo.restore(text)); 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * Проверка текста. 129 | * 130 | * @param {string} text 131 | * @param {string} resource 132 | */ 133 | export function lintText(text, resource) { 134 | const opts = program.opts(); 135 | const safeReplacement = safeEyo.lint(text, opts.sort); 136 | let notSafeReplacement = []; 137 | 138 | if (!opts.onlySafe) { 139 | notSafeReplacement = notSafeEyo.lint(text, opts.sort); 140 | } 141 | 142 | if (safeReplacement.length) { 143 | console.error(chalk.red(errorSymbol) + ' ' + resource); 144 | } else if (notSafeReplacement.length) { 145 | console.warn(chalk.yellow(warningSymbol) + ' ' + resource); 146 | } else if (!opts.onlySafe) { 147 | console.log(chalk.green(okSymbol) + ' ' + resource); 148 | } 149 | 150 | if (safeReplacement.length) { 151 | console.error(chalk.red(`Safe replacements: ${safeReplacement.length}`)); 152 | safeReplacement.forEach(printErrorItem); 153 | console.error(chalk.red('---')); 154 | 155 | if (!process.exitCode) { 156 | process.exitCode = exitCodes.HAS_REPLACEMENT; 157 | } 158 | } 159 | 160 | if (notSafeReplacement.length) { 161 | console.warn(chalk.yellow(`Not safe replacements: ${notSafeReplacement.length}`)); 162 | notSafeReplacement.forEach(printWarningItem); 163 | console.warn(chalk.yellow('---')); 164 | } 165 | } 166 | 167 | /** 168 | * Ёфицировать файл. 169 | * 170 | * @param {string} file 171 | * 172 | * @returns {Promise} 173 | */ 174 | export function processFile(file) { 175 | return new Promise((resolve, reject) => { 176 | if (isFile(file)) { 177 | fs.readFile(file, (error, buffer) => { 178 | if (error) { 179 | reject(error); 180 | return; 181 | } 182 | 183 | if (isutf8(buffer)) { 184 | processText(buffer.toString('utf8'), file); 185 | } else { 186 | console.error(chalk.red(file + ': is not UTF-8.')); 187 | process.exitCode = exitCodes.NOT_UTF8; 188 | } 189 | 190 | resolve(); 191 | }); 192 | } else { 193 | console.error(chalk.red(file + ': no such file.')); 194 | process.exitCode = exitCodes.NO_SUCH_FILE; 195 | 196 | resolve(); 197 | } 198 | }); 199 | } 200 | 201 | /** 202 | * Это файл? 203 | * 204 | * @param {string} file 205 | * @returns {boolean} 206 | */ 207 | export function isFile(file) { 208 | return fs.existsSync(file) && fs.statSync(file).isFile(); 209 | } 210 | 211 | /** 212 | * Ёфицировать страницу. 213 | * 214 | * @param {string} url 215 | */ 216 | export async function processUrl(url) { 217 | try { 218 | const response = await fetch(url); 219 | const arrayBuffer = await response.arrayBuffer(); 220 | const buffer = Buffer.from(arrayBuffer); 221 | 222 | if (response.status !== 200) { 223 | console.log(chalk.red(`${url}: returns status code is ${response.status}.`)); 224 | process.exitCode = exitCodes.ERROR_LOADING; 225 | 226 | return; 227 | } 228 | 229 | if (isutf8(buffer)) { 230 | processText(buffer.toString('utf8'), url); 231 | } else { 232 | console.error('Erroor: is unknown charset.'); 233 | process.exitCode = exitCodes.UNKNOWN_CHARSET; 234 | } 235 | } catch(error) { 236 | console.log(chalk.red(error)); 237 | process.exitCode = exitCodes.ERROR_LOADING; 238 | } 239 | } 240 | --------------------------------------------------------------------------------