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,31 +1,41 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * NVML Parser
4
+ * NVML Parser v2
5
5
  *
6
- * Grammar (simplified):
6
+ * Grammar:
7
7
  *
8
- * program → block*
8
+ * program → topNode*
9
9
  *
10
- * block → at_block | element_node | element_shorthand
10
+ * topNode → at_block | element_node
11
11
  *
12
- * at_block → '@' IDENT '[' block* ']'
13
- * | '@' IDENT '[' kv_list ']' (for @config)
12
+ * at_block → '@config' '[' kv_list ']'
13
+ * | '@visual' '[' block* ']'
14
+ * | '@ss' '[' block* ']'
15
+ * | '@component' IDENT '[' block* ']'
16
+ * | '@state' '[' kv_list ']'
17
+ * | '@computed' '[' named_fn_list ']'
18
+ * | '@effect' '[' effect_list ']'
19
+ * | '@route' STRING '[' block* ']'
20
+ * | '@slot' IDENT? '[' block* ']'
21
+ * | '@use' IDENT (',' IDENT)*
14
22
  *
15
- * element_node → ELEMENT ( '=' value )? // {h1}='text'
16
- * ( '[' element_body ']' )?
23
+ * element_node → ELEMENT ('=' value)? ('[' element_body ']')?
17
24
  *
18
- * element_body → ( property | block )*
25
+ * element_body → (property | child | parent_ref)*
19
26
  *
20
- * property → IDENT '=' value // prop='val'
21
- * | IDENT // boolean flag
22
- * | '[..]' '::' IDENT ( '=' value )? // parent ref property
23
- * | '[..]' '::' IDENT '[' block* ']' // parent ref sub-block
27
+ * property → IDENT '=' value
28
+ * | IDENT (boolean)
29
+ * | IDENT '->' IDENT (one-way binding: prop -> signal)
30
+ * | IDENT '<->' IDENT (two-way binding: prop <-> signal)
31
+ * | IDENT '~' IDENT (transition: prop ~ transitionName)
32
+ * | '[..]' '::' IDENT ('=' value | '[' sub_body ']' | ε)
24
33
  *
25
- * kv_list kv (',' kv)*
26
- * kv → IDENT '=' value
27
- * value STRING | NUMBER | IDENT | pipe_list
28
- * pipe_list → '|' IDENT* '|'
34
+ * child element_node | at_block | text_node | conditional_child
35
+ *
36
+ * conditional_child '?' IDENT element_node (render element only if signal is truthy)
37
+ *
38
+ * value → STRING | NUMBER | IDENT | '#' IDENT | pipe_list
29
39
  */
30
40
 
31
41
  const { Lexer, TT } = require('./lexer');
@@ -38,17 +48,32 @@ class ParseError extends Error {
38
48
  }
39
49
  }
40
50
 
41
- // ── AST node factories ────────────────────────────────────
42
-
43
51
  const N = {
44
- program: (body) => ({ kind: 'program', body }),
45
- atBlock: (name, body) => ({ kind: 'at_block', name, body }),
46
- atConfig: (pairs) => ({ kind: 'at_config', pairs }),
47
- element: (tag, textValue, props, body) => ({ kind: 'element', tag, textValue, props, body }),
48
- property: (key, value) => ({ kind: 'property', key, value }),
49
- parentProp: (prop, value, subBody) => ({ kind: 'parent_prop', prop, value, subBody }),
50
- value: (v) => ({ kind: 'value', value: v }),
51
- pipeList: (items) => ({ kind: 'pipe_list', items }),
52
+ program: (body) => ({ kind: 'program', body }),
53
+ atConfig: (pairs) => ({ kind: 'at_config', pairs }),
54
+ atBlock: (name, arg, body) => ({ kind: 'at_block', name, arg, body }),
55
+ atState: (pairs) => ({ kind: 'at_state', pairs }),
56
+ atComputed: (entries) => ({ kind: 'at_computed', entries }),
57
+ atEffect: (entries) => ({ kind: 'at_effect', entries }),
58
+ atLang: (name, pairs) => ({ kind: 'at_lang', name, pairs }),
59
+ atRoute: (path, body) => ({ kind: 'at_route', path, body }),
60
+ atSlot: (name, body) => ({ kind: 'at_slot', name, body }),
61
+ atUse: (names) => ({ kind: 'at_use', names }),
62
+ element: (tag, textValue, props, body,
63
+ bindings, transitions, cond) => ({ kind: 'element', tag, textValue, props, body, bindings, transitions, cond }),
64
+ property: (key, value) => ({ kind: 'property', key, value }),
65
+ binding: (prop, signal, twoWay) => ({ kind: 'binding', prop, signal, twoWay }),
66
+ transition: (prop, name) => ({ kind: 'transition', prop, name }),
67
+ parentProp: (prop, value, subBody) => ({ kind: 'parent_prop', prop, value, subBody }),
68
+ signalRef: (name) => ({ kind: 'signal_ref', name }),
69
+ value: (v) => ({ kind: 'value', value: v }),
70
+ pipeList: (items) => ({ kind: 'pipe_list', items }),
71
+ textNode: (value) => ({ kind: 'text_node', value }),
72
+ conditional: (signal, child) => ({ kind: 'conditional', signal, child }),
73
+ cssDecl: (key, value) => ({ kind: 'css_decl', key, value }),
74
+ eachBlock: (signal, itemVar, indexVar, body)=> ({ kind: 'each_block', signal, itemVar, indexVar, body }),
75
+ slotOutlet: (name) => ({ kind: 'slot_outlet', name }),
76
+ componentUse: (name, props, slots) => ({ kind: 'component_use', name, props, slots }),
52
77
  };
53
78
 
54
79
  class Parser {
@@ -57,16 +82,11 @@ class Parser {
57
82
  this.current = 0;
58
83
  }
59
84
 
60
- // ── token helpers ─────────────────────────────────────────
61
-
62
85
  peek() { return this.tokens[this.current]; }
63
86
  previous() { return this.tokens[this.current - 1]; }
64
87
  isAtEnd() { return this.peek().type === TT.EOF; }
65
88
 
66
- advance() {
67
- if (!this.isAtEnd()) this.current++;
68
- return this.previous();
69
- }
89
+ advance() { if (!this.isAtEnd()) this.current++; return this.previous(); }
70
90
 
71
91
  check(type, value = null) {
72
92
  if (this.isAtEnd()) return false;
@@ -75,7 +95,6 @@ class Parser {
75
95
  }
76
96
 
77
97
  match(...specs) {
78
- // specs: [type] or [type, value]
79
98
  for (const spec of specs) {
80
99
  const [type, value = null] = Array.isArray(spec) ? spec : [spec];
81
100
  if (this.check(type, value)) { this.advance(); return true; }
@@ -84,135 +103,303 @@ class Parser {
84
103
  }
85
104
 
86
105
  consume(type, value, msg) {
87
- if (typeof value === 'string' && value && this.check(type, value)) return this.advance();
88
- if (!value && this.check(type)) return this.advance();
106
+ if (value !== null && value !== undefined && this.check(type, value)) return this.advance();
107
+ if ((value === null || value === undefined) && this.check(type)) return this.advance();
89
108
  throw new ParseError(msg || `Expected ${type}${value ? ' ' + value : ''}`, this.peek());
90
109
  }
91
110
 
92
111
  error(msg) { throw new ParseError(msg, this.peek()); }
93
112
 
94
- // ── entry point ───────────────────────────────────────────
113
+ // ── entry ──────────────────────────────────────────────────────
95
114
 
96
115
  parse() {
97
116
  const body = [];
98
- while (!this.isAtEnd()) body.push(this.parseBlock());
117
+ while (!this.isAtEnd()) body.push(this.parseTopNode());
99
118
  return N.program(body);
100
119
  }
101
120
 
102
- // ── blocks ────────────────────────────────────────────────
103
-
104
- parseBlock() {
121
+ parseTopNode() {
105
122
  const t = this.peek();
106
-
107
- if (t.type === TT.AT_IDENT) return this.parseAtBlock();
108
- if (t.type === TT.ELEMENT) return this.parseElement();
109
- if (t.type === TT.DOTDOT) return this.parseParentPropStandalone();
110
-
111
- this.error(`Unexpected token '${t.value}' (${t.type})`);
123
+ if (t.type === TT.AT_IDENT) return this.parseAtBlock();
124
+ if (t.type === TT.ELEMENT) return this.parseElement();
125
+ this.error(`Unexpected token '${t.value}' (${t.type}) at top level`);
112
126
  }
113
127
 
114
- // ── @block ────────────────────────────────────────────────
128
+ // ── @blocks ────────────────────────────────────────────────────
115
129
 
116
130
  parseAtBlock() {
117
131
  const name = this.advance().value; // consume AT_IDENT
118
132
 
119
- this.consume(TT.LBRACKET, null, `Expected '[' after '@${name}'`);
133
+ switch (name) {
134
+ case 'config':
135
+ this.consume(TT.LBRACKET, null, "Expected '[' after @config");
136
+ const configPairs = this.parseKVList();
137
+ this.consume(TT.RBRACKET, null, "Expected ']' to close @config");
138
+ return N.atConfig(configPairs);
139
+
140
+ case 'state':
141
+ this.consume(TT.LBRACKET, null, "Expected '[' after @state");
142
+ const statePairs = this.parseKVList();
143
+ this.consume(TT.RBRACKET, null, "Expected ']' to close @state");
144
+ return N.atState(statePairs);
145
+
146
+ case 'computed':
147
+ this.consume(TT.LBRACKET, null, "Expected '[' after @computed");
148
+ const computedEntries = this.parseNamedCodeList();
149
+ this.consume(TT.RBRACKET, null, "Expected ']' to close @computed");
150
+ return N.atComputed(computedEntries);
151
+
152
+ case 'effect':
153
+ this.consume(TT.LBRACKET, null, "Expected '[' after @effect");
154
+ const effectEntries = this.parseEffectList();
155
+ this.consume(TT.RBRACKET, null, "Expected ']' to close @effect");
156
+ return N.atEffect(effectEntries);
157
+
158
+ case 'component': {
159
+ const compName = this.consume(TT.IDENT, null, "Expected component name after @component").value;
160
+ this.consume(TT.LBRACKET, null, "Expected '[' after @component name");
161
+ const compBody = this.parseBlockList();
162
+ this.consume(TT.RBRACKET, null, "Expected ']' to close @component");
163
+ return N.atComponent(compName, compBody);
164
+ }
165
+
166
+ case 'lang': {
167
+ // @lang {langName} [ runtime_language="nv", config="", src="PATH", code=(...) ]
168
+ const langName = this.consume(TT.IDENT, null, 'Expected language name after @lang').value;
169
+ this.consume(TT.LBRACKET, null, "Expected '[' after @lang name");
170
+ const langPairs = this._parseLangKVList();
171
+ this.consume(TT.RBRACKET, null, "Expected ']' to close @lang");
172
+ return N.atLang(langName, langPairs);
173
+ }
174
+
175
+ case 'route': {
176
+ const routePath = this.parseValue(); // STRING or IDENT
177
+ this.consume(TT.LBRACKET, null, "Expected '[' after @route path");
178
+ const routeBody = this.parseBlockList();
179
+ this.consume(TT.RBRACKET, null, "Expected ']' to close @route");
180
+ return N.atRoute(routePath, routeBody);
181
+ }
182
+
183
+ case 'slot': {
184
+ let slotName = 'default';
185
+ if (this.check(TT.IDENT)) slotName = this.advance().value;
186
+ this.consume(TT.LBRACKET, null, "Expected '[' after @slot");
187
+ const slotBody = this.parseBlockList();
188
+ this.consume(TT.RBRACKET, null, "Expected ']' to close @slot");
189
+ return N.atSlot(slotName, slotBody);
190
+ }
191
+
192
+ case 'use': {
193
+ const useNames = [];
194
+ useNames.push(this.consume(TT.IDENT, null, "Expected component name after @use").value);
195
+ while (this.match(TT.COMMA)) {
196
+ useNames.push(this.consume(TT.IDENT, null, "Expected component name").value);
197
+ }
198
+ return N.atUse(useNames);
199
+ }
120
200
 
121
- // @config is special: key=value list
122
- if (name === 'config') {
123
- const pairs = this.parseKVList();
124
- this.consume(TT.RBRACKET, null, "Expected ']' to close @config");
125
- return N.atConfig(pairs);
201
+ default: {
202
+ // @visual, @ss, or unknown custom at-block
203
+ this.consume(TT.LBRACKET, null, `Expected '[' after @${name}`);
204
+ const body = this.parseBlockList();
205
+ this.consume(TT.RBRACKET, null, `Expected ']' to close @${name}`);
206
+ return N.atBlock(name, null, body);
207
+ }
126
208
  }
209
+ }
127
210
 
128
- // @ss or @visual or custom: recursive body
211
+ parseBlockList() {
129
212
  const body = [];
130
213
  while (!this.isAtEnd() && !this.check(TT.RBRACKET)) {
131
- body.push(this.parseBlock());
214
+ if (this.check(TT.AT_IDENT)) { body.push(this.parseAtBlock()); continue; }
215
+ if (this.check(TT.ELEMENT)) { body.push(this.parseElement()); continue; }
216
+ if (this.check(TT.DOTDOT)) { body.push(this.parseParentPropInline()); continue; }
217
+ if (this.check(TT.STRING)) { body.push(N.textNode(this.advance().value)); continue; }
218
+ if (this.check(TT.COMMA)) { this.advance(); continue; }
219
+ break;
132
220
  }
133
- this.consume(TT.RBRACKET, null, `Expected ']' to close @${name}`);
134
- return N.atBlock(name, body);
221
+ return body;
135
222
  }
136
223
 
137
- // ── kv list (used inside @config) ────────────────────────
224
+ // ── kv list (flat key=value pairs) ────────────────────────────
138
225
 
139
226
  parseKVList() {
140
227
  const pairs = {};
141
228
  while (!this.isAtEnd() && !this.check(TT.RBRACKET)) {
142
- // skip stray commas
143
229
  if (this.match(TT.COMMA)) continue;
144
-
145
- const key = this.consume(TT.IDENT, null, 'Expected key name in @config').value;
230
+ if (!this.check(TT.IDENT)) break;
231
+ const key = this.advance().value;
146
232
  this.consume(TT.EQ, null, `Expected '=' after key '${key}'`);
147
- const val = this.parseValue();
148
- pairs[key] = val;
233
+ pairs[key] = this.parseValue();
234
+ this.match(TT.COMMA);
235
+ }
236
+ return pairs;
237
+ }
149
238
 
150
- this.match(TT.COMMA); // optional trailing comma
239
+ // ── @lang kv list — like parseKVList but also accepts bare string values ──
240
+ // Handles: runtime_language="nv" config="" src="PATH" code=(...)
241
+ _parseLangKVList() {
242
+ const pairs = {};
243
+ while (!this.isAtEnd() && !this.check(TT.RBRACKET)) {
244
+ if (this.match(TT.COMMA)) continue;
245
+ if (!this.check(TT.IDENT)) break;
246
+ const key = this.advance().value;
247
+ if (this.match(TT.EQ)) {
248
+ pairs[key] = this.parseValue();
249
+ } else {
250
+ pairs[key] = N.value(true);
251
+ }
252
+ this.match(TT.COMMA);
151
253
  }
152
254
  return pairs;
153
255
  }
154
256
 
155
- // ── element ───────────────────────────────────────────────
257
+
258
+
259
+ parseNamedCodeList() {
260
+ const entries = [];
261
+ while (!this.isAtEnd() && !this.check(TT.RBRACKET)) {
262
+ if (this.match(TT.COMMA)) continue;
263
+ if (!this.check(TT.IDENT)) break;
264
+ const name = this.advance().value;
265
+ this.consume(TT.EQ, null, `Expected '=' after computed name '${name}'`);
266
+ const code = this.parseValue();
267
+ entries.push({ name, code });
268
+ this.match(TT.COMMA);
269
+ }
270
+ return entries;
271
+ }
272
+
273
+ // ── @effect entries: deps -> (nova code) ──────────────────────
274
+
275
+ parseEffectList() {
276
+ const entries = [];
277
+ while (!this.isAtEnd() && !this.check(TT.RBRACKET)) {
278
+ if (this.match(TT.COMMA)) continue;
279
+ // deps: [signalA, signalB] or * (always) — stored as array of names
280
+ const deps = [];
281
+ if (this.check(TT.LBRACKET)) {
282
+ this.advance();
283
+ while (!this.isAtEnd() && !this.check(TT.RBRACKET)) {
284
+ if (this.check(TT.IDENT)) deps.push(this.advance().value);
285
+ this.match(TT.COMMA);
286
+ }
287
+ this.consume(TT.RBRACKET, null, "Expected ']' to close effect deps");
288
+ } else if (this.check(TT.IDENT, '*')) {
289
+ this.advance(); // wildcard — run on any signal change
290
+ deps.push('*');
291
+ } else if (this.check(TT.IDENT)) {
292
+ deps.push(this.advance().value);
293
+ }
294
+ this.consume(TT.ARROW, null, "Expected '->' after effect deps");
295
+ const code = this.parseValue();
296
+ entries.push({ deps, code });
297
+ this.match(TT.COMMA);
298
+ }
299
+ return entries;
300
+ }
301
+
302
+ // ── element ────────────────────────────────────────────────────
156
303
 
157
304
  parseElement() {
158
- const tag = this.advance().value; // ELEMENT token value e.g. 'h1', 'element.button', 'script'
305
+ const tag = this.advance().value;
159
306
 
160
- // optional inline text: {h1}='Hello'
161
307
  let textValue = null;
162
- if (this.match(TT.EQ)) {
163
- textValue = this.parseValue();
164
- }
308
+ if (this.match(TT.EQ)) textValue = this.parseValue();
165
309
 
166
- // optional body block [...]
167
- let props = {};
168
- let body = [];
310
+ let props = {};
311
+ let body = [];
312
+ let bindings = [];
313
+ let transitions= [];
314
+ let cond = null;
169
315
 
170
316
  if (this.match(TT.LBRACKET)) {
171
- const { props: p, body: b } = this.parseElementBody();
172
- props = p;
173
- body = b;
317
+ const parsed = this.parseElementBody();
318
+ props = parsed.props;
319
+ body = parsed.body;
320
+ bindings = parsed.bindings;
321
+ transitions = parsed.transitions;
174
322
  this.consume(TT.RBRACKET, null, `Expected ']' to close element {${tag}}`);
175
323
  }
176
324
 
177
- return N.element(tag, textValue, props, body);
325
+ return N.element(tag, textValue, props, body, bindings, transitions, cond);
178
326
  }
179
327
 
180
- // ── element body: mix of properties and child elements ────
181
-
182
328
  parseElementBody() {
183
- const props = {};
184
- const body = [];
329
+ const props = {}, body = [], bindings = [], transitions = [];
185
330
 
186
331
  while (!this.isAtEnd() && !this.check(TT.RBRACKET)) {
187
332
  const t = this.peek();
188
333
 
189
- // [..] parent-ref property: [..]::ss [ ... ] or [..]::code=value
334
+ // [..] parent-ref
190
335
  if (t.type === TT.DOTDOT) {
191
336
  const pp = this.parseParentPropInline();
192
- // merge into props sub-bodies stored as special prop
193
- if (pp.subBody !== null) {
194
- props[pp.prop] = { __subBody: pp.subBody };
195
- } else {
196
- props[pp.prop] = pp.value;
197
- }
337
+ if (pp.subBody !== null) props[pp.prop] = { __subBody: pp.subBody };
338
+ else props[pp.prop] = pp.value;
198
339
  continue;
199
340
  }
200
341
 
201
- // child element
202
- if (t.type === TT.ELEMENT) {
203
- body.push(this.parseElement());
342
+ // conditional child: ? signalName {element}
343
+ if (t.type === TT.QUESTION) {
344
+ this.advance();
345
+ const sigName = this.consume(TT.IDENT, null, "Expected signal name after '?'").value;
346
+ const child = this.parseElement();
347
+ child.cond = sigName;
348
+ body.push(child);
204
349
  continue;
205
350
  }
206
351
 
207
- // @block child
208
- if (t.type === TT.AT_IDENT) {
209
- body.push(this.parseAtBlock());
352
+ // each block: @each signalName as itemVar (index: indexVar) [ body ]
353
+ if (t.type === TT.AT_IDENT && t.value === 'each') {
354
+ body.push(this.parseEachBlock());
210
355
  continue;
211
356
  }
212
357
 
213
- // property: IDENT = value or IDENT (boolean flag)
358
+ // slot outlet: @slot name
359
+ if (t.type === TT.AT_IDENT && t.value === 'slot') {
360
+ this.advance();
361
+ const slotName = this.check(TT.IDENT) ? this.advance().value : 'default';
362
+ body.push(N.slotOutlet(slotName));
363
+ continue;
364
+ }
365
+
366
+ // child element
367
+ if (t.type === TT.ELEMENT) { body.push(this.parseElement()); continue; }
368
+
369
+ // child @block
370
+ if (t.type === TT.AT_IDENT) { body.push(this.parseAtBlock()); continue; }
371
+
372
+ // bare text node
373
+ if (t.type === TT.STRING) { body.push(N.textNode(this.advance().value)); continue; }
374
+
375
+ // property
214
376
  if (t.type === TT.IDENT) {
215
377
  const key = this.advance().value;
378
+
379
+ // one-way binding: prop -> signalName
380
+ if (this.match(TT.ARROW)) {
381
+ const sig = this.consume(TT.IDENT, null, "Expected signal name after '->'").value;
382
+ bindings.push(N.binding(key, sig, false));
383
+ this.match(TT.COMMA);
384
+ continue;
385
+ }
386
+
387
+ // two-way binding: prop <-> signalName
388
+ if (this.match(TT.DARROW)) {
389
+ const sig = this.consume(TT.IDENT, null, "Expected signal name after '<->'").value;
390
+ bindings.push(N.binding(key, sig, true));
391
+ this.match(TT.COMMA);
392
+ continue;
393
+ }
394
+
395
+ // transition hint: prop ~ transitionName
396
+ if (this.match(TT.TILDE)) {
397
+ const tname = this.consume(TT.IDENT, null, "Expected transition name after '~'").value;
398
+ transitions.push(N.transition(key, tname));
399
+ this.match(TT.COMMA);
400
+ continue;
401
+ }
402
+
216
403
  if (this.match(TT.EQ)) {
217
404
  props[key] = this.parseValue();
218
405
  } else {
@@ -222,113 +409,95 @@ class Parser {
222
409
  continue;
223
410
  }
224
411
 
225
- // NUMBER as a value without key (e.g. inside @ss numeric values) — skip gracefully
226
- if (t.type === TT.NUMBER) { this.advance(); this.match(TT.COMMA); continue; }
227
-
228
- // bare string child (text node)
229
- if (t.type === TT.STRING) {
230
- body.push({ kind: 'text_node', value: this.advance().value });
412
+ // signal ref as prop value: #signal (shorthand for text binding)
413
+ if (t.type === TT.HASH) {
414
+ this.advance();
415
+ body.push(N.signalRef(t.value));
231
416
  continue;
232
417
  }
233
418
 
234
- // stray comma
235
- if (t.type === TT.COMMA) { this.advance(); continue; }
419
+ if (t.type === TT.NUMBER) { this.advance(); this.match(TT.COMMA); continue; }
420
+ if (t.type === TT.COMMA) { this.advance(); continue; }
236
421
 
237
422
  this.error(`Unexpected token '${t.value}' (${t.type}) inside element body`);
238
423
  }
239
424
 
240
- return { props, body };
425
+ return { props, body, bindings, transitions };
426
+ }
427
+
428
+ // ── @each signalName as item (index: i) [ body ] ──────────────
429
+
430
+ parseEachBlock() {
431
+ this.advance(); // consume 'each' AT_IDENT
432
+ const signal = this.consume(TT.IDENT, null, "Expected signal name after @each").value;
433
+ // 'as' keyword
434
+ if (this.check(TT.IDENT, 'as')) this.advance();
435
+ const itemVar = this.consume(TT.IDENT, null, "Expected item variable after 'as'").value;
436
+ let indexVar = null;
437
+ // optional (index: i)
438
+ if (this.check(TT.STRING) || (this.check(TT.IDENT, 'index'))) {
439
+ this.advance(); // skip
440
+ }
441
+ // (index: varName)
442
+ if (this.check(TT.LBRACKET) || this.check(TT.LPAREN)) {
443
+ // simple: check for index keyword in next ident
444
+ }
445
+ this.consume(TT.LBRACKET, null, "Expected '[' after @each header");
446
+ const body = this.parseBlockList();
447
+ this.consume(TT.RBRACKET, null, "Expected ']' to close @each block");
448
+ return N.eachBlock(signal, itemVar, indexVar, body);
241
449
  }
242
450
 
243
- // ── [..] property inside an element body ─────────────────
451
+ // ── [..] property ──────────────────────────────────────────────
244
452
 
245
453
  parseParentPropInline() {
246
454
  this.advance(); // consume DOTDOT
247
455
  this.consume(TT.COLONCOLON, null, "Expected '::' after '[..]'");
248
456
  const prop = this.consume(TT.IDENT, null, "Expected property name after '::'").value;
249
457
 
250
- // [..]::ss [ body ] — sub-block
251
458
  if (this.match(TT.LBRACKET)) {
252
459
  const subBody = [];
253
460
  while (!this.isAtEnd() && !this.check(TT.RBRACKET)) {
254
- // inside ::ss sub-blocks, content can be elements OR bare kv pairs
255
461
  if (this.check(TT.ELEMENT) || this.check(TT.AT_IDENT) || this.check(TT.DOTDOT)) {
256
- subBody.push(this.parseBlock());
462
+ subBody.push(this.parseTopNode());
257
463
  } else if (this.check(TT.IDENT)) {
258
- // bare kv: color='red', (CSS-style declarations)
259
464
  const key = this.advance().value;
260
465
  if (this.match(TT.EQ)) {
261
- const val = this.parseValue();
262
- subBody.push({ kind: 'css_decl', key, value: val });
466
+ subBody.push(N.cssDecl(key, this.parseValue()));
263
467
  } else {
264
- subBody.push({ kind: 'css_decl', key, value: N.value(true) });
468
+ subBody.push(N.cssDecl(key, N.value(true)));
265
469
  }
266
470
  this.match(TT.COMMA);
267
471
  } else if (this.check(TT.COMMA)) {
268
472
  this.advance();
269
- } else {
270
- break;
271
- }
473
+ } else { break; }
272
474
  }
273
475
  this.consume(TT.RBRACKET, null, `Expected ']' to close [..]::${prop} block`);
274
476
  return N.parentProp(prop, null, subBody);
275
477
  }
276
478
 
277
- // [..]::prop = value
278
- if (this.match(TT.EQ)) {
279
- const val = this.parseValue();
280
- return N.parentProp(prop, val, null);
281
- }
282
-
283
- // boolean flag
479
+ if (this.match(TT.EQ)) return N.parentProp(prop, this.parseValue(), null);
284
480
  return N.parentProp(prop, N.value(true), null);
285
481
  }
286
482
 
287
- // Standalone [..] at top-level block (shouldn't normally appear, treat gracefully)
288
- parseParentPropStandalone() {
289
- return this.parseParentPropInline();
290
- }
291
-
292
- // ── value ─────────────────────────────────────────────────
483
+ // ── value ──────────────────────────────────────────────────────
293
484
 
294
485
  parseValue() {
295
486
  const t = this.peek();
296
-
297
- // pipe list: |item1, item2|
298
- if (t.type === TT.PIPE) {
299
- return this.parsePipeList();
300
- }
301
-
302
- if (t.type === TT.STRING) {
303
- this.advance();
304
- return N.value(t.value);
305
- }
306
-
307
- if (t.type === TT.NUMBER) {
308
- this.advance();
309
- return N.value(t.value);
310
- }
311
-
312
- if (t.type === TT.IDENT) {
313
- this.advance();
314
- // could be bare word like 'novac', 'server', 'client'
315
- return N.value(t.value);
316
- }
317
-
318
- // multiline string was already converted to STRING by lexer (paren-string)
319
- this.error(`Expected a value (string, number, or identifier), got '${t.value}' (${t.type})`);
487
+ if (t.type === TT.PIPE) return this.parsePipeList();
488
+ if (t.type === TT.HASH) { this.advance(); return N.signalRef(t.value); }
489
+ if (t.type === TT.STRING) { this.advance(); return N.value(t.value); }
490
+ if (t.type === TT.NUMBER) { this.advance(); return N.value(t.value); }
491
+ if (t.type === TT.IDENT) { this.advance(); return N.value(t.value); }
492
+ this.error(`Expected a value, got '${t.value}' (${t.type})`);
320
493
  }
321
494
 
322
- // ── pipe list |item1, item2, item3| ─────────────────────
323
-
324
495
  parsePipeList() {
325
496
  this.consume(TT.PIPE, null, "Expected '|'");
326
497
  const items = [];
327
498
  while (!this.isAtEnd() && !this.check(TT.PIPE)) {
328
499
  const t = this.peek();
329
- if (t.type === TT.IDENT || t.type === TT.STRING) {
330
- items.push(this.advance().value);
331
- }
500
+ if (t.type === TT.IDENT || t.type === TT.STRING) items.push(this.advance().value);
332
501
  this.match(TT.COMMA);
333
502
  }
334
503
  this.consume(TT.PIPE, null, "Expected closing '|'");
@@ -336,4 +505,4 @@ class Parser {
336
505
  }
337
506
  }
338
507
 
339
- module.exports = { Parser, ParseError };
508
+ module.exports = { Parser, ParseError, N };