UNPKG

@regele/devtools

Version:

A collection of developer utilities for code processing and text analysis

357 lines (302 loc) 12.8 kB
#!/usr/bin/env node try { // Direct implementation of text analysis const fs = require('fs'); const path = require('path'); const { Command } = require('commander'); const chalk = require('chalk'); const ora = require('ora'); const fg = require('fast-glob'); // Ensure ora is properly imported const createSpinner = ora.default || ora; // Enhanced word counter implementation class WordCounter { constructor(text = '', wordsPerMinute = 200, handwritingWpm = 25, keyboardWpm = 60) { this.text = text; this.wordsPerMinute = wordsPerMinute; this.handwritingWpm = handwritingWpm; this.keyboardWpm = keyboardWpm; } setText(text) { this.text = text; } getStats() { const characters = this.text.length; const charactersNoSpaces = this.text.replace(/\s/g, '').length; const words = this.text.split(/\s+/).filter(word => word.length > 0).length; const sentences = this.text.split(/[.!?]+/).filter(sentence => sentence.trim().length > 0).length; const paragraphs = this.text.split(/\n\s*\n/).filter(paragraph => paragraph.trim().length > 0).length; // Calculate different time estimates const readingTime = this.formatTime(words, this.wordsPerMinute); const handwritingTime = this.estimateHandwritingTime(words, characters); const keyboardTime = this.formatTime(words, this.keyboardWpm); return { characters, charactersNoSpaces, words, sentences, paragraphs, readingTime, handwritingTime, keyboardTime }; } /** * Estimate handwriting time using a more realistic algorithm * * @param {number} words - Number of words * @param {number} characters - Number of characters * @returns {string} Formatted time estimate */ estimateHandwritingTime(words, characters) { // Base handwriting speed (words per minute) const baseWpm = this.handwritingWpm; // Complexity factor based on average word length // Longer words are generally more complex to write const avgWordLength = words > 0 ? characters / words : 0; const complexityFactor = Math.min(1.5, Math.max(1.0, avgWordLength / 5)); // Skill factor - assuming average skill const skillFactor = 1.0; // Calculate adjusted writing speed const adjustedWpm = baseWpm / (complexityFactor * skillFactor); // Calculate time in minutes const timeMinutes = words / adjustedWpm; // Format the time return this.formatTimeValue(timeMinutes); } formatTime(wordCount, wpm) { if (wordCount === 0) return '~0 min'; const minutes = wordCount / wpm; return this.formatTimeValue(minutes); } formatTimeValue(minutes) { if (minutes < 1) { return '< 1 min'; } else if (minutes < 60) { return `~${Math.ceil(minutes)} min`; } else { const hours = Math.floor(minutes / 60); const remainingMinutes = Math.ceil(minutes % 60); return `~${hours}h ${remainingMinutes}m`; } } getReadingTime() { return this.getStats().readingTime; } getHandwritingTime() { return this.getStats().handwritingTime; } getKeyboardTime() { return this.getStats().keyboardTime; } getReadabilityScore() { // Simple readability score based on average sentence length and word length const words = this.text.split(/\s+/).filter(word => word.length > 0); const sentences = this.text.split(/[.!?]+/).filter(sentence => sentence.trim().length > 0); if (words.length === 0 || sentences.length === 0) { return 0; } const avgSentenceLength = words.length / sentences.length; const avgWordLength = words.join('').length / words.length; // Higher score for shorter sentences and words (easier to read) const sentenceScore = Math.max(0, 100 - (avgSentenceLength - 10) * 5); const wordScore = Math.max(0, 100 - (avgWordLength - 4) * 10); return Math.round((sentenceScore + wordScore) / 2); } } // Create the analyze command const command = new Command('devtools-analyze'); command .description('Analyze text content in files') .argument('<patterns...>', 'File patterns to analyze (e.g., "src/**/*.md")') .option('--summary', 'Show summary only', false) .option('--ignore <pattern>', 'Files to ignore (comma-separated)', value => value.split(',').map(item => item.trim())) .option('--output <path>', 'Write report to file') .action(async (patterns, options) => { try { // Start spinner const spinner = createSpinner({ text: 'Analyzing files...', color: 'cyan', spinner: 'dots' }).start(); // Process each file let results = []; // Default ignore patterns const defaultIgnore = [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/package-lock.json', '**/yarn.lock', '**/*.min.js', '**/*.min.css', '**/*.bundle.js' ]; // Combine with user-provided ignore patterns const ignorePatterns = options.ignore ? [...defaultIgnore, ...options.ignore] : defaultIgnore; // Normalize patterns for Windows paths const normalizedPatterns = patterns.map(pattern => { // Convert Windows backslashes to forward slashes for glob return pattern.replace(/\\/g, '/'); }); // Find all files matching the patterns const files = await fg(normalizedPatterns, { ignore: ignorePatterns, onlyFiles: true, absolute: true, // Use absolute paths to handle Windows paths better followSymbolicLinks: false // Don't follow symlinks to avoid circular references }); // Create a word counter const wordCounter = new WordCounter(); // Process each file for (const file of files) { try { // Read the file const content = fs.readFileSync(file, 'utf8'); // Skip binary files if (isBinaryContent(content)) { continue; } // Set the text and get stats wordCounter.setText(content); const stats = wordCounter.getStats(); const readabilityScore = wordCounter.getReadabilityScore(); results.push({ path: file, success: true, stats: { ...stats, readabilityScore } }); } catch (error) { results.push({ path: file, success: false, error: error.message }); } } // Stop spinner spinner.succeed(`Analyzed ${results.length} files`); // Calculate summary const successResults = results.filter(r => r.success); const totalWords = successResults.reduce((sum, r) => sum + r.stats.words, 0); const totalCharacters = successResults.reduce((sum, r) => sum + r.stats.characters, 0); const totalReadabilityScore = successResults.length > 0 ? successResults.reduce((sum, r) => sum + r.stats.readabilityScore, 0) / successResults.length : 0; // Log results console.log('\n' + chalk.bold('Detailed Results:')); if (!options.summary && successResults.length > 0) { successResults.forEach(result => { console.log(`\n${chalk.cyan(result.path)}:`); console.log(` Words: ${result.stats.words}`); console.log(` Characters: ${result.stats.characters}`); console.log(` Sentences: ${result.stats.sentences}`); console.log(` Paragraphs: ${result.stats.paragraphs}`); console.log(` Reading Time: ${result.stats.readingTime}`); console.log(` Handwriting Time: ${result.stats.handwritingTime || '~0 min'}`); console.log(` Keyboard Time: ${result.stats.keyboardTime || '~0 min'}`); console.log(` Readability: ${result.stats.readabilityScore}/100`); }); } // Calculate average times const calculateAverageTime = (results, timeProperty) => { const validResults = results.filter(r => r.success && r.stats && r.stats[timeProperty]); if (validResults.length === 0) { return '~0 min'; } // For handwriting time, we need to calculate the total words and characters if (timeProperty === 'handwritingTime') { const totalWords = validResults.reduce((sum, r) => sum + r.stats.words, 0); const totalChars = validResults.reduce((sum, r) => sum + r.stats.characters, 0); // Create a temporary WordCounter to calculate the handwriting time const tempCounter = new WordCounter(); return tempCounter.estimateHandwritingTime(totalWords, totalChars); } // For other time properties, use the standard approach // Extract numeric values from time strings const timeValues = validResults.map(r => { const timeStr = r.stats[timeProperty]; if (timeStr.includes('h')) { // Handle "~Xh Ym" format const hours = parseInt(timeStr.split('h')[0].replace(/[^0-9]/g, ''), 10) || 0; const minutes = parseInt(timeStr.split('h')[1].replace(/[^0-9]/g, ''), 10) || 0; return hours * 60 + minutes; } else { // Handle "~X min" format return parseInt(timeStr.replace(/[^0-9]/g, ''), 10) || 0; } }); // Calculate average const totalMinutes = timeValues.reduce((sum, val) => sum + val, 0); const avgMinutes = totalMinutes / timeValues.length; // Format the result if (avgMinutes < 1) { return '< 1 min'; } else if (avgMinutes < 60) { return `~${Math.ceil(avgMinutes)} min`; } else { const hours = Math.floor(avgMinutes / 60); const remainingMinutes = Math.ceil(avgMinutes % 60); return `~${hours}h ${remainingMinutes}m`; } }; const avgReadingTime = calculateAverageTime(successResults, 'readingTime'); const avgHandwritingTime = calculateAverageTime(successResults, 'handwritingTime'); const avgKeyboardTime = calculateAverageTime(successResults, 'keyboardTime'); console.log('\n' + chalk.bold('Summary:')); console.log(` Total Files: ${successResults.length}`); console.log(` Total Words: ${totalWords}`); console.log(` Total Characters: ${totalCharacters}`); console.log(` Average Readability: ${totalReadabilityScore.toFixed(2)}/100`); console.log(` Estimated Reading Time: ${avgReadingTime}`); console.log(` Estimated Handwriting Time: ${avgHandwritingTime}`); console.log(` Estimated Keyboard Time: ${avgKeyboardTime}`); // Write report to file if requested if (options.output) { const report = { summary: { totalFiles: successResults.length, totalWords, totalCharacters, averageReadability: totalReadabilityScore, estimatedReadingTime: avgReadingTime, estimatedHandwritingTime: avgHandwritingTime, estimatedKeyboardTime: avgKeyboardTime }, files: successResults.map(r => ({ path: r.path, stats: r.stats })) }; fs.writeFileSync(options.output, JSON.stringify(report, null, 2), 'utf8'); console.log(`\nReport written to ${options.output}`); } } catch (error) { console.error(chalk.red('✗ ') + error.message); process.exit(1); } }); // Helper function to check if content is binary function isBinaryContent(content) { // Simple binary check: if the content contains a high percentage of null bytes or // other control characters, it's likely binary const controlCharCount = content.split('').filter(char => { const code = char.charCodeAt(0); return code < 9 || (code > 13 && code < 32 && code !== 27); }).length; // If more than 5% is control chars, consider binary return controlCharCount > content.length * 0.05; } // Execute the command command.parse(process.argv); } catch (error) { console.error('Error loading command:', error.message); process.exit(1); }