@alexneri/readability-ts
Version:
A CLI app that runs the Flesch-kincaid readability score recursively on all *.adoc files in the current directory.
192 lines (191 loc) • 9.16 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const perf_hooks_1 = require("perf_hooks");
// Flesch-Kincaid readability score calculation
function fleschKincaid(text) {
const sentences = text.split(/[.!?]+/).filter(Boolean).length;
const words = text.split(/\s+/).filter(Boolean).length;
const syllables = text.split(/\s+/).reduce((acc, word) => acc + countSyllables(word), 0);
const score = 206.835 - 1.015 * (words / sentences) - 84.6 * (syllables / words);
return Math.max(0, Math.min(100, score)); // Clamp score between 0 and 100
}
// Helper function to count syllables in a word
function countSyllables(word) {
word = word.toLowerCase();
if (word.length <= 3)
return 1; // Treat short words as having one syllable
const vowels = ['a', 'e', 'i', 'o', 'u', 'y'];
let syllableCount = 0;
let prevCharWasVowel = false;
for (const char of word) {
const isVowel = vowels.includes(char);
if (isVowel && !prevCharWasVowel) {
syllableCount++;
}
prevCharWasVowel = isVowel;
}
// Remove silent 'e'
if (word.endsWith('e'))
syllableCount--;
return Math.max(1, syllableCount); // Ensure at least one syllable
}
// Rating system based on Flesch-Kincaid score
function getRating(score) {
if (score >= 100)
return '5th grade level - Very easy to read. Easily understood by an average 11-year-old student.';
if (score >= 90)
return '6th grade level - Very easy to read. Easily understood by an average 11-year-old student.';
if (score >= 80)
return '7th grade level - Fairly easy to read.';
if (score >= 70)
return '8th & 9th grade - Plain English. Easily understood by 13- to 15-year-old students.';
if (score >= 60)
return '10th - 12th grade - Fairly difficult to read.';
if (score >= 50)
return 'College - Difficult to read.';
if (score >= 30)
return 'College grad - Very difficult to read. Best understood by university graduates.';
return 'Professional - Extremely difficult to read. Best understood by university graduates.';
}
// Remove AsciiDoc/Markdown tags and code block formatting
function cleanContent(content) {
const hasCodeBlock = /```[\s\S]*?```|----/.test(content); // Detect code blocks
const cleanedText = content.replace(/(```[\s\S]*?```|==+|--+|\.\.\.\.|\[.*?\])/g, '').trim();
return { cleanedText, hasCodeBlock };
}
// Count acronyms in the text
function countAcronyms(text) {
const acronymRegex = /\b[A-Z]{2,}\b/g; // Matches words with all uppercase letters of length >=2
const matches = text.match(acronymRegex);
return matches ? matches.length : 0;
}
// Recursively scan directory for .adoc and .md files
function scanDirectory(dirPath) {
return __awaiter(this, void 0, void 0, function* () {
let filesToProcess = [];
const files = yield fs.readdir(dirPath, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dirPath, file.name);
if (file.isDirectory()) {
filesToProcess = filesToProcess.concat(yield scanDirectory(fullPath));
}
else if (file.isFile() &&
(file.name.endsWith('.adoc') || file.name.endsWith('.md'))) {
filesToProcess.push(fullPath);
}
}
return filesToProcess;
});
}
// Process each file (.adoc or .md)
function processFile(filePath) {
return __awaiter(this, void 0, void 0, function* () {
const content = yield fs.readFile(filePath, 'utf-8');
// Clean up the content by removing AsciiDoc/Markdown-specific tags and code blocks
const { cleanedText, hasCodeBlock } = cleanContent(content);
// Compute readability score
const score = fleschKincaid(cleanedText);
// Compute additional metrics
const lines = content.split('\n');
const lineCount = lines.length;
const sentences = cleanedText.split(/[.!?]+/).filter(Boolean);
const sentenceCount = sentences.length;
const words = cleanedText.split(/\s+/).filter(Boolean);
const wordCount = words.length;
const avgWordLength = wordCount > 0 ? words.reduce((sum, word) => sum + word.length, 0) / wordCount : 0;
const avgSyllables = wordCount > 0 ? words.reduce((sum, word) => sum + countSyllables(word), 0) / wordCount : 0;
const acronymCount = countAcronyms(cleanedText);
// Prepend the score as a comment at the top of the file
const rating = getRating(score);
const newContent = `// Readability score: ${score.toFixed(2)}: ${rating}\n\n${content}`;
yield fs.writeFile(filePath, newContent);
return {
filePath,
score,
lineCount,
sentenceCount,
wordCount,
avgWordLength,
avgSyllables,
hasCodeBlock,
acronymCount,
};
});
}
// Display progress bar in console
function displayProgressBar(processed, total) {
const percentage = Math.floor((processed / total) * 100);
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(`Progress: [${'='.repeat(percentage / 2)}${' '.repeat(50 - percentage / 2)}] ${percentage}%`);
}
// Main function to run the app
function main() {
return __awaiter(this, void 0, void 0, function* () {
const startTime = perf_hooks_1.performance.now();
// Get folder path from command-line arguments or use current working directory if not provided
const folderPath = process.argv[2] || process.cwd();
try {
// Recursively scan directory for .adoc and .md files
const filesToProcess = yield scanDirectory(folderPath);
if (filesToProcess.length === 0) {
console.log('No .adoc or .md files found.');
process.exit(0);
}
let processedFilesCount = 0;
let scoresSummary = [];
for (const file of filesToProcess) {
const result = yield processFile(file);
scoresSummary.push(`${result.filePath}, File length: ${result.lineCount}, Sentence count: ${result.sentenceCount}, Word count: ${result.wordCount}, Average word length: ${result.avgWordLength.toFixed(2)}, Average syllables per word: ${result.avgSyllables.toFixed(2)}, Code present: ${result.hasCodeBlock ? 'Yes' : 'No'}, Acronyms: ${result.acronymCount}`);
processedFilesCount++;
displayProgressBar(processedFilesCount, filesToProcess.length);
}
// Write scores summary to scores.txt in the top-level folder
yield fs.writeFile(path.join(folderPath, 'scores.txt'), scoresSummary.join('\n'));
const endTime = perf_hooks_1.performance.now();
const totalTimeInSeconds = ((endTime - startTime) / 1000).toFixed(2);
console.log(`\nReadability scores computed! Total files processed: ${processedFilesCount} in ${totalTimeInSeconds} seconds.`);
console.log('Summary of scores saved to scores.txt');
}
catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
});
}
main();