DEV Community

Cover image for Build a Hemingway Editor Clone with TypeScript in 50 Lines
ckmtools
ckmtools

Posted on

Build a Hemingway Editor Clone with TypeScript in 50 Lines

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:

  1. Reads a markdown or text file
  2. Reports the grade level and Flesch Reading Ease score
  3. Flags hard-to-read sentences (grade 10+)
  4. Flags very hard sentences (grade 14+)
  5. Counts adverbs
  6. 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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 */ }
}
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode
npx tsc
node dist/index.js README.md
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

This is part of the textlens series — tutorials on text analysis in JavaScript and TypeScript.

Top comments (0)