tova 0.5.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/bin/tova.js +261 -60
  2. package/package.json +1 -1
  3. package/src/analyzer/analyzer.js +351 -11
  4. package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
  5. package/src/analyzer/deploy-analyzer.js +44 -0
  6. package/src/analyzer/form-analyzer.js +113 -0
  7. package/src/analyzer/scope.js +2 -2
  8. package/src/codegen/base-codegen.js +1160 -10
  9. package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
  10. package/src/codegen/codegen.js +119 -28
  11. package/src/codegen/deploy-codegen.js +49 -0
  12. package/src/codegen/edge-codegen.js +1351 -0
  13. package/src/codegen/form-codegen.js +553 -0
  14. package/src/codegen/security-codegen.js +5 -5
  15. package/src/codegen/server-codegen.js +88 -7
  16. package/src/codegen/shared-codegen.js +5 -0
  17. package/src/codegen/wasm-codegen.js +6 -0
  18. package/src/config/edit-toml.js +6 -2
  19. package/src/config/git-resolver.js +128 -0
  20. package/src/config/lock-file.js +57 -0
  21. package/src/config/module-cache.js +58 -0
  22. package/src/config/module-entry.js +37 -0
  23. package/src/config/module-path.js +31 -0
  24. package/src/config/pkg-errors.js +62 -0
  25. package/src/config/resolve.js +17 -0
  26. package/src/config/resolver.js +139 -0
  27. package/src/config/search.js +28 -0
  28. package/src/config/semver.js +72 -0
  29. package/src/config/toml.js +48 -5
  30. package/src/deploy/deploy.js +217 -0
  31. package/src/deploy/infer.js +218 -0
  32. package/src/deploy/provision.js +311 -0
  33. package/src/diagnostics/error-codes.js +1 -1
  34. package/src/docs/generator.js +1 -1
  35. package/src/formatter/formatter.js +4 -4
  36. package/src/lexer/tokens.js +12 -2
  37. package/src/lsp/server.js +483 -1
  38. package/src/parser/ast.js +60 -5
  39. package/src/parser/{client-ast.js → browser-ast.js} +3 -3
  40. package/src/parser/{client-parser.js → browser-parser.js} +42 -15
  41. package/src/parser/concurrency-ast.js +15 -0
  42. package/src/parser/concurrency-parser.js +236 -0
  43. package/src/parser/deploy-ast.js +37 -0
  44. package/src/parser/deploy-parser.js +132 -0
  45. package/src/parser/edge-ast.js +83 -0
  46. package/src/parser/edge-parser.js +262 -0
  47. package/src/parser/form-ast.js +80 -0
  48. package/src/parser/form-parser.js +206 -0
  49. package/src/parser/parser.js +82 -14
  50. package/src/parser/select-ast.js +39 -0
  51. package/src/registry/plugins/browser-plugin.js +30 -0
  52. package/src/registry/plugins/concurrency-plugin.js +32 -0
  53. package/src/registry/plugins/deploy-plugin.js +33 -0
  54. package/src/registry/plugins/edge-plugin.js +32 -0
  55. package/src/registry/register-all.js +8 -2
  56. package/src/runtime/ssr.js +2 -2
  57. package/src/stdlib/inline.js +38 -6
  58. package/src/stdlib/runtime-bridge.js +152 -0
  59. package/src/version.js +1 -1
  60. package/src/registry/plugins/client-plugin.js +0 -30
@@ -0,0 +1,262 @@
1
+ // Edge/serverless-specific parser methods for the Tova language
2
+ // Extracted from parser.js for lazy loading — only loaded when edge { } blocks are encountered.
3
+
4
+ import { TokenType } from '../lexer/tokens.js';
5
+ import * as AST from './ast.js';
6
+ import { installServerParser } from './server-parser.js';
7
+ import {
8
+ EdgeConfigField, EdgeKVDeclaration, EdgeSQLDeclaration,
9
+ EdgeStorageDeclaration, EdgeQueueDeclaration, EdgeEnvDeclaration,
10
+ EdgeSecretDeclaration, EdgeScheduleDeclaration, EdgeConsumeDeclaration,
11
+ } from './edge-ast.js';
12
+
13
+ // Valid config keys inside edge blocks
14
+ const EDGE_CONFIG_KEYS = new Set(['target']);
15
+
16
+ // Valid edge targets
17
+ const EDGE_TARGETS = new Set(['cloudflare', 'deno', 'vercel', 'lambda', 'bun']);
18
+
19
+ // Edge binding keywords (contextual identifiers)
20
+ const EDGE_BINDING_KEYWORDS = new Set(['kv', 'sql', 'storage', 'queue', 'env', 'secret']);
21
+
22
+ export function installEdgeParser(ParserClass) {
23
+ if (ParserClass.prototype._edgeParserInstalled) return;
24
+ ParserClass.prototype._edgeParserInstalled = true;
25
+
26
+ // Edge reuses parseRoute() and parseMiddleware() from the server parser
27
+ installServerParser(ParserClass);
28
+
29
+ ParserClass.prototype.parseEdgeBlock = function() {
30
+ const l = this.loc();
31
+ this.advance(); // consume 'edge'
32
+
33
+ // Optional name: edge "api" { }
34
+ let name = null;
35
+ if (this.check(TokenType.STRING)) {
36
+ name = this.advance().value;
37
+ }
38
+
39
+ this.expect(TokenType.LBRACE, "Expected '{' after 'edge'");
40
+ const body = [];
41
+
42
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
43
+ try {
44
+ const stmt = this.parseEdgeStatement();
45
+ if (stmt) body.push(stmt);
46
+ } catch (e) {
47
+ this.errors.push(e);
48
+ this._synchronizeBlock();
49
+ }
50
+ }
51
+
52
+ this.expect(TokenType.RBRACE, "Expected '}' to close edge block");
53
+ return new AST.EdgeBlock(body, l, name);
54
+ };
55
+
56
+ ParserClass.prototype.parseEdgeStatement = function() {
57
+ // route keyword → reuse server route parser
58
+ if (this.check(TokenType.ROUTE)) {
59
+ // Ensure server parser is installed (for parseRoute)
60
+ return this.parseRoute();
61
+ }
62
+
63
+ // Contextual keywords in edge blocks
64
+ if (this.check(TokenType.IDENTIFIER)) {
65
+ const val = this.current().value;
66
+
67
+ // middleware fn name(req, next) { ... }
68
+ if (val === 'middleware' && this.peek(1).type === TokenType.FN) {
69
+ return this.parseMiddleware();
70
+ }
71
+
72
+ // kv BINDING_NAME or kv BINDING_NAME { config }
73
+ if (val === 'kv') {
74
+ return this.parseEdgeKV();
75
+ }
76
+
77
+ // sql BINDING_NAME
78
+ if (val === 'sql') {
79
+ return this.parseEdgeSQL();
80
+ }
81
+
82
+ // storage BINDING_NAME
83
+ if (val === 'storage') {
84
+ return this.parseEdgeStorage();
85
+ }
86
+
87
+ // queue BINDING_NAME
88
+ if (val === 'queue') {
89
+ return this.parseEdgeQueue();
90
+ }
91
+
92
+ // env VAR_NAME = default_value
93
+ if (val === 'env') {
94
+ return this.parseEdgeEnv();
95
+ }
96
+
97
+ // secret SECRET_NAME
98
+ if (val === 'secret') {
99
+ return this.parseEdgeSecret();
100
+ }
101
+
102
+ // schedule "name" cron("...") { body }
103
+ if (val === 'schedule') {
104
+ return this.parseEdgeSchedule();
105
+ }
106
+
107
+ // consume QUEUE_NAME fn(messages) { body }
108
+ if (val === 'consume') {
109
+ return this.parseEdgeConsume();
110
+ }
111
+
112
+ // health "/path"
113
+ if (val === 'health') {
114
+ return this.parseHealthCheck();
115
+ }
116
+
117
+ // cors { ... }
118
+ if (val === 'cors') {
119
+ return this.parseCorsConfig();
120
+ }
121
+
122
+ // on_error fn(err, req) { ... }
123
+ if (val === 'on_error') {
124
+ return this.parseErrorHandler();
125
+ }
126
+
127
+ // Config field: identifier: value (e.g., target: "cloudflare")
128
+ // Accept any identifier + colon pattern; analyzer validates the key
129
+ if (this.peek(1).type === TokenType.COLON && !EDGE_BINDING_KEYWORDS.has(val)) {
130
+ return this.parseEdgeConfigField();
131
+ }
132
+ }
133
+
134
+ // fn or async fn → regular function declaration
135
+ if (this.check(TokenType.FN) ||
136
+ (this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FN)) {
137
+ return this.parseStatement();
138
+ }
139
+
140
+ // Fallback to regular statement
141
+ return this.parseStatement();
142
+ };
143
+
144
+ ParserClass.prototype.parseEdgeConfigField = function() {
145
+ const l = this.loc();
146
+ const key = this.advance().value; // consume identifier (e.g., 'target')
147
+ this.expect(TokenType.COLON, "Expected ':' after config key");
148
+ const value = this.parseExpression();
149
+ return new EdgeConfigField(key, value, l);
150
+ };
151
+
152
+ ParserClass.prototype.parseEdgeKV = function() {
153
+ const l = this.loc();
154
+ this.advance(); // consume 'kv'
155
+ const name = this.expect(TokenType.IDENTIFIER, "Expected KV binding name").value;
156
+ let config = null;
157
+ if (this.check(TokenType.LBRACE)) {
158
+ config = this._parseEdgeBindingConfig();
159
+ }
160
+ return new EdgeKVDeclaration(name, config, l);
161
+ };
162
+
163
+ ParserClass.prototype.parseEdgeSQL = function() {
164
+ const l = this.loc();
165
+ this.advance(); // consume 'sql'
166
+ const name = this.expect(TokenType.IDENTIFIER, "Expected SQL binding name").value;
167
+ let config = null;
168
+ if (this.check(TokenType.LBRACE)) {
169
+ config = this._parseEdgeBindingConfig();
170
+ }
171
+ return new EdgeSQLDeclaration(name, config, l);
172
+ };
173
+
174
+ ParserClass.prototype.parseEdgeStorage = function() {
175
+ const l = this.loc();
176
+ this.advance(); // consume 'storage'
177
+ const name = this.expect(TokenType.IDENTIFIER, "Expected storage binding name").value;
178
+ let config = null;
179
+ if (this.check(TokenType.LBRACE)) {
180
+ config = this._parseEdgeBindingConfig();
181
+ }
182
+ return new EdgeStorageDeclaration(name, config, l);
183
+ };
184
+
185
+ ParserClass.prototype.parseEdgeQueue = function() {
186
+ const l = this.loc();
187
+ this.advance(); // consume 'queue'
188
+ const name = this.expect(TokenType.IDENTIFIER, "Expected queue binding name").value;
189
+ let config = null;
190
+ if (this.check(TokenType.LBRACE)) {
191
+ config = this._parseEdgeBindingConfig();
192
+ }
193
+ return new EdgeQueueDeclaration(name, config, l);
194
+ };
195
+
196
+ ParserClass.prototype.parseEdgeEnv = function() {
197
+ const l = this.loc();
198
+ this.advance(); // consume 'env'
199
+ const name = this.expect(TokenType.IDENTIFIER, "Expected env var name").value;
200
+ let defaultValue = null;
201
+ if (this.match(TokenType.ASSIGN)) {
202
+ defaultValue = this.parseExpression();
203
+ }
204
+ return new EdgeEnvDeclaration(name, defaultValue, l);
205
+ };
206
+
207
+ ParserClass.prototype.parseEdgeSecret = function() {
208
+ const l = this.loc();
209
+ this.advance(); // consume 'secret'
210
+ const name = this.expect(TokenType.IDENTIFIER, "Expected secret name").value;
211
+ return new EdgeSecretDeclaration(name, l);
212
+ };
213
+
214
+ ParserClass.prototype.parseEdgeSchedule = function() {
215
+ const l = this.loc();
216
+ this.advance(); // consume 'schedule'
217
+ const name = this.expect(TokenType.STRING, "Expected schedule name string").value;
218
+
219
+ // cron("expression")
220
+ const cronIdent = this.expect(TokenType.IDENTIFIER, "Expected 'cron' after schedule name");
221
+ if (cronIdent.value !== 'cron') {
222
+ this.error("Expected 'cron' keyword after schedule name");
223
+ }
224
+ this.expect(TokenType.LPAREN, "Expected '(' after 'cron'");
225
+ const cronExpr = this.expect(TokenType.STRING, "Expected cron expression string").value;
226
+ this.expect(TokenType.RPAREN, "Expected ')' after cron expression");
227
+
228
+ const body = this.parseBlock();
229
+ return new EdgeScheduleDeclaration(name, cronExpr, body, l);
230
+ };
231
+
232
+ ParserClass.prototype.parseEdgeConsume = function() {
233
+ const l = this.loc();
234
+ this.advance(); // consume 'consume'
235
+ const queue = this.expect(TokenType.IDENTIFIER, "Expected queue name").value;
236
+
237
+ // fn(messages) { ... } or a function reference
238
+ this.expect(TokenType.FN, "Expected 'fn' after queue name in consume");
239
+ this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
240
+ const params = this.parseParameterList();
241
+ this.expect(TokenType.RPAREN, "Expected ')' after consume parameters");
242
+ const body = this.parseBlock();
243
+
244
+ const handler = new AST.LambdaExpression(params, body, l);
245
+ return new EdgeConsumeDeclaration(queue, handler, l);
246
+ };
247
+
248
+ // Helper: parse { key: value, ... } config block for bindings
249
+ ParserClass.prototype._parseEdgeBindingConfig = function() {
250
+ this.expect(TokenType.LBRACE, "Expected '{' for binding config");
251
+ const config = {};
252
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
253
+ const key = this.expect(TokenType.IDENTIFIER, "Expected config key").value;
254
+ this.expect(TokenType.COLON, "Expected ':' after config key");
255
+ const value = this.parseExpression();
256
+ config[key] = value;
257
+ this.match(TokenType.COMMA);
258
+ }
259
+ this.expect(TokenType.RBRACE, "Expected '}' to close binding config");
260
+ return config;
261
+ };
262
+ }
@@ -0,0 +1,80 @@
1
+ // Form-specific AST Node definitions for the Tova language
2
+ // Extracted for lazy loading — only loaded when form { } blocks are used.
3
+
4
+ // ============================================================
5
+ // Form-specific nodes
6
+ // ============================================================
7
+
8
+ export class FormDeclaration {
9
+ constructor(name, typeAnnotation, fields, groups, arrays, computeds, steps, onSubmit, loc) {
10
+ this.type = 'FormDeclaration';
11
+ this.name = name;
12
+ this.typeAnnotation = typeAnnotation;
13
+ this.fields = fields;
14
+ this.groups = groups;
15
+ this.arrays = arrays;
16
+ this.computeds = computeds;
17
+ this.steps = steps;
18
+ this.onSubmit = onSubmit;
19
+ this.loc = loc;
20
+ }
21
+ }
22
+
23
+ export class FormFieldDeclaration {
24
+ constructor(name, typeAnnotation, initialValue, validators, loc) {
25
+ this.type = 'FormFieldDeclaration';
26
+ this.name = name;
27
+ this.typeAnnotation = typeAnnotation;
28
+ this.initialValue = initialValue;
29
+ this.validators = validators;
30
+ this.loc = loc;
31
+ }
32
+ }
33
+
34
+ export class FormGroupDeclaration {
35
+ constructor(name, condition, fields, groups, loc) {
36
+ this.type = 'FormGroupDeclaration';
37
+ this.name = name;
38
+ this.condition = condition;
39
+ this.fields = fields;
40
+ this.groups = groups;
41
+ this.loc = loc;
42
+ }
43
+ }
44
+
45
+ export class FormArrayDeclaration {
46
+ constructor(name, fields, validators, loc) {
47
+ this.type = 'FormArrayDeclaration';
48
+ this.name = name;
49
+ this.fields = fields;
50
+ this.validators = validators;
51
+ this.loc = loc;
52
+ }
53
+ }
54
+
55
+ export class FormValidator {
56
+ constructor(name, args, isAsync, loc) {
57
+ this.type = 'FormValidator';
58
+ this.name = name;
59
+ this.args = args;
60
+ this.isAsync = isAsync;
61
+ this.loc = loc;
62
+ }
63
+ }
64
+
65
+ export class FormStepsDeclaration {
66
+ constructor(steps, loc) {
67
+ this.type = 'FormStepsDeclaration';
68
+ this.steps = steps;
69
+ this.loc = loc;
70
+ }
71
+ }
72
+
73
+ export class FormStep {
74
+ constructor(label, members, loc) {
75
+ this.type = 'FormStep';
76
+ this.label = label;
77
+ this.members = members;
78
+ this.loc = loc;
79
+ }
80
+ }
@@ -0,0 +1,206 @@
1
+ // Form-specific parser methods for the Tova language
2
+ // Extracted for lazy loading — only loaded when form { } blocks are encountered.
3
+
4
+ import {
5
+ FormDeclaration, FormFieldDeclaration, FormGroupDeclaration,
6
+ FormArrayDeclaration, FormValidator, FormStepsDeclaration, FormStep,
7
+ } from './form-ast.js';
8
+ import { TokenType } from '../lexer/tokens.js';
9
+
10
+ export function installFormParser(ParserClass) {
11
+ if (ParserClass.prototype._formParserInstalled) return;
12
+ ParserClass.prototype._formParserInstalled = true;
13
+
14
+ ParserClass.prototype.parseFormDeclaration = function() {
15
+ const l = this.loc();
16
+ this.expect(TokenType.FORM);
17
+ const name = this.expect(TokenType.IDENTIFIER, "Expected form name").value;
18
+
19
+ let typeAnnotation = null;
20
+ if (this.match(TokenType.COLON)) {
21
+ typeAnnotation = this.parseTypeAnnotation();
22
+ }
23
+
24
+ this.expect(TokenType.LBRACE, "Expected '{' after form name");
25
+
26
+ const fields = [];
27
+ const groups = [];
28
+ const arrays = [];
29
+ const computeds = [];
30
+ let steps = null;
31
+ let onSubmit = null;
32
+
33
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
34
+ if (this.check(TokenType.FIELD)) {
35
+ fields.push(this.parseFormField());
36
+ } else if (this.check(TokenType.GROUP)) {
37
+ groups.push(this.parseFormGroup());
38
+ } else if (this._checkFormContextual('array')) {
39
+ arrays.push(this.parseFormArray());
40
+ } else if (this.check(TokenType.COMPUTED)) {
41
+ computeds.push(this.parseComputed());
42
+ } else if (this.check(TokenType.STEPS)) {
43
+ steps = this.parseFormSteps();
44
+ } else if (this._checkFormContextual('on') && this._peekFormContextual(1, 'submit')) {
45
+ onSubmit = this.parseFormOnSubmit();
46
+ } else {
47
+ this.error("Expected 'field', 'group', 'array', 'computed', 'steps', or 'on submit' inside form block");
48
+ }
49
+ }
50
+
51
+ this.expect(TokenType.RBRACE, "Expected '}' to close form block");
52
+ return new FormDeclaration(name, typeAnnotation, fields, groups, arrays, computeds, steps, onSubmit, l);
53
+ };
54
+
55
+ ParserClass.prototype.parseFormField = function() {
56
+ const l = this.loc();
57
+ this.expect(TokenType.FIELD);
58
+ const name = this.expect(TokenType.IDENTIFIER, "Expected field name").value;
59
+
60
+ let typeAnnotation = null;
61
+ if (this.match(TokenType.COLON)) {
62
+ typeAnnotation = this.parseTypeAnnotation();
63
+ }
64
+
65
+ let initialValue = null;
66
+ if (this.match(TokenType.ASSIGN)) {
67
+ initialValue = this.parseExpression();
68
+ }
69
+
70
+ const validators = [];
71
+ if (this.check(TokenType.LBRACE)) {
72
+ this.advance(); // consume {
73
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
74
+ validators.push(this.parseFormValidator());
75
+ }
76
+ this.expect(TokenType.RBRACE, "Expected '}' to close validator block");
77
+ }
78
+
79
+ return new FormFieldDeclaration(name, typeAnnotation, initialValue, validators, l);
80
+ };
81
+
82
+ ParserClass.prototype.parseFormValidator = function() {
83
+ const l = this.loc();
84
+ let isAsync = false;
85
+ if (this.check(TokenType.ASYNC)) {
86
+ isAsync = true;
87
+ this.advance();
88
+ }
89
+ const name = this.expect(TokenType.IDENTIFIER, "Expected validator name").value;
90
+
91
+ const args = [];
92
+ if (this.match(TokenType.LPAREN)) {
93
+ if (!this.check(TokenType.RPAREN)) {
94
+ args.push(this.parseExpression());
95
+ while (this.match(TokenType.COMMA)) {
96
+ args.push(this.parseExpression());
97
+ }
98
+ }
99
+ this.expect(TokenType.RPAREN, "Expected ')' after validator arguments");
100
+ }
101
+
102
+ return new FormValidator(name, args, isAsync, l);
103
+ };
104
+
105
+ ParserClass.prototype.parseFormGroup = function() {
106
+ const l = this.loc();
107
+ this.expect(TokenType.GROUP);
108
+ const name = this.expect(TokenType.IDENTIFIER, "Expected group name").value;
109
+
110
+ let condition = null;
111
+ if (this.check(TokenType.WHEN)) {
112
+ this.advance(); // consume 'when'
113
+ condition = this.parseExpression();
114
+ }
115
+
116
+ this.expect(TokenType.LBRACE, "Expected '{' after group name");
117
+
118
+ const fields = [];
119
+ const groups = [];
120
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
121
+ if (this.check(TokenType.FIELD)) {
122
+ fields.push(this.parseFormField());
123
+ } else if (this.check(TokenType.GROUP)) {
124
+ groups.push(this.parseFormGroup());
125
+ } else {
126
+ this.error("Expected 'field' or 'group' inside form group");
127
+ }
128
+ }
129
+
130
+ this.expect(TokenType.RBRACE, "Expected '}' to close group block");
131
+ return new FormGroupDeclaration(name, condition, fields, groups, l);
132
+ };
133
+
134
+ ParserClass.prototype.parseFormArray = function() {
135
+ const l = this.loc();
136
+ this.advance(); // consume 'array' (contextual keyword — it's an IDENTIFIER with value "array")
137
+ const name = this.expect(TokenType.IDENTIFIER, "Expected array name").value;
138
+
139
+ this.expect(TokenType.LBRACE, "Expected '{' after array name");
140
+
141
+ const fields = [];
142
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
143
+ if (this.check(TokenType.FIELD)) {
144
+ fields.push(this.parseFormField());
145
+ } else {
146
+ this.error("Expected 'field' inside form array");
147
+ }
148
+ }
149
+
150
+ this.expect(TokenType.RBRACE, "Expected '}' to close array block");
151
+ return new FormArrayDeclaration(name, fields, [], l);
152
+ };
153
+
154
+ ParserClass.prototype.parseFormSteps = function() {
155
+ const l = this.loc();
156
+ this.expect(TokenType.STEPS);
157
+ this.expect(TokenType.LBRACE, "Expected '{' after steps");
158
+
159
+ const stepsArr = [];
160
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
161
+ stepsArr.push(this.parseFormStep());
162
+ }
163
+
164
+ this.expect(TokenType.RBRACE, "Expected '}' to close steps block");
165
+ return new FormStepsDeclaration(stepsArr, l);
166
+ };
167
+
168
+ ParserClass.prototype.parseFormStep = function() {
169
+ const l = this.loc();
170
+ if (!this._checkFormContextual('step')) {
171
+ this.error("Expected 'step' inside steps block");
172
+ }
173
+ this.advance(); // consume 'step'
174
+ const label = this.expect(TokenType.STRING, "Expected step label string").value;
175
+ this.expect(TokenType.LBRACE, "Expected '{' after step label");
176
+
177
+ const members = [];
178
+ members.push(this.expect(TokenType.IDENTIFIER, "Expected field/group/array name").value);
179
+ while (this.match(TokenType.COMMA)) {
180
+ members.push(this.expect(TokenType.IDENTIFIER, "Expected field/group/array name").value);
181
+ }
182
+
183
+ this.expect(TokenType.RBRACE, "Expected '}' to close step");
184
+ return new FormStep(label, members, l);
185
+ };
186
+
187
+ ParserClass.prototype.parseFormOnSubmit = function() {
188
+ this.advance(); // consume 'on'
189
+ this.advance(); // consume 'submit'
190
+ return this.parseBlock();
191
+ };
192
+
193
+ // Helper: check if current token is an identifier with a specific value
194
+ if (!ParserClass.prototype._checkFormContextual) {
195
+ ParserClass.prototype._checkFormContextual = function(name) {
196
+ return this.check(TokenType.IDENTIFIER) && this.current().value === name;
197
+ };
198
+ }
199
+
200
+ if (!ParserClass.prototype._peekFormContextual) {
201
+ ParserClass.prototype._peekFormContextual = function(offset, name) {
202
+ const token = this.peek(offset);
203
+ return token && token.type === TokenType.IDENTIFIER && token.value === name;
204
+ };
205
+ }
206
+ }