novac 2.0.1 → 2.2.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/LICENSE +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
|
@@ -1,92 +1,186 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* NVML Executor
|
|
4
|
+
* NVML Executor v2
|
|
5
5
|
*
|
|
6
|
-
* Walks the
|
|
7
|
-
*
|
|
8
|
-
* - @
|
|
9
|
-
* - @
|
|
10
|
-
* - @
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
6
|
+
* Walks the AST and builds a live NvmlDocument.
|
|
7
|
+
* New in v2:
|
|
8
|
+
* - @state → reactive signal store
|
|
9
|
+
* - @computed → derived signals (Nova code evaluated server-side)
|
|
10
|
+
* - @effect → side-effect declarations (run client-side on signal change)
|
|
11
|
+
* - @component → named reusable component definitions
|
|
12
|
+
* - @route → client-side route definitions
|
|
13
|
+
* - @slot → slot content definitions
|
|
14
|
+
* - @use → declare component imports for a page
|
|
15
|
+
* - bindings → one-way (->) and two-way (<->) signal bindings on elements
|
|
16
|
+
* - transitions → CSS transition hints (~) on elements
|
|
17
|
+
* - each_block → @each signal as item [ ... ] — reactive list rendering
|
|
18
|
+
* - conditional → ? signal {element} — conditional rendering
|
|
19
|
+
* - slot_outlet → @slot name inside a component — slot insertion point
|
|
14
20
|
*/
|
|
15
21
|
|
|
16
22
|
class ExecError extends Error {
|
|
17
23
|
constructor(msg) { super(`[NVML Executor] ${msg}`); this.name = 'ExecError'; }
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
// ── Document model
|
|
26
|
+
// ── Document model ────────────────────────────────────────────────────
|
|
21
27
|
|
|
22
28
|
class NvmlElement {
|
|
23
29
|
constructor(tag, id) {
|
|
24
|
-
this.tag
|
|
25
|
-
this.id
|
|
26
|
-
this.textValue
|
|
27
|
-
this.props
|
|
28
|
-
this.ss
|
|
29
|
-
this.code
|
|
30
|
-
this.children
|
|
31
|
-
this.extraProps
|
|
30
|
+
this.tag = tag;
|
|
31
|
+
this.id = id;
|
|
32
|
+
this.textValue = null;
|
|
33
|
+
this.props = {};
|
|
34
|
+
this.ss = null; // scoped CSS string
|
|
35
|
+
this.code = null; // script code
|
|
36
|
+
this.children = [];
|
|
37
|
+
this.extraProps = {};
|
|
38
|
+
this.bindings = []; // [{ prop, signal, twoWay }]
|
|
39
|
+
this.transitions = []; // [{ prop, name }]
|
|
40
|
+
this.cond = null; // signal name — only render if truthy
|
|
41
|
+
this.eachSignal = null; // for each_block wrapper
|
|
42
|
+
this.eachItemVar = null;
|
|
43
|
+
this.isSlotOutlet = false;
|
|
44
|
+
this.slotName = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class NvmlComponent {
|
|
49
|
+
constructor(name, body) {
|
|
50
|
+
this.name = name;
|
|
51
|
+
this.body = body; // raw AST body — instantiated on use
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* NvmlLangDef — a user-defined language block registered via @lang
|
|
57
|
+
*
|
|
58
|
+
* @lang {name} [
|
|
59
|
+
* runtime_language="nv" // "nv" | "js" | "nodejs"
|
|
60
|
+
* config="" // JSON string or path to config.json
|
|
61
|
+
* src="PATH" // path to implementation file
|
|
62
|
+
* code=(...) // inline implementation code
|
|
63
|
+
* ]
|
|
64
|
+
*
|
|
65
|
+
* Scope rules:
|
|
66
|
+
* runtime_language="nv" → server-side Nova (same as scope='server' lang='nv')
|
|
67
|
+
* runtime_language="nodejs" → server-side Node.js (new built-in)
|
|
68
|
+
* runtime_language="js" → client-side JavaScript (embedded in page)
|
|
69
|
+
*/
|
|
70
|
+
class NvmlLangDef {
|
|
71
|
+
constructor(name, opts = {}) {
|
|
72
|
+
this.name = name;
|
|
73
|
+
this.runtimeLanguage = opts.runtime_language || opts.runtimeLanguage || 'js';
|
|
74
|
+
this.config = opts.config || '';
|
|
75
|
+
this.src = opts.src || null;
|
|
76
|
+
this.code = opts.code || '';
|
|
77
|
+
this.scope = this._deriveScope();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_deriveScope() {
|
|
81
|
+
switch (this.runtimeLanguage) {
|
|
82
|
+
case 'nv':
|
|
83
|
+
case 'nova':
|
|
84
|
+
case 'novac': return 'server-nova';
|
|
85
|
+
case 'nodejs':
|
|
86
|
+
case 'node': return 'server-node';
|
|
87
|
+
case 'js':
|
|
88
|
+
default: return 'client';
|
|
89
|
+
}
|
|
32
90
|
}
|
|
33
91
|
}
|
|
34
92
|
|
|
35
93
|
class NvmlDocument {
|
|
36
94
|
constructor() {
|
|
37
|
-
this.config
|
|
38
|
-
this.visual
|
|
39
|
-
this.globalStyles = '';
|
|
40
|
-
this.
|
|
95
|
+
this.config = {}; // @config key→value
|
|
96
|
+
this.visual = []; // top-level NvmlElement[]
|
|
97
|
+
this.globalStyles = ''; // @ss content
|
|
98
|
+
this.state = {}; // @state signal name→initial value
|
|
99
|
+
this.computed = []; // [{ name, code }]
|
|
100
|
+
this.effects = []; // [{ deps, code }]
|
|
101
|
+
this.components = {}; // name→NvmlComponent
|
|
102
|
+
this.routes = []; // [{ path, body }]
|
|
103
|
+
this.slots = {}; // name→NvmlElement[]
|
|
104
|
+
this.usedComponents= []; // component names declared via @use
|
|
105
|
+
this.langs = {}; // name→NvmlLangDef (user-defined language extensions)
|
|
106
|
+
this._idCounter = 0;
|
|
41
107
|
}
|
|
42
108
|
|
|
43
109
|
_nextId() { return '_nvml_' + (this._idCounter++); }
|
|
44
110
|
|
|
45
|
-
|
|
46
|
-
|
|
111
|
+
setConfig(key, value) { this.config[key] = value; }
|
|
112
|
+
set(id, content) { this._mutateElement(id, el => { el.textValue = content; }); }
|
|
113
|
+
get(id) { return this._findElement(id)?.textValue ?? null; }
|
|
114
|
+
setProp(id, key, value) { this._mutateElement(id, el => { el.props[key] = value; }); }
|
|
115
|
+
getProp(id, key) { return this._findElement(id)?.props[key] ?? null; }
|
|
116
|
+
addClass(id, cls) { this._mutateElement(id, el => { el.props.class = ((el.props.class || '') + ' ' + cls).trim(); }); }
|
|
117
|
+
setClass(id, cls) { this._mutateElement(id, el => { el.props.class = cls; }); }
|
|
118
|
+
addStyle(css) { this.globalStyles += '\n' + css; }
|
|
119
|
+
addElementStyle(id, css) { this._mutateElement(id, el => { el.ss = (el.ss || '') + '\n' + css; }); }
|
|
120
|
+
hide(id) { this._mutateElement(id, el => { el.props.style = ((el.props.style || '') + '; display:none').replace(/^;/, ''); }); }
|
|
121
|
+
show(id, display = 'block') { this._mutateElement(id, el => { el.props.style = ((el.props.style || '') + `; display:${display}`).replace(/^;/, ''); }); }
|
|
122
|
+
setTitle(t) { this.config.title = t; }
|
|
123
|
+
setMeta(k, v) { this.config[k] = v; }
|
|
124
|
+
alert(msg) { /* no-op at render time */ }
|
|
125
|
+
setHTML(id, html) { this._mutateElement(id, el => { el._rawHTML = html; }); }
|
|
126
|
+
|
|
127
|
+
_findElement(id) {
|
|
128
|
+
const search = (els) => {
|
|
129
|
+
for (const el of els) {
|
|
130
|
+
if (el.props?.id === id || el.id === id) return el;
|
|
131
|
+
if (el.children) { const r = search(el.children); if (r) return r; }
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
};
|
|
135
|
+
return search(this.visual);
|
|
136
|
+
}
|
|
47
137
|
|
|
48
|
-
|
|
49
|
-
|
|
138
|
+
_mutateElement(id, fn) {
|
|
139
|
+
const el = this._findElement(id);
|
|
140
|
+
if (el) fn(el);
|
|
141
|
+
}
|
|
50
142
|
}
|
|
51
143
|
|
|
52
|
-
// ── Executor
|
|
144
|
+
// ── Executor ──────────────────────────────────────────────────────────
|
|
53
145
|
|
|
54
146
|
class Executor {
|
|
55
|
-
/**
|
|
56
|
-
* @param {object} novaRunner - { run(source, scope): any }
|
|
57
|
-
* Used to execute server-side Nova script blocks.
|
|
58
|
-
*/
|
|
59
147
|
constructor(novaRunner) {
|
|
60
148
|
this.novaRunner = novaRunner;
|
|
61
149
|
this.doc = null;
|
|
62
150
|
}
|
|
63
151
|
|
|
64
|
-
// ── public entry point ────────────────────────────────────
|
|
65
|
-
|
|
66
152
|
execute(ast, existingDoc) {
|
|
67
153
|
this.doc = existingDoc || new NvmlDocument();
|
|
68
|
-
for (const node of ast.body)
|
|
69
|
-
this._execNode(node, null);
|
|
70
|
-
}
|
|
154
|
+
for (const node of ast.body) this._execNode(node, null);
|
|
71
155
|
return this.doc;
|
|
72
156
|
}
|
|
73
157
|
|
|
74
|
-
// ── dispatch ──────────────────────────────────────────────
|
|
75
|
-
|
|
76
158
|
_execNode(node, parentElement) {
|
|
77
159
|
switch (node.kind) {
|
|
78
|
-
case 'at_config':
|
|
79
|
-
case '
|
|
80
|
-
case '
|
|
81
|
-
case '
|
|
82
|
-
case '
|
|
83
|
-
case '
|
|
160
|
+
case 'at_config': return this._execConfig(node);
|
|
161
|
+
case 'at_state': return this._execState(node);
|
|
162
|
+
case 'at_computed': return this._execComputed(node);
|
|
163
|
+
case 'at_effect': return this._execEffect(node);
|
|
164
|
+
case 'at_component': return this._execComponent(node);
|
|
165
|
+
case 'at_route': return this._execRoute(node);
|
|
166
|
+
case 'at_slot': return this._execSlot(node);
|
|
167
|
+
case 'at_use': return this._execUse(node);
|
|
168
|
+
case 'at_lang': return this._execLang(node);
|
|
169
|
+
case 'at_block': return this._execAtBlock(node, parentElement);
|
|
170
|
+
case 'element': return this._execElement(node, parentElement);
|
|
171
|
+
case 'each_block': return this._execEachBlock(node, parentElement);
|
|
172
|
+
case 'text_node': return this._execTextNode(node, parentElement);
|
|
173
|
+
case 'parent_prop': return this._execParentProp(node, parentElement);
|
|
174
|
+
case 'slot_outlet': return this._execSlotOutlet(node, parentElement);
|
|
175
|
+
case 'component_use': return this._execComponentUse(node, parentElement);
|
|
176
|
+
case 'signal_ref': return; // resolved at render time
|
|
177
|
+
case 'css_decl': return;
|
|
84
178
|
default:
|
|
85
179
|
throw new ExecError(`Unknown AST node kind: ${node.kind}`);
|
|
86
180
|
}
|
|
87
181
|
}
|
|
88
182
|
|
|
89
|
-
// ── @config
|
|
183
|
+
// ── @config ──────────────────────────────────────────────────────
|
|
90
184
|
|
|
91
185
|
_execConfig(node) {
|
|
92
186
|
for (const [key, valNode] of Object.entries(node.pairs)) {
|
|
@@ -94,158 +188,309 @@ class Executor {
|
|
|
94
188
|
}
|
|
95
189
|
}
|
|
96
190
|
|
|
97
|
-
// ── @
|
|
191
|
+
// ── @state ───────────────────────────────────────────────────────
|
|
98
192
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
193
|
+
_execState(node) {
|
|
194
|
+
for (const [key, valNode] of Object.entries(node.pairs)) {
|
|
195
|
+
this.doc.state[key] = this._resolveValue(valNode);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── @computed ────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
_execComputed(node) {
|
|
202
|
+
for (const entry of node.entries) {
|
|
203
|
+
const code = this._resolveValue(entry.code);
|
|
204
|
+
let value = null;
|
|
205
|
+
// Run the Nova code server-side if possible to get an initial value
|
|
206
|
+
if (this.novaRunner && code) {
|
|
207
|
+
try { value = this.novaRunner(code, this.doc); } catch (e) { value = null; }
|
|
106
208
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
209
|
+
this.doc.computed.push({ name: entry.name, code, initialValue: value });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── @effect ──────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
_execEffect(node) {
|
|
216
|
+
for (const entry of node.entries) {
|
|
217
|
+
this.doc.effects.push({
|
|
218
|
+
deps: entry.deps,
|
|
219
|
+
code: this._resolveValue(entry.code),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── @component ───────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
_execComponent(node) {
|
|
227
|
+
this.doc.components[node.name] = new NvmlComponent(node.name, node.body);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── @route ───────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
_execRoute(node) {
|
|
233
|
+
this.doc.routes.push({ path: this._resolveValue(node.arg || node.path), body: node.body });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── @slot ────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
_execSlot(node) {
|
|
239
|
+
const els = [];
|
|
240
|
+
for (const child of node.body) {
|
|
241
|
+
const el = this._buildElement(child, null);
|
|
242
|
+
if (el) els.push(el);
|
|
243
|
+
}
|
|
244
|
+
this.doc.slots[node.name] = els;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── @lang ─────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
_execLang(node) {
|
|
250
|
+
const opts = {};
|
|
251
|
+
for (const [key, valNode] of Object.entries(node.pairs || {})) {
|
|
252
|
+
opts[key] = this._resolveValue(valNode);
|
|
253
|
+
}
|
|
254
|
+
// code may come from inline code= or src= file
|
|
255
|
+
if (!opts.code && opts.src) {
|
|
256
|
+
try {
|
|
257
|
+
const fs = require('fs');
|
|
258
|
+
const path = require('path');
|
|
259
|
+
opts.code = fs.readFileSync(path.resolve(process.cwd(), opts.src), 'utf8');
|
|
260
|
+
} catch (e) {
|
|
261
|
+
process.stderr.write(`[NVML @lang ${node.name}] Cannot read src: ${e.message}\n`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// load JSON config if it's a path string
|
|
265
|
+
if (opts.config && !opts.config.trim().startsWith('{')) {
|
|
266
|
+
try {
|
|
267
|
+
const fs = require('fs');
|
|
268
|
+
const path = require('path');
|
|
269
|
+
opts.config = fs.readFileSync(path.resolve(process.cwd(), opts.config), 'utf8');
|
|
270
|
+
} catch (e) {
|
|
271
|
+
process.stderr.write(`[NVML @lang ${node.name}] Cannot read config file: ${e.message}\n`);
|
|
111
272
|
}
|
|
112
|
-
return;
|
|
113
273
|
}
|
|
274
|
+
this.doc.langs[node.name] = new NvmlLangDef(node.name, opts);
|
|
275
|
+
}
|
|
114
276
|
|
|
277
|
+
_execUse(node) {
|
|
278
|
+
for (const name of node.names) {
|
|
279
|
+
if (!this.doc.usedComponents.includes(name)) this.doc.usedComponents.push(name);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── @visual / @ss / custom ───────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
_execAtBlock(node, parentElement) {
|
|
286
|
+
if (node.name === 'ss') {
|
|
287
|
+
const css = node.body.map(c => this._nodeToCSS(c)).join('\n');
|
|
288
|
+
if (parentElement) parentElement.ss = (parentElement.ss || '') + css;
|
|
289
|
+
else this.doc.globalStyles += css;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
115
292
|
if (node.name === 'visual') {
|
|
116
|
-
for (const child of node.body)
|
|
117
|
-
this._execNode(child, null);
|
|
118
|
-
}
|
|
293
|
+
for (const child of node.body) this._execNode(child, null);
|
|
119
294
|
return;
|
|
120
295
|
}
|
|
121
|
-
|
|
122
|
-
|
|
296
|
+
// each inside element body
|
|
297
|
+
if (node.name === 'each') {
|
|
298
|
+
// handled by parseEachBlock
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// unknown @block → store in doc.config
|
|
123
302
|
const section = [];
|
|
124
303
|
for (const child of node.body) {
|
|
125
|
-
const el = this.
|
|
304
|
+
const el = this._buildElement(child, null);
|
|
126
305
|
if (el) section.push(el);
|
|
127
306
|
}
|
|
128
307
|
this.doc.config[node.name] = section;
|
|
129
308
|
}
|
|
130
309
|
|
|
131
|
-
// ── element
|
|
310
|
+
// ── element ───────────────────────────────────────────────────────
|
|
132
311
|
|
|
133
312
|
_execElement(node, parentElement) {
|
|
134
|
-
const el = this.
|
|
313
|
+
const el = this._buildElement(node, parentElement);
|
|
135
314
|
if (!el) return;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
parentElement.children.push(el);
|
|
139
|
-
} else {
|
|
140
|
-
this.doc.visual.push(el);
|
|
141
|
-
}
|
|
315
|
+
if (parentElement) parentElement.children.push(el);
|
|
316
|
+
else this.doc.visual.push(el);
|
|
142
317
|
}
|
|
143
318
|
|
|
144
|
-
|
|
319
|
+
_buildElement(node, parentElement) {
|
|
320
|
+
if (!node) return null;
|
|
321
|
+
|
|
145
322
|
if (node.kind === 'text_node') {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
323
|
+
if (parentElement) parentElement.textValue = (parentElement.textValue || '') + node.value;
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
if (node.kind === 'signal_ref') {
|
|
327
|
+
// Inline signal reference: bind text content to signal
|
|
328
|
+
if (parentElement) parentElement._signalText = node.name || node.value;
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
if (node.kind === 'each_block') {
|
|
332
|
+
const wrapper = new NvmlElement('each-block', this.doc._nextId());
|
|
333
|
+
wrapper.eachSignal = node.signal;
|
|
334
|
+
wrapper.eachItemVar = node.itemVar;
|
|
335
|
+
wrapper.eachIndexVar = node.indexVar;
|
|
336
|
+
wrapper._eachBody = node.body; // raw AST — rendered per-item at client
|
|
337
|
+
if (parentElement) parentElement.children.push(wrapper);
|
|
338
|
+
else this.doc.visual.push(wrapper);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
if (node.kind === 'slot_outlet') {
|
|
342
|
+
const outlet = new NvmlElement('slot-outlet', this.doc._nextId());
|
|
343
|
+
outlet.isSlotOutlet = true;
|
|
344
|
+
outlet.slotName = node.name || 'default';
|
|
345
|
+
if (parentElement) parentElement.children.push(outlet);
|
|
150
346
|
return null;
|
|
151
347
|
}
|
|
152
348
|
if (node.kind !== 'element') return null;
|
|
153
349
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
350
|
+
// resolve tag
|
|
351
|
+
let tag = node.tag;
|
|
352
|
+
if (tag.startsWith('element.')) tag = tag.slice('element.'.length);
|
|
353
|
+
else if (tag.includes('.')) tag = tag.split('.').pop();
|
|
354
|
+
if (tag === '..' || tag === '') return null;
|
|
355
|
+
|
|
356
|
+
// Check if this tag is a component name
|
|
357
|
+
if (this.doc.components[tag] || this.doc.usedComponents.includes(tag)) {
|
|
358
|
+
return this._instantiateComponent(tag, node, parentElement);
|
|
162
359
|
}
|
|
163
|
-
// {..} is parent-ref used standalone — skip
|
|
164
|
-
if (rawTag === '..') return null;
|
|
165
360
|
|
|
166
361
|
const el = new NvmlElement(tag, this.doc._nextId());
|
|
167
362
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
el.
|
|
363
|
+
if (node.textValue !== null && node.textValue !== undefined) {
|
|
364
|
+
const tv = node.textValue;
|
|
365
|
+
if (tv && tv.kind === 'signal_ref') el._signalText = tv.name || tv.value;
|
|
366
|
+
else el.textValue = this._resolveValue(tv);
|
|
171
367
|
}
|
|
172
368
|
|
|
369
|
+
// conditional render
|
|
370
|
+
if (node.cond) el.cond = node.cond;
|
|
371
|
+
|
|
372
|
+
// bindings
|
|
373
|
+
el.bindings = (node.bindings || []).map(b => ({ prop: b.prop, signal: b.signal, twoWay: b.twoWay }));
|
|
374
|
+
el.transitions = (node.transitions|| []).map(t => ({ prop: t.prop, name: t.name }));
|
|
375
|
+
|
|
173
376
|
// props
|
|
174
|
-
for (const [key, valNode] of Object.entries(node.props)) {
|
|
377
|
+
for (const [key, valNode] of Object.entries(node.props || {})) {
|
|
175
378
|
if (valNode && valNode.__subBody !== undefined) {
|
|
176
|
-
// [..]::key [ subBody ] — stored as special sub-body
|
|
177
379
|
if (key === 'ss') {
|
|
178
|
-
|
|
179
|
-
el.ss = cssLines.join('\n');
|
|
380
|
+
el.ss = valNode.__subBody.map(c => this._nodeToCSS(c)).filter(Boolean).join('\n');
|
|
180
381
|
} else if (key === 'code') {
|
|
181
|
-
|
|
182
|
-
el.code = valNode.__subBody
|
|
183
|
-
.map(c => c.kind === 'text_node' ? c.value : '')
|
|
184
|
-
.join('\n');
|
|
382
|
+
el.code = valNode.__subBody.map(c => c.kind === 'text_node' ? c.value : '').join('\n');
|
|
185
383
|
} else {
|
|
186
384
|
el.extraProps[key] = valNode.__subBody;
|
|
187
385
|
}
|
|
386
|
+
} else if (valNode && valNode.kind === 'signal_ref') {
|
|
387
|
+
// prop -> signal binding via value shorthand
|
|
388
|
+
el.bindings.push({ prop: key, signal: valNode.name || valNode.value, twoWay: false });
|
|
188
389
|
} else {
|
|
189
390
|
const resolved = this._resolveValue(valNode);
|
|
190
|
-
if (key === 'ss')
|
|
191
|
-
else if (key === 'code')
|
|
192
|
-
else
|
|
391
|
+
if (key === 'ss') el.ss = resolved;
|
|
392
|
+
else if (key === 'code') el.code = resolved;
|
|
393
|
+
else el.props[key] = resolved;
|
|
193
394
|
}
|
|
194
395
|
}
|
|
195
396
|
|
|
196
|
-
// body
|
|
197
|
-
for (const child of node.body) {
|
|
397
|
+
// body children
|
|
398
|
+
for (const child of (node.body || [])) {
|
|
198
399
|
if (child.kind === 'text_node') {
|
|
199
400
|
el.textValue = (el.textValue || '') + child.value;
|
|
200
401
|
} else if (child.kind === 'parent_prop') {
|
|
201
402
|
this._applyParentProp(child, el);
|
|
403
|
+
} else if (child.kind === 'signal_ref') {
|
|
404
|
+
el._signalText = child.name || child.value;
|
|
202
405
|
} else {
|
|
203
406
|
this._execNode(child, el);
|
|
204
407
|
}
|
|
205
408
|
}
|
|
206
409
|
|
|
207
|
-
//
|
|
208
|
-
if (tag === 'script')
|
|
209
|
-
this._handleScript(el);
|
|
210
|
-
}
|
|
410
|
+
// script handling
|
|
411
|
+
if (tag === 'script') this._handleScript(el);
|
|
211
412
|
|
|
212
413
|
return el;
|
|
213
414
|
}
|
|
214
415
|
|
|
215
|
-
// ──
|
|
416
|
+
// ── component instantiation ───────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
_instantiateComponent(name, node, parentElement) {
|
|
419
|
+
const comp = this.doc.components[name];
|
|
420
|
+
if (!comp) {
|
|
421
|
+
// External component (registered via @use but defined elsewhere) — emit a placeholder
|
|
422
|
+
const placeholder = new NvmlElement('div', this.doc._nextId());
|
|
423
|
+
placeholder.props['data-component'] = name;
|
|
424
|
+
placeholder.props['data-props'] = JSON.stringify(
|
|
425
|
+
Object.fromEntries(Object.entries(node.props || {}).map(([k,v]) => [k, this._resolveValue(v)]))
|
|
426
|
+
);
|
|
427
|
+
// pass bindings as data attrs
|
|
428
|
+
for (const b of (node.bindings || [])) {
|
|
429
|
+
placeholder.props[`data-bind-${b.prop}`] = b.signal + (b.twoWay ? ':2way' : '');
|
|
430
|
+
}
|
|
431
|
+
if (parentElement) parentElement.children.push(placeholder);
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
// Inline component: instantiate its body with prop overrides
|
|
435
|
+
const compRoot = new NvmlElement('div', this.doc._nextId());
|
|
436
|
+
compRoot.props['data-component'] = name;
|
|
437
|
+
for (const child of comp.body) this._execNode(child, compRoot);
|
|
438
|
+
if (parentElement) parentElement.children.push(compRoot);
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── @each block ───────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
_execEachBlock(node, parentElement) {
|
|
445
|
+
const wrapper = new NvmlElement('each-block', this.doc._nextId());
|
|
446
|
+
wrapper.eachSignal = node.signal;
|
|
447
|
+
wrapper.eachItemVar = node.itemVar;
|
|
448
|
+
wrapper.eachIndexVar = node.indexVar;
|
|
449
|
+
wrapper._eachBody = node.body;
|
|
450
|
+
if (parentElement) parentElement.children.push(wrapper);
|
|
451
|
+
else this.doc.visual.push(wrapper);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── slot outlet ───────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
_execSlotOutlet(node, parentElement) {
|
|
457
|
+
const outlet = new NvmlElement('slot-outlet', this.doc._nextId());
|
|
458
|
+
outlet.isSlotOutlet = true;
|
|
459
|
+
outlet.slotName = node.name || 'default';
|
|
460
|
+
if (parentElement) parentElement.children.push(outlet);
|
|
461
|
+
else this.doc.visual.push(outlet);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── [..] prop ────────────────────────────────────────────────────
|
|
216
465
|
|
|
217
|
-
_applyParentProp(node,
|
|
466
|
+
_applyParentProp(node, el) {
|
|
218
467
|
const prop = node.prop;
|
|
219
468
|
if (node.subBody !== null && node.subBody !== undefined) {
|
|
220
469
|
if (prop === 'ss') {
|
|
221
|
-
|
|
222
|
-
parentElement.ss = (parentElement.ss || '') + cssLines.join('\n');
|
|
470
|
+
el.ss = (el.ss || '') + node.subBody.map(c => this._nodeToCSS(c)).join('\n');
|
|
223
471
|
} else if (prop === 'code') {
|
|
224
|
-
|
|
225
|
-
parentElement.code = codeLines.join('\n');
|
|
472
|
+
el.code = node.subBody.map(c => c.kind === 'text_node' ? c.value : '').join('\n');
|
|
226
473
|
} else {
|
|
227
|
-
|
|
474
|
+
el.extraProps[prop] = node.subBody;
|
|
228
475
|
}
|
|
229
476
|
} else {
|
|
230
477
|
const resolved = node.value !== null ? this._resolveValue(node.value) : true;
|
|
231
|
-
if
|
|
232
|
-
else if (prop === 'code')
|
|
233
|
-
else
|
|
478
|
+
if (prop === 'ss') el.ss = resolved;
|
|
479
|
+
else if (prop === 'code') el.code = resolved;
|
|
480
|
+
else el.extraProps[prop] = resolved;
|
|
234
481
|
}
|
|
235
482
|
}
|
|
236
483
|
|
|
237
484
|
_execParentProp(node, parentElement) {
|
|
238
|
-
if (!parentElement) return;
|
|
485
|
+
if (!parentElement) return;
|
|
239
486
|
this._applyParentProp(node, parentElement);
|
|
240
487
|
}
|
|
241
488
|
|
|
242
489
|
_execTextNode(node, parentElement) {
|
|
243
|
-
if (parentElement)
|
|
244
|
-
parentElement.textValue = (parentElement.textValue || '') + node.value;
|
|
245
|
-
}
|
|
490
|
+
if (parentElement) parentElement.textValue = (parentElement.textValue || '') + node.value;
|
|
246
491
|
}
|
|
247
492
|
|
|
248
|
-
// ── script handling
|
|
493
|
+
// ── script handling ───────────────────────────────────────────────
|
|
249
494
|
|
|
250
495
|
_handleScript(el) {
|
|
251
496
|
const lang = el.props.language || el.props.lang || 'js';
|
|
@@ -253,85 +498,242 @@ class Executor {
|
|
|
253
498
|
const code = el.code || el.textValue || '';
|
|
254
499
|
const trigger = el.props.trigger || null;
|
|
255
500
|
|
|
256
|
-
el._scriptLang
|
|
257
|
-
el._scriptScope
|
|
258
|
-
el._scriptCode
|
|
259
|
-
el._ranOnServer
|
|
501
|
+
el._scriptLang = lang;
|
|
502
|
+
el._scriptScope = scope;
|
|
503
|
+
el._scriptCode = code;
|
|
504
|
+
el._ranOnServer = false;
|
|
260
505
|
|
|
261
|
-
//
|
|
262
|
-
// so their document mutations are baked into the HTML before it is sent.
|
|
263
|
-
// Triggered scripts (click, change, …) have no code to run at render
|
|
264
|
-
// time — they fire later via the /_nvml/run endpoint.
|
|
506
|
+
// ── Nova server-side (scope=server, no trigger) ──────────────
|
|
265
507
|
if ((lang === 'novac' || lang === 'nv') && scope === 'server' && !trigger && code.trim()) {
|
|
266
508
|
if (this.novaRunner) {
|
|
267
|
-
// Mark as ran-on-server immediately: any partial mutations applied before
|
|
268
|
-
// an error are already baked into the document, so we must not re-run
|
|
269
|
-
// the same code client-side (that would double-apply the mutations).
|
|
270
509
|
el._ranOnServer = true;
|
|
271
|
-
try {
|
|
272
|
-
|
|
273
|
-
} catch (e) {
|
|
274
|
-
process.stderr.write(`[NVML Executor] Server script error: ${e.message}\n`);
|
|
275
|
-
}
|
|
510
|
+
try { this.novaRunner(code, this.doc); }
|
|
511
|
+
catch (e) { process.stderr.write(`[NVML Executor] Server script error: ${e.message}\n`); }
|
|
276
512
|
} else {
|
|
277
|
-
process.stderr.write(
|
|
278
|
-
'[NVML Executor] Warning: server-side Nova script found but no novaRunner provided — script skipped.\n'
|
|
279
|
-
);
|
|
513
|
+
process.stderr.write('[NVML Executor] Warning: server-side Nova script found but no novaRunner provided.\n');
|
|
280
514
|
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ── Node.js server-side (lang=nodejs, no trigger) ─────────────
|
|
519
|
+
if ((lang === 'nodejs' || lang === 'node') && !trigger && code.trim()) {
|
|
520
|
+
el._ranOnServer = true;
|
|
521
|
+
try { this._runNodejsScript(code, el); }
|
|
522
|
+
catch (e) { process.stderr.write(`[NVML Executor] nodejs script error: ${e.message}\n`); }
|
|
523
|
+
return;
|
|
281
524
|
}
|
|
525
|
+
|
|
526
|
+
// ── User-defined language (registered via @lang) ──────────────
|
|
527
|
+
const langDef = this.doc.langs[lang];
|
|
528
|
+
if (langDef && !trigger && code.trim()) {
|
|
529
|
+
if (langDef.scope === 'server-nova' && this.novaRunner) {
|
|
530
|
+
el._ranOnServer = true;
|
|
531
|
+
// Wrap: call the lang's implementation code first, then user code
|
|
532
|
+
const wrapped = langDef.code
|
|
533
|
+
? langDef.code + '\n' + code
|
|
534
|
+
: code;
|
|
535
|
+
try { this.novaRunner(wrapped, this.doc); }
|
|
536
|
+
catch (e) { process.stderr.write(`[NVML Executor] @lang ${lang} (nv) error: ${e.message}\n`); }
|
|
537
|
+
} else if (langDef.scope === 'server-node') {
|
|
538
|
+
el._ranOnServer = true;
|
|
539
|
+
try { this._runNodejsScript((langDef.code ? langDef.code + '\n' : '') + code, el); }
|
|
540
|
+
catch (e) { process.stderr.write(`[NVML Executor] @lang ${lang} (nodejs) error: ${e.message}\n`); }
|
|
541
|
+
}
|
|
542
|
+
// client-scoped custom langs: renderer handles embedding
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── Node.js script runner ────────────────────────────────────────
|
|
548
|
+
// Runs code in Node's VM with document proxy + bf object in scope.
|
|
549
|
+
_runNodejsScript(code, el) {
|
|
550
|
+
const vm = require('vm');
|
|
551
|
+
const doc = this.doc;
|
|
552
|
+
|
|
553
|
+
// Build the document proxy (same surface as Nova runner)
|
|
554
|
+
const documentProxy = this._makeNodejsDocumentProxy(doc);
|
|
555
|
+
|
|
556
|
+
const ctx = vm.createContext({
|
|
557
|
+
document: documentProxy,
|
|
558
|
+
bf: makeBfObject(),
|
|
559
|
+
console,
|
|
560
|
+
require,
|
|
561
|
+
process,
|
|
562
|
+
__filename: '',
|
|
563
|
+
__dirname: process.cwd(),
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
vm.runInContext(code, ctx, { timeout: 5000 });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
_makeNodejsDocumentProxy(doc) {
|
|
570
|
+
const findEl = (id) => {
|
|
571
|
+
const search = (els) => {
|
|
572
|
+
for (const el of els) {
|
|
573
|
+
if (el.props?.id === id || el.id === id) return el;
|
|
574
|
+
if (el.children) { const r = search(el.children); if (r) return r; }
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
};
|
|
578
|
+
return search(doc.visual);
|
|
579
|
+
};
|
|
580
|
+
return {
|
|
581
|
+
setConfig: (k, v) => { doc.config[k] = v; },
|
|
582
|
+
setTitle: (t) => { doc.config.title = t; },
|
|
583
|
+
setMeta: (k, v) => { doc.config[k] = v; },
|
|
584
|
+
getConfig: (k) => doc.config[k],
|
|
585
|
+
set: (id, val) => { const el = findEl(id); if (el) el.textValue = String(val); },
|
|
586
|
+
get: (id) => { const el = findEl(id); return el ? (el.textValue || null) : null; },
|
|
587
|
+
setHTML: (id, html)=> { const el = findEl(id); if (el) el._rawHTML = html; },
|
|
588
|
+
setProp: (id, k, v)=> { const el = findEl(id); if (el) el.props[k] = String(v); },
|
|
589
|
+
getProp: (id, k) => { const el = findEl(id); return el ? (el.props[k] || null) : null; },
|
|
590
|
+
addClass: (id, cls) => { const el = findEl(id); if (!el) return; el.props.class = ((el.props.class||'')+' '+cls).trim(); },
|
|
591
|
+
setClass: (id, cls) => { const el = findEl(id); if (el) el.props.class = cls; },
|
|
592
|
+
addStyle: (css) => { doc.globalStyles += '\n' + css; },
|
|
593
|
+
addElementStyle:(id, css) => { const el = findEl(id); if (el) el.ss = (el.ss||'')+'\n'+css; },
|
|
594
|
+
hide: (id) => { const el = findEl(id); if (el) el.props.style = ((el.props.style||'')+';display:none').replace(/^;/,''); },
|
|
595
|
+
show: (id, d) => { const el = findEl(id); if (el) el.props.style = ((el.props.style||'')+`;display:${d||'block'}`).replace(/^;/,''); },
|
|
596
|
+
setSignal: (n, v) => { doc.state[n] = v; },
|
|
597
|
+
getSignal: (n) => doc.state[n],
|
|
598
|
+
alert: (_) => {},
|
|
599
|
+
config: doc.config,
|
|
600
|
+
_doc: doc,
|
|
601
|
+
};
|
|
282
602
|
}
|
|
283
603
|
|
|
284
|
-
// ── CSS
|
|
604
|
+
// ── CSS serializer ────────────────────────────────────────────────
|
|
285
605
|
|
|
286
606
|
_nodeToCSS(node) {
|
|
287
607
|
if (!node) return '';
|
|
288
608
|
if (node.kind === 'text_node') return node.value;
|
|
289
|
-
if (node.kind === 'css_decl') {
|
|
290
|
-
const val = this._resolveValue(node.value);
|
|
291
|
-
return `${node.key}: ${val};`;
|
|
292
|
-
}
|
|
609
|
+
if (node.kind === 'css_decl') return `${node.key}: ${this._resolveValue(node.value)};`;
|
|
293
610
|
if (node.kind === 'element') {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if ((textVal !== null && textVal !== undefined) &&
|
|
301
|
-
Object.keys(nodeProps).length === 0 &&
|
|
302
|
-
nodeChildren.length === 0 &&
|
|
303
|
-
nodeBody.length === 0) {
|
|
304
|
-
const propName = node.tag;
|
|
305
|
-
const propVal = typeof textVal === 'object'
|
|
306
|
-
? this._resolveValue(textVal)
|
|
307
|
-
: textVal;
|
|
308
|
-
return ` ${propName}: ${propVal};`;
|
|
611
|
+
const tv = node.textValue;
|
|
612
|
+
if ((tv !== null && tv !== undefined) &&
|
|
613
|
+
Object.keys(node.props || {}).length === 0 &&
|
|
614
|
+
(node.body || []).length === 0) {
|
|
615
|
+
const propVal = typeof tv === 'object' ? this._resolveValue(tv) : tv;
|
|
616
|
+
return ` ${node.tag}: ${propVal};`;
|
|
309
617
|
}
|
|
310
|
-
// treat {selector} [ decl='val', ] as a full CSS rule
|
|
311
618
|
const sel = node.tag;
|
|
312
|
-
const decls = Object.entries(
|
|
313
|
-
|
|
314
|
-
.join('\n');
|
|
315
|
-
const nested = nodeBody.map(c => this._nodeToCSS(c)).filter(Boolean).join('\n');
|
|
619
|
+
const decls = Object.entries(node.props || {}).map(([k,v]) => ` ${k}: ${this._resolveValue(v)};`).join('\n');
|
|
620
|
+
const nested= (node.body || []).map(c => this._nodeToCSS(c)).filter(Boolean).join('\n');
|
|
316
621
|
return `${sel} {\n${decls}${nested ? '\n' + nested : ''}\n}`;
|
|
317
622
|
}
|
|
318
623
|
return '';
|
|
319
624
|
}
|
|
320
625
|
|
|
321
|
-
|
|
322
|
-
if (!node) return '';
|
|
323
|
-
if (node.kind === 'text_node') return node.value;
|
|
324
|
-
return '';
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// ── value resolver ────────────────────────────────────────
|
|
626
|
+
// ── value resolver ────────────────────────────────────────────────
|
|
328
627
|
|
|
329
628
|
_resolveValue(valNode) {
|
|
330
629
|
if (!valNode) return undefined;
|
|
331
|
-
if (valNode.kind === 'value')
|
|
332
|
-
if (valNode.kind === 'pipe_list')
|
|
630
|
+
if (valNode.kind === 'value') return valNode.value;
|
|
631
|
+
if (valNode.kind === 'pipe_list') return valNode.items;
|
|
632
|
+
if (valNode.kind === 'signal_ref') return `__sig:${valNode.name || valNode.value}`;
|
|
333
633
|
return undefined;
|
|
334
634
|
}
|
|
335
635
|
}
|
|
336
636
|
|
|
337
|
-
|
|
637
|
+
// ── Brainfuck object ──────────────────────────────────────────────────
|
|
638
|
+
//
|
|
639
|
+
// Available as `bf` in every language scope (server and client).
|
|
640
|
+
//
|
|
641
|
+
// Server-side (Nova / nodejs scripts): a live JS object.
|
|
642
|
+
// Client-side: serialised to the page as window.__nvml_bf and also
|
|
643
|
+
// injected into the custom-language JS wrapper.
|
|
644
|
+
//
|
|
645
|
+
// Properties / methods:
|
|
646
|
+
// bf.tape — Uint8Array(30000) — the cell tape
|
|
647
|
+
// bf.pointer — current data pointer (integer)
|
|
648
|
+
// bf.output — string accumulated by '.' instructions
|
|
649
|
+
// bf.input — string used as the input buffer for ',' instructions
|
|
650
|
+
// bf.run(code) — run a BF program; returns the output string
|
|
651
|
+
// bf.reset() — clear tape, pointer, I/O buffers
|
|
652
|
+
// bf.cell(n?) — get (or with value: set) the value of cell n (default: current cell)
|
|
653
|
+
|
|
654
|
+
function makeBfObject(opts = {}) {
|
|
655
|
+
const TAPE_SIZE = opts.tapeSize || 30000;
|
|
656
|
+
let tape = new Uint8Array(TAPE_SIZE);
|
|
657
|
+
let ptr = 0;
|
|
658
|
+
let output = '';
|
|
659
|
+
let input = opts.input || '';
|
|
660
|
+
let inPtr = 0;
|
|
661
|
+
|
|
662
|
+
const obj = {
|
|
663
|
+
get tape() { return tape; },
|
|
664
|
+
get pointer() { return ptr; },
|
|
665
|
+
get output() { return output; },
|
|
666
|
+
get input() { return input; },
|
|
667
|
+
set input(v) { input = String(v); inPtr = 0; },
|
|
668
|
+
|
|
669
|
+
cell(n, v) {
|
|
670
|
+
const idx = (n === undefined) ? ptr : Number(n);
|
|
671
|
+
if (v !== undefined) tape[idx] = Number(v) & 0xFF;
|
|
672
|
+
return tape[idx];
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
reset() {
|
|
676
|
+
tape = new Uint8Array(TAPE_SIZE);
|
|
677
|
+
ptr = 0;
|
|
678
|
+
output = '';
|
|
679
|
+
input = '';
|
|
680
|
+
inPtr = 0;
|
|
681
|
+
return obj;
|
|
682
|
+
},
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* run(code, inputStr?) → output string
|
|
686
|
+
* Executes BF source. Stops on EOF (',') if input buffer is exhausted.
|
|
687
|
+
* Cell values wrap at 255/0 (unsigned byte).
|
|
688
|
+
*/
|
|
689
|
+
run(code, inputStr) {
|
|
690
|
+
if (inputStr !== undefined) { input = String(inputStr); inPtr = 0; }
|
|
691
|
+
const src = String(code).split('').filter(c => '><+-.,[]'.includes(c));
|
|
692
|
+
let ip = 0;
|
|
693
|
+
const MAX_OPS = opts.maxOps || 10_000_000;
|
|
694
|
+
let ops = 0;
|
|
695
|
+
|
|
696
|
+
// pre-build bracket map for O(1) jump
|
|
697
|
+
const brackets = {};
|
|
698
|
+
const stack = [];
|
|
699
|
+
for (let i = 0; i < src.length; i++) {
|
|
700
|
+
if (src[i] === '[') { stack.push(i); }
|
|
701
|
+
else if (src[i] === ']') {
|
|
702
|
+
const open = stack.pop();
|
|
703
|
+
if (open === undefined) throw new Error(`[bf] Unmatched ']' at ${i}`);
|
|
704
|
+
brackets[open] = i;
|
|
705
|
+
brackets[i] = open;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (stack.length) throw new Error(`[bf] Unmatched '[' at ${stack[0]}`);
|
|
709
|
+
|
|
710
|
+
while (ip < src.length) {
|
|
711
|
+
if (++ops > MAX_OPS) throw new Error('[bf] Maximum operations exceeded');
|
|
712
|
+
const cmd = src[ip];
|
|
713
|
+
switch (cmd) {
|
|
714
|
+
case '>': ptr = (ptr + 1) % TAPE_SIZE; break;
|
|
715
|
+
case '<': ptr = (ptr - 1 + TAPE_SIZE) % TAPE_SIZE; break;
|
|
716
|
+
case '+': tape[ptr] = (tape[ptr] + 1) & 0xFF; break;
|
|
717
|
+
case '-': tape[ptr] = (tape[ptr] - 1 + 256) & 0xFF; break;
|
|
718
|
+
case '.': output += String.fromCharCode(tape[ptr]); break;
|
|
719
|
+
case ',': {
|
|
720
|
+
if (inPtr < input.length) {
|
|
721
|
+
tape[ptr] = input.charCodeAt(inPtr++) & 0xFF;
|
|
722
|
+
} else {
|
|
723
|
+
tape[ptr] = 0; // EOF → 0
|
|
724
|
+
}
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
case '[': if (tape[ptr] === 0) ip = brackets[ip]; break;
|
|
728
|
+
case ']': if (tape[ptr] !== 0) ip = brackets[ip]; break;
|
|
729
|
+
}
|
|
730
|
+
ip++;
|
|
731
|
+
}
|
|
732
|
+
return output;
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
return obj;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
module.exports = { Executor, NvmlDocument, NvmlElement, NvmlComponent, NvmlLangDef, ExecError, makeBfObject };
|