@lingo-dsl/compiler 0.1.0

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 (57) hide show
  1. package/dist/Compiler.d.ts +27 -0
  2. package/dist/Compiler.d.ts.map +1 -0
  3. package/dist/Compiler.js +53 -0
  4. package/dist/Compiler.js.map +1 -0
  5. package/dist/analyzer/ISemanticAnalyzer.d.ts +15 -0
  6. package/dist/analyzer/ISemanticAnalyzer.d.ts.map +1 -0
  7. package/dist/analyzer/ISemanticAnalyzer.js +3 -0
  8. package/dist/analyzer/ISemanticAnalyzer.js.map +1 -0
  9. package/dist/analyzer/LingoAnalyzer.d.ts +24 -0
  10. package/dist/analyzer/LingoAnalyzer.d.ts.map +1 -0
  11. package/dist/analyzer/LingoAnalyzer.js +204 -0
  12. package/dist/analyzer/LingoAnalyzer.js.map +1 -0
  13. package/dist/codegen/ICodeGenerator.d.ts +12 -0
  14. package/dist/codegen/ICodeGenerator.d.ts.map +1 -0
  15. package/dist/codegen/ICodeGenerator.js +3 -0
  16. package/dist/codegen/ICodeGenerator.js.map +1 -0
  17. package/dist/codegen/JSCodeGenerator.d.ts +31 -0
  18. package/dist/codegen/JSCodeGenerator.d.ts.map +1 -0
  19. package/dist/codegen/JSCodeGenerator.js +421 -0
  20. package/dist/codegen/JSCodeGenerator.js.map +1 -0
  21. package/dist/errors/ConsoleErrorReporter.d.ts +9 -0
  22. package/dist/errors/ConsoleErrorReporter.d.ts.map +1 -0
  23. package/dist/errors/ConsoleErrorReporter.js +26 -0
  24. package/dist/errors/ConsoleErrorReporter.js.map +1 -0
  25. package/dist/errors/IErrorReporter.d.ts +17 -0
  26. package/dist/errors/IErrorReporter.d.ts.map +1 -0
  27. package/dist/errors/IErrorReporter.js +9 -0
  28. package/dist/errors/IErrorReporter.js.map +1 -0
  29. package/dist/index.d.ts +15 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +32 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/parser/AST.d.ts +132 -0
  34. package/dist/parser/AST.d.ts.map +1 -0
  35. package/dist/parser/AST.js +15 -0
  36. package/dist/parser/AST.js.map +1 -0
  37. package/dist/parser/IParser.d.ts +6 -0
  38. package/dist/parser/IParser.d.ts.map +1 -0
  39. package/dist/parser/IParser.js +3 -0
  40. package/dist/parser/IParser.js.map +1 -0
  41. package/dist/parser/LingoParser.d.ts +50 -0
  42. package/dist/parser/LingoParser.d.ts.map +1 -0
  43. package/dist/parser/LingoParser.js +697 -0
  44. package/dist/parser/LingoParser.js.map +1 -0
  45. package/dist/tokenizer/ITokenizer.d.ts +5 -0
  46. package/dist/tokenizer/ITokenizer.d.ts.map +1 -0
  47. package/dist/tokenizer/ITokenizer.js +3 -0
  48. package/dist/tokenizer/ITokenizer.js.map +1 -0
  49. package/dist/tokenizer/LingoTokenizer.d.ts +31 -0
  50. package/dist/tokenizer/LingoTokenizer.d.ts.map +1 -0
  51. package/dist/tokenizer/LingoTokenizer.js +315 -0
  52. package/dist/tokenizer/LingoTokenizer.js.map +1 -0
  53. package/dist/tokenizer/Token.d.ts +107 -0
  54. package/dist/tokenizer/Token.d.ts.map +1 -0
  55. package/dist/tokenizer/Token.js +107 -0
  56. package/dist/tokenizer/Token.js.map +1 -0
  57. package/package.json +39 -0
@@ -0,0 +1,697 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LingoParser = void 0;
4
+ const Token_1 = require("../tokenizer/Token");
5
+ const IErrorReporter_1 = require("../errors/IErrorReporter");
6
+ const AST_1 = require("./AST");
7
+ class LingoParser {
8
+ constructor(errorReporter) {
9
+ this.errorReporter = errorReporter;
10
+ this.tokens = [];
11
+ this.current = 0;
12
+ }
13
+ parse(tokens) {
14
+ this.tokens = tokens;
15
+ this.current = 0;
16
+ const statements = [];
17
+ while (!this.isAtEnd()) {
18
+ // Skip newlines and comments at the top level
19
+ if (this.check(Token_1.TokenType.NEWLINE)) {
20
+ this.advance();
21
+ continue;
22
+ }
23
+ const stmt = this.parseStatement();
24
+ if (stmt) {
25
+ statements.push(stmt);
26
+ }
27
+ }
28
+ return {
29
+ type: AST_1.ASTNodeType.PROGRAM,
30
+ statements,
31
+ location: this.currentLocation(),
32
+ };
33
+ }
34
+ parseStatement() {
35
+ try {
36
+ if (this.check(Token_1.TokenType.THERE)) {
37
+ return this.parseStateDecl();
38
+ }
39
+ if (this.check(Token_1.TokenType.SHOW)) {
40
+ return this.parseShowStmt();
41
+ }
42
+ if (this.check(Token_1.TokenType.WHEN)) {
43
+ return this.parseEventBlock();
44
+ }
45
+ if (this.check(Token_1.TokenType.IF)) {
46
+ return this.parseIfBlock();
47
+ }
48
+ if (this.check(Token_1.TokenType.FOR)) {
49
+ return this.parseForEachBlock();
50
+ }
51
+ // Unknown statement, report error and skip to next line
52
+ this.reportError(`Unexpected token: ${this.peek().value}`);
53
+ this.synchronize();
54
+ return null;
55
+ }
56
+ catch (error) {
57
+ // Error already reported, synchronize to next statement
58
+ this.synchronize();
59
+ return null;
60
+ }
61
+ }
62
+ parseStateDecl() {
63
+ const location = this.currentLocation();
64
+ this.consume(Token_1.TokenType.THERE, "Expected 'There'");
65
+ this.consume(Token_1.TokenType.IS, "Expected 'is'");
66
+ // Article (a/an) - optional for backwards compatibility
67
+ if (this.check(Token_1.TokenType.A) || this.check(Token_1.TokenType.AN)) {
68
+ this.advance();
69
+ }
70
+ // Type
71
+ const varType = this.parseType();
72
+ this.consume(Token_1.TokenType.CALLED, "Expected 'called'");
73
+ const identifier = this.consumeIdentifierOrKeyword();
74
+ this.consume(Token_1.TokenType.STARTING, "Expected 'starting'");
75
+ // Optional 'at' keyword (as identifier 'at')
76
+ if (this.check(Token_1.TokenType.IDENTIFIER) && this.peek().value === "at") {
77
+ this.advance();
78
+ }
79
+ const initialValue = this.parseValue();
80
+ this.consume(Token_1.TokenType.PERIOD, "Expected '.'");
81
+ this.skipNewlines();
82
+ return {
83
+ type: AST_1.ASTNodeType.STATE_DECL,
84
+ varType,
85
+ identifier,
86
+ initialValue,
87
+ location,
88
+ };
89
+ }
90
+ parseType() {
91
+ const token = this.peek();
92
+ switch (token.type) {
93
+ case Token_1.TokenType.NUMBER_TYPE:
94
+ this.advance();
95
+ return "number";
96
+ case Token_1.TokenType.TEXT_TYPE:
97
+ case Token_1.TokenType.TEXT: // "text" can be both a type and a widget
98
+ this.advance();
99
+ return "text";
100
+ case Token_1.TokenType.BOOLEAN_TYPE:
101
+ this.advance();
102
+ return "boolean";
103
+ case Token_1.TokenType.LIST_TYPE:
104
+ this.advance();
105
+ return "list";
106
+ case Token_1.TokenType.INPUT:
107
+ case Token_1.TokenType.BUTTON:
108
+ case Token_1.TokenType.HEADING:
109
+ case Token_1.TokenType.IMAGE:
110
+ case Token_1.TokenType.ROW:
111
+ case Token_1.TokenType.COLUMN:
112
+ // Widgets can also be used as types for convenience
113
+ this.advance();
114
+ return "text"; // Treat widget types as text for now
115
+ default:
116
+ this.reportError(`Expected type, got '${token.value}'`);
117
+ this.advance();
118
+ return "number"; // Default fallback
119
+ }
120
+ }
121
+ parseValue() {
122
+ const token = this.peek();
123
+ if (token.type === Token_1.TokenType.NUMBER) {
124
+ this.advance();
125
+ return { type: "number", value: parseFloat(token.value) };
126
+ }
127
+ if (token.type === Token_1.TokenType.STRING) {
128
+ this.advance();
129
+ return { type: "text", value: token.value };
130
+ }
131
+ if (token.type === Token_1.TokenType.TRUE) {
132
+ this.advance();
133
+ return { type: "boolean", value: true };
134
+ }
135
+ if (token.type === Token_1.TokenType.FALSE) {
136
+ this.advance();
137
+ return { type: "boolean", value: false };
138
+ }
139
+ if (token.type === Token_1.TokenType.EMPTY) {
140
+ this.advance();
141
+ return { type: "empty" };
142
+ }
143
+ // Allow identifiers and widget/type keywords as identifier values
144
+ const allowedTypes = [
145
+ Token_1.TokenType.IDENTIFIER,
146
+ Token_1.TokenType.INPUT,
147
+ Token_1.TokenType.BUTTON,
148
+ Token_1.TokenType.TEXT,
149
+ Token_1.TokenType.HEADING,
150
+ Token_1.TokenType.IMAGE,
151
+ Token_1.TokenType.ROW,
152
+ Token_1.TokenType.COLUMN,
153
+ ];
154
+ if (allowedTypes.includes(token.type)) {
155
+ this.advance();
156
+ return { type: "identifier", name: token.value };
157
+ }
158
+ this.reportError(`Expected value, got '${token.value}'`);
159
+ this.advance();
160
+ return { type: "empty" };
161
+ }
162
+ parseShowStmt(lowercase = false) {
163
+ const location = this.currentLocation();
164
+ this.consume(Token_1.TokenType.SHOW, lowercase ? "Expected 'show'" : "Expected 'Show'");
165
+ // Article - optional for backwards compatibility
166
+ if (this.check(Token_1.TokenType.A) || this.check(Token_1.TokenType.AN)) {
167
+ this.advance();
168
+ }
169
+ // Check if this is a custom widget (identifier instead of keyword)
170
+ const isCustom = this.check(Token_1.TokenType.IDENTIFIER);
171
+ const widget = isCustom ? this.parseCustomWidget() : this.parseWidget();
172
+ const config = isCustom ? this.parseCustomConfig() : this.parseShowConfig();
173
+ this.consume(Token_1.TokenType.PERIOD, "Expected '.'");
174
+ this.skipNewlines();
175
+ return {
176
+ type: AST_1.ASTNodeType.SHOW_STMT,
177
+ widget,
178
+ config,
179
+ isCustom,
180
+ location,
181
+ };
182
+ }
183
+ parseCustomWidget() {
184
+ // Custom widget is just an identifier (e.g., "card", "modal", etc.)
185
+ const token = this.consume(Token_1.TokenType.IDENTIFIER, "Expected widget name");
186
+ return token.value;
187
+ }
188
+ parseCustomConfig() {
189
+ const params = {};
190
+ // Parse: with key "value" and key2 "value2" ...
191
+ if (this.check(Token_1.TokenType.WITH)) {
192
+ this.advance();
193
+ do {
194
+ // Parse parameter name (can be any identifier or keyword)
195
+ const paramName = this.consumeIdentifierOrKeyword();
196
+ // Parse parameter value (string)
197
+ const paramValue = this.consume(Token_1.TokenType.STRING, "Expected string value for parameter").value;
198
+ params[paramName] = paramValue;
199
+ // Check for "and" to continue with more parameters
200
+ if (this.check(Token_1.TokenType.AND)) {
201
+ this.advance();
202
+ }
203
+ else {
204
+ break;
205
+ }
206
+ } while (true);
207
+ }
208
+ return { type: "custom", params };
209
+ }
210
+ parseWidget() {
211
+ const token = this.peek();
212
+ const widgets = [
213
+ Token_1.TokenType.HEADING,
214
+ Token_1.TokenType.TEXT,
215
+ Token_1.TokenType.PARAGRAPH,
216
+ Token_1.TokenType.BUTTON,
217
+ Token_1.TokenType.INPUT,
218
+ Token_1.TokenType.TEXTAREA,
219
+ Token_1.TokenType.IMAGE,
220
+ Token_1.TokenType.ROW,
221
+ Token_1.TokenType.COLUMN,
222
+ Token_1.TokenType.CONTAINER,
223
+ Token_1.TokenType.DIVISION,
224
+ Token_1.TokenType.ITALIC,
225
+ Token_1.TokenType.BOLD,
226
+ Token_1.TokenType.STRONG,
227
+ Token_1.TokenType.EMPHASIS,
228
+ Token_1.TokenType.UNDERLINE,
229
+ Token_1.TokenType.SMALL,
230
+ Token_1.TokenType.MARK,
231
+ Token_1.TokenType.DELETED,
232
+ Token_1.TokenType.INSERTED,
233
+ Token_1.TokenType.SUBSCRIPT,
234
+ Token_1.TokenType.SUPERSCRIPT,
235
+ Token_1.TokenType.CODE,
236
+ Token_1.TokenType.PREFORMATTED,
237
+ Token_1.TokenType.QUOTE,
238
+ Token_1.TokenType.LINK,
239
+ Token_1.TokenType.UNORDERED_LIST,
240
+ Token_1.TokenType.ORDERED_LIST,
241
+ Token_1.TokenType.LISTITEM,
242
+ Token_1.TokenType.SECTION,
243
+ Token_1.TokenType.ARTICLE,
244
+ Token_1.TokenType.ASIDE,
245
+ Token_1.TokenType.HEADER,
246
+ Token_1.TokenType.FOOTER,
247
+ Token_1.TokenType.NAV,
248
+ Token_1.TokenType.MAIN,
249
+ Token_1.TokenType.SPAN,
250
+ Token_1.TokenType.LINEBREAK,
251
+ Token_1.TokenType.RULE,
252
+ Token_1.TokenType.TABLE,
253
+ Token_1.TokenType.TABLEROW,
254
+ Token_1.TokenType.TABLEDATA,
255
+ Token_1.TokenType.TABLEHEADER,
256
+ ];
257
+ if (!widgets.includes(token.type)) {
258
+ this.reportError(`Expected widget type, got '${token.value}'`);
259
+ this.advance();
260
+ return "text"; // Default fallback
261
+ }
262
+ this.advance();
263
+ // Map English-friendly names to HTML tags
264
+ return this.mapTokenToWidgetType(token.type);
265
+ }
266
+ mapTokenToWidgetType(tokenType) {
267
+ const mapping = {
268
+ // Original widgets - keep as-is for backwards compatibility
269
+ [Token_1.TokenType.HEADING]: "heading",
270
+ [Token_1.TokenType.TEXT]: "text",
271
+ [Token_1.TokenType.BUTTON]: "button",
272
+ [Token_1.TokenType.INPUT]: "input",
273
+ [Token_1.TokenType.IMAGE]: "image",
274
+ [Token_1.TokenType.ROW]: "row",
275
+ [Token_1.TokenType.COLUMN]: "column",
276
+ // New HTML-friendly widgets
277
+ [Token_1.TokenType.PARAGRAPH]: "p",
278
+ [Token_1.TokenType.TEXTAREA]: "textarea",
279
+ [Token_1.TokenType.CONTAINER]: "div",
280
+ [Token_1.TokenType.DIVISION]: "div",
281
+ [Token_1.TokenType.ITALIC]: "i",
282
+ [Token_1.TokenType.BOLD]: "b",
283
+ [Token_1.TokenType.STRONG]: "strong",
284
+ [Token_1.TokenType.EMPHASIS]: "em",
285
+ [Token_1.TokenType.UNDERLINE]: "u",
286
+ [Token_1.TokenType.SMALL]: "small",
287
+ [Token_1.TokenType.MARK]: "mark",
288
+ [Token_1.TokenType.DELETED]: "del",
289
+ [Token_1.TokenType.INSERTED]: "ins",
290
+ [Token_1.TokenType.SUBSCRIPT]: "sub",
291
+ [Token_1.TokenType.SUPERSCRIPT]: "sup",
292
+ [Token_1.TokenType.CODE]: "code",
293
+ [Token_1.TokenType.PREFORMATTED]: "pre",
294
+ [Token_1.TokenType.QUOTE]: "blockquote",
295
+ [Token_1.TokenType.LINK]: "a",
296
+ [Token_1.TokenType.UNORDERED_LIST]: "ul",
297
+ [Token_1.TokenType.ORDERED_LIST]: "ol",
298
+ [Token_1.TokenType.LISTITEM]: "li",
299
+ [Token_1.TokenType.SECTION]: "section",
300
+ [Token_1.TokenType.ARTICLE]: "article",
301
+ [Token_1.TokenType.ASIDE]: "aside",
302
+ [Token_1.TokenType.HEADER]: "header",
303
+ [Token_1.TokenType.FOOTER]: "footer",
304
+ [Token_1.TokenType.NAV]: "nav",
305
+ [Token_1.TokenType.MAIN]: "main",
306
+ [Token_1.TokenType.SPAN]: "span",
307
+ [Token_1.TokenType.LINEBREAK]: "br",
308
+ [Token_1.TokenType.RULE]: "hr",
309
+ [Token_1.TokenType.TABLE]: "table",
310
+ [Token_1.TokenType.TABLEROW]: "tr",
311
+ [Token_1.TokenType.TABLEDATA]: "td",
312
+ [Token_1.TokenType.TABLEHEADER]: "th",
313
+ };
314
+ return mapping[tokenType] || "text";
315
+ }
316
+ parseShowConfig() {
317
+ if (this.check(Token_1.TokenType.SAYING)) {
318
+ this.advance();
319
+ const template = this.consume(Token_1.TokenType.STRING, "Expected string after 'saying'").value;
320
+ return { type: "saying", template };
321
+ }
322
+ if (this.check(Token_1.TokenType.CALLED)) {
323
+ this.advance();
324
+ const identifier = this.consumeIdentifierOrKeyword();
325
+ return { type: "called", identifier };
326
+ }
327
+ if (this.check(Token_1.TokenType.WITH)) {
328
+ this.advance();
329
+ this.consume(Token_1.TokenType.SOURCE, "Expected 'source' after 'with'");
330
+ const source = this.consume(Token_1.TokenType.STRING, "Expected string after 'source'").value;
331
+ return { type: "image", source };
332
+ }
333
+ return { type: "empty" };
334
+ }
335
+ parseEventBlock() {
336
+ const location = this.currentLocation();
337
+ this.consume(Token_1.TokenType.WHEN, "Expected 'When'");
338
+ this.consume(Token_1.TokenType.I, "Expected 'I'");
339
+ const verb = this.parseEventVerb();
340
+ this.consume(Token_1.TokenType.THE, "Expected 'the'");
341
+ const widgetRef = this.parseWidgetRef();
342
+ this.consume(Token_1.TokenType.COMMA, "Expected ','");
343
+ this.skipNewlines();
344
+ const actions = [];
345
+ while (!this.isAtEnd() && !this.isStatementStart()) {
346
+ if (this.check(Token_1.TokenType.NEWLINE)) {
347
+ this.advance();
348
+ continue;
349
+ }
350
+ const action = this.parseActionStmt();
351
+ if (action) {
352
+ actions.push(action);
353
+ }
354
+ }
355
+ return {
356
+ type: AST_1.ASTNodeType.EVENT_BLOCK,
357
+ verb,
358
+ widgetRef,
359
+ actions,
360
+ location,
361
+ };
362
+ }
363
+ parseEventVerb() {
364
+ const token = this.peek();
365
+ if (token.type === Token_1.TokenType.CLICK) {
366
+ this.advance();
367
+ return "click";
368
+ }
369
+ if (token.type === Token_1.TokenType.TYPE) {
370
+ this.advance();
371
+ return "type";
372
+ }
373
+ this.reportError(`Expected event verb (click/type), got '${token.value}'`);
374
+ this.advance();
375
+ return "click";
376
+ }
377
+ parseWidgetRef() {
378
+ const widget = this.parseWidget();
379
+ if (this.check(Token_1.TokenType.STRING)) {
380
+ const label = this.advance().value;
381
+ return { type: "literal", widget, label };
382
+ }
383
+ if (this.check(Token_1.TokenType.CALLED)) {
384
+ this.advance();
385
+ const identifier = this.consume(Token_1.TokenType.IDENTIFIER, "Expected identifier").value;
386
+ return { type: "identifier", widget, identifier };
387
+ }
388
+ this.reportError("Expected string or 'called' after widget type");
389
+ return { type: "literal", widget, label: "" };
390
+ }
391
+ parseActionStmt() {
392
+ const location = this.currentLocation();
393
+ const action = this.parseAction();
394
+ if (!action)
395
+ return null;
396
+ this.consume(Token_1.TokenType.PERIOD, "Expected '.' after action");
397
+ this.skipNewlines();
398
+ return {
399
+ type: AST_1.ASTNodeType.ACTION_STMT,
400
+ action,
401
+ location,
402
+ };
403
+ }
404
+ parseAction() {
405
+ if (this.check(Token_1.TokenType.INCREASE)) {
406
+ return this.parseIncreaseAction();
407
+ }
408
+ if (this.check(Token_1.TokenType.DECREASE)) {
409
+ return this.parseDecreaseAction();
410
+ }
411
+ if (this.check(Token_1.TokenType.SET)) {
412
+ return this.parseSetAction();
413
+ }
414
+ if (this.check(Token_1.TokenType.ADD)) {
415
+ return this.parseAddAction();
416
+ }
417
+ if (this.check(Token_1.TokenType.REMOVE)) {
418
+ return this.parseRemoveAction();
419
+ }
420
+ if (this.check(Token_1.TokenType.TOGGLE)) {
421
+ return this.parseToggleAction();
422
+ }
423
+ // Check for custom action (identifier)
424
+ if (this.check(Token_1.TokenType.IDENTIFIER)) {
425
+ return this.parseCustomAction();
426
+ }
427
+ this.reportError(`Expected action keyword, got '${this.peek().value}'`);
428
+ return null;
429
+ }
430
+ parseCustomAction() {
431
+ const name = this.consume(Token_1.TokenType.IDENTIFIER, "Expected action name").value;
432
+ const identifier = this.consumeIdentifierOrKeyword();
433
+ // Optional parameters: with param1 "value1" and param2 "value2"
434
+ const params = {};
435
+ if (this.check(Token_1.TokenType.WITH)) {
436
+ this.advance();
437
+ do {
438
+ const paramName = this.consumeIdentifierOrKeyword();
439
+ const paramValue = this.consume(Token_1.TokenType.STRING, "Expected string value").value;
440
+ params[paramName] = paramValue;
441
+ if (this.check(Token_1.TokenType.AND)) {
442
+ this.advance();
443
+ }
444
+ else {
445
+ break;
446
+ }
447
+ } while (true);
448
+ }
449
+ return {
450
+ type: "custom",
451
+ name,
452
+ identifier,
453
+ params: Object.keys(params).length > 0 ? params : undefined,
454
+ };
455
+ }
456
+ parseIncreaseAction() {
457
+ this.consume(Token_1.TokenType.INCREASE, "Expected 'increase'");
458
+ const identifier = this.consumeIdentifierOrKeyword();
459
+ this.consume(Token_1.TokenType.BY, "Expected 'by'");
460
+ const amount = parseFloat(this.consume(Token_1.TokenType.NUMBER, "Expected number").value);
461
+ return { type: "increase", identifier, amount };
462
+ }
463
+ parseDecreaseAction() {
464
+ this.consume(Token_1.TokenType.DECREASE, "Expected 'decrease'");
465
+ const identifier = this.consumeIdentifierOrKeyword();
466
+ this.consume(Token_1.TokenType.BY, "Expected 'by'");
467
+ const amount = parseFloat(this.consume(Token_1.TokenType.NUMBER, "Expected number").value);
468
+ return { type: "decrease", identifier, amount };
469
+ }
470
+ parseSetAction() {
471
+ this.consume(Token_1.TokenType.SET, "Expected 'set'");
472
+ const identifier = this.consumeIdentifierOrKeyword();
473
+ this.consume(Token_1.TokenType.TO, "Expected 'to'");
474
+ const value = this.parseValue();
475
+ return { type: "set", identifier, value };
476
+ }
477
+ parseAddAction() {
478
+ this.consume(Token_1.TokenType.ADD, "Expected 'add'");
479
+ const value = this.parseValue();
480
+ this.consume(Token_1.TokenType.TO, "Expected 'to'");
481
+ const list = this.consumeIdentifierOrKeyword();
482
+ return { type: "add", value, list };
483
+ }
484
+ parseRemoveAction() {
485
+ this.consume(Token_1.TokenType.REMOVE, "Expected 'remove'");
486
+ const value = this.parseValue();
487
+ this.consume(Token_1.TokenType.FROM, "Expected 'from'");
488
+ const list = this.consumeIdentifierOrKeyword();
489
+ return { type: "remove", value, list };
490
+ }
491
+ parseToggleAction() {
492
+ this.consume(Token_1.TokenType.TOGGLE, "Expected 'toggle'");
493
+ const identifier = this.consumeIdentifierOrKeyword();
494
+ return { type: "toggle", identifier };
495
+ }
496
+ parseIfBlock() {
497
+ const location = this.currentLocation();
498
+ this.consume(Token_1.TokenType.IF, "Expected 'If'");
499
+ const condition = this.parseCondition();
500
+ this.consume(Token_1.TokenType.COMMA, "Expected ','");
501
+ this.skipNewlines();
502
+ const body = [];
503
+ while (!this.isAtEnd() && !this.isStatementStart()) {
504
+ if (this.check(Token_1.TokenType.NEWLINE)) {
505
+ this.advance();
506
+ continue;
507
+ }
508
+ const showStmt = this.parseShowStmt(true);
509
+ body.push(showStmt);
510
+ }
511
+ return {
512
+ type: AST_1.ASTNodeType.IF_BLOCK,
513
+ condition,
514
+ body,
515
+ location,
516
+ };
517
+ }
518
+ parseCondition() {
519
+ const location = this.currentLocation();
520
+ const identifier = this.consumeIdentifierOrKeyword();
521
+ this.consume(Token_1.TokenType.IS, "Expected 'is'");
522
+ const comparator = this.parseComparator();
523
+ const value = this.parseValue();
524
+ return {
525
+ type: AST_1.ASTNodeType.CONDITION,
526
+ identifier,
527
+ comparator,
528
+ value,
529
+ location,
530
+ };
531
+ }
532
+ parseComparator() {
533
+ // Handle 'not equal to'
534
+ if (this.check(Token_1.TokenType.NOT)) {
535
+ this.advance();
536
+ this.consume(Token_1.TokenType.EQUAL, "Expected 'equal'");
537
+ this.consume(Token_1.TokenType.TO, "Expected 'to'");
538
+ return "notEqual";
539
+ }
540
+ // Handle 'equal to'
541
+ if (this.check(Token_1.TokenType.EQUAL)) {
542
+ this.advance();
543
+ this.consume(Token_1.TokenType.TO, "Expected 'to'");
544
+ return "equal";
545
+ }
546
+ // Handle 'greater than [or equal to]'
547
+ if (this.check(Token_1.TokenType.GREATER)) {
548
+ this.advance();
549
+ this.consume(Token_1.TokenType.THAN, "Expected 'than'");
550
+ if (this.check(Token_1.TokenType.OR)) {
551
+ this.advance();
552
+ this.consume(Token_1.TokenType.EQUAL, "Expected 'equal'");
553
+ this.consume(Token_1.TokenType.TO, "Expected 'to'");
554
+ return "greaterOrEqual";
555
+ }
556
+ return "greater";
557
+ }
558
+ // Handle 'less than [or equal to]'
559
+ if (this.check(Token_1.TokenType.LESS)) {
560
+ this.advance();
561
+ this.consume(Token_1.TokenType.THAN, "Expected 'than'");
562
+ if (this.check(Token_1.TokenType.OR)) {
563
+ this.advance();
564
+ this.consume(Token_1.TokenType.EQUAL, "Expected 'equal'");
565
+ this.consume(Token_1.TokenType.TO, "Expected 'to'");
566
+ return "lessOrEqual";
567
+ }
568
+ return "less";
569
+ }
570
+ this.reportError("Expected comparator");
571
+ return "equal";
572
+ }
573
+ parseForEachBlock() {
574
+ const location = this.currentLocation();
575
+ this.consume(Token_1.TokenType.FOR, "Expected 'For'");
576
+ this.consume(Token_1.TokenType.EACH, "Expected 'each'");
577
+ const itemName = this.consumeIdentifierOrKeyword();
578
+ this.consume(Token_1.TokenType.IN, "Expected 'in'");
579
+ const listName = this.consumeIdentifierOrKeyword();
580
+ this.consume(Token_1.TokenType.COMMA, "Expected ','");
581
+ this.skipNewlines();
582
+ const body = [];
583
+ while (!this.isAtEnd() && !this.isStatementStart()) {
584
+ if (this.check(Token_1.TokenType.NEWLINE)) {
585
+ this.advance();
586
+ continue;
587
+ }
588
+ const showStmt = this.parseShowStmt(true);
589
+ body.push(showStmt);
590
+ }
591
+ return {
592
+ type: AST_1.ASTNodeType.FOR_EACH_BLOCK,
593
+ itemName,
594
+ listName,
595
+ body,
596
+ location,
597
+ };
598
+ }
599
+ // Helper methods
600
+ isStatementStart() {
601
+ const token = this.peek();
602
+ return (this.check(Token_1.TokenType.THERE) ||
603
+ (this.check(Token_1.TokenType.SHOW) && token.value === "Show") || // Only capitalized Show
604
+ this.check(Token_1.TokenType.WHEN) ||
605
+ this.check(Token_1.TokenType.IF) ||
606
+ this.check(Token_1.TokenType.FOR));
607
+ }
608
+ skipNewlines() {
609
+ while (this.check(Token_1.TokenType.NEWLINE)) {
610
+ this.advance();
611
+ }
612
+ }
613
+ check(type) {
614
+ if (this.isAtEnd())
615
+ return false;
616
+ return this.peek().type === type;
617
+ }
618
+ advance() {
619
+ if (!this.isAtEnd())
620
+ this.current++;
621
+ return this.previous();
622
+ }
623
+ isAtEnd() {
624
+ return this.peek().type === Token_1.TokenType.EOF;
625
+ }
626
+ peek() {
627
+ return this.tokens[this.current];
628
+ }
629
+ previous() {
630
+ return this.tokens[this.current - 1];
631
+ }
632
+ consumeIdentifierOrKeyword() {
633
+ const token = this.peek();
634
+ // Allow identifiers and almost any keyword as identifiers in certain contexts
635
+ // This is useful for variable names and custom widget parameters
636
+ const allowedTypes = [
637
+ Token_1.TokenType.IDENTIFIER,
638
+ // Widgets
639
+ Token_1.TokenType.INPUT,
640
+ Token_1.TokenType.BUTTON,
641
+ Token_1.TokenType.TEXT,
642
+ Token_1.TokenType.HEADING,
643
+ Token_1.TokenType.IMAGE,
644
+ Token_1.TokenType.ROW,
645
+ Token_1.TokenType.COLUMN,
646
+ // Types
647
+ Token_1.TokenType.NUMBER_TYPE,
648
+ Token_1.TokenType.TEXT_TYPE,
649
+ Token_1.TokenType.BOOLEAN_TYPE,
650
+ Token_1.TokenType.LIST_TYPE,
651
+ // Other keywords that might be used as parameter names
652
+ Token_1.TokenType.TYPE,
653
+ Token_1.TokenType.SOURCE,
654
+ Token_1.TokenType.BY,
655
+ Token_1.TokenType.TO,
656
+ Token_1.TokenType.FROM,
657
+ Token_1.TokenType.WITH,
658
+ Token_1.TokenType.AT,
659
+ ];
660
+ if (allowedTypes.includes(token.type)) {
661
+ this.advance();
662
+ return token.value;
663
+ }
664
+ this.reportError("Expected identifier");
665
+ throw new Error("Expected identifier");
666
+ }
667
+ consume(type, message) {
668
+ if (this.check(type))
669
+ return this.advance();
670
+ this.reportError(message);
671
+ throw new Error(message);
672
+ }
673
+ currentLocation() {
674
+ return this.peek().location;
675
+ }
676
+ reportError(message) {
677
+ this.errorReporter.report({
678
+ message,
679
+ location: this.currentLocation(),
680
+ severity: IErrorReporter_1.ErrorSeverity.ERROR,
681
+ });
682
+ }
683
+ synchronize() {
684
+ this.advance();
685
+ while (!this.isAtEnd()) {
686
+ if (this.previous().type === Token_1.TokenType.NEWLINE) {
687
+ return;
688
+ }
689
+ if (this.isStatementStart()) {
690
+ return;
691
+ }
692
+ this.advance();
693
+ }
694
+ }
695
+ }
696
+ exports.LingoParser = LingoParser;
697
+ //# sourceMappingURL=LingoParser.js.map