UNPKG

check-content

Version:

A tool for evaluating content against content standards.

392 lines • 15.7 kB
#!/usr/bin/env node import * as fs from 'fs'; import * as path from 'path'; import { glob } from 'glob'; import chalk from 'chalk'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; class ContentExtractor { constructor() { this.minContentLength = 3; this.maxContentLength = 700; } extractFromFile(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); const ext = path.extname(filePath); const contentItems = []; if (ext === '.html') { contentItems.push(...this.extractFromHTML(content)); } else if (ext === '.jsx' || ext === '.tsx' || ext === '.js') { contentItems.push(...this.extractFromJSX(content)); } const filteredItems = this.filterAndDedupeContent(contentItems); return { group_id: this.generateGroupId(filePath), file_path: filePath, content_items: filteredItems }; } extractFromHTML(content) { const items = []; const textPatterns = [ { pattern: /<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, type: 'heading' }, { pattern: /<p[^>]*>(.*?)<\/p>/gi, type: 'description' }, { pattern: /<button[^>]*>(.*?)<\/button>/gi, type: 'button' }, { pattern: /<a[^>]*>(.*?)<\/a>/gi, type: 'button' }, { pattern: /alt\s*=\s*["'](.*?)["']/gi, type: 'alt_text' }, { pattern: /placeholder\s*=\s*["'](.*?)["']/gi, type: 'placeholder' }, { pattern: /title\s*=\s*["'](.*?)["']/gi, type: 'label' }, { pattern: /aria-label\s*=\s*["'](.*?)["']/gi, type: 'label' } ]; textPatterns.forEach(({ pattern, type }) => { let match; while ((match = pattern.exec(content)) !== null) { const text = this.cleanText(match[1]); if (this.isValidContent(text)) { items.push({ text, element_type: type, line_number: this.getLineNumber(content, match.index), context: this.getContext(content, match.index) }); } } }); return items; } extractFromJSX(content) { const items = []; const jsxPatterns = [ { pattern: />([^<>{}\n]+)</g, type: 'ui_text' }, { pattern: /(?:alt|aria-label)\s*=\s*["'`]([^"'`]+)["'`]/gi, type: 'alt_text' }, { pattern: /placeholder\s*=\s*["'`]([^"'`]+)["'`]/gi, type: 'placeholder' }, { pattern: /<button[^>]*>([^<]+)<\/button>/gi, type: 'button' }, { pattern: /<h[1-6][^>]*>([^<]+)<\/h[1-6]>/gi, type: 'heading' } ]; const linesToSkip = this.getTechnicalLines(content); jsxPatterns.forEach(({ pattern, type }) => { let match; while ((match = pattern.exec(content)) !== null) { const text = this.cleanText(match[1]); const lineNum = this.getLineNumber(content, match.index); if (linesToSkip.includes(lineNum)) continue; if (this.isValidContent(text)) { items.push({ text, element_type: type, line_number: lineNum, context: this.getContext(content, match.index) }); } } }); return items; } getTechnicalLines(content) { return content.split('\n').map((line, index) => { const trimmed = line.trim(); if (trimmed.startsWith('import ') || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.includes('console.') || trimmed.startsWith('const ') || trimmed.startsWith('let ') || trimmed.startsWith('var ') || trimmed.startsWith('function ') || trimmed.startsWith('class ') || trimmed.startsWith('export ')) { return index + 1; } return null; }).filter(Boolean); } cleanText(text) { return text .replace(/\s+/g, ' ') .replace(/[{}]/g, '') .trim(); } isValidContent(text) { if (text.length < this.minContentLength) return false; if (text.length > this.maxContentLength) return false; const skipPatterns = [ /^[\d\s\-_.,;:()[\]{}]+$/, // Only numbers and punctuation /^[A-Z_][A-Z0-9_]*$/, // Constants/enums /^[a-z][a-zA-Z0-9]*$/, // Variable names /^\$\{.*\}$/, // Template literals /^function|class|const|let|var/, // Code declarations /^\/\*|^\/\/|^#/, // Comments /^import|export/, // Module statements /^[0-9]+$/, // Just numbers /^[^a-zA-Z]+$/, // No letters /^[a-zA-Z]+[0-9]+$/, // Variable names with numbers /^[0-9]+[a-zA-Z]+$/ // Numbers with letters ]; return !skipPatterns.some(pattern => pattern.test(text)); } filterAndDedupeContent(items) { const seen = new Set(); const filtered = []; for (const item of items) { const key = item.text.toLowerCase().trim(); if (!seen.has(key)) { seen.add(key); filtered.push(item); } } return filtered; } getLineNumber(content, index) { return content.substring(0, index).split('\n').length; } getContext(content, index) { const lines = content.split('\n'); const lineNum = this.getLineNumber(content, index); const contextLines = lines.slice(Math.max(0, lineNum - 2), lineNum + 1); return contextLines.join('\n').trim(); } generateGroupId(filePath) { return path.basename(filePath) + '_' + Date.now(); } } // ===== ENDPOINT COMMUNICATION ===== class EndpointChecker { constructor(endpoint) { this.endpoint = endpoint; } async checkContent(groups) { if (groups.length === 0) return []; console.log(chalk.blue('šŸ“¤ Sending ' + groups.length + ' content groups to endpoint for analysis...')); try { const response = await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content_groups: groups.map(group => ({ group_id: group.group_id, file_path: group.file_path, content_items: group.content_items.map(item => ({ text: item.text, element_type: item.element_type, line_number: item.line_number, element_context: item.context })) })) }) }); if (!response.ok) { const errorText = await response.text(); throw new Error('Endpoint request failed: ' + response.status + ' ' + response.statusText + ' - ' + errorText); } const data = await response.json(); // Basic validation to ensure it has the expected structure if (!data || !data.analysis_results || !Array.isArray(data.analysis_results)) { throw new Error('Invalid response format from endpoint'); } return this.parseEndpointResponse(data); } catch (error) { console.error(chalk.red('Endpoint analysis failed:'), error.message); return []; } } parseEndpointResponse(response) { const issues = []; response.analysis_results.forEach(result => { result.violations.forEach(violation => { // Create a more concise AI message const explanation = violation.explanation.length > 80 ? violation.explanation.substring(0, 80) + '...' : violation.explanation; const aiMessage = `${violation.message} (AI: ${explanation})`; issues.push({ file: result.file_path, severity: violation.severity === 'info' ? 'warning' : violation.severity, rule: violation.rule_violated, message: aiMessage, suggestion: violation.suggestion, context: violation.text }); }); }); return issues; } } // ===== MAIN CLI ===== async function main() { const argv = await yargs(hideBin(process.argv)) .option('input', { alias: 'i', description: 'Input file or directory to analyze', type: 'string', default: '.', }) .option('endpoint', { alias: 'e', description: 'Content analysis API endpoint', type: 'string', default: 'https://33a77ff1-3df8-4aaf-a862-4362448c2870-00-tra3gj8503ef.picard.replit.dev/api/v1/analyze' }) .option('format', { alias: 'f', description: 'Output format', choices: ['console', 'json'], default: 'console' }) .version('1.0.0') .help() .argv; try { const files = await findFiles(argv.input); if (files.length === 0) { console.log(chalk.yellow('No content files found to analyze.')); return; } console.log(chalk.blue('šŸ” Extracting content from ' + files.length + ' files...')); const extractor = new ContentExtractor(); const allGroups = []; files.forEach(file => { try { const group = extractor.extractFromFile(file); if (group.content_items.length > 0) { allGroups.push(group); } } catch (error) { console.warn(chalk.yellow('Warning: Could not process ' + file + ': ' + error.message)); } }); const totalItems = allGroups.reduce((sum, group) => sum + group.content_items.length, 0); console.log(chalk.blue('šŸ“ Found ' + totalItems + ' content items in ' + allGroups.length + ' groups')); if (allGroups.length === 0) { console.log(chalk.yellow('No content found to analyze.')); return; } // Send to endpoint for analysis console.log(chalk.blue('šŸ“¤ Sending content to endpoint for analysis...')); const endpointChecker = new EndpointChecker(argv.endpoint); const issues = await endpointChecker.checkContent(allGroups); // Output results if (argv.format === 'json') { console.log(JSON.stringify(issues, null, 2)); } else { displayResults(issues, files.length); } if (issues.some(issue => issue.severity === 'error')) { process.exit(1); } } catch (error) { console.error(chalk.red('Error:'), error.message); process.exit(1); } } async function findFiles(input) { try { const stat = fs.statSync(input); if (stat.isFile()) { return [input]; } if (stat.isDirectory()) { const patterns = [ input + '/**/*.jsx', input + '/**/*.tsx', input + '/**/*.js', input + '/**/*.html' ]; const files = []; for (const pattern of patterns) { const matches = await glob(pattern, { ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/coverage/**', '**/build/**'] }); files.push(...matches); } return [...new Set(files)]; } throw new Error('Input path is not a file or directory: ' + input); } catch (error) { if (error.code === 'ENOENT') { throw new Error('Input path does not exist: ' + input); } throw error; } } function displayResults(issues, fileCount) { if (issues.length === 0) { console.log(chalk.green('āœ… No content issues found!')); console.log(chalk.gray(' Analyzed ' + fileCount + ' files')); return; } const errorCount = issues.filter(i => i.severity === 'error').length; const warningCount = issues.filter(i => i.severity === 'warning').length; // Calculate content quality score const totalIssues = issues.length; const qualityScore = Math.max(0, Math.round((1 - totalIssues / 20) * 100)); // Rough estimate console.log('\n' + chalk.bold('Content Analysis Results:')); console.log(chalk.gray('='.repeat(50))); const issuesByFile = issues.reduce((acc, issue) => { if (!acc[issue.file]) acc[issue.file] = []; acc[issue.file].push(issue); return acc; }, {}); Object.entries(issuesByFile).forEach(([file, fileIssues]) => { console.log('\n' + chalk.bold(path.relative(process.cwd(), file)) + ':'); fileIssues.forEach(issue => { const icon = issue.severity === 'error' ? 'āŒ' : 'āš ļø'; const color = issue.severity === 'error' ? chalk.red : chalk.yellow; const lineInfo = issue.line ? `:line ${issue.line}` : ''; console.log(' ' + icon + ' ' + color(issue.message) + ' ' + chalk.gray('[' + issue.rule + lineInfo + ']')); if (issue.suggestion) { console.log(' ' + chalk.green('→') + ' ' + issue.suggestion); } if (issue.context && issue.context.length < 100) { console.log(' ' + chalk.gray('"' + issue.context + '"')); } else if (issue.context) { console.log(' ' + chalk.gray('"' + issue.context.substring(0, 80) + '..."')); } }); }); console.log('\n' + chalk.bold('Summary:')); // Show quality score with appropriate celebration or encouragement if (qualityScore >= 90) { console.log(' ' + chalk.green('šŸŽ‰ Excellent! ' + qualityScore + '% content quality score')); } else if (qualityScore >= 70) { console.log(' ' + chalk.yellow('šŸ“Š Good progress: ' + qualityScore + '% content quality score')); } else { console.log(' ' + chalk.red('šŸ“Š Content quality score: ' + qualityScore + '%')); } if (errorCount > 0) { console.log(' ' + chalk.red('āŒ ' + errorCount + ' ' + (errorCount === 1 ? 'error' : 'errors'))); } if (warningCount > 0) { console.log(' ' + chalk.yellow('āš ļø ' + warningCount + ' ' + (warningCount === 1 ? 'warning' : 'warnings'))); } console.log(chalk.gray(' Analyzed ' + fileCount + ' ' + (fileCount === 1 ? 'file' : 'files'))); if (errorCount > 0) { console.log(chalk.red('\nāš ļø Content issues found! Please review and address them.')); } else if (qualityScore >= 90) { console.log(chalk.green('\n🌟 Outstanding work! Your content meets high standards.')); } } if (require.main === module) { main().catch(error => { console.error(chalk.red('Fatal error:'), error.message); process.exit(1); }); } export { main, ContentExtractor, EndpointChecker }; //# sourceMappingURL=cli.js.map