clou-lang 0.3.6 → 0.4.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/compiler.js CHANGED
@@ -17,6 +17,14 @@ class Compiler {
17
17
  this.hasAccordion = false;
18
18
  this.hasProgress = false;
19
19
  this.hasSlider = false;
20
+ this.hasCountdown = false;
21
+ this.hasTyping = false;
22
+ this.hasCarousel = false;
23
+ this.hasToast = false;
24
+ this.hasSearch = false;
25
+ this.hasTooltip = false;
26
+ this.hasDarkmode = false;
27
+ this.hasStars = false;
20
28
  this.hoverStyles = [];
21
29
  this.variables = {};
22
30
  this.templates = {};
@@ -80,6 +88,14 @@ class Compiler {
80
88
  const accordionCSS = this.hasAccordion ? this.buildAccordionCSS() : '';
81
89
  const progressCSS = this.hasProgress ? this.buildProgressCSS() : '';
82
90
  const sliderCSS = this.hasSlider ? this.buildSliderCSS() : '';
91
+ const countdownCSS = this.hasCountdown ? this.buildCountdownCSS() : '';
92
+ const typingCSS = this.hasTyping ? this.buildTypingCSS() : '';
93
+ const carouselCSS = this.hasCarousel ? this.buildCarouselCSS() : '';
94
+ const toastCSS = this.hasToast ? this.buildToastCSS() : '';
95
+ const searchCSS = this.hasSearch ? this.buildSearchCSS() : '';
96
+ const tooltipCSS = this.hasTooltip ? this.buildTooltipCSS() : '';
97
+ const darkmodeCSS = this.hasDarkmode ? this.buildDarkmodeCSS() : '';
98
+ const starsCSS = this.hasStars ? this.buildStarsCSS() : '';
83
99
  const hoverCSS = this.hoverStyles.join('\n');
84
100
  const themeCSS = this.activeTheme ? `\n /* Theme: ${this.activeTheme.name} */\n${generateThemeCSS(this.activeTheme)}` : '';
85
101
 
@@ -340,7 +356,7 @@ class Compiler {
340
356
  opacity: 0.7;
341
357
  border-top: 1px solid rgba(128,128,128,0.2);
342
358
  }
343
- ${navbarCSS}${modalCSS}${tabsCSS}${accordionCSS}${progressCSS}${sliderCSS}${animationCSS}${hoverCSS}${themeCSS}
359
+ ${navbarCSS}${modalCSS}${tabsCSS}${accordionCSS}${progressCSS}${sliderCSS}${countdownCSS}${typingCSS}${carouselCSS}${toastCSS}${searchCSS}${tooltipCSS}${darkmodeCSS}${starsCSS}${animationCSS}${hoverCSS}${themeCSS}
344
360
  </style>
345
361
  </head>
346
362
  <body>
@@ -489,6 +505,92 @@ ${scripts}
489
505
  `;
490
506
  }
491
507
 
508
+ buildCountdownCSS() {
509
+ const accent = this.buttonColorCSS || '#4A90D9';
510
+ return `
511
+ .clou-countdown { display: flex; gap: 16px; justify-content: center; margin: 16px 0; flex-wrap: wrap; }
512
+ .clou-countdown-label { width: 100%; text-align: center; font-size: 14px; opacity: 0.8; margin-bottom: 8px; }
513
+ .clou-countdown-unit { display: flex; flex-direction: column; align-items: center; padding: 16px 20px; background: rgba(128,128,128,0.1); border-radius: 12px; min-width: 80px; }
514
+ .clou-countdown-num { font-size: 2em; font-weight: 700; color: ${accent}; }
515
+ .clou-countdown-text { font-size: 12px; opacity: 0.7; text-transform: uppercase; letter-spacing: 1px; }
516
+ `;
517
+ }
518
+
519
+ buildTypingCSS() {
520
+ return `
521
+ .clou-typing { overflow: hidden; border-right: 3px solid currentColor; white-space: nowrap; margin: 16px 0; font-size: 1.5em; font-weight: 600; animation: clou-typing-text 3s steps(40) 1s forwards, clou-typing-cursor 0.75s step-end infinite; width: 0; }
522
+ @keyframes clou-typing-text { from { width: 0; } to { width: 100%; } }
523
+ @keyframes clou-typing-cursor { 50% { border-color: transparent; } }
524
+ `;
525
+ }
526
+
527
+ buildCarouselCSS() {
528
+ const accent = this.buttonColorCSS || '#4A90D9';
529
+ return `
530
+ .clou-carousel { position: relative; overflow: hidden; border-radius: 12px; margin: 16px 0; }
531
+ .clou-carousel-track { display: flex; transition: transform 0.5s ease; }
532
+ .clou-carousel-slide { min-width: 100%; }
533
+ .clou-carousel-slide img { width: 100%; height: auto; display: block; }
534
+ .clou-carousel-btn { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(0,0,0,0.5); color: white; border: none; padding: 12px 16px; cursor: pointer; font-size: 18px; border-radius: 50%; z-index: 10; transition: background 0.2s; }
535
+ .clou-carousel-btn:hover { background: ${accent}; transform: translateY(-50%) scale(1.05); box-shadow: none; }
536
+ .clou-carousel-prev { left: 12px; }
537
+ .clou-carousel-next { right: 12px; }
538
+ .clou-carousel-dots { display: flex; justify-content: center; gap: 8px; padding: 12px 0; }
539
+ .clou-carousel-dot { width: 10px; height: 10px; border-radius: 50%; background: rgba(128,128,128,0.4); border: none; cursor: pointer; padding: 0; transition: background 0.2s; }
540
+ .clou-carousel-dot:hover { transform: none; box-shadow: none; }
541
+ .clou-carousel-dot.active { background: ${accent}; }
542
+ `;
543
+ }
544
+
545
+ buildToastCSS() {
546
+ return `
547
+ .clou-toast-container { position: fixed; top: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 8px; }
548
+ .clou-toast { padding: 14px 24px; background: #333; color: white; border-radius: 8px; font-size: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); animation: clou-toast-in 0.3s ease, clou-toast-out 0.3s ease 2.7s forwards; }
549
+ @keyframes clou-toast-in { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } }
550
+ @keyframes clou-toast-out { from { opacity: 1; } to { opacity: 0; transform: translateX(40px); } }
551
+ `;
552
+ }
553
+
554
+ buildSearchCSS() {
555
+ const accent = this.buttonColorCSS || '#4A90D9';
556
+ return `
557
+ .clou-search { position: relative; margin: 12px 0; max-width: 500px; }
558
+ .clou-search input { width: 100%; padding: 14px 20px 14px 48px; font-size: 16px; border: 2px solid rgba(128,128,128,0.2); border-radius: 50px; outline: none; background: rgba(255,255,255,0.1); color: inherit; transition: border-color 0.2s, box-shadow 0.2s; }
559
+ .clou-search input:focus { border-color: ${accent}; box-shadow: 0 0 0 3px ${accent}33; }
560
+ .clou-search-icon { position: absolute; left: 18px; top: 50%; transform: translateY(-50%); font-size: 18px; opacity: 0.5; pointer-events: none; }
561
+ `;
562
+ }
563
+
564
+ buildTooltipCSS() {
565
+ return `
566
+ .clou-tooltip { position: relative; display: inline-block; cursor: help; border-bottom: 1px dotted currentColor; }
567
+ .clou-tooltip-text { visibility: hidden; background: #333; color: white; text-align: center; padding: 8px 14px; border-radius: 6px; font-size: 13px; position: absolute; z-index: 1000; bottom: 125%; left: 50%; transform: translateX(-50%); white-space: nowrap; opacity: 0; transition: opacity 0.2s; box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
568
+ .clou-tooltip-text::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #333 transparent transparent transparent; }
569
+ .clou-tooltip:hover .clou-tooltip-text { visibility: visible; opacity: 1; }
570
+ `;
571
+ }
572
+
573
+ buildDarkmodeCSS() {
574
+ return `
575
+ .clou-darkmode-toggle { position: fixed; top: 16px; right: 16px; z-index: 9999; width: 48px; height: 48px; border-radius: 50%; border: 2px solid rgba(128,128,128,0.3); background: rgba(128,128,128,0.1); backdrop-filter: blur(8px); cursor: pointer; font-size: 22px; display: flex; align-items: center; justify-content: center; transition: background 0.3s, transform 0.2s; padding: 0; }
576
+ .clou-darkmode-toggle:hover { background: rgba(128,128,128,0.2); transform: scale(1.1); box-shadow: none; }
577
+ body.clou-light { background: #ffffff !important; color: #333333 !important; }
578
+ body.clou-light .clou-card { background: rgba(0,0,0,0.05); }
579
+ body.clou-light .clou-navbar { background: rgba(255,255,255,0.8); }
580
+ body.clou-light input, body.clou-light textarea { background: rgba(0,0,0,0.05); border-color: rgba(0,0,0,0.15); color: #333; }
581
+ `;
582
+ }
583
+
584
+ buildStarsCSS() {
585
+ return `
586
+ .clou-stars { display: inline-flex; gap: 4px; margin: 8px 0; align-items: center; }
587
+ .clou-star { font-size: 1.4em; }
588
+ .clou-star.filled { color: #FFD700; }
589
+ .clou-star.empty { color: rgba(128,128,128,0.3); }
590
+ .clou-stars-label { margin-left: 8px; font-size: 14px; opacity: 0.7; }
591
+ `;
592
+ }
593
+
492
594
  buildAnimationCSS() {
493
595
  let css = '';
494
596
 
@@ -543,6 +645,52 @@ ${scripts}
543
645
  `;
544
646
  }
545
647
 
648
+ if (this.animations.has('pulse')) {
649
+ css += `
650
+ @keyframes clou-pulse {
651
+ 0%, 100% { transform: scale(1); opacity: 1; }
652
+ 50% { transform: scale(1.05); opacity: 0.8; }
653
+ }
654
+ .clou-animate-pulse { animation: clou-pulse 2s ease infinite; }
655
+ `;
656
+ }
657
+
658
+ if (this.animations.has('shake')) {
659
+ css += `
660
+ @keyframes clou-shake {
661
+ 0%, 100% { transform: translateX(0); }
662
+ 20% { transform: translateX(-8px); }
663
+ 40% { transform: translateX(8px); }
664
+ 60% { transform: translateX(-4px); }
665
+ 80% { transform: translateX(4px); }
666
+ }
667
+ .clou-animate-shake { animation: clou-shake 0.6s ease; }
668
+ `;
669
+ }
670
+
671
+ if (this.animations.has('float')) {
672
+ css += `
673
+ @keyframes clou-float {
674
+ 0%, 100% { transform: translateY(0); }
675
+ 50% { transform: translateY(-10px); }
676
+ }
677
+ .clou-animate-float { animation: clou-float 3s ease-in-out infinite; }
678
+ `;
679
+ }
680
+
681
+ if (this.animations.has('typewrite')) {
682
+ css += `
683
+ @keyframes clou-typewrite {
684
+ from { width: 0; }
685
+ to { width: 100%; }
686
+ }
687
+ @keyframes clou-blink {
688
+ 50% { border-color: transparent; }
689
+ }
690
+ .clou-animate-typewrite { overflow: hidden; white-space: nowrap; border-right: 3px solid currentColor; animation: clou-typewrite 3s steps(40) forwards, clou-blink 0.75s step-end infinite; }
691
+ `;
692
+ }
693
+
546
694
  return css;
547
695
  }
548
696
 
@@ -836,6 +984,137 @@ ${scripts}
836
984
  case 'Submit':
837
985
  return ` <button type="submit">${this.escapeHtml(this.interpolate(node.label))}</button>`;
838
986
 
987
+ case 'Countdown': {
988
+ this.hasCountdown = true;
989
+ const cdId = this.uid();
990
+ const label = node.label ? `<div class="clou-countdown-label">${this.escapeHtml(this.interpolate(node.label))}</div>` : '';
991
+ this.scripts.push(
992
+ `(function() {\n` +
993
+ ` var target = new Date(${JSON.stringify(node.date)}).getTime();\n` +
994
+ ` var el = document.getElementById('${cdId}');\n` +
995
+ ` function update() {\n` +
996
+ ` var now = Date.now();\n` +
997
+ ` var diff = Math.max(0, target - now);\n` +
998
+ ` var d = Math.floor(diff / 86400000);\n` +
999
+ ` var h = Math.floor((diff % 86400000) / 3600000);\n` +
1000
+ ` var m = Math.floor((diff % 3600000) / 60000);\n` +
1001
+ ` var s = Math.floor((diff % 60000) / 1000);\n` +
1002
+ ` el.querySelector('.cd-d').textContent = d;\n` +
1003
+ ` el.querySelector('.cd-h').textContent = h;\n` +
1004
+ ` el.querySelector('.cd-m').textContent = m;\n` +
1005
+ ` el.querySelector('.cd-s').textContent = s;\n` +
1006
+ ` if (diff > 0) setTimeout(update, 1000);\n` +
1007
+ ` }\n` +
1008
+ ` update();\n` +
1009
+ `})();`
1010
+ );
1011
+ return ` <div class="clou-countdown" id="${cdId}">\n ${label}\n <div class="clou-countdown-unit"><span class="clou-countdown-num cd-d">0</span><span class="clou-countdown-text">Days</span></div>\n <div class="clou-countdown-unit"><span class="clou-countdown-num cd-h">0</span><span class="clou-countdown-text">Hours</span></div>\n <div class="clou-countdown-unit"><span class="clou-countdown-num cd-m">0</span><span class="clou-countdown-text">Min</span></div>\n <div class="clou-countdown-unit"><span class="clou-countdown-num cd-s">0</span><span class="clou-countdown-text">Sec</span></div>\n </div>`;
1012
+ }
1013
+
1014
+ case 'Typing': {
1015
+ this.hasTyping = true;
1016
+ return ` <div class="clou-typing">${this.escapeHtml(this.interpolate(node.value))}</div>`;
1017
+ }
1018
+
1019
+ case 'Carousel': {
1020
+ this.hasCarousel = true;
1021
+ const carId = this.uid();
1022
+ const slides = (node.children || []).filter(c => c.type === 'Image' || c.type === 'Card');
1023
+ let slidesHtml = '';
1024
+ slides.forEach((slide, i) => {
1025
+ slidesHtml += `\n <div class="clou-carousel-slide">${this.compileNode(slide)}</div>`;
1026
+ });
1027
+ let dotsHtml = '';
1028
+ slides.forEach((_, i) => {
1029
+ dotsHtml += `<button class="clou-carousel-dot${i === 0 ? ' active' : ''}" data-idx="${i}"></button>`;
1030
+ });
1031
+ this.scripts.push(
1032
+ `(function() {\n` +
1033
+ ` var el = document.getElementById('${carId}');\n` +
1034
+ ` var track = el.querySelector('.clou-carousel-track');\n` +
1035
+ ` var dots = el.querySelectorAll('.clou-carousel-dot');\n` +
1036
+ ` var total = ${slides.length};\n` +
1037
+ ` var idx = 0;\n` +
1038
+ ` function go(n) { idx = (n + total) % total; track.style.transform = 'translateX(-' + (idx * 100) + '%)'; dots.forEach(function(d,i) { d.classList.toggle('active', i === idx); }); }\n` +
1039
+ ` el.querySelector('.clou-carousel-prev').onclick = function() { go(idx - 1); };\n` +
1040
+ ` el.querySelector('.clou-carousel-next').onclick = function() { go(idx + 1); };\n` +
1041
+ ` dots.forEach(function(d) { d.onclick = function() { go(parseInt(d.dataset.idx)); }; });\n` +
1042
+ `})();`
1043
+ );
1044
+ return ` <div class="clou-carousel" id="${carId}">\n <div class="clou-carousel-track">${slidesHtml}\n </div>\n <button class="clou-carousel-btn clou-carousel-prev">&#10094;</button>\n <button class="clou-carousel-btn clou-carousel-next">&#10095;</button>\n <div class="clou-carousel-dots">${dotsHtml}</div>\n </div>`;
1045
+ }
1046
+
1047
+ case 'Embed': {
1048
+ const url = this.interpolate(node.url);
1049
+ // Convert YouTube watch URLs to embed
1050
+ let embedUrl = url;
1051
+ if (url.includes('youtube.com/watch')) {
1052
+ const vid = url.split('v=')[1];
1053
+ if (vid) embedUrl = 'https://www.youtube.com/embed/' + vid.split('&')[0];
1054
+ } else if (url.includes('youtu.be/')) {
1055
+ embedUrl = 'https://www.youtube.com/embed/' + url.split('youtu.be/')[1];
1056
+ }
1057
+ return ` <iframe src="${this.escapeHtml(embedUrl)}" style="width: 100%; height: 400px; border: none; border-radius: 12px; margin: 12px 0;" allowfullscreen></iframe>`;
1058
+ }
1059
+
1060
+ case 'Badge': {
1061
+ const style = this.extractInlineStyles(node.children || []);
1062
+ const accent = this.buttonColorCSS || '#4A90D9';
1063
+ const styleStr = style ? style + '; ' : '';
1064
+ return ` <span style="${styleStr}display: inline-block; padding: 4px 12px; font-size: 12px; font-weight: 600; border-radius: 20px; background: ${accent}; color: white; text-transform: uppercase; letter-spacing: 0.5px;">${this.escapeHtml(this.interpolate(node.value))}</span>`;
1065
+ }
1066
+
1067
+ case 'Avatar': {
1068
+ const style = this.extractInlineStyles(node.children || []);
1069
+ const styleStr = style ? style + '; ' : '';
1070
+ return ` <img src="${this.escapeHtml(this.interpolate(node.src))}" alt="avatar" style="${styleStr}width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin: 8px 0;">`;
1071
+ }
1072
+
1073
+ case 'Stars': {
1074
+ this.hasStars = true;
1075
+ const count = Math.min(Math.max(node.value, 0), 5);
1076
+ let starsHtml = '';
1077
+ for (let i = 0; i < 5; i++) {
1078
+ starsHtml += `<span class="clou-star ${i < count ? 'filled' : 'empty'}">${i < count ? '\u2605' : '\u2606'}</span>`;
1079
+ }
1080
+ const label = node.label ? `<span class="clou-stars-label">${this.escapeHtml(this.interpolate(node.label))}</span>` : '';
1081
+ return ` <div class="clou-stars">${starsHtml}${label}</div>`;
1082
+ }
1083
+
1084
+ case 'Search': {
1085
+ this.hasSearch = true;
1086
+ const id = this.uid();
1087
+ return ` <div class="clou-search">\n <span class="clou-search-icon">\u{1F50D}</span>\n <input id="${id}" type="text" placeholder="${this.escapeHtml(this.interpolate(node.placeholder))}">\n </div>`;
1088
+ }
1089
+
1090
+ case 'Toast': {
1091
+ // Toast as action (inside button)
1092
+ this.hasToast = true;
1093
+ return '';
1094
+ }
1095
+
1096
+ case 'Tooltip': {
1097
+ this.hasTooltip = true;
1098
+ return ` <span class="clou-tooltip">${this.escapeHtml(this.interpolate(node.text))}<span class="clou-tooltip-text">${this.escapeHtml(this.interpolate(node.tip))}</span></span>`;
1099
+ }
1100
+
1101
+ case 'Darkmode': {
1102
+ this.hasDarkmode = true;
1103
+ const dmId = this.uid();
1104
+ this.scripts.push(
1105
+ `(function() {\n` +
1106
+ ` var btn = document.getElementById('${dmId}');\n` +
1107
+ ` var isDark = true;\n` +
1108
+ ` btn.onclick = function() {\n` +
1109
+ ` isDark = !isDark;\n` +
1110
+ ` document.body.classList.toggle('clou-light', !isDark);\n` +
1111
+ ` btn.textContent = isDark ? '\\u263E' : '\\u2600';\n` +
1112
+ ` };\n` +
1113
+ `})();`
1114
+ );
1115
+ return ` <button class="clou-darkmode-toggle" id="${dmId}">\u263E</button>`;
1116
+ }
1117
+
839
1118
  case 'Icon':
840
1119
  return ` <span class="clou-icon">${node.value}</span>`;
841
1120
 
@@ -942,6 +1221,9 @@ ${scripts}
942
1221
  return `document.getElementById(${JSON.stringify(node.target)} + '-overlay').classList.remove('active');`;
943
1222
  case 'GoTo':
944
1223
  return `window.location.href = ${JSON.stringify(node.url)};`;
1224
+ case 'Toast':
1225
+ this.hasToast = true;
1226
+ return `(function() { var c = document.querySelector('.clou-toast-container'); if (!c) { c = document.createElement('div'); c.className = 'clou-toast-container'; document.body.appendChild(c); } var t = document.createElement('div'); t.className = 'clou-toast'; t.textContent = ${JSON.stringify(this.interpolate(node.value))}; c.appendChild(t); setTimeout(function() { t.remove(); }, 3000); })();`;
945
1227
  default:
946
1228
  return '';
947
1229
  }
@@ -988,7 +1270,7 @@ ${scripts}
988
1270
  }
989
1271
 
990
1272
  isAction(node) {
991
- return ['ShowMessage', 'Show', 'Hide', 'Toggle', 'GoTo', 'Open', 'Close'].includes(node.type);
1273
+ return ['ShowMessage', 'Show', 'Hide', 'Toggle', 'GoTo', 'Open', 'Close', 'Toast'].includes(node.type);
992
1274
  }
993
1275
 
994
1276
  // Filter only action nodes
package/src/errors.js CHANGED
@@ -31,6 +31,8 @@ const KNOWN_KEYWORDS = [
31
31
  'card', 'icon', 'modal', 'grid', 'section', 'space', 'columns',
32
32
  'form', 'table', 'tabs', 'tab', 'accordion', 'panel', 'progress',
33
33
  'dropdown', 'option', 'textarea', 'checkbox', 'audio', 'code', 'slider', 'submit',
34
+ 'countdown', 'typing', 'carousel', 'embed', 'badge', 'avatar', 'stars', 'search',
35
+ 'toast', 'tooltip', 'darkmode', 'random', 'fetch', 'menu',
34
36
  'show', 'message', 'hide', 'toggle', 'go', 'open', 'close',
35
37
  'style', 'background', 'color', 'size', 'bold', 'italic', 'center',
36
38
  'left', 'right', 'rounded', 'shadow', 'padding', 'margin', 'width',
package/src/lexer.js CHANGED
@@ -53,6 +53,17 @@ const TokenType = {
53
53
  CODE: 'CODE',
54
54
  SLIDER: 'SLIDER',
55
55
  SUBMIT: 'SUBMIT',
56
+ COUNTDOWN: 'COUNTDOWN',
57
+ TYPING: 'TYPING',
58
+ CAROUSEL: 'CAROUSEL',
59
+ EMBED: 'EMBED',
60
+ BADGE: 'BADGE',
61
+ AVATAR: 'AVATAR',
62
+ STARS: 'STARS',
63
+ SEARCH: 'SEARCH',
64
+ TOAST: 'TOAST',
65
+ TOOLTIP: 'TOOLTIP',
66
+ DARKMODE: 'DARKMODE',
56
67
 
57
68
  // Keywords - Actions
58
69
  SHOW: 'SHOW',
@@ -140,6 +151,9 @@ const TokenType = {
140
151
  FUNCTION: 'FUNCTION',
141
152
  CALL: 'CALL',
142
153
  RETURN: 'RETURN',
154
+ RANDOM: 'RANDOM',
155
+ FETCH: 'FETCH',
156
+ MENU: 'MENU',
143
157
  LPAREN: 'LPAREN',
144
158
  RPAREN: 'RPAREN',
145
159
  COMMA: 'COMMA',
@@ -195,6 +209,17 @@ const KEYWORDS = {
195
209
  'code': TokenType.CODE,
196
210
  'slider': TokenType.SLIDER,
197
211
  'submit': TokenType.SUBMIT,
212
+ 'countdown': TokenType.COUNTDOWN,
213
+ 'typing': TokenType.TYPING,
214
+ 'carousel': TokenType.CAROUSEL,
215
+ 'embed': TokenType.EMBED,
216
+ 'badge': TokenType.BADGE,
217
+ 'avatar': TokenType.AVATAR,
218
+ 'stars': TokenType.STARS,
219
+ 'search': TokenType.SEARCH,
220
+ 'toast': TokenType.TOAST,
221
+ 'tooltip': TokenType.TOOLTIP,
222
+ 'darkmode': TokenType.DARKMODE,
198
223
  'show': TokenType.SHOW,
199
224
  'message': TokenType.MESSAGE,
200
225
  'hide': TokenType.HIDE,
@@ -274,6 +299,9 @@ const KEYWORDS = {
274
299
  'function': TokenType.FUNCTION,
275
300
  'call': TokenType.CALL,
276
301
  'return': TokenType.RETURN,
302
+ 'random': TokenType.RANDOM,
303
+ 'fetch': TokenType.FETCH,
304
+ 'menu': TokenType.MENU,
277
305
  'import': TokenType.IMPORT,
278
306
  'at': TokenType.AT,
279
307
  'to': TokenType.TO,
package/src/parser.js CHANGED
@@ -172,6 +172,17 @@ class Parser {
172
172
  case TokenType.CODE: return this.parseCode();
173
173
  case TokenType.SLIDER: return this.parseSlider();
174
174
  case TokenType.SUBMIT: return this.parseSubmit();
175
+ case TokenType.COUNTDOWN: return this.parseCountdown();
176
+ case TokenType.TYPING: return this.parseTyping();
177
+ case TokenType.CAROUSEL: return this.parseCarousel();
178
+ case TokenType.EMBED: return this.parseEmbed();
179
+ case TokenType.BADGE: return this.parseBadge();
180
+ case TokenType.AVATAR: return this.parseAvatar();
181
+ case TokenType.STARS: return this.parseStars();
182
+ case TokenType.SEARCH: return this.parseSearch();
183
+ case TokenType.TOAST: return this.parseToast();
184
+ case TokenType.TOOLTIP: return this.parseTooltip();
185
+ case TokenType.DARKMODE: return this.parseDarkmode();
175
186
  case TokenType.SET: return this.parseSetVariable();
176
187
  case TokenType.USE: return this.parseUse();
177
188
  case TokenType.REPEAT: return this.parseRepeat();
@@ -690,6 +701,117 @@ class Parser {
690
701
  return { type: 'Submit', label };
691
702
  }
692
703
 
704
+ // countdown "2026-12-31" OR countdown "2026-12-31" "Event Name"
705
+ parseCountdown() {
706
+ this.expect(TokenType.COUNTDOWN);
707
+ const date = this.expect(TokenType.STRING).value;
708
+ let label = null;
709
+ if (this.peek() && this.peek().type === TokenType.STRING) {
710
+ label = this.advance().value;
711
+ }
712
+ return { type: 'Countdown', date, label };
713
+ }
714
+
715
+ // typing "Hello World"
716
+ parseTyping() {
717
+ this.expect(TokenType.TYPING);
718
+ const value = this.expect(TokenType.STRING).value;
719
+ return { type: 'Typing', value };
720
+ }
721
+
722
+ // carousel:
723
+ parseCarousel() {
724
+ this.expect(TokenType.CAROUSEL);
725
+ const node = { type: 'Carousel', children: [] };
726
+
727
+ if (this.peek() && this.peek().type === TokenType.COLON) {
728
+ this.advance();
729
+ this.skipNewlines();
730
+ node.children = this.parseBlock();
731
+ }
732
+
733
+ return node;
734
+ }
735
+
736
+ // embed "url"
737
+ parseEmbed() {
738
+ this.expect(TokenType.EMBED);
739
+ const url = this.expect(TokenType.STRING).value;
740
+ return { type: 'Embed', url };
741
+ }
742
+
743
+ // badge "text"
744
+ parseBadge() {
745
+ this.expect(TokenType.BADGE);
746
+ const value = this.expect(TokenType.STRING).value;
747
+ const node = { type: 'Badge', value, children: [] };
748
+
749
+ if (this.peek() && this.peek().type === TokenType.COLON) {
750
+ this.advance();
751
+ this.skipNewlines();
752
+ node.children = this.parseBlock();
753
+ }
754
+
755
+ return node;
756
+ }
757
+
758
+ // avatar "url"
759
+ parseAvatar() {
760
+ this.expect(TokenType.AVATAR);
761
+ const src = this.expect(TokenType.STRING).value;
762
+ const node = { type: 'Avatar', src, children: [] };
763
+
764
+ if (this.peek() && this.peek().type === TokenType.COLON) {
765
+ this.advance();
766
+ this.skipNewlines();
767
+ node.children = this.parseBlock();
768
+ }
769
+
770
+ return node;
771
+ }
772
+
773
+ // stars 4 OR stars 4 "Rating"
774
+ parseStars() {
775
+ this.expect(TokenType.STARS);
776
+ const value = this.expect(TokenType.NUMBER).value;
777
+ let label = null;
778
+ if (this.peek() && this.peek().type === TokenType.STRING) {
779
+ label = this.advance().value;
780
+ }
781
+ return { type: 'Stars', value: parseInt(value, 10), label };
782
+ }
783
+
784
+ // search "placeholder"
785
+ parseSearch() {
786
+ this.expect(TokenType.SEARCH);
787
+ const placeholder = this.expect(TokenType.STRING).value;
788
+ return { type: 'Search', placeholder };
789
+ }
790
+
791
+ // toast "message" (as action inside button)
792
+ parseToast() {
793
+ this.expect(TokenType.TOAST);
794
+ const value = this.expect(TokenType.STRING).value;
795
+ return { type: 'Toast', value };
796
+ }
797
+
798
+ // tooltip "text" "hover text"
799
+ parseTooltip() {
800
+ this.expect(TokenType.TOOLTIP);
801
+ const text = this.expect(TokenType.STRING).value;
802
+ let tip = '';
803
+ if (this.peek() && this.peek().type === TokenType.STRING) {
804
+ tip = this.advance().value;
805
+ }
806
+ return { type: 'Tooltip', text, tip };
807
+ }
808
+
809
+ // darkmode
810
+ parseDarkmode() {
811
+ this.expect(TokenType.DARKMODE);
812
+ return { type: 'Darkmode' };
813
+ }
814
+
693
815
  // button "text":
694
816
  parseButton() {
695
817
  this.expect(TokenType.BUTTON);
@@ -106,6 +106,9 @@ class TerminalParser {
106
106
  case TokenType.FUNCTION: return this.parseFunction();
107
107
  case TokenType.CALL: return this.parseCall();
108
108
  case TokenType.RETURN: return this.parseReturn();
109
+ case TokenType.RANDOM: return this.parseRandom();
110
+ case TokenType.FETCH: return this.parseFetch();
111
+ case TokenType.MENU: return this.parseMenu();
109
112
  default:
110
113
  this.advance();
111
114
  return { type: 'Noop' };
@@ -348,6 +351,71 @@ class TerminalParser {
348
351
  return { type: 'Call', name, args };
349
352
  }
350
353
 
354
+ // random 1 to 10 save as num
355
+ parseRandom() {
356
+ this.expect(TokenType.RANDOM);
357
+ const min = this.advance().value;
358
+ this.expect(TokenType.TO);
359
+ const max = this.advance().value;
360
+ this.expect(TokenType.SAVE);
361
+ this.expect(TokenType.AS);
362
+ const varName = this.advance().value;
363
+ return { type: 'Random', min, max, varName };
364
+ }
365
+
366
+ // fetch "url" save as data
367
+ parseFetch() {
368
+ this.expect(TokenType.FETCH);
369
+ const url = this.expect(TokenType.STRING).value;
370
+ let varName = null;
371
+ if (this.match(TokenType.SAVE)) {
372
+ this.expect(TokenType.AS);
373
+ varName = this.advance().value;
374
+ }
375
+ return { type: 'Fetch', url, varName };
376
+ }
377
+
378
+ // menu "Choose?":
379
+ // option "A":
380
+ // print "selected A"
381
+ // option "B":
382
+ // print "selected B"
383
+ parseMenu() {
384
+ this.expect(TokenType.MENU);
385
+ const prompt = this.expect(TokenType.STRING).value;
386
+ let varName = null;
387
+ if (this.match(TokenType.SAVE)) {
388
+ this.expect(TokenType.AS);
389
+ varName = this.advance().value;
390
+ }
391
+ this.expect(TokenType.COLON);
392
+ this.skipNewlines();
393
+
394
+ const options = [];
395
+ if (this.match(TokenType.INDENT)) {
396
+ this.skipNewlines();
397
+ while (this.peek() && this.peek().type !== TokenType.DEDENT && this.peek().type !== TokenType.EOF) {
398
+ this.skipNewlines();
399
+ if (this.peek() && this.peek().type === TokenType.OPTION) {
400
+ this.advance(); // option
401
+ const label = this.expect(TokenType.STRING).value;
402
+ let body = [];
403
+ if (this.match(TokenType.COLON)) {
404
+ this.skipNewlines();
405
+ body = this.parseBlock();
406
+ }
407
+ options.push({ label, body });
408
+ } else if (this.peek() && this.peek().type !== TokenType.DEDENT && this.peek().type !== TokenType.EOF) {
409
+ this.advance(); // skip unknowns
410
+ }
411
+ this.skipNewlines();
412
+ }
413
+ this.match(TokenType.DEDENT);
414
+ }
415
+
416
+ return { type: 'Menu', prompt, varName, options };
417
+ }
418
+
351
419
  // return "value"
352
420
  parseReturn() {
353
421
  this.expect(TokenType.RETURN);
@@ -290,6 +290,62 @@ class TerminalRuntime {
290
290
  break;
291
291
  }
292
292
 
293
+ case 'Random': {
294
+ const min = Math.floor(this.resolveNum(node.min));
295
+ const max = Math.floor(this.resolveNum(node.max));
296
+ const rand = Math.floor(Math.random() * (max - min + 1)) + min;
297
+ this.variables[node.varName] = String(rand);
298
+ break;
299
+ }
300
+
301
+ case 'Fetch': {
302
+ try {
303
+ const url = this.interpolate(node.url);
304
+ // Use dynamic import for fetch (Node 18+) or fallback to https
305
+ let data;
306
+ if (typeof fetch === 'function') {
307
+ const res = await fetch(url);
308
+ data = await res.text();
309
+ } else {
310
+ // Fallback for older Node.js
311
+ const https = url.startsWith('https') ? require('https') : require('http');
312
+ data = await new Promise((resolve, reject) => {
313
+ https.get(url, (res) => {
314
+ let body = '';
315
+ res.on('data', chunk => body += chunk);
316
+ res.on('end', () => resolve(body));
317
+ }).on('error', reject);
318
+ });
319
+ }
320
+ if (node.varName) {
321
+ this.variables[node.varName] = data;
322
+ } else {
323
+ console.log(data);
324
+ }
325
+ } catch (err) {
326
+ this.print(`Fetch error: ${err.message}`, 'red');
327
+ if (node.varName) this.variables[node.varName] = '';
328
+ }
329
+ break;
330
+ }
331
+
332
+ case 'Menu': {
333
+ // Show numbered options
334
+ this.print(this.interpolate(node.prompt), 'cyan');
335
+ for (let i = 0; i < node.options.length; i++) {
336
+ this.print(` ${COLORS.yellow}${i + 1}${COLORS.reset}) ${node.options[i].label}`);
337
+ }
338
+ const answer = await this.askInput('Pick a number');
339
+ const idx = parseInt(answer) - 1;
340
+ if (node.varName) {
341
+ this.variables[node.varName] = idx >= 0 && idx < node.options.length ? node.options[idx].label : '';
342
+ }
343
+ if (idx >= 0 && idx < node.options.length && node.options[idx].body.length > 0) {
344
+ await this.execute(node.options[idx].body);
345
+ }
346
+ break;
347
+ }
348
+
293
349
  case 'Return':
294
350
  // In terminal mode, return just stops function execution
295
351
  break;