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.
Files changed (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /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 parsed AST and builds a live Document object.
7
- * Handles:
8
- * - @config document.config map
9
- * - @visual document.visual element tree
10
- * - @ss document.globalStyles string
11
- * - {script} server-side Nova execution OR client JS collection
12
- * - {element.*} element tree nodes
13
- * - [..]::prop property on nearest parent element
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 = tag; // 'h1', 'div', 'button', 'script', …
25
- this.id = id; // unique auto-id for DOM targeting
26
- this.textValue = null; // {h1}='text' shorthand
27
- this.props = {}; // prop='value' pairs
28
- this.ss = null; // inline style string ([..]::ss)
29
- this.code = null; // script code ([..]::code)
30
- this.children = []; // child NvmlElement[]
31
- this.extraProps = {}; // any other [..]::X values
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 = {}; // @config key→value
38
- this.visual = []; // top-level NvmlElement[]
39
- this.globalStyles = ''; // @ss content
40
- this._idCounter = 0;
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
- // Called by executor to set a config value at runtime
46
- setConfig(key, value) { this.config[key] = value; }
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
- // Expose title shorthand
49
- get title() { return this.config.title || ''; }
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': return this._execConfig(node);
79
- case 'at_block': return this._execAtBlock(node, parentElement);
80
- case 'element': return this._execElement(node, parentElement);
81
- case 'text_node': return this._execTextNode(node, parentElement);
82
- case 'parent_prop': return this._execParentProp(node, parentElement);
83
- case 'css_decl': return; // handled inline in sub-body processing
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
- // ── @visual / @ss / custom at-blocks ─────────────────────
191
+ // ── @state ───────────────────────────────────────────────────────
98
192
 
99
- _execAtBlock(node, parentElement) {
100
- if (node.name === 'ss') {
101
- // @ss [ ... ] collect raw CSS-like content from nested text or sub-blocks
102
- // We allow any nested blocks and stringify them as CSS
103
- const cssLines = [];
104
- for (const child of node.body) {
105
- cssLines.push(this._nodeToCSS(child));
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
- if (parentElement) {
108
- parentElement.ss = (parentElement.ss || '') + cssLines.join('\n');
109
- } else {
110
- this.doc.globalStyles += cssLines.join('\n');
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
- // Unknown @block treat as a named section stored in doc.config
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._execElementToModel(child, null);
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._execElementToModel(node, parentElement);
313
+ const el = this._buildElement(node, parentElement);
135
314
  if (!el) return;
136
-
137
- if (parentElement) {
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
- _execElementToModel(node, parentElement) {
319
+ _buildElement(node, parentElement) {
320
+ if (!node) return null;
321
+
145
322
  if (node.kind === 'text_node') {
146
- // bare string inside a parent
147
- if (parentElement) {
148
- parentElement.textValue = (parentElement.textValue || '') + node.value;
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
- // Resolve tag: 'element.button' → 'button', 'h1' → 'h1'
155
- const rawTag = node.tag;
156
- let tag = rawTag;
157
- if (rawTag.startsWith('element.')) {
158
- tag = rawTag.slice('element.'.split('.').length > 1 ? 'element.'.length : 0) || rawTag;
159
- } else if (rawTag.includes('.')) {
160
- // namespaced: element.NAME — already handled above but catch remainder
161
- tag = rawTag.split('.').pop();
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
- // inline text
169
- if (node.textValue !== null) {
170
- el.textValue = this._resolveValue(node.textValue);
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
- const cssLines = valNode.__subBody.map(c => this._nodeToCSS(c)).filter(Boolean);
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
- // sub-body code block: collect text_node and css_decl values
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') { el.ss = resolved; }
191
- else if (key === 'code') { el.code = resolved; }
192
- else { el.props[key] = resolved; }
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: child elements and @blocks
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
- // if element is a script, handle server vs client
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
- // ── [..] property applied to nearest parent ───────────────
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, parentElement) {
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
- const cssLines = node.subBody.map(c => this._nodeToCSS(c));
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
- const codeLines = node.subBody.map(c => this._nodeToRaw(c));
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
- parentElement.extraProps[prop] = node.subBody;
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 (prop === 'ss') parentElement.ss = resolved;
232
- else if (prop === 'code') parentElement.code = resolved;
233
- else parentElement.extraProps[prop] = resolved;
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; // top-level [..] — ignore
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 = lang;
257
- el._scriptScope = scope;
258
- el._scriptCode = code;
259
- el._ranOnServer = false;
501
+ el._scriptLang = lang;
502
+ el._scriptScope = scope;
503
+ el._scriptCode = code;
504
+ el._ranOnServer = false;
260
505
 
261
- // Server-side scripts with no trigger must run NOW, during execution,
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
- this.novaRunner(code, this.doc);
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 node serializer (crude but functional) ────────────
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 nodeProps = node.props || {};
295
- const nodeBody = node.body || [];
296
- const nodeChildren = node.children || [];
297
- const textVal = node.textValue;
298
-
299
- // {color}='#aaa' ← CSS property shorthand: textValue is the CSS value
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(nodeProps)
313
- .map(([k, v]) => ` ${k}: ${this._resolveValue(v)};`)
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
- _nodeToRaw(node) {
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') return valNode.value;
332
- if (valNode.kind === 'pipe_list') return valNode.items;
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
- module.exports = { Executor, NvmlDocument, NvmlElement, ExecError };
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 };