UNPKG

@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
#!/usr/bin/env node "use strict"; 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();