Files
cv-site/validate-hyperscript.mjs
T
2025-11-17 08:34:50 +00:00

190 lines
5.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Hyperscript Syntax Validator
* Validates hyperscript 0.9.12 syntax rules
*/
import fs from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const HYPERSCRIPT_FILE = join(__dirname, 'static/hyperscript/functions._hs');
// Validation Rules for Hyperscript 0.9.12
const validationRules = [
{
name: 'No else statements',
pattern: /\belse\b/,
shouldNotMatch: true,
severity: 'error',
message: 'Hyperscript 0.9.12 does not support "else". Use separate "if not" blocks.'
},
{
name: 'All def blocks have end',
pattern: /^def\s+\w+/gm,
validator: (content, matches) => {
const defCount = matches.length;
const endCount = (content.match(/^end$/gm) || []).length;
// We need at least as many 'end' as 'def' (some 'end' are for loops/conditionals)
return endCount >= defCount;
},
severity: 'error',
message: 'All function definitions must end with "end"'
},
{
name: 'Valid function names',
pattern: /^def\s+([a-zA-Z]\w*)/gm,
validator: (content, matches) => {
return matches.every(match => /^[a-zA-Z][a-zA-Z0-9]*$/.test(match[1]));
},
severity: 'error',
message: 'Function names must start with a letter and contain only alphanumeric characters'
},
{
name: 'No nested event handlers',
pattern: /def\s+\w+[\s\S]*?on\s+\w+[\s\S]*?end/,
shouldNotMatch: true,
severity: 'error',
message: 'Hyperscript 0.9.12 does not support nested event handlers (on ... end inside def)'
},
{
name: 'Proper localStorage calls',
pattern: /localStorage\.(setItem|getItem)/,
validator: (content, matches) => {
// Check if all localStorage calls use proper syntax
const setItemCalls = content.match(/call\s+localStorage\.setItem\([^)]+\)/g) || [];
const getItemCalls = content.match(/localStorage\.getItem\([^)]+\)/g) || [];
return setItemCalls.length + getItemCalls.length === matches.length;
},
severity: 'warning',
message: 'localStorage should be called with "call localStorage.setItem(...)" or "localStorage.getItem(...)"'
},
{
name: 'Proper class manipulation',
pattern: /(add|remove)\s+\./,
validator: (content, matches) => {
// All class additions/removals should use proper syntax
return matches.every(match => {
const fullMatch = match[0];
return /^(add|remove)\s+\.\w+\s+(to|from)\s+/.test(fullMatch);
});
},
severity: 'error',
message: 'Class manipulation must use "add .class to element" or "remove .class from element"'
}
];
function validateFile(filePath) {
console.log('🔍 Validating Hyperscript File:', filePath);
console.log('='.repeat(80));
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
console.log(`📄 File: ${lines.length} lines\n`);
let errors = 0;
let warnings = 0;
let passed = 0;
validationRules.forEach(rule => {
const matches = [...content.matchAll(new RegExp(rule.pattern, 'gm'))];
let isValid = true;
let details = '';
if (rule.shouldNotMatch) {
isValid = matches.length === 0;
if (!isValid) {
details = `Found ${matches.length} occurrence(s)`;
}
} else if (rule.validator) {
isValid = rule.validator(content, matches);
} else {
isValid = matches.length > 0;
}
const icon = isValid ? '✅' : (rule.severity === 'error' ? '❌' : '⚠️');
const status = isValid ? 'PASS' : rule.severity.toUpperCase();
console.log(`${icon} ${rule.name}: ${status}`);
if (!isValid) {
console.log(` ${rule.message}`);
if (details) {
console.log(` ${details}`);
}
if (rule.severity === 'error') {
errors++;
} else {
warnings++;
}
} else {
passed++;
}
});
console.log('\n' + '='.repeat(80));
console.log('📊 VALIDATION SUMMARY:');
console.log(` ✅ Passed: ${passed}`);
console.log(` ⚠️ Warnings: ${warnings}`);
console.log(` ❌ Errors: ${errors}`);
console.log('='.repeat(80));
if (errors > 0) {
console.log('\n❌ VALIDATION FAILED - Errors found!');
process.exit(1);
} else if (warnings > 0) {
console.log('\n⚠️ VALIDATION PASSED WITH WARNINGS');
process.exit(0);
} else {
console.log('\n✅ VALIDATION PASSED - No issues found!');
process.exit(0);
}
} catch (error) {
console.error('❌ ERROR:', error.message);
process.exit(1);
}
}
// Additional checks
function performAdditionalChecks(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
console.log('\n📋 ADDITIONAL CHECKS:');
console.log('='.repeat(80));
// Count functions
const functions = [...content.matchAll(/^def\s+(\w+)/gm)];
console.log(`✅ Total functions defined: ${functions.length}`);
functions.forEach(([, name]) => {
console.log(` - ${name}()`);
});
// Check for localStorage usage
const localStorageUse = content.match(/localStorage\.(setItem|getItem)/g) || [];
console.log(`\n✅ LocalStorage operations: ${localStorageUse.length}`);
// Check for class manipulations
const classOps = content.match(/(add|remove)\s+\.\w+/g) || [];
console.log(`✅ Class manipulations: ${classOps.length}`);
// Check for loops
const loops = content.match(/for\s+\w+\s+in/g) || [];
console.log(`✅ Loop constructs: ${loops.length}`);
console.log('='.repeat(80));
}
// Run validation
validateFile(HYPERSCRIPT_FILE);
performAdditionalChecks(HYPERSCRIPT_FILE);