check-content
Version:
A tool for evaluating content against content standards.
392 lines ⢠15.7 kB
JavaScript
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