clou-lang 0.3.1 → 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 CHANGED
@@ -11,14 +11,14 @@
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.3
21
+ ║ CLOU Language v0.3.5
22
22
  ║ Describe it once. Run everywhere. ║
23
23
  ╚═══════════════════════════════════════╝
24
24
 
@@ -132,6 +132,12 @@ async function runTerminal(inputFile) {
132
132
  function runFile(inputFile) {
133
133
  const source = fs.readFileSync(inputFile, 'utf-8');
134
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
+
135
141
  // Auto-detect: terminal app or website?
136
142
  if (isTerminalApp(source)) {
137
143
  runTerminal(inputFile).catch(err => {
@@ -166,6 +172,13 @@ function runFile(inputFile) {
166
172
 
167
173
  function buildFile(inputFile, outputFile) {
168
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
+
169
182
  const basePath = path.dirname(path.resolve(inputFile));
170
183
  const result = buildClou(source, { basePath, filename: inputFile });
171
184
 
@@ -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
- <div class="clou-box">
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!");
@@ -11,7 +11,7 @@ template feature-card(emoji, title, desc):
11
11
  animate slide
12
12
 
13
13
  page "{brand} - Theme Showcase":
14
- theme "ocean"
14
+ theme "aurora"
15
15
 
16
16
  navbar "{brand}":
17
17
  link "Features" to "#features"
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clou-lang",
3
- "version": "0.3.1",
3
+ "version": "0.3.5",
4
4
  "description": "Clou - A programming language so simple, even kids can build websites and terminal apps",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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.TEMPLATE) {
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">&#9660;</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
 
@@ -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 suggestions
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
- // Format a nice error message with source context
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 += ' ' + '-'.repeat(38) + '\n\n';
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
- const marker = (i + 1 === lineNum) ? ' > ' : ' ';
51
- output += `${marker}${num} | ${lines[i]}\n`;
52
-
53
- // Show pointer to error column
54
- if (i + 1 === lineNum && colNum) {
55
- const pointer = ' '.repeat(colNum - 1) + '^';
56
- output += ` | ${pointer}\n`;
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 (clean up technical jargon)
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
- const names = tokenName(got);
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;
@@ -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
 
@@ -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);