clou-lang 0.3.0 → 0.3.5
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/bin/clou.js +24 -3
- package/examples/hello.html +80 -2
- package/examples/themes-demo.clou +1 -1
- package/examples/themes-demo.html +76 -48
- package/package.json +1 -1
- package/playground/clou-browser.js +523 -5
- package/playground/index.html +72 -0
- package/src/errors.js +159 -29
- package/src/lexer.js +9 -0
- package/src/terminal-parser.js +9 -0
- package/src/terminal-runtime.js +15 -0
package/bin/clou.js
CHANGED
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { buildClou } = require('../src/index');
|
|
14
|
-
const { formatError } = require('../src/errors');
|
|
14
|
+
const { formatError, formatWarnings, scanWarnings, color } = require('../src/errors');
|
|
15
15
|
|
|
16
16
|
const args = process.argv.slice(2);
|
|
17
17
|
|
|
18
18
|
function printHelp() {
|
|
19
19
|
console.log(`
|
|
20
20
|
╔═══════════════════════════════════════╗
|
|
21
|
-
║ CLOU Language v0.
|
|
22
|
-
║
|
|
21
|
+
║ CLOU Language v0.3.5 ║
|
|
22
|
+
║ Describe it once. Run everywhere. ║
|
|
23
23
|
╚═══════════════════════════════════════╝
|
|
24
24
|
|
|
25
25
|
Usage:
|
|
@@ -41,6 +41,14 @@ function printHelp() {
|
|
|
41
41
|
clou mysite.clou
|
|
42
42
|
clou build mysite.clou -o index.html
|
|
43
43
|
clou dev mysite.clou
|
|
44
|
+
clou deploy mysite.clou
|
|
45
|
+
|
|
46
|
+
Elements (30+):
|
|
47
|
+
page, heading, text, image, video, button, link,
|
|
48
|
+
input, textarea, checkbox, dropdown, slider, form,
|
|
49
|
+
list, box, row, grid, card, section, navbar, footer,
|
|
50
|
+
modal, icon, space, table, tabs, accordion, progress,
|
|
51
|
+
audio, code
|
|
44
52
|
|
|
45
53
|
Themes:
|
|
46
54
|
neon, ocean, sunset, forest, candy,
|
|
@@ -124,6 +132,12 @@ async function runTerminal(inputFile) {
|
|
|
124
132
|
function runFile(inputFile) {
|
|
125
133
|
const source = fs.readFileSync(inputFile, 'utf-8');
|
|
126
134
|
|
|
135
|
+
// Scan for warnings (typos, etc.)
|
|
136
|
+
const warnings = scanWarnings(source, inputFile);
|
|
137
|
+
if (warnings.length > 0) {
|
|
138
|
+
console.error(formatWarnings(warnings, source, inputFile));
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
// Auto-detect: terminal app or website?
|
|
128
142
|
if (isTerminalApp(source)) {
|
|
129
143
|
runTerminal(inputFile).catch(err => {
|
|
@@ -158,6 +172,13 @@ function runFile(inputFile) {
|
|
|
158
172
|
|
|
159
173
|
function buildFile(inputFile, outputFile) {
|
|
160
174
|
const source = fs.readFileSync(inputFile, 'utf-8');
|
|
175
|
+
|
|
176
|
+
// Scan for warnings (typos, etc.)
|
|
177
|
+
const warnings = scanWarnings(source, inputFile);
|
|
178
|
+
if (warnings.length > 0) {
|
|
179
|
+
console.error(formatWarnings(warnings, source, inputFile));
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
const basePath = path.dirname(path.resolve(inputFile));
|
|
162
183
|
const result = buildClou(source, { basePath, filename: inputFile });
|
|
163
184
|
|
package/examples/hello.html
CHANGED
|
@@ -174,6 +174,82 @@
|
|
|
174
174
|
display: inline-block;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
.clou-table {
|
|
178
|
+
width: 100%;
|
|
179
|
+
border-collapse: collapse;
|
|
180
|
+
margin: 16px 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.clou-table th, .clou-table td {
|
|
184
|
+
padding: 12px 16px;
|
|
185
|
+
text-align: left;
|
|
186
|
+
border-bottom: 1px solid rgba(128,128,128,0.2);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.clou-table th {
|
|
190
|
+
font-weight: 600;
|
|
191
|
+
opacity: 0.8;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.clou-table tr:hover td {
|
|
195
|
+
background: rgba(128,128,128,0.05);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.clou-form {
|
|
199
|
+
margin: 16px 0;
|
|
200
|
+
display: flex;
|
|
201
|
+
flex-direction: column;
|
|
202
|
+
gap: 12px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.clou-checkbox {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
gap: 8px;
|
|
209
|
+
margin: 8px 0;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.clou-checkbox input[type="checkbox"] {
|
|
214
|
+
width: 18px;
|
|
215
|
+
height: 18px;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.clou-code {
|
|
220
|
+
background: rgba(0,0,0,0.3);
|
|
221
|
+
border-radius: 8px;
|
|
222
|
+
padding: 16px 20px;
|
|
223
|
+
margin: 12px 0;
|
|
224
|
+
overflow-x: auto;
|
|
225
|
+
font-family: 'Fira Code', 'Consolas', monospace;
|
|
226
|
+
font-size: 14px;
|
|
227
|
+
line-height: 1.5;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.clou-audio {
|
|
231
|
+
width: 100%;
|
|
232
|
+
max-width: 400px;
|
|
233
|
+
margin: 8px 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.clou-dropdown {
|
|
237
|
+
margin: 8px 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.clou-dropdown label {
|
|
241
|
+
display: block;
|
|
242
|
+
margin-bottom: 6px;
|
|
243
|
+
font-size: 14px;
|
|
244
|
+
opacity: 0.8;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
select.clou-input {
|
|
248
|
+
width: auto;
|
|
249
|
+
min-width: 200px;
|
|
250
|
+
cursor: pointer;
|
|
251
|
+
}
|
|
252
|
+
|
|
177
253
|
.clou-footer {
|
|
178
254
|
padding: 40px 20px;
|
|
179
255
|
margin-top: 40px;
|
|
@@ -192,11 +268,12 @@
|
|
|
192
268
|
<p>It's so easy, anyone can do it!</p>
|
|
193
269
|
</div>
|
|
194
270
|
<hr>
|
|
195
|
-
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
196
274
|
<h2>About Me</h2>
|
|
197
275
|
<p>I love coding with Clou because it's simple and fun.</p>
|
|
198
276
|
<img src="https://picsum.photos/600/300" alt="image">
|
|
199
|
-
</div>
|
|
200
277
|
<hr>
|
|
201
278
|
<div class="clou-row">
|
|
202
279
|
<button id="clou-1">Say Hello</button>
|
|
@@ -208,6 +285,7 @@
|
|
|
208
285
|
<li>Fun to use</li>
|
|
209
286
|
<li>Makes real websites</li>
|
|
210
287
|
</ul>
|
|
288
|
+
|
|
211
289
|
<script>
|
|
212
290
|
document.getElementById('clou-1').addEventListener('click', function() {
|
|
213
291
|
alert("Hello from Clou!");
|
|
@@ -172,6 +172,82 @@
|
|
|
172
172
|
display: inline-block;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
.clou-table {
|
|
176
|
+
width: 100%;
|
|
177
|
+
border-collapse: collapse;
|
|
178
|
+
margin: 16px 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.clou-table th, .clou-table td {
|
|
182
|
+
padding: 12px 16px;
|
|
183
|
+
text-align: left;
|
|
184
|
+
border-bottom: 1px solid rgba(128,128,128,0.2);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.clou-table th {
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
opacity: 0.8;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.clou-table tr:hover td {
|
|
193
|
+
background: rgba(128,128,128,0.05);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.clou-form {
|
|
197
|
+
margin: 16px 0;
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
gap: 12px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.clou-checkbox {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
gap: 8px;
|
|
207
|
+
margin: 8px 0;
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.clou-checkbox input[type="checkbox"] {
|
|
212
|
+
width: 18px;
|
|
213
|
+
height: 18px;
|
|
214
|
+
cursor: pointer;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.clou-code {
|
|
218
|
+
background: rgba(0,0,0,0.3);
|
|
219
|
+
border-radius: 8px;
|
|
220
|
+
padding: 16px 20px;
|
|
221
|
+
margin: 12px 0;
|
|
222
|
+
overflow-x: auto;
|
|
223
|
+
font-family: 'Fira Code', 'Consolas', monospace;
|
|
224
|
+
font-size: 14px;
|
|
225
|
+
line-height: 1.5;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.clou-audio {
|
|
229
|
+
width: 100%;
|
|
230
|
+
max-width: 400px;
|
|
231
|
+
margin: 8px 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.clou-dropdown {
|
|
235
|
+
margin: 8px 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.clou-dropdown label {
|
|
239
|
+
display: block;
|
|
240
|
+
margin-bottom: 6px;
|
|
241
|
+
font-size: 14px;
|
|
242
|
+
opacity: 0.8;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
select.clou-input {
|
|
246
|
+
width: auto;
|
|
247
|
+
min-width: 200px;
|
|
248
|
+
cursor: pointer;
|
|
249
|
+
}
|
|
250
|
+
|
|
175
251
|
.clou-footer {
|
|
176
252
|
padding: 40px 20px;
|
|
177
253
|
margin-top: 40px;
|
|
@@ -231,54 +307,6 @@
|
|
|
231
307
|
}
|
|
232
308
|
.clou-animate-grow { animation: clou-grow 0.5s ease both; }
|
|
233
309
|
|
|
234
|
-
/* Theme: Ocean */
|
|
235
|
-
body {
|
|
236
|
-
background: linear-gradient(180deg, #0c1b33 0%, #1a3a5c 50%, #0c1b33 100%);
|
|
237
|
-
background-attachment: fixed;
|
|
238
|
-
color: #c8dbe6;
|
|
239
|
-
font-family: 'Georgia', 'Times New Roman', serif;
|
|
240
|
-
}
|
|
241
|
-
button, .clou-button {
|
|
242
|
-
background-color: #4fc3f7;
|
|
243
|
-
}
|
|
244
|
-
button:hover, .clou-button:hover {
|
|
245
|
-
background-color: #29b6f6;
|
|
246
|
-
}
|
|
247
|
-
.clou-card {
|
|
248
|
-
background: rgba(79, 195, 247, 0.08);
|
|
249
|
-
border: 1px solid rgba(79, 195, 247, 0.15);
|
|
250
|
-
backdrop-filter: blur(12px);
|
|
251
|
-
}
|
|
252
|
-
.clou-card:hover {
|
|
253
|
-
background: rgba(79, 195, 247, 0.12);
|
|
254
|
-
box-shadow: 0 8px 32px rgba(79, 195, 247, 0.15);
|
|
255
|
-
}
|
|
256
|
-
.clou-navbar {
|
|
257
|
-
background: rgba(12, 27, 51, 0.9);
|
|
258
|
-
backdrop-filter: blur(12px);
|
|
259
|
-
}
|
|
260
|
-
input, textarea, .clou-input {
|
|
261
|
-
background: rgba(79, 195, 247, 0.08);
|
|
262
|
-
border: 1px solid rgba(79, 195, 247, 0.25);
|
|
263
|
-
color: #c8dbe6;
|
|
264
|
-
}
|
|
265
|
-
input:focus, textarea:focus {
|
|
266
|
-
border-color: #4fc3f7;
|
|
267
|
-
box-shadow: 0 0 0 3px #4fc3f733;
|
|
268
|
-
}
|
|
269
|
-
h1, h2 {
|
|
270
|
-
color: #4fc3f7;
|
|
271
|
-
}
|
|
272
|
-
a {
|
|
273
|
-
color: #4fc3f7;
|
|
274
|
-
}
|
|
275
|
-
hr {
|
|
276
|
-
border-top-color: rgba(79, 195, 247, 0.2);
|
|
277
|
-
}
|
|
278
|
-
.clou-footer {
|
|
279
|
-
border-top-color: rgba(79, 195, 247, 0.2);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
310
|
</style>
|
|
283
311
|
</head>
|
|
284
312
|
<body>
|
package/package.json
CHANGED
|
@@ -45,6 +45,21 @@ const TokenType = {
|
|
|
45
45
|
SECTION: 'SECTION',
|
|
46
46
|
SPACE: 'SPACE',
|
|
47
47
|
COLUMNS: 'COLUMNS',
|
|
48
|
+
FORM: 'FORM',
|
|
49
|
+
TABLE: 'TABLE',
|
|
50
|
+
TABS: 'TABS',
|
|
51
|
+
TAB: 'TAB',
|
|
52
|
+
ACCORDION: 'ACCORDION',
|
|
53
|
+
PANEL: 'PANEL',
|
|
54
|
+
PROGRESS: 'PROGRESS',
|
|
55
|
+
DROPDOWN: 'DROPDOWN',
|
|
56
|
+
OPTION: 'OPTION',
|
|
57
|
+
TEXTAREA: 'TEXTAREA',
|
|
58
|
+
CHECKBOX: 'CHECKBOX',
|
|
59
|
+
AUDIO: 'AUDIO',
|
|
60
|
+
CODE: 'CODE',
|
|
61
|
+
SLIDER: 'SLIDER',
|
|
62
|
+
SUBMIT: 'SUBMIT',
|
|
48
63
|
|
|
49
64
|
// Keywords - Actions
|
|
50
65
|
SHOW: 'SHOW',
|
|
@@ -136,6 +151,10 @@ const TokenType = {
|
|
|
136
151
|
RPAREN: 'RPAREN',
|
|
137
152
|
COMMA: 'COMMA',
|
|
138
153
|
|
|
154
|
+
// Keywords - Imports & Routing
|
|
155
|
+
IMPORT: 'IMPORT',
|
|
156
|
+
AT: 'AT',
|
|
157
|
+
|
|
139
158
|
// Keywords - Connectors
|
|
140
159
|
TO: 'TO',
|
|
141
160
|
AND: 'AND',
|
|
@@ -168,6 +187,21 @@ const KEYWORDS = {
|
|
|
168
187
|
'section': TokenType.SECTION,
|
|
169
188
|
'space': TokenType.SPACE,
|
|
170
189
|
'columns': TokenType.COLUMNS,
|
|
190
|
+
'form': TokenType.FORM,
|
|
191
|
+
'table': TokenType.TABLE,
|
|
192
|
+
'tabs': TokenType.TABS,
|
|
193
|
+
'tab': TokenType.TAB,
|
|
194
|
+
'accordion': TokenType.ACCORDION,
|
|
195
|
+
'panel': TokenType.PANEL,
|
|
196
|
+
'progress': TokenType.PROGRESS,
|
|
197
|
+
'dropdown': TokenType.DROPDOWN,
|
|
198
|
+
'option': TokenType.OPTION,
|
|
199
|
+
'textarea': TokenType.TEXTAREA,
|
|
200
|
+
'checkbox': TokenType.CHECKBOX,
|
|
201
|
+
'audio': TokenType.AUDIO,
|
|
202
|
+
'code': TokenType.CODE,
|
|
203
|
+
'slider': TokenType.SLIDER,
|
|
204
|
+
'submit': TokenType.SUBMIT,
|
|
171
205
|
'show': TokenType.SHOW,
|
|
172
206
|
'message': TokenType.MESSAGE,
|
|
173
207
|
'hide': TokenType.HIDE,
|
|
@@ -247,6 +281,8 @@ const KEYWORDS = {
|
|
|
247
281
|
'function': TokenType.FUNCTION,
|
|
248
282
|
'call': TokenType.CALL,
|
|
249
283
|
'return': TokenType.RETURN,
|
|
284
|
+
'import': TokenType.IMPORT,
|
|
285
|
+
'at': TokenType.AT,
|
|
250
286
|
'to': TokenType.TO,
|
|
251
287
|
'and': TokenType.AND,
|
|
252
288
|
};
|
|
@@ -368,6 +404,15 @@ function tokenize(source) {
|
|
|
368
404
|
continue;
|
|
369
405
|
}
|
|
370
406
|
|
|
407
|
+
// Math operators: +, -, *, / (but not // which is a comment, and not - in words)
|
|
408
|
+
if ((ch === '+' || ch === '*') ||
|
|
409
|
+
(ch === '-' && col + 1 < line.length && line[col + 1] === ' ') ||
|
|
410
|
+
(ch === '/' && col + 1 < line.length && line[col + 1] !== '/')) {
|
|
411
|
+
tokens.push(new Token(TokenType.IDENTIFIER, ch, lineNum + 1, col + 1));
|
|
412
|
+
col++;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
371
416
|
// String literal (double or single quotes)
|
|
372
417
|
if (ch === '"' || ch === "'") {
|
|
373
418
|
const quote = ch;
|
|
@@ -995,7 +1040,12 @@ class Parser {
|
|
|
995
1040
|
this.skipNewlines();
|
|
996
1041
|
if (!this.peek() || this.peek().type === TokenType.EOF) break;
|
|
997
1042
|
|
|
998
|
-
if (this.peek().type === TokenType.
|
|
1043
|
+
if (this.peek().type === TokenType.IMPORT) {
|
|
1044
|
+
// imports are handled at a higher level, store as-is
|
|
1045
|
+
const imp = this.parseImport();
|
|
1046
|
+
if (!this.ast_imports) this.ast_imports = [];
|
|
1047
|
+
this.ast_imports.push(imp);
|
|
1048
|
+
} else if (this.peek().type === TokenType.TEMPLATE) {
|
|
999
1049
|
templates.push(this.parseTemplate());
|
|
1000
1050
|
} else if (this.peek().type === TokenType.SET) {
|
|
1001
1051
|
variables.push(this.parseSetVariable());
|
|
@@ -1013,18 +1063,31 @@ class Parser {
|
|
|
1013
1063
|
this.skipNewlines();
|
|
1014
1064
|
}
|
|
1015
1065
|
|
|
1016
|
-
return { type: 'Program', pages, styles, templates, variables };
|
|
1066
|
+
return { type: 'Program', pages, styles, templates, variables, imports: this.ast_imports || [] };
|
|
1017
1067
|
}
|
|
1018
1068
|
|
|
1019
|
-
// page "Title":
|
|
1069
|
+
// page "Title": OR page "Title" at "/path":
|
|
1020
1070
|
parsePage() {
|
|
1021
1071
|
this.expect(TokenType.PAGE);
|
|
1022
1072
|
const title = this.expect(TokenType.STRING).value;
|
|
1073
|
+
let route = null;
|
|
1074
|
+
|
|
1075
|
+
if (this.match(TokenType.AT)) {
|
|
1076
|
+
route = this.expect(TokenType.STRING).value;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1023
1079
|
this.expect(TokenType.COLON);
|
|
1024
1080
|
this.skipNewlines();
|
|
1025
1081
|
|
|
1026
1082
|
const children = this.parseBlock();
|
|
1027
|
-
return { type: 'Page', title, children };
|
|
1083
|
+
return { type: 'Page', title, route, children };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// import "file.clou"
|
|
1087
|
+
parseImport() {
|
|
1088
|
+
this.expect(TokenType.IMPORT);
|
|
1089
|
+
const file = this.expect(TokenType.STRING).value;
|
|
1090
|
+
return { type: 'Import', file };
|
|
1028
1091
|
}
|
|
1029
1092
|
|
|
1030
1093
|
// Parse an indented block of elements
|
|
@@ -1071,6 +1134,21 @@ class Parser {
|
|
|
1071
1134
|
case TokenType.GRID: return this.parseGrid();
|
|
1072
1135
|
case TokenType.SECTION: return this.parseSection();
|
|
1073
1136
|
case TokenType.SPACE: return this.parseSpace();
|
|
1137
|
+
case TokenType.FORM: return this.parseForm();
|
|
1138
|
+
case TokenType.TABLE: return this.parseTable();
|
|
1139
|
+
case TokenType.TABS: return this.parseTabs();
|
|
1140
|
+
case TokenType.TAB: return this.parseTab();
|
|
1141
|
+
case TokenType.ACCORDION: return this.parseAccordion();
|
|
1142
|
+
case TokenType.PANEL: return this.parsePanel();
|
|
1143
|
+
case TokenType.PROGRESS: return this.parseProgress();
|
|
1144
|
+
case TokenType.DROPDOWN: return this.parseDropdown();
|
|
1145
|
+
case TokenType.OPTION: return this.parseOption();
|
|
1146
|
+
case TokenType.TEXTAREA: return this.parseTextarea();
|
|
1147
|
+
case TokenType.CHECKBOX: return this.parseCheckbox();
|
|
1148
|
+
case TokenType.AUDIO: return this.parseAudio();
|
|
1149
|
+
case TokenType.CODE: return this.parseCode();
|
|
1150
|
+
case TokenType.SLIDER: return this.parseSlider();
|
|
1151
|
+
case TokenType.SUBMIT: return this.parseSubmit();
|
|
1074
1152
|
case TokenType.SET: return this.parseSetVariable();
|
|
1075
1153
|
case TokenType.USE: return this.parseUse();
|
|
1076
1154
|
case TokenType.REPEAT: return this.parseRepeat();
|
|
@@ -1410,6 +1488,185 @@ class Parser {
|
|
|
1410
1488
|
return { type: 'Space', value };
|
|
1411
1489
|
}
|
|
1412
1490
|
|
|
1491
|
+
// form:
|
|
1492
|
+
parseForm() {
|
|
1493
|
+
this.expect(TokenType.FORM);
|
|
1494
|
+
const node = { type: 'Form', action: null, children: [] };
|
|
1495
|
+
|
|
1496
|
+
if (this.peek() && this.peek().type === TokenType.STRING) {
|
|
1497
|
+
node.action = this.advance().value;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
1501
|
+
this.advance();
|
|
1502
|
+
this.skipNewlines();
|
|
1503
|
+
node.children = this.parseBlock();
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return node;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// table:
|
|
1510
|
+
parseTable() {
|
|
1511
|
+
this.expect(TokenType.TABLE);
|
|
1512
|
+
const node = { type: 'Table', children: [] };
|
|
1513
|
+
|
|
1514
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
1515
|
+
this.advance();
|
|
1516
|
+
this.skipNewlines();
|
|
1517
|
+
node.children = this.parseBlock();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
return node;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// tabs:
|
|
1524
|
+
parseTabs() {
|
|
1525
|
+
this.expect(TokenType.TABS);
|
|
1526
|
+
const node = { type: 'Tabs', children: [] };
|
|
1527
|
+
|
|
1528
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
1529
|
+
this.advance();
|
|
1530
|
+
this.skipNewlines();
|
|
1531
|
+
node.children = this.parseBlock();
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
return node;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// tab "name":
|
|
1538
|
+
parseTab() {
|
|
1539
|
+
this.expect(TokenType.TAB);
|
|
1540
|
+
const name = this.expect(TokenType.STRING).value;
|
|
1541
|
+
const node = { type: 'Tab', name, children: [] };
|
|
1542
|
+
|
|
1543
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
1544
|
+
this.advance();
|
|
1545
|
+
this.skipNewlines();
|
|
1546
|
+
node.children = this.parseBlock();
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
return node;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// accordion:
|
|
1553
|
+
parseAccordion() {
|
|
1554
|
+
this.expect(TokenType.ACCORDION);
|
|
1555
|
+
const node = { type: 'Accordion', children: [] };
|
|
1556
|
+
|
|
1557
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
1558
|
+
this.advance();
|
|
1559
|
+
this.skipNewlines();
|
|
1560
|
+
node.children = this.parseBlock();
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
return node;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// panel "title":
|
|
1567
|
+
parsePanel() {
|
|
1568
|
+
this.expect(TokenType.PANEL);
|
|
1569
|
+
const name = this.expect(TokenType.STRING).value;
|
|
1570
|
+
const node = { type: 'Panel', name, children: [] };
|
|
1571
|
+
|
|
1572
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
1573
|
+
this.advance();
|
|
1574
|
+
this.skipNewlines();
|
|
1575
|
+
node.children = this.parseBlock();
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
return node;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// progress 75 OR progress 75 "Label"
|
|
1582
|
+
parseProgress() {
|
|
1583
|
+
this.expect(TokenType.PROGRESS);
|
|
1584
|
+
const value = this.expect(TokenType.NUMBER).value;
|
|
1585
|
+
let label = null;
|
|
1586
|
+
if (this.peek() && this.peek().type === TokenType.STRING) {
|
|
1587
|
+
label = this.advance().value;
|
|
1588
|
+
}
|
|
1589
|
+
return { type: 'Progress', value, label };
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// dropdown "label":
|
|
1593
|
+
parseDropdown() {
|
|
1594
|
+
this.expect(TokenType.DROPDOWN);
|
|
1595
|
+
const label = this.expect(TokenType.STRING).value;
|
|
1596
|
+
const node = { type: 'Dropdown', label, children: [] };
|
|
1597
|
+
|
|
1598
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
1599
|
+
this.advance();
|
|
1600
|
+
this.skipNewlines();
|
|
1601
|
+
node.children = this.parseBlock();
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
return node;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// option "value"
|
|
1608
|
+
parseOption() {
|
|
1609
|
+
this.expect(TokenType.OPTION);
|
|
1610
|
+
const value = this.expect(TokenType.STRING).value;
|
|
1611
|
+
return { type: 'Option', value };
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// textarea "placeholder"
|
|
1615
|
+
parseTextarea() {
|
|
1616
|
+
this.expect(TokenType.TEXTAREA);
|
|
1617
|
+
const placeholder = this.expect(TokenType.STRING).value;
|
|
1618
|
+
const node = { type: 'Textarea', placeholder, children: [] };
|
|
1619
|
+
|
|
1620
|
+
if (this.peek() && this.peek().type === TokenType.COLON) {
|
|
1621
|
+
this.advance();
|
|
1622
|
+
this.skipNewlines();
|
|
1623
|
+
node.children = this.parseBlock();
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
return node;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// checkbox "label"
|
|
1630
|
+
parseCheckbox() {
|
|
1631
|
+
this.expect(TokenType.CHECKBOX);
|
|
1632
|
+
const label = this.expect(TokenType.STRING).value;
|
|
1633
|
+
return { type: 'Checkbox', label };
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// audio "src"
|
|
1637
|
+
parseAudio() {
|
|
1638
|
+
this.expect(TokenType.AUDIO);
|
|
1639
|
+
const src = this.expect(TokenType.STRING).value;
|
|
1640
|
+
return { type: 'Audio', src };
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// code "content"
|
|
1644
|
+
parseCode() {
|
|
1645
|
+
this.expect(TokenType.CODE);
|
|
1646
|
+
const value = this.expect(TokenType.STRING).value;
|
|
1647
|
+
return { type: 'Code', value };
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// slider "label" 0 to 100
|
|
1651
|
+
parseSlider() {
|
|
1652
|
+
this.expect(TokenType.SLIDER);
|
|
1653
|
+
const label = this.expect(TokenType.STRING).value;
|
|
1654
|
+
let min = '0', max = '100';
|
|
1655
|
+
if (this.peek() && this.peek().type === TokenType.NUMBER) {
|
|
1656
|
+
min = this.advance().value;
|
|
1657
|
+
this.expect(TokenType.TO);
|
|
1658
|
+
max = this.expect(TokenType.NUMBER).value;
|
|
1659
|
+
}
|
|
1660
|
+
return { type: 'Slider', label, min, max };
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// submit "text"
|
|
1664
|
+
parseSubmit() {
|
|
1665
|
+
this.expect(TokenType.SUBMIT);
|
|
1666
|
+
const label = this.expect(TokenType.STRING).value;
|
|
1667
|
+
return { type: 'Submit', label };
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1413
1670
|
// button "text":
|
|
1414
1671
|
parseButton() {
|
|
1415
1672
|
this.expect(TokenType.BUTTON);
|
|
@@ -1809,6 +2066,10 @@ class Compiler {
|
|
|
1809
2066
|
this.animations = new Set();
|
|
1810
2067
|
this.hasModal = false;
|
|
1811
2068
|
this.hasNavbar = false;
|
|
2069
|
+
this.hasTabs = false;
|
|
2070
|
+
this.hasAccordion = false;
|
|
2071
|
+
this.hasProgress = false;
|
|
2072
|
+
this.hasSlider = false;
|
|
1812
2073
|
this.hoverStyles = [];
|
|
1813
2074
|
this.variables = {};
|
|
1814
2075
|
this.templates = {};
|
|
@@ -1868,6 +2129,10 @@ class Compiler {
|
|
|
1868
2129
|
const animationCSS = this.buildAnimationCSS();
|
|
1869
2130
|
const modalCSS = this.hasModal ? this.buildModalCSS() : '';
|
|
1870
2131
|
const navbarCSS = this.hasNavbar ? this.buildNavbarCSS() : '';
|
|
2132
|
+
const tabsCSS = this.hasTabs ? this.buildTabsCSS() : '';
|
|
2133
|
+
const accordionCSS = this.hasAccordion ? this.buildAccordionCSS() : '';
|
|
2134
|
+
const progressCSS = this.hasProgress ? this.buildProgressCSS() : '';
|
|
2135
|
+
const sliderCSS = this.hasSlider ? this.buildSliderCSS() : '';
|
|
1871
2136
|
const hoverCSS = this.hoverStyles.join('\n');
|
|
1872
2137
|
const themeCSS = this.activeTheme ? `\n /* Theme: ${this.activeTheme.name} */\n${generateThemeCSS(this.activeTheme)}` : '';
|
|
1873
2138
|
|
|
@@ -2045,6 +2310,82 @@ class Compiler {
|
|
|
2045
2310
|
display: inline-block;
|
|
2046
2311
|
}
|
|
2047
2312
|
|
|
2313
|
+
.clou-table {
|
|
2314
|
+
width: 100%;
|
|
2315
|
+
border-collapse: collapse;
|
|
2316
|
+
margin: 16px 0;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
.clou-table th, .clou-table td {
|
|
2320
|
+
padding: 12px 16px;
|
|
2321
|
+
text-align: left;
|
|
2322
|
+
border-bottom: 1px solid rgba(128,128,128,0.2);
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
.clou-table th {
|
|
2326
|
+
font-weight: 600;
|
|
2327
|
+
opacity: 0.8;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
.clou-table tr:hover td {
|
|
2331
|
+
background: rgba(128,128,128,0.05);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
.clou-form {
|
|
2335
|
+
margin: 16px 0;
|
|
2336
|
+
display: flex;
|
|
2337
|
+
flex-direction: column;
|
|
2338
|
+
gap: 12px;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
.clou-checkbox {
|
|
2342
|
+
display: flex;
|
|
2343
|
+
align-items: center;
|
|
2344
|
+
gap: 8px;
|
|
2345
|
+
margin: 8px 0;
|
|
2346
|
+
cursor: pointer;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
.clou-checkbox input[type="checkbox"] {
|
|
2350
|
+
width: 18px;
|
|
2351
|
+
height: 18px;
|
|
2352
|
+
cursor: pointer;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
.clou-code {
|
|
2356
|
+
background: rgba(0,0,0,0.3);
|
|
2357
|
+
border-radius: 8px;
|
|
2358
|
+
padding: 16px 20px;
|
|
2359
|
+
margin: 12px 0;
|
|
2360
|
+
overflow-x: auto;
|
|
2361
|
+
font-family: 'Fira Code', 'Consolas', monospace;
|
|
2362
|
+
font-size: 14px;
|
|
2363
|
+
line-height: 1.5;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
.clou-audio {
|
|
2367
|
+
width: 100%;
|
|
2368
|
+
max-width: 400px;
|
|
2369
|
+
margin: 8px 0;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
.clou-dropdown {
|
|
2373
|
+
margin: 8px 0;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
.clou-dropdown label {
|
|
2377
|
+
display: block;
|
|
2378
|
+
margin-bottom: 6px;
|
|
2379
|
+
font-size: 14px;
|
|
2380
|
+
opacity: 0.8;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
select.clou-input {
|
|
2384
|
+
width: auto;
|
|
2385
|
+
min-width: 200px;
|
|
2386
|
+
cursor: pointer;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2048
2389
|
.clou-footer {
|
|
2049
2390
|
padding: 40px 20px;
|
|
2050
2391
|
margin-top: 40px;
|
|
@@ -2052,7 +2393,7 @@ class Compiler {
|
|
|
2052
2393
|
opacity: 0.7;
|
|
2053
2394
|
border-top: 1px solid rgba(128,128,128,0.2);
|
|
2054
2395
|
}
|
|
2055
|
-
${navbarCSS}${modalCSS}${animationCSS}${hoverCSS}${themeCSS}
|
|
2396
|
+
${navbarCSS}${modalCSS}${tabsCSS}${accordionCSS}${progressCSS}${sliderCSS}${animationCSS}${hoverCSS}${themeCSS}
|
|
2056
2397
|
</style>
|
|
2057
2398
|
</head>
|
|
2058
2399
|
<body>
|
|
@@ -2157,6 +2498,50 @@ ${scripts}
|
|
|
2157
2498
|
`;
|
|
2158
2499
|
}
|
|
2159
2500
|
|
|
2501
|
+
buildTabsCSS() {
|
|
2502
|
+
return `
|
|
2503
|
+
.clou-tabs { margin: 16px 0; }
|
|
2504
|
+
.clou-tab-buttons { display: flex; gap: 0; border-bottom: 2px solid rgba(128,128,128,0.2); margin-bottom: 16px; }
|
|
2505
|
+
.clou-tab-btn { padding: 10px 24px; background: none; border: none; color: inherit; cursor: pointer; font-size: 16px; font-weight: 500; opacity: 0.6; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: opacity 0.2s, border-color 0.2s; }
|
|
2506
|
+
.clou-tab-btn:hover { opacity: 0.9; transform: none; box-shadow: none; }
|
|
2507
|
+
.clou-tab-btn.active { opacity: 1; border-bottom-color: currentColor; }
|
|
2508
|
+
.clou-tab-panel { display: none; }
|
|
2509
|
+
.clou-tab-panel.active { display: block; }
|
|
2510
|
+
`;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
buildAccordionCSS() {
|
|
2514
|
+
return `
|
|
2515
|
+
.clou-accordion { margin: 16px 0; }
|
|
2516
|
+
.clou-panel { border: 1px solid rgba(128,128,128,0.2); border-radius: 8px; margin: 8px 0; overflow: hidden; }
|
|
2517
|
+
.clou-panel-header { padding: 16px 20px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-weight: 600; transition: background 0.2s; }
|
|
2518
|
+
.clou-panel-header:hover { background: rgba(128,128,128,0.1); }
|
|
2519
|
+
.clou-panel-arrow { transition: transform 0.3s; font-size: 12px; }
|
|
2520
|
+
.clou-panel.open .clou-panel-arrow { transform: rotate(180deg); }
|
|
2521
|
+
.clou-panel-body { max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; }
|
|
2522
|
+
.clou-panel.open .clou-panel-body { max-height: 500px; padding: 0 20px 16px; }
|
|
2523
|
+
`;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
buildProgressCSS() {
|
|
2527
|
+
const accent = this.buttonColorCSS || '#4A90D9';
|
|
2528
|
+
return `
|
|
2529
|
+
.clou-progress { margin: 12px 0; }
|
|
2530
|
+
.clou-progress-label { font-size: 14px; margin-bottom: 6px; opacity: 0.8; }
|
|
2531
|
+
.clou-progress-bar { width: 100%; height: 12px; background: rgba(128,128,128,0.2); border-radius: 6px; overflow: hidden; }
|
|
2532
|
+
.clou-progress-fill { height: 100%; background: ${accent}; border-radius: 6px; transition: width 0.6s ease; }
|
|
2533
|
+
`;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
buildSliderCSS() {
|
|
2537
|
+
const accent = this.buttonColorCSS || '#4A90D9';
|
|
2538
|
+
return `
|
|
2539
|
+
.clou-slider { margin: 12px 0; }
|
|
2540
|
+
.clou-slider label { display: block; font-size: 14px; margin-bottom: 6px; }
|
|
2541
|
+
.clou-slider input[type="range"] { width: 100%; max-width: 400px; accent-color: ${accent}; }
|
|
2542
|
+
`;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2160
2545
|
buildAnimationCSS() {
|
|
2161
2546
|
let css = '';
|
|
2162
2547
|
|
|
@@ -2371,6 +2756,139 @@ ${scripts}
|
|
|
2371
2756
|
` </div>`;
|
|
2372
2757
|
}
|
|
2373
2758
|
|
|
2759
|
+
case 'Form': {
|
|
2760
|
+
const id = this.uid();
|
|
2761
|
+
const content = this.compileChildren(node.children || []);
|
|
2762
|
+
this.scripts.push(
|
|
2763
|
+
`document.getElementById('${id}').addEventListener('submit', function(e) {\n` +
|
|
2764
|
+
` e.preventDefault();\n` +
|
|
2765
|
+
` alert('Form submitted!');\n` +
|
|
2766
|
+
` });`
|
|
2767
|
+
);
|
|
2768
|
+
const action = node.action ? ` action="${this.escapeHtml(node.action)}"` : '';
|
|
2769
|
+
return ` <form id="${id}"${action} class="clou-form">\n${content}\n </form>`;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
case 'Table': {
|
|
2773
|
+
const rows = (node.children || []).filter(c => c.type === 'Row' || c.type === 'TableRow');
|
|
2774
|
+
let html = ' <table class="clou-table">\n';
|
|
2775
|
+
for (const row of node.children || []) {
|
|
2776
|
+
if (row.type === 'Heading') {
|
|
2777
|
+
// Table heading row
|
|
2778
|
+
html += ' <thead><tr>';
|
|
2779
|
+
const cells = row.value.split(',').map(c => c.trim());
|
|
2780
|
+
for (const cell of cells) {
|
|
2781
|
+
html += `<th>${this.escapeHtml(this.interpolate(cell))}</th>`;
|
|
2782
|
+
}
|
|
2783
|
+
html += '</tr></thead>\n';
|
|
2784
|
+
} else if (row.children) {
|
|
2785
|
+
html += ' <tr>';
|
|
2786
|
+
for (const cell of row.children || []) {
|
|
2787
|
+
if (cell.type === 'Text') {
|
|
2788
|
+
html += `<td>${this.escapeHtml(this.interpolate(cell.value))}</td>`;
|
|
2789
|
+
} else {
|
|
2790
|
+
html += `<td>${this.compileNode(cell)}</td>`;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
html += '</tr>\n';
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
html += ' </table>';
|
|
2797
|
+
return html;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
case 'Tabs': {
|
|
2801
|
+
this.hasTabs = true;
|
|
2802
|
+
const tabId = this.uid();
|
|
2803
|
+
const tabs = (node.children || []).filter(c => c.type === 'Tab');
|
|
2804
|
+
let buttonsHtml = ` <div class="clou-tabs" id="${tabId}">\n <div class="clou-tab-buttons">`;
|
|
2805
|
+
let panelsHtml = '';
|
|
2806
|
+
|
|
2807
|
+
tabs.forEach((tab, i) => {
|
|
2808
|
+
const active = i === 0 ? ' active' : '';
|
|
2809
|
+
const panelId = `${tabId}-panel-${i}`;
|
|
2810
|
+
buttonsHtml += `\n <button class="clou-tab-btn${active}" data-panel="${panelId}" onclick="document.querySelectorAll('#${tabId} .clou-tab-btn').forEach(b=>b.classList.remove('active'));this.classList.add('active');document.querySelectorAll('#${tabId} .clou-tab-panel').forEach(p=>p.classList.remove('active'));document.getElementById('${panelId}').classList.add('active');">${this.escapeHtml(this.interpolate(tab.name))}</button>`;
|
|
2811
|
+
const content = this.compileChildren(this.filterContent(tab.children || []));
|
|
2812
|
+
panelsHtml += `\n <div id="${panelId}" class="clou-tab-panel${active}">\n${content}\n </div>`;
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
buttonsHtml += `\n </div>`;
|
|
2816
|
+
return `${buttonsHtml}${panelsHtml}\n </div>`;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
case 'Tab':
|
|
2820
|
+
// Handled by Tabs parent
|
|
2821
|
+
return '';
|
|
2822
|
+
|
|
2823
|
+
case 'Accordion': {
|
|
2824
|
+
this.hasAccordion = true;
|
|
2825
|
+
const panels = (node.children || []).filter(c => c.type === 'Panel');
|
|
2826
|
+
let html = ' <div class="clou-accordion">';
|
|
2827
|
+
for (const panel of panels) {
|
|
2828
|
+
const panelId = this.uid();
|
|
2829
|
+
const content = this.compileChildren(this.filterContent(panel.children || []));
|
|
2830
|
+
html += `\n <div class="clou-panel" id="${panelId}">`;
|
|
2831
|
+
html += `\n <div class="clou-panel-header" onclick="this.parentElement.classList.toggle('open')">${this.escapeHtml(this.interpolate(panel.name))}<span class="clou-panel-arrow">▼</span></div>`;
|
|
2832
|
+
html += `\n <div class="clou-panel-body">\n${content}\n </div>`;
|
|
2833
|
+
html += `\n </div>`;
|
|
2834
|
+
}
|
|
2835
|
+
html += '\n </div>';
|
|
2836
|
+
return html;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
case 'Panel':
|
|
2840
|
+
// Handled by Accordion parent
|
|
2841
|
+
return '';
|
|
2842
|
+
|
|
2843
|
+
case 'Progress': {
|
|
2844
|
+
this.hasProgress = true;
|
|
2845
|
+
const pct = parseInt(node.value, 10) || 0;
|
|
2846
|
+
const label = node.label ? `\n <div class="clou-progress-label">${this.escapeHtml(this.interpolate(node.label))}</div>` : '';
|
|
2847
|
+
return ` <div class="clou-progress">${label}\n <div class="clou-progress-bar"><div class="clou-progress-fill" style="width: ${pct}%"></div></div>\n </div>`;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
case 'Dropdown': {
|
|
2851
|
+
const id = this.uid();
|
|
2852
|
+
const options = (node.children || []).filter(c => c.type === 'Option');
|
|
2853
|
+
let html = ` <div class="clou-dropdown">\n <label for="${id}">${this.escapeHtml(this.interpolate(node.label))}</label>\n <select id="${id}" class="clou-input">`;
|
|
2854
|
+
for (const opt of options) {
|
|
2855
|
+
html += `\n <option value="${this.escapeHtml(opt.value)}">${this.escapeHtml(this.interpolate(opt.value))}</option>`;
|
|
2856
|
+
}
|
|
2857
|
+
html += '\n </select>\n </div>';
|
|
2858
|
+
return html;
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
case 'Option':
|
|
2862
|
+
// Handled by Dropdown parent
|
|
2863
|
+
return '';
|
|
2864
|
+
|
|
2865
|
+
case 'Textarea': {
|
|
2866
|
+
const id = this.uid();
|
|
2867
|
+
const style = this.extractInlineStyles(node.children || []);
|
|
2868
|
+
const styleAttr = style ? ` style="${style}"` : '';
|
|
2869
|
+
return ` <textarea id="${id}" placeholder="${this.escapeHtml(this.interpolate(node.placeholder))}" rows="4"${styleAttr}></textarea>`;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
case 'Checkbox': {
|
|
2873
|
+
const id = this.uid();
|
|
2874
|
+
return ` <label class="clou-checkbox"><input type="checkbox" id="${id}"> ${this.escapeHtml(this.interpolate(node.label))}</label>`;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
case 'Audio':
|
|
2878
|
+
return ` <audio src="${this.escapeHtml(node.src)}" controls class="clou-audio"></audio>`;
|
|
2879
|
+
|
|
2880
|
+
case 'Code':
|
|
2881
|
+
return ` <pre class="clou-code"><code>${this.escapeHtml(this.interpolate(node.value))}</code></pre>`;
|
|
2882
|
+
|
|
2883
|
+
case 'Slider': {
|
|
2884
|
+
this.hasSlider = true;
|
|
2885
|
+
const id = this.uid();
|
|
2886
|
+
return ` <div class="clou-slider">\n <label for="${id}">${this.escapeHtml(this.interpolate(node.label))}</label>\n <input type="range" id="${id}" min="${node.min}" max="${node.max}">\n </div>`;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
case 'Submit':
|
|
2890
|
+
return ` <button type="submit">${this.escapeHtml(this.interpolate(node.label))}</button>`;
|
|
2891
|
+
|
|
2374
2892
|
case 'Icon':
|
|
2375
2893
|
return ` <span class="clou-icon">${node.value}</span>`;
|
|
2376
2894
|
|
package/playground/index.html
CHANGED
|
@@ -509,6 +509,78 @@ page "{name}'s Portfolio":
|
|
|
509
509
|
footer:
|
|
510
510
|
text "Sweet Shop © 2026 — Made with Clou"`,
|
|
511
511
|
|
|
512
|
+
'New Elements': `page "All Elements":
|
|
513
|
+
theme "ocean"
|
|
514
|
+
|
|
515
|
+
navbar "Clou Demo":
|
|
516
|
+
link "Forms" to "#forms"
|
|
517
|
+
link "Data" to "#data"
|
|
518
|
+
link "Interactive" to "#interactive"
|
|
519
|
+
|
|
520
|
+
section "forms":
|
|
521
|
+
heading "Forms & Inputs":
|
|
522
|
+
huge
|
|
523
|
+
animate fade
|
|
524
|
+
space 20
|
|
525
|
+
form:
|
|
526
|
+
input "Your name"
|
|
527
|
+
input "Your email"
|
|
528
|
+
textarea "Write a message"
|
|
529
|
+
checkbox "I agree to the terms"
|
|
530
|
+
dropdown "Country":
|
|
531
|
+
option "Germany"
|
|
532
|
+
option "USA"
|
|
533
|
+
option "Japan"
|
|
534
|
+
slider "Volume" 0 to 100
|
|
535
|
+
submit "Send"
|
|
536
|
+
|
|
537
|
+
section "data":
|
|
538
|
+
heading "Data Display":
|
|
539
|
+
huge
|
|
540
|
+
space 20
|
|
541
|
+
table:
|
|
542
|
+
heading "Name, Role, Status"
|
|
543
|
+
row:
|
|
544
|
+
text "Alex"
|
|
545
|
+
text "Developer"
|
|
546
|
+
text "Active"
|
|
547
|
+
row:
|
|
548
|
+
text "Sam"
|
|
549
|
+
text "Designer"
|
|
550
|
+
text "Away"
|
|
551
|
+
|
|
552
|
+
space 20
|
|
553
|
+
progress 75 "Project Progress"
|
|
554
|
+
progress 40 "Bug Fixes"
|
|
555
|
+
space 20
|
|
556
|
+
code "clou deploy mysite.clou"
|
|
557
|
+
|
|
558
|
+
section "interactive":
|
|
559
|
+
heading "Interactive":
|
|
560
|
+
huge
|
|
561
|
+
space 20
|
|
562
|
+
tabs:
|
|
563
|
+
tab "About":
|
|
564
|
+
text "Clou is the simplest language ever."
|
|
565
|
+
text "Even kids can learn it!"
|
|
566
|
+
tab "Features":
|
|
567
|
+
text "30+ elements, 10 themes."
|
|
568
|
+
text "One command deploy."
|
|
569
|
+
tab "Code":
|
|
570
|
+
code "page \\"Hello\\": heading \\"World\\""
|
|
571
|
+
|
|
572
|
+
space 20
|
|
573
|
+
accordion:
|
|
574
|
+
panel "What is Clou?":
|
|
575
|
+
text "The simplest programming language for building websites."
|
|
576
|
+
panel "How do I install it?":
|
|
577
|
+
text "npm install -g clou-lang"
|
|
578
|
+
panel "Is it free?":
|
|
579
|
+
text "Yes! Clou is open source and free forever."
|
|
580
|
+
|
|
581
|
+
footer:
|
|
582
|
+
text "Built with Clou v0.3"`,
|
|
583
|
+
|
|
512
584
|
'Minimal': `page "Minimal":
|
|
513
585
|
theme "minimal"
|
|
514
586
|
|
package/src/errors.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
// Clou Language - Error Formatting
|
|
2
|
-
// Kid-friendly error messages with line numbers and
|
|
2
|
+
// Kid-friendly error messages with line numbers, colors, and typo detection
|
|
3
|
+
|
|
4
|
+
// ── ANSI Colors ──
|
|
5
|
+
const color = {
|
|
6
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
7
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
8
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
9
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
10
|
+
gray: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
11
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
12
|
+
bgRed: (s) => `\x1b[41m\x1b[37m${s}\x1b[0m`,
|
|
13
|
+
bgYellow: (s) => `\x1b[43m\x1b[30m${s}\x1b[0m`,
|
|
14
|
+
};
|
|
3
15
|
|
|
4
16
|
class ClouError extends Error {
|
|
5
17
|
constructor(message, options = {}) {
|
|
@@ -12,15 +24,143 @@ class ClouError extends Error {
|
|
|
12
24
|
}
|
|
13
25
|
}
|
|
14
26
|
|
|
15
|
-
//
|
|
27
|
+
// ── All known Clou keywords ──
|
|
28
|
+
const KNOWN_KEYWORDS = [
|
|
29
|
+
'page', 'title', 'heading', 'text', 'image', 'box', 'link', 'button',
|
|
30
|
+
'input', 'list', 'item', 'line', 'video', 'navbar', 'footer', 'logo',
|
|
31
|
+
'card', 'icon', 'modal', 'grid', 'section', 'space', 'columns',
|
|
32
|
+
'form', 'table', 'tabs', 'tab', 'accordion', 'panel', 'progress',
|
|
33
|
+
'dropdown', 'option', 'textarea', 'checkbox', 'audio', 'code', 'slider', 'submit',
|
|
34
|
+
'show', 'message', 'hide', 'toggle', 'go', 'open', 'close',
|
|
35
|
+
'style', 'background', 'color', 'size', 'bold', 'italic', 'center',
|
|
36
|
+
'left', 'right', 'rounded', 'shadow', 'padding', 'margin', 'width',
|
|
37
|
+
'height', 'font', 'gap', 'row', 'gradient', 'border', 'opacity',
|
|
38
|
+
'hover', 'animate', 'full', 'dark', 'light', 'small', 'big', 'huge',
|
|
39
|
+
'tiny', 'sticky', 'fixed', 'wrap', 'grow',
|
|
40
|
+
'set', 'template', 'use', 'repeat', 'theme', 'to', 'and', 'at',
|
|
41
|
+
'import', 'app', 'print', 'ask', 'save', 'as', 'if', 'else',
|
|
42
|
+
'is', 'not', 'greater', 'less', 'than', 'or', 'add', 'subtract',
|
|
43
|
+
'multiply', 'divide', 'by', 'wait', 'read', 'write', 'file', 'run',
|
|
44
|
+
'exit', 'clear', 'each', 'in', 'while', 'true', 'false',
|
|
45
|
+
'function', 'call', 'return',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// ── Levenshtein Distance (for typo detection) ──
|
|
49
|
+
function levenshtein(a, b) {
|
|
50
|
+
const m = a.length, n = b.length;
|
|
51
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
52
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
53
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
54
|
+
for (let i = 1; i <= m; i++) {
|
|
55
|
+
for (let j = 1; j <= n; j++) {
|
|
56
|
+
dp[i][j] = Math.min(
|
|
57
|
+
dp[i - 1][j] + 1,
|
|
58
|
+
dp[i][j - 1] + 1,
|
|
59
|
+
dp[i - 1][j - 1] + (a[i - 1] !== b[j - 1] ? 1 : 0)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return dp[m][n];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Find closest keyword match
|
|
67
|
+
function findClosest(word) {
|
|
68
|
+
const lower = word.toLowerCase();
|
|
69
|
+
if (KNOWN_KEYWORDS.includes(lower)) return null; // exact match
|
|
70
|
+
|
|
71
|
+
let best = null;
|
|
72
|
+
let bestDist = Infinity;
|
|
73
|
+
|
|
74
|
+
for (const kw of KNOWN_KEYWORDS) {
|
|
75
|
+
const dist = levenshtein(lower, kw);
|
|
76
|
+
// Allow max distance based on word length
|
|
77
|
+
const maxDist = lower.length <= 3 ? 1 : 2;
|
|
78
|
+
if (dist <= maxDist && dist < bestDist) {
|
|
79
|
+
bestDist = dist;
|
|
80
|
+
best = kw;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return best;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Warning Scanner ──
|
|
88
|
+
// Scans source code for potential typos and issues BEFORE compilation
|
|
89
|
+
function scanWarnings(source, filename) {
|
|
90
|
+
const lines = source.split('\n');
|
|
91
|
+
const warnings = [];
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
const trimmed = line.trim();
|
|
96
|
+
|
|
97
|
+
// Skip empty lines, comments, strings-only lines
|
|
98
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('#')) continue;
|
|
99
|
+
|
|
100
|
+
// Get the first word on the line
|
|
101
|
+
const firstWordMatch = trimmed.match(/^([a-zA-Z_-]+)/);
|
|
102
|
+
if (!firstWordMatch) continue;
|
|
103
|
+
|
|
104
|
+
const firstWord = firstWordMatch[1];
|
|
105
|
+
const lower = firstWord.toLowerCase();
|
|
106
|
+
|
|
107
|
+
// Skip if it's a known keyword
|
|
108
|
+
if (KNOWN_KEYWORDS.includes(lower)) continue;
|
|
109
|
+
|
|
110
|
+
// Skip if it looks like a value (after a keyword on same line)
|
|
111
|
+
// We only warn about words that START a line (likely meant to be a keyword)
|
|
112
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
113
|
+
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
114
|
+
|
|
115
|
+
// Check for possible typo
|
|
116
|
+
const suggestion = findClosest(firstWord);
|
|
117
|
+
if (suggestion) {
|
|
118
|
+
warnings.push({
|
|
119
|
+
line: i + 1,
|
|
120
|
+
col: indent + 1,
|
|
121
|
+
word: firstWord,
|
|
122
|
+
suggestion,
|
|
123
|
+
message: `"${firstWord}" is not a Clou keyword. Did you mean "${suggestion}"?`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return warnings;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Format Warnings ──
|
|
132
|
+
function formatWarnings(warnings, source, filename) {
|
|
133
|
+
if (warnings.length === 0) return '';
|
|
134
|
+
|
|
135
|
+
const lines = source.split('\n');
|
|
136
|
+
let output = '';
|
|
137
|
+
|
|
138
|
+
const file = filename ? ` in ${filename}` : '';
|
|
139
|
+
output += `\n ${color.bgYellow(' WARNING ')}${color.yellow(` ${warnings.length} possible issue${warnings.length > 1 ? 's' : ''} found${file}`)}\n`;
|
|
140
|
+
output += ` ${color.gray('-'.repeat(38))}\n\n`;
|
|
141
|
+
|
|
142
|
+
for (const warn of warnings) {
|
|
143
|
+
const lineNum = warn.line;
|
|
144
|
+
const lineText = lines[lineNum - 1] || '';
|
|
145
|
+
|
|
146
|
+
output += ` ${color.yellow('>')} ${color.gray(String(lineNum).padStart(4))} ${color.gray('|')} ${lineText}\n`;
|
|
147
|
+
const pointer = ' '.repeat(warn.col - 1) + '~'.repeat(warn.word.length);
|
|
148
|
+
output += ` ${color.gray('|')} ${color.yellow(pointer)}\n`;
|
|
149
|
+
output += ` ${color.yellow(`Did you mean "${color.bold(warn.suggestion)}"?`)}\n\n`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return output;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Format Error (with colors) ──
|
|
16
156
|
function formatError(error, source, filename) {
|
|
17
157
|
const lines = (source || '').split('\n');
|
|
18
158
|
let output = '\n';
|
|
19
159
|
|
|
20
|
-
// Header
|
|
160
|
+
// Header with red
|
|
21
161
|
const file = filename ? ` in ${filename}` : '';
|
|
22
|
-
output += ` Clou Error${file}\n`;
|
|
23
|
-
output +=
|
|
162
|
+
output += ` ${color.bgRed(' ERROR ')}${color.red(` Clou Error${file}`)}\n`;
|
|
163
|
+
output += ` ${color.gray('-'.repeat(38))}\n\n`;
|
|
24
164
|
|
|
25
165
|
// Get line number from error
|
|
26
166
|
let lineNum = null;
|
|
@@ -33,40 +173,40 @@ function formatError(error, source, filename) {
|
|
|
33
173
|
lineNum = error.token.line;
|
|
34
174
|
colNum = error.token.col || 1;
|
|
35
175
|
} else {
|
|
36
|
-
// Try to extract line number from error message
|
|
37
176
|
const lineMatch = error.message.match(/[Ll]ine\s+(\d+)/);
|
|
38
177
|
if (lineMatch) lineNum = parseInt(lineMatch[1], 10);
|
|
39
178
|
const colMatch = error.message.match(/[Cc]ol\s+(\d+)/);
|
|
40
179
|
if (colMatch) colNum = parseInt(colMatch[1], 10);
|
|
41
180
|
}
|
|
42
181
|
|
|
43
|
-
// Show source context
|
|
182
|
+
// Show source context with colors
|
|
44
183
|
if (lineNum && lines.length > 0 && lineNum <= lines.length) {
|
|
45
184
|
const start = Math.max(0, lineNum - 3);
|
|
46
185
|
const end = Math.min(lines.length, lineNum + 2);
|
|
47
186
|
|
|
48
187
|
for (let i = start; i < end; i++) {
|
|
49
188
|
const num = String(i + 1).padStart(4);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
189
|
+
if (i + 1 === lineNum) {
|
|
190
|
+
output += ` ${color.red('>')} ${color.red(num)} ${color.gray('|')} ${lines[i]}\n`;
|
|
191
|
+
if (colNum) {
|
|
192
|
+
const pointer = ' '.repeat(colNum - 1) + '^';
|
|
193
|
+
output += ` ${color.gray('|')} ${color.red(pointer)}\n`;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
output += ` ${color.gray(num)} ${color.gray('|')} ${color.gray(lines[i])}\n`;
|
|
57
197
|
}
|
|
58
198
|
}
|
|
59
199
|
output += '\n';
|
|
60
200
|
}
|
|
61
201
|
|
|
62
|
-
// Problem description
|
|
202
|
+
// Problem description
|
|
63
203
|
const friendlyMsg = makeFriendly(error.message);
|
|
64
|
-
output += ` Problem: ${friendlyMsg}\n`;
|
|
204
|
+
output += ` ${color.red('Problem:')} ${friendlyMsg}\n`;
|
|
65
205
|
|
|
66
206
|
// Tip
|
|
67
207
|
const tip = error.tip || suggestFix(error.message, lineNum ? lines[lineNum - 1] : '');
|
|
68
208
|
if (tip) {
|
|
69
|
-
output += `\n Tip: ${tip}\n`;
|
|
209
|
+
output += `\n ${color.cyan('Tip:')} ${tip}\n`;
|
|
70
210
|
}
|
|
71
211
|
|
|
72
212
|
output += '\n';
|
|
@@ -75,14 +215,11 @@ function formatError(error, source, filename) {
|
|
|
75
215
|
|
|
76
216
|
// Make error messages less technical
|
|
77
217
|
function makeFriendly(msg) {
|
|
78
|
-
// Remove "Line X, Col Y:" prefix since we show it visually
|
|
79
218
|
msg = msg.replace(/Line \d+, Col \d+:\s*/g, '');
|
|
80
219
|
msg = msg.replace(/at line \d+, col \d+/g, '');
|
|
81
220
|
|
|
82
|
-
// Replace technical parser messages
|
|
83
221
|
msg = msg.replace(/Expected STRING but got (\w+)/g, (_, got) => {
|
|
84
|
-
|
|
85
|
-
return `Expected text in quotes (like "hello") but found ${names}`;
|
|
222
|
+
return `Expected text in quotes (like "hello") but found ${tokenName(got)}`;
|
|
86
223
|
});
|
|
87
224
|
msg = msg.replace(/Expected (\w+) but got (\w+)/g, (_, exp, got) => {
|
|
88
225
|
return `Expected ${tokenName(exp)} but found ${tokenName(got)}`;
|
|
@@ -126,17 +263,14 @@ function suggestFix(msg, line) {
|
|
|
126
263
|
if (!line) line = '';
|
|
127
264
|
const trimmed = line.trim();
|
|
128
265
|
|
|
129
|
-
// Missing colon
|
|
130
266
|
if (msg.includes('Expected COLON') || msg.includes('Expected a colon')) {
|
|
131
267
|
return 'Add a colon : at the end of the line. Example: page "My Site":';
|
|
132
268
|
}
|
|
133
269
|
|
|
134
|
-
// Unterminated string
|
|
135
270
|
if (msg.includes('Unterminated') || msg.includes('not closed')) {
|
|
136
271
|
return 'Every string needs opening and closing quotes: "Hello World"';
|
|
137
272
|
}
|
|
138
273
|
|
|
139
|
-
// Missing string after keyword
|
|
140
274
|
if (msg.includes('Expected text in quotes') || msg.includes('Expected STRING')) {
|
|
141
275
|
if (trimmed.startsWith('page')) return 'page needs a title: page "My Website":';
|
|
142
276
|
if (trimmed.startsWith('heading')) return 'heading needs text: heading "Hello World"';
|
|
@@ -156,22 +290,18 @@ function suggestFix(msg, line) {
|
|
|
156
290
|
return 'Add text in quotes after the keyword: keyword "text here"';
|
|
157
291
|
}
|
|
158
292
|
|
|
159
|
-
// Missing TO in link
|
|
160
293
|
if (msg.includes('Expected "to"') || msg.includes('Expected TO')) {
|
|
161
294
|
return 'link needs "to" keyword: link "Click" to "https://example.com"';
|
|
162
295
|
}
|
|
163
296
|
|
|
164
|
-
// Indentation issues
|
|
165
297
|
if (msg.includes('INDENT') || msg.includes('indented')) {
|
|
166
298
|
return 'Content inside a block needs to be indented with spaces (4 spaces recommended).';
|
|
167
299
|
}
|
|
168
300
|
|
|
169
|
-
// Import errors
|
|
170
301
|
if (msg.includes('Import error') || msg.includes('Could not read')) {
|
|
171
302
|
return 'Check the file path. It should be relative to your .clou file.';
|
|
172
303
|
}
|
|
173
304
|
|
|
174
|
-
// File ended early
|
|
175
305
|
if (msg.includes('ended too early') || msg.includes('ended unexpectedly')) {
|
|
176
306
|
return 'Your code seems incomplete. Did you forget to close a block or add content?';
|
|
177
307
|
}
|
|
@@ -179,4 +309,4 @@ function suggestFix(msg, line) {
|
|
|
179
309
|
return null;
|
|
180
310
|
}
|
|
181
311
|
|
|
182
|
-
module.exports = { ClouError, formatError, makeFriendly, suggestFix };
|
|
312
|
+
module.exports = { ClouError, formatError, formatWarnings, scanWarnings, findClosest, makeFriendly, suggestFix, color };
|
package/src/lexer.js
CHANGED
|
@@ -397,6 +397,15 @@ function tokenize(source) {
|
|
|
397
397
|
continue;
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
// Math operators: +, -, *, / (but not // which is a comment, and not - in words)
|
|
401
|
+
if ((ch === '+' || ch === '*') ||
|
|
402
|
+
(ch === '-' && col + 1 < line.length && line[col + 1] === ' ') ||
|
|
403
|
+
(ch === '/' && col + 1 < line.length && line[col + 1] !== '/')) {
|
|
404
|
+
tokens.push(new Token(TokenType.IDENTIFIER, ch, lineNum + 1, col + 1));
|
|
405
|
+
col++;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
400
409
|
// String literal (double or single quotes)
|
|
401
410
|
if (ch === '"' || ch === "'") {
|
|
402
411
|
const quote = ch;
|
package/src/terminal-parser.js
CHANGED
|
@@ -142,6 +142,15 @@ class TerminalParser {
|
|
|
142
142
|
const name = this.advance().value;
|
|
143
143
|
this.expect(TokenType.TO);
|
|
144
144
|
const value = this.advance().value;
|
|
145
|
+
|
|
146
|
+
// Check for math operators: set x to 5 + 3, set x to y * 2
|
|
147
|
+
const peek = this.peek();
|
|
148
|
+
if (peek && peek.type === TokenType.IDENTIFIER && ['+', '-', '*', '/'].includes(peek.value)) {
|
|
149
|
+
const op = this.advance().value;
|
|
150
|
+
const right = this.advance().value;
|
|
151
|
+
return { type: 'SetMath', name, left: value, op, right };
|
|
152
|
+
}
|
|
153
|
+
|
|
145
154
|
return { type: 'Set', name, value };
|
|
146
155
|
}
|
|
147
156
|
|
package/src/terminal-runtime.js
CHANGED
|
@@ -177,6 +177,21 @@ class TerminalRuntime {
|
|
|
177
177
|
break;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
case 'SetMath': {
|
|
181
|
+
const left = this.resolveNum(node.left);
|
|
182
|
+
const right = this.resolveNum(node.right);
|
|
183
|
+
let val;
|
|
184
|
+
switch (node.op) {
|
|
185
|
+
case '+': val = left + right; break;
|
|
186
|
+
case '-': val = left - right; break;
|
|
187
|
+
case '*': val = left * right; break;
|
|
188
|
+
case '/': val = right !== 0 ? left / right : 0; break;
|
|
189
|
+
default: val = left;
|
|
190
|
+
}
|
|
191
|
+
this.variables[node.name] = Number.isInteger(val) ? String(val) : String(Math.round(val * 100) / 100);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
180
195
|
case 'Math': {
|
|
181
196
|
const amount = this.resolveNum(node.amount);
|
|
182
197
|
const current = this.resolveNum(node.varName);
|