clou-lang 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ // Clou Language - Live Dev Server
2
+ // Watches .clou files, compiles on change, auto-reloads browser
3
+
4
+ const http = require('http');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { buildClou } = require('./index');
8
+
9
+ const RELOAD_SCRIPT = `
10
+ <script>
11
+ (function() {
12
+ var lastCheck = Date.now();
13
+ setInterval(function() {
14
+ fetch('/__clou_check?t=' + lastCheck)
15
+ .then(function(r) { return r.json(); })
16
+ .then(function(data) {
17
+ if (data.changed) {
18
+ lastCheck = Date.now();
19
+ location.reload();
20
+ }
21
+ })
22
+ .catch(function() {});
23
+ }, 500);
24
+ })();
25
+ </script>
26
+ `;
27
+
28
+ class DevServer {
29
+ constructor(inputFile, port) {
30
+ this.inputFile = path.resolve(inputFile);
31
+ this.port = port || 3000;
32
+ this.lastModified = 0;
33
+ this.cachedHtml = '';
34
+ this.buildError = null;
35
+ }
36
+
37
+ build() {
38
+ try {
39
+ const source = fs.readFileSync(this.inputFile, 'utf-8');
40
+ let html = buildClou(source).html;
41
+
42
+ // Inject auto-reload script before </body>
43
+ html = html.replace('</body>', RELOAD_SCRIPT + '</body>');
44
+
45
+ this.cachedHtml = html;
46
+ this.buildError = null;
47
+ this.lastModified = Date.now();
48
+ return true;
49
+ } catch (err) {
50
+ this.buildError = err.message;
51
+ this.lastModified = Date.now();
52
+ // Show error page
53
+ this.cachedHtml = `<!DOCTYPE html>
54
+ <html><head><title>Clou Error</title>
55
+ <style>
56
+ body { background: #1a1a2e; color: #ff6b6b; font-family: monospace; padding: 40px; }
57
+ h1 { color: #ff6b6b; }
58
+ pre { background: #0d1117; padding: 20px; border-radius: 8px; color: #ffa657; margin-top: 20px; white-space: pre-wrap; }
59
+ </style></head><body>
60
+ <h1>Clou Build Error</h1>
61
+ <pre>${err.message}</pre>
62
+ <p style="color: #888; margin-top: 20px;">Fix the error and save — the page will auto-reload.</p>
63
+ ${RELOAD_SCRIPT}
64
+ </body></html>`;
65
+ return false;
66
+ }
67
+ }
68
+
69
+ start() {
70
+ // Initial build
71
+ this.build();
72
+
73
+ // Watch file for changes
74
+ let lastStat = fs.statSync(this.inputFile).mtimeMs;
75
+
76
+ const checkInterval = setInterval(() => {
77
+ try {
78
+ const stat = fs.statSync(this.inputFile);
79
+ if (stat.mtimeMs !== lastStat) {
80
+ lastStat = stat.mtimeMs;
81
+ const ok = this.build();
82
+ const time = new Date().toLocaleTimeString();
83
+ if (ok) {
84
+ console.log(` [${time}] Rebuilt successfully`);
85
+ } else {
86
+ console.log(` [${time}] Build error: ${this.buildError}`);
87
+ }
88
+ }
89
+ } catch (e) {
90
+ // File might be temporarily unavailable during save
91
+ }
92
+ }, 300);
93
+
94
+ // HTTP Server
95
+ const server = http.createServer((req, res) => {
96
+ // Check endpoint for auto-reload
97
+ if (req.url.startsWith('/__clou_check')) {
98
+ const url = new URL(req.url, `http://localhost:${this.port}`);
99
+ const clientTime = parseInt(url.searchParams.get('t')) || 0;
100
+ res.writeHead(200, {
101
+ 'Content-Type': 'application/json',
102
+ 'Access-Control-Allow-Origin': '*',
103
+ 'Cache-Control': 'no-cache'
104
+ });
105
+ res.end(JSON.stringify({ changed: this.lastModified > clientTime }));
106
+ return;
107
+ }
108
+
109
+ // Serve the compiled HTML
110
+ res.writeHead(200, {
111
+ 'Content-Type': 'text/html; charset=utf-8',
112
+ 'Cache-Control': 'no-cache'
113
+ });
114
+ res.end(this.cachedHtml);
115
+ });
116
+
117
+ server.listen(this.port, () => {
118
+ console.log(`
119
+ ╔═══════════════════════════════════════╗
120
+ ║ CLOU Dev Server ║
121
+ ╚═══════════════════════════════════════╝
122
+
123
+ Watching: ${this.inputFile}
124
+ Server: http://localhost:${this.port}
125
+
126
+ Edit your .clou file and save — the
127
+ browser will auto-reload instantly!
128
+
129
+ Press Ctrl+C to stop.
130
+ `);
131
+ });
132
+
133
+ // Open in browser
134
+ const { exec } = require('child_process');
135
+ const url = `http://localhost:${this.port}`;
136
+ if (process.platform === 'win32') {
137
+ exec(`start "" "${url}"`);
138
+ } else if (process.platform === 'darwin') {
139
+ exec(`open "${url}"`);
140
+ } else {
141
+ exec(`xdg-open "${url}"`);
142
+ }
143
+
144
+ // Graceful shutdown
145
+ process.on('SIGINT', () => {
146
+ clearInterval(checkInterval);
147
+ server.close();
148
+ console.log('\n Dev server stopped.\n');
149
+ process.exit(0);
150
+ });
151
+ }
152
+ }
153
+
154
+ module.exports = { DevServer };
package/src/index.js ADDED
@@ -0,0 +1,87 @@
1
+ // Clou Language - Main Module
2
+ // Ties together Lexer, Parser, and Compiler
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { tokenize, LexerError } = require('./lexer');
7
+ const { parse, ParseError } = require('./parser');
8
+ const { compile } = require('./compiler');
9
+
10
+ // Resolve imports: reads imported files and prepends their source
11
+ function resolveImports(source, basePath) {
12
+ const importRegex = /^import\s+"([^"]+)"\s*$/gm;
13
+ let resolved = source;
14
+ const imported = new Set();
15
+
16
+ let match;
17
+ while ((match = importRegex.exec(source)) !== null) {
18
+ const importFile = match[1];
19
+ const fullPath = path.resolve(basePath, importFile);
20
+
21
+ if (imported.has(fullPath)) continue;
22
+ imported.add(fullPath);
23
+
24
+ try {
25
+ let importedSource = fs.readFileSync(fullPath, 'utf-8');
26
+ // Recursively resolve imports in the imported file
27
+ importedSource = resolveImports(importedSource, path.dirname(fullPath));
28
+ // Replace the import statement with the file contents
29
+ resolved = resolved.replace(match[0], importedSource);
30
+ } catch (err) {
31
+ throw new Error(`Import error: Could not read "${importFile}" (${fullPath})`);
32
+ }
33
+ }
34
+
35
+ return resolved;
36
+ }
37
+
38
+ function buildClou(source, options = {}) {
39
+ const basePath = options.basePath || process.cwd();
40
+
41
+ // Step 0: Resolve imports
42
+ const resolvedSource = resolveImports(source, basePath);
43
+
44
+ // Step 1: Tokenize
45
+ const tokens = tokenize(resolvedSource);
46
+
47
+ // Step 2: Parse into AST
48
+ const ast = parse(tokens);
49
+
50
+ // Step 3: Check for multi-page
51
+ if (ast.pages.length > 1 && ast.pages.some(p => p.route)) {
52
+ return buildMultiPage(ast, resolvedSource);
53
+ }
54
+
55
+ // Step 4: Compile to HTML (single page)
56
+ const html = compile(ast);
57
+
58
+ return { tokens, ast, html, pages: null };
59
+ }
60
+
61
+ // Build multiple pages from a multi-page AST
62
+ function buildMultiPage(ast) {
63
+ const pages = {};
64
+
65
+ for (const page of ast.pages) {
66
+ const route = page.route || '/';
67
+ const filename = route === '/' ? 'index.html' : route.replace(/^\//, '').replace(/\/$/, '') + '.html';
68
+
69
+ // Create a single-page AST for this page
70
+ const singlePageAST = {
71
+ type: 'Program',
72
+ pages: [page],
73
+ styles: ast.styles,
74
+ templates: ast.templates,
75
+ variables: ast.variables,
76
+ imports: [],
77
+ };
78
+
79
+ // Inject navigation links between pages
80
+ const html = compile(singlePageAST);
81
+ pages[filename] = { html, title: page.title, route };
82
+ }
83
+
84
+ return { pages, html: pages['index.html'] ? pages['index.html'].html : Object.values(pages)[0].html };
85
+ }
86
+
87
+ module.exports = { buildClou, resolveImports, tokenize, parse, compile, LexerError, ParseError };
package/src/lexer.js ADDED
@@ -0,0 +1,456 @@
1
+ // Clou Language - Lexer
2
+ // Breaks Clou source code into tokens
3
+
4
+ const TokenType = {
5
+ // Structure
6
+ INDENT: 'INDENT',
7
+ DEDENT: 'DEDENT',
8
+ NEWLINE: 'NEWLINE',
9
+ COLON: 'COLON',
10
+ EOF: 'EOF',
11
+
12
+ // Literals
13
+ STRING: 'STRING',
14
+ NUMBER: 'NUMBER',
15
+ COLOR: 'COLOR',
16
+
17
+ // Keywords - Page structure
18
+ PAGE: 'PAGE',
19
+ TITLE: 'TITLE',
20
+ HEADING: 'HEADING',
21
+ TEXT: 'TEXT',
22
+ IMAGE: 'IMAGE',
23
+ BOX: 'BOX',
24
+ LINK: 'LINK',
25
+ BUTTON: 'BUTTON',
26
+ INPUT: 'INPUT',
27
+ LIST: 'LIST',
28
+ ITEM: 'ITEM',
29
+ LINE: 'LINE',
30
+ VIDEO: 'VIDEO',
31
+ NAVBAR: 'NAVBAR',
32
+ FOOTER: 'FOOTER',
33
+ LOGO: 'LOGO',
34
+ CARD: 'CARD',
35
+ ICON: 'ICON',
36
+ MODAL: 'MODAL',
37
+ GRID: 'GRID',
38
+ SECTION: 'SECTION',
39
+ SPACE: 'SPACE',
40
+ COLUMNS: 'COLUMNS',
41
+
42
+ // Keywords - Actions
43
+ SHOW: 'SHOW',
44
+ MESSAGE: 'MESSAGE',
45
+ HIDE: 'HIDE',
46
+ TOGGLE: 'TOGGLE',
47
+ GO: 'GO',
48
+ OPEN: 'OPEN',
49
+ CLOSE: 'CLOSE',
50
+
51
+ // Keywords - Style
52
+ STYLE: 'STYLE',
53
+ BACKGROUND: 'BACKGROUND',
54
+ COLOR_KW: 'COLOR_KW',
55
+ SIZE: 'SIZE',
56
+ BOLD: 'BOLD',
57
+ ITALIC: 'ITALIC',
58
+ CENTER: 'CENTER',
59
+ LEFT: 'LEFT',
60
+ RIGHT: 'RIGHT',
61
+ ROUNDED: 'ROUNDED',
62
+ SHADOW: 'SHADOW',
63
+ PADDING: 'PADDING',
64
+ MARGIN: 'MARGIN',
65
+ WIDTH: 'WIDTH',
66
+ HEIGHT: 'HEIGHT',
67
+ FONT: 'FONT',
68
+ GAP: 'GAP',
69
+ ROW: 'ROW',
70
+ GRADIENT: 'GRADIENT',
71
+ BORDER: 'BORDER',
72
+ OPACITY: 'OPACITY',
73
+ HOVER: 'HOVER',
74
+ ANIMATE: 'ANIMATE',
75
+ FULL: 'FULL',
76
+ DARK: 'DARK',
77
+ LIGHT: 'LIGHT',
78
+ SMALL: 'SMALL',
79
+ BIG: 'BIG',
80
+ HUGE: 'HUGE',
81
+ TINY: 'TINY',
82
+ STICKY: 'STICKY',
83
+ FIXED: 'FIXED',
84
+ WRAP: 'WRAP',
85
+ GROW: 'GROW',
86
+
87
+ // Keywords - Variables & Templates
88
+ SET: 'SET',
89
+ TEMPLATE: 'TEMPLATE',
90
+ USE: 'USE',
91
+ REPEAT: 'REPEAT',
92
+ THEME: 'THEME',
93
+
94
+ // Keywords - Terminal / App
95
+ APP: 'APP',
96
+ PRINT: 'PRINT',
97
+ ASK: 'ASK',
98
+ SAVE: 'SAVE',
99
+ AS: 'AS',
100
+ IF: 'IF',
101
+ ELSE: 'ELSE',
102
+ IS: 'IS',
103
+ NOT: 'NOT',
104
+ GREATER: 'GREATER',
105
+ LESS: 'LESS',
106
+ THAN: 'THAN',
107
+ OR: 'OR',
108
+ ADD: 'ADD',
109
+ SUBTRACT: 'SUBTRACT',
110
+ MULTIPLY: 'MULTIPLY',
111
+ DIVIDE: 'DIVIDE',
112
+ BY: 'BY',
113
+ WAIT: 'WAIT',
114
+ READ: 'READ',
115
+ WRITE: 'WRITE',
116
+ FILE: 'FILE',
117
+ RUN: 'RUN',
118
+ EXIT: 'EXIT',
119
+ CLEAR: 'CLEAR',
120
+ EACH: 'EACH',
121
+ IN: 'IN',
122
+ WHILE: 'WHILE',
123
+ TRUE: 'TRUE',
124
+ FALSE: 'FALSE',
125
+ FUNCTION: 'FUNCTION',
126
+ CALL: 'CALL',
127
+ RETURN: 'RETURN',
128
+ LPAREN: 'LPAREN',
129
+ RPAREN: 'RPAREN',
130
+ COMMA: 'COMMA',
131
+
132
+ // Keywords - Imports & Routing
133
+ IMPORT: 'IMPORT',
134
+ AT: 'AT',
135
+
136
+ // Keywords - Connectors
137
+ TO: 'TO',
138
+ AND: 'AND',
139
+
140
+ // Identifiers (for unknown words)
141
+ IDENTIFIER: 'IDENTIFIER',
142
+ };
143
+
144
+ const KEYWORDS = {
145
+ 'page': TokenType.PAGE,
146
+ 'title': TokenType.TITLE,
147
+ 'heading': TokenType.HEADING,
148
+ 'text': TokenType.TEXT,
149
+ 'image': TokenType.IMAGE,
150
+ 'box': TokenType.BOX,
151
+ 'link': TokenType.LINK,
152
+ 'button': TokenType.BUTTON,
153
+ 'input': TokenType.INPUT,
154
+ 'list': TokenType.LIST,
155
+ 'item': TokenType.ITEM,
156
+ 'line': TokenType.LINE,
157
+ 'video': TokenType.VIDEO,
158
+ 'navbar': TokenType.NAVBAR,
159
+ 'footer': TokenType.FOOTER,
160
+ 'logo': TokenType.LOGO,
161
+ 'card': TokenType.CARD,
162
+ 'icon': TokenType.ICON,
163
+ 'modal': TokenType.MODAL,
164
+ 'grid': TokenType.GRID,
165
+ 'section': TokenType.SECTION,
166
+ 'space': TokenType.SPACE,
167
+ 'columns': TokenType.COLUMNS,
168
+ 'show': TokenType.SHOW,
169
+ 'message': TokenType.MESSAGE,
170
+ 'hide': TokenType.HIDE,
171
+ 'toggle': TokenType.TOGGLE,
172
+ 'go': TokenType.GO,
173
+ 'open': TokenType.OPEN,
174
+ 'close': TokenType.CLOSE,
175
+ 'style': TokenType.STYLE,
176
+ 'background': TokenType.BACKGROUND,
177
+ 'color': TokenType.COLOR_KW,
178
+ 'size': TokenType.SIZE,
179
+ 'bold': TokenType.BOLD,
180
+ 'italic': TokenType.ITALIC,
181
+ 'center': TokenType.CENTER,
182
+ 'left': TokenType.LEFT,
183
+ 'right': TokenType.RIGHT,
184
+ 'rounded': TokenType.ROUNDED,
185
+ 'shadow': TokenType.SHADOW,
186
+ 'padding': TokenType.PADDING,
187
+ 'margin': TokenType.MARGIN,
188
+ 'width': TokenType.WIDTH,
189
+ 'height': TokenType.HEIGHT,
190
+ 'font': TokenType.FONT,
191
+ 'gap': TokenType.GAP,
192
+ 'row': TokenType.ROW,
193
+ 'gradient': TokenType.GRADIENT,
194
+ 'border': TokenType.BORDER,
195
+ 'opacity': TokenType.OPACITY,
196
+ 'hover': TokenType.HOVER,
197
+ 'animate': TokenType.ANIMATE,
198
+ 'full': TokenType.FULL,
199
+ 'dark': TokenType.DARK,
200
+ 'light': TokenType.LIGHT,
201
+ 'small': TokenType.SMALL,
202
+ 'big': TokenType.BIG,
203
+ 'huge': TokenType.HUGE,
204
+ 'tiny': TokenType.TINY,
205
+ 'sticky': TokenType.STICKY,
206
+ 'fixed': TokenType.FIXED,
207
+ 'wrap': TokenType.WRAP,
208
+ 'grow': TokenType.GROW,
209
+ 'set': TokenType.SET,
210
+ 'template': TokenType.TEMPLATE,
211
+ 'use': TokenType.USE,
212
+ 'repeat': TokenType.REPEAT,
213
+ 'theme': TokenType.THEME,
214
+ 'app': TokenType.APP,
215
+ 'print': TokenType.PRINT,
216
+ 'ask': TokenType.ASK,
217
+ 'save': TokenType.SAVE,
218
+ 'as': TokenType.AS,
219
+ 'if': TokenType.IF,
220
+ 'else': TokenType.ELSE,
221
+ 'is': TokenType.IS,
222
+ 'not': TokenType.NOT,
223
+ 'greater': TokenType.GREATER,
224
+ 'less': TokenType.LESS,
225
+ 'than': TokenType.THAN,
226
+ 'or': TokenType.OR,
227
+ 'add': TokenType.ADD,
228
+ 'subtract': TokenType.SUBTRACT,
229
+ 'multiply': TokenType.MULTIPLY,
230
+ 'divide': TokenType.DIVIDE,
231
+ 'by': TokenType.BY,
232
+ 'wait': TokenType.WAIT,
233
+ 'read': TokenType.READ,
234
+ 'write': TokenType.WRITE,
235
+ 'file': TokenType.FILE,
236
+ 'run': TokenType.RUN,
237
+ 'exit': TokenType.EXIT,
238
+ 'clear': TokenType.CLEAR,
239
+ 'each': TokenType.EACH,
240
+ 'in': TokenType.IN,
241
+ 'while': TokenType.WHILE,
242
+ 'true': TokenType.TRUE,
243
+ 'false': TokenType.FALSE,
244
+ 'function': TokenType.FUNCTION,
245
+ 'call': TokenType.CALL,
246
+ 'return': TokenType.RETURN,
247
+ 'import': TokenType.IMPORT,
248
+ 'at': TokenType.AT,
249
+ 'to': TokenType.TO,
250
+ 'and': TokenType.AND,
251
+ };
252
+
253
+ class Token {
254
+ constructor(type, value, line, col) {
255
+ this.type = type;
256
+ this.value = value;
257
+ this.line = line;
258
+ this.col = col;
259
+ }
260
+ }
261
+
262
+ class LexerError extends Error {
263
+ constructor(message, line, col) {
264
+ super(`Line ${line}, Col ${col}: ${message}`);
265
+ this.line = line;
266
+ this.col = col;
267
+ }
268
+ }
269
+
270
+ function tokenize(source) {
271
+ const tokens = [];
272
+ const lines = source.split('\n');
273
+ const indentStack = [0];
274
+
275
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
276
+ const rawLine = lines[lineNum];
277
+
278
+ // Skip empty lines and comment lines (but not hex colors like #ff0000)
279
+ const trimmed = rawLine.trim();
280
+ if (trimmed === '' || trimmed.startsWith('//')) {
281
+ continue;
282
+ }
283
+ if (trimmed.startsWith('#') && (trimmed.length === 1 || trimmed[1] === ' ' || trimmed[1] === '\t')) {
284
+ continue;
285
+ }
286
+
287
+ // Calculate indentation (spaces)
288
+ let indent = 0;
289
+ for (let i = 0; i < rawLine.length; i++) {
290
+ if (rawLine[i] === ' ') {
291
+ indent++;
292
+ } else if (rawLine[i] === '\t') {
293
+ indent += 4;
294
+ } else {
295
+ break;
296
+ }
297
+ }
298
+
299
+ // Handle indentation changes
300
+ if (indent > indentStack[indentStack.length - 1]) {
301
+ indentStack.push(indent);
302
+ tokens.push(new Token(TokenType.INDENT, indent, lineNum + 1, 1));
303
+ } else {
304
+ while (indent < indentStack[indentStack.length - 1]) {
305
+ indentStack.pop();
306
+ tokens.push(new Token(TokenType.DEDENT, indent, lineNum + 1, 1));
307
+ }
308
+ }
309
+
310
+ // Tokenize the line content
311
+ let col = indent;
312
+ const line = rawLine;
313
+
314
+ while (col < line.length) {
315
+ const ch = line[col];
316
+
317
+ // Skip whitespace
318
+ if (ch === ' ' || ch === '\t') {
319
+ col++;
320
+ continue;
321
+ }
322
+
323
+ // Skip inline comments
324
+ if (ch === '/' && col + 1 < line.length && line[col + 1] === '/') {
325
+ break;
326
+ }
327
+ // # is a comment only if followed by space or end of line (not hex colors like #ff0000)
328
+ if (ch === '#' && (col + 1 >= line.length || line[col + 1] === ' ' || line[col + 1] === '\t')) {
329
+ break;
330
+ }
331
+
332
+ // Hex color like #f0f4f8
333
+ if (ch === '#') {
334
+ let hex = '#';
335
+ col++;
336
+ while (col < line.length && /[0-9a-fA-F]/.test(line[col])) {
337
+ hex += line[col];
338
+ col++;
339
+ }
340
+ tokens.push(new Token(TokenType.IDENTIFIER, hex, lineNum + 1, col + 1));
341
+ continue;
342
+ }
343
+
344
+ // Colon
345
+ if (ch === ':') {
346
+ tokens.push(new Token(TokenType.COLON, ':', lineNum + 1, col + 1));
347
+ col++;
348
+ continue;
349
+ }
350
+
351
+ // Parentheses (for templates)
352
+ if (ch === '(') {
353
+ tokens.push(new Token(TokenType.LPAREN, '(', lineNum + 1, col + 1));
354
+ col++;
355
+ continue;
356
+ }
357
+ if (ch === ')') {
358
+ tokens.push(new Token(TokenType.RPAREN, ')', lineNum + 1, col + 1));
359
+ col++;
360
+ continue;
361
+ }
362
+
363
+ // Comma
364
+ if (ch === ',') {
365
+ tokens.push(new Token(TokenType.COMMA, ',', lineNum + 1, col + 1));
366
+ col++;
367
+ continue;
368
+ }
369
+
370
+ // String literal (double or single quotes)
371
+ if (ch === '"' || ch === "'") {
372
+ const quote = ch;
373
+ let str = '';
374
+ col++;
375
+ while (col < line.length && line[col] !== quote) {
376
+ if (line[col] === '\\' && col + 1 < line.length) {
377
+ col++;
378
+ if (line[col] === 'n') str += '\n';
379
+ else if (line[col] === 't') str += '\t';
380
+ else str += line[col];
381
+ } else {
382
+ str += line[col];
383
+ }
384
+ col++;
385
+ }
386
+ if (col >= line.length) {
387
+ throw new LexerError(`Unterminated string`, lineNum + 1, col + 1);
388
+ }
389
+ col++; // skip closing quote
390
+ tokens.push(new Token(TokenType.STRING, str, lineNum + 1, col + 1));
391
+ continue;
392
+ }
393
+
394
+ // Number
395
+ if (ch >= '0' && ch <= '9') {
396
+ let num = '';
397
+ while (col < line.length && ((line[col] >= '0' && line[col] <= '9') || line[col] === '.')) {
398
+ num += line[col];
399
+ col++;
400
+ }
401
+ // Check for units like px, %, em, rem
402
+ if (col < line.length && line[col] === '%') {
403
+ num += '%';
404
+ col++;
405
+ } else {
406
+ let unit = '';
407
+ while (col < line.length && line[col] >= 'a' && line[col] <= 'z') {
408
+ unit += line[col];
409
+ col++;
410
+ }
411
+ if (unit) num += unit;
412
+ }
413
+ tokens.push(new Token(TokenType.NUMBER, num, lineNum + 1, col + 1));
414
+ continue;
415
+ }
416
+
417
+ // Words (keywords or identifiers)
418
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '-') {
419
+ let word = '';
420
+ while (col < line.length && (
421
+ (line[col] >= 'a' && line[col] <= 'z') ||
422
+ (line[col] >= 'A' && line[col] <= 'Z') ||
423
+ (line[col] >= '0' && line[col] <= '9') ||
424
+ line[col] === '_' || line[col] === '-'
425
+ )) {
426
+ word += line[col];
427
+ col++;
428
+ }
429
+
430
+ const lower = word.toLowerCase();
431
+ if (KEYWORDS[lower]) {
432
+ tokens.push(new Token(KEYWORDS[lower], lower, lineNum + 1, col + 1));
433
+ } else {
434
+ tokens.push(new Token(TokenType.IDENTIFIER, word, lineNum + 1, col + 1));
435
+ }
436
+ continue;
437
+ }
438
+
439
+ // Unknown character - skip it
440
+ col++;
441
+ }
442
+
443
+ tokens.push(new Token(TokenType.NEWLINE, '\\n', lineNum + 1, col + 1));
444
+ }
445
+
446
+ // Close remaining indents
447
+ while (indentStack.length > 1) {
448
+ indentStack.pop();
449
+ tokens.push(new Token(TokenType.DEDENT, 0, lines.length, 1));
450
+ }
451
+
452
+ tokens.push(new Token(TokenType.EOF, null, lines.length, 1));
453
+ return tokens;
454
+ }
455
+
456
+ module.exports = { tokenize, Token, TokenType, LexerError };