#!/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);