structscript 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/structscript.js +2 -67
- package/lib/editor.html +1084 -476
- package/lib/interpreter.js +454 -21
- package/package.json +1 -1
package/lib/interpreter.js
CHANGED
|
@@ -51,6 +51,8 @@ class Interpreter {
|
|
|
51
51
|
this.structs = {};
|
|
52
52
|
this.callDepth = 0;
|
|
53
53
|
this.sourceLines = [];
|
|
54
|
+
this._sourceFile = null; // set by CLI to the running file's path
|
|
55
|
+
this._importedFiles = new Set(); // prevent circular imports
|
|
54
56
|
this._registerBuiltins();
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -62,25 +64,6 @@ class Interpreter {
|
|
|
62
64
|
G.define('print', a => { this.outputFn(a.map(x => this._str(x)).join(' ')); return null; });
|
|
63
65
|
G.define('warn', a => { this.warnFn(this._str(a[0])); return null; });
|
|
64
66
|
|
|
65
|
-
// input(prompt?) — synchronous stdin read, works like Python's input()
|
|
66
|
-
G.define('input', a => {
|
|
67
|
-
const prompt = a[0] !== undefined ? this._str(a[0]) : '';
|
|
68
|
-
if (prompt) process.stdout.write(prompt);
|
|
69
|
-
// Read synchronously from stdin one byte at a time until newline
|
|
70
|
-
const buf = Buffer.alloc(1);
|
|
71
|
-
let result = '';
|
|
72
|
-
while (true) {
|
|
73
|
-
let bytesRead = 0;
|
|
74
|
-
try { bytesRead = require('fs').readSync(process.stdin.fd, buf, 0, 1, null); }
|
|
75
|
-
catch (e) { break; }
|
|
76
|
-
if (bytesRead === 0) break;
|
|
77
|
-
const ch = buf.toString('utf8');
|
|
78
|
-
if (ch === '\n') break;
|
|
79
|
-
if (ch !== '\r') result += ch;
|
|
80
|
-
}
|
|
81
|
-
return result;
|
|
82
|
-
});
|
|
83
|
-
|
|
84
67
|
// Math
|
|
85
68
|
G.define('abs', a => Math.abs(a[0]));
|
|
86
69
|
G.define('sqrt', a => Math.sqrt(a[0]));
|
|
@@ -160,9 +143,99 @@ class Interpreter {
|
|
|
160
143
|
G.define('assert', a => { if (!a[0]) throw new SSError('Assertion failed' + (a[1] ? ': ' + a[1] : '')); return null; });
|
|
161
144
|
G.define('error', a => { throw new SSError(String(a[0] ?? 'Runtime error')); });
|
|
162
145
|
G.define('exit', a => { process.exit(a[0] ?? 0); });
|
|
146
|
+
|
|
147
|
+
// ── HTTP / API ─────────────────────────────────────────────────
|
|
148
|
+
// Synchronous HTTP using a child process so we don't need async
|
|
149
|
+
const _httpFetch = (url, method, body, headers) => {
|
|
150
|
+
const { execFileSync } = require('child_process');
|
|
151
|
+
const script = `
|
|
152
|
+
const h = require('https'), u = require('url'), http = require('http');
|
|
153
|
+
const parsed = new u.URL(${JSON.stringify('__URL__')}.replace('__URL__', process.argv[1]));
|
|
154
|
+
const lib = parsed.protocol === 'https:' ? h : http;
|
|
155
|
+
const opts = {
|
|
156
|
+
hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
157
|
+
path: parsed.pathname + parsed.search,
|
|
158
|
+
method: process.argv[2] || 'GET',
|
|
159
|
+
headers: JSON.parse(process.argv[3] || '{}'),
|
|
160
|
+
};
|
|
161
|
+
const bodyData = process.argv[4] || '';
|
|
162
|
+
if (bodyData) opts.headers['Content-Length'] = Buffer.byteLength(bodyData);
|
|
163
|
+
const req = lib.request(opts, res => {
|
|
164
|
+
let d = ''; res.on('data', c => d += c);
|
|
165
|
+
res.on('end', () => process.stdout.write(JSON.stringify({ status: res.statusCode, headers: res.headers, body: d })));
|
|
166
|
+
});
|
|
167
|
+
req.on('error', e => process.stdout.write(JSON.stringify({ error: e.message })));
|
|
168
|
+
if (bodyData) req.write(bodyData);
|
|
169
|
+
req.end();
|
|
170
|
+
`;
|
|
171
|
+
try {
|
|
172
|
+
const mergedHeaders = Object.assign({ 'User-Agent': 'StructScript/1.4' }, headers || {});
|
|
173
|
+
if (body && !mergedHeaders['Content-Type']) mergedHeaders['Content-Type'] = 'application/json';
|
|
174
|
+
const raw = execFileSync(process.execPath, ['-e', script, '--', url, method || 'GET',
|
|
175
|
+
JSON.stringify(mergedHeaders), body ? (typeof body === 'string' ? body : JSON.stringify(body)) : ''],
|
|
176
|
+
{ timeout: 15000, maxBuffer: 10 * 1024 * 1024 });
|
|
177
|
+
return JSON.parse(raw.toString());
|
|
178
|
+
} catch (e) {
|
|
179
|
+
throw new SSError('fetch error: ' + (e.message || String(e)));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// fetch(url) → returns response object { status, body, json() }
|
|
184
|
+
G.define('fetch', a => {
|
|
185
|
+
const url = String(a[0] ?? '');
|
|
186
|
+
const options = (a[1] && typeof a[1] === 'object') ? a[1] : {};
|
|
187
|
+
const method = options.method || 'GET';
|
|
188
|
+
const body = options.body || null;
|
|
189
|
+
const headers = options.headers || {};
|
|
190
|
+
const res = _httpFetch(url, method, body, headers);
|
|
191
|
+
if (res.error) throw new SSError('fetch failed: ' + res.error);
|
|
192
|
+
// Try to auto-parse JSON
|
|
193
|
+
let parsed = null;
|
|
194
|
+
try { parsed = JSON.parse(res.body); } catch (_) {}
|
|
195
|
+
return {
|
|
196
|
+
__struct: 'Response',
|
|
197
|
+
status: res.status,
|
|
198
|
+
ok: res.status >= 200 && res.status < 300,
|
|
199
|
+
body: res.body,
|
|
200
|
+
data: parsed, // auto-parsed JSON (or nothing if not JSON)
|
|
201
|
+
headers: res.headers,
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// fetchJson(url, options?) → directly returns parsed JSON data
|
|
206
|
+
G.define('fetchJson', a => {
|
|
207
|
+
const url = String(a[0] ?? '');
|
|
208
|
+
const options = (a[1] && typeof a[1] === 'object') ? a[1] : {};
|
|
209
|
+
const res = _httpFetch(url, options.method || 'GET', options.body || null,
|
|
210
|
+
Object.assign({ 'Accept': 'application/json' }, options.headers || {}));
|
|
211
|
+
if (res.error) throw new SSError('fetchJson failed: ' + res.error);
|
|
212
|
+
try { return JSON.parse(res.body); }
|
|
213
|
+
catch (_) { throw new SSError('fetchJson: response is not valid JSON\n' + res.body.slice(0, 200)); }
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// fetchPost(url, data, headers?) → POST with JSON body, returns parsed response
|
|
217
|
+
G.define('fetchPost', a => {
|
|
218
|
+
const url = String(a[0] ?? '');
|
|
219
|
+
const data = a[1] !== undefined ? a[1] : {};
|
|
220
|
+
const headers = (a[2] && typeof a[2] === 'object') ? a[2] : {};
|
|
221
|
+
const body = typeof data === 'string' ? data : JSON.stringify(data);
|
|
222
|
+
const res = _httpFetch(url, 'POST', body, Object.assign({ 'Accept': 'application/json' }, headers));
|
|
223
|
+
if (res.error) throw new SSError('fetchPost failed: ' + res.error);
|
|
224
|
+
let parsed = null;
|
|
225
|
+
try { parsed = JSON.parse(res.body); } catch (_) {}
|
|
226
|
+
return {
|
|
227
|
+
__struct: 'Response',
|
|
228
|
+
status: res.status,
|
|
229
|
+
ok: res.status >= 200 && res.status < 300,
|
|
230
|
+
body: res.body,
|
|
231
|
+
data: parsed,
|
|
232
|
+
headers: res.headers,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
163
235
|
}
|
|
164
236
|
|
|
165
|
-
run(source) {
|
|
237
|
+
run(source, sourceFile) {
|
|
238
|
+
if (sourceFile) this._sourceFile = sourceFile;
|
|
166
239
|
this.sourceLines = source.split('\n');
|
|
167
240
|
this._execBlock(this.sourceLines, 0, this.sourceLines.length, this.globals);
|
|
168
241
|
}
|
|
@@ -206,6 +279,38 @@ class Interpreter {
|
|
|
206
279
|
}
|
|
207
280
|
|
|
208
281
|
_exec(line, allLines, lineIdx, indent, env) {
|
|
282
|
+
// import "file.ss"
|
|
283
|
+
if (line.startsWith('import ')) {
|
|
284
|
+
const m = line.match(/^import\s+"([^"]+)"|^import\s+'([^']+)'/);
|
|
285
|
+
if (!m) throw new SSError('Invalid import — use: import "filename.ss"');
|
|
286
|
+
const importPath = m[1] || m[2];
|
|
287
|
+
const fs = require('fs');
|
|
288
|
+
const path = require('path');
|
|
289
|
+
|
|
290
|
+
// Resolve relative to the currently running file, or cwd
|
|
291
|
+
const base = this._sourceFile ? path.dirname(this._sourceFile) : process.cwd();
|
|
292
|
+
const absPath = path.resolve(base, importPath);
|
|
293
|
+
|
|
294
|
+
if (this._importedFiles.has(absPath)) return null; // already imported
|
|
295
|
+
this._importedFiles.add(absPath);
|
|
296
|
+
|
|
297
|
+
if (!fs.existsSync(absPath)) throw new SSError(`import: file not found: "${importPath}" (resolved to ${absPath})`);
|
|
298
|
+
const source = fs.readFileSync(absPath, 'utf8');
|
|
299
|
+
|
|
300
|
+
// Run the imported file in the same global environment
|
|
301
|
+
const savedSourceFile = this._sourceFile;
|
|
302
|
+
const savedSourceLines = this.sourceLines;
|
|
303
|
+
this._sourceFile = absPath;
|
|
304
|
+
this.sourceLines = source.split('\n');
|
|
305
|
+
try {
|
|
306
|
+
this._execBlock(this.sourceLines, 0, this.sourceLines.length, env);
|
|
307
|
+
} finally {
|
|
308
|
+
this._sourceFile = savedSourceFile;
|
|
309
|
+
this.sourceLines = savedSourceLines;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
209
314
|
// say shorthand
|
|
210
315
|
if (line.startsWith('say ')) { const v = this._eval(line.slice(4).trim(), env); this.outputFn(this._str(v)); return null; }
|
|
211
316
|
|
|
@@ -736,4 +841,332 @@ class Interpreter {
|
|
|
736
841
|
}
|
|
737
842
|
}
|
|
738
843
|
|
|
739
|
-
|
|
844
|
+
// ── Web Engine ──────────────────────────────────────────────────────────────
|
|
845
|
+
|
|
846
|
+
function isWebCode(code) {
|
|
847
|
+
return /^\s*(page\b|add\s+\w|css\s*:?$|script\s*:?$)/m.test(code);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function buildWebDoc(rawCode) {
|
|
851
|
+
// Normalise line endings
|
|
852
|
+
const code = rawCode.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
853
|
+
|
|
854
|
+
let pageTitle = 'StructScript Page';
|
|
855
|
+
let pageLang = 'en';
|
|
856
|
+
let pageStyles = {};
|
|
857
|
+
let headTags = [];
|
|
858
|
+
let cssBlocks = [];
|
|
859
|
+
let jsBlocks = [];
|
|
860
|
+
let bodyAttrs = {};
|
|
861
|
+
const elements = [];
|
|
862
|
+
const elStack = [];
|
|
863
|
+
let i = 0;
|
|
864
|
+
let _uid = 0;
|
|
865
|
+
const lines = code.split('\n');
|
|
866
|
+
|
|
867
|
+
function indent(l) { let n=0; while(n<l.length&&l[n]===' ')n++; return n; }
|
|
868
|
+
function str(s) { s=s.trim(); return (s[0]==='"'&&s[s.length-1]==='"')||(s[0]==="'"&&s[s.length-1]==="'") ? s.slice(1,-1) : s; }
|
|
869
|
+
function kebab(s) { return s.replace(/([A-Z])/g,'-$1').toLowerCase(); }
|
|
870
|
+
function camel(s) { return s.replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); }
|
|
871
|
+
function uid() { return '_ss'+(++_uid); }
|
|
872
|
+
|
|
873
|
+
// ── parse one line, advancing i if it consumes more ──────────
|
|
874
|
+
while (i < lines.length) {
|
|
875
|
+
const raw = lines[i], t = raw.trim(), ind = indent(raw);
|
|
876
|
+
i++;
|
|
877
|
+
if (!t || t.startsWith('//')) continue;
|
|
878
|
+
|
|
879
|
+
// page "Title" lang? :
|
|
880
|
+
if (/^page\b/.test(t)) {
|
|
881
|
+
const m = t.match(/^page\s+"([^"]*)"|^page\s+'([^']*)'/);
|
|
882
|
+
if (m) pageTitle = m[1]||m[2];
|
|
883
|
+
const lm = t.match(/\blang\s+"([^"]+)"/);
|
|
884
|
+
if (lm) pageLang = lm[1];
|
|
885
|
+
// consume indented page-level directives
|
|
886
|
+
while (i < lines.length) {
|
|
887
|
+
const nr = lines[i].trim(), ni = indent(lines[i]);
|
|
888
|
+
if (!nr || nr.startsWith('//')) { i++; continue; }
|
|
889
|
+
if (ni <= ind) break;
|
|
890
|
+
i++;
|
|
891
|
+
let m2;
|
|
892
|
+
if ((m2=nr.match(/^style\s+([\w-]+)\s+(.+)$/))) { pageStyles[camel(m2[1])]=str(m2[2]); continue; }
|
|
893
|
+
if ((m2=nr.match(/^charset\s+(.+)$/))) { headTags.push(`<meta charset="${str(m2[1])}">`); continue; }
|
|
894
|
+
if ((m2=nr.match(/^viewport\s+(.+)$/))) { headTags.push(`<meta name="viewport" content="${str(m2[1])}">`); continue; }
|
|
895
|
+
if ((m2=nr.match(/^meta\s+([\w-]+)\s+(.+)$/))) { headTags.push(`<meta name="${m2[1]}" content="${str(m2[2])}">`); continue; }
|
|
896
|
+
if ((m2=nr.match(/^metahttp\s+([\w-]+)\s+(.+)$/))) { headTags.push(`<meta http-equiv="${m2[1]}" content="${str(m2[2])}">`); continue; }
|
|
897
|
+
if ((m2=nr.match(/^link\s+(.+)$/))) { headTags.push(`<link ${str(m2[1])}>`); continue; }
|
|
898
|
+
if ((m2=nr.match(/^favicon\s+(.+)$/))) { headTags.push(`<link rel="icon" href="${str(m2[1])}">`); continue; }
|
|
899
|
+
if ((m2=nr.match(/^font\s+(.+)$/))) { headTags.push(`<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=${str(m2[1]).replace(/ /g,'+')}:wght@300;400;600;700;800&display=swap">`); continue; }
|
|
900
|
+
if ((m2=nr.match(/^script\s+src\s+(.+)$/))) { headTags.push(`<script src="${str(m2[1])}"><\/script>`); continue; }
|
|
901
|
+
if ((m2=nr.match(/^bodyattr\s+([\w-]+)\s+(.+)$/))) { bodyAttrs[m2[1]]=str(m2[2]); continue; }
|
|
902
|
+
if ((m2=nr.match(/^(og|twitter):([\w:]+)\s+(.+)$/))){ headTags.push(`<meta property="${m2[1]}:${m2[2]}" content="${str(m2[3])}">`); continue; }
|
|
903
|
+
if ((m2=nr.match(/^canonical\s+(.+)$/))) { headTags.push(`<link rel="canonical" href="${str(m2[1])}">`); continue; }
|
|
904
|
+
if ((m2=nr.match(/^base\s+(.+)$/))) { headTags.push(`<base href="${str(m2[1])}">`); continue; }
|
|
905
|
+
}
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// css: raw CSS block
|
|
910
|
+
if (/^css\s*:?$/.test(t)) {
|
|
911
|
+
const buf=[];
|
|
912
|
+
while(i<lines.length){ const nr=lines[i],ni=indent(nr); if(!nr.trim()||ni>ind){buf.push(nr.slice(Math.min(ni,ind+2)));i++;}else break; }
|
|
913
|
+
cssBlocks.push(buf.join('\n'));
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// script: raw JS block
|
|
918
|
+
if (/^script\s*:?$/.test(t)) {
|
|
919
|
+
const buf=[];
|
|
920
|
+
while(i<lines.length){ const nr=lines[i],ni=indent(nr); if(!nr.trim()||ni>ind){buf.push(nr.slice(Math.min(ni,ind+2)));i++;}else break; }
|
|
921
|
+
jsBlocks.push(buf.join('\n'));
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// add TAG "id/classes": — the workhorse
|
|
926
|
+
// Supports: add div "myId" add div ".btn.active" add div "#main .hero big"
|
|
927
|
+
const addM = t.match(/^add\s+([\w-]+)(?:\s+"([^"]*)")?(?:\s+'([^']*)')?\s*:?$/);
|
|
928
|
+
if (addM) {
|
|
929
|
+
while (elStack.length>1 && elStack[elStack.length-1].indent>=ind) elStack.pop();
|
|
930
|
+
const parent = elStack.length ? elStack[elStack.length-1].el : null;
|
|
931
|
+
const rawSel = (addM[2]||addM[3]||'').trim();
|
|
932
|
+
const el = {
|
|
933
|
+
tag:addM[1].toLowerCase(), id:'', classes:[],
|
|
934
|
+
text:'', html:'',
|
|
935
|
+
styles:{}, pseudo:{}, mediaRules:[],
|
|
936
|
+
beforeContent:null, afterContent:null,
|
|
937
|
+
attrs:{}, events:[], children:[],
|
|
938
|
+
_indent:ind, _anim:null,
|
|
939
|
+
};
|
|
940
|
+
// parse selector tokens: #id .class plain-word→id
|
|
941
|
+
rawSel.split(/\s+/).filter(Boolean).forEach(tok => {
|
|
942
|
+
if (tok.startsWith('#')) el.id = tok.slice(1);
|
|
943
|
+
else if (tok.startsWith('.')) el.classes.push(tok.slice(1));
|
|
944
|
+
else if (!el.id) el.id = tok;
|
|
945
|
+
else el.classes.push(tok);
|
|
946
|
+
});
|
|
947
|
+
if (parent && !parent._isPage) parent.children.push(el);
|
|
948
|
+
else elements.push(el);
|
|
949
|
+
elStack.push({el, indent:ind});
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ── properties inside an element ─────────────────────────
|
|
954
|
+
const pe = elStack[elStack.length-1];
|
|
955
|
+
if (!pe || ind <= pe.indent) continue;
|
|
956
|
+
const el = pe.el;
|
|
957
|
+
|
|
958
|
+
let m2;
|
|
959
|
+
|
|
960
|
+
// content
|
|
961
|
+
if ((m2=t.match(/^text\s+(.+)$/))) { el.text=str(m2[1]); continue; }
|
|
962
|
+
if ((m2=t.match(/^html\s+(.+)$/))) { el.html=str(m2[1]); continue; }
|
|
963
|
+
|
|
964
|
+
// common attrs — shorthand keywords
|
|
965
|
+
const ATTR_KW = {
|
|
966
|
+
src:1,href:1,alt:1,title:1,type:1,name:1,value:1,
|
|
967
|
+
for:1,action:1,method:1,target:1,rel:1,rows:1,cols:1,
|
|
968
|
+
min:1,max:1,step:1,role:1,tabindex:1,colspan:1,rowspan:1,
|
|
969
|
+
width:1,height:1,loading:1,decoding:1,crossorigin:1,
|
|
970
|
+
enctype:1,accept:1,pattern:1,maxlength:1,minlength:1,
|
|
971
|
+
download:1,sizes:1,srcset:1,poster:1,preload:1,
|
|
972
|
+
sandbox:1,allow:1,frameborder:1,scrolling:1,
|
|
973
|
+
};
|
|
974
|
+
if ((m2=t.match(/^([\w-]+)\s+(.+)$/)) && ATTR_KW[m2[1]]) { el.attrs[m2[1]]=str(m2[2]); continue; }
|
|
975
|
+
|
|
976
|
+
// boolean attrs
|
|
977
|
+
if (/^(checked|disabled|readonly|required|autoplay|loop|controls|muted|multiple|autofocus|hidden|open|selected|novalidate|async|defer|reversed|ismap|allowfullscreen|default|formnovalidate|spellcheck)$/.test(t))
|
|
978
|
+
{ el.attrs[t]=''; continue; }
|
|
979
|
+
|
|
980
|
+
// aria-*, data-*
|
|
981
|
+
if ((m2=t.match(/^aria-([\w-]+)\s+(.+)$/))) { el.attrs[`aria-${m2[1]}`]=str(m2[2]); continue; }
|
|
982
|
+
if ((m2=t.match(/^data-([\w-]+)\s+(.+)$/))) { el.attrs[`data-${m2[1]}`]=str(m2[2]); continue; }
|
|
983
|
+
|
|
984
|
+
// generic attr fallback
|
|
985
|
+
if ((m2=t.match(/^attr\s+([\w-]+)\s+(.+)$/))) { el.attrs[m2[1]]=str(m2[2]); continue; }
|
|
986
|
+
|
|
987
|
+
// class
|
|
988
|
+
if ((m2=t.match(/^class\s+(.+)$/))) { str(m2[1]).split(/\s+/).forEach(c=>c&&el.classes.push(c)); continue; }
|
|
989
|
+
|
|
990
|
+
// style — any CSS property, camelCase or kebab
|
|
991
|
+
if ((m2=t.match(/^style\s+([\w-]+)\s+(.+)$/))) { el.styles[camel(m2[1])]=str(m2[2]); continue; }
|
|
992
|
+
|
|
993
|
+
// pseudo-class styles: hover / focus / active / visited / checked / disabled / focusVisible / focusWithin / placeholder / selection
|
|
994
|
+
if ((m2=t.match(/^(hover|focus|active|visited|checked|disabled|focus-visible|focus-within|placeholder|first-child|last-child|nth-child|not|invalid|valid)\s+([\w-]+)\s+(.+)$/)))
|
|
995
|
+
{ if(!el.pseudo[m2[1]])el.pseudo[m2[1]]={}; el.pseudo[m2[1]][camel(m2[2])]=str(m2[3]); continue; }
|
|
996
|
+
|
|
997
|
+
// pseudo-elements: before / after / first-line / first-letter / marker / selection / backdrop
|
|
998
|
+
if ((m2=t.match(/^before\s+(.+)$/))) { el.beforeContent=str(m2[1]); continue; }
|
|
999
|
+
if ((m2=t.match(/^after\s+(.+)$/))) { el.afterContent=str(m2[1]); continue; }
|
|
1000
|
+
|
|
1001
|
+
// shorthand style helpers
|
|
1002
|
+
if ((m2=t.match(/^transition\s+(.+)$/))) { el.styles.transition=str(m2[1]); continue; }
|
|
1003
|
+
if ((m2=t.match(/^transform\s+(.+)$/))) { el.styles.transform=str(m2[1]); continue; }
|
|
1004
|
+
if ((m2=t.match(/^filter\s+(.+)$/))) { el.styles.filter=str(m2[1]); continue; }
|
|
1005
|
+
if ((m2=t.match(/^grid\s+(.+)$/))) { el.styles.gridTemplateColumns=str(m2[1]); continue; }
|
|
1006
|
+
if ((m2=t.match(/^flex\s+(.+)$/))) { el.styles.flex=str(m2[1]); continue; }
|
|
1007
|
+
if ((m2=t.match(/^gap\s+(.+)$/))) { el.styles.gap=str(m2[1]); continue; }
|
|
1008
|
+
if ((m2=t.match(/^bg\s+(.+)$/))) { el.styles.background=str(m2[1]); continue; }
|
|
1009
|
+
if ((m2=t.match(/^shadow\s+(.+)$/))) { el.styles.boxShadow=str(m2[1]); continue; }
|
|
1010
|
+
if ((m2=t.match(/^clip\s+(.+)$/))) { el.styles.clipPath=str(m2[1]); continue; }
|
|
1011
|
+
if ((m2=t.match(/^mask\s+(.+)$/))) { el.styles.mask=str(m2[1]); continue; }
|
|
1012
|
+
if ((m2=t.match(/^outline\s+(.+)$/))) { el.styles.outline=str(m2[1]); continue; }
|
|
1013
|
+
if ((m2=t.match(/^overflow\s+(.+)$/))) { el.styles.overflow=str(m2[1]); continue; }
|
|
1014
|
+
if ((m2=t.match(/^cursor\s+(.+)$/))) { el.styles.cursor=str(m2[1]); continue; }
|
|
1015
|
+
if ((m2=t.match(/^opacity\s+(.+)$/))) { el.styles.opacity=str(m2[1]); continue; }
|
|
1016
|
+
if ((m2=t.match(/^zindex\s+(.+)$/))) { el.styles.zIndex=str(m2[1]); continue; }
|
|
1017
|
+
if ((m2=t.match(/^var\s+(--[\w-]+)\s+(.+)$/))) { el.styles[m2[1]]=str(m2[2]); continue; }
|
|
1018
|
+
|
|
1019
|
+
// @media per-element: media "max-width:600px" prop value
|
|
1020
|
+
if ((m2=t.match(/^media\s+"([^"]+)"\s+([\w-]+)\s+(.+)$/)))
|
|
1021
|
+
{ el.mediaRules.push({q:m2[1], p:camel(m2[2]), v:str(m2[3])}); continue; }
|
|
1022
|
+
// media "value" (HTML attribute, e.g. on <link> or <source>)
|
|
1023
|
+
if ((m2=t.match(/^media\s+(.+)$/)) && !m2[1].trim().match(/^"[^"]*"\s+[\w-]+/))
|
|
1024
|
+
{ el.attrs.media=str(m2[1]); continue; }
|
|
1025
|
+
// placeholder "text" as HTML attribute (not pseudo-class)
|
|
1026
|
+
if ((m2=t.match(/^placeholder\s+(.+)$/)) && !m2[1].match(/^[\w-]/))
|
|
1027
|
+
{ el.attrs.placeholder=str(m2[1]); continue; }
|
|
1028
|
+
|
|
1029
|
+
// animate NAME DURATION? EASING?
|
|
1030
|
+
if ((m2=t.match(/^animate\s+(\w+)(?:\s+([\d.]+))?(?:\s+([\w-]+))?$/)))
|
|
1031
|
+
{ el._anim={name:m2[1], dur:parseFloat(m2[2]||0.4), ease:m2[3]||'ease'}; continue; }
|
|
1032
|
+
|
|
1033
|
+
// on EVENT: — raw JS handler, indented body
|
|
1034
|
+
if ((m2=t.match(/^on\s+([\w]+)\s*:?$/))) {
|
|
1035
|
+
const buf=[];
|
|
1036
|
+
while(i<lines.length){ const nr=lines[i],ni=indent(nr); if(!nr.trim()||ni<=ind)break; buf.push(nr.slice(ni)); i++; }
|
|
1037
|
+
el.events.push({ev:m2[1], code:buf.join('\n')});
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// ── Render ────────────────────────────────────────────────
|
|
1043
|
+
const pseudoCSS = [];
|
|
1044
|
+
const mediaCSS = {};
|
|
1045
|
+
|
|
1046
|
+
const VOID = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
|
|
1047
|
+
|
|
1048
|
+
function ensureId(el) { if (!el.id) el.id=uid(); return el.id; }
|
|
1049
|
+
|
|
1050
|
+
function renderEl(el) {
|
|
1051
|
+
const tag = el.tag;
|
|
1052
|
+
|
|
1053
|
+
// Build inline style string
|
|
1054
|
+
let styleStr = Object.entries(el.styles).map(([k,v])=>`${kebab(k)}:${v}`).join(';');
|
|
1055
|
+
|
|
1056
|
+
// Animations
|
|
1057
|
+
if (el._anim) {
|
|
1058
|
+
const ANIMS = {
|
|
1059
|
+
fadeIn:'fadeIn',fadeOut:'fadeOut',
|
|
1060
|
+
slideIn:'slideIn',slideInRight:'slideInRight',
|
|
1061
|
+
slideUp:'slideUp',slideDown:'slideDown',
|
|
1062
|
+
pop:'pop',bounce:'bounce',spin:'spin',
|
|
1063
|
+
pulse:'pulse',shake:'shake',flip:'flip',
|
|
1064
|
+
zoom:'zoom',wiggle:'wiggle',typewriter:'typewriter',
|
|
1065
|
+
};
|
|
1066
|
+
const aname = ANIMS[el._anim.name]||el._anim.name;
|
|
1067
|
+
const fill = /^(bounce|spin|pulse)$/.test(el._anim.name)?'infinite':'both';
|
|
1068
|
+
const ease = el._anim.name==='spin'?'linear':el._anim.ease;
|
|
1069
|
+
styleStr += (styleStr?';':'')+`animation:${aname} ${el._anim.dur}s ${ease} ${fill}`;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Pseudo-class rules
|
|
1073
|
+
Object.entries(el.pseudo).forEach(([pseudo, styles]) => {
|
|
1074
|
+
const r = Object.entries(styles).map(([k,v])=>`${kebab(k)}:${v}`).join(';');
|
|
1075
|
+
if (r) { ensureId(el); pseudoCSS.push(`#${el.id}:${pseudo}{${r}}`); }
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
// before/after pseudo-elements
|
|
1079
|
+
if (el.beforeContent!==null) { ensureId(el); pseudoCSS.push(`#${el.id}::before{content:"${el.beforeContent.replace(/"/g,'\\"')}"}`); }
|
|
1080
|
+
if (el.afterContent!==null) { ensureId(el); pseudoCSS.push(`#${el.id}::after{content:"${el.afterContent.replace(/"/g,'\\"')}"}`); }
|
|
1081
|
+
|
|
1082
|
+
// @media rules
|
|
1083
|
+
el.mediaRules.forEach(({q,p,v}) => {
|
|
1084
|
+
const s = el.id ? `#${el.id}` : (el.classes[0] ? `.${el.classes[0]}` : tag);
|
|
1085
|
+
if (!mediaCSS[q]) mediaCSS[q]=[];
|
|
1086
|
+
mediaCSS[q].push(`${s}{${kebab(p)}:${v}}`);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// Build attribute string
|
|
1090
|
+
const idA = el.id ? ` id="${el.id}"` : '';
|
|
1091
|
+
const clsA = el.classes.length ? ` class="${el.classes.join(' ')}"` : '';
|
|
1092
|
+
const styA = styleStr ? ` style="${styleStr}"` : '';
|
|
1093
|
+
const xtra = Object.entries(el.attrs).map(([k,v])=>v===''?` ${k}`:` ${k}="${v}"`).join('');
|
|
1094
|
+
const evts = el.events.map(({ev,code})=>{
|
|
1095
|
+
return ` data-ss-${ev}="${code.replace(/"/g,'"').replace(/\n/g,' ')}"`;
|
|
1096
|
+
}).join('');
|
|
1097
|
+
|
|
1098
|
+
if (VOID.has(tag)) return `<${tag}${idA}${clsA}${styA}${xtra}>\n`;
|
|
1099
|
+
const inner = el.html || el.text || el.children.map(renderEl).join('');
|
|
1100
|
+
return `<${tag}${idA}${clsA}${styA}${xtra}${evts}>${inner}</${tag}>\n`;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const bodyStyleStr = Object.entries(pageStyles).map(([k,v])=>`${kebab(k)}:${v}`).join(';');
|
|
1104
|
+
const bodyXtra = Object.entries(bodyAttrs).map(([k,v])=>` ${k}="${v}"`).join('');
|
|
1105
|
+
const bodyHtml = elements.map(renderEl).join('');
|
|
1106
|
+
|
|
1107
|
+
const mediaCSSStr = Object.entries(mediaCSS)
|
|
1108
|
+
.map(([q,rules])=>`@media (${q}){${rules.join('')}}`).join('\n');
|
|
1109
|
+
|
|
1110
|
+
const KEYFRAMES = `
|
|
1111
|
+
@keyframes fadeIn {0%{opacity:0;transform:translateY(16px)}100%{opacity:1;transform:none}}
|
|
1112
|
+
@keyframes fadeOut {0%{opacity:1}100%{opacity:0}}
|
|
1113
|
+
@keyframes slideIn {0%{transform:translateX(-40px);opacity:0}100%{transform:none;opacity:1}}
|
|
1114
|
+
@keyframes slideInRight {0%{transform:translateX(40px);opacity:0}100%{transform:none;opacity:1}}
|
|
1115
|
+
@keyframes slideUp {0%{transform:translateY(40px);opacity:0}100%{transform:none;opacity:1}}
|
|
1116
|
+
@keyframes slideDown {0%{transform:translateY(-40px);opacity:0}100%{transform:none;opacity:1}}
|
|
1117
|
+
@keyframes bounce {0%,100%{transform:translateY(0)}50%{transform:translateY(-18px)}}
|
|
1118
|
+
@keyframes pulse {0%,100%{opacity:1}50%{opacity:0.4}}
|
|
1119
|
+
@keyframes spin {to{transform:rotate(360deg)}}
|
|
1120
|
+
@keyframes shake {0%,100%{transform:translateX(0)}20%{transform:translateX(-8px)}40%{transform:translateX(8px)}60%{transform:translateX(-5px)}80%{transform:translateX(5px)}}
|
|
1121
|
+
@keyframes pop {0%{transform:scale(0.5);opacity:0}70%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}
|
|
1122
|
+
@keyframes flip {0%{transform:rotateY(-90deg);opacity:0}100%{transform:rotateY(0);opacity:1}}
|
|
1123
|
+
@keyframes zoom {0%{transform:scale(0);opacity:0}100%{transform:scale(1);opacity:1}}
|
|
1124
|
+
@keyframes wiggle {0%,100%{transform:rotate(0)}25%{transform:rotate(-8deg)}75%{transform:rotate(8deg)}}
|
|
1125
|
+
@keyframes typewriter {from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}`;
|
|
1126
|
+
|
|
1127
|
+
// All DOM events wired up
|
|
1128
|
+
const ALL_EVENTS = [
|
|
1129
|
+
'click','dblclick','contextmenu',
|
|
1130
|
+
'mousedown','mouseup','mouseover','mouseout','mouseenter','mouseleave','mousemove',
|
|
1131
|
+
'keydown','keyup','keypress',
|
|
1132
|
+
'input','change','focus','blur','submit','reset','select',
|
|
1133
|
+
'scroll','resize','wheel',
|
|
1134
|
+
'dragstart','drag','dragend','dragover','dragenter','dragleave','drop',
|
|
1135
|
+
'touchstart','touchmove','touchend','touchcancel',
|
|
1136
|
+
'pointerdown','pointermove','pointerup','pointercancel','pointerenter','pointerleave',
|
|
1137
|
+
'animationstart','animationend','animationiteration',
|
|
1138
|
+
'transitionstart','transitionend',
|
|
1139
|
+
'load','error','abort','canplay','play','pause','ended','timeupdate','volumechange',
|
|
1140
|
+
'copy','cut','paste','beforeinput',
|
|
1141
|
+
'fullscreenchange','visibilitychange',
|
|
1142
|
+
];
|
|
1143
|
+
const evScript = ALL_EVENTS.map(ev =>
|
|
1144
|
+
`document.querySelectorAll('[data-ss-${ev}]').forEach(function(el){el.addEventListener('${ev}',function(event){try{(new Function('event',this.getAttribute('data-ss-${ev}'))).call(this,event)}catch(e){console.error(e)}}.bind(el));});`
|
|
1145
|
+
).join('\n');
|
|
1146
|
+
|
|
1147
|
+
return `<!DOCTYPE html>
|
|
1148
|
+
<html lang="${pageLang}">
|
|
1149
|
+
<head>
|
|
1150
|
+
<meta charset="UTF-8">
|
|
1151
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1152
|
+
<title>${pageTitle}</title>
|
|
1153
|
+
${headTags.join('\n')}
|
|
1154
|
+
<style>
|
|
1155
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
1156
|
+
${KEYFRAMES}
|
|
1157
|
+
${pseudoCSS.join('\n')}
|
|
1158
|
+
${mediaCSSStr}
|
|
1159
|
+
${cssBlocks.join('\n')}
|
|
1160
|
+
</style>
|
|
1161
|
+
</head>
|
|
1162
|
+
<body${bodyStyleStr?` style="${bodyStyleStr}"`:''}>
|
|
1163
|
+
${bodyHtml}<script>(function(){
|
|
1164
|
+
${evScript}
|
|
1165
|
+
${jsBlocks.join('\n')}
|
|
1166
|
+
})()</script>
|
|
1167
|
+
</body>
|
|
1168
|
+
</html>`;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
module.exports = { Interpreter, SSError, Environment, isWebCode, buildWebDoc };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "structscript",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "The StructScript programming language — clean, readable scripting for everyone. Includes a built-in visual editor.",
|
|
5
5
|
"keywords": ["structscript", "language", "interpreter", "scripting", "programming-language"],
|
|
6
6
|
"author": "StructScript",
|