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 +4 -1
- package/bin/clou.js +57 -6
- package/package.json +1 -1
- package/src/compiler.js +262 -1
- package/src/deploy.js +232 -0
- package/src/errors.js +182 -0
- package/src/index.js +32 -16
- package/src/lexer.js +30 -0
- package/src/parser.js +194 -0
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
|
-
- **
|
|
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.
|
|
21
|
-
║
|
|
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(
|
|
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
|
-
|
|
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
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">▼</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
|
|
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
|
-
|
|
42
|
-
|
|
46
|
+
try {
|
|
47
|
+
// Step 0: Resolve imports
|
|
48
|
+
const resolvedSource = resolveImports(source, basePath);
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
// Step 1: Tokenize
|
|
51
|
+
const tokens = tokenize(resolvedSource);
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
// Step 2: Parse into AST
|
|
54
|
+
const ast = parse(tokens);
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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);
|