Что будет реализовано
В статье рассматривается процесс разработки небольшого, но полностью автономного утилита hemingway-check. Инструмент работает в режиме командной строки, принимает файл в формате Markdown или простого текста и выводит:
- уровень читаемости по шкале Флеша и соответствующий «grade level»;
- количество наречий (слова, оканчивающиеся на
-ly); - список предложений, которые попадают в категории «hard» (grade ≥ 10) и «very hard» (grade ≥ 14);
- ненулевой код завершения (1), если уровень сложности превышает заданный порог – удобно для интеграции в CI‑pipeline.
Все функции реализованы в рамках одного файла TypeScript, а сборка происходит в директорию dist без дополнительных зависимостей.
Подготовка проекта
-
Создаём рабочую папку и инициализируем npm‑модуль
mkdir hemingway-check && cd hemingway-check npm init -y -
Устанавливаем необходимые пакеты
textlens– библиотека, предоставляющая функции расчёта читаемости и статистики текста.typescriptи типы для Node.js в качестве dev‑зависимостей.
npm install textlens npm install -D typescript @types/node -
Генерируем конфигурацию TypeScript
npx tsc --init \ --target es2020 \ --module nodenext \ --moduleResolution nodenext \ --outDir dist -
Создаём каталог исходного кода
mkdir src
Основной скрипт
Файл src/index.ts содержит всю бизнес‑логику. Ниже – пошаговое объяснение ключевых блоков.
Чтение входного файла
import { readFileSync } from 'fs';
import { readability, statistics } from 'textlens';
const file = process.argv[2];
if (!file) {
console.error('Usage: hemingway-check <file>');
process.exit(1);
}
const text = readFileSync(file, 'utf8');
CLI‑утилита ожидает единственный аргумент – путь к файлу. При отсутствии аргумента выводится сообщение об ошибке и процесс завершается с кодом 1.
Расчёт глобальных метрик
const scores = readability(text);
const stats = statistics(text);
Функция readability возвращает объект, включающий consensusGrade (уровень сложности) и fleschReadingEase. statistics предоставляет количество слов, предложений и другие показатели, которые пригодятся позже.
Разделение текста на предложения
const sentences = text
.replace(/\n/g, ' ')
.split(/(?<=[.!?])\s+/)
.filter(s => s.trim().length > 0);
Регулярное выражение учитывает точки, восклицательные и вопросительные знаки, а также сохраняет пробелы после них. Пустые строки отбрасываются.
Оценка сложности каждого предложения
let hard = 0;
let veryHard = 0;
const flagged: { sentence: string; grade: number; level: string }[] = [];
for (const sentence of sentences) {
if (sentence.split(/\s+/).length < 5) continue; // игнорируем короткие фрагменты
const r = readability(sentence);
const grade = r.consensusGrade;
if (grade >= 14) {
veryHard++;
flagged.push({ sentence: sentence.slice(0, 80), grade, level: 'VERY HARD' });
} else if (grade >= 10) {
hard++;
flagged.push({ sentence: sentence.slice(0, 80), grade, level: 'HARD' });
}
}
- Предложения менее пяти слов считаются «фрагментами» и пропускаются.
- Для каждого предложения рассчитывается отдельный
grade. - При
grade ≥ 14предложение попадает в категорию VERY HARD, иначе – HARD. В массивflaggedсохраняются первые 80 символов предложения, его оценка и уровень сложности.
Подсчёт наречий
const exceptions = new Set(['only', 'early', 'daily', 'likely']);
const adverbRegex = /\b\w+ly\b/gi;
let adverbCount = 0;
for (const match of text.matchAll(adverbRegex)) {
const word = match[0].toLowerCase();
if (!exceptions.has(word)) adverbCount++;
}
Регулярное выражение ищет любые слова, заканчивающиеся на ly. Список exceptions исключает часто‑встречающиеся слова, которые не являются стилистическими наречиями (например, only).
Форматированный вывод результатов
console.log(`Flesch Reading Ease: ${scores.fleschReadingEase.toFixed(2)}`);
console.log(`Consensus Grade: ${scores.consensusGrade}`);
console.log(`Adverbs (‑ly): ${adverbCount}`);
console.log(`Hard sentences (grade ≥10): ${hard}`);
console.log(`Very hard sentences (grade ≥14): ${veryHard}`);
if (flagged.length) {
console.log('\nFlagged sentences:');
flagged.forEach(item => {
console.log(`[${item.level}] (grade ${item.grade}) ${item.sentence}…`);
});
}
Вывод включает глобальные метрики, количество наречий и статистику по «трудным» предложениям. Если найдены флаги, они печатаются в отдельном блоке.
Завершение с кодом возврата
if (hard + veryHard > 0) {
// При наличии хотя бы одного сложного предложения считаем проверку неуспешной
process.exit(1);
}
process.exit(0);
Код 1 позволяет использовать утилиту в CI‑процессе: любой файл, содержащий предложения уровня сложности 10 и выше, будет приводить к падению сборки.
Сборка и запуск
npx tsc # компиляция TypeScript в ./dist
node dist/index.js path/to/document.md
Для удобства можно добавить скрипт в package.json:
"scripts": {
"check": "node dist/index.js"
}
Тогда проверка будет выглядеть так:
npm run check -- path/to/readme.md
Расширение функционала
- Пороговые настройки – добавить флаги
--hard-levelи--very-hard-level, позволяющие менять пороги оценки. - Поддержка JSON‑отчётов – выводить результаты в формате JSON для последующего парсинга.
- Интеграция с ESLint – реализовать плагин, который будет вызывать
hemingway-checkна каждом файле в процессе lint‑проверки. - Подсчёт пассивного залога – расширить
textlensили добавить собственные правила для поиска конструкций типа «was written».
Применение в CI/CD
Интеграция в пайплайн выглядит просто:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build checker
run: npm run build
- name: Run readability check
run: node dist/index.js docs/**/*.md
continue-on-error: false # провалит сборку при exit code 1
Таким образом, любой коммит, содержащий слишком сложный текст, будет автоматически отклонён, а разработчики получат подробный список проблемных предложений.
Выводы о практичности подхода
- Минимальный набор зависимостей –
textlensпокрывает всё, что требуется для расчётов читаемости, без необходимости подключать тяжёлые NLP‑библиотеки. - Лёгкая кастомизация – весь код находится в одном файле, что упрощает добавление новых правил.
- Универсальность – утилита работает с любыми текстовыми файлами, а не только с веб‑интерфейсом, благодаря чему её можно использовать в генерации документации, проверке README и даже в образовательных проектах.