The Hemingway Editor highlights hard-to-read sentences, flags adverbs, and shows your text's grade level. It costs $20 and doesn't have an API. You can't plug it into a build step, run it against your docs folder, or automate it in any way.
Let's build a readability checker for developers that does what Hemingway does — from the command line, in about 50 lines of TypeScript.
What We're Building
A CLI tool called hemingway-check that:
- Reads a markdown or text file
- Reports the grade level and Flesch Reading Ease score
- Flags hard-to-read sentences (grade 10+)
- Flags very hard sentences (grade 14+)
- Counts adverbs
- Exits with code 1 if the text is too complex (for CI use)
Setup
mkdir hemingway-check && cd hemingway-check
npm init -y
npm install textlens
npm install -D typescript @types/node
npx tsc --init --target es2020 --module nodenext --moduleResolution nodenext --outDir dist
The Full Script
Create 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');
const scores = readability(text);
const stats = statistics(text);
// Split into sentences and score each one
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; // skip short fragments
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' });
}
}
// Count adverbs (words ending in -ly, with common exceptions)
const exceptions = new Set(['only', 'early', 'daily', 'likely', 'family', 'apply', 'supply', 'reply', 'holy', 'july', 'fly', 'rely', 'ally', 'italy', 'belly', 'jelly', 'bully', 'ugly']);
const words = text.toLowerCase().match(/\b\w+\b/g) || [];
const adverbs = words.filter(w => w.endsWith('ly') && w.length > 3 && !exceptions.has(w));
const adverbTarget = Math.max(1, Math.floor(stats.sentences * 0.3));
// Report
console.log(`\n Hemingway Check: ${file}\n`);
console.log(` Grade Level: ${scores.consensusGrade.toFixed(1)}`);
console.log(` Flesch Ease: ${scores.fleschReadingEase.score.toFixed(1)} (${scores.fleschReadingEase.interpretation})`);
console.log(` Words: ${stats.words}`);
console.log(` Sentences: ${stats.sentences}`);
console.log(` Avg sentence: ${stats.avgSentenceLength.toFixed(1)} words`);
console.log(` Hard sentences: ${hard}`);
console.log(` Very hard: ${veryHard}`);
console.log(` Adverbs: ${adverbs.length} (target: <${adverbTarget})`);
if (flagged.length > 0) {
console.log('\n Flagged sentences:\n');
for (const f of flagged) {
const color = f.level === 'VERY HARD' ? '\x1b[31m' : '\x1b[33m';
console.log(` ${color}[${f.level}]\x1b[0m Grade ${f.grade.toFixed(1)}: "${f.sentence}..."`);
}
}
const pass = scores.consensusGrade <= 10 && veryHard === 0;
console.log(`\n Result: ${pass ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'}\n`);
process.exit(pass ? 0 : 1);
That's 50 lines of logic (excluding blank lines and the shebang). Let's break down the key parts.
How It Works
Grade-level scoring
const scores = readability(text);
console.log(scores.consensusGrade); // e.g., 7.2
textlens runs 8 readability formulas (Flesch-Kincaid, Coleman-Liau, Gunning Fog, SMOG, ARI, Dale-Chall, Linsear Write, and Flesch Reading Ease) and averages the grade-level results into consensusGrade. One function call gives you a number backed by seven formulas.
Sentence-level analysis
The Hemingway Editor highlights individual sentences. We do the same by splitting the text on sentence boundaries and scoring each one:
const sentences = text.split(/(?<=[.!?])\s+/);
for (const sentence of sentences) {
const r = readability(sentence);
if (r.consensusGrade >= 14) { /* very hard */ }
else if (r.consensusGrade >= 10) { /* hard */ }
}
Sentences at grade 10+ are "hard to read." Grade 14+ is "very hard." These thresholds match what the Hemingway Editor uses internally.
Adverb detection
A simple but effective heuristic: words ending in "-ly" that aren't in an exception list. This catches "quickly," "extremely," "very" but skips "family," "apply," "only."
const adverbs = words.filter(w =>
w.endsWith('ly') && w.length > 3 && !exceptions.has(w)
);
Professional editors recommend keeping adverbs under 30% of your sentence count. The tool calculates this target automatically.
Running It
Add to package.json:
{
"scripts": {
"build": "tsc",
"check": "node dist/index.js"
},
"bin": {
"hemingway-check": "./dist/index.js"
}
}
npx tsc
node dist/index.js README.md
Sample output:
Hemingway Check: README.md
Grade Level: 6.2
Flesch Ease: 68.4 (Standard / average)
Words: 342
Sentences: 28
Avg sentence: 12.2 words
Hard sentences: 3
Very hard: 0
Adverbs: 2 (target: <8)
[HARD] Grade 11.2: "The formula penalizes complex words with three or more syllables..."
Result: PASS
Adding It to CI
Add a prepublish script or a GitHub Actions step:
- name: Readability check
run: |
npm install textlens
npx tsc
node dist/index.js docs/guide.md
The script exits with code 1 when it fails, so it blocks merges automatically.
How This Compares to the Real Hemingway Editor
| Feature | Hemingway Editor | hemingway-check |
|---|---|---|
| Grade level | Yes | Yes (8 formulas + consensus) |
| Hard sentence highlighting | Yes (color-coded) | Yes (terminal colors) |
| Adverb detection | Yes | Yes (heuristic) |
| Passive voice | Yes | Not yet |
| Simpler alternatives | Yes | Not yet |
| Works in CI | No | Yes |
| API/programmatic use | No | Yes (textlens library) |
| Price | $20 | Free (MIT) |
The Hemingway Editor has a better UI and catches passive voice. Our CLI version works in automation pipelines and costs nothing.
Extending It
Want to add passive voice detection? Sentence simplification suggestions? The textlens analyze() function gives you everything you need as a foundation:
const { analyze } = require('textlens');
const result = analyze(text);
// result.readability — all 8 formulas
// result.sentiment — is the tone appropriate?
// result.keywords — what's the text about?
// result.statistics — word/sentence/syllable counts
You could build a full writing assistant with readability scoring, tone checking, keyword analysis, and SEO optimization — all from one zero-dependency package.
Source Code
The complete source for this tutorial is about 50 lines. No framework, no build tool beyond TypeScript itself.
-
textlens on npm —
npm install textlens - GitHub: ckmtools/textlens
This is part of the textlens series — tutorials on text analysis in JavaScript and TypeScript.
Top comments (0)