@firesmasher/velox.js 1.0.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/index.js +3 -0
- package/docs/GUIDE.md +333 -0
- package/docs/README.md +140 -0
- package/docs/SYNTAXES.md +335 -0
- package/lib/cli.js +90 -0
- package/lib/scaffold.js +102 -0
- package/lib/templates.js +1400 -0
- package/package.json +25 -0
package/lib/templates.js
ADDED
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
export const templates = {
|
|
2
|
+
|
|
3
|
+
config: (a) => `// Velox.js Configuration File
|
|
4
|
+
// Project: ${a.projectName}
|
|
5
|
+
// Important — configure paths precisely before use.
|
|
6
|
+
{
|
|
7
|
+
// Core paths
|
|
8
|
+
"tagsFolder": "../../tags",
|
|
9
|
+
"pagesFolder": "../../public",
|
|
10
|
+
|
|
11
|
+
// URL registry
|
|
12
|
+
"localUrlJson": "../../tags/json/UrlS.json",
|
|
13
|
+
|
|
14
|
+
// Variable storage
|
|
15
|
+
"variableStorage": "../../storage/variables/normal.json",
|
|
16
|
+
"SHA1VariableStorage": "../../storage/variables/sha1.json",
|
|
17
|
+
|
|
18
|
+
// Assets
|
|
19
|
+
"mainCssLocation": "../../assets/css/style.css",
|
|
20
|
+
"savedFilesPath": "../../tags/savedFiles",
|
|
21
|
+
|
|
22
|
+
// Icons (not automatically created — add your own files here)
|
|
23
|
+
"icons": {
|
|
24
|
+
"icon.png": "../../assets/icons/png/icon.png",
|
|
25
|
+
"icon.svg": "../../assets/icons/svg/icon.svg"
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// Quick path imports — lets you use @env:key in .vlx files
|
|
29
|
+
"QuickPathsVariableStorage": "../../storage/env/quickPaths.json"
|
|
30
|
+
}
|
|
31
|
+
`,
|
|
32
|
+
|
|
33
|
+
quickPaths: () => `{}
|
|
34
|
+
`,
|
|
35
|
+
|
|
36
|
+
urlS: () => `{}
|
|
37
|
+
`,
|
|
38
|
+
|
|
39
|
+
css: (a) => `/* Velox.js — Main Stylesheet */
|
|
40
|
+
/* Project: ${a.projectName} */
|
|
41
|
+
|
|
42
|
+
*, *::before, *::after {
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
margin: 0;
|
|
45
|
+
padding: 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:root {
|
|
49
|
+
--velox-bg: #0d0d0d;
|
|
50
|
+
--velox-surface: #161616;
|
|
51
|
+
--velox-accent: #00c2ff;
|
|
52
|
+
--velox-text: #e8e8e8;
|
|
53
|
+
--velox-muted: #6b6b6b;
|
|
54
|
+
--velox-font: system-ui, -apple-system, sans-serif;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
body {
|
|
58
|
+
background: var(--velox-bg);
|
|
59
|
+
color: var(--velox-text);
|
|
60
|
+
font-family: var(--velox-font);
|
|
61
|
+
line-height: 1.6;
|
|
62
|
+
padding: 2rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
button {
|
|
66
|
+
background: var(--velox-accent);
|
|
67
|
+
color: #000;
|
|
68
|
+
border: none;
|
|
69
|
+
padding: .5rem 1.2rem;
|
|
70
|
+
border-radius: 6px;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
font-size: 1rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
button:hover { opacity: .85; }
|
|
76
|
+
`,
|
|
77
|
+
|
|
78
|
+
// ── How to connect a .vlx to HTML ──────────────────────────────────────────
|
|
79
|
+
//
|
|
80
|
+
// Option 1 — inline tag (runs at page load):
|
|
81
|
+
// <?velox Dom.setText("Hello") $target="$id(myDiv)" >
|
|
82
|
+
//
|
|
83
|
+
// Option 2 — run a .vlx file at page load:
|
|
84
|
+
// <?velox Velox.run("../assets/scripts/velox/myscript.vlx") $target="$id(out)" >
|
|
85
|
+
//
|
|
86
|
+
// Option 3 — event listener (runs on user interaction):
|
|
87
|
+
// <?velox $on click "$id(myBtn)": Dom.setText("Clicked!") $target="$id(out)" >
|
|
88
|
+
// <?velox $on click "$id(myBtn)": Velox.run("myscript.vlx") $target="$id(out)" >
|
|
89
|
+
//
|
|
90
|
+
// The runtime auto-scans for all <?velox ...> tags on DOMContentLoaded.
|
|
91
|
+
// velox.js MUST be loaded BEFORE the closing </body> tag.
|
|
92
|
+
|
|
93
|
+
indexHtml: (a) => `<!DOCTYPE html>
|
|
94
|
+
<html lang="en">
|
|
95
|
+
<head>
|
|
96
|
+
<meta charset="UTF-8" />
|
|
97
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
98
|
+
<title>${a.projectName}</title>
|
|
99
|
+
<link rel="stylesheet" href="../assets/css/style.css" />
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
|
|
103
|
+
<h1>Welcome to ${a.projectName}</h1>
|
|
104
|
+
<p>Your Velox.js project is ready.</p>
|
|
105
|
+
|
|
106
|
+
<!-- Output element -->
|
|
107
|
+
<div id="output" style="margin-top:1rem;"></div>
|
|
108
|
+
|
|
109
|
+
<!-- Button wired by a <?velox $on > tag below -->
|
|
110
|
+
<button id="myBtn" style="margin-top:1rem;">Click me</button>
|
|
111
|
+
|
|
112
|
+
<!--
|
|
113
|
+
<?velox > tags are scanned at page load by the runtime.
|
|
114
|
+
|
|
115
|
+
Example 1 — run inline velox on page load:
|
|
116
|
+
<?velox Dom.setText("Page loaded!") $target="$id(output)" >
|
|
117
|
+
|
|
118
|
+
Example 2 — run a .vlx file on page load:
|
|
119
|
+
<?velox Velox.run("../assets/scripts/velox/myscript.vlx") $target="$id(output)" >
|
|
120
|
+
|
|
121
|
+
Example 3 — wire a click event to a .vlx file:
|
|
122
|
+
<?velox $on click "$id(myBtn)": Velox.run("../assets/scripts/velox/myscript.vlx") $target="$id(output)" >
|
|
123
|
+
|
|
124
|
+
Example 4 — inline command on click:
|
|
125
|
+
<?velox $on click "$id(myBtn)": Dom.setText("Hello from Velox!") $target="$id(output)" >
|
|
126
|
+
-->
|
|
127
|
+
|
|
128
|
+
<!-- Active example: click button → run inline velox command -->
|
|
129
|
+
<?velox $on click "$id(myBtn)": Dom.setText("Hello from Velox!") $target="$id(output)" >
|
|
130
|
+
|
|
131
|
+
<!-- Velox.js runtime — must come BEFORE closing </body> -->
|
|
132
|
+
<script src="../assets/scripts/velox/velox.js"></script>
|
|
133
|
+
|
|
134
|
+
</body>
|
|
135
|
+
</html>
|
|
136
|
+
`,
|
|
137
|
+
|
|
138
|
+
projectMeta: (a) => JSON.stringify({
|
|
139
|
+
name: a.projectName,
|
|
140
|
+
version: a.version || "1.0.0",
|
|
141
|
+
author: a.author || "",
|
|
142
|
+
framework: "velox.js",
|
|
143
|
+
frameworkVersion: "2.0.0",
|
|
144
|
+
created: new Date().toISOString().split("T")[0],
|
|
145
|
+
}, null, 2) + "\n",
|
|
146
|
+
|
|
147
|
+
gitignore: () => `# Velox.js
|
|
148
|
+
node_modules/
|
|
149
|
+
.DS_Store
|
|
150
|
+
Thumbs.db
|
|
151
|
+
*.log
|
|
152
|
+
|
|
153
|
+
# Local secrets / overrides
|
|
154
|
+
storage/variables/sha1.json
|
|
155
|
+
`,
|
|
156
|
+
|
|
157
|
+
// myscript.vlx — starter script
|
|
158
|
+
starterVlx: () => `// myscript.vlx — Velox.js starter script
|
|
159
|
+
// Statements are separated by ;
|
|
160
|
+
// Run this from HTML with:
|
|
161
|
+
// <?velox Velox.run("../assets/scripts/velox/myscript.vlx") $target="$id(output)" >
|
|
162
|
+
|
|
163
|
+
Dom.setText("Script ran successfully!")
|
|
164
|
+
$if .: echo("Target updated")
|
|
165
|
+
$else: echo.err("No target found");
|
|
166
|
+
|
|
167
|
+
echo("myscript.vlx loaded");
|
|
168
|
+
`,
|
|
169
|
+
|
|
170
|
+
veloxRuntime: () => `/**
|
|
171
|
+
* Velox.js Runtime v2.0.0
|
|
172
|
+
*
|
|
173
|
+
* ── How to connect a .vlx to HTML ──────────────────────────────────────────
|
|
174
|
+
*
|
|
175
|
+
* 1. Inline tag (runs at page load):
|
|
176
|
+
* <?velox Dom.setText("Hello World") $target="$id(myDiv)" >
|
|
177
|
+
*
|
|
178
|
+
* 2. Run a .vlx file:
|
|
179
|
+
* <?velox Velox.run("myscript.vlx") $target="$id(myDiv)" >
|
|
180
|
+
*
|
|
181
|
+
* 3. Event-driven (runs on click, hover, etc.):
|
|
182
|
+
* <?velox $on click "$id(btn)": Velox.run("script.vlx") $target="$id(out)" >
|
|
183
|
+
* <?velox $on click "$id(btn)": Dom.setText("Clicked!") $target="$id(out)" >
|
|
184
|
+
*
|
|
185
|
+
* ── Value rules ─────────────────────────────────────────────────────────────
|
|
186
|
+
* 0 = false / none
|
|
187
|
+
* 1 = true
|
|
188
|
+
* . = pass / blank / zero
|
|
189
|
+
* _ = same as .
|
|
190
|
+
*
|
|
191
|
+
* ── Statement separator ──────────────────────────────────────────────────────
|
|
192
|
+
* ; ends a statement (use inside .vlx files and inline tags)
|
|
193
|
+
*
|
|
194
|
+
* ── Target selectors ────────────────────────────────────────────────────────
|
|
195
|
+
* $id(myId) → document.getElementById("myId")
|
|
196
|
+
* $class(myClass) → first element with that class
|
|
197
|
+
* $query(selector) → document.querySelector(any CSS selector)
|
|
198
|
+
* $tag(div) → first element of that tag
|
|
199
|
+
*
|
|
200
|
+
* ── Commands reference ───────────────────────────────────────────────────────
|
|
201
|
+
*
|
|
202
|
+
* DOM
|
|
203
|
+
* Dom.setText("text") set textContent
|
|
204
|
+
* Dom.setHtml("html") set innerHTML
|
|
205
|
+
* Dom.append("html") appendChild parsed HTML
|
|
206
|
+
* Dom.prepend("html") prependChild parsed HTML
|
|
207
|
+
* Dom.remove() remove element from DOM
|
|
208
|
+
* Dom.clear() clear innerHTML
|
|
209
|
+
* Dom.show() remove display:none
|
|
210
|
+
* Dom.hide() display:none
|
|
211
|
+
* Dom.toggle() toggle visibility
|
|
212
|
+
* Dom.addClass("cls") add class
|
|
213
|
+
* Dom.removeClass("cls") remove class
|
|
214
|
+
* Dom.toggleClass("cls") toggle class
|
|
215
|
+
* Dom.setAttr("attr:value") set attribute
|
|
216
|
+
* Dom.removeAttr("attr") remove attribute
|
|
217
|
+
* Dom.setStyle("prop:value") set inline style
|
|
218
|
+
* Dom.focus() focus element
|
|
219
|
+
* Dom.click() programmatic click
|
|
220
|
+
* Dom.scrollTo() scroll element into view
|
|
221
|
+
* Dom.getValue() log input value to console
|
|
222
|
+
* Dom.setValue("val") set input value
|
|
223
|
+
*
|
|
224
|
+
* ANIMATION
|
|
225
|
+
* Anim.fadeIn("duration") fade in (ms)
|
|
226
|
+
* Anim.fadeOut("duration") fade out (ms)
|
|
227
|
+
* Anim.slide("down|up:duration") slide open or close (ms)
|
|
228
|
+
* Anim.shake() shake element
|
|
229
|
+
* Anim.pulse("duration") pulse element
|
|
230
|
+
*
|
|
231
|
+
* EVENTS
|
|
232
|
+
* $on click "$id(btn)": command() attach click listener
|
|
233
|
+
* $on hover "$id(btn)": command() attach mouseenter listener
|
|
234
|
+
* $on input "$id(inp)": command() attach input listener
|
|
235
|
+
* $on change "$id(sel)": command() attach change listener
|
|
236
|
+
* $on submit "$id(frm)": command() attach submit listener (prevents default)
|
|
237
|
+
* $on keydown "$id(el)": command() attach keydown listener
|
|
238
|
+
* $on load ".": command() window load listener
|
|
239
|
+
*
|
|
240
|
+
* FORM
|
|
241
|
+
* Form.getValue("$id(inp)") log value of input
|
|
242
|
+
* Form.setValue("$id(inp):val") set value of input
|
|
243
|
+
* Form.clear("$id(frm)") clear all inputs in a form
|
|
244
|
+
* Form.serialize("$id(frm)") log form data as JSON
|
|
245
|
+
*
|
|
246
|
+
* LOCAL STORAGE
|
|
247
|
+
* Store.set("key:value") localStorage.setItem
|
|
248
|
+
* Store.get("key") localStorage.getItem (logs result)
|
|
249
|
+
* Store.remove("key") localStorage.removeItem
|
|
250
|
+
* Store.clear() localStorage.clear
|
|
251
|
+
*
|
|
252
|
+
* COOKIE
|
|
253
|
+
* Cookie.set("key:value") document.cookie setter
|
|
254
|
+
* Cookie.get("key") cookie getter (logs result)
|
|
255
|
+
* Cookie.remove("key") expire cookie
|
|
256
|
+
*
|
|
257
|
+
* VARIABLES
|
|
258
|
+
* Var.set("key:value") set in-memory variable
|
|
259
|
+
* Var.get("key") log in-memory variable
|
|
260
|
+
* Var.clear("key|*") clear one or all variables
|
|
261
|
+
*
|
|
262
|
+
* NETWORK
|
|
263
|
+
* Url.get("url") fetch text, insert into target
|
|
264
|
+
* Url.getJson("url") fetch JSON, insert as pretty text
|
|
265
|
+
* Url.post("url") POST request
|
|
266
|
+
* Url.postJson("url:body") POST with JSON body
|
|
267
|
+
* Url.fetch("url") generic fetch (download if $type="download")
|
|
268
|
+
* iframe.Url("url") embed URL in iframe inside target
|
|
269
|
+
*
|
|
270
|
+
* FILE
|
|
271
|
+
* file.localFetch("path") fetch local file (download if $type="download")
|
|
272
|
+
*
|
|
273
|
+
* PAGE
|
|
274
|
+
* Page.redirect("url") navigate to URL
|
|
275
|
+
* Page.reload() reload current page
|
|
276
|
+
* Page.title("text") set document title
|
|
277
|
+
* Page.meta("name:content") set/update meta tag
|
|
278
|
+
* Page.scrollTop() scroll window to top
|
|
279
|
+
*
|
|
280
|
+
* ROUTER (SPA)
|
|
281
|
+
* Router.define("route:vlxPath") register a SPA route
|
|
282
|
+
* Router.go("route") navigate to a route
|
|
283
|
+
* Router.start("$id(outlet)") start router with hash-based routing
|
|
284
|
+
*
|
|
285
|
+
* TEMPLATE
|
|
286
|
+
* Template.render("tpl:data") render a <template id="tpl"> with JSON data
|
|
287
|
+
*
|
|
288
|
+
* SCRIPT / STYLE
|
|
289
|
+
* Script.load("url") load external JS file
|
|
290
|
+
* Style.load("url") load external CSS file
|
|
291
|
+
*
|
|
292
|
+
* LOGGING
|
|
293
|
+
* echo("msg") console.log
|
|
294
|
+
* echo.warn("msg") console.warn
|
|
295
|
+
* echo.err("msg") console.error
|
|
296
|
+
*/
|
|
297
|
+
|
|
298
|
+
const Velox = (() => {
|
|
299
|
+
|
|
300
|
+
let _config = null;
|
|
301
|
+
let _quickPaths = {};
|
|
302
|
+
let _vars = {};
|
|
303
|
+
const _routes = {};
|
|
304
|
+
let _routerOutlet = null;
|
|
305
|
+
|
|
306
|
+
// ── Logging ────────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
const log = (msg) => console.log(\`[Velox] \${msg}\`);
|
|
309
|
+
const warn = (msg) => console.warn(\`[Velox] \${msg}\`);
|
|
310
|
+
const err = (msg) => console.error(\`[Velox] \${msg}\`);
|
|
311
|
+
|
|
312
|
+
// ── Config ─────────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
async function loadConfig() {
|
|
315
|
+
try {
|
|
316
|
+
const res = await fetch("/.velox/conf/config.jsonc");
|
|
317
|
+
if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
|
|
318
|
+
const text = await res.text();
|
|
319
|
+
const stripped = text.replace(/\\/\\/.*$/gm, "").replace(/,(\\s*[}\\]])/g, "$1");
|
|
320
|
+
_config = JSON.parse(stripped);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
_config = {};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function loadQuickPaths() {
|
|
327
|
+
try {
|
|
328
|
+
const p = _config?.QuickPathsVariableStorage || "/storage/env/quickPaths.json";
|
|
329
|
+
const res = await fetch(resolveRelative(p));
|
|
330
|
+
if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
|
|
331
|
+
_quickPaths = await res.json();
|
|
332
|
+
} catch (e) {
|
|
333
|
+
_quickPaths = {};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function loadVars() {
|
|
338
|
+
try {
|
|
339
|
+
const p = _config?.variableStorage || "/storage/variables/normal.json";
|
|
340
|
+
const res = await fetch(resolveRelative(p));
|
|
341
|
+
if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
|
|
342
|
+
_vars = await res.json();
|
|
343
|
+
} catch (e) {
|
|
344
|
+
_vars = {};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Path helpers ───────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
function resolveEnv(value) {
|
|
351
|
+
return value.replace(/@env:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
352
|
+
if (_quickPaths[key] !== undefined) return _quickPaths[key];
|
|
353
|
+
warn(\`@env:\${key} not found in quickPaths.json\`);
|
|
354
|
+
return value;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function resolveRelative(p) {
|
|
359
|
+
if (!p) return p;
|
|
360
|
+
if (p.startsWith("http://") || p.startsWith("https://")) return p;
|
|
361
|
+
if (p.startsWith("/")) return p;
|
|
362
|
+
// Convert relative paths (../../) to absolute from root
|
|
363
|
+
return p.replace(/^(\\.\\.\\/)+/, "/").replace(/^\\.\\//, "/");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function resolvePath(raw) {
|
|
367
|
+
return resolveRelative(resolveEnv(raw.trim()));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Selector resolver ──────────────────────────────────────────────────────
|
|
371
|
+
// Supports: $id(x) $class(x) $query(x) $tag(x)
|
|
372
|
+
// Also accepts raw CSS selectors directly
|
|
373
|
+
|
|
374
|
+
function resolveTarget(target) {
|
|
375
|
+
if (!target || target === "." || target === "_") return document.body;
|
|
376
|
+
if (target instanceof Element) return target;
|
|
377
|
+
if (typeof target !== "string") return document.body;
|
|
378
|
+
|
|
379
|
+
const t = target.trim();
|
|
380
|
+
const idMatch = t.match(/^\\$id\\(([^)]+)\\)$/);
|
|
381
|
+
const classMatch = t.match(/^\\$class\\(([^)]+)\\)$/);
|
|
382
|
+
const queryMatch = t.match(/^\\$query\\((.+)\\)$/);
|
|
383
|
+
const tagMatch = t.match(/^\\$tag\\(([^)]+)\\)$/);
|
|
384
|
+
|
|
385
|
+
if (idMatch) return document.getElementById(idMatch[1]);
|
|
386
|
+
if (classMatch) return document.getElementsByClassName(classMatch[1])[0] || null;
|
|
387
|
+
if (queryMatch) return document.querySelector(queryMatch[1]);
|
|
388
|
+
if (tagMatch) return document.getElementsByTagName(tagMatch[1])[0] || null;
|
|
389
|
+
|
|
390
|
+
// Last resort: treat as a CSS selector
|
|
391
|
+
return document.querySelector(t) || document.body;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── Value rules ────────────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
function resolveValue(v) {
|
|
397
|
+
if (v === "1") return true;
|
|
398
|
+
if (v === "0") return false;
|
|
399
|
+
if (v === "." || v === "_") return null;
|
|
400
|
+
return v;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── Block / modifier parser ────────────────────────────────────────────────
|
|
404
|
+
// Matches: $keyword [value]: callStr
|
|
405
|
+
// e.g. $if true: echo("ok")
|
|
406
|
+
// $on click "$id(btn)": Dom.show()
|
|
407
|
+
|
|
408
|
+
function parseBlocks(src) {
|
|
409
|
+
const blocks = [];
|
|
410
|
+
// Extended to support $on events
|
|
411
|
+
const blockRe = /\\$(if else|if|else|on|set|delay|repeat|log|err)\\s*([^:]*)?:\\s*([^;>]+)/g;
|
|
412
|
+
let m;
|
|
413
|
+
while ((m = blockRe.exec(src)) !== null) {
|
|
414
|
+
blocks.push({
|
|
415
|
+
keyword: m[1].trim(),
|
|
416
|
+
value: m[2] ? m[2].trim() : null,
|
|
417
|
+
call: m[3].trim(),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return blocks;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── .vlx source tokenizer ──────────────────────────────────────────────────
|
|
424
|
+
// Splits on ; but respects quoted strings and nested parens
|
|
425
|
+
|
|
426
|
+
function tokenizeVlx(source) {
|
|
427
|
+
// Strip line comments
|
|
428
|
+
source = source.replace(/\\/\\/.*$/gm, "");
|
|
429
|
+
const stmts = [];
|
|
430
|
+
let buf = "";
|
|
431
|
+
let depth = 0;
|
|
432
|
+
let inStr = false;
|
|
433
|
+
let strChar = "";
|
|
434
|
+
|
|
435
|
+
for (let i = 0; i < source.length; i++) {
|
|
436
|
+
const c = source[i];
|
|
437
|
+
if (inStr) {
|
|
438
|
+
buf += c;
|
|
439
|
+
if (c === strChar && source[i - 1] !== "\\\\") inStr = false;
|
|
440
|
+
} else if (c === '"' || c === "'") {
|
|
441
|
+
inStr = true; strChar = c; buf += c;
|
|
442
|
+
} else if (c === "(") {
|
|
443
|
+
depth++; buf += c;
|
|
444
|
+
} else if (c === ")") {
|
|
445
|
+
depth--; buf += c;
|
|
446
|
+
} else if (c === ";" && depth === 0) {
|
|
447
|
+
const trimmed = buf.trim();
|
|
448
|
+
if (trimmed) stmts.push(trimmed);
|
|
449
|
+
buf = "";
|
|
450
|
+
} else {
|
|
451
|
+
buf += c;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const trimmed = buf.trim();
|
|
455
|
+
if (trimmed) stmts.push(trimmed);
|
|
456
|
+
return stmts;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Statement parser ────────────────────────────────────────────────────────
|
|
460
|
+
// Parses one statement: Cmd.name("arg") [$modifiers]
|
|
461
|
+
// Also handles $on event blocks
|
|
462
|
+
|
|
463
|
+
function parseStatement(stmt) {
|
|
464
|
+
stmt = stmt.trim();
|
|
465
|
+
if (!stmt) return null;
|
|
466
|
+
|
|
467
|
+
// Check for $on event block: $on click "$id(btn)": Dom.show()
|
|
468
|
+
const onMatch = stmt.match(/^\\$on\\s+(\\w+)\\s+"([^"]+)":\\s*(.+)$/s);
|
|
469
|
+
if (onMatch) {
|
|
470
|
+
return {
|
|
471
|
+
fn: "$on",
|
|
472
|
+
event: onMatch[1].trim(),
|
|
473
|
+
selector: onMatch[2].trim(),
|
|
474
|
+
call: onMatch[3].trim(),
|
|
475
|
+
delay: 0,
|
|
476
|
+
repeat: 1,
|
|
477
|
+
blocks: [],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Standard call: Fn.name("arg") [$mods]
|
|
482
|
+
// arg may contain any chars except unescaped "
|
|
483
|
+
const callMatch = stmt.match(/^([a-zA-Z$][a-zA-Z0-9$.]*)\\("((?:[^"\\\\]|\\\\.)*)"\\)([\\s\\S]*)$/);
|
|
484
|
+
if (callMatch) {
|
|
485
|
+
const fn = callMatch[1];
|
|
486
|
+
const arg = callMatch[2].replace(/\\\\"/g, '"');
|
|
487
|
+
const rest = callMatch[3];
|
|
488
|
+
|
|
489
|
+
const typeMatch = rest.match(/\\$type\\s*=\\s*"([^"]+)"/);
|
|
490
|
+
const targetMatch = rest.match(/\\$target\\s*=\\s*"([^"]+)"/);
|
|
491
|
+
const delayMatch = rest.match(/\\$delay\\s+(\\d+)/);
|
|
492
|
+
const repeatMatch = rest.match(/\\$repeat\\s+(\\d+)/);
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
fn,
|
|
496
|
+
arg,
|
|
497
|
+
type: typeMatch ? typeMatch[1] : null,
|
|
498
|
+
target: targetMatch ? targetMatch[1] : null,
|
|
499
|
+
delay: delayMatch ? parseInt(delayMatch[1]) : 0,
|
|
500
|
+
repeat: repeatMatch ? parseInt(repeatMatch[1]) : 1,
|
|
501
|
+
blocks: parseBlocks(rest),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Zero-arg call: Dom.remove() Dom.show() etc.
|
|
506
|
+
const noArgMatch = stmt.match(/^([a-zA-Z$][a-zA-Z0-9$.]*)\\(\\)([\\s\\S]*)$/);
|
|
507
|
+
if (noArgMatch) {
|
|
508
|
+
const fn = noArgMatch[1];
|
|
509
|
+
const rest = noArgMatch[2];
|
|
510
|
+
const typeMatch = rest.match(/\\$type\\s*=\\s*"([^"]+)"/);
|
|
511
|
+
const targetMatch = rest.match(/\\$target\\s*=\\s*"([^"]+)"/);
|
|
512
|
+
const delayMatch = rest.match(/\\$delay\\s+(\\d+)/);
|
|
513
|
+
const repeatMatch = rest.match(/\\$repeat\\s+(\\d+)/);
|
|
514
|
+
return {
|
|
515
|
+
fn,
|
|
516
|
+
arg: "",
|
|
517
|
+
type: typeMatch ? typeMatch[1] : null,
|
|
518
|
+
target: targetMatch ? targetMatch[1] : null,
|
|
519
|
+
delay: delayMatch ? parseInt(delayMatch[1]) : 0,
|
|
520
|
+
repeat: repeatMatch ? parseInt(repeatMatch[1]) : 1,
|
|
521
|
+
blocks: parseBlocks(rest),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
warn(\`Could not parse statement: \${stmt}\`);
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function parseVlx(source) {
|
|
530
|
+
return tokenizeVlx(source)
|
|
531
|
+
.map(parseStatement)
|
|
532
|
+
.filter(Boolean);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ── <?velox ...> tag scanner ───────────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
function extractVeloxTags(html) {
|
|
538
|
+
const results = [];
|
|
539
|
+
// Match <?velox ... > — non-greedy, supports multiline
|
|
540
|
+
const re = /<\\?velox([\\s\\S]*?)>/g;
|
|
541
|
+
let m;
|
|
542
|
+
while ((m = re.exec(html)) !== null) {
|
|
543
|
+
results.push(m[1].trim());
|
|
544
|
+
}
|
|
545
|
+
return results;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ── Inline tag parser: handles Velox.run("path") $target="..." ────────────
|
|
549
|
+
|
|
550
|
+
function parseInlineTag(src) {
|
|
551
|
+
// Velox.run("path") [$target="..."]
|
|
552
|
+
const runMatch = src.match(/^Velox\\.run\\("([^"]+)"\\)([\\s\\S]*)$/);
|
|
553
|
+
if (runMatch) {
|
|
554
|
+
const vlxPath = runMatch[1];
|
|
555
|
+
const rest = runMatch[2];
|
|
556
|
+
const targetMatch = rest.match(/\\$target\\s*=\\s*"([^"]+)"/);
|
|
557
|
+
const typeMatch = rest.match(/\\$type\\s*=\\s*"([^"]+)"/);
|
|
558
|
+
return {
|
|
559
|
+
kind: "run",
|
|
560
|
+
vlxPath,
|
|
561
|
+
target: targetMatch ? targetMatch[1] : null,
|
|
562
|
+
type: typeMatch ? typeMatch[1] : null,
|
|
563
|
+
blocks: parseBlocks(rest),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ── Animation helpers ──────────────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
function animFadeIn(el, ms = 300) {
|
|
572
|
+
el.style.transition = \`opacity \${ms}ms\`;
|
|
573
|
+
el.style.opacity = "0";
|
|
574
|
+
el.style.display = "";
|
|
575
|
+
requestAnimationFrame(() => {
|
|
576
|
+
requestAnimationFrame(() => { el.style.opacity = "1"; });
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function animFadeOut(el, ms = 300) {
|
|
581
|
+
el.style.transition = \`opacity \${ms}ms\`;
|
|
582
|
+
el.style.opacity = "0";
|
|
583
|
+
setTimeout(() => { el.style.display = "none"; }, ms);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function animSlide(el, dir, ms = 300) {
|
|
587
|
+
if (dir === "down") {
|
|
588
|
+
el.style.overflow = "hidden";
|
|
589
|
+
el.style.height = "0";
|
|
590
|
+
el.style.display = "";
|
|
591
|
+
const h = el.scrollHeight;
|
|
592
|
+
el.style.transition = \`height \${ms}ms\`;
|
|
593
|
+
requestAnimationFrame(() => {
|
|
594
|
+
requestAnimationFrame(() => { el.style.height = h + "px"; });
|
|
595
|
+
});
|
|
596
|
+
setTimeout(() => { el.style.height = ""; el.style.overflow = ""; }, ms);
|
|
597
|
+
} else {
|
|
598
|
+
el.style.overflow = "hidden";
|
|
599
|
+
el.style.height = el.scrollHeight + "px";
|
|
600
|
+
el.style.transition = \`height \${ms}ms\`;
|
|
601
|
+
requestAnimationFrame(() => {
|
|
602
|
+
requestAnimationFrame(() => { el.style.height = "0"; });
|
|
603
|
+
});
|
|
604
|
+
setTimeout(() => { el.style.display = "none"; el.style.height = ""; el.style.overflow = ""; }, ms);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function animShake(el) {
|
|
609
|
+
el.style.animation = "none";
|
|
610
|
+
const id = \`vlx-shake-\${Date.now()}\`;
|
|
611
|
+
if (!document.getElementById(id)) {
|
|
612
|
+
const s = document.createElement("style");
|
|
613
|
+
s.id = id;
|
|
614
|
+
s.textContent = \`@keyframes vlx-shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-8px)}40%,80%{transform:translateX(8px)}}\`;
|
|
615
|
+
document.head.appendChild(s);
|
|
616
|
+
}
|
|
617
|
+
el.style.animation = "vlx-shake 0.4s";
|
|
618
|
+
setTimeout(() => { el.style.animation = ""; }, 400);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function animPulse(el, ms = 600) {
|
|
622
|
+
const id = \`vlx-pulse-\${Date.now()}\`;
|
|
623
|
+
if (!document.getElementById(id)) {
|
|
624
|
+
const s = document.createElement("style");
|
|
625
|
+
s.id = id;
|
|
626
|
+
s.textContent = \`@keyframes vlx-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}}\`;
|
|
627
|
+
document.head.appendChild(s);
|
|
628
|
+
}
|
|
629
|
+
el.style.animation = \`vlx-pulse \${ms}ms\`;
|
|
630
|
+
setTimeout(() => { el.style.animation = ""; }, ms);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ── Cookie helpers ─────────────────────────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
function cookieGet(key) {
|
|
636
|
+
const m = document.cookie.match(new RegExp(\`(?:^|;\\\\s*)\${encodeURIComponent(key)}=([^;]*)\`));
|
|
637
|
+
return m ? decodeURIComponent(m[1]) : null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function cookieSet(key, value, days = 365) {
|
|
641
|
+
const exp = new Date(Date.now() + days * 864e5).toUTCString();
|
|
642
|
+
document.cookie = \`\${encodeURIComponent(key)}=\${encodeURIComponent(value)};expires=\${exp};path=/\`;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function cookieRemove(key) {
|
|
646
|
+
document.cookie = \`\${encodeURIComponent(key)}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/\`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ── Template rendering ─────────────────────────────────────────────────────
|
|
650
|
+
// <template id="myTpl"><div>{{name}}</div></template>
|
|
651
|
+
// Template.render("myTpl:{\\"name\\":\\"World\\"}")
|
|
652
|
+
|
|
653
|
+
function renderTemplate(tplId, data, target) {
|
|
654
|
+
const tpl = document.getElementById(tplId);
|
|
655
|
+
if (!tpl || tpl.tagName !== "TEMPLATE") {
|
|
656
|
+
warn(\`Template #\${tplId} not found\`);
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
let html = tpl.innerHTML;
|
|
660
|
+
for (const [k, v] of Object.entries(data)) {
|
|
661
|
+
html = html.replaceAll(\`{{\${k}}}\`, v);
|
|
662
|
+
}
|
|
663
|
+
if (target) target.innerHTML = html;
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── SPA Router ────────────────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
async function routerNavigate(route) {
|
|
670
|
+
const vlxPath = _routes[route] || _routes["*"];
|
|
671
|
+
if (!vlxPath) { warn(\`No route defined for: \${route}\`); return; }
|
|
672
|
+
window.location.hash = route;
|
|
673
|
+
if (_routerOutlet) {
|
|
674
|
+
_routerOutlet.innerHTML = "";
|
|
675
|
+
await runScript(vlxPath, { target: _routerOutlet });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function routerStart(outlet) {
|
|
680
|
+
_routerOutlet = outlet;
|
|
681
|
+
const go = () => {
|
|
682
|
+
const route = window.location.hash.slice(1) || "/";
|
|
683
|
+
routerNavigate(route);
|
|
684
|
+
};
|
|
685
|
+
window.addEventListener("hashchange", go);
|
|
686
|
+
go(); // run on start
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ── $on event binder ──────────────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
async function bindEvent(cmd, options = {}) {
|
|
692
|
+
const { event, selector, call } = cmd;
|
|
693
|
+
const els = selector === "." || selector === "window"
|
|
694
|
+
? [window]
|
|
695
|
+
: Array.from(document.querySelectorAll(selectorToCss(selector)));
|
|
696
|
+
|
|
697
|
+
if (!els.length) {
|
|
698
|
+
warn(\`$on \${event}: no elements match "\${selector}"\`);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const domEvent = event === "hover" ? "mouseenter" : event;
|
|
703
|
+
|
|
704
|
+
for (const el of els) {
|
|
705
|
+
const handler = async (e) => {
|
|
706
|
+
if (event === "submit") e.preventDefault();
|
|
707
|
+
const target = resolveTarget(options.target || null) || el;
|
|
708
|
+
|
|
709
|
+
// Inline call inside $on
|
|
710
|
+
const stmt = parseStatement(call);
|
|
711
|
+
if (stmt) await executeCommand(stmt, { target, event: e, sourceEl: el });
|
|
712
|
+
};
|
|
713
|
+
el.addEventListener(domEvent, handler);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Convert Velox selector syntax to a CSS selector string
|
|
718
|
+
function selectorToCss(sel) {
|
|
719
|
+
if (!sel) return "body";
|
|
720
|
+
const idM = sel.match(/^\\$id\\(([^)]+)\\)$/);
|
|
721
|
+
const clsM = sel.match(/^\\$class\\(([^)]+)\\)$/);
|
|
722
|
+
const qM = sel.match(/^\\$query\\((.+)\\)$/);
|
|
723
|
+
const tagM = sel.match(/^\\$tag\\(([^)]+)\\)$/);
|
|
724
|
+
if (idM) return \`#\${idM[1]}\`;
|
|
725
|
+
if (clsM) return \`.\${clsM[1]}\`;
|
|
726
|
+
if (qM) return qM[1];
|
|
727
|
+
if (tagM) return tagM[1];
|
|
728
|
+
return sel;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ── Command executor ───────────────────────────────────────────────────────
|
|
732
|
+
|
|
733
|
+
async function executeCommand(cmd, options = {}) {
|
|
734
|
+
// Per-command $target overrides the options target
|
|
735
|
+
const targetSel = cmd.target || options.target;
|
|
736
|
+
const target = typeof targetSel === "string"
|
|
737
|
+
? resolveTarget(targetSel)
|
|
738
|
+
: (targetSel instanceof Element ? targetSel : resolveTarget(options.target));
|
|
739
|
+
|
|
740
|
+
// ── $on event binding ──────────────────────────────────────────────────
|
|
741
|
+
if (cmd.fn === "$on") {
|
|
742
|
+
await bindEvent(cmd, options);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const execute = async () => {
|
|
747
|
+
switch (cmd.fn) {
|
|
748
|
+
|
|
749
|
+
// ── DOM ─────────────────────────────────────────────────────────────
|
|
750
|
+
|
|
751
|
+
case "Dom.setText":
|
|
752
|
+
if (target) target.textContent = cmd.arg;
|
|
753
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
754
|
+
|
|
755
|
+
case "Dom.setHtml":
|
|
756
|
+
if (target) target.innerHTML = cmd.arg;
|
|
757
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
758
|
+
|
|
759
|
+
case "Dom.append": {
|
|
760
|
+
if (target) {
|
|
761
|
+
const tpl = document.createElement("template");
|
|
762
|
+
tpl.innerHTML = cmd.arg;
|
|
763
|
+
target.appendChild(tpl.content.cloneNode(true));
|
|
764
|
+
}
|
|
765
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
case "Dom.prepend": {
|
|
769
|
+
if (target) {
|
|
770
|
+
const tpl = document.createElement("template");
|
|
771
|
+
tpl.innerHTML = cmd.arg;
|
|
772
|
+
target.prepend(tpl.content.cloneNode(true));
|
|
773
|
+
}
|
|
774
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
case "Dom.remove":
|
|
778
|
+
if (target && target !== document.body) target.remove();
|
|
779
|
+
evalBlocks(cmd.blocks, true, null); break;
|
|
780
|
+
|
|
781
|
+
case "Dom.clear":
|
|
782
|
+
if (target) target.innerHTML = "";
|
|
783
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
784
|
+
|
|
785
|
+
case "Dom.show":
|
|
786
|
+
if (target) target.style.display = "";
|
|
787
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
788
|
+
|
|
789
|
+
case "Dom.hide":
|
|
790
|
+
if (target) target.style.display = "none";
|
|
791
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
792
|
+
|
|
793
|
+
case "Dom.toggle":
|
|
794
|
+
if (target) target.style.display = target.style.display === "none" ? "" : "none";
|
|
795
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
796
|
+
|
|
797
|
+
case "Dom.addClass":
|
|
798
|
+
if (target) target.classList.add(cmd.arg);
|
|
799
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
800
|
+
|
|
801
|
+
case "Dom.removeClass":
|
|
802
|
+
if (target) target.classList.remove(cmd.arg);
|
|
803
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
804
|
+
|
|
805
|
+
case "Dom.toggleClass":
|
|
806
|
+
if (target) target.classList.toggle(cmd.arg);
|
|
807
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
808
|
+
|
|
809
|
+
case "Dom.setAttr": {
|
|
810
|
+
if (target) {
|
|
811
|
+
const [attr, ...vals] = cmd.arg.split(":");
|
|
812
|
+
target.setAttribute(attr.trim(), vals.join(":").trim());
|
|
813
|
+
}
|
|
814
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
case "Dom.removeAttr":
|
|
818
|
+
if (target) target.removeAttribute(cmd.arg);
|
|
819
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
820
|
+
|
|
821
|
+
case "Dom.setStyle": {
|
|
822
|
+
if (target) {
|
|
823
|
+
const colonIdx = cmd.arg.indexOf(":");
|
|
824
|
+
const prop = cmd.arg.slice(0, colonIdx).trim();
|
|
825
|
+
const val = cmd.arg.slice(colonIdx + 1).trim();
|
|
826
|
+
target.style[prop] = val;
|
|
827
|
+
}
|
|
828
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
case "Dom.focus":
|
|
832
|
+
if (target) target.focus();
|
|
833
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
834
|
+
|
|
835
|
+
case "Dom.click":
|
|
836
|
+
if (target) target.click();
|
|
837
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
838
|
+
|
|
839
|
+
case "Dom.scrollTo":
|
|
840
|
+
if (target) target.scrollIntoView({ behavior: "smooth" });
|
|
841
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
842
|
+
|
|
843
|
+
case "Dom.getValue":
|
|
844
|
+
log(\`Dom.getValue: \${target?.value ?? "undefined"}\`);
|
|
845
|
+
evalBlocks(cmd.blocks, target?.value !== undefined, target); break;
|
|
846
|
+
|
|
847
|
+
case "Dom.setValue":
|
|
848
|
+
if (target) target.value = cmd.arg;
|
|
849
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
850
|
+
|
|
851
|
+
// ── ANIMATION ───────────────────────────────────────────────────────
|
|
852
|
+
|
|
853
|
+
case "Anim.fadeIn": {
|
|
854
|
+
if (target) animFadeIn(target, parseInt(cmd.arg) || 300);
|
|
855
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
case "Anim.fadeOut": {
|
|
859
|
+
if (target) animFadeOut(target, parseInt(cmd.arg) || 300);
|
|
860
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
case "Anim.slide": {
|
|
864
|
+
if (target) {
|
|
865
|
+
const [dir, ms] = cmd.arg.split(":");
|
|
866
|
+
animSlide(target, dir.trim(), parseInt(ms) || 300);
|
|
867
|
+
}
|
|
868
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
case "Anim.shake":
|
|
872
|
+
if (target) animShake(target);
|
|
873
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
874
|
+
|
|
875
|
+
case "Anim.pulse":
|
|
876
|
+
if (target) animPulse(target, parseInt(cmd.arg) || 600);
|
|
877
|
+
evalBlocks(cmd.blocks, !!target, target); break;
|
|
878
|
+
|
|
879
|
+
// ── FORM ────────────────────────────────────────────────────────────
|
|
880
|
+
|
|
881
|
+
case "Form.getValue": {
|
|
882
|
+
const el = resolveTarget(cmd.arg);
|
|
883
|
+
log(\`Form.getValue: \${el?.value ?? "undefined"}\`);
|
|
884
|
+
evalBlocks(cmd.blocks, el?.value !== undefined, target); break;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
case "Form.setValue": {
|
|
888
|
+
const colonIdx = cmd.arg.indexOf(":");
|
|
889
|
+
const sel = cmd.arg.slice(0, colonIdx).trim();
|
|
890
|
+
const val = cmd.arg.slice(colonIdx + 1).trim();
|
|
891
|
+
const el = resolveTarget(sel);
|
|
892
|
+
if (el) el.value = val;
|
|
893
|
+
evalBlocks(cmd.blocks, !!el, target); break;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
case "Form.clear": {
|
|
897
|
+
const form = resolveTarget(cmd.arg) || target;
|
|
898
|
+
if (form) form.querySelectorAll("input,textarea,select").forEach(i => { i.value = ""; });
|
|
899
|
+
evalBlocks(cmd.blocks, !!form, target); break;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
case "Form.serialize": {
|
|
903
|
+
const form = resolveTarget(cmd.arg) || target;
|
|
904
|
+
if (form) {
|
|
905
|
+
const data = {};
|
|
906
|
+
form.querySelectorAll("[name]").forEach(i => { data[i.name] = i.value; });
|
|
907
|
+
log(\`Form.serialize: \${JSON.stringify(data)}\`);
|
|
908
|
+
if (target && target !== form) target.textContent = JSON.stringify(data, null, 2);
|
|
909
|
+
}
|
|
910
|
+
evalBlocks(cmd.blocks, !!form, target); break;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ── LOCAL STORAGE ────────────────────────────────────────────────────
|
|
914
|
+
|
|
915
|
+
case "Store.set": {
|
|
916
|
+
const colonIdx = cmd.arg.indexOf(":");
|
|
917
|
+
const k = cmd.arg.slice(0, colonIdx).trim();
|
|
918
|
+
const v = cmd.arg.slice(colonIdx + 1).trim();
|
|
919
|
+
localStorage.setItem(k, v);
|
|
920
|
+
log(\`Store.set: \${k} = \${v}\`);
|
|
921
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
case "Store.get": {
|
|
925
|
+
const val = localStorage.getItem(cmd.arg);
|
|
926
|
+
log(\`Store.get: \${cmd.arg} = \${val ?? "null"}\`);
|
|
927
|
+
if (target && val !== null) target.textContent = val;
|
|
928
|
+
evalBlocks(cmd.blocks, val !== null, target); break;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
case "Store.remove":
|
|
932
|
+
localStorage.removeItem(cmd.arg);
|
|
933
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
934
|
+
|
|
935
|
+
case "Store.clear":
|
|
936
|
+
localStorage.clear();
|
|
937
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
938
|
+
|
|
939
|
+
// ── COOKIES ─────────────────────────────────────────────────────────
|
|
940
|
+
|
|
941
|
+
case "Cookie.set": {
|
|
942
|
+
const colonIdx = cmd.arg.indexOf(":");
|
|
943
|
+
const k = cmd.arg.slice(0, colonIdx).trim();
|
|
944
|
+
const v = cmd.arg.slice(colonIdx + 1).trim();
|
|
945
|
+
cookieSet(k, v);
|
|
946
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
case "Cookie.get": {
|
|
950
|
+
const val = cookieGet(cmd.arg);
|
|
951
|
+
log(\`Cookie.get: \${cmd.arg} = \${val ?? "null"}\`);
|
|
952
|
+
if (target && val !== null) target.textContent = val;
|
|
953
|
+
evalBlocks(cmd.blocks, val !== null, target); break;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
case "Cookie.remove":
|
|
957
|
+
cookieRemove(cmd.arg);
|
|
958
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
959
|
+
|
|
960
|
+
// ── VARIABLES ────────────────────────────────────────────────────────
|
|
961
|
+
|
|
962
|
+
case "Var.set": {
|
|
963
|
+
const colonIdx = cmd.arg.indexOf(":");
|
|
964
|
+
const k = cmd.arg.slice(0, colonIdx).trim();
|
|
965
|
+
const v = cmd.arg.slice(colonIdx + 1).trim();
|
|
966
|
+
_vars[k] = v;
|
|
967
|
+
log(\`Var.set: \${k} = \${v}\`);
|
|
968
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
case "Var.get": {
|
|
972
|
+
const val = _vars[cmd.arg];
|
|
973
|
+
log(\`Var.get: \${cmd.arg} = \${val ?? "undefined"}\`);
|
|
974
|
+
if (target && val !== undefined) target.textContent = val;
|
|
975
|
+
evalBlocks(cmd.blocks, val !== undefined, target); break;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
case "Var.clear":
|
|
979
|
+
if (cmd.arg === "*") _vars = {};
|
|
980
|
+
else delete _vars[cmd.arg];
|
|
981
|
+
evalBlocks(cmd.blocks, true, target); break;
|
|
982
|
+
|
|
983
|
+
// ── NETWORK ─────────────────────────────────────────────────────────
|
|
984
|
+
|
|
985
|
+
case "iframe.Url": {
|
|
986
|
+
const url = resolvePath(cmd.arg);
|
|
987
|
+
let iframeEl;
|
|
988
|
+
if (target && target.tagName === "IFRAME") {
|
|
989
|
+
iframeEl = target;
|
|
990
|
+
} else {
|
|
991
|
+
iframeEl = document.createElement("iframe");
|
|
992
|
+
iframeEl.style.cssText = "width:100%;height:600px;border:none;display:block;";
|
|
993
|
+
(target || document.body).appendChild(iframeEl);
|
|
994
|
+
}
|
|
995
|
+
iframeEl.src = url;
|
|
996
|
+
evalBlocks(cmd.blocks, true, iframeEl);
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
case "Url.fetch": {
|
|
1001
|
+
const url = resolvePath(cmd.arg);
|
|
1002
|
+
try {
|
|
1003
|
+
const res = await fetch(url);
|
|
1004
|
+
if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
|
|
1005
|
+
if (cmd.type === "download") {
|
|
1006
|
+
const blob = await res.blob();
|
|
1007
|
+
const a = document.createElement("a");
|
|
1008
|
+
a.href = URL.createObjectURL(blob);
|
|
1009
|
+
a.download = url.split("/").pop();
|
|
1010
|
+
a.click();
|
|
1011
|
+
}
|
|
1012
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1013
|
+
} catch (e) {
|
|
1014
|
+
warn(\`Url.fetch failed: \${e.message}\`);
|
|
1015
|
+
evalBlocks(cmd.blocks, false, target);
|
|
1016
|
+
}
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
case "Url.get": {
|
|
1021
|
+
const url = resolvePath(cmd.arg);
|
|
1022
|
+
try {
|
|
1023
|
+
const res = await fetch(url);
|
|
1024
|
+
if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
|
|
1025
|
+
const text = await res.text();
|
|
1026
|
+
if (target) target.textContent = text;
|
|
1027
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
warn(\`Url.get failed: \${e.message}\`);
|
|
1030
|
+
evalBlocks(cmd.blocks, false, target);
|
|
1031
|
+
}
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
case "Url.getJson": {
|
|
1036
|
+
const url = resolvePath(cmd.arg);
|
|
1037
|
+
try {
|
|
1038
|
+
const res = await fetch(url);
|
|
1039
|
+
if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
|
|
1040
|
+
const json = await res.json();
|
|
1041
|
+
if (target) target.textContent = JSON.stringify(json, null, 2);
|
|
1042
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
warn(\`Url.getJson failed: \${e.message}\`);
|
|
1045
|
+
evalBlocks(cmd.blocks, false, target);
|
|
1046
|
+
}
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
case "Url.post": {
|
|
1051
|
+
const url = resolvePath(cmd.arg);
|
|
1052
|
+
try {
|
|
1053
|
+
const res = await fetch(url, { method: "POST" });
|
|
1054
|
+
if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
|
|
1055
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
warn(\`Url.post failed: \${e.message}\`);
|
|
1058
|
+
evalBlocks(cmd.blocks, false, target);
|
|
1059
|
+
}
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
case "Url.postJson": {
|
|
1064
|
+
// arg format: "url:jsonBody"
|
|
1065
|
+
const colonIdx = cmd.arg.indexOf(":");
|
|
1066
|
+
const url = resolvePath(cmd.arg.slice(0, colonIdx).trim());
|
|
1067
|
+
const body = cmd.arg.slice(colonIdx + 1).trim();
|
|
1068
|
+
try {
|
|
1069
|
+
const res = await fetch(url, {
|
|
1070
|
+
method: "POST",
|
|
1071
|
+
headers: { "Content-Type": "application/json" },
|
|
1072
|
+
body,
|
|
1073
|
+
});
|
|
1074
|
+
if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
|
|
1075
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
warn(\`Url.postJson failed: \${e.message}\`);
|
|
1078
|
+
evalBlocks(cmd.blocks, false, target);
|
|
1079
|
+
}
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
case "file.localFetch": {
|
|
1084
|
+
const url = resolvePath(cmd.arg);
|
|
1085
|
+
try {
|
|
1086
|
+
const res = await fetch(url);
|
|
1087
|
+
if (!res.ok) throw new Error("Not found");
|
|
1088
|
+
if (cmd.type === "download") {
|
|
1089
|
+
const blob = await res.blob();
|
|
1090
|
+
const a = document.createElement("a");
|
|
1091
|
+
a.href = URL.createObjectURL(blob);
|
|
1092
|
+
a.download = url.split("/").pop();
|
|
1093
|
+
a.click();
|
|
1094
|
+
}
|
|
1095
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
warn(\`file.localFetch failed: \${e.message}\`);
|
|
1098
|
+
evalBlocks(cmd.blocks, false, target);
|
|
1099
|
+
}
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// ── PAGE ─────────────────────────────────────────────────────────────
|
|
1104
|
+
|
|
1105
|
+
case "Page.redirect":
|
|
1106
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1107
|
+
window.location.href = resolvePath(cmd.arg);
|
|
1108
|
+
break;
|
|
1109
|
+
|
|
1110
|
+
case "Page.reload":
|
|
1111
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1112
|
+
window.location.reload();
|
|
1113
|
+
break;
|
|
1114
|
+
|
|
1115
|
+
case "Page.title":
|
|
1116
|
+
document.title = cmd.arg;
|
|
1117
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1118
|
+
break;
|
|
1119
|
+
|
|
1120
|
+
case "Page.meta": {
|
|
1121
|
+
const colonIdx = cmd.arg.indexOf(":");
|
|
1122
|
+
const name = cmd.arg.slice(0, colonIdx).trim();
|
|
1123
|
+
const content = cmd.arg.slice(colonIdx + 1).trim();
|
|
1124
|
+
let meta = document.querySelector(\`meta[name="\${name}"]\`);
|
|
1125
|
+
if (!meta) { meta = document.createElement("meta"); meta.name = name; document.head.appendChild(meta); }
|
|
1126
|
+
meta.content = content;
|
|
1127
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
case "Page.scrollTop":
|
|
1132
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
1133
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1134
|
+
break;
|
|
1135
|
+
|
|
1136
|
+
// ── ROUTER ───────────────────────────────────────────────────────────
|
|
1137
|
+
|
|
1138
|
+
case "Router.define": {
|
|
1139
|
+
const colonIdx = cmd.arg.indexOf(":");
|
|
1140
|
+
const route = cmd.arg.slice(0, colonIdx).trim();
|
|
1141
|
+
const vlxPath = cmd.arg.slice(colonIdx + 1).trim();
|
|
1142
|
+
_routes[route] = vlxPath;
|
|
1143
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
case "Router.go":
|
|
1148
|
+
await routerNavigate(cmd.arg);
|
|
1149
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1150
|
+
break;
|
|
1151
|
+
|
|
1152
|
+
case "Router.start":
|
|
1153
|
+
routerStart(target || document.body);
|
|
1154
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1155
|
+
break;
|
|
1156
|
+
|
|
1157
|
+
// ── TEMPLATE ─────────────────────────────────────────────────────────
|
|
1158
|
+
|
|
1159
|
+
case "Template.render": {
|
|
1160
|
+
// arg: "templateId:jsonData"
|
|
1161
|
+
const colonIdx = cmd.arg.indexOf(":");
|
|
1162
|
+
const tplId = cmd.arg.slice(0, colonIdx).trim();
|
|
1163
|
+
let data = {};
|
|
1164
|
+
try { data = JSON.parse(cmd.arg.slice(colonIdx + 1).trim()); } catch (_) {}
|
|
1165
|
+
const ok = renderTemplate(tplId, data, target);
|
|
1166
|
+
evalBlocks(cmd.blocks, ok, target);
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// ── SCRIPT / STYLE ───────────────────────────────────────────────────
|
|
1171
|
+
|
|
1172
|
+
case "Script.load": {
|
|
1173
|
+
const url = resolvePath(cmd.arg);
|
|
1174
|
+
try {
|
|
1175
|
+
await new Promise((resolve, reject) => {
|
|
1176
|
+
const s = document.createElement("script");
|
|
1177
|
+
s.src = url; s.onload = resolve; s.onerror = reject;
|
|
1178
|
+
document.head.appendChild(s);
|
|
1179
|
+
});
|
|
1180
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
warn(\`Script.load failed: \${e.message}\`);
|
|
1183
|
+
evalBlocks(cmd.blocks, false, target);
|
|
1184
|
+
}
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
case "Style.load": {
|
|
1189
|
+
const link = document.createElement("link");
|
|
1190
|
+
link.rel = "stylesheet";
|
|
1191
|
+
link.href = resolvePath(cmd.arg);
|
|
1192
|
+
document.head.appendChild(link);
|
|
1193
|
+
evalBlocks(cmd.blocks, true, target);
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// ── ECHO / LOGGING ────────────────────────────────────────────────────
|
|
1198
|
+
|
|
1199
|
+
case "echo": log(cmd.arg); break;
|
|
1200
|
+
case "echo.warn": warn(cmd.arg); break;
|
|
1201
|
+
case "echo.err": err(cmd.arg); break;
|
|
1202
|
+
|
|
1203
|
+
default:
|
|
1204
|
+
warn(\`Unknown command: \${cmd.fn}\`);
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
// Delay + repeat support
|
|
1209
|
+
if (cmd.delay) await sleep(cmd.delay);
|
|
1210
|
+
for (let i = 0; i < (cmd.repeat || 1); i++) {
|
|
1211
|
+
if (i > 0 && cmd.delay) await sleep(cmd.delay);
|
|
1212
|
+
await execute();
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// ── Block evaluator ────────────────────────────────────────────────────────
|
|
1217
|
+
|
|
1218
|
+
function evalBlocks(blocks, success, target) {
|
|
1219
|
+
let handled = false;
|
|
1220
|
+
for (const block of blocks) {
|
|
1221
|
+
switch (block.keyword) {
|
|
1222
|
+
case "if": {
|
|
1223
|
+
const cond = block.value === null ? success : (block.value === true || block.value === "true");
|
|
1224
|
+
if (cond) { runCall(block.call, target); handled = true; }
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
case "if else": {
|
|
1228
|
+
if (!handled) {
|
|
1229
|
+
const cond = block.value === null ? success : (block.value === true || block.value === "true");
|
|
1230
|
+
if (cond) { runCall(block.call, target); handled = true; }
|
|
1231
|
+
}
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
case "else":
|
|
1235
|
+
if (!handled) { runCall(block.call, target); handled = true; }
|
|
1236
|
+
break;
|
|
1237
|
+
case "log":
|
|
1238
|
+
runCall(block.call, target); break;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// ── Inline callback runner (used inside $if/$else blocks) ─────────────────
|
|
1244
|
+
|
|
1245
|
+
function runCall(callStr, target) {
|
|
1246
|
+
if (!callStr) return;
|
|
1247
|
+
callStr = callStr.trim();
|
|
1248
|
+
|
|
1249
|
+
// echo / echo.err / echo.warn
|
|
1250
|
+
const echoMatch = callStr.match(/^echo(?:\\.(err|warn))?\\("([^"]*)"\\)$/);
|
|
1251
|
+
if (echoMatch) {
|
|
1252
|
+
const type = echoMatch[1], msg = echoMatch[2];
|
|
1253
|
+
if (type === "err") err(msg);
|
|
1254
|
+
else if (type === "warn") warn(msg);
|
|
1255
|
+
else log(msg);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Dom calls
|
|
1260
|
+
const domTextMatch = callStr.match(/^Dom\\.(setText|setHtml)\\("([^"]*)"\\)$/);
|
|
1261
|
+
if (domTextMatch && target) {
|
|
1262
|
+
if (domTextMatch[1] === "setText") target.textContent = domTextMatch[2];
|
|
1263
|
+
else target.innerHTML = domTextMatch[2];
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const domVisMatch = callStr.match(/^Dom\\.(show|hide|toggle|remove|clear|focus|click|scrollTo)\\(\\)$/);
|
|
1268
|
+
if (domVisMatch && target) {
|
|
1269
|
+
switch (domVisMatch[1]) {
|
|
1270
|
+
case "show": target.style.display = ""; break;
|
|
1271
|
+
case "hide": target.style.display = "none"; break;
|
|
1272
|
+
case "toggle": target.style.display = target.style.display === "none" ? "" : "none"; break;
|
|
1273
|
+
case "remove": target.remove(); break;
|
|
1274
|
+
case "clear": target.innerHTML = ""; break;
|
|
1275
|
+
case "focus": target.focus(); break;
|
|
1276
|
+
case "click": target.click(); break;
|
|
1277
|
+
case "scrollTo": target.scrollIntoView({ behavior: "smooth" }); break;
|
|
1278
|
+
}
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const domClassMatch = callStr.match(/^Dom\\.(addClass|removeClass|toggleClass)\\("([^"]*)"\\)$/);
|
|
1283
|
+
if (domClassMatch && target) {
|
|
1284
|
+
if (domClassMatch[1] === "addClass") target.classList.add(domClassMatch[2]);
|
|
1285
|
+
else if (domClassMatch[1] === "removeClass") target.classList.remove(domClassMatch[2]);
|
|
1286
|
+
else target.classList.toggle(domClassMatch[2]);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const domStyleMatch = callStr.match(/^Dom\\.setStyle\\("([^"]+)"\\)$/);
|
|
1291
|
+
if (domStyleMatch && target) {
|
|
1292
|
+
const colonIdx = domStyleMatch[1].indexOf(":");
|
|
1293
|
+
target.style[domStyleMatch[1].slice(0, colonIdx).trim()] = domStyleMatch[1].slice(colonIdx + 1).trim();
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Var calls
|
|
1298
|
+
const varSetMatch = callStr.match(/^Var\\.set\\("([^"]+)"\\)$/);
|
|
1299
|
+
if (varSetMatch) {
|
|
1300
|
+
const colonIdx = varSetMatch[1].indexOf(":");
|
|
1301
|
+
_vars[varSetMatch[1].slice(0, colonIdx).trim()] = varSetMatch[1].slice(colonIdx + 1).trim();
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Anim calls
|
|
1306
|
+
const animMatch = callStr.match(/^Anim\\.(fadeIn|fadeOut|shake|pulse)\\("?([^"]*)"?\\)$/);
|
|
1307
|
+
if (animMatch && target) {
|
|
1308
|
+
const [, fn, arg] = animMatch;
|
|
1309
|
+
if (fn === "fadeIn") animFadeIn(target, parseInt(arg) || 300);
|
|
1310
|
+
if (fn === "fadeOut") animFadeOut(target, parseInt(arg) || 300);
|
|
1311
|
+
if (fn === "shake") animShake(target);
|
|
1312
|
+
if (fn === "pulse") animPulse(target, parseInt(arg) || 600);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Page calls
|
|
1317
|
+
if (callStr === "Page.reload()") { window.location.reload(); return; }
|
|
1318
|
+
if (callStr === "Page.scrollTop()") { window.scrollTo({ top: 0, behavior: "smooth" }); return; }
|
|
1319
|
+
const redirectMatch = callStr.match(/^Page\\.redirect\\("([^"]*)"\\)$/);
|
|
1320
|
+
if (redirectMatch) { window.location.href = resolvePath(redirectMatch[1]); return; }
|
|
1321
|
+
|
|
1322
|
+
warn(\`Unknown callback: \${callStr}\`);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
1326
|
+
|
|
1327
|
+
// ── Run a .vlx file ────────────────────────────────────────────────────────
|
|
1328
|
+
|
|
1329
|
+
async function runScript(vlxPath, options = {}) {
|
|
1330
|
+
const resolved = resolvePath(vlxPath);
|
|
1331
|
+
try {
|
|
1332
|
+
const res = await fetch(resolved);
|
|
1333
|
+
if (!res.ok) throw new Error(\`Could not load \${resolved} (HTTP \${res.status})\`);
|
|
1334
|
+
const source = await res.text();
|
|
1335
|
+
const commands = parseVlx(source);
|
|
1336
|
+
for (const cmd of commands) await executeCommand(cmd, options);
|
|
1337
|
+
} catch (e) {
|
|
1338
|
+
err("Runtime error: " + e.message);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// ── Run inline .vlx source ─────────────────────────────────────────────────
|
|
1343
|
+
|
|
1344
|
+
async function runInline(source, options = {}) {
|
|
1345
|
+
const commands = parseVlx(source);
|
|
1346
|
+
for (const cmd of commands) await executeCommand(cmd, options);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// ── Auto-init: scan HTML for <?velox ... > tags ────────────────────────────
|
|
1350
|
+
|
|
1351
|
+
async function autoInit() {
|
|
1352
|
+
await loadConfig();
|
|
1353
|
+
await loadQuickPaths();
|
|
1354
|
+
await loadVars();
|
|
1355
|
+
|
|
1356
|
+
// Read raw HTML source to find <?velox > tags
|
|
1357
|
+
const html = document.documentElement.outerHTML;
|
|
1358
|
+
const tags = extractVeloxTags(html);
|
|
1359
|
+
|
|
1360
|
+
for (const tag of tags) {
|
|
1361
|
+
if (!tag) continue;
|
|
1362
|
+
|
|
1363
|
+
// Velox.run("path") [$target="..."]
|
|
1364
|
+
const inline = parseInlineTag(tag);
|
|
1365
|
+
if (inline && inline.kind === "run") {
|
|
1366
|
+
const resolvedTarget = resolveTarget(inline.target);
|
|
1367
|
+
evalBlocks(inline.blocks, true, resolvedTarget);
|
|
1368
|
+
await runScript(inline.vlxPath, { target: resolvedTarget, type: inline.type });
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Raw .vlx syntax directly in the tag
|
|
1373
|
+
// Parse out optional $target="..." at the tag level
|
|
1374
|
+
const tagTargetMatch = tag.match(/\\$target\\s*=\\s*"([^"]+)"/);
|
|
1375
|
+
const tagTarget = tagTargetMatch ? resolveTarget(tagTargetMatch[1]) : null;
|
|
1376
|
+
await runInline(tag, { target: tagTarget });
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// ── Boot ───────────────────────────────────────────────────────────────────
|
|
1381
|
+
|
|
1382
|
+
if (document.readyState === "loading") {
|
|
1383
|
+
document.addEventListener("DOMContentLoaded", autoInit);
|
|
1384
|
+
} else {
|
|
1385
|
+
autoInit();
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
1389
|
+
|
|
1390
|
+
return {
|
|
1391
|
+
run: runScript,
|
|
1392
|
+
runInline,
|
|
1393
|
+
var: (key) => _vars[key] ?? null,
|
|
1394
|
+
navigate: routerNavigate,
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
})();
|
|
1398
|
+
`,
|
|
1399
|
+
|
|
1400
|
+
};
|