Spaces:
Sleeping
Sleeping
| // Minimal Liftoscript validator using Lezer parsers | |
| // No dependencies on the full Liftosaur codebase | |
| const { parser: liftoscriptParser } = require('./liftoscript-parser.js'); | |
| const { parser: plannerParser } = require('./planner-parser.js'); | |
| function getLineAndColumn(text, position) { | |
| const lines = text.split('\n'); | |
| let currentPos = 0; | |
| for (let i = 0; i < lines.length; i++) { | |
| const lineLength = lines[i].length + 1; // +1 for newline | |
| if (position < currentPos + lineLength) { | |
| return { | |
| line: i + 1, | |
| column: position - currentPos + 1 | |
| }; | |
| } | |
| currentPos += lineLength; | |
| } | |
| return { line: lines.length, column: 1 }; | |
| } | |
| function detectScriptType(script) { | |
| // Detect if this is planner syntax or pure Liftoscript | |
| const plannerIndicators = [ | |
| /^#\s+Week/m, | |
| /^##\s+Day/m, | |
| /^\s*\w+\s*\/\s*\d+x\d+/m, // Exercise format like "Squat / 3x5" | |
| /\/\s*progress:/, | |
| /\/\s*warmup:/, | |
| /\/\s*update:/ | |
| ]; | |
| return plannerIndicators.some(regex => regex.test(script)) ? 'planner' : 'liftoscript'; | |
| } | |
| function validateLiftoscript(script) { | |
| try { | |
| const scriptType = detectScriptType(script); | |
| let tree; | |
| if (scriptType === 'planner') { | |
| // Parse with planner parser | |
| tree = plannerParser.parse(script); | |
| } else { | |
| // Parse as pure Liftoscript | |
| tree = liftoscriptParser.parse(script); | |
| } | |
| // Check for error nodes | |
| let hasError = false; | |
| let errorNode = null; | |
| let errorType = null; | |
| tree.iterate({ | |
| enter: (node) => { | |
| if (node.type.isError) { | |
| hasError = true; | |
| errorNode = node; | |
| errorType = node.type.name; | |
| return false; // Stop iteration | |
| } | |
| } | |
| }); | |
| if (hasError && errorNode) { | |
| const { line, column } = getLineAndColumn(script, errorNode.from); | |
| // Try to provide more helpful error messages | |
| let message = `Syntax error`; | |
| const problemText = script.substring(errorNode.from, Math.min(errorNode.to, errorNode.from + 20)); | |
| if (scriptType === 'planner') { | |
| if (problemText.includes('/')) { | |
| message = "Invalid exercise format. Expected: 'Exercise / Sets x Reps'"; | |
| } else if (problemText.includes(':')) { | |
| message = "Invalid property format. Expected: 'property: value' or 'property: function(args)'"; | |
| } | |
| } else { | |
| if (problemText.includes('=')) { | |
| message = "Invalid assignment. Check variable names and syntax"; | |
| } else if (problemText.includes('{') || problemText.includes('}')) { | |
| message = "Unmatched braces"; | |
| } | |
| } | |
| return { | |
| valid: false, | |
| error: { | |
| message: `${message} at "${problemText.trim()}"`, | |
| line: line, | |
| column: column, | |
| type: scriptType | |
| } | |
| }; | |
| } | |
| // No syntax errors found | |
| return { | |
| valid: true, | |
| error: null, | |
| type: scriptType | |
| }; | |
| } catch (e) { | |
| return { | |
| valid: false, | |
| error: { | |
| message: `Parser error: ${e.message}`, | |
| line: 0, | |
| column: 0 | |
| } | |
| }; | |
| } | |
| } | |
| // Export for use | |
| module.exports = { validateLiftoscript }; | |
| // CLI interface | |
| if (require.main === module) { | |
| const fs = require('fs'); | |
| const args = process.argv.slice(2); | |
| if (args.length === 0) { | |
| console.log('Liftoscript Validator'); | |
| console.log('Usage: node minimal-validator.js <script or -> [--json]'); | |
| console.log('\nExamples:'); | |
| console.log(' node minimal-validator.js "state.weight = 100lb"'); | |
| console.log(' node minimal-validator.js - < program.liftoscript'); | |
| console.log(' echo "Squat / 3x5" | node minimal-validator.js - --json'); | |
| process.exit(0); | |
| } | |
| let script; | |
| const jsonOutput = args.includes('--json'); | |
| if (args[0] === '-') { | |
| // Read from stdin | |
| script = fs.readFileSync(0, 'utf-8'); | |
| } else { | |
| script = args[0]; | |
| } | |
| const result = validateLiftoscript(script); | |
| if (jsonOutput) { | |
| console.log(JSON.stringify(result, null, 2)); | |
| } else { | |
| if (result.valid) { | |
| console.log(`✅ Valid ${result.type} syntax`); | |
| } else { | |
| console.error(`❌ Invalid syntax (${result.error.type || 'unknown'} mode)`); | |
| console.error(` Line ${result.error.line}, Column ${result.error.column}: ${result.error.message}`); | |
| process.exit(1); | |
| } | |
| } | |
| } |