File size: 4,432 Bytes
d0808a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#!/usr/bin/env node

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