@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.
- package/dist/Compiler.d.ts +27 -0
- package/dist/Compiler.d.ts.map +1 -0
- package/dist/Compiler.js +53 -0
- package/dist/Compiler.js.map +1 -0
- package/dist/analyzer/ISemanticAnalyzer.d.ts +15 -0
- package/dist/analyzer/ISemanticAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/ISemanticAnalyzer.js +3 -0
- package/dist/analyzer/ISemanticAnalyzer.js.map +1 -0
- package/dist/analyzer/LingoAnalyzer.d.ts +24 -0
- package/dist/analyzer/LingoAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/LingoAnalyzer.js +204 -0
- package/dist/analyzer/LingoAnalyzer.js.map +1 -0
- package/dist/codegen/ICodeGenerator.d.ts +12 -0
- package/dist/codegen/ICodeGenerator.d.ts.map +1 -0
- package/dist/codegen/ICodeGenerator.js +3 -0
- package/dist/codegen/ICodeGenerator.js.map +1 -0
- package/dist/codegen/JSCodeGenerator.d.ts +31 -0
- package/dist/codegen/JSCodeGenerator.d.ts.map +1 -0
- package/dist/codegen/JSCodeGenerator.js +421 -0
- package/dist/codegen/JSCodeGenerator.js.map +1 -0
- package/dist/errors/ConsoleErrorReporter.d.ts +9 -0
- package/dist/errors/ConsoleErrorReporter.d.ts.map +1 -0
- package/dist/errors/ConsoleErrorReporter.js +26 -0
- package/dist/errors/ConsoleErrorReporter.js.map +1 -0
- package/dist/errors/IErrorReporter.d.ts +17 -0
- package/dist/errors/IErrorReporter.d.ts.map +1 -0
- package/dist/errors/IErrorReporter.js +9 -0
- package/dist/errors/IErrorReporter.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/AST.d.ts +132 -0
- package/dist/parser/AST.d.ts.map +1 -0
- package/dist/parser/AST.js +15 -0
- package/dist/parser/AST.js.map +1 -0
- package/dist/parser/IParser.d.ts +6 -0
- package/dist/parser/IParser.d.ts.map +1 -0
- package/dist/parser/IParser.js +3 -0
- package/dist/parser/IParser.js.map +1 -0
- package/dist/parser/LingoParser.d.ts +50 -0
- package/dist/parser/LingoParser.d.ts.map +1 -0
- package/dist/parser/LingoParser.js +697 -0
- package/dist/parser/LingoParser.js.map +1 -0
- package/dist/tokenizer/ITokenizer.d.ts +5 -0
- package/dist/tokenizer/ITokenizer.d.ts.map +1 -0
- package/dist/tokenizer/ITokenizer.js +3 -0
- package/dist/tokenizer/ITokenizer.js.map +1 -0
- package/dist/tokenizer/LingoTokenizer.d.ts +31 -0
- package/dist/tokenizer/LingoTokenizer.d.ts.map +1 -0
- package/dist/tokenizer/LingoTokenizer.js +315 -0
- package/dist/tokenizer/LingoTokenizer.js.map +1 -0
- package/dist/tokenizer/Token.d.ts +107 -0
- package/dist/tokenizer/Token.d.ts.map +1 -0
- package/dist/tokenizer/Token.js +107 -0
- package/dist/tokenizer/Token.js.map +1 -0
- 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
|