clou-lang 0.2.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/LICENSE +21 -0
- package/README.md +128 -0
- package/ai/clou-ai-prompt.md +239 -0
- package/bin/clou.js +281 -0
- package/examples/calculator.clou +41 -0
- package/examples/hello-terminal.clou +22 -0
- package/examples/hello.clou +37 -0
- package/examples/hello.html +220 -0
- package/examples/multipage/about.html +319 -0
- package/examples/multipage/contact.html +308 -0
- package/examples/multipage/index.html +322 -0
- package/examples/multipage/shared.clou +19 -0
- package/examples/multipage/site.clou +102 -0
- package/examples/portfolio.clou +51 -0
- package/examples/portfolio.html +217 -0
- package/examples/quiz.clou +90 -0
- package/examples/showcase.clou +136 -0
- package/examples/showcase.html +410 -0
- package/examples/startup.clou +153 -0
- package/examples/startup.html +469 -0
- package/examples/themes-demo.clou +117 -0
- package/examples/themes-demo.html +429 -0
- package/package.json +48 -0
- package/playground/clou-browser.js +2576 -0
- package/playground/index.html +682 -0
- package/src/bundle-browser.js +62 -0
- package/src/compiler.js +761 -0
- package/src/devserver.js +154 -0
- package/src/index.js +87 -0
- package/src/lexer.js +456 -0
- package/src/parser.js +879 -0
- package/src/terminal-parser.js +358 -0
- package/src/terminal-runtime.js +310 -0
- package/src/themes.js +469 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
// Clou Language - Parser
|
|
2
|
+
// Converts tokens into an Abstract Syntax Tree (AST)
|
|
3
|
+
|
|
4
|
+
const { TokenType } = require('./lexer');
|
|
5
|
+
|
|
6
|
+
class ParseError extends Error {
|
|
7
|
+
constructor(message, token) {
|
|
8
|
+
const loc = token ? ` at line ${token.line}, col ${token.col}` : '';
|
|
9
|
+
super(`${message}${loc}`);
|
|
10
|
+
this.token = token;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class Parser {
|
|
15
|
+
constructor(tokens) {
|
|
16
|
+
this.tokens = tokens;
|
|
17
|
+
this.pos = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
peek() {
|
|
21
|
+
return this.tokens[this.pos];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
advance() {
|
|
25
|
+
const token = this.tokens[this.pos];
|
|
26
|
+
this.pos++;
|
|
27
|
+
return token;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
expect(type) {
|
|
31
|
+
const token = this.peek();
|
|
32
|
+
if (!token || token.type !== type) {
|
|
33
|
+
throw new ParseError(
|
|
34
|
+
`Expected ${type} but got ${token ? token.type : 'end of file'}`,
|
|
35
|
+
token
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return this.advance();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
match(type) {
|
|
42
|
+
if (this.peek() && this.peek().type === type) {
|
|
43
|
+
return this.advance();
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
skipNewlines() {
|
|
49
|
+
while (this.peek() && this.peek().type === TokenType.NEWLINE) {
|
|
50
|
+
this.advance();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Main entry: parse full program
|
|
55
|
+
parse() {
|
|
56
|
+
this.skipNewlines();
|
|
57
|
+
const pages = [];
|
|
58
|
+
const styles = [];
|
|
59
|
+
const templates = [];
|
|
60
|
+
const variables = [];
|
|
61
|
+
|
|
62
|
+
while (this.peek() && this.peek().type !== TokenType.EOF) {
|
|
63
|
+
this.skipNewlines();
|
|
64
|
+
if (!this.peek() || this.peek().type === TokenType.EOF) break;
|
|
65
|
+
|
|
66
|
+
if (this.peek().type === TokenType.IMPORT) {
|
|
67
|
+
// imports are handled at a higher level, store as-is
|
|
68
|
+
const imp = this.parseImport();
|
|
69
|
+
if (!this.ast_imports) this.ast_imports = [];
|
|
70
|
+
this.ast_imports.push(imp);
|
|
71
|
+
} else if (this.peek().type === TokenType.TEMPLATE) {
|
|
72
|
+
templates.push(this.parseTemplate());
|
|
73
|
+
} else if (this.peek().type === TokenType.SET) {
|
|
74
|
+
variables.push(this.parseSetVariable());
|
|
75
|
+
} else if (this.peek().type === TokenType.PAGE) {
|
|
76
|
+
pages.push(this.parsePage());
|
|
77
|
+
} else if (this.peek().type === TokenType.STYLE) {
|
|
78
|
+
styles.push(this.parseStyleBlock());
|
|
79
|
+
} else {
|
|
80
|
+
// Top-level elements (no page wrapper)
|
|
81
|
+
if (pages.length === 0) {
|
|
82
|
+
pages.push({ type: 'Page', title: 'Clou App', children: [] });
|
|
83
|
+
}
|
|
84
|
+
pages[0].children.push(this.parseElement());
|
|
85
|
+
}
|
|
86
|
+
this.skipNewlines();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { type: 'Program', pages, styles, templates, variables, imports: this.ast_imports || [] };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// page "Title": OR page "Title" at "/path":
|
|
93
|
+
parsePage() {
|
|
94
|
+
this.expect(TokenType.PAGE);
|
|
95
|
+
const title = this.expect(TokenType.STRING).value;
|
|
96
|
+
let route = null;
|
|
97
|
+
|
|
98
|
+
if (this.match(TokenType.AT)) {
|
|
99
|
+
route = this.expect(TokenType.STRING).value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.expect(TokenType.COLON);
|
|
103
|
+
this.skipNewlines();
|
|
104
|
+
|
|
105
|
+
const children = this.parseBlock();
|
|
106
|
+
return { type: 'Page', title, route, children };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// import "file.clou"
|
|
110
|
+
parseImport() {
|
|
111
|
+
this.expect(TokenType.IMPORT);
|
|
112
|
+
const file = this.expect(TokenType.STRING).value;
|
|
113
|
+
return { type: 'Import', file };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Parse an indented block of elements
|
|
117
|
+
parseBlock() {
|
|
118
|
+
const children = [];
|
|
119
|
+
if (!this.match(TokenType.INDENT)) return children;
|
|
120
|
+
|
|
121
|
+
this.skipNewlines();
|
|
122
|
+
while (this.peek() && this.peek().type !== TokenType.DEDENT && this.peek().type !== TokenType.EOF) {
|
|
123
|
+
this.skipNewlines();
|
|
124
|
+
if (this.peek() && this.peek().type !== TokenType.DEDENT && this.peek().type !== TokenType.EOF) {
|
|
125
|
+
children.push(this.parseElement());
|
|
126
|
+
}
|
|
127
|
+
this.skipNewlines();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.match(TokenType.DEDENT);
|
|
131
|
+
return children;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Parse a single element
|
|
135
|
+
parseElement() {
|
|
136
|
+
const token = this.peek();
|
|
137
|
+
if (!token) throw new ParseError('Unexpected end of file');
|
|
138
|
+
|
|
139
|
+
switch (token.type) {
|
|
140
|
+
case TokenType.TITLE: return this.parseTitle();
|
|
141
|
+
case TokenType.HEADING: return this.parseHeading();
|
|
142
|
+
case TokenType.TEXT: return this.parseText();
|
|
143
|
+
case TokenType.IMAGE: return this.parseImage();
|
|
144
|
+
case TokenType.VIDEO: return this.parseVideo();
|
|
145
|
+
case TokenType.BOX: return this.parseBox();
|
|
146
|
+
case TokenType.ROW: return this.parseRow();
|
|
147
|
+
case TokenType.BUTTON: return this.parseButton();
|
|
148
|
+
case TokenType.LINK: return this.parseLink();
|
|
149
|
+
case TokenType.INPUT: return this.parseInput();
|
|
150
|
+
case TokenType.LIST: return this.parseList();
|
|
151
|
+
case TokenType.LINE: return this.parseLine();
|
|
152
|
+
case TokenType.NAVBAR: return this.parseNavbar();
|
|
153
|
+
case TokenType.FOOTER: return this.parseFooter();
|
|
154
|
+
case TokenType.CARD: return this.parseCard();
|
|
155
|
+
case TokenType.ICON: return this.parseIcon();
|
|
156
|
+
case TokenType.MODAL: return this.parseModal();
|
|
157
|
+
case TokenType.GRID: return this.parseGrid();
|
|
158
|
+
case TokenType.SECTION: return this.parseSection();
|
|
159
|
+
case TokenType.SPACE: return this.parseSpace();
|
|
160
|
+
case TokenType.SET: return this.parseSetVariable();
|
|
161
|
+
case TokenType.USE: return this.parseUse();
|
|
162
|
+
case TokenType.REPEAT: return this.parseRepeat();
|
|
163
|
+
case TokenType.THEME: return this.parseTheme();
|
|
164
|
+
case TokenType.SHOW: return this.parseShow();
|
|
165
|
+
case TokenType.HIDE: return this.parseHide();
|
|
166
|
+
case TokenType.TOGGLE: return this.parseToggle();
|
|
167
|
+
case TokenType.OPEN: return this.parseOpen();
|
|
168
|
+
case TokenType.CLOSE: return this.parseClose();
|
|
169
|
+
case TokenType.GO: return this.parseGo();
|
|
170
|
+
case TokenType.STYLE: return this.parseStyleBlock();
|
|
171
|
+
// Style properties
|
|
172
|
+
case TokenType.BACKGROUND: return this.parseStyleProp();
|
|
173
|
+
case TokenType.COLOR_KW: return this.parseStyleProp();
|
|
174
|
+
case TokenType.SIZE: return this.parseStyleProp();
|
|
175
|
+
case TokenType.BOLD: return this.parseSimpleStyle();
|
|
176
|
+
case TokenType.ITALIC: return this.parseSimpleStyle();
|
|
177
|
+
case TokenType.CENTER: return this.parseSimpleStyle();
|
|
178
|
+
case TokenType.LEFT: return this.parseSimpleStyle();
|
|
179
|
+
case TokenType.RIGHT: return this.parseSimpleStyle();
|
|
180
|
+
case TokenType.ROUNDED: return this.parseSimpleStyle();
|
|
181
|
+
case TokenType.SHADOW: return this.parseSimpleStyle();
|
|
182
|
+
case TokenType.PADDING: return this.parseStyleProp();
|
|
183
|
+
case TokenType.MARGIN: return this.parseStyleProp();
|
|
184
|
+
case TokenType.WIDTH: return this.parseStyleProp();
|
|
185
|
+
case TokenType.HEIGHT: return this.parseStyleProp();
|
|
186
|
+
case TokenType.FONT: return this.parseStyleProp();
|
|
187
|
+
case TokenType.GAP: return this.parseStyleProp();
|
|
188
|
+
case TokenType.GRADIENT: return this.parseGradient();
|
|
189
|
+
case TokenType.BORDER: return this.parseStyleProp();
|
|
190
|
+
case TokenType.OPACITY: return this.parseStyleProp();
|
|
191
|
+
case TokenType.ANIMATE: return this.parseAnimate();
|
|
192
|
+
case TokenType.FULL: return this.parseSimpleStyle();
|
|
193
|
+
case TokenType.DARK: return this.parseSimpleStyle();
|
|
194
|
+
case TokenType.LIGHT: return this.parseSimpleStyle();
|
|
195
|
+
case TokenType.SMALL: return this.parseSimpleStyle();
|
|
196
|
+
case TokenType.BIG: return this.parseSimpleStyle();
|
|
197
|
+
case TokenType.HUGE: return this.parseSimpleStyle();
|
|
198
|
+
case TokenType.TINY: return this.parseSimpleStyle();
|
|
199
|
+
case TokenType.STICKY: return this.parseSimpleStyle();
|
|
200
|
+
case TokenType.FIXED: return this.parseSimpleStyle();
|
|
201
|
+
case TokenType.WRAP: return this.parseSimpleStyle();
|
|
202
|
+
case TokenType.GROW: return this.parseSimpleStyle();
|
|
203
|
+
case TokenType.HOVER: return this.parseHoverBlock();
|
|
204
|
+
case TokenType.COLUMNS: return this.parseStyleProp();
|
|
205
|
+
default:
|
|
206
|
+
this.advance();
|
|
207
|
+
return { type: 'Unknown', value: token.value };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// set name to "value"
|
|
212
|
+
parseSetVariable() {
|
|
213
|
+
this.expect(TokenType.SET);
|
|
214
|
+
const name = this.expect(TokenType.IDENTIFIER).value;
|
|
215
|
+
this.expect(TokenType.TO);
|
|
216
|
+
let value;
|
|
217
|
+
if (this.peek() && this.peek().type === TokenType.STRING) {
|
|
218
|
+
value = this.advance().value;
|
|
219
|
+
} else if (this.peek() && this.peek().type === TokenType.NUMBER) {
|
|
220
|
+
value = this.advance().value;
|
|
221
|
+
} else {
|
|
222
|
+
value = this.advance().value;
|
|
223
|
+
}
|
|
224
|
+
return { type: 'SetVariable', name, value };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// template name(param1, param2):
|
|
228
|
+
parseTemplate() {
|
|
229
|
+
this.expect(TokenType.TEMPLATE);
|
|
230
|
+
const name = this.advance().value; // template name (identifier or keyword)
|
|
231
|
+
const params = [];
|
|
232
|
+
|
|
233
|
+
if (this.match(TokenType.LPAREN)) {
|
|
234
|
+
// Parse parameter names
|
|
235
|
+
if (this.peek() && this.peek().type !== TokenType.RPAREN) {
|
|
236
|
+
params.push(this.advance().value);
|
|
237
|
+
while (this.match(TokenType.COMMA)) {
|
|
238
|
+
params.push(this.advance().value);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
this.expect(TokenType.RPAREN);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.expect(TokenType.COLON);
|
|
245
|
+
this.skipNewlines();
|
|
246
|
+
const children = this.parseBlock();
|
|
247
|
+
|
|
248
|
+
return { type: 'Template', name, params, children };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// use templateName("arg1", "arg2")
|
|
252
|
+
parseUse() {
|
|
253
|
+
this.expect(TokenType.USE);
|
|
254
|
+
const name = this.advance().value;
|
|
255
|
+
const args = [];
|
|
256
|
+
|
|
257
|
+
if (this.match(TokenType.LPAREN)) {
|
|
258
|
+
if (this.peek() && this.peek().type !== TokenType.RPAREN) {
|
|
259
|
+
args.push(this.advance().value);
|
|
260
|
+
while (this.match(TokenType.COMMA)) {
|
|
261
|
+
args.push(this.advance().value);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
this.expect(TokenType.RPAREN);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { type: 'UseTemplate', name, args };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// repeat 3:
|
|
271
|
+
parseRepeat() {
|
|
272
|
+
this.expect(TokenType.REPEAT);
|
|
273
|
+
const count = this.expect(TokenType.NUMBER).value;
|
|
274
|
+
this.expect(TokenType.COLON);
|
|
275
|
+
this.skipNewlines();
|
|
276
|
+
const children = this.parseBlock();
|
|
277
|
+
|
|
278
|
+
return { type: 'Repeat', count: parseInt(count, 10), children };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// theme "name"
|
|
282
|
+
parseTheme() {
|
|
283
|
+
this.expect(TokenType.THEME);
|
|
284
|
+
const name = this.expect(TokenType.STRING).value;
|
|
285
|
+
return { type: 'Theme', name };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// title "text"
|
|
289
|
+
parseTitle() {
|
|
290
|
+
this.expect(TokenType.TITLE);
|
|
291
|
+
const value = this.expect(TokenType.STRING).value;
|
|
292
|
+
return { type: 'Title', value };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// heading "text"
|
|
296
|
+
parseHeading() {
|
|
297
|
+
this.expect(TokenType.HEADING);
|
|
298
|
+
const value = this.expect(TokenType.STRING).value;
|
|
299
|
+
const node = { type: 'Heading', value, children: [] };
|
|
300
|
+
|
|
301
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
302
|
+
this.advance();
|
|
303
|
+
this.skipNewlines();
|
|
304
|
+
node.children = this.parseBlock();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return node;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// text "content"
|
|
311
|
+
parseText() {
|
|
312
|
+
this.expect(TokenType.TEXT);
|
|
313
|
+
|
|
314
|
+
// "text color x" is a style property
|
|
315
|
+
if (this.peek() && this.peek().type === TokenType.COLOR_KW) {
|
|
316
|
+
return this.parseStylePropFrom('text');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const value = this.expect(TokenType.STRING).value;
|
|
320
|
+
const node = { type: 'Text', value, children: [] };
|
|
321
|
+
|
|
322
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
323
|
+
this.advance();
|
|
324
|
+
this.skipNewlines();
|
|
325
|
+
node.children = this.parseBlock();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return node;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// image "src"
|
|
332
|
+
parseImage() {
|
|
333
|
+
this.expect(TokenType.IMAGE);
|
|
334
|
+
const src = this.expect(TokenType.STRING).value;
|
|
335
|
+
const node = { type: 'Image', src, children: [] };
|
|
336
|
+
|
|
337
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
338
|
+
this.advance();
|
|
339
|
+
this.skipNewlines();
|
|
340
|
+
node.children = this.parseBlock();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return node;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// video "src"
|
|
347
|
+
parseVideo() {
|
|
348
|
+
this.expect(TokenType.VIDEO);
|
|
349
|
+
const src = this.expect(TokenType.STRING).value;
|
|
350
|
+
return { type: 'Video', src };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// box:
|
|
354
|
+
parseBox() {
|
|
355
|
+
this.expect(TokenType.BOX);
|
|
356
|
+
const node = { type: 'Box', name: null, children: [] };
|
|
357
|
+
|
|
358
|
+
if (this.peek() && this.peek().type === TokenType.STRING) {
|
|
359
|
+
node.name = this.advance().value;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
363
|
+
this.advance();
|
|
364
|
+
this.skipNewlines();
|
|
365
|
+
node.children = this.parseBlock();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return node;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// row:
|
|
372
|
+
parseRow() {
|
|
373
|
+
this.expect(TokenType.ROW);
|
|
374
|
+
const node = { type: 'Row', children: [] };
|
|
375
|
+
|
|
376
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
377
|
+
this.advance();
|
|
378
|
+
this.skipNewlines();
|
|
379
|
+
node.children = this.parseBlock();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return node;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// navbar:
|
|
386
|
+
parseNavbar() {
|
|
387
|
+
this.expect(TokenType.NAVBAR);
|
|
388
|
+
const node = { type: 'Navbar', children: [] };
|
|
389
|
+
|
|
390
|
+
if (this.peek() && this.peek().type === TokenType.STRING) {
|
|
391
|
+
node.brand = this.advance().value;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
395
|
+
this.advance();
|
|
396
|
+
this.skipNewlines();
|
|
397
|
+
node.children = this.parseBlock();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return node;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// footer:
|
|
404
|
+
parseFooter() {
|
|
405
|
+
this.expect(TokenType.FOOTER);
|
|
406
|
+
const node = { type: 'Footer', children: [] };
|
|
407
|
+
|
|
408
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
409
|
+
this.advance();
|
|
410
|
+
this.skipNewlines();
|
|
411
|
+
node.children = this.parseBlock();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return node;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// card:
|
|
418
|
+
parseCard() {
|
|
419
|
+
this.expect(TokenType.CARD);
|
|
420
|
+
const node = { type: 'Card', name: null, children: [] };
|
|
421
|
+
|
|
422
|
+
if (this.peek() && this.peek().type === TokenType.STRING) {
|
|
423
|
+
node.name = this.advance().value;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
427
|
+
this.advance();
|
|
428
|
+
this.skipNewlines();
|
|
429
|
+
node.children = this.parseBlock();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return node;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// icon "name"
|
|
436
|
+
parseIcon() {
|
|
437
|
+
this.expect(TokenType.ICON);
|
|
438
|
+
const value = this.expect(TokenType.STRING).value;
|
|
439
|
+
return { type: 'Icon', value };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// modal "name":
|
|
443
|
+
parseModal() {
|
|
444
|
+
this.expect(TokenType.MODAL);
|
|
445
|
+
const name = this.expect(TokenType.STRING).value;
|
|
446
|
+
const node = { type: 'Modal', name, children: [] };
|
|
447
|
+
|
|
448
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
449
|
+
this.advance();
|
|
450
|
+
this.skipNewlines();
|
|
451
|
+
node.children = this.parseBlock();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return node;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// grid:
|
|
458
|
+
parseGrid() {
|
|
459
|
+
this.expect(TokenType.GRID);
|
|
460
|
+
const node = { type: 'Grid', children: [] };
|
|
461
|
+
|
|
462
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
463
|
+
this.advance();
|
|
464
|
+
this.skipNewlines();
|
|
465
|
+
node.children = this.parseBlock();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return node;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// section:
|
|
472
|
+
parseSection() {
|
|
473
|
+
this.expect(TokenType.SECTION);
|
|
474
|
+
const node = { type: 'Section', name: null, children: [] };
|
|
475
|
+
|
|
476
|
+
if (this.peek() && this.peek().type === TokenType.STRING) {
|
|
477
|
+
node.name = this.advance().value;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
481
|
+
this.advance();
|
|
482
|
+
this.skipNewlines();
|
|
483
|
+
node.children = this.parseBlock();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return node;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// space 20
|
|
490
|
+
parseSpace() {
|
|
491
|
+
this.expect(TokenType.SPACE);
|
|
492
|
+
let value = '20';
|
|
493
|
+
if (this.peek() && this.peek().type === TokenType.NUMBER) {
|
|
494
|
+
value = this.advance().value;
|
|
495
|
+
}
|
|
496
|
+
return { type: 'Space', value };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// button "text":
|
|
500
|
+
parseButton() {
|
|
501
|
+
this.expect(TokenType.BUTTON);
|
|
502
|
+
|
|
503
|
+
// "button color x" is a style
|
|
504
|
+
if (this.peek() && this.peek().type === TokenType.COLOR_KW) {
|
|
505
|
+
return this.parseStylePropFrom('button');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const label = this.expect(TokenType.STRING).value;
|
|
509
|
+
const node = { type: 'Button', label, children: [] };
|
|
510
|
+
|
|
511
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
512
|
+
this.advance();
|
|
513
|
+
this.skipNewlines();
|
|
514
|
+
node.children = this.parseBlock();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return node;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// link "text" to "url"
|
|
521
|
+
parseLink() {
|
|
522
|
+
this.expect(TokenType.LINK);
|
|
523
|
+
const label = this.expect(TokenType.STRING).value;
|
|
524
|
+
this.expect(TokenType.TO);
|
|
525
|
+
const url = this.expect(TokenType.STRING).value;
|
|
526
|
+
return { type: 'Link', label, url };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// input "placeholder"
|
|
530
|
+
parseInput() {
|
|
531
|
+
this.expect(TokenType.INPUT);
|
|
532
|
+
const placeholder = this.expect(TokenType.STRING).value;
|
|
533
|
+
const node = { type: 'Input', placeholder, children: [] };
|
|
534
|
+
|
|
535
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
536
|
+
this.advance();
|
|
537
|
+
this.skipNewlines();
|
|
538
|
+
node.children = this.parseBlock();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return node;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// list:
|
|
545
|
+
parseList() {
|
|
546
|
+
this.expect(TokenType.LIST);
|
|
547
|
+
const node = { type: 'List', children: [] };
|
|
548
|
+
|
|
549
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
550
|
+
this.advance();
|
|
551
|
+
this.skipNewlines();
|
|
552
|
+
node.children = this.parseBlock();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return node;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// line
|
|
559
|
+
parseLine() {
|
|
560
|
+
this.advance();
|
|
561
|
+
return { type: 'Line' };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// show message "text"
|
|
565
|
+
parseShow() {
|
|
566
|
+
this.expect(TokenType.SHOW);
|
|
567
|
+
if (this.match(TokenType.MESSAGE)) {
|
|
568
|
+
const value = this.expect(TokenType.STRING).value;
|
|
569
|
+
return { type: 'ShowMessage', value };
|
|
570
|
+
}
|
|
571
|
+
const target = this.expect(TokenType.STRING).value;
|
|
572
|
+
return { type: 'Show', target };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// hide "elementName"
|
|
576
|
+
parseHide() {
|
|
577
|
+
this.expect(TokenType.HIDE);
|
|
578
|
+
const target = this.expect(TokenType.STRING).value;
|
|
579
|
+
return { type: 'Hide', target };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// toggle "elementName"
|
|
583
|
+
parseToggle() {
|
|
584
|
+
this.expect(TokenType.TOGGLE);
|
|
585
|
+
const target = this.expect(TokenType.STRING).value;
|
|
586
|
+
return { type: 'Toggle', target };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// open "modalName"
|
|
590
|
+
parseOpen() {
|
|
591
|
+
this.expect(TokenType.OPEN);
|
|
592
|
+
const target = this.expect(TokenType.STRING).value;
|
|
593
|
+
return { type: 'Open', target };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// close "modalName"
|
|
597
|
+
parseClose() {
|
|
598
|
+
this.expect(TokenType.CLOSE);
|
|
599
|
+
const target = this.expect(TokenType.STRING).value;
|
|
600
|
+
return { type: 'Close', target };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// go to "url"
|
|
604
|
+
parseGo() {
|
|
605
|
+
this.expect(TokenType.GO);
|
|
606
|
+
this.expect(TokenType.TO);
|
|
607
|
+
const url = this.expect(TokenType.STRING).value;
|
|
608
|
+
return { type: 'GoTo', url };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// animate fade / animate slide / animate bounce / animate grow
|
|
612
|
+
parseAnimate() {
|
|
613
|
+
this.expect(TokenType.ANIMATE);
|
|
614
|
+
const animation = this.advance().value;
|
|
615
|
+
return { type: 'StyleProp', property: 'animation', value: animation };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// gradient blue to purple
|
|
619
|
+
parseGradient() {
|
|
620
|
+
this.expect(TokenType.GRADIENT);
|
|
621
|
+
const color1 = this.advance().value;
|
|
622
|
+
this.expect(TokenType.TO);
|
|
623
|
+
const color2 = this.advance().value;
|
|
624
|
+
return { type: 'StyleProp', property: 'gradient', value: `${color1},${color2}` };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// hover:
|
|
628
|
+
parseHoverBlock() {
|
|
629
|
+
this.expect(TokenType.HOVER);
|
|
630
|
+
const node = { type: 'HoverBlock', properties: [] };
|
|
631
|
+
|
|
632
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
633
|
+
this.advance();
|
|
634
|
+
this.skipNewlines();
|
|
635
|
+
if (this.match(TokenType.INDENT)) {
|
|
636
|
+
this.skipNewlines();
|
|
637
|
+
while (this.peek() && this.peek().type !== TokenType.DEDENT && this.peek().type !== TokenType.EOF) {
|
|
638
|
+
this.skipNewlines();
|
|
639
|
+
if (this.peek() && this.peek().type !== TokenType.DEDENT && this.peek().type !== TokenType.EOF) {
|
|
640
|
+
const prop = this.parseStyleProperty();
|
|
641
|
+
if (prop) node.properties.push(prop);
|
|
642
|
+
}
|
|
643
|
+
this.skipNewlines();
|
|
644
|
+
}
|
|
645
|
+
this.match(TokenType.DEDENT);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return node;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// style:
|
|
653
|
+
parseStyleBlock() {
|
|
654
|
+
this.expect(TokenType.STYLE);
|
|
655
|
+
const node = { type: 'StyleBlock', properties: [] };
|
|
656
|
+
|
|
657
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
658
|
+
this.advance();
|
|
659
|
+
this.skipNewlines();
|
|
660
|
+
|
|
661
|
+
if (this.match(TokenType.INDENT)) {
|
|
662
|
+
this.skipNewlines();
|
|
663
|
+
while (this.peek() && this.peek().type !== TokenType.DEDENT && this.peek().type !== TokenType.EOF) {
|
|
664
|
+
this.skipNewlines();
|
|
665
|
+
if (this.peek() && this.peek().type !== TokenType.DEDENT && this.peek().type !== TokenType.EOF) {
|
|
666
|
+
const prop = this.parseStyleProperty();
|
|
667
|
+
if (prop) node.properties.push(prop);
|
|
668
|
+
}
|
|
669
|
+
this.skipNewlines();
|
|
670
|
+
}
|
|
671
|
+
this.match(TokenType.DEDENT);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return node;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Parse a style property like "background color blue"
|
|
679
|
+
parseStyleProperty() {
|
|
680
|
+
const token = this.peek();
|
|
681
|
+
if (!token) return null;
|
|
682
|
+
|
|
683
|
+
switch (token.type) {
|
|
684
|
+
case TokenType.BACKGROUND: {
|
|
685
|
+
this.advance();
|
|
686
|
+
this.match(TokenType.COLOR_KW);
|
|
687
|
+
const value = this.advance().value;
|
|
688
|
+
return { property: 'background-color', value };
|
|
689
|
+
}
|
|
690
|
+
case TokenType.TEXT: {
|
|
691
|
+
this.advance();
|
|
692
|
+
this.expect(TokenType.COLOR_KW);
|
|
693
|
+
const value = this.advance().value;
|
|
694
|
+
return { property: 'color', value };
|
|
695
|
+
}
|
|
696
|
+
case TokenType.BUTTON: {
|
|
697
|
+
this.advance();
|
|
698
|
+
this.expect(TokenType.COLOR_KW);
|
|
699
|
+
const value = this.advance().value;
|
|
700
|
+
return { property: 'button-color', value };
|
|
701
|
+
}
|
|
702
|
+
case TokenType.SIZE: {
|
|
703
|
+
this.advance();
|
|
704
|
+
const value = this.advance().value;
|
|
705
|
+
return { property: 'font-size', value };
|
|
706
|
+
}
|
|
707
|
+
case TokenType.FONT: {
|
|
708
|
+
this.advance();
|
|
709
|
+
const value = this.advance().value;
|
|
710
|
+
return { property: 'font-family', value };
|
|
711
|
+
}
|
|
712
|
+
case TokenType.PADDING: {
|
|
713
|
+
this.advance();
|
|
714
|
+
const value = this.advance().value;
|
|
715
|
+
return { property: 'padding', value };
|
|
716
|
+
}
|
|
717
|
+
case TokenType.MARGIN: {
|
|
718
|
+
this.advance();
|
|
719
|
+
const value = this.advance().value;
|
|
720
|
+
return { property: 'margin', value };
|
|
721
|
+
}
|
|
722
|
+
case TokenType.WIDTH: {
|
|
723
|
+
this.advance();
|
|
724
|
+
const value = this.advance().value;
|
|
725
|
+
return { property: 'width', value };
|
|
726
|
+
}
|
|
727
|
+
case TokenType.HEIGHT: {
|
|
728
|
+
this.advance();
|
|
729
|
+
const value = this.advance().value;
|
|
730
|
+
return { property: 'height', value };
|
|
731
|
+
}
|
|
732
|
+
case TokenType.GAP: {
|
|
733
|
+
this.advance();
|
|
734
|
+
const value = this.advance().value;
|
|
735
|
+
return { property: 'gap', value };
|
|
736
|
+
}
|
|
737
|
+
case TokenType.BORDER: {
|
|
738
|
+
this.advance();
|
|
739
|
+
const value = this.advance().value;
|
|
740
|
+
return { property: 'border', value };
|
|
741
|
+
}
|
|
742
|
+
case TokenType.OPACITY: {
|
|
743
|
+
this.advance();
|
|
744
|
+
const value = this.advance().value;
|
|
745
|
+
return { property: 'opacity', value };
|
|
746
|
+
}
|
|
747
|
+
case TokenType.COLUMNS: {
|
|
748
|
+
this.advance();
|
|
749
|
+
const value = this.advance().value;
|
|
750
|
+
return { property: 'columns', value };
|
|
751
|
+
}
|
|
752
|
+
case TokenType.GRADIENT: {
|
|
753
|
+
this.advance();
|
|
754
|
+
const color1 = this.advance().value;
|
|
755
|
+
this.expect(TokenType.TO);
|
|
756
|
+
const color2 = this.advance().value;
|
|
757
|
+
return { property: 'gradient', value: `${color1},${color2}` };
|
|
758
|
+
}
|
|
759
|
+
case TokenType.BOLD: {
|
|
760
|
+
this.advance();
|
|
761
|
+
return { property: 'font-weight', value: 'bold' };
|
|
762
|
+
}
|
|
763
|
+
case TokenType.ITALIC: {
|
|
764
|
+
this.advance();
|
|
765
|
+
return { property: 'font-style', value: 'italic' };
|
|
766
|
+
}
|
|
767
|
+
case TokenType.CENTER: {
|
|
768
|
+
this.advance();
|
|
769
|
+
return { property: 'text-align', value: 'center' };
|
|
770
|
+
}
|
|
771
|
+
case TokenType.LEFT: {
|
|
772
|
+
this.advance();
|
|
773
|
+
return { property: 'text-align', value: 'left' };
|
|
774
|
+
}
|
|
775
|
+
case TokenType.RIGHT: {
|
|
776
|
+
this.advance();
|
|
777
|
+
return { property: 'text-align', value: 'right' };
|
|
778
|
+
}
|
|
779
|
+
case TokenType.ROUNDED: {
|
|
780
|
+
this.advance();
|
|
781
|
+
return { property: 'border-radius', value: '12px' };
|
|
782
|
+
}
|
|
783
|
+
case TokenType.SHADOW: {
|
|
784
|
+
this.advance();
|
|
785
|
+
return { property: 'box-shadow', value: '0 4px 12px rgba(0,0,0,0.15)' };
|
|
786
|
+
}
|
|
787
|
+
case TokenType.COLOR_KW: {
|
|
788
|
+
this.advance();
|
|
789
|
+
const value = this.advance().value;
|
|
790
|
+
return { property: 'color', value };
|
|
791
|
+
}
|
|
792
|
+
case TokenType.FULL: {
|
|
793
|
+
this.advance();
|
|
794
|
+
return { property: 'width', value: '100%' };
|
|
795
|
+
}
|
|
796
|
+
case TokenType.DARK: {
|
|
797
|
+
this.advance();
|
|
798
|
+
return { property: 'theme', value: 'dark' };
|
|
799
|
+
}
|
|
800
|
+
case TokenType.LIGHT: {
|
|
801
|
+
this.advance();
|
|
802
|
+
return { property: 'theme', value: 'light' };
|
|
803
|
+
}
|
|
804
|
+
case TokenType.SMALL: {
|
|
805
|
+
this.advance();
|
|
806
|
+
return { property: 'font-size', value: '14px' };
|
|
807
|
+
}
|
|
808
|
+
case TokenType.BIG: {
|
|
809
|
+
this.advance();
|
|
810
|
+
return { property: 'font-size', value: '24px' };
|
|
811
|
+
}
|
|
812
|
+
case TokenType.HUGE: {
|
|
813
|
+
this.advance();
|
|
814
|
+
return { property: 'font-size', value: '48px' };
|
|
815
|
+
}
|
|
816
|
+
case TokenType.TINY: {
|
|
817
|
+
this.advance();
|
|
818
|
+
return { property: 'font-size', value: '12px' };
|
|
819
|
+
}
|
|
820
|
+
case TokenType.STICKY: {
|
|
821
|
+
this.advance();
|
|
822
|
+
return { property: 'position', value: 'sticky; top: 0; z-index: 100' };
|
|
823
|
+
}
|
|
824
|
+
case TokenType.FIXED: {
|
|
825
|
+
this.advance();
|
|
826
|
+
return { property: 'position', value: 'fixed' };
|
|
827
|
+
}
|
|
828
|
+
case TokenType.GROW: {
|
|
829
|
+
this.advance();
|
|
830
|
+
return { property: 'flex', value: '1' };
|
|
831
|
+
}
|
|
832
|
+
case TokenType.ANIMATE: {
|
|
833
|
+
this.advance();
|
|
834
|
+
const anim = this.advance().value;
|
|
835
|
+
return { property: 'animation', value: anim };
|
|
836
|
+
}
|
|
837
|
+
default:
|
|
838
|
+
this.advance();
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Style property like "text color white" or "button color red"
|
|
844
|
+
parseStylePropFrom(prefix) {
|
|
845
|
+
this.expect(TokenType.COLOR_KW);
|
|
846
|
+
const value = this.advance().value;
|
|
847
|
+
if (prefix === 'text') {
|
|
848
|
+
return { type: 'StyleProp', property: 'color', value };
|
|
849
|
+
}
|
|
850
|
+
if (prefix === 'button') {
|
|
851
|
+
return { type: 'StyleProp', property: 'button-color', value };
|
|
852
|
+
}
|
|
853
|
+
return { type: 'StyleProp', property: prefix, value };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Style as element (inside a box/element block)
|
|
857
|
+
parseStyleProp() {
|
|
858
|
+
const prop = this.parseStyleProperty();
|
|
859
|
+
if (prop) {
|
|
860
|
+
return { type: 'StyleProp', ...prop };
|
|
861
|
+
}
|
|
862
|
+
return { type: 'Unknown' };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
parseSimpleStyle() {
|
|
866
|
+
const prop = this.parseStyleProperty();
|
|
867
|
+
if (prop) {
|
|
868
|
+
return { type: 'StyleProp', ...prop };
|
|
869
|
+
}
|
|
870
|
+
return { type: 'Unknown' };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function parse(tokens) {
|
|
875
|
+
const parser = new Parser(tokens);
|
|
876
|
+
return parser.parse();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
module.exports = { parse, Parser, ParseError };
|