@alexneri/readability-ts
Version:
A CLI app that runs the Flesch-kincaid readability score recursively on all *.adoc files in the current directory.
247 lines (246 loc) • 13.4 kB
JavaScript
;
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());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
var fs = require("fs/promises");
var path = require("path");
var perf_hooks_1 = require("perf_hooks");
// Flesch-Kincaid readability score calculation
function fleschKincaid(text) {
var sentences = text.split(/[.!?]+/).filter(Boolean).length;
var words = text.split(/\s+/).filter(Boolean).length;
var syllables = text.split(/\s+/).reduce(function (acc, word) { return acc + countSyllables(word); }, 0);
var 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
var vowels = ['a', 'e', 'i', 'o', 'u', 'y'];
var syllableCount = 0;
var prevCharWasVowel = false;
for (var _i = 0, word_1 = word; _i < word_1.length; _i++) {
var char = word_1[_i];
var 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) {
var hasCodeBlock = /```[\s\S]*?```|----/.test(content); // Detect code blocks
var cleanedText = content.replace(/(```[\s\S]*?```|==+|--+|\.\.\.\.|\[.*?\])/g, '').trim();
return { cleanedText: cleanedText, hasCodeBlock: hasCodeBlock };
}
// Count acronyms in the text
function countAcronyms(text) {
var acronymRegex = /\b[A-Z]{2,}\b/g; // Matches words with all uppercase letters of length >=2
var 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 () {
var filesToProcess, files, _i, files_1, file, fullPath, _a, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
filesToProcess = [];
return [4 /*yield*/, fs.readdir(dirPath, { withFileTypes: true })];
case 1:
files = _c.sent();
_i = 0, files_1 = files;
_c.label = 2;
case 2:
if (!(_i < files_1.length)) return [3 /*break*/, 6];
file = files_1[_i];
fullPath = path.join(dirPath, file.name);
if (!file.isDirectory()) return [3 /*break*/, 4];
_b = (_a = filesToProcess).concat;
return [4 /*yield*/, scanDirectory(fullPath)];
case 3:
filesToProcess = _b.apply(_a, [_c.sent()]);
return [3 /*break*/, 5];
case 4:
if (file.isFile() &&
(file.name.endsWith('.adoc') || file.name.endsWith('.md'))) {
filesToProcess.push(fullPath);
}
_c.label = 5;
case 5:
_i++;
return [3 /*break*/, 2];
case 6: return [2 /*return*/, filesToProcess];
}
});
});
}
// Process each file (.adoc or .md)
function processFile(filePath) {
return __awaiter(this, void 0, void 0, function () {
var content, _a, cleanedText, hasCodeBlock, score, rating, lines, lineCount, sentences, sentenceCount, words, wordCount, avgWordLength, avgSyllables, acronymCount, newContent;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, fs.readFile(filePath, 'utf-8')];
case 1:
content = _b.sent();
_a = cleanContent(content), cleanedText = _a.cleanedText, hasCodeBlock = _a.hasCodeBlock;
score = fleschKincaid(cleanedText);
rating = getRating(score);
lines = content.split('\n');
lineCount = lines.length;
sentences = cleanedText.split(/[.!?]+/).filter(Boolean);
sentenceCount = sentences.length;
words = cleanedText.split(/\s+/).filter(Boolean);
wordCount = words.length;
avgWordLength = wordCount > 0 ? words.reduce(function (sum, word) { return sum + word.length; }, 0) / wordCount : 0;
avgSyllables = wordCount > 0 ? words.reduce(function (sum, word) { return sum + countSyllables(word); }, 0) / wordCount : 0;
acronymCount = countAcronyms(cleanedText);
newContent = "// Readability score: ".concat(score.toFixed(2), "\n") +
"// ".concat(rating, "\n") +
"// File length: ".concat(lineCount, ", Sentence count: ").concat(sentenceCount, ", Word count: ").concat(wordCount, ", Average word length: ").concat(avgWordLength.toFixed(2), ", Average syllables per word: ").concat(avgSyllables.toFixed(2), ", Code present: ").concat(hasCodeBlock ? 'Yes' : 'No', ", Acronyms: ").concat(acronymCount, "\n\n") +
content;
return [4 /*yield*/, fs.writeFile(filePath, newContent)];
case 2:
_b.sent();
return [2 /*return*/, {
filePath: filePath,
score: score,
lineCount: lineCount,
sentenceCount: sentenceCount,
wordCount: wordCount,
avgWordLength: avgWordLength,
avgSyllables: avgSyllables,
hasCodeBlock: hasCodeBlock,
acronymCount: acronymCount,
}];
}
});
});
}
// Display progress bar in console
function displayProgressBar(processed, total) {
var percentage = Math.floor((processed / total) * 100);
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write("Progress: [".concat('='.repeat(percentage / 2)).concat(' '.repeat(50 - percentage / 2), "] ").concat(percentage, "%"));
}
// Main function to run the app
function main() {
return __awaiter(this, void 0, void 0, function () {
var startTime, folderPath, filesToProcess, processedFilesCount, scoresSummary, _i, filesToProcess_1, file, result, endTime, totalTimeInSeconds, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
startTime = perf_hooks_1.performance.now();
folderPath = process.argv[2] || process.cwd();
_a.label = 1;
case 1:
_a.trys.push([1, 8, , 9]);
return [4 /*yield*/, scanDirectory(folderPath)];
case 2:
filesToProcess = _a.sent();
if (filesToProcess.length === 0) {
console.log('No .adoc or .md files found.');
process.exit(0);
}
processedFilesCount = 0;
scoresSummary = [];
_i = 0, filesToProcess_1 = filesToProcess;
_a.label = 3;
case 3:
if (!(_i < filesToProcess_1.length)) return [3 /*break*/, 6];
file = filesToProcess_1[_i];
return [4 /*yield*/, processFile(file)];
case 4:
result = _a.sent();
scoresSummary.push("".concat(result.filePath, ", Readability score: ").concat(result.score.toFixed(2), ", File length: ").concat(result.lineCount, ", Sentence count: ").concat(result.sentenceCount, ", Word count: ").concat(result.wordCount, ", Average word length: ").concat(result.avgWordLength.toFixed(2), ", Average syllables per word: ").concat(result.avgSyllables.toFixed(2), ", Code present: ").concat(result.hasCodeBlock ? 'Yes' : 'No', ", Acronyms: ").concat(result.acronymCount));
processedFilesCount++;
displayProgressBar(processedFilesCount, filesToProcess.length);
_a.label = 5;
case 5:
_i++;
return [3 /*break*/, 3];
case 6:
// Write scores summary to scores.txt in the top-level folder
return [4 /*yield*/, fs.writeFile(path.join(folderPath, 'scores.txt'), scoresSummary.join('\n'))];
case 7:
// Write scores summary to scores.txt in the top-level folder
_a.sent();
endTime = perf_hooks_1.performance.now();
totalTimeInSeconds = ((endTime - startTime) / 1000).toFixed(2);
console.log("\nReadability scores computed! Total files processed: ".concat(processedFilesCount, " in ").concat(totalTimeInSeconds, " seconds."));
console.log('Summary of scores saved to scores.txt');
return [3 /*break*/, 9];
case 8:
error_1 = _a.sent();
console.error('Error:', error_1.message);
process.exit(1);
return [3 /*break*/, 9];
case 9: return [2 /*return*/];
}
});
});
}
main();