Spaces:
Running
Running
// 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); | |
} | |
} | |
} |