clewy-lang 1.0.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/src/parser.js ADDED
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Clewy Parser
3
+ * Converts .cly source text into an AST (Abstract Syntax Tree)
4
+ */
5
+
6
+ const TOKEN_TYPES = {
7
+ KEYWORD: 'KEYWORD',
8
+ STRING: 'STRING',
9
+ NUMBER: 'NUMBER',
10
+ ARROW: 'ARROW',
11
+ IDENTIFIER: 'IDENTIFIER',
12
+ END: 'END',
13
+ NEWLINE: 'NEWLINE',
14
+ EOF: 'EOF',
15
+ };
16
+
17
+ const KEYWORDS = new Set([
18
+ 'page', 'text', 'button', 'input', 'show', 'loop',
19
+ 'component', 'use', 'end', 'style', 'link', 'image',
20
+ 'if', 'var', 'set', 'nav', 'footer', 'section',
21
+ 'title', 'subtitle', 'alert',
22
+ ]);
23
+
24
+ // ─── Lexer ────────────────────────────────────────────────────────────────────
25
+
26
+ function tokenize(source) {
27
+ const tokens = [];
28
+ let i = 0;
29
+ let line = 1;
30
+
31
+ while (i < source.length) {
32
+ // Skip whitespace (but not newlines)
33
+ if (source[i] === ' ' || source[i] === '\t' || source[i] === '\r') {
34
+ i++;
35
+ continue;
36
+ }
37
+
38
+ // Comments
39
+ if (source[i] === '#') {
40
+ while (i < source.length && source[i] !== '\n') i++;
41
+ continue;
42
+ }
43
+
44
+ // Newlines
45
+ if (source[i] === '\n') {
46
+ tokens.push({ type: TOKEN_TYPES.NEWLINE, value: '\n', line });
47
+ line++;
48
+ i++;
49
+ continue;
50
+ }
51
+
52
+ // Arrow =>
53
+ if (source[i] === '=' && source[i + 1] === '>') {
54
+ tokens.push({ type: TOKEN_TYPES.ARROW, value: '=>', line });
55
+ i += 2;
56
+ continue;
57
+ }
58
+
59
+ // String literals
60
+ if (source[i] === '"' || source[i] === "'") {
61
+ const quote = source[i];
62
+ i++;
63
+ let str = '';
64
+ while (i < source.length && source[i] !== quote) {
65
+ if (source[i] === '\\' && i + 1 < source.length) {
66
+ i++;
67
+ const escapes = { n: '\n', t: '\t', '"': '"', "'": "'", '\\': '\\' };
68
+ str += escapes[source[i]] || source[i];
69
+ } else {
70
+ str += source[i];
71
+ }
72
+ i++;
73
+ }
74
+ i++; // closing quote
75
+ tokens.push({ type: TOKEN_TYPES.STRING, value: str, line });
76
+ continue;
77
+ }
78
+
79
+ // Numbers
80
+ if (/[0-9]/.test(source[i])) {
81
+ let num = '';
82
+ while (i < source.length && /[0-9.]/.test(source[i])) {
83
+ num += source[i++];
84
+ }
85
+ tokens.push({ type: TOKEN_TYPES.NUMBER, value: parseFloat(num), line });
86
+ continue;
87
+ }
88
+
89
+ // Keywords and identifiers
90
+ if (/[a-zA-Z_]/.test(source[i])) {
91
+ let word = '';
92
+ while (i < source.length && /[a-zA-Z0-9_-]/.test(source[i])) {
93
+ word += source[i++];
94
+ }
95
+ const type = KEYWORDS.has(word) ? TOKEN_TYPES.KEYWORD : TOKEN_TYPES.IDENTIFIER;
96
+ tokens.push({ type, value: word, line });
97
+ continue;
98
+ }
99
+
100
+ // Unknown character — skip
101
+ i++;
102
+ }
103
+
104
+ tokens.push({ type: TOKEN_TYPES.EOF, value: null, line });
105
+ return tokens;
106
+ }
107
+
108
+ // ─── Parser ───────────────────────────────────────────────────────────────────
109
+
110
+ function parse(source) {
111
+ const tokens = tokenize(source);
112
+ let pos = 0;
113
+
114
+ function peek() { return tokens[pos]; }
115
+ function consume() { return tokens[pos++]; }
116
+ function expect(type, value) {
117
+ const t = peek();
118
+ if (t.type !== type || (value !== undefined && t.value !== value)) {
119
+ throw new Error(`Line ${t.line}: Expected ${value || type}, got "${t.value}"`);
120
+ }
121
+ return consume();
122
+ }
123
+
124
+ function skipNewlines() {
125
+ while (peek().type === TOKEN_TYPES.NEWLINE) consume();
126
+ }
127
+
128
+ function parseStatement() {
129
+ skipNewlines();
130
+ const t = peek();
131
+
132
+ if (t.type === TOKEN_TYPES.EOF) return null;
133
+ if (t.type === TOKEN_TYPES.KEYWORD && t.value === 'end') return null;
134
+
135
+ if (t.type !== TOKEN_TYPES.KEYWORD) {
136
+ // skip unknown tokens gracefully
137
+ consume();
138
+ return null;
139
+ }
140
+
141
+ consume(); // eat keyword
142
+ const keyword = t.value;
143
+
144
+ switch (keyword) {
145
+ case 'page': return parsePage();
146
+ case 'text': return parseText();
147
+ case 'title': return parseTitle();
148
+ case 'subtitle':return parseSubtitle();
149
+ case 'button': return parseButton();
150
+ case 'input': return parseInput();
151
+ case 'show': return parseShow();
152
+ case 'alert': return parseAlert();
153
+ case 'loop': return parseLoop();
154
+ case 'component': return parseComponent();
155
+ case 'use': return parseUse();
156
+ case 'style': return parseStyle();
157
+ case 'link': return parseLink();
158
+ case 'image': return parseImage();
159
+ case 'if': return parseIf();
160
+ case 'var': return parseVar();
161
+ case 'set': return parseSet();
162
+ case 'nav': return parseNav();
163
+ case 'footer': return parseFooter();
164
+ case 'section': return parseSection();
165
+ default: return null;
166
+ }
167
+ }
168
+
169
+ function parseStringArg() {
170
+ if (peek().type === TOKEN_TYPES.STRING) return consume().value;
171
+ if (peek().type === TOKEN_TYPES.IDENTIFIER) return consume().value;
172
+ return '';
173
+ }
174
+
175
+ function parseAction() {
176
+ // Parses what comes after =>
177
+ const t = peek();
178
+ if (t.type !== TOKEN_TYPES.KEYWORD) return null;
179
+ consume();
180
+ switch (t.value) {
181
+ case 'show': return { type: 'show', text: parseStringArg() };
182
+ case 'alert': return { type: 'alert', text: parseStringArg() };
183
+ case 'set': return { type: 'set', name: parseStringArg(), value: parseStringArg() };
184
+ default: return { type: t.value };
185
+ }
186
+ }
187
+
188
+ function parsePage() {
189
+ const label = parseStringArg();
190
+ return { type: 'page', label };
191
+ }
192
+
193
+ function parseText() {
194
+ const content = parseStringArg();
195
+ return { type: 'text', content };
196
+ }
197
+
198
+ function parseTitle() {
199
+ const content = parseStringArg();
200
+ return { type: 'title', content };
201
+ }
202
+
203
+ function parseSubtitle() {
204
+ const content = parseStringArg();
205
+ return { type: 'subtitle', content };
206
+ }
207
+
208
+ function parseButton() {
209
+ const label = parseStringArg();
210
+ let action = null;
211
+ if (peek().type === TOKEN_TYPES.ARROW) {
212
+ consume();
213
+ action = parseAction();
214
+ }
215
+ return { type: 'button', label, action };
216
+ }
217
+
218
+ function parseInput() {
219
+ const placeholder = parseStringArg();
220
+ let name = null;
221
+ if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'as') {
222
+ consume();
223
+ name = parseStringArg();
224
+ } else if (peek().type === TOKEN_TYPES.IDENTIFIER) {
225
+ name = consume().value;
226
+ }
227
+ return { type: 'input', placeholder, name };
228
+ }
229
+
230
+ function parseShow() {
231
+ const text = parseStringArg();
232
+ return { type: 'show', text };
233
+ }
234
+
235
+ function parseAlert() {
236
+ const text = parseStringArg();
237
+ return { type: 'alert', text };
238
+ }
239
+
240
+ function parseLoop() {
241
+ let count = 3;
242
+ if (peek().type === TOKEN_TYPES.NUMBER) count = consume().value;
243
+ let body = [];
244
+ if (peek().type === TOKEN_TYPES.ARROW) {
245
+ consume();
246
+ const stmt = parseStatement();
247
+ if (stmt) body = [stmt];
248
+ } else {
249
+ // multiline loop body
250
+ skipNewlines();
251
+ while (peek().type !== TOKEN_TYPES.EOF) {
252
+ if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'end') { consume(); break; }
253
+ const stmt = parseStatement();
254
+ if (stmt) body.push(stmt);
255
+ skipNewlines();
256
+ }
257
+ }
258
+ return { type: 'loop', count, body };
259
+ }
260
+
261
+ function parseComponent() {
262
+ const name = parseStringArg() || (peek().type === TOKEN_TYPES.IDENTIFIER ? consume().value : 'Component');
263
+ const body = [];
264
+ skipNewlines();
265
+ while (peek().type !== TOKEN_TYPES.EOF) {
266
+ if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'end') { consume(); break; }
267
+ const stmt = parseStatement();
268
+ if (stmt) body.push(stmt);
269
+ skipNewlines();
270
+ }
271
+ return { type: 'component', name, body };
272
+ }
273
+
274
+ function parseUse() {
275
+ const name = parseStringArg() || (peek().type === TOKEN_TYPES.IDENTIFIER ? consume().value : '');
276
+ return { type: 'use', name };
277
+ }
278
+
279
+ function parseStyle() {
280
+ const prop = parseStringArg();
281
+ const value = parseStringArg();
282
+ return { type: 'style', prop, value };
283
+ }
284
+
285
+ function parseLink() {
286
+ const label = parseStringArg();
287
+ const href = parseStringArg() || '#';
288
+ return { type: 'link', label, href };
289
+ }
290
+
291
+ function parseImage() {
292
+ const src = parseStringArg();
293
+ const alt = parseStringArg() || '';
294
+ return { type: 'image', src, alt };
295
+ }
296
+
297
+ function parseIf() {
298
+ const condition = parseStringArg();
299
+ const body = [];
300
+ skipNewlines();
301
+ while (peek().type !== TOKEN_TYPES.EOF) {
302
+ if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'end') { consume(); break; }
303
+ const stmt = parseStatement();
304
+ if (stmt) body.push(stmt);
305
+ skipNewlines();
306
+ }
307
+ return { type: 'if', condition, body };
308
+ }
309
+
310
+ function parseVar() {
311
+ const name = parseStringArg() || (peek().type === TOKEN_TYPES.IDENTIFIER ? consume().value : 'x');
312
+ const value = parseStringArg();
313
+ return { type: 'var', name, value };
314
+ }
315
+
316
+ function parseSet() {
317
+ const name = parseStringArg() || (peek().type === TOKEN_TYPES.IDENTIFIER ? consume().value : 'x');
318
+ const value = parseStringArg();
319
+ return { type: 'set', name, value };
320
+ }
321
+
322
+ function parseNav() {
323
+ const items = [];
324
+ // Collect link items until newline or end
325
+ while (peek().type === TOKEN_TYPES.STRING || peek().type === TOKEN_TYPES.IDENTIFIER) {
326
+ items.push(consume().value);
327
+ }
328
+ return { type: 'nav', items };
329
+ }
330
+
331
+ function parseFooter() {
332
+ const content = parseStringArg();
333
+ return { type: 'footer', content };
334
+ }
335
+
336
+ function parseSection() {
337
+ const label = parseStringArg();
338
+ const body = [];
339
+ skipNewlines();
340
+ while (peek().type !== TOKEN_TYPES.EOF) {
341
+ if (peek().type === TOKEN_TYPES.KEYWORD && peek().value === 'end') { consume(); break; }
342
+ const stmt = parseStatement();
343
+ if (stmt) body.push(stmt);
344
+ skipNewlines();
345
+ }
346
+ return { type: 'section', label, body };
347
+ }
348
+
349
+ // ── Top-level parse loop
350
+ const ast = { type: 'program', body: [] };
351
+ while (peek().type !== TOKEN_TYPES.EOF) {
352
+ const stmt = parseStatement();
353
+ if (stmt) ast.body.push(stmt);
354
+ }
355
+
356
+ return ast;
357
+ }
358
+
359
+ module.exports = { parse, tokenize };
package/src/runtime.js ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Clewy Runtime
3
+ * Dev server with live-reload for .cly projects
4
+ */
5
+
6
+ const http = require('http');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { compile } = require('./compiler');
10
+
11
+ // ─── MIME types ───────────────────────────────────────────────────────────────
12
+
13
+ const MIME = {
14
+ '.html': 'text/html',
15
+ '.css': 'text/css',
16
+ '.js': 'application/javascript',
17
+ '.png': 'image/png',
18
+ '.jpg': 'image/jpeg',
19
+ '.jpeg': 'image/jpeg',
20
+ '.gif': 'image/gif',
21
+ '.svg': 'image/svg+xml',
22
+ '.ico': 'image/x-icon',
23
+ '.json': 'application/json',
24
+ };
25
+
26
+ // ─── Build pipeline ──────────────────────────────────────────────────────────
27
+
28
+ function buildProject(projectDir) {
29
+ const clyPath = path.join(projectDir, 'main.cly');
30
+
31
+ if (!fs.existsSync(clyPath)) {
32
+ console.error(` ✗ No main.cly found in ${projectDir}`);
33
+ return false;
34
+ }
35
+
36
+ try {
37
+ const source = fs.readFileSync(clyPath, 'utf8');
38
+ const { html, css, js } = compile(source);
39
+
40
+ fs.writeFileSync(path.join(projectDir, 'index.html'), html);
41
+ fs.writeFileSync(path.join(projectDir, 'style.css'), css);
42
+ fs.writeFileSync(path.join(projectDir, 'script.js'), js);
43
+
44
+ return true;
45
+ } catch (err) {
46
+ console.error(` ✗ Compile error: ${err.message}`);
47
+ return false;
48
+ }
49
+ }
50
+
51
+ // ─── Live-reload injection ────────────────────────────────────────────────────
52
+
53
+ const LIVERELOAD_SCRIPT = `
54
+ <!-- Clewy live reload -->
55
+ <script>
56
+ (function() {
57
+ var src = new EventSource('/__clewy_sse');
58
+ src.onmessage = function(e) {
59
+ if (e.data === 'reload') window.location.reload();
60
+ };
61
+ })();
62
+ </script>
63
+ `;
64
+
65
+ function injectLiveReload(html) {
66
+ return html.replace('</body>', LIVERELOAD_SCRIPT + '</body>');
67
+ }
68
+
69
+ // ─── Dev server ───────────────────────────────────────────────────────────────
70
+
71
+ function startServer(projectDir, port = 3000) {
72
+ let sseClients = [];
73
+
74
+ // Initial build
75
+ console.log('\n 🔨 Building...');
76
+ const ok = buildProject(projectDir);
77
+ if (ok) console.log(' ✓ Build complete\n');
78
+
79
+ // File watcher
80
+ const clyPath = path.join(projectDir, 'main.cly');
81
+ let debounceTimer = null;
82
+
83
+ fs.watch(clyPath, () => {
84
+ clearTimeout(debounceTimer);
85
+ debounceTimer = setTimeout(() => {
86
+ console.log(' ↻ Detected change, rebuilding...');
87
+ const rebuilt = buildProject(projectDir);
88
+ if (rebuilt) {
89
+ console.log(' ✓ Rebuilt — refreshing browser\n');
90
+ sseClients.forEach(res => res.write('data: reload\n\n'));
91
+ }
92
+ }, 80);
93
+ });
94
+
95
+ // HTTP server
96
+ const server = http.createServer((req, res) => {
97
+ const url = req.url === '/' ? '/index.html' : req.url;
98
+
99
+ // SSE endpoint for live reload
100
+ if (url === '/__clewy_sse') {
101
+ res.writeHead(200, {
102
+ 'Content-Type': 'text/event-stream',
103
+ 'Cache-Control': 'no-cache',
104
+ 'Connection': 'keep-alive',
105
+ 'Access-Control-Allow-Origin': '*',
106
+ });
107
+ res.write('data: connected\n\n');
108
+ sseClients.push(res);
109
+ req.on('close', () => {
110
+ sseClients = sseClients.filter(c => c !== res);
111
+ });
112
+ return;
113
+ }
114
+
115
+ const filePath = path.join(projectDir, url.split('?')[0]);
116
+ const ext = path.extname(filePath).toLowerCase();
117
+
118
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
119
+ const fallback = path.join(projectDir, 'index.html');
120
+ if (fs.existsSync(fallback)) {
121
+ let html = fs.readFileSync(fallback, 'utf8');
122
+ html = injectLiveReload(html);
123
+ res.writeHead(200, { 'Content-Type': 'text/html' });
124
+ res.end(html);
125
+ } else {
126
+ res.writeHead(404);
127
+ res.end('Not found');
128
+ }
129
+ return;
130
+ }
131
+
132
+ try {
133
+ let content = fs.readFileSync(filePath);
134
+ const mime = MIME[ext] || 'application/octet-stream';
135
+
136
+ if (ext === '.html') {
137
+ content = injectLiveReload(content.toString());
138
+ }
139
+
140
+ res.writeHead(200, { 'Content-Type': mime });
141
+ res.end(content);
142
+ } catch {
143
+ res.writeHead(500);
144
+ res.end('Server error');
145
+ }
146
+ });
147
+
148
+ server.listen(port, () => {
149
+ console.log(` ┌──────────────────────────────────────┐`);
150
+ console.log(` │ 🚀 Clewy dev server running │`);
151
+ console.log(` │ │`);
152
+ console.log(` │ Local: http://localhost:${port} │`);
153
+ console.log(` │ │`);
154
+ console.log(` │ Watching main.cly for changes... │`);
155
+ console.log(` │ Press Ctrl+C to stop │`);
156
+ console.log(` └──────────────────────────────────────┘\n`);
157
+
158
+ // Try to open browser
159
+ const open = process.platform === 'win32'
160
+ ? `start http://localhost:${port}`
161
+ : process.platform === 'darwin'
162
+ ? `open http://localhost:${port}`
163
+ : `xdg-open http://localhost:${port}`;
164
+
165
+ require('child_process').exec(open, () => {});
166
+ });
167
+
168
+ server.on('error', (err) => {
169
+ if (err.code === 'EADDRINUSE') {
170
+ console.error(` ✗ Port ${port} is already in use. Try: clewy run --port ${port + 1}`);
171
+ } else {
172
+ console.error(` ✗ Server error: ${err.message}`);
173
+ }
174
+ process.exit(1);
175
+ });
176
+
177
+ return server;
178
+ }
179
+
180
+ module.exports = { buildProject, startServer };