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.
- package/ai/clou-ai-prompt.md +74 -9
- package/bin/clou.js +65 -8
- 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/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);
|