clou-lang 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -47,6 +47,7 @@ That's it. Your website opens in the browser.
47
47
  clou mysite.clou # Build and open in browser
48
48
  clou build mysite.clou # Build to HTML
49
49
  clou dev mysite.clou # Live dev server with auto-reload
50
+ clou deploy mysite.clou # Deploy to the web (one command!)
50
51
  clou playground # Open browser-based live editor
51
52
  clou themes # List all 10 built-in themes
52
53
  clou ai # Copy AI prompt to clipboard
@@ -56,14 +57,16 @@ clou help # Show help
56
57
  ## Features
57
58
 
58
59
  ### Website Mode
59
- - **19 elements**: page, heading, text, image, video, button, link, input, list, box, row, grid, card, section, navbar, footer, modal, icon, space
60
+ - **30+ elements**: page, heading, text, image, video, button, link, input, list, box, row, grid, card, section, navbar, footer, modal, icon, space, form, table, tabs, accordion, progress, dropdown, textarea, checkbox, audio, code, slider
60
61
  - **10 themes**: neon, ocean, sunset, forest, candy, minimal, midnight, retro, glass, aurora
61
62
  - **Variables**: `set name to "Alex"` + `{name}` interpolation
62
63
  - **Templates**: reusable components with parameters
63
64
  - **Animations**: fade, slide, bounce, grow, glow
64
65
  - **Multi-page**: `page "About" at "/about":`
65
66
  - **Import**: `import "shared.clou"`
67
+ - **Deploy**: `clou deploy mysite.clou` — one command to go live
66
68
  - **Styling**: gradients, shadows, rounded corners, responsive grids
69
+ - **Smart errors**: helpful error messages with line numbers and tips
67
70
 
68
71
  ### Terminal Mode
69
72
  - **Print with colors**: `print color green "Success!"`
package/bin/clou.js CHANGED
@@ -11,14 +11,15 @@
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
13
  const { buildClou } = require('../src/index');
14
+ const { formatError } = require('../src/errors');
14
15
 
15
16
  const args = process.argv.slice(2);
16
17
 
17
18
  function printHelp() {
18
19
  console.log(`
19
20
  ╔═══════════════════════════════════════╗
20
- ║ CLOU Language v0.2
21
- Simple enough for kids to code.
21
+ ║ CLOU Language v0.3
22
+ Describe it once. Run everywhere.
22
23
  ╚═══════════════════════════════════════╝
23
24
 
24
25
  Usage:
@@ -28,13 +29,26 @@ function printHelp() {
28
29
  clou build <file.clou> -o <output> Build to specific file
29
30
  clou dev <file.clou> Start live dev server
30
31
  clou dev <file.clou> -p <port> Dev server on custom port
32
+ clou deploy <file.clou> Deploy to the web (surge)
33
+ clou deploy <file.clou> --github Deploy to GitHub Pages
34
+ clou deploy <file.clou> --netlify Deploy with Netlify
31
35
  clou themes List all available themes
36
+ clou playground Open browser-based editor
37
+ clou ai Copy AI prompt to clipboard
32
38
  clou help Show this help
33
39
 
34
40
  Examples:
35
41
  clou mysite.clou
36
42
  clou build mysite.clou -o index.html
37
43
  clou dev mysite.clou
44
+ clou deploy mysite.clou
45
+
46
+ Elements (30+):
47
+ page, heading, text, image, video, button, link,
48
+ input, textarea, checkbox, dropdown, slider, form,
49
+ list, box, row, grid, card, section, navbar, footer,
50
+ modal, icon, space, table, tabs, accordion, progress,
51
+ audio, code
38
52
 
39
53
  Themes:
40
54
  neon, ocean, sunset, forest, candy,
@@ -121,14 +135,14 @@ function runFile(inputFile) {
121
135
  // Auto-detect: terminal app or website?
122
136
  if (isTerminalApp(source)) {
123
137
  runTerminal(inputFile).catch(err => {
124
- console.error(`\n Clou Error: ${err.message}\n`);
138
+ console.error(formatError(err, source, inputFile));
125
139
  process.exit(1);
126
140
  });
127
141
  return;
128
142
  }
129
143
 
130
144
  const basePath = path.dirname(path.resolve(inputFile));
131
- const result = buildClou(source, { basePath });
145
+ const result = buildClou(source, { basePath, filename: inputFile });
132
146
 
133
147
  if (result.pages) {
134
148
  // Multi-page build
@@ -153,7 +167,7 @@ function runFile(inputFile) {
153
167
  function buildFile(inputFile, outputFile) {
154
168
  const source = fs.readFileSync(inputFile, 'utf-8');
155
169
  const basePath = path.dirname(path.resolve(inputFile));
156
- const result = buildClou(source, { basePath });
170
+ const result = buildClou(source, { basePath, filename: inputFile });
157
171
 
158
172
  if (result.pages) {
159
173
  const outDir = outputFile ? path.dirname(outputFile) : path.dirname(path.resolve(inputFile));
@@ -235,6 +249,40 @@ if (args[0] === 'playground' || args[0] === 'play') {
235
249
  process.exit(0);
236
250
  }
237
251
 
252
+ // Handle deploy command (before smart detection)
253
+ if (args[0] === 'deploy') {
254
+ const { ClouDeploy } = require('../src/deploy');
255
+ const deployFile = args[1];
256
+
257
+ if (!deployFile) {
258
+ console.error(' Error: No input file specified.');
259
+ console.error(' Usage: clou deploy mysite.clou');
260
+ process.exit(1);
261
+ }
262
+
263
+ if (!fs.existsSync(deployFile)) {
264
+ console.error(` Error: File not found: ${deployFile}`);
265
+ process.exit(1);
266
+ }
267
+
268
+ let target = 'auto';
269
+ if (args.includes('--github')) target = 'github';
270
+ else if (args.includes('--netlify')) target = 'netlify';
271
+ else if (args.includes('--surge')) target = 'surge';
272
+
273
+ const domainIdx = args.indexOf('--domain');
274
+ const domain = domainIdx !== -1 ? args[domainIdx + 1] : null;
275
+
276
+ const deployer = new ClouDeploy(deployFile, { target, domain });
277
+ deployer.deploy().catch(err => {
278
+ console.error(`\n Deploy Error: ${err.message}\n`);
279
+ process.exit(1);
280
+ });
281
+
282
+ // Don't fall through to other commands
283
+ return;
284
+ }
285
+
238
286
  // Smart detection: if first arg ends with .clou, treat as "clou run file.clou"
239
287
  let command, inputFile;
240
288
 
@@ -276,6 +324,9 @@ try {
276
324
  process.exit(1);
277
325
  }
278
326
  } catch (err) {
279
- console.error(`\n Clou Error: ${err.message}\n`);
327
+ // Read source for error formatting
328
+ let source = '';
329
+ try { source = fs.readFileSync(inputFile, 'utf-8'); } catch {}
330
+ console.error(formatError(err, source, inputFile));
280
331
  process.exit(1);
281
332
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clou-lang",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Clou - A programming language so simple, even kids can build websites and terminal apps",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/compiler.js CHANGED
@@ -13,6 +13,10 @@ class Compiler {
13
13
  this.animations = new Set();
14
14
  this.hasModal = false;
15
15
  this.hasNavbar = false;
16
+ this.hasTabs = false;
17
+ this.hasAccordion = false;
18
+ this.hasProgress = false;
19
+ this.hasSlider = false;
16
20
  this.hoverStyles = [];
17
21
  this.variables = {};
18
22
  this.templates = {};
@@ -72,6 +76,10 @@ class Compiler {
72
76
  const animationCSS = this.buildAnimationCSS();
73
77
  const modalCSS = this.hasModal ? this.buildModalCSS() : '';
74
78
  const navbarCSS = this.hasNavbar ? this.buildNavbarCSS() : '';
79
+ const tabsCSS = this.hasTabs ? this.buildTabsCSS() : '';
80
+ const accordionCSS = this.hasAccordion ? this.buildAccordionCSS() : '';
81
+ const progressCSS = this.hasProgress ? this.buildProgressCSS() : '';
82
+ const sliderCSS = this.hasSlider ? this.buildSliderCSS() : '';
75
83
  const hoverCSS = this.hoverStyles.join('\n');
76
84
  const themeCSS = this.activeTheme ? `\n /* Theme: ${this.activeTheme.name} */\n${generateThemeCSS(this.activeTheme)}` : '';
77
85
 
@@ -249,6 +257,82 @@ class Compiler {
249
257
  display: inline-block;
250
258
  }
251
259
 
260
+ .clou-table {
261
+ width: 100%;
262
+ border-collapse: collapse;
263
+ margin: 16px 0;
264
+ }
265
+
266
+ .clou-table th, .clou-table td {
267
+ padding: 12px 16px;
268
+ text-align: left;
269
+ border-bottom: 1px solid rgba(128,128,128,0.2);
270
+ }
271
+
272
+ .clou-table th {
273
+ font-weight: 600;
274
+ opacity: 0.8;
275
+ }
276
+
277
+ .clou-table tr:hover td {
278
+ background: rgba(128,128,128,0.05);
279
+ }
280
+
281
+ .clou-form {
282
+ margin: 16px 0;
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: 12px;
286
+ }
287
+
288
+ .clou-checkbox {
289
+ display: flex;
290
+ align-items: center;
291
+ gap: 8px;
292
+ margin: 8px 0;
293
+ cursor: pointer;
294
+ }
295
+
296
+ .clou-checkbox input[type="checkbox"] {
297
+ width: 18px;
298
+ height: 18px;
299
+ cursor: pointer;
300
+ }
301
+
302
+ .clou-code {
303
+ background: rgba(0,0,0,0.3);
304
+ border-radius: 8px;
305
+ padding: 16px 20px;
306
+ margin: 12px 0;
307
+ overflow-x: auto;
308
+ font-family: 'Fira Code', 'Consolas', monospace;
309
+ font-size: 14px;
310
+ line-height: 1.5;
311
+ }
312
+
313
+ .clou-audio {
314
+ width: 100%;
315
+ max-width: 400px;
316
+ margin: 8px 0;
317
+ }
318
+
319
+ .clou-dropdown {
320
+ margin: 8px 0;
321
+ }
322
+
323
+ .clou-dropdown label {
324
+ display: block;
325
+ margin-bottom: 6px;
326
+ font-size: 14px;
327
+ opacity: 0.8;
328
+ }
329
+
330
+ select.clou-input {
331
+ width: auto;
332
+ min-width: 200px;
333
+ cursor: pointer;
334
+ }
335
+
252
336
  .clou-footer {
253
337
  padding: 40px 20px;
254
338
  margin-top: 40px;
@@ -256,7 +340,7 @@ class Compiler {
256
340
  opacity: 0.7;
257
341
  border-top: 1px solid rgba(128,128,128,0.2);
258
342
  }
259
- ${navbarCSS}${modalCSS}${animationCSS}${hoverCSS}${themeCSS}
343
+ ${navbarCSS}${modalCSS}${tabsCSS}${accordionCSS}${progressCSS}${sliderCSS}${animationCSS}${hoverCSS}${themeCSS}
260
344
  </style>
261
345
  </head>
262
346
  <body>
@@ -361,6 +445,50 @@ ${scripts}
361
445
  `;
362
446
  }
363
447
 
448
+ buildTabsCSS() {
449
+ return `
450
+ .clou-tabs { margin: 16px 0; }
451
+ .clou-tab-buttons { display: flex; gap: 0; border-bottom: 2px solid rgba(128,128,128,0.2); margin-bottom: 16px; }
452
+ .clou-tab-btn { padding: 10px 24px; background: none; border: none; color: inherit; cursor: pointer; font-size: 16px; font-weight: 500; opacity: 0.6; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: opacity 0.2s, border-color 0.2s; }
453
+ .clou-tab-btn:hover { opacity: 0.9; transform: none; box-shadow: none; }
454
+ .clou-tab-btn.active { opacity: 1; border-bottom-color: currentColor; }
455
+ .clou-tab-panel { display: none; }
456
+ .clou-tab-panel.active { display: block; }
457
+ `;
458
+ }
459
+
460
+ buildAccordionCSS() {
461
+ return `
462
+ .clou-accordion { margin: 16px 0; }
463
+ .clou-panel { border: 1px solid rgba(128,128,128,0.2); border-radius: 8px; margin: 8px 0; overflow: hidden; }
464
+ .clou-panel-header { padding: 16px 20px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-weight: 600; transition: background 0.2s; }
465
+ .clou-panel-header:hover { background: rgba(128,128,128,0.1); }
466
+ .clou-panel-arrow { transition: transform 0.3s; font-size: 12px; }
467
+ .clou-panel.open .clou-panel-arrow { transform: rotate(180deg); }
468
+ .clou-panel-body { max-height: 0; overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; }
469
+ .clou-panel.open .clou-panel-body { max-height: 500px; padding: 0 20px 16px; }
470
+ `;
471
+ }
472
+
473
+ buildProgressCSS() {
474
+ const accent = this.buttonColorCSS || '#4A90D9';
475
+ return `
476
+ .clou-progress { margin: 12px 0; }
477
+ .clou-progress-label { font-size: 14px; margin-bottom: 6px; opacity: 0.8; }
478
+ .clou-progress-bar { width: 100%; height: 12px; background: rgba(128,128,128,0.2); border-radius: 6px; overflow: hidden; }
479
+ .clou-progress-fill { height: 100%; background: ${accent}; border-radius: 6px; transition: width 0.6s ease; }
480
+ `;
481
+ }
482
+
483
+ buildSliderCSS() {
484
+ const accent = this.buttonColorCSS || '#4A90D9';
485
+ return `
486
+ .clou-slider { margin: 12px 0; }
487
+ .clou-slider label { display: block; font-size: 14px; margin-bottom: 6px; }
488
+ .clou-slider input[type="range"] { width: 100%; max-width: 400px; accent-color: ${accent}; }
489
+ `;
490
+ }
491
+
364
492
  buildAnimationCSS() {
365
493
  let css = '';
366
494
 
@@ -575,6 +703,139 @@ ${scripts}
575
703
  ` </div>`;
576
704
  }
577
705
 
706
+ case 'Form': {
707
+ const id = this.uid();
708
+ const content = this.compileChildren(node.children || []);
709
+ this.scripts.push(
710
+ `document.getElementById('${id}').addEventListener('submit', function(e) {\n` +
711
+ ` e.preventDefault();\n` +
712
+ ` alert('Form submitted!');\n` +
713
+ ` });`
714
+ );
715
+ const action = node.action ? ` action="${this.escapeHtml(node.action)}"` : '';
716
+ return ` <form id="${id}"${action} class="clou-form">\n${content}\n </form>`;
717
+ }
718
+
719
+ case 'Table': {
720
+ const rows = (node.children || []).filter(c => c.type === 'Row' || c.type === 'TableRow');
721
+ let html = ' <table class="clou-table">\n';
722
+ for (const row of node.children || []) {
723
+ if (row.type === 'Heading') {
724
+ // Table heading row
725
+ html += ' <thead><tr>';
726
+ const cells = row.value.split(',').map(c => c.trim());
727
+ for (const cell of cells) {
728
+ html += `<th>${this.escapeHtml(this.interpolate(cell))}</th>`;
729
+ }
730
+ html += '</tr></thead>\n';
731
+ } else if (row.children) {
732
+ html += ' <tr>';
733
+ for (const cell of row.children || []) {
734
+ if (cell.type === 'Text') {
735
+ html += `<td>${this.escapeHtml(this.interpolate(cell.value))}</td>`;
736
+ } else {
737
+ html += `<td>${this.compileNode(cell)}</td>`;
738
+ }
739
+ }
740
+ html += '</tr>\n';
741
+ }
742
+ }
743
+ html += ' </table>';
744
+ return html;
745
+ }
746
+
747
+ case 'Tabs': {
748
+ this.hasTabs = true;
749
+ const tabId = this.uid();
750
+ const tabs = (node.children || []).filter(c => c.type === 'Tab');
751
+ let buttonsHtml = ` <div class="clou-tabs" id="${tabId}">\n <div class="clou-tab-buttons">`;
752
+ let panelsHtml = '';
753
+
754
+ tabs.forEach((tab, i) => {
755
+ const active = i === 0 ? ' active' : '';
756
+ const panelId = `${tabId}-panel-${i}`;
757
+ buttonsHtml += `\n <button class="clou-tab-btn${active}" data-panel="${panelId}" onclick="document.querySelectorAll('#${tabId} .clou-tab-btn').forEach(b=>b.classList.remove('active'));this.classList.add('active');document.querySelectorAll('#${tabId} .clou-tab-panel').forEach(p=>p.classList.remove('active'));document.getElementById('${panelId}').classList.add('active');">${this.escapeHtml(this.interpolate(tab.name))}</button>`;
758
+ const content = this.compileChildren(this.filterContent(tab.children || []));
759
+ panelsHtml += `\n <div id="${panelId}" class="clou-tab-panel${active}">\n${content}\n </div>`;
760
+ });
761
+
762
+ buttonsHtml += `\n </div>`;
763
+ return `${buttonsHtml}${panelsHtml}\n </div>`;
764
+ }
765
+
766
+ case 'Tab':
767
+ // Handled by Tabs parent
768
+ return '';
769
+
770
+ case 'Accordion': {
771
+ this.hasAccordion = true;
772
+ const panels = (node.children || []).filter(c => c.type === 'Panel');
773
+ let html = ' <div class="clou-accordion">';
774
+ for (const panel of panels) {
775
+ const panelId = this.uid();
776
+ const content = this.compileChildren(this.filterContent(panel.children || []));
777
+ html += `\n <div class="clou-panel" id="${panelId}">`;
778
+ html += `\n <div class="clou-panel-header" onclick="this.parentElement.classList.toggle('open')">${this.escapeHtml(this.interpolate(panel.name))}<span class="clou-panel-arrow">&#9660;</span></div>`;
779
+ html += `\n <div class="clou-panel-body">\n${content}\n </div>`;
780
+ html += `\n </div>`;
781
+ }
782
+ html += '\n </div>';
783
+ return html;
784
+ }
785
+
786
+ case 'Panel':
787
+ // Handled by Accordion parent
788
+ return '';
789
+
790
+ case 'Progress': {
791
+ this.hasProgress = true;
792
+ const pct = parseInt(node.value, 10) || 0;
793
+ const label = node.label ? `\n <div class="clou-progress-label">${this.escapeHtml(this.interpolate(node.label))}</div>` : '';
794
+ return ` <div class="clou-progress">${label}\n <div class="clou-progress-bar"><div class="clou-progress-fill" style="width: ${pct}%"></div></div>\n </div>`;
795
+ }
796
+
797
+ case 'Dropdown': {
798
+ const id = this.uid();
799
+ const options = (node.children || []).filter(c => c.type === 'Option');
800
+ let html = ` <div class="clou-dropdown">\n <label for="${id}">${this.escapeHtml(this.interpolate(node.label))}</label>\n <select id="${id}" class="clou-input">`;
801
+ for (const opt of options) {
802
+ html += `\n <option value="${this.escapeHtml(opt.value)}">${this.escapeHtml(this.interpolate(opt.value))}</option>`;
803
+ }
804
+ html += '\n </select>\n </div>';
805
+ return html;
806
+ }
807
+
808
+ case 'Option':
809
+ // Handled by Dropdown parent
810
+ return '';
811
+
812
+ case 'Textarea': {
813
+ const id = this.uid();
814
+ const style = this.extractInlineStyles(node.children || []);
815
+ const styleAttr = style ? ` style="${style}"` : '';
816
+ return ` <textarea id="${id}" placeholder="${this.escapeHtml(this.interpolate(node.placeholder))}" rows="4"${styleAttr}></textarea>`;
817
+ }
818
+
819
+ case 'Checkbox': {
820
+ const id = this.uid();
821
+ return ` <label class="clou-checkbox"><input type="checkbox" id="${id}"> ${this.escapeHtml(this.interpolate(node.label))}</label>`;
822
+ }
823
+
824
+ case 'Audio':
825
+ return ` <audio src="${this.escapeHtml(node.src)}" controls class="clou-audio"></audio>`;
826
+
827
+ case 'Code':
828
+ return ` <pre class="clou-code"><code>${this.escapeHtml(this.interpolate(node.value))}</code></pre>`;
829
+
830
+ case 'Slider': {
831
+ this.hasSlider = true;
832
+ const id = this.uid();
833
+ return ` <div class="clou-slider">\n <label for="${id}">${this.escapeHtml(this.interpolate(node.label))}</label>\n <input type="range" id="${id}" min="${node.min}" max="${node.max}">\n </div>`;
834
+ }
835
+
836
+ case 'Submit':
837
+ return ` <button type="submit">${this.escapeHtml(this.interpolate(node.label))}</button>`;
838
+
578
839
  case 'Icon':
579
840
  return ` <span class="clou-icon">${node.value}</span>`;
580
841
 
package/src/deploy.js ADDED
@@ -0,0 +1,232 @@
1
+ // Clou Language - Deploy Module
2
+ // Deploys Clou websites with a single command
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { execSync, exec } = require('child_process');
7
+ const { buildClou } = require('./index');
8
+
9
+ class ClouDeploy {
10
+ constructor(inputFile, options = {}) {
11
+ this.inputFile = inputFile;
12
+ this.options = options;
13
+ this.buildDir = path.join(path.dirname(path.resolve(inputFile)), '_clou_build');
14
+ }
15
+
16
+ // Build the site to a directory
17
+ build() {
18
+ console.log('');
19
+ console.log(' Building site...');
20
+
21
+ const source = fs.readFileSync(this.inputFile, 'utf-8');
22
+ const basePath = path.dirname(path.resolve(this.inputFile));
23
+ const result = buildClou(source, { basePath });
24
+
25
+ // Create build directory
26
+ if (fs.existsSync(this.buildDir)) {
27
+ this.rmDir(this.buildDir);
28
+ }
29
+ fs.mkdirSync(this.buildDir, { recursive: true });
30
+
31
+ if (result.pages) {
32
+ // Multi-page site
33
+ for (const [filename, pageData] of Object.entries(result.pages)) {
34
+ const outPath = path.join(this.buildDir, filename);
35
+ fs.writeFileSync(outPath, pageData.html, 'utf-8');
36
+ console.log(` ${filename}`);
37
+ }
38
+ } else {
39
+ // Single page
40
+ fs.writeFileSync(path.join(this.buildDir, 'index.html'), result.html, 'utf-8');
41
+ console.log(' index.html');
42
+ }
43
+
44
+ console.log(' Build complete!');
45
+ return this.buildDir;
46
+ }
47
+
48
+ // Remove directory recursively
49
+ rmDir(dir) {
50
+ if (fs.existsSync(dir)) {
51
+ fs.readdirSync(dir).forEach(file => {
52
+ const curPath = path.join(dir, file);
53
+ if (fs.lstatSync(curPath).isDirectory()) {
54
+ this.rmDir(curPath);
55
+ } else {
56
+ fs.unlinkSync(curPath);
57
+ }
58
+ });
59
+ fs.rmdirSync(dir);
60
+ }
61
+ }
62
+
63
+ // Check if a command exists
64
+ commandExists(cmd) {
65
+ try {
66
+ if (process.platform === 'win32') {
67
+ execSync(`where ${cmd}`, { stdio: 'ignore' });
68
+ } else {
69
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
70
+ }
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ // Deploy with Surge
78
+ deploySurge(domain) {
79
+ console.log('');
80
+ console.log(' Deploying with Surge...');
81
+ console.log('');
82
+
83
+ try {
84
+ const cmd = domain
85
+ ? `surge "${this.buildDir}" ${domain}`
86
+ : `surge "${this.buildDir}"`;
87
+ execSync(cmd, { stdio: 'inherit' });
88
+ console.log('');
89
+ console.log(' Deploy complete!');
90
+ } catch (err) {
91
+ console.error(' Deploy failed. Make sure you have surge installed:');
92
+ console.error(' npm install -g surge');
93
+ }
94
+ }
95
+
96
+ // Deploy with GitHub Pages (using gh-pages or gh CLI)
97
+ deployGithub() {
98
+ console.log('');
99
+ console.log(' Deploying to GitHub Pages...');
100
+
101
+ try {
102
+ // Check if we're in a git repo
103
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
104
+ } catch {
105
+ console.error(' Error: Not a git repository. Initialize one with:');
106
+ console.error(' git init && git remote add origin <your-repo-url>');
107
+ return;
108
+ }
109
+
110
+ try {
111
+ // Use gh-pages approach: create orphan branch and push
112
+ const buildDir = this.buildDir;
113
+
114
+ execSync(`git add "${buildDir}"`, { stdio: 'ignore' });
115
+ execSync('git stash', { stdio: 'ignore' });
116
+
117
+ try {
118
+ execSync('git branch -D gh-pages', { stdio: 'ignore' });
119
+ } catch { /* branch may not exist */ }
120
+
121
+ execSync('git checkout --orphan gh-pages', { stdio: 'ignore' });
122
+ execSync('git rm -rf .', { stdio: 'ignore' });
123
+
124
+ // Copy build files to root
125
+ const files = fs.readdirSync(buildDir);
126
+ for (const file of files) {
127
+ fs.copyFileSync(path.join(buildDir, file), file);
128
+ }
129
+
130
+ execSync('git add -A', { stdio: 'ignore' });
131
+ execSync('git commit -m "Deploy Clou site"', { stdio: 'ignore' });
132
+ execSync('git push origin gh-pages --force', { stdio: 'inherit' });
133
+
134
+ // Go back to previous branch
135
+ execSync('git checkout -', { stdio: 'ignore' });
136
+ execSync('git stash pop', { stdio: 'ignore' });
137
+
138
+ console.log('');
139
+ console.log(' Deployed to GitHub Pages!');
140
+ console.log(' Enable Pages in your repo settings (branch: gh-pages)');
141
+ } catch (err) {
142
+ console.error(' Deploy failed: ' + err.message);
143
+ // Try to recover
144
+ try { execSync('git checkout -', { stdio: 'ignore' }); } catch {}
145
+ try { execSync('git stash pop', { stdio: 'ignore' }); } catch {}
146
+ }
147
+ }
148
+
149
+ // Deploy with Netlify CLI
150
+ deployNetlify() {
151
+ console.log('');
152
+ console.log(' Deploying with Netlify...');
153
+ console.log('');
154
+
155
+ try {
156
+ execSync(`netlify deploy --prod --dir="${this.buildDir}"`, { stdio: 'inherit' });
157
+ console.log('');
158
+ console.log(' Deploy complete!');
159
+ } catch (err) {
160
+ console.error(' Deploy failed. Make sure you have netlify-cli installed:');
161
+ console.error(' npm install -g netlify-cli');
162
+ }
163
+ }
164
+
165
+ // Main deploy function
166
+ async deploy() {
167
+ // Step 1: Build
168
+ this.build();
169
+
170
+ const target = this.options.target || 'auto';
171
+ const domain = this.options.domain || null;
172
+
173
+ // Step 2: Deploy based on target
174
+ if (target === 'surge' || target === 'auto') {
175
+ if (this.commandExists('surge')) {
176
+ this.deploySurge(domain);
177
+ return;
178
+ }
179
+ if (target === 'surge') {
180
+ console.log('');
181
+ console.log(' Surge not found. Install it with:');
182
+ console.log(' npm install -g surge');
183
+ console.log('');
184
+ console.log(' Then run: clou deploy ' + this.inputFile);
185
+ return;
186
+ }
187
+ }
188
+
189
+ if (target === 'github') {
190
+ this.deployGithub();
191
+ return;
192
+ }
193
+
194
+ if (target === 'netlify') {
195
+ if (this.commandExists('netlify')) {
196
+ this.deployNetlify();
197
+ return;
198
+ }
199
+ console.log('');
200
+ console.log(' Netlify CLI not found. Install it with:');
201
+ console.log(' npm install -g netlify-cli');
202
+ return;
203
+ }
204
+
205
+ // Auto mode: no surge found, give options
206
+ console.log('');
207
+ console.log(' ╔═══════════════════════════════════════╗');
208
+ console.log(' ║ Ready to deploy! ║');
209
+ console.log(' ╚═══════════════════════════════════════╝');
210
+ console.log('');
211
+ console.log(' Your site is built in: _clou_build/');
212
+ console.log('');
213
+ console.log(' To deploy, install one of these (pick one):');
214
+ console.log('');
215
+ console.log(' 1. Surge (easiest - recommended):');
216
+ console.log(' npm install -g surge');
217
+ console.log(' clou deploy ' + this.inputFile);
218
+ console.log('');
219
+ console.log(' 2. Netlify:');
220
+ console.log(' npm install -g netlify-cli');
221
+ console.log(' clou deploy ' + this.inputFile + ' --netlify');
222
+ console.log('');
223
+ console.log(' 3. GitHub Pages:');
224
+ console.log(' clou deploy ' + this.inputFile + ' --github');
225
+ console.log('');
226
+ console.log(' 4. Manual: Upload the _clou_build/ folder');
227
+ console.log(' to any web host!');
228
+ console.log('');
229
+ }
230
+ }
231
+
232
+ module.exports = { ClouDeploy };
package/src/errors.js ADDED
@@ -0,0 +1,182 @@
1
+ // Clou Language - Error Formatting
2
+ // Kid-friendly error messages with line numbers and suggestions
3
+
4
+ class ClouError extends Error {
5
+ constructor(message, options = {}) {
6
+ super(message);
7
+ this.line = options.line || null;
8
+ this.col = options.col || null;
9
+ this.source = options.source || null;
10
+ this.tip = options.tip || null;
11
+ this.phase = options.phase || 'build';
12
+ }
13
+ }
14
+
15
+ // Format a nice error message with source context
16
+ function formatError(error, source, filename) {
17
+ const lines = (source || '').split('\n');
18
+ let output = '\n';
19
+
20
+ // Header
21
+ const file = filename ? ` in ${filename}` : '';
22
+ output += ` Clou Error${file}\n`;
23
+ output += ' ' + '-'.repeat(38) + '\n\n';
24
+
25
+ // Get line number from error
26
+ let lineNum = null;
27
+ let colNum = null;
28
+
29
+ if (error.line) {
30
+ lineNum = error.line;
31
+ colNum = error.col || 1;
32
+ } else if (error.token && error.token.line) {
33
+ lineNum = error.token.line;
34
+ colNum = error.token.col || 1;
35
+ } else {
36
+ // Try to extract line number from error message
37
+ const lineMatch = error.message.match(/[Ll]ine\s+(\d+)/);
38
+ if (lineMatch) lineNum = parseInt(lineMatch[1], 10);
39
+ const colMatch = error.message.match(/[Cc]ol\s+(\d+)/);
40
+ if (colMatch) colNum = parseInt(colMatch[1], 10);
41
+ }
42
+
43
+ // Show source context
44
+ if (lineNum && lines.length > 0 && lineNum <= lines.length) {
45
+ const start = Math.max(0, lineNum - 3);
46
+ const end = Math.min(lines.length, lineNum + 2);
47
+
48
+ for (let i = start; i < end; i++) {
49
+ const num = String(i + 1).padStart(4);
50
+ const marker = (i + 1 === lineNum) ? ' > ' : ' ';
51
+ output += `${marker}${num} | ${lines[i]}\n`;
52
+
53
+ // Show pointer to error column
54
+ if (i + 1 === lineNum && colNum) {
55
+ const pointer = ' '.repeat(colNum - 1) + '^';
56
+ output += ` | ${pointer}\n`;
57
+ }
58
+ }
59
+ output += '\n';
60
+ }
61
+
62
+ // Problem description (clean up technical jargon)
63
+ const friendlyMsg = makeFriendly(error.message);
64
+ output += ` Problem: ${friendlyMsg}\n`;
65
+
66
+ // Tip
67
+ const tip = error.tip || suggestFix(error.message, lineNum ? lines[lineNum - 1] : '');
68
+ if (tip) {
69
+ output += `\n Tip: ${tip}\n`;
70
+ }
71
+
72
+ output += '\n';
73
+ return output;
74
+ }
75
+
76
+ // Make error messages less technical
77
+ function makeFriendly(msg) {
78
+ // Remove "Line X, Col Y:" prefix since we show it visually
79
+ msg = msg.replace(/Line \d+, Col \d+:\s*/g, '');
80
+ msg = msg.replace(/at line \d+, col \d+/g, '');
81
+
82
+ // Replace technical parser messages
83
+ msg = msg.replace(/Expected STRING but got (\w+)/g, (_, got) => {
84
+ const names = tokenName(got);
85
+ return `Expected text in quotes (like "hello") but found ${names}`;
86
+ });
87
+ msg = msg.replace(/Expected (\w+) but got (\w+)/g, (_, exp, got) => {
88
+ return `Expected ${tokenName(exp)} but found ${tokenName(got)}`;
89
+ });
90
+ msg = msg.replace(/Expected (\w+) but got end of file/g, (_, exp) => {
91
+ return `File ended too early. Expected ${tokenName(exp)}`;
92
+ });
93
+ msg = msg.replace(/Unexpected end of file/g, 'File ended unexpectedly. Something might be missing.');
94
+ msg = msg.replace(/Unterminated string/g, 'String is not closed. Add a closing quote "');
95
+
96
+ return msg.trim();
97
+ }
98
+
99
+ // Map token types to friendly names
100
+ function tokenName(type) {
101
+ const names = {
102
+ 'STRING': 'text in quotes ("...")',
103
+ 'NUMBER': 'a number',
104
+ 'COLON': 'a colon (:)',
105
+ 'INDENT': 'indented content',
106
+ 'DEDENT': 'end of block',
107
+ 'NEWLINE': 'a new line',
108
+ 'EOF': 'end of file',
109
+ 'IDENTIFIER': 'a name',
110
+ 'TO': '"to"',
111
+ 'AT': '"at"',
112
+ 'LPAREN': 'an opening parenthesis (',
113
+ 'RPAREN': 'a closing parenthesis )',
114
+ 'COMMA': 'a comma',
115
+ 'PAGE': '"page"',
116
+ 'HEADING': '"heading"',
117
+ 'TEXT': '"text"',
118
+ 'BUTTON': '"button"',
119
+ 'IMAGE': '"image"',
120
+ };
121
+ return names[type] || `"${type.toLowerCase()}"`;
122
+ }
123
+
124
+ // Suggest fixes based on common mistakes
125
+ function suggestFix(msg, line) {
126
+ if (!line) line = '';
127
+ const trimmed = line.trim();
128
+
129
+ // Missing colon
130
+ if (msg.includes('Expected COLON') || msg.includes('Expected a colon')) {
131
+ return 'Add a colon : at the end of the line. Example: page "My Site":';
132
+ }
133
+
134
+ // Unterminated string
135
+ if (msg.includes('Unterminated') || msg.includes('not closed')) {
136
+ return 'Every string needs opening and closing quotes: "Hello World"';
137
+ }
138
+
139
+ // Missing string after keyword
140
+ if (msg.includes('Expected text in quotes') || msg.includes('Expected STRING')) {
141
+ if (trimmed.startsWith('page')) return 'page needs a title: page "My Website":';
142
+ if (trimmed.startsWith('heading')) return 'heading needs text: heading "Hello World"';
143
+ if (trimmed.startsWith('text')) return 'text needs content: text "Some text here"';
144
+ if (trimmed.startsWith('button')) return 'button needs a label: button "Click me":';
145
+ if (trimmed.startsWith('image')) return 'image needs a source: image "photo.jpg"';
146
+ if (trimmed.startsWith('link')) return 'link needs text and url: link "Click" to "page.html"';
147
+ if (trimmed.startsWith('input')) return 'input needs a placeholder: input "Enter name"';
148
+ if (trimmed.startsWith('modal')) return 'modal needs a name: modal "popup":';
149
+ if (trimmed.startsWith('theme')) return 'theme needs a name: theme "neon"';
150
+ if (trimmed.startsWith('tab')) return 'tab needs a name: tab "First":';
151
+ if (trimmed.startsWith('panel')) return 'panel needs a title: panel "Question":';
152
+ if (trimmed.startsWith('dropdown')) return 'dropdown needs a label: dropdown "Choose":';
153
+ if (trimmed.startsWith('option')) return 'option needs a value: option "Choice A"';
154
+ if (trimmed.startsWith('textarea')) return 'textarea needs a placeholder: textarea "Write here"';
155
+ if (trimmed.startsWith('checkbox')) return 'checkbox needs a label: checkbox "I agree"';
156
+ return 'Add text in quotes after the keyword: keyword "text here"';
157
+ }
158
+
159
+ // Missing TO in link
160
+ if (msg.includes('Expected "to"') || msg.includes('Expected TO')) {
161
+ return 'link needs "to" keyword: link "Click" to "https://example.com"';
162
+ }
163
+
164
+ // Indentation issues
165
+ if (msg.includes('INDENT') || msg.includes('indented')) {
166
+ return 'Content inside a block needs to be indented with spaces (4 spaces recommended).';
167
+ }
168
+
169
+ // Import errors
170
+ if (msg.includes('Import error') || msg.includes('Could not read')) {
171
+ return 'Check the file path. It should be relative to your .clou file.';
172
+ }
173
+
174
+ // File ended early
175
+ if (msg.includes('ended too early') || msg.includes('ended unexpectedly')) {
176
+ return 'Your code seems incomplete. Did you forget to close a block or add content?';
177
+ }
178
+
179
+ return null;
180
+ }
181
+
182
+ module.exports = { ClouError, formatError, makeFriendly, suggestFix };
package/src/index.js CHANGED
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const { tokenize, LexerError } = require('./lexer');
7
7
  const { parse, ParseError } = require('./parser');
8
8
  const { compile } = require('./compiler');
9
+ const { ClouError, formatError } = require('./errors');
9
10
 
10
11
  // Resolve imports: reads imported files and prepends their source
11
12
  function resolveImports(source, basePath) {
@@ -28,7 +29,10 @@ function resolveImports(source, basePath) {
28
29
  // Replace the import statement with the file contents
29
30
  resolved = resolved.replace(match[0], importedSource);
30
31
  } catch (err) {
31
- throw new Error(`Import error: Could not read "${importFile}" (${fullPath})`);
32
+ throw new ClouError(`Import error: Could not read "${importFile}"`, {
33
+ source,
34
+ tip: `Check that the file "${importFile}" exists at: ${fullPath}`
35
+ });
32
36
  }
33
37
  }
34
38
 
@@ -37,25 +41,37 @@ function resolveImports(source, basePath) {
37
41
 
38
42
  function buildClou(source, options = {}) {
39
43
  const basePath = options.basePath || process.cwd();
44
+ const filename = options.filename || null;
40
45
 
41
- // Step 0: Resolve imports
42
- const resolvedSource = resolveImports(source, basePath);
46
+ try {
47
+ // Step 0: Resolve imports
48
+ const resolvedSource = resolveImports(source, basePath);
43
49
 
44
- // Step 1: Tokenize
45
- const tokens = tokenize(resolvedSource);
50
+ // Step 1: Tokenize
51
+ const tokens = tokenize(resolvedSource);
46
52
 
47
- // Step 2: Parse into AST
48
- const ast = parse(tokens);
53
+ // Step 2: Parse into AST
54
+ const ast = parse(tokens);
49
55
 
50
- // Step 3: Check for multi-page
51
- if (ast.pages.length > 1 && ast.pages.some(p => p.route)) {
52
- return buildMultiPage(ast, resolvedSource);
53
- }
54
-
55
- // Step 4: Compile to HTML (single page)
56
- const html = compile(ast);
56
+ // Step 3: Check for multi-page
57
+ if (ast.pages.length > 1 && ast.pages.some(p => p.route)) {
58
+ return buildMultiPage(ast, resolvedSource);
59
+ }
57
60
 
58
- return { tokens, ast, html, pages: null };
61
+ // Step 4: Compile to HTML (single page)
62
+ const html = compile(ast);
63
+
64
+ return { tokens, ast, html, pages: null };
65
+ } catch (err) {
66
+ // Wrap with formatted error if running in CLI mode
67
+ if (options.formatErrors) {
68
+ const formatted = formatError(err, source, filename);
69
+ const wrapped = new Error(formatted);
70
+ wrapped.formatted = true;
71
+ throw wrapped;
72
+ }
73
+ throw err;
74
+ }
59
75
  }
60
76
 
61
77
  // Build multiple pages from a multi-page AST
@@ -84,4 +100,4 @@ function buildMultiPage(ast) {
84
100
  return { pages, html: pages['index.html'] ? pages['index.html'].html : Object.values(pages)[0].html };
85
101
  }
86
102
 
87
- module.exports = { buildClou, resolveImports, tokenize, parse, compile, LexerError, ParseError };
103
+ module.exports = { buildClou, resolveImports, tokenize, parse, compile, LexerError, ParseError, ClouError, formatError };
package/src/lexer.js CHANGED
@@ -38,6 +38,21 @@ const TokenType = {
38
38
  SECTION: 'SECTION',
39
39
  SPACE: 'SPACE',
40
40
  COLUMNS: 'COLUMNS',
41
+ FORM: 'FORM',
42
+ TABLE: 'TABLE',
43
+ TABS: 'TABS',
44
+ TAB: 'TAB',
45
+ ACCORDION: 'ACCORDION',
46
+ PANEL: 'PANEL',
47
+ PROGRESS: 'PROGRESS',
48
+ DROPDOWN: 'DROPDOWN',
49
+ OPTION: 'OPTION',
50
+ TEXTAREA: 'TEXTAREA',
51
+ CHECKBOX: 'CHECKBOX',
52
+ AUDIO: 'AUDIO',
53
+ CODE: 'CODE',
54
+ SLIDER: 'SLIDER',
55
+ SUBMIT: 'SUBMIT',
41
56
 
42
57
  // Keywords - Actions
43
58
  SHOW: 'SHOW',
@@ -165,6 +180,21 @@ const KEYWORDS = {
165
180
  'section': TokenType.SECTION,
166
181
  'space': TokenType.SPACE,
167
182
  'columns': TokenType.COLUMNS,
183
+ 'form': TokenType.FORM,
184
+ 'table': TokenType.TABLE,
185
+ 'tabs': TokenType.TABS,
186
+ 'tab': TokenType.TAB,
187
+ 'accordion': TokenType.ACCORDION,
188
+ 'panel': TokenType.PANEL,
189
+ 'progress': TokenType.PROGRESS,
190
+ 'dropdown': TokenType.DROPDOWN,
191
+ 'option': TokenType.OPTION,
192
+ 'textarea': TokenType.TEXTAREA,
193
+ 'checkbox': TokenType.CHECKBOX,
194
+ 'audio': TokenType.AUDIO,
195
+ 'code': TokenType.CODE,
196
+ 'slider': TokenType.SLIDER,
197
+ 'submit': TokenType.SUBMIT,
168
198
  'show': TokenType.SHOW,
169
199
  'message': TokenType.MESSAGE,
170
200
  'hide': TokenType.HIDE,
package/src/parser.js CHANGED
@@ -157,6 +157,21 @@ class Parser {
157
157
  case TokenType.GRID: return this.parseGrid();
158
158
  case TokenType.SECTION: return this.parseSection();
159
159
  case TokenType.SPACE: return this.parseSpace();
160
+ case TokenType.FORM: return this.parseForm();
161
+ case TokenType.TABLE: return this.parseTable();
162
+ case TokenType.TABS: return this.parseTabs();
163
+ case TokenType.TAB: return this.parseTab();
164
+ case TokenType.ACCORDION: return this.parseAccordion();
165
+ case TokenType.PANEL: return this.parsePanel();
166
+ case TokenType.PROGRESS: return this.parseProgress();
167
+ case TokenType.DROPDOWN: return this.parseDropdown();
168
+ case TokenType.OPTION: return this.parseOption();
169
+ case TokenType.TEXTAREA: return this.parseTextarea();
170
+ case TokenType.CHECKBOX: return this.parseCheckbox();
171
+ case TokenType.AUDIO: return this.parseAudio();
172
+ case TokenType.CODE: return this.parseCode();
173
+ case TokenType.SLIDER: return this.parseSlider();
174
+ case TokenType.SUBMIT: return this.parseSubmit();
160
175
  case TokenType.SET: return this.parseSetVariable();
161
176
  case TokenType.USE: return this.parseUse();
162
177
  case TokenType.REPEAT: return this.parseRepeat();
@@ -496,6 +511,185 @@ class Parser {
496
511
  return { type: 'Space', value };
497
512
  }
498
513
 
514
+ // form:
515
+ parseForm() {
516
+ this.expect(TokenType.FORM);
517
+ const node = { type: 'Form', action: null, children: [] };
518
+
519
+ if (this.peek() && this.peek().type === TokenType.STRING) {
520
+ node.action = this.advance().value;
521
+ }
522
+
523
+ if (this.peek() && this.peek().type === TokenType.COLON) {
524
+ this.advance();
525
+ this.skipNewlines();
526
+ node.children = this.parseBlock();
527
+ }
528
+
529
+ return node;
530
+ }
531
+
532
+ // table:
533
+ parseTable() {
534
+ this.expect(TokenType.TABLE);
535
+ const node = { type: 'Table', children: [] };
536
+
537
+ if (this.peek() && this.peek().type === TokenType.COLON) {
538
+ this.advance();
539
+ this.skipNewlines();
540
+ node.children = this.parseBlock();
541
+ }
542
+
543
+ return node;
544
+ }
545
+
546
+ // tabs:
547
+ parseTabs() {
548
+ this.expect(TokenType.TABS);
549
+ const node = { type: 'Tabs', children: [] };
550
+
551
+ if (this.peek() && this.peek().type === TokenType.COLON) {
552
+ this.advance();
553
+ this.skipNewlines();
554
+ node.children = this.parseBlock();
555
+ }
556
+
557
+ return node;
558
+ }
559
+
560
+ // tab "name":
561
+ parseTab() {
562
+ this.expect(TokenType.TAB);
563
+ const name = this.expect(TokenType.STRING).value;
564
+ const node = { type: 'Tab', name, children: [] };
565
+
566
+ if (this.peek() && this.peek().type === TokenType.COLON) {
567
+ this.advance();
568
+ this.skipNewlines();
569
+ node.children = this.parseBlock();
570
+ }
571
+
572
+ return node;
573
+ }
574
+
575
+ // accordion:
576
+ parseAccordion() {
577
+ this.expect(TokenType.ACCORDION);
578
+ const node = { type: 'Accordion', children: [] };
579
+
580
+ if (this.peek() && this.peek().type === TokenType.COLON) {
581
+ this.advance();
582
+ this.skipNewlines();
583
+ node.children = this.parseBlock();
584
+ }
585
+
586
+ return node;
587
+ }
588
+
589
+ // panel "title":
590
+ parsePanel() {
591
+ this.expect(TokenType.PANEL);
592
+ const name = this.expect(TokenType.STRING).value;
593
+ const node = { type: 'Panel', name, children: [] };
594
+
595
+ if (this.peek() && this.peek().type === TokenType.COLON) {
596
+ this.advance();
597
+ this.skipNewlines();
598
+ node.children = this.parseBlock();
599
+ }
600
+
601
+ return node;
602
+ }
603
+
604
+ // progress 75 OR progress 75 "Label"
605
+ parseProgress() {
606
+ this.expect(TokenType.PROGRESS);
607
+ const value = this.expect(TokenType.NUMBER).value;
608
+ let label = null;
609
+ if (this.peek() && this.peek().type === TokenType.STRING) {
610
+ label = this.advance().value;
611
+ }
612
+ return { type: 'Progress', value, label };
613
+ }
614
+
615
+ // dropdown "label":
616
+ parseDropdown() {
617
+ this.expect(TokenType.DROPDOWN);
618
+ const label = this.expect(TokenType.STRING).value;
619
+ const node = { type: 'Dropdown', label, children: [] };
620
+
621
+ if (this.peek() && this.peek().type === TokenType.COLON) {
622
+ this.advance();
623
+ this.skipNewlines();
624
+ node.children = this.parseBlock();
625
+ }
626
+
627
+ return node;
628
+ }
629
+
630
+ // option "value"
631
+ parseOption() {
632
+ this.expect(TokenType.OPTION);
633
+ const value = this.expect(TokenType.STRING).value;
634
+ return { type: 'Option', value };
635
+ }
636
+
637
+ // textarea "placeholder"
638
+ parseTextarea() {
639
+ this.expect(TokenType.TEXTAREA);
640
+ const placeholder = this.expect(TokenType.STRING).value;
641
+ const node = { type: 'Textarea', placeholder, children: [] };
642
+
643
+ if (this.peek() && this.peek().type === TokenType.COLON) {
644
+ this.advance();
645
+ this.skipNewlines();
646
+ node.children = this.parseBlock();
647
+ }
648
+
649
+ return node;
650
+ }
651
+
652
+ // checkbox "label"
653
+ parseCheckbox() {
654
+ this.expect(TokenType.CHECKBOX);
655
+ const label = this.expect(TokenType.STRING).value;
656
+ return { type: 'Checkbox', label };
657
+ }
658
+
659
+ // audio "src"
660
+ parseAudio() {
661
+ this.expect(TokenType.AUDIO);
662
+ const src = this.expect(TokenType.STRING).value;
663
+ return { type: 'Audio', src };
664
+ }
665
+
666
+ // code "content"
667
+ parseCode() {
668
+ this.expect(TokenType.CODE);
669
+ const value = this.expect(TokenType.STRING).value;
670
+ return { type: 'Code', value };
671
+ }
672
+
673
+ // slider "label" 0 to 100
674
+ parseSlider() {
675
+ this.expect(TokenType.SLIDER);
676
+ const label = this.expect(TokenType.STRING).value;
677
+ let min = '0', max = '100';
678
+ if (this.peek() && this.peek().type === TokenType.NUMBER) {
679
+ min = this.advance().value;
680
+ this.expect(TokenType.TO);
681
+ max = this.expect(TokenType.NUMBER).value;
682
+ }
683
+ return { type: 'Slider', label, min, max };
684
+ }
685
+
686
+ // submit "text"
687
+ parseSubmit() {
688
+ this.expect(TokenType.SUBMIT);
689
+ const label = this.expect(TokenType.STRING).value;
690
+ return { type: 'Submit', label };
691
+ }
692
+
499
693
  // button "text":
500
694
  parseButton() {
501
695
  this.expect(TokenType.BUTTON);