Something went wrong. Try again.
Central platform for European atproto.<cc> country community websites atproto.eu
community atproto
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122#!/usr/bin/env node// Advisory readability report for the handbook MDX. Nudges toward clear language for a// technical-but-mixed audience. It PRINTS numbers and never fails: exit code is always 0, so it is// safe as a non-blocking CI step (see .tangled/workflows/deploy.yml) and a local `npm run// check:readability`. No dependencies, Node built-ins only.//// What it flags per page:// - long sentences (> LONG_SENTENCE words) — the main clarity lever// - Flesch Reading Ease (higher = easier; ~50+ is comfortable for this audience)// - average words per sentence// Headings, code blocks, imports, and JSX are stripped before counting so only real prose is scored.
import { readdir, readFile } from 'node:fs/promises';
const DOCS_URL = new URL('../src/content/docs/', import.meta.url);const LONG_SENTENCE = 30; // words; a soft ceiling, not a ruleconst LOW_FLESCH = 45; // below this reads as dense for a general-technical audience
/** Strip MDX/markdown down to plain prose sentences (drops frontmatter, code, JSX, headings). */function toProse(raw) { let text = raw; text = text.replace(/^---\n[\s\S]*?\n---\n/, ''); // frontmatter text = text.replace(/```[\s\S]*?```/g, ''); // fenced code text = text.replace(/`[^`]*`/g, ''); // inline code const kept = []; for (const line of text.split('\n')) { const t = line.trim(); if (!t) continue; if (t.startsWith('#')) continue; // headings are not sentences if (t.startsWith('import ') || t.startsWith('export ')) continue; if (t.startsWith('<') || t.startsWith('/>') || t.startsWith('{')) continue; // JSX/expr kept.push(t); } return kept .join(' ') .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1') // [text](url) and  -> text/alt .replace(/[*_>#]+/g, ' ') // markdown emphasis / list / quote marks .replace(/^\s*[-\d.]+\s+/gm, ' ') .replace(/\s+/g, ' ') .trim();}
function splitSentences(prose) { return prose .split(/(?<=[.!?])\s+/) .map((s) => s.trim()) .filter((s) => /[a-z]/i.test(s));}
function words(s) { return s.split(/\s+/).filter((w) => /[a-z0-9]/i.test(w));}
/** Rough syllable count: vowel groups, minus common silent-e, floor of 1. Good enough for Flesch. */function syllables(word) { const w = word.toLowerCase().replace(/[^a-z]/g, ''); if (!w) return 0; const groups = w.match(/[aeiouy]+/g); let n = groups ? groups.length : 1; if (w.endsWith('e') && !/[aeiouy]e$/.test(w) && n > 1) n -= 1; return Math.max(1, n);}
function flesch(sentenceCount, wordCount, syllableCount) { if (!sentenceCount || !wordCount) return null; return 206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (syllableCount / wordCount);}
async function mdxFiles(dir, base = '') { const out = []; for (const ent of await readdir(dir, { withFileTypes: true })) { const rel = base ? `${base}/${ent.name}` : ent.name; if (ent.isDirectory()) out.push(...(await mdxFiles(new URL(`${ent.name}/`, dir), rel))); else if (ent.name.endsWith('.mdx') || ent.name.endsWith('.md')) out.push(rel); } return out;}
const files = (await mdxFiles(DOCS_URL)).sort();let totalLong = 0;const rows = [];
for (const rel of files) { const raw = await readFile(new URL(rel, DOCS_URL), 'utf8'); const prose = toProse(raw); const sentences = splitSentences(prose); const wordCount = sentences.reduce((n, s) => n + words(s).length, 0); const syllableCount = sentences.reduce( (n, s) => n + words(s).reduce((m, w) => m + syllables(w), 0), 0, ); const longOnes = sentences.filter((s) => words(s).length > LONG_SENTENCE); totalLong += longOnes.length; const avg = sentences.length ? Math.round(wordCount / sentences.length) : 0; const fre = flesch(sentences.length, wordCount, syllableCount); rows.push({ rel, avg, fre, long: longOnes.length, longest: longOnes });}
console.log('docs readability report (advisory, never fails the build)\n');for (const r of rows) { const freStr = r.fre === null ? ' n/a' : r.fre.toFixed(0).padStart(4); const flags = []; if (r.long) flags.push(`${r.long} long`); if (r.fre !== null && r.fre < LOW_FLESCH) flags.push('dense'); const tail = flags.length ? ` <- ${flags.join(', ')}` : ''; console.log(` ${r.rel.padEnd(34)} avg ${String(r.avg).padStart(2)} wps Flesch ${freStr}${tail}`);}console.log( `\nThresholds: sentence > ${LONG_SENTENCE} words = "long"; Flesch < ${LOW_FLESCH} = "dense". Higher Flesch is easier.`,);if (totalLong) { console.log(`\n${totalLong} long sentence(s) worth a second look:`); for (const r of rows) { for (const s of r.longest) { const w = words(s).length; console.log(` ${r.rel} (${w}w): ${s.slice(0, 100)}${s.length > 100 ? '...' : ''}`); } }}// Advisory: always succeed.process.exit(0);