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.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/ai/clou-ai-prompt.md +239 -0
- package/bin/clou.js +281 -0
- package/examples/calculator.clou +41 -0
- package/examples/hello-terminal.clou +22 -0
- package/examples/hello.clou +37 -0
- package/examples/hello.html +220 -0
- package/examples/multipage/about.html +319 -0
- package/examples/multipage/contact.html +308 -0
- package/examples/multipage/index.html +322 -0
- package/examples/multipage/shared.clou +19 -0
- package/examples/multipage/site.clou +102 -0
- package/examples/portfolio.clou +51 -0
- package/examples/portfolio.html +217 -0
- package/examples/quiz.clou +90 -0
- package/examples/showcase.clou +136 -0
- package/examples/showcase.html +410 -0
- package/examples/startup.clou +153 -0
- package/examples/startup.html +469 -0
- package/examples/themes-demo.clou +117 -0
- package/examples/themes-demo.html +429 -0
- package/package.json +48 -0
- package/playground/clou-browser.js +2576 -0
- package/playground/index.html +682 -0
- package/src/bundle-browser.js +62 -0
- package/src/compiler.js +761 -0
- package/src/devserver.js +154 -0
- package/src/index.js +87 -0
- package/src/lexer.js +456 -0
- package/src/parser.js +879 -0
- package/src/terminal-parser.js +358 -0
- package/src/terminal-runtime.js +310 -0
- package/src/themes.js +469 -0
package/src/devserver.js
ADDED
|
@@ -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 };
|