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/ai/clou-ai-prompt.md +126 -6
- package/bin/clou.js +89 -1
- package/package.json +1 -1
- package/playground/clou-browser.js +434 -2
- package/playground/index.html +4 -7
- package/src/compiler.js +284 -2
- package/src/errors.js +2 -0
- package/src/lexer.js +28 -0
- package/src/parser.js +122 -0
- package/src/terminal-parser.js +68 -0
- package/src/terminal-runtime.js +56 -0
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">❮</button>\n <button class="clou-carousel-btn clou-carousel-next">❯</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);
|
package/src/terminal-parser.js
CHANGED
|
@@ -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);
|
package/src/terminal-runtime.js
CHANGED
|
@@ -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;
|