@regele/devtools
Version:
A collection of developer utilities for code processing and text analysis
357 lines (302 loc) • 12.8 kB
JavaScript
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);
}