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.
@@ -0,0 +1,536 @@
1
+ /**
2
+ * Clewy Compiler
3
+ * Transforms a Clewy AST into HTML, CSS, and JavaScript output
4
+ */
5
+
6
+ const { parse } = require('./parser');
7
+
8
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
9
+
10
+ let _idCounter = 0;
11
+ function uid(prefix = 'el') {
12
+ return `${prefix}_${++_idCounter}`;
13
+ }
14
+ function esc(str) {
15
+ return String(str)
16
+ .replace(/&/g, '&')
17
+ .replace(/</g, '&lt;')
18
+ .replace(/>/g, '&gt;')
19
+ .replace(/"/g, '&quot;');
20
+ }
21
+ function jsStr(str) {
22
+ return JSON.stringify(String(str));
23
+ }
24
+
25
+ // ─── Compiler Class ───────────────────────────────────────────────────────────
26
+
27
+ class Compiler {
28
+ constructor() {
29
+ this.html = []; // HTML lines
30
+ this.js = []; // JS lines
31
+ this.components = {}; // name → body AST
32
+ this.pageTitle = 'Clewy App';
33
+ this.outputId = uid('output'); // default output div
34
+ _idCounter = 0;
35
+ }
36
+
37
+ compile(source) {
38
+ const ast = parse(source);
39
+ this._walk(ast.body);
40
+ return this._assemble();
41
+ }
42
+
43
+ // ─── Walk statements ────────────────────────────────────────────────────────
44
+
45
+ _walk(stmts) {
46
+ for (const node of stmts) {
47
+ this._emit(node);
48
+ }
49
+ }
50
+
51
+ _emit(node) {
52
+ if (!node) return;
53
+ switch (node.type) {
54
+ case 'page': return this._page(node);
55
+ case 'text': return this._text(node);
56
+ case 'title': return this._heading(node, 'h1');
57
+ case 'subtitle': return this._heading(node, 'h2');
58
+ case 'button': return this._button(node);
59
+ case 'input': return this._input(node);
60
+ case 'show': return this._show(node);
61
+ case 'alert': return this._alertCmd(node);
62
+ case 'loop': return this._loop(node);
63
+ case 'component': return this._componentDef(node);
64
+ case 'use': return this._componentUse(node);
65
+ case 'style': return; // handled at CSS level
66
+ case 'link': return this._link(node);
67
+ case 'image': return this._image(node);
68
+ case 'var': return this._varDecl(node);
69
+ case 'set': return this._varSet(node);
70
+ case 'nav': return this._nav(node);
71
+ case 'footer': return this._footer(node);
72
+ case 'section': return this._section(node);
73
+ default: break;
74
+ }
75
+ }
76
+
77
+ // ─── Node emitters ───────────────────────────────────────────────────────────
78
+
79
+ _page(node) {
80
+ this.pageTitle = node.label || 'Clewy App';
81
+ }
82
+
83
+ _text(node) {
84
+ this.html.push(` <p class="cly-text">${esc(node.content)}</p>`);
85
+ }
86
+
87
+ _heading(node, tag) {
88
+ this.html.push(` <${tag} class="cly-heading">${esc(node.content)}</${tag}>`);
89
+ }
90
+
91
+ _button(node) {
92
+ const id = uid('btn');
93
+ this.html.push(` <button id="${id}" class="cly-button">${esc(node.label)}</button>`);
94
+
95
+ if (node.action) {
96
+ const action = node.action;
97
+ if (action.type === 'show') {
98
+ const outId = uid('out');
99
+ this.html.push(` <div id="${outId}" class="cly-output" style="display:none">${esc(action.text)}</div>`);
100
+ this.js.push(`document.getElementById(${jsStr(id)}).addEventListener('click', function() {`);
101
+ this.js.push(` var el = document.getElementById(${jsStr(outId)});`);
102
+ this.js.push(` el.style.display = el.style.display === 'none' ? 'block' : 'none';`);
103
+ this.js.push(`});`);
104
+ } else if (action.type === 'alert') {
105
+ this.js.push(`document.getElementById(${jsStr(id)}).addEventListener('click', function() {`);
106
+ this.js.push(` alert(${jsStr(action.text)});`);
107
+ this.js.push(`});`);
108
+ } else if (action.type === 'set') {
109
+ this.js.push(`document.getElementById(${jsStr(id)}).addEventListener('click', function() {`);
110
+ this.js.push(` __clewy_vars[${jsStr(action.name)}] = ${jsStr(action.value)};`);
111
+ this.js.push(` __clewy_render();`);
112
+ this.js.push(`});`);
113
+ }
114
+ }
115
+ }
116
+
117
+ _input(node) {
118
+ const id = node.name ? `input_${node.name}` : uid('inp');
119
+ this.html.push(` <input id="${id}" class="cly-input" type="text" placeholder="${esc(node.placeholder)}" />`);
120
+ if (node.name) {
121
+ this.js.push(`document.getElementById(${jsStr(id)}).addEventListener('input', function() {`);
122
+ this.js.push(` __clewy_vars[${jsStr(node.name)}] = this.value;`);
123
+ this.js.push(`});`);
124
+ }
125
+ }
126
+
127
+ _show(node) {
128
+ const id = uid('show');
129
+ this.html.push(` <div id="${id}" class="cly-output">${esc(node.text)}</div>`);
130
+ }
131
+
132
+ _alertCmd(node) {
133
+ this.js.push(`alert(${jsStr(node.text)});`);
134
+ }
135
+
136
+ _loop(node) {
137
+ const count = node.count;
138
+ // Generate the loop body `count` times
139
+ for (let i = 0; i < count; i++) {
140
+ for (const child of node.body) {
141
+ this._emit(child);
142
+ }
143
+ }
144
+ }
145
+
146
+ _componentDef(node) {
147
+ // Store component definition for later use
148
+ this.components[node.name] = node.body;
149
+ }
150
+
151
+ _componentUse(node) {
152
+ const body = this.components[node.name];
153
+ if (!body) {
154
+ this.html.push(` <!-- component "${esc(node.name)}" not found -->`);
155
+ return;
156
+ }
157
+ this.html.push(` <div class="cly-component cly-component--${esc(node.name.toLowerCase())}">`);
158
+ this._walk(body);
159
+ this.html.push(` </div>`);
160
+ }
161
+
162
+ _link(node) {
163
+ const href = node.href.startsWith('http') ? node.href : node.href;
164
+ this.html.push(` <a class="cly-link" href="${esc(href)}">${esc(node.label)}</a>`);
165
+ }
166
+
167
+ _image(node) {
168
+ this.html.push(` <img class="cly-image" src="${esc(node.src)}" alt="${esc(node.alt)}" />`);
169
+ }
170
+
171
+ _varDecl(node) {
172
+ this.js.push(`__clewy_vars[${jsStr(node.name)}] = ${jsStr(node.value)};`);
173
+ }
174
+
175
+ _varSet(node) {
176
+ this.js.push(`__clewy_vars[${jsStr(node.name)}] = ${jsStr(node.value)};`);
177
+ }
178
+
179
+ _nav(node) {
180
+ const items = node.items
181
+ .map(item => `<a class="cly-nav__link" href="#">${esc(item)}</a>`)
182
+ .join('\n ');
183
+ this.html.push(` <nav class="cly-nav">\n ${items}\n </nav>`);
184
+ }
185
+
186
+ _footer(node) {
187
+ this.html.push(` <footer class="cly-footer"><p>${esc(node.content)}</p></footer>`);
188
+ }
189
+
190
+ _section(node) {
191
+ this.html.push(` <section class="cly-section">`);
192
+ if (node.label) {
193
+ this.html.push(` <h3 class="cly-section__label">${esc(node.label)}</h3>`);
194
+ }
195
+ this._walk(node.body);
196
+ this.html.push(` </section>`);
197
+ }
198
+
199
+ // ─── Assemble final output ────────────────────────────────────────────────
200
+
201
+ _assemble() {
202
+ const htmlBody = this.html.join('\n');
203
+ const jsCode = [
204
+ `var __clewy_vars = {};`,
205
+ `function __clewy_render() {}`,
206
+ `(function() {`,
207
+ this.js.map(l => ' ' + l).join('\n'),
208
+ `})();`,
209
+ ].join('\n');
210
+
211
+ return { html: htmlBody, js: jsCode, title: this.pageTitle };
212
+ }
213
+ }
214
+
215
+ // ─── Public compile() API ─────────────────────────────────────────────────────
216
+
217
+ function compile(source) {
218
+ const compiler = new Compiler();
219
+ const { html: bodyHtml, js, title } = compiler.compile(source);
220
+ const css = generateCSS();
221
+
222
+ const indexHtml = `<!DOCTYPE html>
223
+ <html lang="en">
224
+ <head>
225
+ <meta charset="UTF-8" />
226
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
227
+ <title>${esc(title)}</title>
228
+ <link rel="preconnect" href="https://fonts.googleapis.com">
229
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
230
+ <link rel="stylesheet" href="style.css" />
231
+ </head>
232
+ <body>
233
+ <main class="cly-page">
234
+ ${bodyHtml}
235
+ </main>
236
+ <script src="script.js"></script>
237
+ </body>
238
+ </html>`;
239
+
240
+ return { html: indexHtml, css, js };
241
+ }
242
+
243
+ // ─── CSS Generator ────────────────────────────────────────────────────────────
244
+
245
+ function generateCSS() {
246
+ return `/* ── Clewy Generated Styles ── */
247
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
248
+
249
+ :root {
250
+ --cly-bg: #08080f;
251
+ --cly-surface: #12121e;
252
+ --cly-surface2: #1a1a2e;
253
+ --cly-primary: #7c6bff;
254
+ --cly-primary-glow: rgba(124,107,255,0.4);
255
+ --cly-accent: #ff6bcd;
256
+ --cly-accent2: #6bffda;
257
+ --cly-text: #e8e8f4;
258
+ --cly-text2: #9898b8;
259
+ --cly-muted: #55557a;
260
+ --cly-border: rgba(124,107,255,0.2);
261
+ --cly-border2: rgba(255,255,255,0.05);
262
+ --cly-radius: 14px;
263
+ --cly-font: 'Inter', 'Segoe UI', system-ui, sans-serif;
264
+ }
265
+
266
+ /* ── Base ── */
267
+ html { scroll-behavior: smooth; }
268
+
269
+ body {
270
+ font-family: var(--cly-font);
271
+ background: var(--cly-bg);
272
+ background-image:
273
+ radial-gradient(ellipse 80% 50% at 50% -10%, rgba(124,107,255,0.12) 0%, transparent 70%),
274
+ radial-gradient(ellipse 60% 40% at 80% 100%, rgba(255,107,205,0.07) 0%, transparent 60%);
275
+ background-attachment: fixed;
276
+ color: var(--cly-text);
277
+ line-height: 1.65;
278
+ min-height: 100vh;
279
+ -webkit-font-smoothing: antialiased;
280
+ }
281
+
282
+ .cly-page {
283
+ max-width: 780px;
284
+ margin: 0 auto;
285
+ padding: 0 24px 100px;
286
+ display: flex;
287
+ flex-direction: column;
288
+ gap: 20px;
289
+ }
290
+
291
+ /* ── Nav ── */
292
+ .cly-nav {
293
+ position: sticky;
294
+ top: 0;
295
+ z-index: 100;
296
+ display: flex;
297
+ gap: 0;
298
+ padding: 0;
299
+ margin: 0 -24px 16px;
300
+ background: rgba(8,8,15,0.8);
301
+ backdrop-filter: blur(20px) saturate(180%);
302
+ -webkit-backdrop-filter: blur(20px);
303
+ border-bottom: 1px solid var(--cly-border);
304
+ flex-wrap: wrap;
305
+ overflow: hidden;
306
+ }
307
+ .cly-nav__link {
308
+ color: var(--cly-text2);
309
+ text-decoration: none;
310
+ font-weight: 500;
311
+ font-size: 0.875rem;
312
+ letter-spacing: 0.02em;
313
+ padding: 16px 20px;
314
+ transition: color .2s, background .2s;
315
+ position: relative;
316
+ }
317
+ .cly-nav__link::after {
318
+ content: '';
319
+ position: absolute;
320
+ bottom: 0; left: 20%; right: 20%;
321
+ height: 2px;
322
+ background: var(--cly-primary);
323
+ border-radius: 2px;
324
+ opacity: 0;
325
+ transition: opacity .2s;
326
+ }
327
+ .cly-nav__link:hover { color: var(--cly-text); background: rgba(124,107,255,0.06); }
328
+ .cly-nav__link:hover::after { opacity: 1; }
329
+
330
+ /* ── Headings ── */
331
+ .cly-heading {
332
+ font-size: clamp(2rem, 5vw, 3.2rem);
333
+ font-weight: 800;
334
+ line-height: 1.15;
335
+ letter-spacing: -0.03em;
336
+ background: linear-gradient(135deg, #ffffff 0%, #c8c0ff 50%, #ff9edd 100%);
337
+ -webkit-background-clip: text;
338
+ -webkit-text-fill-color: transparent;
339
+ background-clip: text;
340
+ padding-top: 48px;
341
+ }
342
+ h2.cly-heading {
343
+ font-size: clamp(1.1rem, 2.5vw, 1.4rem);
344
+ font-weight: 500;
345
+ background: linear-gradient(90deg, var(--cly-text2), var(--cly-accent));
346
+ -webkit-background-clip: text;
347
+ -webkit-text-fill-color: transparent;
348
+ background-clip: text;
349
+ letter-spacing: 0;
350
+ padding-top: 0;
351
+ margin-top: -8px;
352
+ margin-bottom: 8px;
353
+ }
354
+
355
+ /* ── Text ── */
356
+ .cly-text {
357
+ font-size: 1.05rem;
358
+ color: var(--cly-text2);
359
+ max-width: 640px;
360
+ line-height: 1.7;
361
+ }
362
+
363
+ /* ── Button ── */
364
+ .cly-button {
365
+ display: inline-flex;
366
+ align-items: center;
367
+ gap: 8px;
368
+ padding: 12px 26px;
369
+ background: linear-gradient(135deg, var(--cly-primary) 0%, #9b6bff 100%);
370
+ color: #fff;
371
+ border: none;
372
+ border-radius: 100px;
373
+ font-size: 0.925rem;
374
+ font-weight: 600;
375
+ font-family: var(--cly-font);
376
+ cursor: pointer;
377
+ transition: transform .18s cubic-bezier(.34,1.56,.64,1), box-shadow .2s;
378
+ box-shadow: 0 0 0 0 var(--cly-primary-glow), 0 4px 20px rgba(0,0,0,0.3);
379
+ position: relative;
380
+ overflow: hidden;
381
+ }
382
+ .cly-button::before {
383
+ content: '';
384
+ position: absolute;
385
+ inset: 0;
386
+ background: linear-gradient(rgba(255,255,255,0.12), transparent);
387
+ opacity: 0;
388
+ transition: opacity .2s;
389
+ }
390
+ .cly-button:hover {
391
+ transform: translateY(-2px) scale(1.02);
392
+ box-shadow: 0 0 28px var(--cly-primary-glow), 0 8px 32px rgba(0,0,0,0.4);
393
+ }
394
+ .cly-button:hover::before { opacity: 1; }
395
+ .cly-button:active { transform: scale(0.98); }
396
+
397
+ /* ── Input ── */
398
+ .cly-input {
399
+ width: 100%;
400
+ max-width: 440px;
401
+ padding: 12px 18px;
402
+ background: var(--cly-surface2);
403
+ border: 1.5px solid var(--cly-border);
404
+ border-radius: var(--cly-radius);
405
+ font-size: 0.975rem;
406
+ font-family: var(--cly-font);
407
+ color: var(--cly-text);
408
+ transition: border-color .2s, box-shadow .2s, background .2s;
409
+ outline: none;
410
+ }
411
+ .cly-input::placeholder { color: var(--cly-muted); }
412
+ .cly-input:focus {
413
+ border-color: var(--cly-primary);
414
+ background: rgba(124,107,255,0.06);
415
+ box-shadow: 0 0 0 3px rgba(124,107,255,0.15);
416
+ }
417
+
418
+ /* ── Output / Show ── */
419
+ .cly-output {
420
+ padding: 16px 20px;
421
+ background: linear-gradient(135deg, rgba(124,107,255,0.1), rgba(255,107,205,0.06));
422
+ border: 1px solid var(--cly-border);
423
+ border-radius: var(--cly-radius);
424
+ font-size: 1rem;
425
+ color: var(--cly-text);
426
+ animation: cly-pop .25s cubic-bezier(.34,1.56,.64,1);
427
+ backdrop-filter: blur(8px);
428
+ }
429
+
430
+ /* ── Link ── */
431
+ .cly-link {
432
+ display: inline-flex;
433
+ align-items: center;
434
+ gap: 6px;
435
+ color: var(--cly-primary);
436
+ text-decoration: none;
437
+ font-weight: 500;
438
+ font-size: 0.95rem;
439
+ padding: 6px 14px;
440
+ border: 1px solid var(--cly-border);
441
+ border-radius: 100px;
442
+ transition: all .2s;
443
+ margin: 2px;
444
+ }
445
+ .cly-link:hover {
446
+ background: rgba(124,107,255,0.1);
447
+ border-color: var(--cly-primary);
448
+ transform: translateY(-1px);
449
+ }
450
+
451
+ /* ── Image ── */
452
+ .cly-image {
453
+ max-width: 100%;
454
+ border-radius: var(--cly-radius);
455
+ box-shadow: 0 8px 40px rgba(0,0,0,0.5), 0 0 0 1px var(--cly-border2);
456
+ }
457
+
458
+ /* ── Section ── */
459
+ .cly-section {
460
+ background: var(--cly-surface);
461
+ border-radius: 20px;
462
+ padding: 28px;
463
+ border: 1px solid var(--cly-border2);
464
+ display: flex;
465
+ flex-direction: column;
466
+ gap: 14px;
467
+ position: relative;
468
+ overflow: hidden;
469
+ animation: cly-rise .4s ease both;
470
+ }
471
+ .cly-section::before {
472
+ content: '';
473
+ position: absolute;
474
+ top: 0; left: 0; right: 0;
475
+ height: 1px;
476
+ background: linear-gradient(90deg, transparent, var(--cly-primary), var(--cly-accent), transparent);
477
+ opacity: 0.5;
478
+ }
479
+ .cly-section__label {
480
+ font-size: 0.75rem;
481
+ font-weight: 700;
482
+ text-transform: uppercase;
483
+ letter-spacing: 0.12em;
484
+ color: var(--cly-primary);
485
+ margin-bottom: 4px;
486
+ }
487
+
488
+ /* ── Component ── */
489
+ .cly-component {
490
+ background: var(--cly-surface2);
491
+ border-radius: 18px;
492
+ padding: 24px;
493
+ border: 1px solid var(--cly-border);
494
+ display: flex;
495
+ flex-direction: column;
496
+ gap: 12px;
497
+ transition: transform .2s, box-shadow .2s, border-color .2s;
498
+ animation: cly-rise .35s ease both;
499
+ }
500
+ .cly-component:hover {
501
+ transform: translateY(-3px);
502
+ box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(124,107,255,0.3);
503
+ border-color: rgba(124,107,255,0.4);
504
+ }
505
+
506
+ /* ── Footer ── */
507
+ .cly-footer {
508
+ margin-top: 60px;
509
+ padding: 32px 0;
510
+ border-top: 1px solid var(--cly-border2);
511
+ color: var(--cly-muted);
512
+ font-size: 0.875rem;
513
+ text-align: center;
514
+ }
515
+
516
+ /* ── Animations ── */
517
+ @keyframes cly-pop {
518
+ from { opacity: 0; transform: scale(.95) translateY(4px); }
519
+ to { opacity: 1; transform: scale(1) translateY(0); }
520
+ }
521
+ @keyframes cly-rise {
522
+ from { opacity: 0; transform: translateY(16px); }
523
+ to { opacity: 1; transform: translateY(0); }
524
+ }
525
+
526
+ /* ── Responsive ── */
527
+ @media (max-width: 600px) {
528
+ .cly-page { padding: 0 16px 80px; }
529
+ .cly-heading { padding-top: 32px; }
530
+ .cly-button { width: 100%; justify-content: center; }
531
+ .cly-input { max-width: 100%; }
532
+ .cly-section, .cly-component { padding: 20px; border-radius: 16px; }
533
+ }
534
+ `;
535
+ }
536
+ module.exports = { compile, generateCSS };