structscript 1.3.0 → 1.4.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/bin/structscript.js +2 -2
- package/lib/editor.html +3276 -0
- package/lib/interpreter.js +371 -21
- package/lib/main.js +261 -0
- package/lib/preload.js +21 -0
- package/package.json +7 -5
package/lib/editor.html
ADDED
|
@@ -0,0 +1,3276 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>StructScript Editor</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ============================================================
|
|
9
|
+
THEME VARIABLES — StructScript Brand v1.2
|
|
10
|
+
============================================================ */
|
|
11
|
+
:root {
|
|
12
|
+
/* Dark theme (default) */
|
|
13
|
+
--bg: #0a1614;
|
|
14
|
+
--bg2: #0d1f1d;
|
|
15
|
+
--bg3: #112420;
|
|
16
|
+
--surface: #162b28;
|
|
17
|
+
--surface2: #1e3530;
|
|
18
|
+
--border: #234440;
|
|
19
|
+
--border2: #2e5550;
|
|
20
|
+
--ink: #e0f0ee;
|
|
21
|
+
--ink2: #8ab8b4;
|
|
22
|
+
--muted: #4a7470;
|
|
23
|
+
--accent: #0b7a75;
|
|
24
|
+
--accent-lt: #12a89e;
|
|
25
|
+
--accent-dim: rgba(11,122,117,0.18);
|
|
26
|
+
--lime: #b8f000;
|
|
27
|
+
--lime-dim: rgba(184,240,0,0.12);
|
|
28
|
+
--accent2: #b8f000;
|
|
29
|
+
--accent3: #5b9cf6;
|
|
30
|
+
--err: #f07070;
|
|
31
|
+
--warn: #f0c060;
|
|
32
|
+
--kw-color: #b8f000;
|
|
33
|
+
--str-color: #7ed8c0;
|
|
34
|
+
--num-color: #89b4f8;
|
|
35
|
+
--cmt-color: #3a6460;
|
|
36
|
+
--fn-color: #c4a5f8;
|
|
37
|
+
--op-color: #5bc8c4;
|
|
38
|
+
--bool-color: #f07070;
|
|
39
|
+
--line-h: 22px;
|
|
40
|
+
--font-size: 13px;
|
|
41
|
+
--pad: 16px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[data-theme="light"] {
|
|
45
|
+
--bg: #f2f8f7;
|
|
46
|
+
--bg2: #e8f4f2;
|
|
47
|
+
--bg3: #dceeed;
|
|
48
|
+
--surface: #ffffff;
|
|
49
|
+
--surface2: #f2f8f7;
|
|
50
|
+
--border: #c4dedd;
|
|
51
|
+
--border2: #9ac8c6;
|
|
52
|
+
--ink: #0d1f1e;
|
|
53
|
+
--ink2: #2a5552;
|
|
54
|
+
--muted: #6a9896;
|
|
55
|
+
--accent: #0b7a75;
|
|
56
|
+
--accent-lt: #12a89e;
|
|
57
|
+
--accent-dim: rgba(11,122,117,0.1);
|
|
58
|
+
--lime: #8ab800;
|
|
59
|
+
--lime-dim: rgba(138,184,0,0.12);
|
|
60
|
+
--accent2: #8ab800;
|
|
61
|
+
--accent3: #1d4db0;
|
|
62
|
+
--err: #c0392b;
|
|
63
|
+
--warn: #b07820;
|
|
64
|
+
--kw-color: #8ab800;
|
|
65
|
+
--str-color: #0b7a75;
|
|
66
|
+
--num-color: #1d4db0;
|
|
67
|
+
--cmt-color: #9ac8c6;
|
|
68
|
+
--fn-color: #7c3aed;
|
|
69
|
+
--op-color: #0b7a75;
|
|
70
|
+
--bool-color: #c0392b;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
74
|
+
|
|
75
|
+
body {
|
|
76
|
+
background: var(--bg);
|
|
77
|
+
color: var(--ink);
|
|
78
|
+
font-family: 'Manrope', sans-serif;
|
|
79
|
+
height: 100vh;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
transition: background 0.3s, color 0.3s;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ============================================================
|
|
85
|
+
TOPBAR
|
|
86
|
+
============================================================ */
|
|
87
|
+
.topbar {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 8px;
|
|
91
|
+
height: 48px;
|
|
92
|
+
padding: 0 16px;
|
|
93
|
+
border-bottom: 1px solid var(--border);
|
|
94
|
+
background: var(--bg2);
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
position: relative;
|
|
97
|
+
z-index: 50;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.logo { display: flex; align-items: center; gap: 6px; text-decoration: none; }
|
|
101
|
+
.logo-s { font-size: 17px; font-weight: 800; color: var(--ink); letter-spacing: -0.5px; }
|
|
102
|
+
.logo-sc { font-size: 17px; font-weight: 800; color: var(--accent-lt); letter-spacing: -0.5px; }
|
|
103
|
+
.logo-v { font-size: 9px; font-family: 'DM Mono', monospace; color: var(--muted);
|
|
104
|
+
padding: 1px 5px; border: 1px solid var(--border2); border-radius: 3px; margin-left: 2px; }
|
|
105
|
+
|
|
106
|
+
.divider { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
|
|
107
|
+
|
|
108
|
+
.nav-tabs { display: flex; gap: 2px; }
|
|
109
|
+
.nav-tab {
|
|
110
|
+
padding: 5px 12px; border-radius: 5px; font-size: 12px; font-weight: 600;
|
|
111
|
+
cursor: pointer; border: none; background: none; color: var(--ink2);
|
|
112
|
+
transition: all 0.15s; font-family: 'Manrope', sans-serif;
|
|
113
|
+
}
|
|
114
|
+
.nav-tab:hover { background: var(--surface); color: var(--ink); }
|
|
115
|
+
.nav-tab.active { background: var(--accent-dim); color: var(--accent); }
|
|
116
|
+
|
|
117
|
+
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
|
118
|
+
|
|
119
|
+
.icon-btn {
|
|
120
|
+
width: 30px; height: 30px; border-radius: 6px; border: 1px solid var(--border);
|
|
121
|
+
background: var(--surface); color: var(--ink2); cursor: pointer;
|
|
122
|
+
display: flex; align-items: center; justify-content: center; font-size: 14px;
|
|
123
|
+
transition: all 0.15s;
|
|
124
|
+
}
|
|
125
|
+
.icon-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
126
|
+
|
|
127
|
+
.file-btn {
|
|
128
|
+
display: flex; align-items: center; gap: 6px;
|
|
129
|
+
padding: 5px 12px; border-radius: 6px; font-size: 12px; font-weight: 600;
|
|
130
|
+
cursor: pointer; border: 1px solid var(--border); background: var(--surface);
|
|
131
|
+
color: var(--ink2); transition: all 0.15s; font-family: 'Manrope', sans-serif;
|
|
132
|
+
}
|
|
133
|
+
.file-btn:hover { border-color: var(--border2); color: var(--ink); }
|
|
134
|
+
|
|
135
|
+
.run-btn {
|
|
136
|
+
display: flex; align-items: center; gap: 6px;
|
|
137
|
+
padding: 6px 16px; background: var(--lime); border: none; border-radius: 6px;
|
|
138
|
+
color: #0d1f1e; font-family: 'Manrope', sans-serif; font-size: 13px; font-weight: 800;
|
|
139
|
+
cursor: pointer; transition: all 0.15s; letter-spacing: 0.2px;
|
|
140
|
+
}
|
|
141
|
+
.run-btn:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 4px 14px rgba(184,240,0,0.3); }
|
|
142
|
+
.run-btn:active { transform: none; }
|
|
143
|
+
.run-kbd { font-size: 10px; opacity: 0.65; font-family: 'DM Mono', monospace; }
|
|
144
|
+
|
|
145
|
+
/* ============================================================
|
|
146
|
+
LAYOUT
|
|
147
|
+
============================================================ */
|
|
148
|
+
.app-body {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
height: calc(100vh - 48px);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.page { display: none; flex: 1; overflow: hidden; }
|
|
155
|
+
.page.active { display: flex; flex-direction: column; }
|
|
156
|
+
|
|
157
|
+
/* ============================================================
|
|
158
|
+
PLAYGROUND
|
|
159
|
+
============================================================ */
|
|
160
|
+
.playground {
|
|
161
|
+
display: grid;
|
|
162
|
+
grid-template-columns: 1fr 1fr;
|
|
163
|
+
flex: 1;
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
min-height: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Editor */
|
|
169
|
+
.editor-pane {
|
|
170
|
+
display: flex; flex-direction: column;
|
|
171
|
+
border-right: 1px solid var(--border);
|
|
172
|
+
background: var(--bg3);
|
|
173
|
+
min-height: 0;
|
|
174
|
+
position: relative;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.pane-bar {
|
|
178
|
+
display: flex; align-items: center; gap: 8px;
|
|
179
|
+
padding: 6px 12px;
|
|
180
|
+
border-bottom: 1px solid var(--border);
|
|
181
|
+
background: var(--bg2);
|
|
182
|
+
font-family: 'DM Mono', monospace; font-size: 10px; color: var(--muted);
|
|
183
|
+
flex-shrink: 0;
|
|
184
|
+
}
|
|
185
|
+
.pane-bar .filename { color: var(--ink2); font-size: 11px; }
|
|
186
|
+
.pane-bar .modified { color: var(--warn); font-size: 10px; }
|
|
187
|
+
.wc-dots { display: flex; gap: 4px; }
|
|
188
|
+
.wc-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
189
|
+
|
|
190
|
+
.editor-container {
|
|
191
|
+
display: flex;
|
|
192
|
+
flex: 1;
|
|
193
|
+
overflow: hidden;
|
|
194
|
+
position: relative;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Line numbers */
|
|
198
|
+
.line-numbers {
|
|
199
|
+
width: 48px;
|
|
200
|
+
flex-shrink: 0;
|
|
201
|
+
padding: 12px 0;
|
|
202
|
+
background: var(--bg2);
|
|
203
|
+
border-right: 1px solid var(--border);
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
user-select: none;
|
|
206
|
+
font-family: 'DM Mono', monospace;
|
|
207
|
+
font-size: var(--font-size);
|
|
208
|
+
line-height: var(--line-h);
|
|
209
|
+
color: var(--muted);
|
|
210
|
+
text-align: right;
|
|
211
|
+
}
|
|
212
|
+
.line-num {
|
|
213
|
+
padding-right: 10px;
|
|
214
|
+
display: block;
|
|
215
|
+
height: var(--line-h);
|
|
216
|
+
transition: color 0.1s;
|
|
217
|
+
}
|
|
218
|
+
.line-num.active { color: var(--accent); }
|
|
219
|
+
|
|
220
|
+
/* Code area */
|
|
221
|
+
.code-wrap {
|
|
222
|
+
flex: 1; position: relative; overflow: auto;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#code-editor {
|
|
226
|
+
position: absolute; inset: 0;
|
|
227
|
+
width: 100%; height: 100%;
|
|
228
|
+
padding: 12px var(--pad);
|
|
229
|
+
background: transparent;
|
|
230
|
+
border: none; outline: none;
|
|
231
|
+
color: transparent;
|
|
232
|
+
caret-color: var(--accent);
|
|
233
|
+
z-index: 2;
|
|
234
|
+
font-family: 'DM Mono', monospace;
|
|
235
|
+
font-size: var(--font-size);
|
|
236
|
+
line-height: var(--line-h);
|
|
237
|
+
resize: none; tab-size: 2;
|
|
238
|
+
z-index: 2;
|
|
239
|
+
white-space: pre;
|
|
240
|
+
overflow-wrap: normal;
|
|
241
|
+
overflow: auto;
|
|
242
|
+
spellcheck: false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#highlight-layer {
|
|
246
|
+
position: absolute; inset: 0;
|
|
247
|
+
padding: 12px var(--pad);
|
|
248
|
+
font-family: 'DM Mono', monospace;
|
|
249
|
+
font-size: var(--font-size);
|
|
250
|
+
line-height: var(--line-h);
|
|
251
|
+
pointer-events: none;
|
|
252
|
+
white-space: pre;
|
|
253
|
+
overflow-wrap: normal;
|
|
254
|
+
z-index: 1;
|
|
255
|
+
color: var(--ink);
|
|
256
|
+
overflow: hidden;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Autocomplete */
|
|
260
|
+
#autocomplete {
|
|
261
|
+
position: absolute;
|
|
262
|
+
background: var(--surface);
|
|
263
|
+
border: 1px solid var(--border2);
|
|
264
|
+
border-radius: 8px;
|
|
265
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
266
|
+
z-index: 100;
|
|
267
|
+
min-width: 200px;
|
|
268
|
+
max-height: 200px;
|
|
269
|
+
overflow-y: auto;
|
|
270
|
+
display: none;
|
|
271
|
+
font-family: 'DM Mono', monospace;
|
|
272
|
+
font-size: 12px;
|
|
273
|
+
}
|
|
274
|
+
.ac-item {
|
|
275
|
+
display: flex; align-items: center; gap: 8px;
|
|
276
|
+
padding: 7px 12px; cursor: pointer;
|
|
277
|
+
transition: background 0.1s;
|
|
278
|
+
}
|
|
279
|
+
.ac-item:hover, .ac-item.selected { background: var(--accent-dim); }
|
|
280
|
+
.ac-item .ac-icon { font-size: 11px; opacity: 0.7; width: 14px; }
|
|
281
|
+
.ac-item .ac-name { color: var(--ink); flex: 1; }
|
|
282
|
+
.ac-item .ac-type { font-size: 10px; color: var(--muted); }
|
|
283
|
+
|
|
284
|
+
/* Output */
|
|
285
|
+
.output-pane {
|
|
286
|
+
display: flex; flex-direction: column;
|
|
287
|
+
background: var(--bg);
|
|
288
|
+
min-height: 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.out-tabs {
|
|
292
|
+
display: flex; gap: 0;
|
|
293
|
+
border-bottom: 1px solid var(--border);
|
|
294
|
+
background: var(--bg2);
|
|
295
|
+
flex-shrink: 0;
|
|
296
|
+
}
|
|
297
|
+
.out-tab {
|
|
298
|
+
padding: 7px 16px; font-size: 11px; font-weight: 600;
|
|
299
|
+
cursor: pointer; border: none; background: none;
|
|
300
|
+
color: var(--muted); transition: all 0.15s;
|
|
301
|
+
font-family: 'Manrope', sans-serif; border-bottom: 2px solid transparent;
|
|
302
|
+
}
|
|
303
|
+
.out-tab:hover { color: var(--ink2); }
|
|
304
|
+
.out-tab.active { color: var(--ink); border-bottom-color: var(--accent); }
|
|
305
|
+
|
|
306
|
+
.out-panel { display: none; flex: 1; overflow-y: auto; min-height: 0; }
|
|
307
|
+
.out-panel.active { display: block; }
|
|
308
|
+
|
|
309
|
+
#output-display {
|
|
310
|
+
padding: 12px var(--pad);
|
|
311
|
+
font-family: 'DM Mono', monospace;
|
|
312
|
+
font-size: 12px; line-height: 1.9;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.out-line { display: flex; gap: 10px; }
|
|
316
|
+
.out-prefix { color: var(--muted); user-select: none; flex-shrink: 0; width: 20px; text-align: right; }
|
|
317
|
+
.out-text { color: var(--ink2); word-break: break-all; }
|
|
318
|
+
.out-line.err .out-text { color: var(--err); }
|
|
319
|
+
.out-line.warn .out-text { color: var(--warn); }
|
|
320
|
+
.out-line.info .out-text { color: var(--accent3); }
|
|
321
|
+
.out-line.success .out-text { color: var(--accent2); }
|
|
322
|
+
.out-line.sep { border-top: 1px solid var(--border); margin: 4px 0; padding-top: 4px; }
|
|
323
|
+
|
|
324
|
+
/* Error console */
|
|
325
|
+
#error-display {
|
|
326
|
+
padding: 12px var(--pad);
|
|
327
|
+
font-family: 'DM Mono', monospace;
|
|
328
|
+
font-size: 12px; line-height: 1.9;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.err-card {
|
|
332
|
+
background: rgba(240,112,112,0.06);
|
|
333
|
+
border: 1px solid rgba(240,112,112,0.25);
|
|
334
|
+
border-left: 3px solid var(--err);
|
|
335
|
+
border-radius: 6px; padding: 12px 14px; margin-bottom: 10px;
|
|
336
|
+
}
|
|
337
|
+
.err-title { color: var(--err); font-weight: 600; margin-bottom: 4px; }
|
|
338
|
+
.err-detail { color: var(--ink2); font-size: 11px; }
|
|
339
|
+
.err-line-ref { color: var(--muted); font-size: 11px; margin-top: 6px; }
|
|
340
|
+
.err-snippet {
|
|
341
|
+
background: var(--bg3); border-radius: 4px; padding: 6px 10px;
|
|
342
|
+
margin-top: 8px; color: var(--ink); font-size: 11px;
|
|
343
|
+
}
|
|
344
|
+
.err-arrow { color: var(--err); }
|
|
345
|
+
|
|
346
|
+
.no-errors {
|
|
347
|
+
padding: 40px; text-align: center;
|
|
348
|
+
color: var(--muted); font-size: 12px;
|
|
349
|
+
}
|
|
350
|
+
.no-errors-icon { font-size: 28px; margin-bottom: 8px; }
|
|
351
|
+
|
|
352
|
+
/* Examples bar */
|
|
353
|
+
.examples-bar {
|
|
354
|
+
display: flex; align-items: center; gap: 6px;
|
|
355
|
+
padding: 7px 12px;
|
|
356
|
+
border-top: 1px solid var(--border);
|
|
357
|
+
background: var(--bg2);
|
|
358
|
+
overflow-x: auto; flex-shrink: 0;
|
|
359
|
+
}
|
|
360
|
+
.ex-label { font-size: 10px; font-family: 'DM Mono', monospace;
|
|
361
|
+
color: var(--muted); white-space: nowrap; text-transform: uppercase; letter-spacing: 1px; }
|
|
362
|
+
.ex-chip {
|
|
363
|
+
padding: 4px 10px; border-radius: 5px; font-size: 11px;
|
|
364
|
+
font-family: 'DM Mono', monospace; border: 1px solid var(--border);
|
|
365
|
+
background: var(--surface); color: var(--ink2); cursor: pointer; white-space: nowrap;
|
|
366
|
+
transition: all 0.12s;
|
|
367
|
+
}
|
|
368
|
+
.ex-chip:hover { border-color: var(--accent); color: var(--accent); }
|
|
369
|
+
|
|
370
|
+
/* Status bar */
|
|
371
|
+
.statusbar {
|
|
372
|
+
display: flex; align-items: center; gap: 12px;
|
|
373
|
+
height: 24px; padding: 0 12px;
|
|
374
|
+
border-top: 1px solid var(--border);
|
|
375
|
+
background: var(--bg2);
|
|
376
|
+
font-family: 'DM Mono', monospace; font-size: 10px; color: var(--muted);
|
|
377
|
+
flex-shrink: 0;
|
|
378
|
+
}
|
|
379
|
+
.sb-item { display: flex; align-items: center; gap: 4px; }
|
|
380
|
+
.sb-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); }
|
|
381
|
+
.sb-dot.ok { background: var(--accent2); }
|
|
382
|
+
.sb-dot.err { background: var(--err); }
|
|
383
|
+
.sb-dot.run { background: var(--warn); animation: pulse 0.8s infinite; }
|
|
384
|
+
.sb-sep { width: 1px; height: 12px; background: var(--border); }
|
|
385
|
+
|
|
386
|
+
/* ============================================================
|
|
387
|
+
DOCS PAGE
|
|
388
|
+
============================================================ */
|
|
389
|
+
.docs-page { flex: 1; overflow-y: auto; padding: 48px 32px; max-width: 900px; margin: 0 auto; width: 100%; }
|
|
390
|
+
|
|
391
|
+
.docs-hero { margin-bottom: 48px; }
|
|
392
|
+
.docs-hero h2 { font-size: clamp(28px,5vw,44px); font-weight: 900; letter-spacing: -1.5px; line-height: 1.1; margin-bottom: 12px; }
|
|
393
|
+
.docs-hero h2 em { color: var(--accent); font-style: normal; }
|
|
394
|
+
.docs-hero p { font-size: 15px; color: var(--ink2); line-height: 1.7; max-width: 520px; }
|
|
395
|
+
|
|
396
|
+
.docs-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; margin-bottom: 48px; }
|
|
397
|
+
.docs-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 20px; }
|
|
398
|
+
.docs-card h4 { font-size: 14px; font-weight: 700; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }
|
|
399
|
+
.docs-card p { font-size: 12px; color: var(--ink2); line-height: 1.6; }
|
|
400
|
+
|
|
401
|
+
.docs-section { margin-bottom: 40px; }
|
|
402
|
+
.docs-section h3 { font-size: 18px; font-weight: 800; margin-bottom: 14px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
|
|
403
|
+
|
|
404
|
+
.docs-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 16px; }
|
|
405
|
+
.docs-table th { text-align: left; padding: 8px 12px; background: var(--surface); border: 1px solid var(--border);
|
|
406
|
+
font-family: 'DM Mono', monospace; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); }
|
|
407
|
+
.docs-table td { padding: 8px 12px; border: 1px solid var(--border); vertical-align: top; color: var(--ink2); }
|
|
408
|
+
.docs-table tr:hover td { background: var(--surface); }
|
|
409
|
+
code { font-family: 'DM Mono', monospace; font-size: 11px; color: var(--accent);
|
|
410
|
+
background: var(--accent-dim); padding: 1px 5px; border-radius: 3px; }
|
|
411
|
+
|
|
412
|
+
.docs-code {
|
|
413
|
+
background: var(--bg3); border: 1px solid var(--border); border-radius: 8px;
|
|
414
|
+
padding: 16px; font-family: 'DM Mono', monospace; font-size: 12px;
|
|
415
|
+
line-height: 1.8; overflow-x: auto; margin: 10px 0;
|
|
416
|
+
}
|
|
417
|
+
.kw { color: var(--kw-color); font-weight: 600; }
|
|
418
|
+
.str { color: var(--str-color); }
|
|
419
|
+
.cmt { color: var(--cmt-color); font-style: italic; }
|
|
420
|
+
.num { color: var(--num-color); }
|
|
421
|
+
.fn { color: var(--fn-color); }
|
|
422
|
+
.op { color: var(--op-color); }
|
|
423
|
+
|
|
424
|
+
/* ============================================================
|
|
425
|
+
ANIMATIONS
|
|
426
|
+
============================================================ */
|
|
427
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
428
|
+
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
|
|
429
|
+
.im-field{margin-bottom:14px}
|
|
430
|
+
.im-label{display:block;font-size:11px;font-family:'DM Mono',monospace;color:var(--ink2);margin-bottom:5px}
|
|
431
|
+
.im-prompt-text{color:var(--accent-lt)}
|
|
432
|
+
.im-idx{color:var(--muted);font-size:10px;margin-left:4px}
|
|
433
|
+
.im-input{width:100%;background:var(--bg3);border:1px solid var(--border2);border-radius:6px;padding:8px 12px;color:var(--ink);font-family:'DM Mono',monospace;font-size:12px;outline:none}
|
|
434
|
+
.im-input:focus{border-color:var(--accent)}
|
|
435
|
+
#input-modal-overlay.open{display:flex!important}
|
|
436
|
+
|
|
437
|
+
/* Web preview tab */
|
|
438
|
+
#out-web { padding: 0; overflow: hidden; display: none; flex-direction: column; }
|
|
439
|
+
#out-web.active { display: flex; }
|
|
440
|
+
#web-preview { flex: 1; width: 100%; border: none; background: #fff; }
|
|
441
|
+
.web-toolbar {
|
|
442
|
+
display: flex; align-items: center; gap: 8px;
|
|
443
|
+
padding: 6px 12px; background: var(--bg2);
|
|
444
|
+
border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
445
|
+
}
|
|
446
|
+
.web-toolbar-label { font-size: 10px; font-family: 'DM Mono',monospace; color: var(--muted); flex: 1; }
|
|
447
|
+
.web-dl-btn {
|
|
448
|
+
padding: 4px 10px; border-radius: 5px; font-size: 11px; font-weight: 600;
|
|
449
|
+
cursor: pointer; border: 1px solid var(--border2); background: var(--surface2);
|
|
450
|
+
color: var(--ink2); font-family: 'Manrope',sans-serif; transition: all 0.15s;
|
|
451
|
+
}
|
|
452
|
+
.web-dl-btn:hover { border-color: var(--lime); color: var(--lime); }
|
|
453
|
+
</style>
|
|
454
|
+
<style>
|
|
455
|
+
|
|
456
|
+
/* ── EDITOR-SPECIFIC OVERRIDES ── */
|
|
457
|
+
|
|
458
|
+
/* Hide docs nav tab in editor mode */
|
|
459
|
+
#tab-docs { display: none !important; }
|
|
460
|
+
|
|
461
|
+
/* Sidebar */
|
|
462
|
+
.sidebar {
|
|
463
|
+
width: 240px;
|
|
464
|
+
min-width: 180px;
|
|
465
|
+
max-width: 320px;
|
|
466
|
+
background: var(--bg2);
|
|
467
|
+
border-right: 1px solid var(--border);
|
|
468
|
+
display: flex;
|
|
469
|
+
flex-direction: column;
|
|
470
|
+
flex-shrink: 0;
|
|
471
|
+
overflow: hidden;
|
|
472
|
+
}
|
|
473
|
+
.sidebar-header {
|
|
474
|
+
padding: 10px 12px 8px;
|
|
475
|
+
border-bottom: 1px solid var(--border);
|
|
476
|
+
display: flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
gap: 6px;
|
|
479
|
+
flex-shrink: 0;
|
|
480
|
+
}
|
|
481
|
+
.sidebar-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); flex: 1; }
|
|
482
|
+
.sidebar-action { width: 22px; height: 22px; border-radius: 4px; display: flex; align-items: center; justify-content: center;
|
|
483
|
+
cursor: pointer; background: none; border: none; color: var(--muted); font-size: 13px; transition: all 0.15s; }
|
|
484
|
+
.sidebar-action:hover { background: var(--surface); color: var(--ink); }
|
|
485
|
+
|
|
486
|
+
.file-tree { flex: 1; overflow-y: auto; padding: 4px 0; }
|
|
487
|
+
.file-item {
|
|
488
|
+
display: flex; align-items: center; gap: 6px; padding: 5px 10px 5px 12px;
|
|
489
|
+
cursor: pointer; font-size: 12px; color: var(--ink2); border-radius: 0;
|
|
490
|
+
transition: background 0.1s; user-select: none; position: relative;
|
|
491
|
+
}
|
|
492
|
+
.file-item:hover { background: var(--surface); color: var(--ink); }
|
|
493
|
+
.file-item.active { background: var(--accent-dim); color: var(--accent-lt); }
|
|
494
|
+
.file-item.active::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: var(--accent); }
|
|
495
|
+
.file-icon { font-size: 11px; flex-shrink: 0; }
|
|
496
|
+
.file-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'DM Mono', monospace; }
|
|
497
|
+
.file-delete { opacity: 0; font-size: 11px; color: var(--muted); padding: 1px 3px; border-radius: 3px; transition: all 0.1s; }
|
|
498
|
+
.file-item:hover .file-delete { opacity: 1; }
|
|
499
|
+
.file-delete:hover { background: var(--err-dim); color: var(--err); }
|
|
500
|
+
.file-section { padding: 6px 12px 2px; font-size: 9px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); }
|
|
501
|
+
|
|
502
|
+
.sidebar-new-btn {
|
|
503
|
+
margin: 8px 10px; padding: 7px 10px; border-radius: 7px; font-size: 11px; font-weight: 700;
|
|
504
|
+
cursor: pointer; border: 1px dashed var(--border2); background: transparent;
|
|
505
|
+
color: var(--muted); font-family: 'Manrope', sans-serif; transition: all 0.2s;
|
|
506
|
+
text-align: center;
|
|
507
|
+
}
|
|
508
|
+
.sidebar-new-btn:hover { border-color: var(--lime); color: var(--lime); background: rgba(184,240,0,0.04); }
|
|
509
|
+
|
|
510
|
+
/* File path bar */
|
|
511
|
+
.filepath-bar {
|
|
512
|
+
display: flex; align-items: center; gap: 8px; padding: 0 14px;
|
|
513
|
+
height: 28px; background: var(--bg2); border-bottom: 1px solid var(--border);
|
|
514
|
+
flex-shrink: 0;
|
|
515
|
+
}
|
|
516
|
+
.filepath-text { font-size: 10px; font-family: 'DM Mono', monospace; color: var(--muted);
|
|
517
|
+
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
518
|
+
.filepath-text span { color: var(--ink2); }
|
|
519
|
+
.fp-save-badge { font-size: 9px; padding: 1px 6px; border-radius: 3px; background: var(--accent-dim);
|
|
520
|
+
color: var(--accent); font-weight: 700; display: none; }
|
|
521
|
+
.fp-save-badge.show { display: inline; }
|
|
522
|
+
|
|
523
|
+
/* Override playground layout to include sidebar */
|
|
524
|
+
.playground { display: flex; flex-direction: row; height: 100%; overflow: hidden; }
|
|
525
|
+
.editor-column { display: flex; flex-direction: column; flex: 1; min-width: 0; }
|
|
526
|
+
.editor-pane { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
527
|
+
|
|
528
|
+
/* Save / open buttons in topbar — hide the browser download versions */
|
|
529
|
+
#save-file-btn-dl { display: none !important; }
|
|
530
|
+
|
|
531
|
+
/* Status bar at bottom */
|
|
532
|
+
.status-bar {
|
|
533
|
+
display: flex; align-items: center; gap: 12px; padding: 0 14px;
|
|
534
|
+
height: 22px; background: var(--accent); color: #0d1f1e; font-size: 10px;
|
|
535
|
+
font-family: 'DM Mono', monospace; flex-shrink: 0;
|
|
536
|
+
}
|
|
537
|
+
.status-bar span { opacity: 0.7; }
|
|
538
|
+
.status-bar .sb-sep { opacity: 0.3; }
|
|
539
|
+
#sb-cursor { opacity: 1; font-weight: 600; }
|
|
540
|
+
#sb-lang { font-weight: 700; }
|
|
541
|
+
|
|
542
|
+
/* Open file dialog overlay */
|
|
543
|
+
.browse-overlay {
|
|
544
|
+
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.65);
|
|
545
|
+
z-index: 999; align-items: center; justify-content: center; backdrop-filter: blur(4px);
|
|
546
|
+
}
|
|
547
|
+
.browse-overlay.open { display: flex; }
|
|
548
|
+
.browse-dialog {
|
|
549
|
+
background: var(--surface); border: 1px solid var(--border2); border-radius: 12px;
|
|
550
|
+
width: 520px; max-width: 95vw; height: 480px; max-height: 80vh;
|
|
551
|
+
display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
|
552
|
+
}
|
|
553
|
+
.browse-bar {
|
|
554
|
+
display: flex; align-items: center; gap: 8px; padding: 12px 16px;
|
|
555
|
+
border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
556
|
+
}
|
|
557
|
+
.browse-path-input {
|
|
558
|
+
flex: 1; background: var(--bg3); border: 1px solid var(--border2); border-radius: 6px;
|
|
559
|
+
padding: 6px 10px; color: var(--ink); font-family: 'DM Mono', monospace; font-size: 11px; outline: none;
|
|
560
|
+
}
|
|
561
|
+
.browse-path-input:focus { border-color: var(--accent); }
|
|
562
|
+
.browse-up-btn { padding: 5px 10px; border-radius: 5px; font-size: 11px; cursor: pointer;
|
|
563
|
+
border: 1px solid var(--border2); background: var(--surface2); color: var(--ink2); font-family: 'Manrope', sans-serif; }
|
|
564
|
+
.browse-up-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
565
|
+
.browse-list { flex: 1; overflow-y: auto; padding: 4px 0; }
|
|
566
|
+
.browse-entry { display: flex; align-items: center; gap: 10px; padding: 7px 16px; cursor: pointer;
|
|
567
|
+
font-size: 12px; color: var(--ink2); transition: background 0.1s; }
|
|
568
|
+
.browse-entry:hover { background: var(--surface); color: var(--ink); }
|
|
569
|
+
.browse-entry .be-icon { font-size: 13px; flex-shrink: 0; }
|
|
570
|
+
.browse-entry .be-name { font-family: 'DM Mono', monospace; }
|
|
571
|
+
.browse-entry .be-ss { color: var(--accent); }
|
|
572
|
+
.browse-footer { padding: 10px 16px; border-top: 1px solid var(--border);
|
|
573
|
+
display: flex; justify-content: flex-end; gap: 8px; flex-shrink: 0; }
|
|
574
|
+
.browse-cancel-btn { padding: 7px 14px; border-radius: 6px; font-size: 12px; font-weight: 600;
|
|
575
|
+
cursor: pointer; border: 1px solid var(--border2); background: var(--surface2); color: var(--ink2); font-family: 'Manrope', sans-serif; }
|
|
576
|
+
|
|
577
|
+
/* Save As dialog */
|
|
578
|
+
.saveas-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.65);
|
|
579
|
+
z-index: 999; align-items: center; justify-content: center; backdrop-filter: blur(4px); }
|
|
580
|
+
.saveas-overlay.open { display: flex; }
|
|
581
|
+
.saveas-dialog { background: var(--surface); border: 1px solid var(--border2); border-radius: 12px;
|
|
582
|
+
padding: 24px 28px; width: 420px; max-width: 95vw; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
|
|
583
|
+
.saveas-input { width: 100%; background: var(--bg3); border: 1px solid var(--border2); border-radius: 6px;
|
|
584
|
+
padding: 8px 12px; color: var(--ink); font-family: 'DM Mono', monospace; font-size: 13px; outline: none; margin: 8px 0 16px; }
|
|
585
|
+
.saveas-input:focus { border-color: var(--accent); }
|
|
586
|
+
|
|
587
|
+
/* autosave flash */
|
|
588
|
+
@keyframes savedFlash { 0%{opacity:1} 60%{opacity:1} 100%{opacity:0} }
|
|
589
|
+
.saved-toast { position: fixed; bottom: 32px; right: 24px; padding: 6px 14px; border-radius: 6px;
|
|
590
|
+
background: var(--accent); color: #0d1f1e; font-size: 11px; font-weight: 700;
|
|
591
|
+
font-family: 'Manrope', sans-serif; pointer-events: none; animation: savedFlash 1.6s forwards; z-index: 9999; }
|
|
592
|
+
|
|
593
|
+
</style>
|
|
594
|
+
</head>
|
|
595
|
+
<body data-theme="dark">
|
|
596
|
+
|
|
597
|
+
<!-- TOPBAR -->
|
|
598
|
+
<div class="topbar">
|
|
599
|
+
<div class="logo">
|
|
600
|
+
<svg width="26" height="26" viewBox="0 0 52 52" fill="none" style="flex-shrink:0">
|
|
601
|
+
<path d="M26 3 L47 14.5 L47 37.5 L26 49 L5 37.5 L5 14.5 Z" fill="#0b7a75"/>
|
|
602
|
+
<path d="M20 17 L16 20 C14.5 21 14.5 22.5 16 23.5 L16 28.5 C14.5 29.5 14.5 31 16 32 L20 35" stroke="#b8f000" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
603
|
+
<path d="M32 17 L36 20 C37.5 21 37.5 22.5 36 23.5 L36 28.5 C37.5 29.5 37.5 31 36 32 L32 35" stroke="#b8f000" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
604
|
+
<circle cx="26" cy="26" r="2.5" fill="#b8f000"/>
|
|
605
|
+
</svg>
|
|
606
|
+
<span class="logo-s">Struct</span><span class="logo-sc">Script</span>
|
|
607
|
+
<span class="logo-v">Editor</span>
|
|
608
|
+
</div>
|
|
609
|
+
<div class="divider"></div>
|
|
610
|
+
<div class="nav-tabs">
|
|
611
|
+
<button class="nav-tab active" onclick="showPage('playground',this)" id="tab-playground">Editor</button>
|
|
612
|
+
<button class="nav-tab" onclick="showPage('docs',this)" id="tab-docs">Docs</button>
|
|
613
|
+
</div>
|
|
614
|
+
<div class="topbar-right">
|
|
615
|
+
<button class="file-btn" onclick="newFile()">+ New</button>
|
|
616
|
+
<button class="file-btn" onclick="openFile()">📂 Open</button>
|
|
617
|
+
<button class="file-btn" onclick="saveFile()">💾 Save</button>
|
|
618
|
+
<button class="file-btn" onclick="saveFileAs()">💾 Save As…</button>
|
|
619
|
+
<button class="file-btn" onclick="saveHTML()" id="save-html-btn" style="display:none">🌐 Save .html</button>
|
|
620
|
+
<div class="divider"></div>
|
|
621
|
+
<button class="icon-btn" onclick="toggleTheme()" title="Toggle theme" id="theme-btn">🌙</button>
|
|
622
|
+
<button class="run-btn" onclick="runCode()">▶ Run <span class="run-kbd">Ctrl+↵</span></button>
|
|
623
|
+
</div>
|
|
624
|
+
<input type="file" id="file-input" accept=".ss,.txt" style="display:none" onchange="handleFileOpen(event)">
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
<div class="app-body">
|
|
628
|
+
|
|
629
|
+
<!-- PLAYGROUND PAGE (editor + sidebar) -->
|
|
630
|
+
<div id="page-playground" class="page active">
|
|
631
|
+
<div class="playground">
|
|
632
|
+
|
|
633
|
+
<!-- Sidebar: file explorer -->
|
|
634
|
+
<div class="sidebar">
|
|
635
|
+
<div class="sidebar-header">
|
|
636
|
+
<span class="sidebar-title">Files</span>
|
|
637
|
+
<button class="sidebar-action" onclick="newFile()" title="New file">+</button>
|
|
638
|
+
<button class="sidebar-action" onclick="openFile()" title="Open file">📂</button>
|
|
639
|
+
</div>
|
|
640
|
+
<button class="sidebar-new-btn" onclick="newFile()">+ New .ss file</button>
|
|
641
|
+
<div class="file-tree" id="file-tree-list">
|
|
642
|
+
<div style="padding:16px 14px;font-size:11px;color:var(--muted)">Loading…</div>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<!-- Editor column -->
|
|
647
|
+
<div class="editor-column">
|
|
648
|
+
|
|
649
|
+
<!-- File path bar -->
|
|
650
|
+
<div class="filepath-bar">
|
|
651
|
+
<span style="font-size:10px;color:var(--muted);font-family:'DM Mono',monospace">📄</span>
|
|
652
|
+
<span class="filepath-text" id="fp-path" title="Click to reveal in folder" onclick="if(window.electronAPI&¤tFilePath)window.electronAPI.showInFolder({filePath:currentFilePath})" style="cursor:pointer">untitled.ss</span>
|
|
653
|
+
<span class="fp-save-badge" id="fp-unsaved">unsaved</span>
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
<!-- Editor pane -->
|
|
657
|
+
<div class="editor-pane">
|
|
658
|
+
<div class="pane-bar">
|
|
659
|
+
<div class="wc-dots">
|
|
660
|
+
<div class="wc-dot" style="background:#f87171"></div>
|
|
661
|
+
<div class="wc-dot" style="background:#fcd34d"></div>
|
|
662
|
+
<div class="wc-dot" style="background:#6ee7b7"></div>
|
|
663
|
+
</div>
|
|
664
|
+
<span class="filename" id="current-filename">untitled.ss</span>
|
|
665
|
+
<span class="modified" id="modified-indicator" style="display:none">●</span>
|
|
666
|
+
</div>
|
|
667
|
+
<div class="editor-container">
|
|
668
|
+
<div class="line-numbers" id="line-numbers"></div>
|
|
669
|
+
<div class="code-wrap" id="code-wrap">
|
|
670
|
+
<div id="highlight-layer"></div>
|
|
671
|
+
<textarea id="code-editor" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
|
|
672
|
+
<div id="autocomplete"></div>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
|
|
677
|
+
<!-- Output pane -->
|
|
678
|
+
<div class="output-pane">
|
|
679
|
+
<div class="out-tabs">
|
|
680
|
+
<button class="out-tab active" onclick="switchOutTab('output',this)" id="outtab-output">Output</button>
|
|
681
|
+
<button class="out-tab" onclick="switchOutTab('errors',this)" id="outtab-errors">Errors <span id="err-badge" style="display:none;background:var(--err);color:#fff;font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px"></span></button>
|
|
682
|
+
<button class="out-tab" onclick="switchOutTab('web',this)" id="outtab-web" style="display:none">🌐 Preview</button>
|
|
683
|
+
</div>
|
|
684
|
+
<div id="out-output" class="out-panel active">
|
|
685
|
+
<div id="output-display">
|
|
686
|
+
<div class="out-line info"><span class="out-prefix">›</span><span class="out-text">StructScript Editor — Ready</span></div>
|
|
687
|
+
<div class="out-line info"><span class="out-prefix">›</span><span class="out-text">Press ▶ Run or Ctrl+Enter to execute</span></div>
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
690
|
+
<div id="out-errors" class="out-panel">
|
|
691
|
+
<div id="error-display">
|
|
692
|
+
<div class="no-errors"><div class="no-errors-icon">✓</div>No errors</div>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
<div id="out-web" class="out-panel">
|
|
696
|
+
<iframe id="web-preview" style="width:100%;height:100%;border:none;background:#fff"></iframe>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
<!-- Status bar -->
|
|
701
|
+
<div class="status-bar">
|
|
702
|
+
<span id="sb-lang">StructScript</span>
|
|
703
|
+
<span class="sb-sep">│</span>
|
|
704
|
+
<span id="sb-cursor">Ln 1, Col 1</span>
|
|
705
|
+
<span class="sb-sep">│</span>
|
|
706
|
+
<span id="sb-dot" class="sb-dot ok" style="width:7px;height:7px;border-radius:50%;background:var(--lime);display:inline-block;margin-right:2px"></span>
|
|
707
|
+
<span id="sb-status">Ready</span>
|
|
708
|
+
<span class="sb-sep">│</span>
|
|
709
|
+
<span id="sb-time"></span>
|
|
710
|
+
</div>
|
|
711
|
+
|
|
712
|
+
</div><!-- editor-column -->
|
|
713
|
+
|
|
714
|
+
</div><!-- playground -->
|
|
715
|
+
</div><!-- page-playground -->
|
|
716
|
+
|
|
717
|
+
<!-- DOCS PAGE -->
|
|
718
|
+
<div id="page-docs" class="page">
|
|
719
|
+
<div class="docs-page">
|
|
720
|
+
<div class="docs-hero">
|
|
721
|
+
<h2><em>StructScript</em> v1.1<br>Language Reference</h2>
|
|
722
|
+
<p>A clean, readable language blending natural English with Python-style indentation. Built for beginners, scripters, and systems thinkers.</p>
|
|
723
|
+
</div>
|
|
724
|
+
|
|
725
|
+
<div class="docs-grid">
|
|
726
|
+
<div class="docs-card"><h4>🧱 Structured</h4><p>Indentation-based blocks. No braces needed. Clean and readable at a glance.</p></div>
|
|
727
|
+
<div class="docs-card"><h4>📖 Readable</h4><p>Keywords read like English: <code>say</code>, <code>define</code>, <code>count from to</code>, <code>repeat times</code>.</p></div>
|
|
728
|
+
<div class="docs-card"><h4>⚡ Capable</h4><p>Functions, structs, lists, error handling, string ops, math — all built in.</p></div>
|
|
729
|
+
<div class="docs-card"><h4>🛡️ Safe</h4><p>Clear error messages with line numbers and code snippets. Stack overflow protection.</p></div>
|
|
730
|
+
</div>
|
|
731
|
+
|
|
732
|
+
<div class="docs-section">
|
|
733
|
+
<h3>Variables & Constants</h3>
|
|
734
|
+
<table class="docs-table">
|
|
735
|
+
<tr><th>Syntax</th><th>Example</th><th>Notes</th></tr>
|
|
736
|
+
<tr><td><code>let name = value</code></td><td><code>let x = 42</code></td><td>Mutable variable</td></tr>
|
|
737
|
+
<tr><td><code>const name = value</code></td><td><code>const PI = 3.14159</code></td><td>Immutable constant</td></tr>
|
|
738
|
+
<tr><td><code>set name = value</code></td><td><code>set x = x + 1</code></td><td>Reassign existing variable</td></tr>
|
|
739
|
+
<tr><td><code>set obj.field = val</code></td><td><code>set person.age = 30</code></td><td>Struct field assignment</td></tr>
|
|
740
|
+
</table>
|
|
741
|
+
</div>
|
|
742
|
+
|
|
743
|
+
<div class="docs-section">
|
|
744
|
+
<h3>Types</h3>
|
|
745
|
+
<table class="docs-table">
|
|
746
|
+
<tr><th>Type</th><th>Examples</th><th>Notes</th></tr>
|
|
747
|
+
<tr><td>Number</td><td><code>42</code>, <code>3.14</code>, <code>-7</code></td><td>Integer or float</td></tr>
|
|
748
|
+
<tr><td>String</td><td><code>"hello"</code>, <code>'world'</code></td><td>Single or double quotes</td></tr>
|
|
749
|
+
<tr><td>Boolean</td><td><code>true</code>, <code>false</code></td><td>Logical values</td></tr>
|
|
750
|
+
<tr><td>Nothing</td><td><code>nothing</code></td><td>Null/None equivalent</td></tr>
|
|
751
|
+
<tr><td>List</td><td><code>[1, 2, 3]</code></td><td>Ordered, mutable collection</td></tr>
|
|
752
|
+
<tr><td>Struct</td><td><code>Point()</code></td><td>Custom data type</td></tr>
|
|
753
|
+
</table>
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
<div class="docs-section">
|
|
757
|
+
<h3>Operators</h3>
|
|
758
|
+
<table class="docs-table">
|
|
759
|
+
<tr><th>Category</th><th>Operators</th></tr>
|
|
760
|
+
<tr><td>Arithmetic</td><td><code>+ - * / % ** //</code></td></tr>
|
|
761
|
+
<tr><td>Comparison</td><td><code>== != > < >= <=</code></td></tr>
|
|
762
|
+
<tr><td>Logical</td><td><code>and or not</code></td></tr>
|
|
763
|
+
<tr><td>String</td><td><code>+</code> (concat), <code>*</code> (repeat)</td></tr>
|
|
764
|
+
</table>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
<div class="docs-section">
|
|
768
|
+
<h3>Control Flow</h3>
|
|
769
|
+
<div class="docs-code">
|
|
770
|
+
<span class="kw">if</span> x > <span class="num">0</span>:
|
|
771
|
+
<span class="kw">say</span> <span class="str">"positive"</span>
|
|
772
|
+
<span class="kw">else if</span> x < <span class="num">0</span>:
|
|
773
|
+
<span class="kw">say</span> <span class="str">"negative"</span>
|
|
774
|
+
<span class="kw">else</span>:
|
|
775
|
+
<span class="kw">say</span> <span class="str">"zero"</span>
|
|
776
|
+
|
|
777
|
+
<span class="kw">while</span> x < <span class="num">100</span>:
|
|
778
|
+
<span class="kw">set</span> x = x * <span class="num">2</span>
|
|
779
|
+
|
|
780
|
+
<span class="kw">repeat</span> <span class="num">5</span> <span class="kw">times</span>:
|
|
781
|
+
<span class="kw">say</span> <span class="str">"hello"</span>
|
|
782
|
+
|
|
783
|
+
<span class="kw">count</span> i <span class="kw">from</span> <span class="num">1</span> <span class="kw">to</span> <span class="num">10</span>:
|
|
784
|
+
<span class="kw">say</span> i
|
|
785
|
+
|
|
786
|
+
<span class="kw">for each</span> item <span class="kw">in</span> myList:
|
|
787
|
+
<span class="kw">say</span> item</div>
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
<div class="docs-section">
|
|
791
|
+
<h3>Functions</h3>
|
|
792
|
+
<div class="docs-code">
|
|
793
|
+
<span class="kw">define</span> <span class="fn">greet</span>(name):
|
|
794
|
+
<span class="kw">say</span> <span class="str">"Hello, "</span> + name
|
|
795
|
+
|
|
796
|
+
<span class="kw">define</span> <span class="fn">add</span>(a, b):
|
|
797
|
+
<span class="kw">return</span> a + b
|
|
798
|
+
|
|
799
|
+
<span class="cmt">// Default parameters</span>
|
|
800
|
+
<span class="kw">define</span> <span class="fn">power</span>(base, exp = <span class="num">2</span>):
|
|
801
|
+
<span class="kw">return</span> base ** exp
|
|
802
|
+
|
|
803
|
+
<span class="fn">greet</span>(<span class="str">"World"</span>)
|
|
804
|
+
<span class="kw">let</span> result = <span class="fn">add</span>(<span class="num">3</span>, <span class="num">4</span>)</div>
|
|
805
|
+
</div>
|
|
806
|
+
|
|
807
|
+
<div class="docs-section">
|
|
808
|
+
<h3>Error Handling</h3>
|
|
809
|
+
<div class="docs-code">
|
|
810
|
+
<span class="kw">try</span>:
|
|
811
|
+
<span class="kw">let</span> x = <span class="num">10</span> / <span class="num">0</span>
|
|
812
|
+
<span class="kw">catch</span> err:
|
|
813
|
+
<span class="kw">say</span> <span class="str">"Caught: "</span> + err
|
|
814
|
+
|
|
815
|
+
<span class="kw">try</span>:
|
|
816
|
+
<span class="fn">riskyFunction</span>()
|
|
817
|
+
<span class="kw">catch</span> e:
|
|
818
|
+
<span class="kw">say</span> <span class="str">"Error: "</span> + e
|
|
819
|
+
<span class="kw">finally</span>:
|
|
820
|
+
<span class="kw">say</span> <span class="str">"Always runs"</span></div>
|
|
821
|
+
</div>
|
|
822
|
+
|
|
823
|
+
<div class="docs-section">
|
|
824
|
+
<h3>Built-in Functions</h3>
|
|
825
|
+
<table class="docs-table">
|
|
826
|
+
<tr><th>Function</th><th>Description</th><th>Example</th></tr>
|
|
827
|
+
<tr><td><code>say(x)</code></td><td>Print to output</td><td><code>say "hi"</code></td></tr>
|
|
828
|
+
<tr><td><code>length(x)</code></td><td>Length of list/string</td><td><code>length([1,2,3])</code> → 3</td></tr>
|
|
829
|
+
<tr><td><code>push(list, val)</code></td><td>Append to list</td><td><code>push(nums, 4)</code></td></tr>
|
|
830
|
+
<tr><td><code>pop(list)</code></td><td>Remove last item</td><td><code>pop(nums)</code></td></tr>
|
|
831
|
+
<tr><td><code>join(list, sep)</code></td><td>Join list to string</td><td><code>join(items, ", ")</code></td></tr>
|
|
832
|
+
<tr><td><code>split(str, sep)</code></td><td>Split string to list</td><td><code>split("a,b", ",")</code></td></tr>
|
|
833
|
+
<tr><td><code>upper(str)</code></td><td>Uppercase string</td><td><code>upper("hi")</code> → "HI"</td></tr>
|
|
834
|
+
<tr><td><code>lower(str)</code></td><td>Lowercase string</td><td><code>lower("HI")</code> → "hi"</td></tr>
|
|
835
|
+
<tr><td><code>trim(str)</code></td><td>Strip whitespace</td><td><code>trim(" hi ")</code></td></tr>
|
|
836
|
+
<tr><td><code>contains(str, sub)</code></td><td>Check substring</td><td><code>contains("hello", "ell")</code></td></tr>
|
|
837
|
+
<tr><td><code>replace(str,old,new)</code></td><td>Replace in string</td><td><code>replace("hi","h","b")</code></td></tr>
|
|
838
|
+
<tr><td><code>slice(list, a, b)</code></td><td>Sub-list or substring</td><td><code>slice(nums, 0, 3)</code></td></tr>
|
|
839
|
+
<tr><td><code>sort(list)</code></td><td>Sort a list</td><td><code>sort([3,1,2])</code></td></tr>
|
|
840
|
+
<tr><td><code>reverse(list)</code></td><td>Reverse a list</td><td><code>reverse([1,2,3])</code></td></tr>
|
|
841
|
+
<tr><td><code>range(n)</code> / <code>range(a,b)</code></td><td>Generate number list</td><td><code>range(5)</code> → [0..4]</td></tr>
|
|
842
|
+
<tr><td><code>abs(n)</code></td><td>Absolute value</td><td><code>abs(-5)</code> → 5</td></tr>
|
|
843
|
+
<tr><td><code>sqrt(n)</code></td><td>Square root</td><td><code>sqrt(16)</code> → 4</td></tr>
|
|
844
|
+
<tr><td><code>floor/ceil/round</code></td><td>Rounding</td><td><code>floor(3.7)</code> → 3</td></tr>
|
|
845
|
+
<tr><td><code>max/min</code></td><td>Max/min of args</td><td><code>max(1,5,3)</code> → 5</td></tr>
|
|
846
|
+
<tr><td><code>random()</code></td><td>Random 0–1 float</td><td><code>random()</code></td></tr>
|
|
847
|
+
<tr><td><code>randint(a,b)</code></td><td>Random integer</td><td><code>randint(1,6)</code></td></tr>
|
|
848
|
+
<tr><td><code>str(x)</code></td><td>Convert to string</td><td><code>str(42)</code> → "42"</td></tr>
|
|
849
|
+
<tr><td><code>num(x)</code></td><td>Convert to number</td><td><code>num("42")</code> → 42</td></tr>
|
|
850
|
+
<tr><td><code>bool(x)</code></td><td>Convert to boolean</td><td><code>bool(0)</code> → false</td></tr>
|
|
851
|
+
<tr><td><code>type(x)</code></td><td>Get type name</td><td><code>type(42)</code> → "number"</td></tr>
|
|
852
|
+
<tr><td><code>isNothing(x)</code></td><td>Check for nothing</td><td><code>isNothing(val)</code></td></tr>
|
|
853
|
+
</table>
|
|
854
|
+
</div>
|
|
855
|
+
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
|
|
859
|
+
<div id="input-modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:999;align-items:center;justify-content:center;backdrop-filter:blur(3px)">
|
|
860
|
+
<div style="background:var(--surface);border:1px solid var(--border2);border-radius:12px;padding:24px 28px;min-width:360px;max-width:480px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.5)">
|
|
861
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:18px">
|
|
862
|
+
<div style="width:28px;height:28px;border-radius:6px;background:var(--accent-dim);border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:13px">⌨</div>
|
|
863
|
+
<div>
|
|
864
|
+
<div style="font-size:13px;font-weight:700;color:var(--ink)">Program needs input</div>
|
|
865
|
+
<div style="font-size:10px;color:var(--muted);font-family:'DM Mono',monospace">Fill in values then click Run</div>
|
|
866
|
+
</div>
|
|
867
|
+
</div>
|
|
868
|
+
<div id="input-modal-fields"></div>
|
|
869
|
+
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:20px;padding-top:16px;border-top:1px solid var(--border)">
|
|
870
|
+
<button onclick="cancelInputModal()" style="padding:7px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border2);background:var(--surface2);color:var(--ink2);font-family:'Manrope',sans-serif">Cancel</button>
|
|
871
|
+
<button onclick="submitInputModal()" style="padding:7px 20px;border-radius:6px;font-size:12px;font-weight:800;cursor:pointer;border:none;background:var(--lime);color:#0d1f1e;font-family:'Manrope',sans-serif">▶ Run</button>
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
</div>
|
|
878
|
+
|
|
879
|
+
</div><!-- app-body -->
|
|
880
|
+
|
|
881
|
+
<!-- INPUT MODAL -->
|
|
882
|
+
<div id="input-modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:999;align-items:center;justify-content:center;backdrop-filter:blur(3px)">
|
|
883
|
+
<div style="background:var(--surface);border:1px solid var(--border2);border-radius:12px;padding:24px 28px;min-width:360px;max-width:480px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.5)">
|
|
884
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:18px">
|
|
885
|
+
<div style="width:28px;height:28px;border-radius:6px;background:var(--accent-dim);border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:13px">⌨</div>
|
|
886
|
+
<div>
|
|
887
|
+
<div style="font-size:13px;font-weight:700;color:var(--ink)">Program needs input</div>
|
|
888
|
+
<div style="font-size:10px;color:var(--muted);font-family:'DM Mono',monospace">Fill in values then click Run</div>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
<div id="input-modal-fields"></div>
|
|
892
|
+
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:20px;padding-top:16px;border-top:1px solid var(--border)">
|
|
893
|
+
<button onclick="cancelInputModal()" style="padding:7px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border2);background:var(--surface2);color:var(--ink2);font-family:'Manrope',sans-serif">Cancel</button>
|
|
894
|
+
<button onclick="submitInputModal()" style="padding:7px 20px;border-radius:6px;font-size:12px;font-weight:800;cursor:pointer;border:none;background:var(--lime);color:#0d1f1e;font-family:'Manrope',sans-serif">▶ Run</button>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
<!-- BROWSE DIALOG -->
|
|
900
|
+
<div class="browse-overlay" id="browse-overlay">
|
|
901
|
+
<div class="browse-dialog">
|
|
902
|
+
<div class="browse-bar">
|
|
903
|
+
<button class="browse-up-btn" onclick="browseUp()">↑ Up</button>
|
|
904
|
+
<input class="browse-path-input" id="browse-path-input" placeholder="/path/to/folder"
|
|
905
|
+
onkeydown="if(event.key==='Enter')browseDirFromInput()">
|
|
906
|
+
<button class="browse-up-btn" onclick="browseDirFromInput()">Go</button>
|
|
907
|
+
</div>
|
|
908
|
+
<div class="browse-list" id="browse-list"></div>
|
|
909
|
+
<div class="browse-footer">
|
|
910
|
+
<button class="browse-cancel-btn" onclick="closeBrowseDialog()">Cancel</button>
|
|
911
|
+
</div>
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
|
|
915
|
+
<!-- SAVE AS DIALOG -->
|
|
916
|
+
<div class="saveas-overlay" id="saveas-overlay">
|
|
917
|
+
<div class="saveas-dialog">
|
|
918
|
+
<div style="font-size:13px;font-weight:700;color:var(--ink);margin-bottom:4px">Save As</div>
|
|
919
|
+
<div style="font-size:11px;color:var(--muted);margin-bottom:8px">Enter a full file path (e.g. /home/user/myscript.ss)</div>
|
|
920
|
+
<input class="saveas-input" id="saveas-input" placeholder="/home/you/script.ss"
|
|
921
|
+
onkeydown="if(event.key==='Enter')confirmSaveAs();if(event.key==='Escape')closeSaveAsDialog()">
|
|
922
|
+
<div style="display:flex;justify-content:flex-end;gap:8px">
|
|
923
|
+
<button onclick="closeSaveAsDialog()" style="padding:7px 14px;border-radius:6px;font-size:12px;cursor:pointer;border:1px solid var(--border2);background:var(--surface2);color:var(--ink2);font-family:'Manrope',sans-serif">Cancel</button>
|
|
924
|
+
<button onclick="confirmSaveAs()" style="padding:7px 20px;border-radius:6px;font-size:12px;font-weight:800;cursor:pointer;border:none;background:var(--lime);color:#0d1f1e;font-family:'Manrope',sans-serif">Save</button>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
|
|
929
|
+
<script>
|
|
930
|
+
// ============================================================
|
|
931
|
+
// STRUCTSCRIPT INTERPRETER v1.1
|
|
932
|
+
// ============================================================
|
|
933
|
+
|
|
934
|
+
class SSError extends Error {
|
|
935
|
+
constructor(msg, line, snippet) {
|
|
936
|
+
super(msg);
|
|
937
|
+
this.ssLine = line;
|
|
938
|
+
this.snippet = snippet;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
class ReturnSignal { constructor(v){this.value=v;} }
|
|
943
|
+
class BreakSignal {}
|
|
944
|
+
class ContinueSignal {}
|
|
945
|
+
|
|
946
|
+
class Environment {
|
|
947
|
+
constructor(parent=null) { this.vars={}; this.consts=new Set(); this.parent=parent; }
|
|
948
|
+
get(name) {
|
|
949
|
+
if (name in this.vars) return this.vars[name];
|
|
950
|
+
if (this.parent) return this.parent.get(name);
|
|
951
|
+
throw new SSError(`Undefined variable: "${name}"`);
|
|
952
|
+
}
|
|
953
|
+
set(name, value) {
|
|
954
|
+
if (name in this.vars) {
|
|
955
|
+
if (this.consts.has(name)) throw new SSError(`Cannot reassign constant "${name}"`);
|
|
956
|
+
this.vars[name] = value; return;
|
|
957
|
+
}
|
|
958
|
+
if (this.parent && this.parent.has(name)) { this.parent.set(name,value); return; }
|
|
959
|
+
this.vars[name] = value;
|
|
960
|
+
}
|
|
961
|
+
has(name) { return name in this.vars || (this.parent ? this.parent.has(name) : false); }
|
|
962
|
+
define(name, value, isConst=false) { this.vars[name]=value; if(isConst)this.consts.add(name); }
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
class Interpreter {
|
|
966
|
+
constructor(outputFn, warnFn) {
|
|
967
|
+
this.output = outputFn;
|
|
968
|
+
this.warn = warnFn || (()=>{});
|
|
969
|
+
this.globals = new Environment();
|
|
970
|
+
this.structs = {};
|
|
971
|
+
this.callDepth = 0;
|
|
972
|
+
this.sourceLines = [];
|
|
973
|
+
this._registerBuiltins();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
_registerBuiltins() {
|
|
977
|
+
const G = this.globals;
|
|
978
|
+
// I/O
|
|
979
|
+
G.define('say', (a) => { this.output(this._str(a[0])); return null; });
|
|
980
|
+
// Math
|
|
981
|
+
G.define('abs', a => Math.abs(a[0]));
|
|
982
|
+
G.define('sqrt', a => Math.sqrt(a[0]));
|
|
983
|
+
G.define('floor', a => Math.floor(a[0]));
|
|
984
|
+
G.define('ceil', a => Math.ceil(a[0]));
|
|
985
|
+
G.define('round', a => Math.round(a[0]));
|
|
986
|
+
G.define('max', a => Math.max(...a));
|
|
987
|
+
G.define('min', a => Math.min(...a));
|
|
988
|
+
G.define('pow', a => Math.pow(a[0],a[1]));
|
|
989
|
+
G.define('log', a => Math.log(a[0]));
|
|
990
|
+
G.define('log10', a => Math.log10(a[0]));
|
|
991
|
+
G.define('sin', a => Math.sin(a[0]));
|
|
992
|
+
G.define('cos', a => Math.cos(a[0]));
|
|
993
|
+
G.define('tan', a => Math.tan(a[0]));
|
|
994
|
+
G.define('random', a => Math.random());
|
|
995
|
+
G.define('randint',a => Math.floor(Math.random()*(a[1]-a[0]+1))+a[0]);
|
|
996
|
+
// Type conversion
|
|
997
|
+
G.define('str', a => String(a[0] ?? ''));
|
|
998
|
+
G.define('num', a => { const n=Number(a[0]); if(isNaN(n)) throw new SSError(`Cannot convert "${a[0]}" to number`); return n; });
|
|
999
|
+
G.define('bool', a => Boolean(a[0]));
|
|
1000
|
+
G.define('type', a => { if(Array.isArray(a[0]))return'list'; if(a[0]===null||a[0]===undefined)return'nothing'; return typeof a[0]; });
|
|
1001
|
+
G.define('isNothing', a => a[0]===null||a[0]===undefined);
|
|
1002
|
+
// String
|
|
1003
|
+
G.define('upper', a => String(a[0]).toUpperCase());
|
|
1004
|
+
G.define('lower', a => String(a[0]).toLowerCase());
|
|
1005
|
+
G.define('trim', a => String(a[0]).trim());
|
|
1006
|
+
G.define('length', a => { if(Array.isArray(a[0]))return a[0].length; return String(a[0]).length; });
|
|
1007
|
+
G.define('contains',a => String(a[0]).includes(String(a[1])));
|
|
1008
|
+
G.define('startsWith',a=>String(a[0]).startsWith(String(a[1])));
|
|
1009
|
+
G.define('endsWith', a=>String(a[0]).endsWith(String(a[1])));
|
|
1010
|
+
G.define('replace', a => String(a[0]).split(String(a[1])).join(String(a[2])));
|
|
1011
|
+
G.define('split', a => String(a[0]).split(String(a[1])));
|
|
1012
|
+
G.define('indexOf', a => { const i=Array.isArray(a[0])?a[0].indexOf(a[1]):String(a[0]).indexOf(String(a[1])); return i; });
|
|
1013
|
+
G.define('slice', a => { const s=a[0]; const from=a[1]??0; const to=a[2]; return Array.isArray(s)?s.slice(from,to):String(s).slice(from,to); });
|
|
1014
|
+
G.define('char', a => String.fromCharCode(a[0]));
|
|
1015
|
+
G.define('charCode',a => String(a[0]).charCodeAt(0));
|
|
1016
|
+
G.define('repeat', a => String(a[0]).repeat(a[1]));
|
|
1017
|
+
G.define('format', a => { let s=String(a[0]); for(let i=1;i<a.length;i++) s=s.replace('{}',this._str(a[i])); return s; });
|
|
1018
|
+
// List
|
|
1019
|
+
G.define('push', a => { if(!Array.isArray(a[0]))throw new SSError('push() requires a list'); a[0].push(a[1]); return null; });
|
|
1020
|
+
G.define('pop', a => { if(!Array.isArray(a[0]))throw new SSError('pop() requires a list'); return a[0].pop()??null; });
|
|
1021
|
+
G.define('shift', a => { if(!Array.isArray(a[0]))return null; return a[0].shift()??null; });
|
|
1022
|
+
G.define('unshift', a => { if(Array.isArray(a[0]))a[0].unshift(a[1]); return null; });
|
|
1023
|
+
G.define('join', a => { if(!Array.isArray(a[0]))throw new SSError('join() requires a list'); return a[0].map(x=>this._str(x)).join(a[1]??''); });
|
|
1024
|
+
G.define('sort', a => { if(!Array.isArray(a[0]))throw new SSError('sort() requires a list'); return [...a[0]].sort((x,y)=>x<y?-1:x>y?1:0); });
|
|
1025
|
+
G.define('reverse', a => { if(!Array.isArray(a[0]))throw new SSError('reverse() requires a list'); return [...a[0]].reverse(); });
|
|
1026
|
+
G.define('range', a => { const from=a[1]!==undefined?a[0]:0; const to=a[1]!==undefined?a[1]:a[0]; const r=[]; for(let i=from;i<to;i++)r.push(i); return r; });
|
|
1027
|
+
G.define('flatten', a => { if(!Array.isArray(a[0]))return[a[0]]; return a[0].flat(); });
|
|
1028
|
+
G.define('unique', a => [...new Set(a[0])]);
|
|
1029
|
+
G.define('sum', a => { if(!Array.isArray(a[0]))return 0; return a[0].reduce((acc,v)=>acc+v,0); });
|
|
1030
|
+
G.define('avg', a => { if(!Array.isArray(a[0])||!a[0].length)return 0; return a[0].reduce((acc,v)=>acc+v,0)/a[0].length; });
|
|
1031
|
+
G.define('map', a => { if(!Array.isArray(a[0]))throw new SSError('map() requires a list'); const fn=a[1]; return a[0].map(item=>{ const r=fn([item],null); return r instanceof ReturnSignal?r.value:r; }); });
|
|
1032
|
+
G.define('filter', a => { if(!Array.isArray(a[0]))throw new SSError('filter() requires a list'); const fn=a[1]; return a[0].filter(item=>{ const r=fn([item],null); return r instanceof ReturnSignal?r.value:r; }); });
|
|
1033
|
+
G.define('reduce', a => { if(!Array.isArray(a[0]))throw new SSError('reduce() requires a list'); const fn=a[1]; let acc=a[2]; return a[0].reduce((ac,item)=>{ const r=fn([ac,item],null); return r instanceof ReturnSignal?r.value:r; },acc); });
|
|
1034
|
+
// Utility
|
|
1035
|
+
G.define('print', a => { this.output(a.map(x=>this._str(x)).join(' ')); return null; });
|
|
1036
|
+
G.define('assert', a => { if(!a[0]) throw new SSError('Assertion failed'+(a[1]?': '+a[1]:'')); return null; });
|
|
1037
|
+
G.define('error', a => { throw new SSError(String(a[0]??'Runtime error')); });
|
|
1038
|
+
G.define('input', a => {
|
|
1039
|
+
const prompt = a[0] !== undefined ? this._str(a[0]) : '';
|
|
1040
|
+
if (this._inputQueue && this._inputQueue.length > 0) {
|
|
1041
|
+
const val = this._inputQueue.shift();
|
|
1042
|
+
this.output('» ' + (prompt ? prompt + ' ' : '') + val);
|
|
1043
|
+
return val;
|
|
1044
|
+
}
|
|
1045
|
+
throw new SSError('input() called but no value was provided');
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
run(source) {
|
|
1050
|
+
this.sourceLines = source.split('\n');
|
|
1051
|
+
this._execBlock(this.sourceLines, 0, this.sourceLines.length, this.globals);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
_indent(line) { let i=0; while(i<line.length&&line[i]===' ')i++; return i; }
|
|
1055
|
+
|
|
1056
|
+
_findBlock(lines, startLine, baseIndent) {
|
|
1057
|
+
const block=[]; let i=startLine;
|
|
1058
|
+
while(i<lines.length){
|
|
1059
|
+
const line=lines[i], t=line.trim();
|
|
1060
|
+
if(!t||t.startsWith('//')){ i++; continue; }
|
|
1061
|
+
if(this._indent(line)<=baseIndent) break;
|
|
1062
|
+
block.push(i); i++;
|
|
1063
|
+
}
|
|
1064
|
+
return {block, next:i};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
_execBlock(lines, start, end, env) {
|
|
1068
|
+
let i=start;
|
|
1069
|
+
while(i<end){
|
|
1070
|
+
const raw=lines[i], t=raw.trim();
|
|
1071
|
+
if(!t||t.startsWith('//')){ i++; continue; }
|
|
1072
|
+
const indent=this._indent(raw);
|
|
1073
|
+
const result=this._execLine(t, lines, i, indent, env);
|
|
1074
|
+
if(result instanceof ReturnSignal||result instanceof BreakSignal||result instanceof ContinueSignal) return result;
|
|
1075
|
+
if(result&&result._skip){ i=result._skip; continue; }
|
|
1076
|
+
i++;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
_execLine(line, allLines, lineIdx, indent, env) {
|
|
1081
|
+
try { return this._exec(line, allLines, lineIdx, indent, env); }
|
|
1082
|
+
catch(e) {
|
|
1083
|
+
if(e instanceof SSError){ if(e.ssLine===undefined)e.ssLine=lineIdx; if(!e.snippet)e.snippet=allLines[lineIdx]; throw e; }
|
|
1084
|
+
throw new SSError(e.message, lineIdx, allLines[lineIdx]);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
_exec(line, allLines, lineIdx, indent, env) {
|
|
1089
|
+
// say (shorthand keyword)
|
|
1090
|
+
if(line.startsWith('say ')){ const v=this._eval(line.slice(4).trim(),env); this.output(this._str(v)); return null; }
|
|
1091
|
+
|
|
1092
|
+
// let / const
|
|
1093
|
+
if(line.startsWith('let ')||line.startsWith('const ')){
|
|
1094
|
+
const isConst=line.startsWith('const ');
|
|
1095
|
+
const rest=line.slice(isConst?6:4);
|
|
1096
|
+
const eq=rest.indexOf('='); if(eq===-1) throw new SSError(`Missing = in declaration`);
|
|
1097
|
+
const name=rest.slice(0,eq).trim();
|
|
1098
|
+
const val=this._eval(rest.slice(eq+1).trim(),env);
|
|
1099
|
+
env.define(name,val,isConst); return null;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// set
|
|
1103
|
+
if(line.startsWith('set ')){
|
|
1104
|
+
const rest=line.slice(4);
|
|
1105
|
+
// compound assignment: set x += 1
|
|
1106
|
+
const compMatch=rest.match(/^([a-zA-Z_][\w.]*)\s*(\+|-|\*|\/|%|\*\*|\/\/)=\s*(.+)$/);
|
|
1107
|
+
if(compMatch){
|
|
1108
|
+
const [,target,op,valExpr]=compMatch;
|
|
1109
|
+
const cur=this._getTarget(target,env);
|
|
1110
|
+
const val=this._eval(valExpr.trim(),env);
|
|
1111
|
+
const newVal=this._applyOp(cur,op,val);
|
|
1112
|
+
this._setTarget(target,newVal,env); return null;
|
|
1113
|
+
}
|
|
1114
|
+
const eq=rest.indexOf('='); if(eq===-1) throw new SSError(`Missing = in set`);
|
|
1115
|
+
const target=rest.slice(0,eq).trim(), val=this._eval(rest.slice(eq+1).trim(),env);
|
|
1116
|
+
this._setTarget(target,val,env); return null;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// define function
|
|
1120
|
+
if(line.startsWith('define ')){
|
|
1121
|
+
const sig=line.slice(7).trim().replace(/:$/,'');
|
|
1122
|
+
const po=sig.indexOf('('), pc=sig.lastIndexOf(')');
|
|
1123
|
+
const fname=sig.slice(0,po).trim();
|
|
1124
|
+
const rawParams=sig.slice(po+1,pc).trim();
|
|
1125
|
+
const params=rawParams?rawParams.split(',').map(p=>p.trim()).filter(Boolean):[];
|
|
1126
|
+
const defaults={};
|
|
1127
|
+
params.forEach((p,i)=>{ const eq=p.indexOf('='); if(eq!==-1){ defaults[p.slice(0,eq).trim()]=this._eval(p.slice(eq+1).trim(),env); params[i]=p.slice(0,eq).trim(); }});
|
|
1128
|
+
const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
|
|
1129
|
+
env.define(fname,(args,callEnv)=>{
|
|
1130
|
+
if(this.callDepth>200) throw new SSError('Stack overflow: too many nested calls');
|
|
1131
|
+
this.callDepth++;
|
|
1132
|
+
const fnEnv=new Environment(env);
|
|
1133
|
+
params.forEach((p,i)=>fnEnv.define(p, args[i]!==undefined?args[i]:(defaults[p]!==undefined?defaults[p]:null)));
|
|
1134
|
+
const blockLines=block.map(b=>allLines[b]);
|
|
1135
|
+
const res=this._execBlock(blockLines,0,blockLines.length,fnEnv);
|
|
1136
|
+
this.callDepth--;
|
|
1137
|
+
if(res instanceof ReturnSignal) return res.value;
|
|
1138
|
+
return null;
|
|
1139
|
+
});
|
|
1140
|
+
return {_skip:next};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// struct
|
|
1144
|
+
if(line.startsWith('struct ')){
|
|
1145
|
+
const name=line.slice(7).trim().replace(/:$/,'');
|
|
1146
|
+
const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
|
|
1147
|
+
const defs={};
|
|
1148
|
+
block.forEach(bi=>{ const t=allLines[bi].trim(); if(!t||t.startsWith('//'))return; const eq=t.indexOf('='); if(eq!==-1){ defs[t.slice(0,eq).trim()]=this._eval(t.slice(eq+1).trim(),this.globals); }});
|
|
1149
|
+
this.structs[name]=defs;
|
|
1150
|
+
env.define(name,(args)=>{
|
|
1151
|
+
const inst={__struct:name};
|
|
1152
|
+
Object.entries(defs).forEach(([k,v])=>{ inst[k]=Array.isArray(v)?[...v]:v; });
|
|
1153
|
+
return inst;
|
|
1154
|
+
});
|
|
1155
|
+
return {_skip:next};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// class (with fields + methods)
|
|
1159
|
+
if(line.startsWith('class ')){
|
|
1160
|
+
const name=line.slice(6).trim().replace(/:$/,'');
|
|
1161
|
+
const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
|
|
1162
|
+
const fields={}, methods={};
|
|
1163
|
+
let i2=0;
|
|
1164
|
+
const bLines=block.map(b=>allLines[b]);
|
|
1165
|
+
while(i2<bLines.length){
|
|
1166
|
+
const t=bLines[i2].trim();
|
|
1167
|
+
if(!t||t.startsWith('//')){ i2++; continue; }
|
|
1168
|
+
const bIndent=this._indent(bLines[i2]);
|
|
1169
|
+
if(t.startsWith('define ')){
|
|
1170
|
+
const sig=t.slice(7).replace(/:$/,'');
|
|
1171
|
+
const po=sig.indexOf('('), pc=sig.lastIndexOf(')');
|
|
1172
|
+
const mname=sig.slice(0,po).trim();
|
|
1173
|
+
const rawP=sig.slice(po+1,pc).trim();
|
|
1174
|
+
const params=rawP?rawP.split(',').map(p=>p.trim()).filter(Boolean):[];
|
|
1175
|
+
const defaults2={};
|
|
1176
|
+
params.forEach((p,pi)=>{ const eq=p.indexOf('='); if(eq!==-1){ defaults2[p.slice(0,eq).trim()]=this._eval(p.slice(eq+1).trim(),env); params[pi]=p.slice(0,eq).trim(); }});
|
|
1177
|
+
const {block:mb,next:mn}=this._findBlock(bLines,i2+1,bIndent);
|
|
1178
|
+
methods[mname]={params, defaults:defaults2, body:mb.map(b=>bLines[b])};
|
|
1179
|
+
i2=mn;
|
|
1180
|
+
} else {
|
|
1181
|
+
const eq=t.indexOf('=');
|
|
1182
|
+
if(eq!==-1){ fields[t.slice(0,eq).trim()]=this._eval(t.slice(eq+1).trim(),this.globals); }
|
|
1183
|
+
i2++;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
this.structs[name]={...fields};
|
|
1187
|
+
const classEnv=env;
|
|
1188
|
+
env.define(name,(args)=>{
|
|
1189
|
+
const inst={__struct:name, __class:name};
|
|
1190
|
+
Object.entries(fields).forEach(([k,v])=>{ inst[k]=Array.isArray(v)?[...v]:(typeof v==='object'&&v?Object.assign({},v):v); });
|
|
1191
|
+
// Bind methods
|
|
1192
|
+
Object.entries(methods).forEach(([mname,m])=>{
|
|
1193
|
+
inst[mname]=(callArgs)=>{
|
|
1194
|
+
if(this.callDepth>200) throw new SSError('Stack overflow');
|
|
1195
|
+
this.callDepth++;
|
|
1196
|
+
const fnEnv=new Environment(classEnv);
|
|
1197
|
+
fnEnv.define('self',inst);
|
|
1198
|
+
fnEnv.define('this',inst);
|
|
1199
|
+
m.params.forEach((p,i)=>fnEnv.define(p, callArgs[i]!==undefined?callArgs[i]:(m.defaults[p]!==undefined?m.defaults[p]:null)));
|
|
1200
|
+
const res=this._execBlock(m.body,0,m.body.length,fnEnv);
|
|
1201
|
+
this.callDepth--;
|
|
1202
|
+
if(res instanceof ReturnSignal) return res.value;
|
|
1203
|
+
return null;
|
|
1204
|
+
};
|
|
1205
|
+
});
|
|
1206
|
+
// Call init if exists
|
|
1207
|
+
if(inst.init) inst.init(args);
|
|
1208
|
+
return inst;
|
|
1209
|
+
});
|
|
1210
|
+
return {_skip:next};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// return
|
|
1214
|
+
if(line==='return') return new ReturnSignal(null);
|
|
1215
|
+
if(line.startsWith('return ')) return new ReturnSignal(this._eval(line.slice(7).trim(),env));
|
|
1216
|
+
|
|
1217
|
+
// break / continue
|
|
1218
|
+
if(line==='break') return new BreakSignal();
|
|
1219
|
+
if(line==='continue') return new ContinueSignal();
|
|
1220
|
+
|
|
1221
|
+
// try/catch/finally
|
|
1222
|
+
if(line==='try:'){
|
|
1223
|
+
const {block:tryBlock,next:tryNext}=this._findBlock(allLines,lineIdx+1,indent);
|
|
1224
|
+
let catchVar=null, catchBlock=[], finallyBlock=[], cur=tryNext;
|
|
1225
|
+
if(cur<allLines.length){
|
|
1226
|
+
const ct=allLines[cur].trim();
|
|
1227
|
+
if(ct.startsWith('catch')){
|
|
1228
|
+
const m=ct.match(/^catch\s+(\w+):?$/); catchVar=m?m[1]:'_err';
|
|
1229
|
+
const {block:cb,next:cn}=this._findBlock(allLines,cur+1,this._indent(allLines[cur]));
|
|
1230
|
+
catchBlock=cb; cur=cn;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if(cur<allLines.length&&allLines[cur].trim()==='finally:'){
|
|
1234
|
+
const {block:fb,next:fn}=this._findBlock(allLines,cur+1,this._indent(allLines[cur]));
|
|
1235
|
+
finallyBlock=fb; cur=fn;
|
|
1236
|
+
}
|
|
1237
|
+
let result=null;
|
|
1238
|
+
try {
|
|
1239
|
+
const bLines=tryBlock.map(b=>allLines[b]);
|
|
1240
|
+
result=this._execBlock(bLines,0,bLines.length,new Environment(env));
|
|
1241
|
+
} catch(e) {
|
|
1242
|
+
if(catchBlock.length){
|
|
1243
|
+
const cEnv=new Environment(env);
|
|
1244
|
+
if(catchVar) cEnv.define(catchVar, e.message||String(e));
|
|
1245
|
+
const cLines=catchBlock.map(b=>allLines[b]);
|
|
1246
|
+
result=this._execBlock(cLines,0,cLines.length,cEnv);
|
|
1247
|
+
}
|
|
1248
|
+
} finally {
|
|
1249
|
+
if(finallyBlock.length){
|
|
1250
|
+
const fLines=finallyBlock.map(b=>allLines[b]);
|
|
1251
|
+
this._execBlock(fLines,0,fLines.length,new Environment(env));
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return {_skip:cur};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// if/else if/else
|
|
1258
|
+
if((line.startsWith('if ')&&line.endsWith(':'))){
|
|
1259
|
+
return this._handleIf(line,allLines,lineIdx,indent,env);
|
|
1260
|
+
}
|
|
1261
|
+
if(line.startsWith('else if ')||line==='else:') return null; // handled by if
|
|
1262
|
+
|
|
1263
|
+
// while
|
|
1264
|
+
if(line.startsWith('while ')&&line.endsWith(':')){
|
|
1265
|
+
const cond=line.slice(6,-1).trim();
|
|
1266
|
+
const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
|
|
1267
|
+
let iters=0;
|
|
1268
|
+
while(this._truthy(this._eval(cond,env))){
|
|
1269
|
+
if(++iters>50000) throw new SSError('Infinite loop detected (>50000 iterations)');
|
|
1270
|
+
const bLines=block.map(b=>allLines[b]);
|
|
1271
|
+
const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
|
|
1272
|
+
if(res instanceof BreakSignal) break;
|
|
1273
|
+
if(res instanceof ReturnSignal) return res;
|
|
1274
|
+
}
|
|
1275
|
+
return {_skip:next};
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// repeat N times
|
|
1279
|
+
if(line.startsWith('repeat ')){
|
|
1280
|
+
const m=line.match(/^repeat (.+?) times:?$/);
|
|
1281
|
+
if(m){
|
|
1282
|
+
const count=Math.floor(Number(this._eval(m[1].trim(),env)));
|
|
1283
|
+
if(isNaN(count)) throw new SSError(`repeat count must be a number`);
|
|
1284
|
+
const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
|
|
1285
|
+
for(let i=0;i<count;i++){
|
|
1286
|
+
const bLines=block.map(b=>allLines[b]);
|
|
1287
|
+
const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
|
|
1288
|
+
if(res instanceof BreakSignal) break;
|
|
1289
|
+
if(res instanceof ReturnSignal) return res;
|
|
1290
|
+
}
|
|
1291
|
+
return {_skip:next};
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// count i from X to Y (step Z)?
|
|
1296
|
+
if(line.startsWith('count ')){
|
|
1297
|
+
const m=line.match(/^count (\w+) from (.+?) to (.+?)(?:\s+step\s+(.+?))?:?$/);
|
|
1298
|
+
if(m){
|
|
1299
|
+
const varName=m[1];
|
|
1300
|
+
const from=Number(this._eval(m[2].trim(),env));
|
|
1301
|
+
const to=Number(this._eval(m[3].trim(),env));
|
|
1302
|
+
const step=m[4]?Number(this._eval(m[4].trim(),env)):(from<=to?1:-1);
|
|
1303
|
+
const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
|
|
1304
|
+
for(let i=from;step>0?i<=to:i>=to;i+=step){
|
|
1305
|
+
const loopEnv=new Environment(env); loopEnv.define(varName,i);
|
|
1306
|
+
const bLines=block.map(b=>allLines[b]);
|
|
1307
|
+
const res=this._execBlock(bLines,0,bLines.length,loopEnv);
|
|
1308
|
+
if(res instanceof BreakSignal) break;
|
|
1309
|
+
if(res instanceof ReturnSignal) return res;
|
|
1310
|
+
}
|
|
1311
|
+
return {_skip:next};
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// for each item in list
|
|
1316
|
+
if(line.startsWith('for each ')){
|
|
1317
|
+
const m=line.match(/^for each (\w+) in (.+?):?$/);
|
|
1318
|
+
if(m){
|
|
1319
|
+
const varName=m[1], listVal=this._eval(m[2].trim(),env);
|
|
1320
|
+
if(!Array.isArray(listVal)&&typeof listVal!=='string') throw new SSError(`"${m[2].trim()}" is not iterable`);
|
|
1321
|
+
const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
|
|
1322
|
+
for(const item of listVal){
|
|
1323
|
+
const loopEnv=new Environment(env); loopEnv.define(varName,item);
|
|
1324
|
+
const bLines=block.map(b=>allLines[b]);
|
|
1325
|
+
const res=this._execBlock(bLines,0,bLines.length,loopEnv);
|
|
1326
|
+
if(res instanceof BreakSignal) break;
|
|
1327
|
+
if(res instanceof ReturnSignal) return res;
|
|
1328
|
+
}
|
|
1329
|
+
return {_skip:next};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// bare expression / function call
|
|
1334
|
+
this._eval(line,env);
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
_handleIf(line, allLines, lineIdx, indent, env) {
|
|
1339
|
+
const cond=line.slice(3,-1).trim();
|
|
1340
|
+
const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
|
|
1341
|
+
if(this._truthy(this._eval(cond,env))){
|
|
1342
|
+
const bLines=block.map(b=>allLines[b]);
|
|
1343
|
+
const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
|
|
1344
|
+
if(res instanceof ReturnSignal||res instanceof BreakSignal) return res;
|
|
1345
|
+
// skip else/else if chains
|
|
1346
|
+
let skip=next;
|
|
1347
|
+
while(skip<allLines.length){
|
|
1348
|
+
const t=allLines[skip].trim();
|
|
1349
|
+
if(t.startsWith('else')){
|
|
1350
|
+
const {next:n2}=this._findBlock(allLines,skip+1,this._indent(allLines[skip]));
|
|
1351
|
+
skip=n2;
|
|
1352
|
+
} else break;
|
|
1353
|
+
}
|
|
1354
|
+
return {_skip:skip};
|
|
1355
|
+
} else {
|
|
1356
|
+
let cur=next;
|
|
1357
|
+
while(cur<allLines.length){
|
|
1358
|
+
const t=allLines[cur].trim();
|
|
1359
|
+
const curIndent=this._indent(allLines[cur]);
|
|
1360
|
+
if(t.startsWith('else if ')&&t.endsWith(':')){
|
|
1361
|
+
const cond2=t.slice(8,-1).trim();
|
|
1362
|
+
const {block:b2,next:n2}=this._findBlock(allLines,cur+1,curIndent);
|
|
1363
|
+
if(this._truthy(this._eval(cond2,env))){
|
|
1364
|
+
const bLines=b2.map(b=>allLines[b]);
|
|
1365
|
+
const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
|
|
1366
|
+
if(res instanceof ReturnSignal||res instanceof BreakSignal) return res;
|
|
1367
|
+
// skip remaining else chains
|
|
1368
|
+
let skip2=n2;
|
|
1369
|
+
while(skip2<allLines.length){ const tt=allLines[skip2].trim(); if(tt.startsWith('else')){ const {next:n3}=this._findBlock(allLines,skip2+1,this._indent(allLines[skip2])); skip2=n3; } else break; }
|
|
1370
|
+
return {_skip:skip2};
|
|
1371
|
+
}
|
|
1372
|
+
cur=n2;
|
|
1373
|
+
} else if(t==='else:'){
|
|
1374
|
+
const {block:b3,next:n3}=this._findBlock(allLines,cur+1,curIndent);
|
|
1375
|
+
const bLines=b3.map(b=>allLines[b]);
|
|
1376
|
+
const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
|
|
1377
|
+
if(res instanceof ReturnSignal||res instanceof BreakSignal) return res;
|
|
1378
|
+
return {_skip:n3};
|
|
1379
|
+
} else break;
|
|
1380
|
+
}
|
|
1381
|
+
return {_skip:cur};
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
_getTarget(target, env) {
|
|
1386
|
+
if(target.includes('.')){
|
|
1387
|
+
const parts=target.split('.');
|
|
1388
|
+
let obj=env.get(parts[0]);
|
|
1389
|
+
for(let i=1;i<parts.length-1;i++) obj=obj[parts[i]];
|
|
1390
|
+
return obj[parts[parts.length-1]];
|
|
1391
|
+
}
|
|
1392
|
+
if(target.includes('[')){ const m=target.match(/^(\w+)\[(.+)\]$/); if(m){ const arr=env.get(m[1]); return arr[this._eval(m[2],env)]; } }
|
|
1393
|
+
return env.get(target);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
_setTarget(target, value, env) {
|
|
1397
|
+
if(target.includes('.')){
|
|
1398
|
+
const parts=target.split('.');
|
|
1399
|
+
let obj=env.get(parts[0]);
|
|
1400
|
+
for(let i=1;i<parts.length-1;i++) obj=obj[parts[i]];
|
|
1401
|
+
obj[parts[parts.length-1]]=value; return;
|
|
1402
|
+
}
|
|
1403
|
+
if(target.includes('[')){ const m=target.match(/^(\w+)\[(.+)\]$/); if(m){ const arr=env.get(m[1]); arr[this._eval(m[2],env)]=value; return; } }
|
|
1404
|
+
env.set(target, value);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
_applyOp(a, op, b) {
|
|
1408
|
+
switch(op){
|
|
1409
|
+
case '+': return a+b; case '-': return a-b;
|
|
1410
|
+
case '*': return a*b; case '/': return a/b;
|
|
1411
|
+
case '%': return a%b; case '**': return Math.pow(a,b);
|
|
1412
|
+
case '//': return Math.floor(a/b);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
_truthy(v) { return v!==null&&v!==undefined&&v!==false&&v!==0&&v!==''; }
|
|
1417
|
+
|
|
1418
|
+
_eval(expr, env) {
|
|
1419
|
+
expr=expr.trim();
|
|
1420
|
+
if(!expr) return null;
|
|
1421
|
+
if(expr==='true') return true;
|
|
1422
|
+
if(expr==='false') return false;
|
|
1423
|
+
if(expr==='null'||expr==='nothing') return null;
|
|
1424
|
+
// String
|
|
1425
|
+
if((expr[0]==='"'&&expr[expr.length-1]==='"')||(expr[0]==="'"&&expr[expr.length-1]==="'"))
|
|
1426
|
+
return this._parseString(expr.slice(1,-1), env);
|
|
1427
|
+
// Multiline string (triple quote) - simplified
|
|
1428
|
+
// Number
|
|
1429
|
+
if(/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr);
|
|
1430
|
+
// List
|
|
1431
|
+
if(expr[0]==='['&&expr[expr.length-1]===']'){
|
|
1432
|
+
const inner=expr.slice(1,-1).trim();
|
|
1433
|
+
if(!inner) return [];
|
|
1434
|
+
return this._splitArgs(inner).map(a=>this._eval(a,env));
|
|
1435
|
+
}
|
|
1436
|
+
// Parens
|
|
1437
|
+
if(expr[0]==='('&&expr[expr.length-1]===')') return this._eval(expr.slice(1,-1),env);
|
|
1438
|
+
// Ternary: expr if cond else expr
|
|
1439
|
+
const ternIdx=this._findOp(expr,' if ');
|
|
1440
|
+
if(ternIdx!==-1){
|
|
1441
|
+
const elseIdx=this._findOp(expr,' else ');
|
|
1442
|
+
if(elseIdx>ternIdx){
|
|
1443
|
+
const val=expr.slice(0,ternIdx).trim();
|
|
1444
|
+
const cond=expr.slice(ternIdx+4,elseIdx).trim();
|
|
1445
|
+
const alt=expr.slice(elseIdx+6).trim();
|
|
1446
|
+
return this._truthy(this._eval(cond,env))?this._eval(val,env):this._eval(alt,env);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
// Binary ops in precedence order
|
|
1450
|
+
for(const op of [' or ',' and ']){
|
|
1451
|
+
const i=this._findOp(expr,op);
|
|
1452
|
+
if(i!==-1){
|
|
1453
|
+
if(op===' or ') return this._truthy(this._eval(expr.slice(0,i),env))||this._truthy(this._eval(expr.slice(i+4),env));
|
|
1454
|
+
if(op===' and ') return this._truthy(this._eval(expr.slice(0,i),env))&&this._truthy(this._eval(expr.slice(i+5),env));
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if(expr.startsWith('not ')) return !this._truthy(this._eval(expr.slice(4),env));
|
|
1458
|
+
for(const op of ['>=','<=','!=','==','>','<']){
|
|
1459
|
+
const i=this._findOp(expr,op);
|
|
1460
|
+
if(i!==-1){
|
|
1461
|
+
const l=this._eval(expr.slice(0,i).trim(),env),r=this._eval(expr.slice(i+op.length).trim(),env);
|
|
1462
|
+
switch(op){ case'==':return l==r; case'!=':return l!=r; case'>':return l>r; case'<':return l<r; case'>=':return l>=r; case'<=':return l<=r; }
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
// Arithmetic
|
|
1466
|
+
const addI=this._findOp(expr,'+');
|
|
1467
|
+
if(addI!==-1){ return this._eval(expr.slice(0,addI).trim(),env)+this._eval(expr.slice(addI+1).trim(),env); }
|
|
1468
|
+
const subI=this._findBinaryMinus(expr);
|
|
1469
|
+
if(subI!==-1){ return this._eval(expr.slice(0,subI).trim(),env)-this._eval(expr.slice(subI+1).trim(),env); }
|
|
1470
|
+
const modI=this._findOp(expr,'%');
|
|
1471
|
+
if(modI!==-1){ return this._eval(expr.slice(0,modI).trim(),env)%this._eval(expr.slice(modI+1).trim(),env); }
|
|
1472
|
+
const powI=this._findOp(expr,'**');
|
|
1473
|
+
if(powI!==-1){ return Math.pow(this._eval(expr.slice(0,powI).trim(),env),this._eval(expr.slice(powI+2).trim(),env)); }
|
|
1474
|
+
const fdivI=this._findOp(expr,'//');
|
|
1475
|
+
if(fdivI!==-1){ return Math.floor(this._eval(expr.slice(0,fdivI).trim(),env)/this._eval(expr.slice(fdivI+2).trim(),env)); }
|
|
1476
|
+
const mulI=this._findOp(expr,'*');
|
|
1477
|
+
if(mulI!==-1){ return this._eval(expr.slice(0,mulI).trim(),env)*this._eval(expr.slice(mulI+1).trim(),env); }
|
|
1478
|
+
const divI=this._findOp(expr,'/');
|
|
1479
|
+
if(divI!==-1){ const d=this._eval(expr.slice(divI+1).trim(),env); return this._eval(expr.slice(0,divI).trim(),env)/d; }
|
|
1480
|
+
// Unary minus
|
|
1481
|
+
if(expr[0]==='-') return -this._eval(expr.slice(1),env);
|
|
1482
|
+
// Method call: obj.method(args)
|
|
1483
|
+
const methodM=expr.match(/^([a-zA-Z_]\w*(?:\[.+?\])?(?:\.[a-zA-Z_]\w*(?:\[.+?\])?)*)\.([a-zA-Z_]\w*)\((.*)\)$/s);
|
|
1484
|
+
if(methodM){
|
|
1485
|
+
const objExpr=methodM[1], mname=methodM[2], argsRaw=methodM[3].trim();
|
|
1486
|
+
const obj=this._eval(objExpr,env);
|
|
1487
|
+
const args=argsRaw?this._splitArgs(argsRaw).map(a=>this._eval(a.trim(),env)):[];
|
|
1488
|
+
if(typeof obj==='object'&&obj!==null&&typeof obj[mname]==='function'){
|
|
1489
|
+
return obj[mname](args);
|
|
1490
|
+
}
|
|
1491
|
+
// Built-in string/array methods via bridge
|
|
1492
|
+
if(typeof obj==='string'){
|
|
1493
|
+
if(mname==='upper') return obj.toUpperCase();
|
|
1494
|
+
if(mname==='lower') return obj.toLowerCase();
|
|
1495
|
+
if(mname==='trim') return obj.trim();
|
|
1496
|
+
if(mname==='length') return obj.length;
|
|
1497
|
+
if(mname==='split') return obj.split(args[0]??'');
|
|
1498
|
+
if(mname==='contains') return obj.includes(String(args[0]));
|
|
1499
|
+
if(mname==='replace') return obj.split(String(args[0])).join(String(args[1]));
|
|
1500
|
+
if(mname==='startsWith') return obj.startsWith(String(args[0]));
|
|
1501
|
+
if(mname==='endsWith') return obj.endsWith(String(args[0]));
|
|
1502
|
+
}
|
|
1503
|
+
if(Array.isArray(obj)){
|
|
1504
|
+
if(mname==='push'){ obj.push(args[0]); return null; }
|
|
1505
|
+
if(mname==='pop') return obj.pop()??null;
|
|
1506
|
+
if(mname==='length') return obj.length;
|
|
1507
|
+
if(mname==='sort') return [...obj].sort();
|
|
1508
|
+
if(mname==='reverse') return [...obj].reverse();
|
|
1509
|
+
if(mname==='join') return obj.map(x=>this._str(x)).join(args[0]??'');
|
|
1510
|
+
if(mname==='contains') return obj.includes(args[0]);
|
|
1511
|
+
}
|
|
1512
|
+
throw new SSError(`"${mname}" is not a method of ${this._str(obj)}`);
|
|
1513
|
+
}
|
|
1514
|
+
// Function call
|
|
1515
|
+
const callM=expr.match(/^([a-zA-Z_][\w]*)\((.*)\)$/s);
|
|
1516
|
+
if(callM){
|
|
1517
|
+
const fname=callM[1], argsRaw=callM[2].trim();
|
|
1518
|
+
const args=argsRaw?this._splitArgs(argsRaw).map(a=>this._eval(a.trim(),env)):[];
|
|
1519
|
+
const fn=env.get(fname);
|
|
1520
|
+
if(typeof fn!=='function') throw new SSError(`"${fname}" is not a function`);
|
|
1521
|
+
this.callDepth++;
|
|
1522
|
+
if(this.callDepth>200){ this.callDepth--; throw new SSError('Stack overflow'); }
|
|
1523
|
+
const res=fn(args,env);
|
|
1524
|
+
this.callDepth--;
|
|
1525
|
+
return res;
|
|
1526
|
+
}
|
|
1527
|
+
// Index access
|
|
1528
|
+
const bracketM=expr.match(/^(.+?)\[(.+)\]$/);
|
|
1529
|
+
if(bracketM){
|
|
1530
|
+
const obj=this._eval(bracketM[1].trim(),env);
|
|
1531
|
+
const idx=this._eval(bracketM[2].trim(),env);
|
|
1532
|
+
if(Array.isArray(obj)) return obj[idx]??null;
|
|
1533
|
+
if(typeof obj==='string') return obj[idx]??null;
|
|
1534
|
+
if(typeof obj==='object'&&obj!==null) return obj[idx]??null;
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
// Dot access
|
|
1538
|
+
if(expr.includes('.')&&!/^\d/.test(expr)){
|
|
1539
|
+
const dot=expr.lastIndexOf('.');
|
|
1540
|
+
const obj=this._eval(expr.slice(0,dot),env);
|
|
1541
|
+
const key=expr.slice(dot+1);
|
|
1542
|
+
if(typeof obj==='object'&&obj!==null) return obj[key]??null;
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
// Variable
|
|
1546
|
+
return env.get(expr);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
_parseString(s, env){
|
|
1550
|
+
s = s.replace(/\\n/g,'\n').replace(/\\t/g,'\t').replace(/\\\\/g,'\\').replace(/\\"/g,'"').replace(/\\'/g,"'");
|
|
1551
|
+
if(!env) return s;
|
|
1552
|
+
let result='', i=0;
|
|
1553
|
+
while(i<s.length){
|
|
1554
|
+
if(s[i]==='{'){
|
|
1555
|
+
let depth=1, j=i+1;
|
|
1556
|
+
while(j<s.length&&depth>0){ if(s[j]==='{')depth++; else if(s[j]==='}')depth--; j++; }
|
|
1557
|
+
const expr=s.slice(i+1,j-1);
|
|
1558
|
+
try{ result+=this._str(this._eval(expr.trim(),env)); } catch(e){ result+='{'+expr+'}'; }
|
|
1559
|
+
i=j;
|
|
1560
|
+
} else { result+=s[i++]; }
|
|
1561
|
+
}
|
|
1562
|
+
return result;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
_findOp(expr, op) {
|
|
1566
|
+
let depth=0, inStr=false, sc='';
|
|
1567
|
+
for(let i=0;i<expr.length;i++){
|
|
1568
|
+
const c=expr[i];
|
|
1569
|
+
if(inStr){if(c===sc)inStr=false;continue;}
|
|
1570
|
+
if(c==='"'||c==="'"){inStr=true;sc=c;continue;}
|
|
1571
|
+
if(c==='('||c==='[')depth++;
|
|
1572
|
+
else if(c===')'||c===']')depth--;
|
|
1573
|
+
else if(depth===0&&expr.slice(i,i+op.length)===op) return i;
|
|
1574
|
+
}
|
|
1575
|
+
return -1;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
_findBinaryMinus(expr){
|
|
1579
|
+
let depth=0,inStr=false,sc='';
|
|
1580
|
+
for(let i=expr.length-1;i>0;i--){
|
|
1581
|
+
const c=expr[i];
|
|
1582
|
+
if(c==='"'||c==="'")inStr=!inStr;
|
|
1583
|
+
if(inStr)continue;
|
|
1584
|
+
if(c===')'||c===']')depth++;
|
|
1585
|
+
else if(c==='('||c==='[')depth--;
|
|
1586
|
+
else if(depth===0&&c==='-'){
|
|
1587
|
+
let prev=i-1;
|
|
1588
|
+
while(prev>=0&&expr[prev]===' ')prev--;
|
|
1589
|
+
if(prev>=0&&/[\w\d\)"'\]]/.test(expr[prev])) return i;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return -1;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
_splitArgs(str){
|
|
1596
|
+
const args=[]; let depth=0,inStr=false,sc='',cur='';
|
|
1597
|
+
for(let i=0;i<str.length;i++){
|
|
1598
|
+
const c=str[i];
|
|
1599
|
+
if(inStr){cur+=c;if(c===sc)inStr=false;continue;}
|
|
1600
|
+
if(c==='"'||c==="'"){inStr=true;sc=c;cur+=c;continue;}
|
|
1601
|
+
if(c==='('||c==='['){depth++;cur+=c;continue;}
|
|
1602
|
+
if(c===')'||c===']'){depth--;cur+=c;continue;}
|
|
1603
|
+
if(c===','&&depth===0){args.push(cur.trim());cur='';continue;}
|
|
1604
|
+
cur+=c;
|
|
1605
|
+
}
|
|
1606
|
+
if(cur.trim())args.push(cur.trim());
|
|
1607
|
+
return args;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
_str(val){
|
|
1611
|
+
if(val===null||val===undefined) return 'nothing';
|
|
1612
|
+
if(val===true) return 'true';
|
|
1613
|
+
if(val===false) return 'false';
|
|
1614
|
+
if(Array.isArray(val)) return '['+val.map(v=>this._str(v)).join(', ')+']';
|
|
1615
|
+
if(typeof val==='object'){
|
|
1616
|
+
const n=val.__struct||'struct';
|
|
1617
|
+
const fields=Object.entries(val).filter(([k])=>k!=='__struct').map(([k,v])=>`${k}: ${this._str(v)}`).join(', ');
|
|
1618
|
+
return `${n}{ ${fields} }`;
|
|
1619
|
+
}
|
|
1620
|
+
return String(val);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// ============================================================
|
|
1625
|
+
// EXAMPLES
|
|
1626
|
+
// ============================================================
|
|
1627
|
+
const EXAMPLES = {
|
|
1628
|
+
hello:`// Hello, World! in StructScript v1.2
|
|
1629
|
+
let name = "World"
|
|
1630
|
+
say "Hello, {name}!"
|
|
1631
|
+
say "Welcome to StructScript v1.2"
|
|
1632
|
+
|
|
1633
|
+
// String interpolation — use {expr} inside any string
|
|
1634
|
+
let version = 1.2
|
|
1635
|
+
let year = 2025
|
|
1636
|
+
say "StructScript v{version} — built in {year}"
|
|
1637
|
+
|
|
1638
|
+
let a = 6
|
|
1639
|
+
let b = 7
|
|
1640
|
+
say "The answer is {a * b}"`,
|
|
1641
|
+
|
|
1642
|
+
interpolation:`// String Interpolation — "Hello {name}!"
|
|
1643
|
+
// Any expression works inside { }
|
|
1644
|
+
|
|
1645
|
+
let name = "Alex"
|
|
1646
|
+
let age = 24
|
|
1647
|
+
let score = 97.5
|
|
1648
|
+
|
|
1649
|
+
say "Hello, {name}! You are {age} years old."
|
|
1650
|
+
say "Score: {score}%"
|
|
1651
|
+
|
|
1652
|
+
// Expressions inside braces
|
|
1653
|
+
let x = 10
|
|
1654
|
+
let y = 3
|
|
1655
|
+
say "{x} + {y} = {x + y}"
|
|
1656
|
+
say "{x} * {y} = {x * y}"
|
|
1657
|
+
say "Is x > y? {x > y}"
|
|
1658
|
+
|
|
1659
|
+
// Works with function calls too
|
|
1660
|
+
let items = ["apple", "banana", "cherry"]
|
|
1661
|
+
say "You have {length(items)} items."
|
|
1662
|
+
say "First: {items[0]}, Last: {items[length(items) - 1]}"
|
|
1663
|
+
|
|
1664
|
+
// Nested values
|
|
1665
|
+
let pi = 3.14159
|
|
1666
|
+
say "Pi rounded: {round(pi)}"
|
|
1667
|
+
say "Pi floored: {floor(pi)}"`,
|
|
1668
|
+
|
|
1669
|
+
classes:`// Classes — objects with fields AND methods
|
|
1670
|
+
|
|
1671
|
+
class Animal:
|
|
1672
|
+
name = "Unknown"
|
|
1673
|
+
sound = "..."
|
|
1674
|
+
legs = 4
|
|
1675
|
+
|
|
1676
|
+
define speak():
|
|
1677
|
+
say "{self.name} says {self.sound}!"
|
|
1678
|
+
|
|
1679
|
+
define describe():
|
|
1680
|
+
say "{self.name} has {self.legs} legs."
|
|
1681
|
+
|
|
1682
|
+
define init(n, s):
|
|
1683
|
+
set self.name = n
|
|
1684
|
+
set self.sound = s
|
|
1685
|
+
|
|
1686
|
+
class Counter:
|
|
1687
|
+
value = 0
|
|
1688
|
+
step = 1
|
|
1689
|
+
|
|
1690
|
+
define increment():
|
|
1691
|
+
set self.value = self.value + self.step
|
|
1692
|
+
|
|
1693
|
+
define reset():
|
|
1694
|
+
set self.value = 0
|
|
1695
|
+
|
|
1696
|
+
define get():
|
|
1697
|
+
return self.value
|
|
1698
|
+
|
|
1699
|
+
// Create instances
|
|
1700
|
+
let dog = Animal()
|
|
1701
|
+
dog.init("Rex", "Woof")
|
|
1702
|
+
dog.speak()
|
|
1703
|
+
dog.describe()
|
|
1704
|
+
|
|
1705
|
+
let cat = Animal()
|
|
1706
|
+
cat.init("Whiskers", "Meow")
|
|
1707
|
+
cat.speak()
|
|
1708
|
+
|
|
1709
|
+
// Counter class
|
|
1710
|
+
let c = Counter()
|
|
1711
|
+
set c.step = 5
|
|
1712
|
+
c.increment()
|
|
1713
|
+
c.increment()
|
|
1714
|
+
c.increment()
|
|
1715
|
+
say "Counter: {c.get()}"
|
|
1716
|
+
c.reset()
|
|
1717
|
+
say "After reset: {c.get()}"`,
|
|
1718
|
+
|
|
1719
|
+
variables:`// Variables, constants, and types
|
|
1720
|
+
let name = "Alex"
|
|
1721
|
+
let age = 24
|
|
1722
|
+
let height = 5.9
|
|
1723
|
+
let active = true
|
|
1724
|
+
const MAX_SCORE = 100
|
|
1725
|
+
const PI = 3.14159
|
|
1726
|
+
|
|
1727
|
+
say "Name: " + name
|
|
1728
|
+
say "Age: " + str(age)
|
|
1729
|
+
say "Height: " + str(height)
|
|
1730
|
+
say "Active: " + str(active)
|
|
1731
|
+
say "Max: " + str(MAX_SCORE)
|
|
1732
|
+
|
|
1733
|
+
// Compound assignment
|
|
1734
|
+
let x = 10
|
|
1735
|
+
set x += 5
|
|
1736
|
+
say "x after += 5: " + str(x)
|
|
1737
|
+
set x *= 2
|
|
1738
|
+
say "x after *= 2: " + str(x)
|
|
1739
|
+
|
|
1740
|
+
// Type checking
|
|
1741
|
+
say "type of age: " + type(age)
|
|
1742
|
+
say "type of name: " + type(name)
|
|
1743
|
+
say "type of active: " + type(active)`,
|
|
1744
|
+
|
|
1745
|
+
strings:`// String operations
|
|
1746
|
+
let s = "Hello, StructScript!"
|
|
1747
|
+
|
|
1748
|
+
say upper(s)
|
|
1749
|
+
say lower(s)
|
|
1750
|
+
say length(s)
|
|
1751
|
+
say contains(s, "Script")
|
|
1752
|
+
say replace(s, "Hello", "Goodbye")
|
|
1753
|
+
say slice(s, 0, 5)
|
|
1754
|
+
|
|
1755
|
+
// String repeat
|
|
1756
|
+
say repeat("ha", 3)
|
|
1757
|
+
|
|
1758
|
+
// Format
|
|
1759
|
+
say format("I am {} years old and {} cm tall", 25, 180)
|
|
1760
|
+
|
|
1761
|
+
// Split and join
|
|
1762
|
+
let csv = "apple,banana,cherry"
|
|
1763
|
+
let fruits = split(csv, ",")
|
|
1764
|
+
say fruits
|
|
1765
|
+
say join(fruits, " | ")
|
|
1766
|
+
|
|
1767
|
+
// Check
|
|
1768
|
+
say startsWith("StructScript", "Struct")
|
|
1769
|
+
say endsWith("main.ss", ".ss")`,
|
|
1770
|
+
|
|
1771
|
+
loops:`// All loop types in StructScript
|
|
1772
|
+
|
|
1773
|
+
// Count loop with step
|
|
1774
|
+
say "Even numbers 0–10:"
|
|
1775
|
+
count i from 0 to 10 step 2:
|
|
1776
|
+
say i
|
|
1777
|
+
|
|
1778
|
+
// Countdown
|
|
1779
|
+
say "Countdown:"
|
|
1780
|
+
count i from 5 to 1 step -1:
|
|
1781
|
+
say i
|
|
1782
|
+
say "Blast off!"
|
|
1783
|
+
|
|
1784
|
+
// While with break
|
|
1785
|
+
say "While with break:"
|
|
1786
|
+
let n = 1
|
|
1787
|
+
while n <= 1000:
|
|
1788
|
+
if n > 16:
|
|
1789
|
+
break
|
|
1790
|
+
say n
|
|
1791
|
+
set n = n * 2
|
|
1792
|
+
|
|
1793
|
+
// For each
|
|
1794
|
+
let langs = ["Python", "JavaScript", "StructScript"]
|
|
1795
|
+
say "Languages:"
|
|
1796
|
+
for each lang in langs:
|
|
1797
|
+
say " - " + lang
|
|
1798
|
+
|
|
1799
|
+
// Range
|
|
1800
|
+
say "Range:"
|
|
1801
|
+
for each i in range(5):
|
|
1802
|
+
say i`,
|
|
1803
|
+
|
|
1804
|
+
functions:`// Functions with defaults, recursion, and higher-order
|
|
1805
|
+
|
|
1806
|
+
define greet(name, greeting = "Hello"):
|
|
1807
|
+
say greeting + ", " + name + "!"
|
|
1808
|
+
|
|
1809
|
+
define clamp(val, lo, hi):
|
|
1810
|
+
if val < lo:
|
|
1811
|
+
return lo
|
|
1812
|
+
if val > hi:
|
|
1813
|
+
return hi
|
|
1814
|
+
return val
|
|
1815
|
+
|
|
1816
|
+
define factorial(n):
|
|
1817
|
+
if n <= 1:
|
|
1818
|
+
return 1
|
|
1819
|
+
return n * factorial(n - 1)
|
|
1820
|
+
|
|
1821
|
+
define isPrime(n):
|
|
1822
|
+
if n < 2:
|
|
1823
|
+
return false
|
|
1824
|
+
count i from 2 to n - 1:
|
|
1825
|
+
if n % i == 0:
|
|
1826
|
+
return false
|
|
1827
|
+
return true
|
|
1828
|
+
|
|
1829
|
+
greet("World")
|
|
1830
|
+
greet("StructScript", "Welcome to")
|
|
1831
|
+
|
|
1832
|
+
say "clamp(15, 0, 10) = " + str(clamp(15, 0, 10))
|
|
1833
|
+
say "5! = " + str(factorial(5))
|
|
1834
|
+
say "7 is prime: " + str(isPrime(7))
|
|
1835
|
+
say "9 is prime: " + str(isPrime(9))
|
|
1836
|
+
|
|
1837
|
+
// Primes up to 20
|
|
1838
|
+
say "Primes up to 20:"
|
|
1839
|
+
count i from 2 to 20:
|
|
1840
|
+
if isPrime(i):
|
|
1841
|
+
say i`,
|
|
1842
|
+
|
|
1843
|
+
lists:`// Lists — the workhorse of StructScript
|
|
1844
|
+
|
|
1845
|
+
let nums = [5, 3, 8, 1, 9, 2, 7, 4, 6]
|
|
1846
|
+
say "Original: " + str(nums)
|
|
1847
|
+
say "Sorted: " + str(sort(nums))
|
|
1848
|
+
say "Reversed: " + str(reverse(nums))
|
|
1849
|
+
say "Sum: " + str(sum(nums))
|
|
1850
|
+
say "Average: " + str(avg(nums))
|
|
1851
|
+
say "Max: " + str(max(5, 3, 8, 1, 9))
|
|
1852
|
+
say "Min: " + str(min(5, 3, 8, 1, 9))
|
|
1853
|
+
|
|
1854
|
+
// Push/pop
|
|
1855
|
+
let stack = []
|
|
1856
|
+
push(stack, "first")
|
|
1857
|
+
push(stack, "second")
|
|
1858
|
+
push(stack, "third")
|
|
1859
|
+
say "Stack: " + str(stack)
|
|
1860
|
+
say "Popped: " + pop(stack)
|
|
1861
|
+
say "Stack after pop: " + str(stack)
|
|
1862
|
+
|
|
1863
|
+
// Slice
|
|
1864
|
+
let letters = ["a","b","c","d","e"]
|
|
1865
|
+
say "Slice [1..3]: " + str(slice(letters, 1, 4))
|
|
1866
|
+
|
|
1867
|
+
// Unique
|
|
1868
|
+
let dupes = [1, 2, 2, 3, 3, 3, 4]
|
|
1869
|
+
say "Unique: " + str(unique(dupes))
|
|
1870
|
+
|
|
1871
|
+
// Length
|
|
1872
|
+
say "Length: " + str(length(nums))`,
|
|
1873
|
+
|
|
1874
|
+
structs:`// Structs — custom data types
|
|
1875
|
+
|
|
1876
|
+
struct Vector:
|
|
1877
|
+
x = 0
|
|
1878
|
+
y = 0
|
|
1879
|
+
|
|
1880
|
+
struct Player:
|
|
1881
|
+
name = "Unknown"
|
|
1882
|
+
health = 100
|
|
1883
|
+
score = 0
|
|
1884
|
+
alive = true
|
|
1885
|
+
|
|
1886
|
+
define magnitude(v):
|
|
1887
|
+
return sqrt(v.x * v.x + v.y * v.y)
|
|
1888
|
+
|
|
1889
|
+
define playerStatus(p):
|
|
1890
|
+
if not p.alive:
|
|
1891
|
+
say p.name + " is defeated."
|
|
1892
|
+
return nothing
|
|
1893
|
+
say p.name + " — HP: " + str(p.health) + " | Score: " + str(p.score)
|
|
1894
|
+
|
|
1895
|
+
let v1 = Vector()
|
|
1896
|
+
set v1.x = 3
|
|
1897
|
+
set v1.y = 4
|
|
1898
|
+
say "Vector: " + str(v1.x) + ", " + str(v1.y)
|
|
1899
|
+
say "Magnitude: " + str(magnitude(v1))
|
|
1900
|
+
|
|
1901
|
+
let hero = Player()
|
|
1902
|
+
set hero.name = "Aria"
|
|
1903
|
+
set hero.score = 2400
|
|
1904
|
+
|
|
1905
|
+
let enemy = Player()
|
|
1906
|
+
set enemy.name = "Shadow"
|
|
1907
|
+
set enemy.health = 60
|
|
1908
|
+
|
|
1909
|
+
playerStatus(hero)
|
|
1910
|
+
playerStatus(enemy)
|
|
1911
|
+
|
|
1912
|
+
set hero.health -= 30
|
|
1913
|
+
playerStatus(hero)`,
|
|
1914
|
+
|
|
1915
|
+
errors:`// Error handling with try/catch/finally
|
|
1916
|
+
|
|
1917
|
+
// Basic error handling
|
|
1918
|
+
try:
|
|
1919
|
+
let result = 10 / 0
|
|
1920
|
+
say "Result: " + str(result)
|
|
1921
|
+
catch err:
|
|
1922
|
+
say "Caught division issue: " + err
|
|
1923
|
+
|
|
1924
|
+
// Catching a bad function call
|
|
1925
|
+
define divide(a, b):
|
|
1926
|
+
if b == 0:
|
|
1927
|
+
error("Cannot divide by zero")
|
|
1928
|
+
return a / b
|
|
1929
|
+
|
|
1930
|
+
try:
|
|
1931
|
+
say str(divide(10, 2))
|
|
1932
|
+
say str(divide(5, 0))
|
|
1933
|
+
catch e:
|
|
1934
|
+
say "Error: " + e
|
|
1935
|
+
|
|
1936
|
+
// Finally always runs
|
|
1937
|
+
say "--- Finally demo ---"
|
|
1938
|
+
try:
|
|
1939
|
+
error("Something went wrong")
|
|
1940
|
+
catch e:
|
|
1941
|
+
say "Handled: " + e
|
|
1942
|
+
finally:
|
|
1943
|
+
say "Cleanup complete."
|
|
1944
|
+
|
|
1945
|
+
// Assert
|
|
1946
|
+
try:
|
|
1947
|
+
let age = -5
|
|
1948
|
+
assert(age >= 0, "Age must be non-negative")
|
|
1949
|
+
catch e:
|
|
1950
|
+
say "Assertion: " + e`,
|
|
1951
|
+
|
|
1952
|
+
fibonacci:`// Fibonacci — iterative and recursive
|
|
1953
|
+
|
|
1954
|
+
define fibRecursive(n):
|
|
1955
|
+
if n <= 1:
|
|
1956
|
+
return n
|
|
1957
|
+
return fibRecursive(n - 1) + fibRecursive(n - 2)
|
|
1958
|
+
|
|
1959
|
+
define fibIterative(n):
|
|
1960
|
+
if n <= 1:
|
|
1961
|
+
return n
|
|
1962
|
+
let a = 0
|
|
1963
|
+
let b = 1
|
|
1964
|
+
count i from 2 to n:
|
|
1965
|
+
let temp = a + b
|
|
1966
|
+
set a = b
|
|
1967
|
+
set b = temp
|
|
1968
|
+
return b
|
|
1969
|
+
|
|
1970
|
+
say "Recursive (first 8):"
|
|
1971
|
+
count i from 0 to 7:
|
|
1972
|
+
say " fib(" + str(i) + ") = " + str(fibRecursive(i))
|
|
1973
|
+
|
|
1974
|
+
say "Iterative (first 15):"
|
|
1975
|
+
let fibs = []
|
|
1976
|
+
count i from 0 to 14:
|
|
1977
|
+
push(fibs, fibIterative(i))
|
|
1978
|
+
say fibs`,
|
|
1979
|
+
|
|
1980
|
+
webpage:`// StructScript Web — build a real webpage!
|
|
1981
|
+
|
|
1982
|
+
page "My Page":
|
|
1983
|
+
style background "#0d1f1e"
|
|
1984
|
+
style fontFamily "Manrope, sans-serif"
|
|
1985
|
+
style padding "40px"
|
|
1986
|
+
|
|
1987
|
+
add div "hero":
|
|
1988
|
+
style textAlign "center"
|
|
1989
|
+
style padding "60px 20px"
|
|
1990
|
+
|
|
1991
|
+
add h1 "title":
|
|
1992
|
+
text "Hello from StructScript!"
|
|
1993
|
+
style color "#b8f000"
|
|
1994
|
+
style fontSize "48px"
|
|
1995
|
+
style fontWeight "800"
|
|
1996
|
+
style marginBottom "16px"
|
|
1997
|
+
animate fadeIn 0.6
|
|
1998
|
+
|
|
1999
|
+
add p "subtitle":
|
|
2000
|
+
text "Building web pages with structured scripting"
|
|
2001
|
+
style color "#8ab8b4"
|
|
2002
|
+
style fontSize "18px"
|
|
2003
|
+
style marginBottom "32px"
|
|
2004
|
+
animate fadeIn 0.9
|
|
2005
|
+
|
|
2006
|
+
add button "cta":
|
|
2007
|
+
text "Click me!"
|
|
2008
|
+
style background "#0b7a75"
|
|
2009
|
+
style color "#b8f000"
|
|
2010
|
+
style border "none"
|
|
2011
|
+
style padding "14px 32px"
|
|
2012
|
+
style borderRadius "8px"
|
|
2013
|
+
style fontSize "16px"
|
|
2014
|
+
style fontWeight "700"
|
|
2015
|
+
style cursor "pointer"
|
|
2016
|
+
animate pop 0.5
|
|
2017
|
+
on click:
|
|
2018
|
+
say "You clicked the button!"
|
|
2019
|
+
|
|
2020
|
+
add div "cards":
|
|
2021
|
+
style display "flex"
|
|
2022
|
+
style gap "20px"
|
|
2023
|
+
style justifyContent "center"
|
|
2024
|
+
style marginTop "40px"
|
|
2025
|
+
style flexWrap "wrap"
|
|
2026
|
+
|
|
2027
|
+
add div "":
|
|
2028
|
+
style background "#162b28"
|
|
2029
|
+
style border "1px solid #234440"
|
|
2030
|
+
style borderRadius "12px"
|
|
2031
|
+
style padding "24px"
|
|
2032
|
+
style width "180px"
|
|
2033
|
+
style textAlign "center"
|
|
2034
|
+
animate slideIn 0.4
|
|
2035
|
+
add h3 "": text "Rocket Fast"
|
|
2036
|
+
style color "#b8f000"
|
|
2037
|
+
add p "": text "Runs in the browser"
|
|
2038
|
+
style color "#8ab8b4"
|
|
2039
|
+
style fontSize "13px"
|
|
2040
|
+
|
|
2041
|
+
add div "":
|
|
2042
|
+
style background "#162b28"
|
|
2043
|
+
style border "1px solid #234440"
|
|
2044
|
+
style borderRadius "12px"
|
|
2045
|
+
style padding "24px"
|
|
2046
|
+
style width "180px"
|
|
2047
|
+
style textAlign "center"
|
|
2048
|
+
animate slideIn 0.65
|
|
2049
|
+
add h3 "": text "Super Simple"
|
|
2050
|
+
style color "#b8f000"
|
|
2051
|
+
add p "": text "English-like syntax"
|
|
2052
|
+
style color "#8ab8b4"
|
|
2053
|
+
style fontSize "13px"
|
|
2054
|
+
|
|
2055
|
+
add div "":
|
|
2056
|
+
style background "#162b28"
|
|
2057
|
+
style border "1px solid #234440"
|
|
2058
|
+
style borderRadius "12px"
|
|
2059
|
+
style padding "24px"
|
|
2060
|
+
style width "180px"
|
|
2061
|
+
style textAlign "center"
|
|
2062
|
+
animate slideIn 0.9
|
|
2063
|
+
add h3 "": text "Fully Styled"
|
|
2064
|
+
style color "#b8f000"
|
|
2065
|
+
add p "": text "CSS control built in"
|
|
2066
|
+
style color "#8ab8b4"
|
|
2067
|
+
style fontSize "13px"`,
|
|
2068
|
+
|
|
2069
|
+
fizzbuzz:`// FizzBuzz — classic interview question
|
|
2070
|
+
|
|
2071
|
+
say "FizzBuzz 1–30:"
|
|
2072
|
+
count i from 1 to 30:
|
|
2073
|
+
if i % 15 == 0:
|
|
2074
|
+
say "FizzBuzz"
|
|
2075
|
+
else if i % 3 == 0:
|
|
2076
|
+
say "Fizz"
|
|
2077
|
+
else if i % 5 == 0:
|
|
2078
|
+
say "Buzz"
|
|
2079
|
+
else:
|
|
2080
|
+
say i`,
|
|
2081
|
+
|
|
2082
|
+
sorting:`// Bubble sort implemented in StructScript
|
|
2083
|
+
|
|
2084
|
+
define bubbleSort(arr):
|
|
2085
|
+
let n = length(arr)
|
|
2086
|
+
let sorted = arr
|
|
2087
|
+
count i from 0 to n - 2:
|
|
2088
|
+
count j from 0 to n - 2 - i:
|
|
2089
|
+
if sorted[j] > sorted[j + 1]:
|
|
2090
|
+
let temp = sorted[j]
|
|
2091
|
+
set sorted[j] = sorted[j + 1]
|
|
2092
|
+
set sorted[j + 1] = temp
|
|
2093
|
+
return sorted
|
|
2094
|
+
|
|
2095
|
+
let data = [64, 34, 25, 12, 22, 11, 90]
|
|
2096
|
+
say "Unsorted: " + str(data)
|
|
2097
|
+
let result = bubbleSort(data)
|
|
2098
|
+
say "Sorted: " + str(result)
|
|
2099
|
+
|
|
2100
|
+
// Also test with strings
|
|
2101
|
+
let words = ["banana", "apple", "cherry", "date"]
|
|
2102
|
+
say "Words sorted: " + str(sort(words))`
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
// ============================================================
|
|
2106
|
+
// UI STATE
|
|
2107
|
+
// ============================================================
|
|
2108
|
+
let currentTheme = 'dark';
|
|
2109
|
+
let currentFile = 'untitled.ss';
|
|
2110
|
+
let isModified = false;
|
|
2111
|
+
let acSelectedIdx = -1;
|
|
2112
|
+
let acItems = [];
|
|
2113
|
+
let lastErrors = [];
|
|
2114
|
+
|
|
2115
|
+
const editor = document.getElementById('code-editor');
|
|
2116
|
+
const highlightLayer = document.getElementById('highlight-layer');
|
|
2117
|
+
const lineNumbers = document.getElementById('line-numbers');
|
|
2118
|
+
const acDiv = document.getElementById('autocomplete');
|
|
2119
|
+
const outputDisplay = document.getElementById('output-display');
|
|
2120
|
+
const errorDisplay = document.getElementById('error-display');
|
|
2121
|
+
|
|
2122
|
+
// ============================================================
|
|
2123
|
+
// KEYWORDS & AUTOCOMPLETE DATA
|
|
2124
|
+
// ============================================================
|
|
2125
|
+
const KEYWORDS = ['say','let','const','set','define','return','if','else if','else',
|
|
2126
|
+
'while','repeat','times','count','from','to','step','for','each','in','and','or','not',
|
|
2127
|
+
'true','false','null','nothing','struct','class','self','break','continue','try','catch','finally','error',
|
|
2128
|
+
'page','add','style','on','animate','text','html','attr','remove','clear'];
|
|
2129
|
+
|
|
2130
|
+
const BUILTINS = ['input','length','push','pop','shift','unshift','join','split','upper','lower','trim',
|
|
2131
|
+
'contains','startsWith','endsWith','replace','slice','sort','reverse','range','flatten',
|
|
2132
|
+
'unique','sum','avg','map','filter','reduce','abs','sqrt','floor','ceil','round','max','min',
|
|
2133
|
+
'pow','log','log10','sin','cos','tan','random','randint','str','num','bool','type',
|
|
2134
|
+
'isNothing','print','assert','error','format','char','charCode','repeat','indexOf',
|
|
2135
|
+
'getEl','setStyle','getStyle','addClass','removeClass','toggleClass','setAttr','getAttr',
|
|
2136
|
+
'setHtml','getText','setText','appendTo','prependTo','removeEl','query','queryAll',
|
|
2137
|
+
'fadeIn','fadeOut','slideIn','slideOut','moveTo','scaleTo','rotateTo'];
|
|
2138
|
+
|
|
2139
|
+
const SNIPPETS = {
|
|
2140
|
+
'define': 'define name(params):\n ',
|
|
2141
|
+
'if': 'if condition:\n ',
|
|
2142
|
+
'while': 'while condition:\n ',
|
|
2143
|
+
'for each': 'for each item in list:\n ',
|
|
2144
|
+
'count': 'count i from 1 to 10:\n ',
|
|
2145
|
+
'repeat': 'repeat 5 times:\n ',
|
|
2146
|
+
'try': 'try:\n \ncatch err:\n say err',
|
|
2147
|
+
'struct': 'struct Name:\n field = value',
|
|
2148
|
+
};
|
|
2149
|
+
|
|
2150
|
+
// Load default example
|
|
2151
|
+
editor.value = EXAMPLES.hello;
|
|
2152
|
+
updateAll();
|
|
2153
|
+
|
|
2154
|
+
// ============================================================
|
|
2155
|
+
// SYNTAX HIGHLIGHT
|
|
2156
|
+
// ============================================================
|
|
2157
|
+
function highlight(code) {
|
|
2158
|
+
return code.split('\n').map(line => highlightLine(line)).join('\n');
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
function highlightLine(line) {
|
|
2162
|
+
const tokens = [];
|
|
2163
|
+
let i = 0;
|
|
2164
|
+
|
|
2165
|
+
while (i < line.length) {
|
|
2166
|
+
// Comment
|
|
2167
|
+
if (line[i]==='/' && line[i+1]==='/') {
|
|
2168
|
+
tokens.push(`<span style="color:var(--cmt-color);font-style:italic">${esc(line.slice(i))}</span>`);
|
|
2169
|
+
break;
|
|
2170
|
+
}
|
|
2171
|
+
// String
|
|
2172
|
+
if (line[i]==='"' || line[i]==="'") {
|
|
2173
|
+
const q=line[i]; let j=i+1;
|
|
2174
|
+
while(j<line.length && line[j]!==q) { if(line[j]==='\\')j++; j++; }
|
|
2175
|
+
tokens.push(`<span style="color:var(--str-color)">${esc(line.slice(i,j+1))}</span>`);
|
|
2176
|
+
i=j+1; continue;
|
|
2177
|
+
}
|
|
2178
|
+
// Number
|
|
2179
|
+
if (/\d/.test(line[i]) && (i===0||/\W/.test(line[i-1]))) {
|
|
2180
|
+
let j=i; while(j<line.length&&/[\d.]/.test(line[j]))j++;
|
|
2181
|
+
tokens.push(`<span style="color:var(--num-color)">${esc(line.slice(i,j))}</span>`);
|
|
2182
|
+
i=j; continue;
|
|
2183
|
+
}
|
|
2184
|
+
// Keywords (longest match first)
|
|
2185
|
+
let matched=false;
|
|
2186
|
+
const sorted=[...KEYWORDS].sort((a,b)=>b.length-a.length);
|
|
2187
|
+
for(const kw of sorted){
|
|
2188
|
+
if(line.slice(i).startsWith(kw)){
|
|
2189
|
+
const after=line[i+kw.length];
|
|
2190
|
+
const before=i===0?null:line[i-1];
|
|
2191
|
+
if((after===undefined||/[\s(:,]/.test(after))&&(before===null||/[\s(]/.test(before))){
|
|
2192
|
+
tokens.push(`<span style="color:var(--kw-color);font-weight:600">${esc(kw)}</span>`);
|
|
2193
|
+
i+=kw.length; matched=true; break;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if(matched) continue;
|
|
2198
|
+
// Built-in function call
|
|
2199
|
+
if(/[a-zA-Z_]/.test(line[i])){
|
|
2200
|
+
let j=i; while(j<line.length&&/[\w]/.test(line[j]))j++;
|
|
2201
|
+
const word=line.slice(i,j);
|
|
2202
|
+
if(BUILTINS.includes(word)&&line[j]==='(')
|
|
2203
|
+
tokens.push(`<span style="color:var(--fn-color)">${esc(word)}</span>`);
|
|
2204
|
+
else
|
|
2205
|
+
tokens.push(esc(word));
|
|
2206
|
+
i=j; continue;
|
|
2207
|
+
}
|
|
2208
|
+
// Operators
|
|
2209
|
+
if(/[+\-*\/%=<>!&|]/.test(line[i])){
|
|
2210
|
+
tokens.push(`<span style="color:var(--op-color)">${esc(line[i])}</span>`);
|
|
2211
|
+
i++; continue;
|
|
2212
|
+
}
|
|
2213
|
+
tokens.push(esc(line[i])); i++;
|
|
2214
|
+
}
|
|
2215
|
+
return tokens.join('');
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
2219
|
+
|
|
2220
|
+
// ============================================================
|
|
2221
|
+
// LINE NUMBERS
|
|
2222
|
+
// ============================================================
|
|
2223
|
+
function updateLineNumbers() {
|
|
2224
|
+
const lines = editor.value.split('\n');
|
|
2225
|
+
const cursorPos = editor.selectionStart;
|
|
2226
|
+
const textBefore = editor.value.slice(0, cursorPos);
|
|
2227
|
+
const curLine = textBefore.split('\n').length;
|
|
2228
|
+
|
|
2229
|
+
lineNumbers.innerHTML = lines.map((_,i)=>
|
|
2230
|
+
`<span class="line-num${i+1===curLine?' active':''}">${i+1}</span>`
|
|
2231
|
+
).join('');
|
|
2232
|
+
|
|
2233
|
+
// Sync scroll
|
|
2234
|
+
lineNumbers.scrollTop = editor.scrollTop;
|
|
2235
|
+
|
|
2236
|
+
// Status bar
|
|
2237
|
+
const col = textBefore.split('\n').pop().length + 1;
|
|
2238
|
+
document.getElementById('sb-lines').textContent = lines.length + (lines.length===1?' line':' lines');
|
|
2239
|
+
document.getElementById('sb-pos').textContent = `Ln ${curLine}, Col ${col}`;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// ============================================================
|
|
2243
|
+
// AUTOCOMPLETE
|
|
2244
|
+
// ============================================================
|
|
2245
|
+
function getWordAtCursor() {
|
|
2246
|
+
const pos = editor.selectionStart;
|
|
2247
|
+
const text = editor.value;
|
|
2248
|
+
let start = pos - 1;
|
|
2249
|
+
while (start >= 0 && /[\w]/.test(text[start])) start--;
|
|
2250
|
+
start++;
|
|
2251
|
+
return { word: text.slice(start, pos), start, end: pos };
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
function showAutocomplete() {
|
|
2255
|
+
const { word } = getWordAtCursor();
|
|
2256
|
+
if (!word || word.length < 1) { hideAutocomplete(); return; }
|
|
2257
|
+
|
|
2258
|
+
const all = [...KEYWORDS, ...BUILTINS];
|
|
2259
|
+
acItems = all.filter(k => k.startsWith(word) && k !== word).slice(0, 10);
|
|
2260
|
+
|
|
2261
|
+
if (!acItems.length) { hideAutocomplete(); return; }
|
|
2262
|
+
|
|
2263
|
+
acSelectedIdx = -1;
|
|
2264
|
+
acDiv.innerHTML = acItems.map((item, i) => {
|
|
2265
|
+
const isKw = KEYWORDS.includes(item);
|
|
2266
|
+
const icon = isKw ? '🔑' : '⚡';
|
|
2267
|
+
const typeLabel = isKw ? 'keyword' : 'builtin';
|
|
2268
|
+
return `<div class="ac-item" data-idx="${i}" onmousedown="applyAC(${i})">
|
|
2269
|
+
<span class="ac-icon">${icon}</span>
|
|
2270
|
+
<span class="ac-name">${item}</span>
|
|
2271
|
+
<span class="ac-type">${typeLabel}</span>
|
|
2272
|
+
</div>`;
|
|
2273
|
+
}).join('');
|
|
2274
|
+
|
|
2275
|
+
// Position near cursor
|
|
2276
|
+
const linesBefore = editor.value.slice(0, editor.selectionStart).split('\n');
|
|
2277
|
+
const lineH = 22, padTop = 12;
|
|
2278
|
+
const top = padTop + linesBefore.length * lineH;
|
|
2279
|
+
const col = linesBefore[linesBefore.length-1].length;
|
|
2280
|
+
const charW = 7.8;
|
|
2281
|
+
|
|
2282
|
+
acDiv.style.display = 'block';
|
|
2283
|
+
acDiv.style.top = (top + lineH) + 'px';
|
|
2284
|
+
acDiv.style.left = (16 + col * charW) + 'px';
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function hideAutocomplete() {
|
|
2288
|
+
acDiv.style.display = 'none';
|
|
2289
|
+
acItems = [];
|
|
2290
|
+
acSelectedIdx = -1;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
function applyAC(idx) {
|
|
2294
|
+
const { word, start, end } = getWordAtCursor();
|
|
2295
|
+
const completion = acItems[idx];
|
|
2296
|
+
const snippet = SNIPPETS[completion] || completion;
|
|
2297
|
+
const before = editor.value.slice(0, start);
|
|
2298
|
+
const after = editor.value.slice(end);
|
|
2299
|
+
editor.value = before + snippet + after;
|
|
2300
|
+
const newPos = start + snippet.length;
|
|
2301
|
+
editor.selectionStart = editor.selectionEnd = newPos;
|
|
2302
|
+
hideAutocomplete();
|
|
2303
|
+
updateAll();
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// ============================================================
|
|
2307
|
+
// MAIN UPDATE
|
|
2308
|
+
// ============================================================
|
|
2309
|
+
function updateAll() {
|
|
2310
|
+
highlightLayer.innerHTML = highlight(editor.value);
|
|
2311
|
+
// Sync scroll
|
|
2312
|
+
highlightLayer.scrollTop = editor.scrollTop;
|
|
2313
|
+
highlightLayer.scrollLeft = editor.scrollLeft;
|
|
2314
|
+
updateLineNumbers();
|
|
2315
|
+
markModified(true);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
function markModified(v) {
|
|
2319
|
+
isModified = v;
|
|
2320
|
+
document.getElementById('modified-indicator').style.display = v ? 'inline' : 'none';
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// ============================================================
|
|
2324
|
+
// INPUT MODAL
|
|
2325
|
+
// ============================================================
|
|
2326
|
+
function scanForInputs(code) {
|
|
2327
|
+
const inputs = [];
|
|
2328
|
+
const re = /\binput\s*\(\s*(?:"([^"]*)"|\'([^\']*)\'|([^)]*?))?\s*\)/g;
|
|
2329
|
+
let m;
|
|
2330
|
+
while ((m = re.exec(code)) !== null) {
|
|
2331
|
+
inputs.push((m[1] ?? m[2] ?? m[3] ?? '').trim() || 'Enter a value');
|
|
2332
|
+
}
|
|
2333
|
+
return inputs;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
let _pendingRunFn = null;
|
|
2337
|
+
|
|
2338
|
+
function cancelInputModal() {
|
|
2339
|
+
document.getElementById('input-modal-overlay').style.display='none';
|
|
2340
|
+
_pendingRunFn = null;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
function submitInputModal() {
|
|
2344
|
+
const fields = document.querySelectorAll('#input-modal-fields .im-input');
|
|
2345
|
+
const values = Array.from(fields).map(f => f.value);
|
|
2346
|
+
document.getElementById('input-modal-overlay').style.display='none';
|
|
2347
|
+
if (_pendingRunFn) { _pendingRunFn(values); _pendingRunFn = null; }
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
document.addEventListener('keydown', e => {
|
|
2351
|
+
if (e.key === 'Enter' && document.getElementById('input-modal-overlay').style.display!=='none') {
|
|
2352
|
+
const active = document.activeElement;
|
|
2353
|
+
if (active && active.classList.contains('im-input')) {
|
|
2354
|
+
const fields = [...document.querySelectorAll('#input-modal-fields .im-input')];
|
|
2355
|
+
const idx = fields.indexOf(active);
|
|
2356
|
+
if (idx === fields.length - 1) submitInputModal();
|
|
2357
|
+
else if (idx >= 0) fields[idx + 1].focus();
|
|
2358
|
+
e.preventDefault();
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
if (e.key === 'Escape' && document.getElementById('input-modal-overlay').style.display!=='none') cancelInputModal();
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
|
|
2365
|
+
|
|
2366
|
+
// ============================================================
|
|
2367
|
+
// RUN
|
|
2368
|
+
// ============================================================
|
|
2369
|
+
function runCode() {
|
|
2370
|
+
const code = editor.value;
|
|
2371
|
+
if (isWebCode(code)) { _execCode(code, []); return; }
|
|
2372
|
+
const inputPrompts = scanForInputs(code);
|
|
2373
|
+
if (inputPrompts.length > 0) {
|
|
2374
|
+
const fieldsDiv = document.getElementById('input-modal-fields');
|
|
2375
|
+
fieldsDiv.innerHTML = inputPrompts.map((prompt, i) => `
|
|
2376
|
+
<div class="im-field">
|
|
2377
|
+
<label class="im-label"><span class="im-prompt-text">${esc(prompt)}</span><span class="im-idx">#${i+1}</span></label>
|
|
2378
|
+
<input class="im-input" type="text" placeholder="type here..." autocomplete="off" spellcheck="false">
|
|
2379
|
+
</div>`).join('');
|
|
2380
|
+
document.getElementById('input-modal-overlay').style.display='flex';
|
|
2381
|
+
const first = fieldsDiv.querySelector('.im-input');
|
|
2382
|
+
if (first) setTimeout(() => first.focus(), 50);
|
|
2383
|
+
_pendingRunFn = (values) => _execCode(code, values);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
_execCode(code, []);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function _execCode(code, inputValues) {
|
|
2390
|
+
const dot = document.getElementById('sb-dot');
|
|
2391
|
+
dot.className = 'sb-dot run';
|
|
2392
|
+
document.getElementById('sb-status').textContent = 'Running...';
|
|
2393
|
+
document.getElementById('sb-time').textContent = '';
|
|
2394
|
+
outputDisplay.innerHTML = '';
|
|
2395
|
+
errorDisplay.innerHTML = '';
|
|
2396
|
+
lastErrors = [];
|
|
2397
|
+
|
|
2398
|
+
// ── Web mode ──────────────────────────────────────────────
|
|
2399
|
+
if (isWebCode(code)) {
|
|
2400
|
+
const webTab = document.getElementById('outtab-web');
|
|
2401
|
+
const saveBtn = document.getElementById('save-html-btn');
|
|
2402
|
+
webTab.style.display = '';
|
|
2403
|
+
saveBtn.style.display = '';
|
|
2404
|
+
try {
|
|
2405
|
+
const html = buildWebDoc(code);
|
|
2406
|
+
_lastWebHTML = html;
|
|
2407
|
+
const iframe = document.getElementById('web-preview');
|
|
2408
|
+
const webPanel = document.getElementById('out-web');
|
|
2409
|
+
if (!webPanel.querySelector('.web-toolbar')) {
|
|
2410
|
+
const bar = document.createElement('div');
|
|
2411
|
+
bar.className = 'web-toolbar';
|
|
2412
|
+
bar.innerHTML = '<span class="web-toolbar-label">🌐 Live Preview</span>' +
|
|
2413
|
+
'<button class="web-dl-btn" onclick="saveHTML()">⬇ Download .html</button>';
|
|
2414
|
+
webPanel.insertBefore(bar, iframe);
|
|
2415
|
+
}
|
|
2416
|
+
iframe.srcdoc = html;
|
|
2417
|
+
switchOutTab('web', document.getElementById('outtab-web'));
|
|
2418
|
+
dot.className = 'sb-dot ok';
|
|
2419
|
+
document.getElementById('sb-status').textContent = 'Rendered';
|
|
2420
|
+
document.getElementById('err-badge').style.display = 'none';
|
|
2421
|
+
errorDisplay.innerHTML = '<div class="no-errors"><div class="no-errors-icon">✓</div>No errors</div>';
|
|
2422
|
+
} catch(e) {
|
|
2423
|
+
dot.className = 'sb-dot err';
|
|
2424
|
+
document.getElementById('sb-status').textContent = 'Error';
|
|
2425
|
+
outputDisplay.innerHTML = `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Web error: ${esc(e.message)}</span></div>`;
|
|
2426
|
+
switchOutTab('output', document.getElementById('outtab-output'));
|
|
2427
|
+
}
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// ── Script mode ───────────────────────────────────────────
|
|
2432
|
+
const t0 = performance.now();
|
|
2433
|
+
const lines = [];
|
|
2434
|
+
setTimeout(() => {
|
|
2435
|
+
try {
|
|
2436
|
+
const interp = new Interpreter(
|
|
2437
|
+
msg => lines.push({ text: msg, cls: '' }),
|
|
2438
|
+
msg => lines.push({ text: msg, cls: 'warn' })
|
|
2439
|
+
);
|
|
2440
|
+
interp._inputQueue = [...inputValues];
|
|
2441
|
+
interp.run(code);
|
|
2442
|
+
const elapsed = (performance.now() - t0).toFixed(1);
|
|
2443
|
+
dot.className = 'sb-dot ok';
|
|
2444
|
+
document.getElementById('sb-status').textContent = 'Success';
|
|
2445
|
+
document.getElementById('sb-time').textContent = elapsed + 'ms';
|
|
2446
|
+
if (!lines.length) {
|
|
2447
|
+
outputDisplay.innerHTML = '<div class="out-line success"><span class="out-prefix">✓</span><span class="out-text">Program completed — no output</span></div>';
|
|
2448
|
+
} else {
|
|
2449
|
+
outputDisplay.innerHTML = lines.map((l,i) =>
|
|
2450
|
+
`<div class="out-line ${l.cls}"><span class="out-prefix">${i+1}</span><span class="out-text">${esc(String(l.text))}</span></div>`
|
|
2451
|
+
).join('');
|
|
2452
|
+
}
|
|
2453
|
+
errorDisplay.innerHTML = '<div class="no-errors"><div class="no-errors-icon">✓</div>No errors</div>';
|
|
2454
|
+
document.getElementById('err-badge').style.display = 'none';
|
|
2455
|
+
} catch(e) {
|
|
2456
|
+
const elapsed = (performance.now() - t0).toFixed(1);
|
|
2457
|
+
dot.className = 'sb-dot err';
|
|
2458
|
+
document.getElementById('sb-status').textContent = 'Error';
|
|
2459
|
+
document.getElementById('sb-time').textContent = elapsed + 'ms';
|
|
2460
|
+
if (lines.length) {
|
|
2461
|
+
outputDisplay.innerHTML = lines.map((l,i)=>
|
|
2462
|
+
`<div class="out-line ${l.cls}"><span class="out-prefix">${i+1}</span><span class="out-text">${esc(String(l.text))}</span></div>`
|
|
2463
|
+
).join('') + `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Error: ${esc(e.message)}</span></div>`;
|
|
2464
|
+
} else {
|
|
2465
|
+
outputDisplay.innerHTML = `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Error: ${esc(e.message)}</span></div>`;
|
|
2466
|
+
}
|
|
2467
|
+
const lineNum = e.ssLine !== undefined ? e.ssLine + 1 : '?';
|
|
2468
|
+
const snippet = e.snippet || (e.ssLine !== undefined ? (editor.value.split('\n')[e.ssLine]||'') : '');
|
|
2469
|
+
errorDisplay.innerHTML = `
|
|
2470
|
+
<div class="err-card">
|
|
2471
|
+
<div class="err-title">⚠ Runtime Error</div>
|
|
2472
|
+
<div class="err-detail">${esc(e.message)}</div>
|
|
2473
|
+
${lineNum !== '?' ? `<div class="err-line-ref">Line ${lineNum}</div>` : ''}
|
|
2474
|
+
${snippet ? `<div class="err-snippet"><span class="err-arrow">→ </span>${esc(snippet.trim())}</div>` : ''}
|
|
2475
|
+
</div>`;
|
|
2476
|
+
const badge = document.getElementById('err-badge');
|
|
2477
|
+
badge.textContent = '1'; badge.style.display = 'inline';
|
|
2478
|
+
}
|
|
2479
|
+
outputDisplay.scrollTop = outputDisplay.scrollHeight;
|
|
2480
|
+
}, 30);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
|
|
2484
|
+
|
|
2485
|
+
// ============================================================
|
|
2486
|
+
// WEB ENGINE
|
|
2487
|
+
// ============================================================
|
|
2488
|
+
|
|
2489
|
+
// Detects if the code uses any web commands
|
|
2490
|
+
function isWebCode(code) {
|
|
2491
|
+
return /^\s*(page|add)\s+/m.test(code);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// Build a full HTML document from StructScript web commands
|
|
2495
|
+
function buildWebDoc(code) {
|
|
2496
|
+
let pageTitle = 'StructScript Page';
|
|
2497
|
+
let pageStyles = {};
|
|
2498
|
+
let cssBlocks = []; // raw CSS rule strings from `css` blocks
|
|
2499
|
+
const elements = [];
|
|
2500
|
+
const elStack = [];
|
|
2501
|
+
let i = 0;
|
|
2502
|
+
|
|
2503
|
+
const lines = code.split('\n');
|
|
2504
|
+
|
|
2505
|
+
function getIndent(l) { let n=0; while(n<l.length&&l[n]===' ')n++; return n; }
|
|
2506
|
+
|
|
2507
|
+
function parseStr(s) {
|
|
2508
|
+
s = s.trim();
|
|
2509
|
+
if((s.startsWith('"')&&s.endsWith('"'))||(s.startsWith("'")&&s.endsWith("'"))) return s.slice(1,-1);
|
|
2510
|
+
return s;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// camelCase → kebab-case
|
|
2514
|
+
function toKebab(s) { return s.replace(/([A-Z])/g,'-$1').toLowerCase(); }
|
|
2515
|
+
// kebab-case or camelCase → camelCase (for object keys)
|
|
2516
|
+
function toCamel(s) { return s.replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); }
|
|
2517
|
+
|
|
2518
|
+
while (i < lines.length) {
|
|
2519
|
+
const raw = lines[i], t = raw.trim(), ind = getIndent(raw);
|
|
2520
|
+
i++;
|
|
2521
|
+
if (!t || t.startsWith('//')) continue;
|
|
2522
|
+
|
|
2523
|
+
// ── page "title": ──────────────────────────────────────
|
|
2524
|
+
if (/^page\b/.test(t)) {
|
|
2525
|
+
const m = t.match(/^page\s+"([^"]*)"|^page\s+\'([^']*)\'/);
|
|
2526
|
+
if (m) pageTitle = m[1]||m[2];
|
|
2527
|
+
const el = { _type:'page', styles:{}, attrs:{} };
|
|
2528
|
+
elStack.length = 0;
|
|
2529
|
+
elStack.push({ el, indent: ind });
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// ── css: (raw CSS block) ────────────────────────────────
|
|
2534
|
+
// css:
|
|
2535
|
+
// .btn { background: red }
|
|
2536
|
+
// @media (max-width: 600px) { ... }
|
|
2537
|
+
if (/^css\s*:?$/.test(t)) {
|
|
2538
|
+
const cssLines = [];
|
|
2539
|
+
while (i < lines.length) {
|
|
2540
|
+
const nr = lines[i], ni = getIndent(nr);
|
|
2541
|
+
if (nr.trim() === '' || ni > ind) { cssLines.push(nr.slice(ind+2||0)); i++; }
|
|
2542
|
+
else break;
|
|
2543
|
+
}
|
|
2544
|
+
cssBlocks.push(cssLines.join('\n'));
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// ── add TAG "id": ───────────────────────────────────────
|
|
2549
|
+
const addM = t.match(/^add\s+(\w+)(?:\s+"([^"]*)")?(?:\s+\'([^']*)\')?\s*:?$/);
|
|
2550
|
+
if (addM) {
|
|
2551
|
+
while (elStack.length > 1 && elStack[elStack.length-1].indent >= ind) elStack.pop();
|
|
2552
|
+
const parent = elStack[elStack.length-1]?.el || null;
|
|
2553
|
+
const rawId = addM[2]||addM[3]||'';
|
|
2554
|
+
const el = {
|
|
2555
|
+
tag: addM[1].toLowerCase(),
|
|
2556
|
+
id: rawId.startsWith('#') ? rawId.slice(1) : (rawId.includes(' ')||rawId.startsWith('.')?'':rawId),
|
|
2557
|
+
classes: rawId.startsWith('.') ? [rawId.slice(1)] : (rawId.includes(' ') ? rawId.split(' ') : []),
|
|
2558
|
+
text:'', html:'', styles:{}, hoverStyles:{}, focusStyles:{}, attrs:{}, events:[], children:[],
|
|
2559
|
+
_indent: ind, _anim: null, _cssRules: [],
|
|
2560
|
+
};
|
|
2561
|
+
if (parent && parent._type !== 'page') parent.children.push(el);
|
|
2562
|
+
else elements.push(el);
|
|
2563
|
+
elStack.push({ el, indent: ind });
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// ── Properties inside an element ───────────────────────
|
|
2568
|
+
const parentEntry = elStack[elStack.length-1];
|
|
2569
|
+
if (parentEntry && ind > parentEntry.indent) {
|
|
2570
|
+
const el = parentEntry.el;
|
|
2571
|
+
|
|
2572
|
+
// text / html
|
|
2573
|
+
const textM = t.match(/^text\s+(.+)$/);
|
|
2574
|
+
if (textM) { el.text = parseStr(textM[1]); continue; }
|
|
2575
|
+
const htmlM = t.match(/^html\s+(.+)$/);
|
|
2576
|
+
if (htmlM) { el.html = parseStr(htmlM[1]); continue; }
|
|
2577
|
+
|
|
2578
|
+
// style prop "value" — any valid CSS property (camel or kebab)
|
|
2579
|
+
const styleM = t.match(/^style\s+([\w-]+)\s*:?\s+(.+)$/);
|
|
2580
|
+
if (styleM) {
|
|
2581
|
+
const key = toCamel(styleM[1]);
|
|
2582
|
+
const val = parseStr(styleM[2]);
|
|
2583
|
+
el.styles[key] = val;
|
|
2584
|
+
if (el._type === 'page') pageStyles[key] = val;
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// hover prop "value" — generates :hover CSS rule
|
|
2589
|
+
const hoverM = t.match(/^hover\s+([\w-]+)\s*:?\s+(.+)$/);
|
|
2590
|
+
if (hoverM) {
|
|
2591
|
+
el.hoverStyles[toCamel(hoverM[1])] = parseStr(hoverM[2]);
|
|
2592
|
+
continue;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// focus prop "value" — generates :focus CSS rule
|
|
2596
|
+
const focusM = t.match(/^focus\s+([\w-]+)\s*:?\s+(.+)$/);
|
|
2597
|
+
if (focusM) {
|
|
2598
|
+
el.focusStyles[toCamel(focusM[1])] = parseStr(focusM[2]);
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// transition "prop duration easing"
|
|
2603
|
+
const transM = t.match(/^transition\s+(.+)$/);
|
|
2604
|
+
if (transM) { el.styles.transition = parseStr(transM[1]); continue; }
|
|
2605
|
+
|
|
2606
|
+
// attr / class
|
|
2607
|
+
const attrM = t.match(/^attr\s+(\w+)\s+(.+)$/);
|
|
2608
|
+
if (attrM) { el.attrs[attrM[1]] = parseStr(attrM[2]); continue; }
|
|
2609
|
+
const classM = t.match(/^class\s+(.+)$/);
|
|
2610
|
+
if (classM) { el.classes.push(parseStr(classM[1])); continue; }
|
|
2611
|
+
|
|
2612
|
+
// animate TYPE DURATION EASING
|
|
2613
|
+
const animM = t.match(/^animate\s+(\w+)(?:\s+([\d.]+))?(?:\s+(\w+))?$/);
|
|
2614
|
+
if (animM) {
|
|
2615
|
+
el._anim = { type: animM[1], dur: parseFloat(animM[2]||0.4), easing: animM[3]||'ease' };
|
|
2616
|
+
continue;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// on EVENT: (collect indented handler lines)
|
|
2620
|
+
const onM = t.match(/^on\s+(\w+)\s*:?$/);
|
|
2621
|
+
if (onM) {
|
|
2622
|
+
const handlerLines = [];
|
|
2623
|
+
while (i < lines.length) {
|
|
2624
|
+
const nr = lines[i], ni = getIndent(nr);
|
|
2625
|
+
if (!nr.trim() || ni <= ind) break;
|
|
2626
|
+
handlerLines.push(nr.slice(ni)); i++;
|
|
2627
|
+
}
|
|
2628
|
+
el.events.push({ event: onM[1], code: handlerLines.join('\n') });
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// ── Render ──────────────────────────────────────────────────
|
|
2635
|
+
const pseudoRules = []; // accumulated :hover, :focus rules
|
|
2636
|
+
|
|
2637
|
+
function renderEl(el) {
|
|
2638
|
+
if (el._type === 'page') return '';
|
|
2639
|
+
const tag = el.tag;
|
|
2640
|
+
const idAttr = el.id ? ` id="${el.id}"` : '';
|
|
2641
|
+
const clsAttr = el.classes.length ? ` class="${el.classes.join(' ')}"` : '';
|
|
2642
|
+
|
|
2643
|
+
// Inline styles
|
|
2644
|
+
let styleStr = Object.entries(el.styles).map(([k,v]) => `${toKebab(k)}:${v}`).join(';');
|
|
2645
|
+
|
|
2646
|
+
// Animation
|
|
2647
|
+
if (el._anim) {
|
|
2648
|
+
const animMap = {
|
|
2649
|
+
fadeIn: `fadeIn ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
2650
|
+
fadeOut: `fadeOut ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
2651
|
+
slideIn: `slideIn ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
2652
|
+
slideUp: `slideUp ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
2653
|
+
bounce: `bounce ${el._anim.dur}s ${el._anim.easing} infinite`,
|
|
2654
|
+
pulse: `pulse ${el._anim.dur}s ${el._anim.easing} infinite`,
|
|
2655
|
+
spin: `spin ${el._anim.dur}s linear infinite`,
|
|
2656
|
+
shake: `shake ${el._anim.dur}s ${el._anim.easing}`,
|
|
2657
|
+
pop: `pop ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
2658
|
+
};
|
|
2659
|
+
styleStr += (styleStr?';':'') + `animation:${animMap[el._anim.type]||`${el._anim.type} ${el._anim.dur}s ${el._anim.easing}`}`;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// Generate :hover and :focus rules (needs a selector — use id if available, else generate one)
|
|
2663
|
+
if (Object.keys(el.hoverStyles).length || Object.keys(el.focusStyles).length) {
|
|
2664
|
+
// Ensure element has an id for targeting
|
|
2665
|
+
if (!el.id) { el.id = '_ss_' + Math.random().toString(36).slice(2,8); }
|
|
2666
|
+
if (Object.keys(el.hoverStyles).length) {
|
|
2667
|
+
const hoverStr = Object.entries(el.hoverStyles).map(([k,v])=>`${toKebab(k)}:${v}`).join(';');
|
|
2668
|
+
pseudoRules.push(`#${el.id}:hover{${hoverStr}}`);
|
|
2669
|
+
}
|
|
2670
|
+
if (Object.keys(el.focusStyles).length) {
|
|
2671
|
+
const focusStr = Object.entries(el.focusStyles).map(([k,v])=>`${toKebab(k)}:${v}`).join(';');
|
|
2672
|
+
pseudoRules.push(`#${el.id}:focus{${focusStr}}`);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
const styleAttr = styleStr ? ` style="${styleStr}"` : '';
|
|
2677
|
+
const extraAttrs = Object.entries(el.attrs).map(([k,v])=>` ${k}="${v}"`).join('');
|
|
2678
|
+
const evAttrs = el.events.map(ev=>{
|
|
2679
|
+
const escaped = ev.code.replace(/"/g,'"').replace(/\n/g,' ');
|
|
2680
|
+
return ` data-ss-on${ev.event}="${escaped}"`;
|
|
2681
|
+
}).join('');
|
|
2682
|
+
|
|
2683
|
+
const selfClose = ['img','input','br','hr','meta','link'].includes(tag);
|
|
2684
|
+
if (selfClose) return `<${tag}${idAttr}${clsAttr}${styleAttr}${extraAttrs}/>
|
|
2685
|
+
`;
|
|
2686
|
+
const inner = el.html || el.text || el.children.map(renderEl).join('');
|
|
2687
|
+
return `<${tag}${idAttr}${clsAttr}${styleAttr}${extraAttrs}${evAttrs}>${inner}</${tag}>
|
|
2688
|
+
`;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
const bodyStyleStr = Object.entries(pageStyles).map(([k,v])=>`${toKebab(k)}:${v}`).join(';');
|
|
2692
|
+
const bodyHtml = elements.map(renderEl).join('');
|
|
2693
|
+
|
|
2694
|
+
const KEYFRAMES = `
|
|
2695
|
+
@keyframes fadeIn { from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:none} }
|
|
2696
|
+
@keyframes fadeOut { from{opacity:1} to{opacity:0} }
|
|
2697
|
+
@keyframes slideIn { from{transform:translateX(-40px);opacity:0} to{transform:none;opacity:1} }
|
|
2698
|
+
@keyframes slideUp { from{transform:translateY(40px);opacity:0} to{transform:none;opacity:1} }
|
|
2699
|
+
@keyframes bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-16px)} }
|
|
2700
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
|
2701
|
+
@keyframes spin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
|
|
2702
|
+
@keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-8px)} 75%{transform:translateX(8px)} }
|
|
2703
|
+
@keyframes pop { 0%{transform:scale(0.5);opacity:0} 70%{transform:scale(1.1)} 100%{transform:scale(1);opacity:1} }
|
|
2704
|
+
`;
|
|
2705
|
+
|
|
2706
|
+
const EVENT_TYPES = ['click','mouseover','mouseout','mouseenter','mouseleave','keydown','keyup','change','focus','blur','dblclick'];
|
|
2707
|
+
const evScript = EVENT_TYPES.map(ev =>
|
|
2708
|
+
`document.querySelectorAll('[data-ss-on${ev}]').forEach(function(el){el.addEventListener('${ev}',function(){try{(new Function(this.getAttribute('data-ss-on${ev}')))()}catch(e){console.error(e)}}.bind(el));});`
|
|
2709
|
+
).join('\n');
|
|
2710
|
+
|
|
2711
|
+
const allPseudoCSS = pseudoRules.join('\n');
|
|
2712
|
+
const allCustomCSS = cssBlocks.join('\n');
|
|
2713
|
+
|
|
2714
|
+
return `<!DOCTYPE html>
|
|
2715
|
+
<html lang="en">
|
|
2716
|
+
<head>
|
|
2717
|
+
<meta charset="UTF-8">
|
|
2718
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
2719
|
+
<title>${pageTitle}</title>
|
|
2720
|
+
<style>
|
|
2721
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2722
|
+
${KEYFRAMES}
|
|
2723
|
+
${allPseudoCSS}
|
|
2724
|
+
${allCustomCSS}
|
|
2725
|
+
</style>
|
|
2726
|
+
</head>
|
|
2727
|
+
<body${bodyStyleStr ? ` style="${bodyStyleStr}"` : ''}>
|
|
2728
|
+
${bodyHtml}
|
|
2729
|
+
<script>(function(){${evScript}})()</\/script>
|
|
2730
|
+
</body>
|
|
2731
|
+
</html>`;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// Saved web HTML for download
|
|
2735
|
+
let _lastWebHTML = '';
|
|
2736
|
+
|
|
2737
|
+
async function saveHTML() {
|
|
2738
|
+
if (!_lastWebHTML) return;
|
|
2739
|
+
if (window.electronAPI) {
|
|
2740
|
+
const suggested = (currentFilePath || currentFile).replace(/\.ss$/i, '.html');
|
|
2741
|
+
const result = await window.electronAPI.dialogSaveHtml({ content: _lastWebHTML, suggested });
|
|
2742
|
+
if (!result.canceled) showToast('\u2713 Saved ' + (result.path||'').split(/[\\/]/).pop());
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
const blob = new Blob([_lastWebHTML], { type: 'text/html' });
|
|
2746
|
+
const a = document.createElement('a');
|
|
2747
|
+
a.href = URL.createObjectURL(blob);
|
|
2748
|
+
a.download = currentFile.replace('.ss','.html') || 'page.html';
|
|
2749
|
+
a.click();
|
|
2750
|
+
URL.revokeObjectURL(a.href);
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
|
|
2754
|
+
|
|
2755
|
+
// ============================================================
|
|
2756
|
+
// FILE OPERATIONS (browser fallback — editor overrides these)
|
|
2757
|
+
// ============================================================
|
|
2758
|
+
function handleFileOpen(event) {
|
|
2759
|
+
const file = event.target.files[0]; if (!file) return;
|
|
2760
|
+
const reader = new FileReader();
|
|
2761
|
+
reader.onload = e => {
|
|
2762
|
+
editor.value = e.target.result;
|
|
2763
|
+
currentFile = file.name;
|
|
2764
|
+
document.getElementById('current-filename').textContent = currentFile;
|
|
2765
|
+
updateHighlight(); updateLineNumbers(); setModified(false);
|
|
2766
|
+
};
|
|
2767
|
+
reader.readAsText(file);
|
|
2768
|
+
}
|
|
2769
|
+
function shareCode() {} // disabled in editor mode
|
|
2770
|
+
|
|
2771
|
+
|
|
2772
|
+
// ============================================================
|
|
2773
|
+
// THEME
|
|
2774
|
+
// ============================================================
|
|
2775
|
+
function toggleTheme() {
|
|
2776
|
+
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
2777
|
+
document.body.setAttribute('data-theme', currentTheme);
|
|
2778
|
+
document.getElementById('theme-btn').textContent = currentTheme === 'dark' ? '🌙' : '☀️';
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// ============================================================
|
|
2782
|
+
// PAGE / TAB SWITCHING
|
|
2783
|
+
// ============================================================
|
|
2784
|
+
function showPage(name, btn) {
|
|
2785
|
+
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
|
|
2786
|
+
document.querySelectorAll('.nav-tab').forEach(b=>b.classList.remove('active'));
|
|
2787
|
+
document.getElementById('page-'+name).classList.add('active');
|
|
2788
|
+
if(btn) btn.classList.add('active');
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
function switchOutTab(name, btn) {
|
|
2792
|
+
document.querySelectorAll('.out-tab').forEach(b=>b.classList.remove('active'));
|
|
2793
|
+
document.querySelectorAll('.out-panel').forEach(p=>p.classList.remove('active'));
|
|
2794
|
+
btn.classList.add('active');
|
|
2795
|
+
document.getElementById('out-'+name).classList.add('active');
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// ============================================================
|
|
2799
|
+
// LOAD EXAMPLE
|
|
2800
|
+
// ============================================================
|
|
2801
|
+
function loadExample(name) {
|
|
2802
|
+
if (isModified && !confirm('Load example? Unsaved changes will be lost.')) return;
|
|
2803
|
+
editor.value = EXAMPLES[name] || '';
|
|
2804
|
+
currentFile = name + '.ss';
|
|
2805
|
+
document.getElementById('current-filename').textContent = currentFile;
|
|
2806
|
+
markModified(false);
|
|
2807
|
+
updateAll();
|
|
2808
|
+
editor.focus();
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// ============================================================
|
|
2812
|
+
// EDITOR EVENT LISTENERS
|
|
2813
|
+
// ============================================================
|
|
2814
|
+
editor.addEventListener('input', () => { updateAll(); showAutocomplete(); });
|
|
2815
|
+
editor.addEventListener('scroll', () => {
|
|
2816
|
+
highlightLayer.scrollTop = editor.scrollTop;
|
|
2817
|
+
highlightLayer.scrollLeft = editor.scrollLeft;
|
|
2818
|
+
lineNumbers.scrollTop = editor.scrollTop;
|
|
2819
|
+
});
|
|
2820
|
+
editor.addEventListener('click', () => { updateLineNumbers(); hideAutocomplete(); });
|
|
2821
|
+
editor.addEventListener('blur', () => setTimeout(hideAutocomplete, 150));
|
|
2822
|
+
|
|
2823
|
+
editor.addEventListener('keydown', e => {
|
|
2824
|
+
// Autocomplete navigation
|
|
2825
|
+
if (acDiv.style.display !== 'none') {
|
|
2826
|
+
if (e.key === 'ArrowDown') {
|
|
2827
|
+
e.preventDefault();
|
|
2828
|
+
acSelectedIdx = Math.min(acSelectedIdx+1, acItems.length-1);
|
|
2829
|
+
acDiv.querySelectorAll('.ac-item').forEach((el,i)=>el.classList.toggle('selected',i===acSelectedIdx));
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
if (e.key === 'ArrowUp') {
|
|
2833
|
+
e.preventDefault();
|
|
2834
|
+
acSelectedIdx = Math.max(acSelectedIdx-1, 0);
|
|
2835
|
+
acDiv.querySelectorAll('.ac-item').forEach((el,i)=>el.classList.toggle('selected',i===acSelectedIdx));
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
2839
|
+
if (acSelectedIdx >= 0) { e.preventDefault(); applyAC(acSelectedIdx); return; }
|
|
2840
|
+
}
|
|
2841
|
+
if (e.key === 'Escape') { hideAutocomplete(); return; }
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// Tab = 2 spaces
|
|
2845
|
+
if (e.key === 'Tab') {
|
|
2846
|
+
e.preventDefault();
|
|
2847
|
+
const s = editor.selectionStart, en = editor.selectionEnd;
|
|
2848
|
+
editor.value = editor.value.slice(0,s)+' '+editor.value.slice(en);
|
|
2849
|
+
editor.selectionStart = editor.selectionEnd = s+2;
|
|
2850
|
+
updateAll();
|
|
2851
|
+
}
|
|
2852
|
+
// Run shortcut
|
|
2853
|
+
if (e.key === 'Enter' && (e.ctrlKey||e.metaKey)) { e.preventDefault(); runCode(); }
|
|
2854
|
+
// Save shortcut
|
|
2855
|
+
if (e.key === 's' && (e.ctrlKey||e.metaKey)) { e.preventDefault(); saveFile(); }
|
|
2856
|
+
// Auto-indent after colon
|
|
2857
|
+
if (e.key === 'Enter') {
|
|
2858
|
+
const pos = editor.selectionStart;
|
|
2859
|
+
const lineBefore = editor.value.slice(0, pos).split('\n').pop();
|
|
2860
|
+
if (lineBefore.trimEnd().endsWith(':')) {
|
|
2861
|
+
e.preventDefault();
|
|
2862
|
+
const indent = ' '.repeat(lineBefore.match(/^\s*/)[0].length + 2);
|
|
2863
|
+
const ins = '\n' + indent;
|
|
2864
|
+
editor.value = editor.value.slice(0, pos) + ins + editor.value.slice(pos);
|
|
2865
|
+
editor.selectionStart = editor.selectionEnd = pos + ins.length;
|
|
2866
|
+
updateAll();
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
// Auto-close brackets and quotes
|
|
2870
|
+
const pairs = { '(':')', '[':']', '"':'"', "'":"'" };
|
|
2871
|
+
if (pairs[e.key] && !e.ctrlKey && !e.metaKey) {
|
|
2872
|
+
const pos = editor.selectionStart, sel = editor.selectionEnd;
|
|
2873
|
+
if (pos !== sel) {
|
|
2874
|
+
e.preventDefault();
|
|
2875
|
+
const selected = editor.value.slice(pos, sel);
|
|
2876
|
+
const ins = e.key + selected + pairs[e.key];
|
|
2877
|
+
editor.value = editor.value.slice(0,pos) + ins + editor.value.slice(sel);
|
|
2878
|
+
editor.selectionStart = pos+1; editor.selectionEnd = sel+1;
|
|
2879
|
+
updateAll();
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
|
|
2884
|
+
// Initial render
|
|
2885
|
+
loadSharedCode();
|
|
2886
|
+
updateLineNumbers();
|
|
2887
|
+
|
|
2888
|
+
|
|
2889
|
+
// ============================================================
|
|
2890
|
+
// EDITOR FILE I/O (overrides browser-only versions above)
|
|
2891
|
+
// ============================================================
|
|
2892
|
+
|
|
2893
|
+
// ============================================================
|
|
2894
|
+
// EDITOR MODE — all file I/O goes through the local server API
|
|
2895
|
+
// ============================================================
|
|
2896
|
+
|
|
2897
|
+
const IS_EDITOR = true;
|
|
2898
|
+
let currentFilePath = null; // full absolute path on disk
|
|
2899
|
+
let autoSaveTimer = null;
|
|
2900
|
+
let unsavedChanges = false;
|
|
2901
|
+
|
|
2902
|
+
// ── API helpers ─────────────────────────────────────────────
|
|
2903
|
+
async function api(route, method = 'GET', body = null) {
|
|
2904
|
+
if (window.electronAPI) {
|
|
2905
|
+
const b = body || {};
|
|
2906
|
+
if (route === '/api/run') return window.electronAPI.run(b);
|
|
2907
|
+
if (route === '/api/save') return window.electronAPI.save({ filePath: b.path, content: b.content });
|
|
2908
|
+
if (route === '/api/open') return window.electronAPI.open({ filePath: b.path });
|
|
2909
|
+
if (route === '/api/recent') return window.electronAPI.recent();
|
|
2910
|
+
if (route === '/api/new') return window.electronAPI.newFile(b);
|
|
2911
|
+
if (route.startsWith('/api/browse')) {
|
|
2912
|
+
const dir = new URL(route, 'http://x').searchParams.get('dir');
|
|
2913
|
+
return window.electronAPI.browse({ dir });
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
2917
|
+
if (body) opts.body = JSON.stringify(body);
|
|
2918
|
+
const res = await fetch(route, opts);
|
|
2919
|
+
return res.json();
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
// ── Show saved toast ────────────────────────────────────────
|
|
2923
|
+
function showToast(msg = '✓ Saved') {
|
|
2924
|
+
const t = document.createElement('div');
|
|
2925
|
+
t.className = 'saved-toast';
|
|
2926
|
+
t.textContent = msg;
|
|
2927
|
+
document.body.appendChild(t);
|
|
2928
|
+
setTimeout(() => t.remove(), 1700);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// ── Update sidebar + filepath bar ───────────────────────────
|
|
2932
|
+
function setCurrentFile(fullPath, name) {
|
|
2933
|
+
currentFilePath = fullPath;
|
|
2934
|
+
currentFile = name || fullPath.split(/[\\/]/).pop();
|
|
2935
|
+
document.getElementById('current-filename').textContent = currentFile;
|
|
2936
|
+
document.getElementById('fp-path').textContent = fullPath || '';
|
|
2937
|
+
document.getElementById('fp-path').title = fullPath || '';
|
|
2938
|
+
unsavedChanges = false;
|
|
2939
|
+
document.getElementById('modified-indicator').style.display = 'none';
|
|
2940
|
+
document.getElementById('fp-unsaved').classList.remove('show');
|
|
2941
|
+
refreshSidebar();
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
function markUnsaved() {
|
|
2945
|
+
if (!unsavedChanges) {
|
|
2946
|
+
unsavedChanges = true;
|
|
2947
|
+
document.getElementById('modified-indicator').style.display = 'inline';
|
|
2948
|
+
document.getElementById('fp-unsaved').classList.add('show');
|
|
2949
|
+
}
|
|
2950
|
+
clearTimeout(autoSaveTimer);
|
|
2951
|
+
if (currentFilePath) {
|
|
2952
|
+
autoSaveTimer = setTimeout(() => autoSave(), 2000);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
async function autoSave() {
|
|
2957
|
+
if (!currentFilePath || !unsavedChanges) return;
|
|
2958
|
+
await doSave(currentFilePath, false);
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
async function doSave(filePath, toast = true) {
|
|
2962
|
+
const content = editor.value;
|
|
2963
|
+
const result = await api('/api/save', 'POST', { path: filePath, content });
|
|
2964
|
+
if (result.ok) {
|
|
2965
|
+
currentFilePath = result.path;
|
|
2966
|
+
unsavedChanges = false;
|
|
2967
|
+
document.getElementById('modified-indicator').style.display = 'none';
|
|
2968
|
+
document.getElementById('fp-unsaved').classList.remove('show');
|
|
2969
|
+
if (toast) showToast('✓ Saved');
|
|
2970
|
+
refreshSidebar();
|
|
2971
|
+
} else {
|
|
2972
|
+
showToast('⚠ Save failed: ' + result.error);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// ── File operations (override playground versions) ──────────
|
|
2977
|
+
async function newFile() {
|
|
2978
|
+
const name = prompt('File name:', 'untitled.ss') || 'untitled.ss';
|
|
2979
|
+
const n = name.endsWith('.ss') ? name : name + '.ss';
|
|
2980
|
+
const result = await api('/api/new', 'POST', { name: n });
|
|
2981
|
+
if (result.ok) {
|
|
2982
|
+
editor.value = result.content;
|
|
2983
|
+
setCurrentFile(result.path, result.name);
|
|
2984
|
+
updateHighlight(); updateLineNumbers();
|
|
2985
|
+
document.getElementById('modified-indicator').style.display = 'none';
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
async function saveFile() {
|
|
2990
|
+
if (currentFilePath) {
|
|
2991
|
+
await doSave(currentFilePath);
|
|
2992
|
+
} else {
|
|
2993
|
+
await saveFileAs();
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
async function saveFileAs() {
|
|
2998
|
+
if (window.electronAPI) {
|
|
2999
|
+
const suggested = currentFilePath || (currentFile.endsWith('.ss') ? currentFile : currentFile + '.ss');
|
|
3000
|
+
const result = await window.electronAPI.dialogSave({ suggested, content: editor.value });
|
|
3001
|
+
if (result.canceled) return;
|
|
3002
|
+
setCurrentFile(result.path, result.name);
|
|
3003
|
+
unsavedChanges = false;
|
|
3004
|
+
document.getElementById('modified-indicator').style.display = 'none';
|
|
3005
|
+
document.getElementById('fp-unsaved').classList.remove('show');
|
|
3006
|
+
showToast('\u2713 Saved');
|
|
3007
|
+
refreshSidebar();
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
const suggested = currentFilePath || (currentFile.endsWith('.ss') ? currentFile : currentFile + '.ss');
|
|
3011
|
+
showSaveAsDialog(suggested);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
async function openFile() {
|
|
3015
|
+
if (window.electronAPI) {
|
|
3016
|
+
const result = await window.electronAPI.dialogOpen();
|
|
3017
|
+
if (result.canceled) return;
|
|
3018
|
+
editor.value = result.content;
|
|
3019
|
+
setCurrentFile(result.path, result.name);
|
|
3020
|
+
updateHighlight(); updateLineNumbers();
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
showBrowseDialog();
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
async function openFilePath(filePath) {
|
|
3027
|
+
const result = await api('/api/open', 'POST', { path: filePath });
|
|
3028
|
+
if (result.ok) {
|
|
3029
|
+
editor.value = result.content;
|
|
3030
|
+
setCurrentFile(result.path, result.name);
|
|
3031
|
+
updateHighlight(); updateLineNumbers();
|
|
3032
|
+
closeBrowseDialog();
|
|
3033
|
+
// Switch to playground view
|
|
3034
|
+
showPage('playground', document.getElementById('tab-playground'));
|
|
3035
|
+
} else {
|
|
3036
|
+
showToast('⚠ ' + result.error);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
// ── Run code via server ─────────────────────────────────────
|
|
3041
|
+
// Override the existing runCode to send to server API
|
|
3042
|
+
const _origRunCode = runCode;
|
|
3043
|
+
async function runCode() {
|
|
3044
|
+
const code = editor.value;
|
|
3045
|
+
|
|
3046
|
+
// Web mode: use client-side buildWebDoc (no input needed)
|
|
3047
|
+
if (isWebCode(code)) { _execCode(code, []); return; }
|
|
3048
|
+
|
|
3049
|
+
// Scan for inputs client-side so we can show the modal
|
|
3050
|
+
const inputPrompts = scanForInputs(code);
|
|
3051
|
+
if (inputPrompts.length > 0) {
|
|
3052
|
+
const fieldsDiv = document.getElementById('input-modal-fields');
|
|
3053
|
+
fieldsDiv.innerHTML = inputPrompts.map((prompt, i) => `
|
|
3054
|
+
<div class="im-field">
|
|
3055
|
+
<label class="im-label"><span class="im-prompt-text">${esc(prompt)}</span><span class="im-idx">#${i+1}</span></label>
|
|
3056
|
+
<input class="im-input" type="text" placeholder="type here..." autocomplete="off" spellcheck="false">
|
|
3057
|
+
</div>`).join('');
|
|
3058
|
+
document.getElementById('input-modal-overlay').style.display = 'flex';
|
|
3059
|
+
const first = fieldsDiv.querySelector('.im-input');
|
|
3060
|
+
if (first) setTimeout(() => first.focus(), 50);
|
|
3061
|
+
_pendingRunFn = (values) => _runViaServer(code, values);
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
await _runViaServer(code, []);
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
async function _runViaServer(code, inputs) {
|
|
3068
|
+
const dot = document.getElementById('sb-dot');
|
|
3069
|
+
dot.className = 'sb-dot run';
|
|
3070
|
+
document.getElementById('sb-status').textContent = 'Running...';
|
|
3071
|
+
document.getElementById('sb-time').textContent = '';
|
|
3072
|
+
outputDisplay.innerHTML = '';
|
|
3073
|
+
errorDisplay.innerHTML = '';
|
|
3074
|
+
|
|
3075
|
+
const t0 = performance.now();
|
|
3076
|
+
let result;
|
|
3077
|
+
try {
|
|
3078
|
+
result = await api('/api/run', 'POST', { source: code, inputs });
|
|
3079
|
+
} catch (e) {
|
|
3080
|
+
// fallback to client-side if server unreachable
|
|
3081
|
+
_execCode(code, inputs);
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
const elapsed = (performance.now() - t0).toFixed(1);
|
|
3085
|
+
|
|
3086
|
+
if (result.ok) {
|
|
3087
|
+
dot.className = 'sb-dot ok';
|
|
3088
|
+
document.getElementById('sb-status').textContent = 'Success';
|
|
3089
|
+
document.getElementById('sb-time').textContent = elapsed + 'ms';
|
|
3090
|
+
if (!result.lines.length) {
|
|
3091
|
+
outputDisplay.innerHTML = '<div class="out-line success"><span class="out-prefix">✓</span><span class="out-text">Program completed — no output</span></div>';
|
|
3092
|
+
} else {
|
|
3093
|
+
outputDisplay.innerHTML = result.lines.map((l, i) =>
|
|
3094
|
+
`<div class="out-line ${l.cls}"><span class="out-prefix">${i+1}</span><span class="out-text">${esc(String(l.text))}</span></div>`
|
|
3095
|
+
).join('');
|
|
3096
|
+
}
|
|
3097
|
+
errorDisplay.innerHTML = '<div class="no-errors"><div class="no-errors-icon">✓</div>No errors</div>';
|
|
3098
|
+
document.getElementById('err-badge').style.display = 'none';
|
|
3099
|
+
// Auto-switch to output tab
|
|
3100
|
+
switchOutTab('output', document.getElementById('outtab-output'));
|
|
3101
|
+
} else {
|
|
3102
|
+
dot.className = 'sb-dot err';
|
|
3103
|
+
document.getElementById('sb-status').textContent = 'Error';
|
|
3104
|
+
document.getElementById('sb-time').textContent = elapsed + 'ms';
|
|
3105
|
+
const err = result.error || {};
|
|
3106
|
+
if (result.lines.length) {
|
|
3107
|
+
outputDisplay.innerHTML = result.lines.map((l, i) =>
|
|
3108
|
+
`<div class="out-line ${l.cls}"><span class="out-prefix">${i+1}</span><span class="out-text">${esc(String(l.text))}</span></div>`
|
|
3109
|
+
).join('') + `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Error: ${esc(err.message)}</span></div>`;
|
|
3110
|
+
} else {
|
|
3111
|
+
outputDisplay.innerHTML = `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Error: ${esc(err.message||'Unknown error')}</span></div>`;
|
|
3112
|
+
}
|
|
3113
|
+
errorDisplay.innerHTML = `
|
|
3114
|
+
<div class="err-card">
|
|
3115
|
+
<div class="err-title">⚠ Runtime Error</div>
|
|
3116
|
+
<div class="err-detail">${esc(err.message||'')}</div>
|
|
3117
|
+
${err.line ? `<div class="err-line-ref">Line ${err.line}</div>` : ''}
|
|
3118
|
+
${err.snippet ? `<div class="err-snippet"><span class="err-arrow">→ </span>${esc(err.snippet.trim())}</div>` : ''}
|
|
3119
|
+
</div>`;
|
|
3120
|
+
const badge = document.getElementById('err-badge');
|
|
3121
|
+
badge.textContent = '1'; badge.style.display = 'inline';
|
|
3122
|
+
}
|
|
3123
|
+
outputDisplay.scrollTop = outputDisplay.scrollHeight;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
// ── Sidebar ─────────────────────────────────────────────────
|
|
3127
|
+
async function refreshSidebar() {
|
|
3128
|
+
const result = await api('/api/recent');
|
|
3129
|
+
const list = document.getElementById('file-tree-list');
|
|
3130
|
+
if (!result.files || !result.files.length) {
|
|
3131
|
+
list.innerHTML = '<div style="padding:16px 14px;font-size:11px;color:var(--muted)">No recent files</div>';
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
list.innerHTML = result.files.map(f => {
|
|
3135
|
+
const name = f.split(/[\\/]/).pop();
|
|
3136
|
+
const isActive = f === currentFilePath;
|
|
3137
|
+
return `<div class="file-item ${isActive ? 'active' : ''}" onclick="openFilePath(${JSON.stringify(f)})">
|
|
3138
|
+
<span class="file-icon">${isActive ? '▶' : '◦'}</span>
|
|
3139
|
+
<span class="file-name" title="${esc(f)}">${esc(name)}</span>
|
|
3140
|
+
</div>`;
|
|
3141
|
+
}).join('');
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
// ── Browse dialog ───────────────────────────────────────────
|
|
3145
|
+
let _browseDir = '';
|
|
3146
|
+
|
|
3147
|
+
async function showBrowseDialog() {
|
|
3148
|
+
document.getElementById('browse-overlay').classList.add('open');
|
|
3149
|
+
const homeDir = await getHomeDir();
|
|
3150
|
+
_browseDir = homeDir;
|
|
3151
|
+
await loadBrowseDir(_browseDir);
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
async function getHomeDir() {
|
|
3155
|
+
if (window.electronAPI) return window.electronAPI.homedir();
|
|
3156
|
+
const r = await api('/api/recent');
|
|
3157
|
+
if (r.files && r.files.length) {
|
|
3158
|
+
const parts = r.files[0].split(/[\\/]/);
|
|
3159
|
+
parts.pop();
|
|
3160
|
+
return parts.join('/') || '/';
|
|
3161
|
+
}
|
|
3162
|
+
return '/';
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
async function loadBrowseDir(dir) {
|
|
3166
|
+
_browseDir = dir;
|
|
3167
|
+
document.getElementById('browse-path-input').value = dir;
|
|
3168
|
+
const result = await api(`/api/browse?dir=${encodeURIComponent(dir)}`);
|
|
3169
|
+
if (result.error) return;
|
|
3170
|
+
const list = document.getElementById('browse-list');
|
|
3171
|
+
list.innerHTML = result.items.map(e => {
|
|
3172
|
+
const isSS = !e.isDir && e.name.endsWith('.ss');
|
|
3173
|
+
return `<div class="browse-entry" onclick="${e.isDir ? `loadBrowseDir(${JSON.stringify(e.path)})` : `openFilePath(${JSON.stringify(e.path)})`}">
|
|
3174
|
+
<span class="be-icon">${e.isDir ? '📁' : '📄'}</span>
|
|
3175
|
+
<span class="be-name ${isSS ? 'be-ss' : ''}">${esc(e.name)}</span>
|
|
3176
|
+
</div>`;
|
|
3177
|
+
}).join('') || '<div style="padding:16px;font-size:11px;color:var(--muted)">No .ss files here</div>';
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function closeBrowseDialog() {
|
|
3181
|
+
document.getElementById('browse-overlay').classList.remove('open');
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
function browseUp() {
|
|
3185
|
+
const parts = _browseDir.split(/[\\/]/);
|
|
3186
|
+
parts.pop();
|
|
3187
|
+
const parent = parts.join('/') || '/';
|
|
3188
|
+
if (parent !== _browseDir) loadBrowseDir(parent);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
function browseDirFromInput() {
|
|
3192
|
+
const val = document.getElementById('browse-path-input').value;
|
|
3193
|
+
if (val) loadBrowseDir(val);
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// ── Save As dialog ──────────────────────────────────────────
|
|
3197
|
+
function showSaveAsDialog(suggested) {
|
|
3198
|
+
document.getElementById('saveas-input').value = suggested || '';
|
|
3199
|
+
document.getElementById('saveas-overlay').classList.add('open');
|
|
3200
|
+
setTimeout(() => document.getElementById('saveas-input').focus(), 50);
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
function closeSaveAsDialog() {
|
|
3204
|
+
document.getElementById('saveas-overlay').classList.remove('open');
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
async function confirmSaveAs() {
|
|
3208
|
+
const p = document.getElementById('saveas-input').value.trim();
|
|
3209
|
+
if (!p) return;
|
|
3210
|
+
const path = p.endsWith('.ss') ? p : p + '.ss';
|
|
3211
|
+
closeSaveAsDialog();
|
|
3212
|
+
currentFilePath = path;
|
|
3213
|
+
currentFile = path.split(/[\\/]/).pop();
|
|
3214
|
+
document.getElementById('current-filename').textContent = currentFile;
|
|
3215
|
+
document.getElementById('fp-path').textContent = path;
|
|
3216
|
+
await doSave(path);
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
// ── Hook editor change for autosave ────────────────────────
|
|
3220
|
+
function editorChanged() {
|
|
3221
|
+
markUnsaved();
|
|
3222
|
+
updateHighlight();
|
|
3223
|
+
updateLineNumbers();
|
|
3224
|
+
setModified(true);
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
// ── Keyboard shortcuts ──────────────────────────────────────
|
|
3228
|
+
document.addEventListener('keydown', e => {
|
|
3229
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
3230
|
+
e.preventDefault();
|
|
3231
|
+
saveFile();
|
|
3232
|
+
}
|
|
3233
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'o') {
|
|
3234
|
+
e.preventDefault();
|
|
3235
|
+
openFile();
|
|
3236
|
+
}
|
|
3237
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
|
3238
|
+
e.preventDefault();
|
|
3239
|
+
newFile();
|
|
3240
|
+
}
|
|
3241
|
+
});
|
|
3242
|
+
|
|
3243
|
+
// ── Init ────────────────────────────────────────────────────
|
|
3244
|
+
window.addEventListener('load', async () => {
|
|
3245
|
+
// Check if a file was passed in the URL
|
|
3246
|
+
const params = new URLSearchParams(location.search);
|
|
3247
|
+
const initFile = params.get('file');
|
|
3248
|
+
if (initFile) {
|
|
3249
|
+
await openFilePath(initFile);
|
|
3250
|
+
} else {
|
|
3251
|
+
// Load most recent file, or create untitled
|
|
3252
|
+
const r = await api('/api/recent');
|
|
3253
|
+
if (r.files && r.files.length) {
|
|
3254
|
+
await openFilePath(r.files[0]);
|
|
3255
|
+
} else {
|
|
3256
|
+
await newFile();
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
refreshSidebar();
|
|
3260
|
+
});
|
|
3261
|
+
|
|
3262
|
+
// Hook editor input to markUnsaved (override plain updateEditor)
|
|
3263
|
+
const _origEditorInput = editor.oninput;
|
|
3264
|
+
editor.addEventListener('input', markUnsaved);
|
|
3265
|
+
|
|
3266
|
+
// Warn before closing with unsaved changes
|
|
3267
|
+
window.addEventListener('beforeunload', e => {
|
|
3268
|
+
if (unsavedChanges && currentFilePath) {
|
|
3269
|
+
e.preventDefault(); return '';
|
|
3270
|
+
}
|
|
3271
|
+
});
|
|
3272
|
+
|
|
3273
|
+
|
|
3274
|
+
</script>
|
|
3275
|
+
</body>
|
|
3276
|
+
</html>
|