clou-lang 0.3.1 → 0.3.6

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.
@@ -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);