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,156 +1,145 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * NVML Renderer
4
+ * NVML Renderer v2
5
5
  *
6
- * Converts a live NvmlDocument into a full HTML string.
6
+ * Converts a NvmlDocument into a complete HTML string.
7
7
  *
8
- * Rules:
9
- * - @config keys map to <head> metadata
10
- * - @visual elements map to HTML elements
11
- * - [..]::ss inline <style> on the element (or class injection)
12
- * - {script scope='server'} already ran; emit nothing (or a comment)
13
- * - {script scope='client'} emit <script> tag with code
14
- * - {script language='novac'} Nova code compiled to JS via Emitter, then embedded
8
+ * New in v2:
9
+ * - Reactive signal system: @state window.__nvml.signals, auto-updates DOM
10
+ * - Computed signals: @computed derived values that update when deps change
11
+ * - Effects: @effect run Nova code server-side or JS client-side on signal change
12
+ * - One-way bindings (->): element prop mirrors signal value
13
+ * - Two-way bindings (<->): element input/value synced both directions with signal
14
+ * - Conditional rendering (?): element hidden/shown based on signal truthiness (reactive)
15
+ * - @each blocks: reactive list rendering with keyed diffing
16
+ * - CSS transitions (~): data-nvml-transition attr + auto-generated transition CSS
17
+ * - @component: inline component definitions compiled to reusable template functions
18
+ * - @route: client-side router with history API
19
+ * - @slot: named slot content filling
20
+ * - Virtual DOM diffing: patch() function for efficient DOM updates
21
+ * - Server-sent events: /_nvml/sse endpoint for server-push signal updates
22
+ * - Scoped CSS: [..]::ss generates properly-scoped style blocks
23
+ * - Full document API mutations baked server-side
24
+ * - nodejs scripts: {script}[language='nodejs'] runs server-side in Node.js VM
25
+ * - @lang: user-defined language extensions (server Nova/Node.js, or client JS)
26
+ * - bf: Brainfuck runtime available in every language scope
15
27
  */
16
28
 
29
+ const { makeBfObject } = require('./executor');
30
+
17
31
  const VOID_ELEMENTS = new Set([
18
32
  'area','base','br','col','embed','hr','img','input',
19
33
  'link','meta','param','source','track','wbr',
20
34
  ]);
21
35
 
22
- // HTML attribute name safety (strip anything not alphanumeric/hyphen)
23
- function safeAttr(name) {
24
- return String(name).replace(/[^a-zA-Z0-9\-_:]/g, '');
25
- }
26
-
27
- // Escape HTML text content
28
- function escHtml(str) {
29
- return String(str)
30
- .replace(/&/g, '&amp;')
31
- .replace(/</g, '&lt;')
32
- .replace(/>/g, '&gt;')
33
- .replace(/"/g, '&quot;');
34
- }
36
+ function safeAttr(name) { return String(name).replace(/[^a-zA-Z0-9\-_:.]/g, ''); }
37
+ function escHtml(str) { return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
38
+ function esc(str) { return String(str).replace(/\\/g,'\\\\').replace(/`/g,'\\`').replace(/\$\{/g,'\\${'); }
39
+
40
+ const HTML_ATTRS = new Set([
41
+ 'id','class','style','href','src','alt','title','placeholder','type','value','name',
42
+ 'action','method','target','rel','for','checked','disabled','readonly','required',
43
+ 'autofocus','autocomplete','multiple','size','rows','cols','maxlength','minlength',
44
+ 'min','max','step','pattern','tabindex','accesskey','dir','draggable','hidden',
45
+ 'spellcheck','translate','contenteditable','width','height','loading','decoding',
46
+ 'crossorigin','integrity','referrerpolicy','charset','media','onload','onclick',
47
+ 'onchange','oninput','onsubmit','onkeydown','onkeyup','onkeypress','onmouseover',
48
+ 'onmouseout','onmouseenter','onmouseleave','onfocus','onblur','ondblclick',
49
+ 'onpointerdown','onpointerup','onpointermove','role',
50
+ 'aria-label','aria-hidden','aria-expanded','aria-controls','aria-describedby',
51
+ 'aria-labelledby','aria-live','aria-atomic','aria-relevant','aria-busy',
52
+ 'aria-checked','aria-selected','aria-pressed','aria-disabled','aria-invalid',
53
+ 'aria-required','aria-multiline','aria-multiselectable','aria-orientation',
54
+ 'aria-valuemin','aria-valuemax','aria-valuenow','aria-valuetext','aria-setsize',
55
+ 'aria-posinset','aria-level','aria-readonly','aria-autocomplete','aria-haspopup',
56
+ 'aria-modal','aria-sort','aria-colcount','aria-colindex','aria-rowcount','aria-rowindex',
57
+ 'data','form','formaction','formmethod','formnovalidate','formtarget','enctype',
58
+ 'accept','accept-charset','list','inputmode','enterkeyhint','is','part','slot',
59
+ 'exportparts','inert','popover','popovertarget','popovertargetaction',
60
+ ]);
35
61
 
36
62
  class Renderer {
37
- /**
38
- * @param {object} options
39
- * @param {function} options.novaEmitter - (novaSource) => jsString
40
- * Used to compile novac script to JS.
41
- */
42
63
  constructor(options = {}) {
43
64
  this.novaEmitter = options.novaEmitter || null;
44
65
  }
45
66
 
46
67
  render(doc) {
47
68
  const config = doc.config || {};
48
-
49
- // ── <head> ──────────────────────────────────────────────
50
- const headParts = [];
51
-
52
- const charset = config.charset || 'UTF-8';
53
- headParts.push(` <meta charset="${escHtml(charset)}">`);
54
-
55
- // viewport
56
- const viewport = config.viewport || 'width=device-width, initial-scale=1.0';
57
- headParts.push(` <meta name="viewport" content="${escHtml(viewport)}">`);
58
-
59
- // title
60
- if (config.title) {
61
- headParts.push(` <title>${escHtml(config.title)}</title>`);
62
- }
63
-
64
- // description
65
- if (config.description) {
66
- headParts.push(` <meta name="description" content="${escHtml(config.description)}">`);
67
- }
68
-
69
- // author
70
- if (config.author) {
71
- headParts.push(` <meta name="author" content="${escHtml(config.author)}">`);
72
- }
73
-
74
- // keywords
69
+ const hasState = Object.keys(doc.state || {}).length > 0;
70
+ const hasComputed = (doc.computed || []).length > 0;
71
+ const hasEffects = (doc.effects || []).length > 0;
72
+ const hasRoutes = (doc.routes || []).length > 0;
73
+
74
+ // ── <head> ──────────────────────────────────────────────────────
75
+ const head = [];
76
+
77
+ head.push(` <meta charset="${escHtml(config.charset || 'UTF-8')}">`);
78
+ head.push(` <meta name="viewport" content="${escHtml(config.viewport || 'width=device-width, initial-scale=1.0')}">`);
79
+ if (config.title) head.push(` <title>${escHtml(config.title)}</title>`);
80
+ if (config.description) head.push(` <meta name="description" content="${escHtml(config.description)}">`);
81
+ if (config.author) head.push(` <meta name="author" content="${escHtml(config.author)}">`);
75
82
  if (config.keywords) {
76
83
  const kw = Array.isArray(config.keywords) ? config.keywords.join(', ') : config.keywords;
77
- headParts.push(` <meta name="keywords" content="${escHtml(kw)}">`);
84
+ head.push(` <meta name="keywords" content="${escHtml(kw)}">`);
78
85
  }
86
+ if (config['theme-color']) head.push(` <meta name="theme-color" content="${escHtml(config['theme-color'])}">`);
87
+ if (config.robots) head.push(` <meta name="robots" content="${escHtml(config.robots)}">`);
88
+ if (config.canonical) head.push(` <link rel="canonical" href="${escHtml(config.canonical)}">`);
89
+ if (config.favicon) head.push(` <link rel="icon" href="${escHtml(config.favicon)}">`);
90
+ if (config.base) head.push(` <base href="${escHtml(config.base)}">`);
79
91
 
80
- // Open Graph
81
- for (const key of ['og:title', 'og:description', 'og:image', 'og:url', 'og:type']) {
82
- if (config[key]) {
83
- headParts.push(` <meta property="${escHtml(key)}" content="${escHtml(config[key])}">`);
84
- }
92
+ for (const k of ['og:title','og:description','og:image','og:url','og:type']) {
93
+ if (config[k]) head.push(` <meta property="${escHtml(k)}" content="${escHtml(config[k])}">`);
85
94
  }
86
-
87
- // Twitter card
88
- for (const key of ['twitter:card','twitter:title','twitter:description','twitter:image']) {
89
- if (config[key]) {
90
- headParts.push(` <meta name="${escHtml(key)}" content="${escHtml(config[key])}">`);
91
- }
95
+ for (const k of ['twitter:card','twitter:title','twitter:description','twitter:image']) {
96
+ if (config[k]) head.push(` <meta name="${escHtml(k)}" content="${escHtml(config[k])}">`);
92
97
  }
93
98
 
94
- // stylesheet links: config.stylesheet = 'styles.css' or array
95
- const stylesheets = config.stylesheet
96
- ? (Array.isArray(config.stylesheet) ? config.stylesheet : [config.stylesheet])
97
- : [];
98
- for (const href of stylesheets) {
99
- headParts.push(` <link rel="stylesheet" href="${escHtml(href)}">`);
100
- }
99
+ const stylesheets = config.stylesheet ? [].concat(config.stylesheet) : [];
100
+ for (const href of stylesheets) head.push(` <link rel="stylesheet" href="${escHtml(href)}">`);
101
101
 
102
- // script src links: config.scripts = ['app.js'] or single
103
- const scriptLinks = config.scripts
104
- ? (Array.isArray(config.scripts) ? config.scripts : [config.scripts])
105
- : [];
106
- for (const src of scriptLinks) {
107
- headParts.push(` <script src="${escHtml(src)}"></script>`);
108
- }
102
+ const scripts = config.scripts ? [].concat(config.scripts) : [];
103
+ for (const src of scripts) head.push(` <script src="${escHtml(src)}"></script>`);
109
104
 
110
- // favicon
111
- if (config.favicon) {
112
- headParts.push(` <link rel="icon" href="${escHtml(config.favicon)}">`);
113
- }
114
-
115
- // base href
116
- if (config.base) {
117
- headParts.push(` <base href="${escHtml(config.base)}">`);
118
- }
119
-
120
- // theme-color
121
- if (config['theme-color']) {
122
- headParts.push(` <meta name="theme-color" content="${escHtml(config['theme-color'])}">`);
105
+ // @ss global styles
106
+ if (doc.globalStyles && doc.globalStyles.trim()) {
107
+ head.push(` <style>\n${doc.globalStyles}\n </style>`);
123
108
  }
124
109
 
125
- // robots
126
- if (config.robots) {
127
- headParts.push(` <meta name="robots" content="${escHtml(config.robots)}">`);
128
- }
110
+ // Transition CSS (generated from ~ hints collected during element render)
111
+ this._transitionNames = new Set();
112
+ this._componentDefs = doc.components || {};
113
+ this._slots = doc.slots || {};
114
+ this._langDefs = doc.langs || {};
129
115
 
130
- // canonical
131
- if (config.canonical) {
132
- headParts.push(` <link rel="canonical" href="${escHtml(config.canonical)}">`);
133
- }
116
+ // Render body first to collect transition names and component templates
117
+ this._componentTemplates = {};
118
+ const bodyParts = [];
119
+ for (const el of doc.visual) bodyParts.push(this.renderElement(el, 1, doc));
134
120
 
135
- // Global stylesheet from @ss block
136
- if (doc.globalStyles && doc.globalStyles.trim()) {
137
- headParts.push(` <style>\n${doc.globalStyles}\n </style>`);
121
+ // Transition CSS
122
+ if (this._transitionNames.size > 0) {
123
+ const tCss = [...this._transitionNames].map(name => `
124
+ .nvml-enter-${name} { animation: nvml-enter-${name} var(--nvml-dur-${name}, 0.25s) ease both; }
125
+ .nvml-leave-${name} { animation: nvml-leave-${name} var(--nvml-dur-${name}, 0.25s) ease both; }
126
+ @keyframes nvml-enter-${name} { from { opacity:0; transform: translateY(8px); } to { opacity:1; transform: none; } }
127
+ @keyframes nvml-leave-${name} { from { opacity:1; transform: none; } to { opacity:0; transform: translateY(8px); } }`).join('');
128
+ head.push(` <style>${tCss}\n </style>`);
138
129
  }
139
130
 
140
- // custom head HTML
141
- if (config.head) {
142
- headParts.push(` ${config.head}`);
143
- }
131
+ if (config.head) head.push(` ${config.head}`);
144
132
 
145
- // ── <body> ──────────────────────────────────────────────
146
- const bodyParts = [];
133
+ // ── bf client runtime ────────────────────────────────────────────
134
+ head.push(this._renderBfRuntime());
147
135
 
148
- for (const el of doc.visual) {
149
- bodyParts.push(this.renderElement(el, 1));
136
+ // ── Reactive runtime ────────────────────────────────────────────
137
+ if (hasState || hasComputed || hasEffects || hasRoutes) {
138
+ head.push(this._renderRuntime(doc));
150
139
  }
151
140
 
152
- // ── assemble ────────────────────────────────────────────
153
- const lang = config.lang || 'en';
141
+ // ── <body> ──────────────────────────────────────────────────────
142
+ const lang = config.lang || 'en';
154
143
  const bodyClass = config.bodyClass ? ` class="${escHtml(config.bodyClass)}"` : '';
155
144
  const bodyId = config.bodyId ? ` id="${escHtml(config.bodyId)}"` : '';
156
145
  const bodyStyle = config.bodyStyle ? ` style="${escHtml(config.bodyStyle)}"` : '';
@@ -159,230 +148,751 @@ class Renderer {
159
148
  '<!DOCTYPE html>',
160
149
  `<html lang="${escHtml(lang)}">`,
161
150
  '<head>',
162
- headParts.join('\n'),
151
+ head.join('\n'),
163
152
  '</head>',
164
153
  `<body${bodyClass}${bodyId}${bodyStyle}>`,
165
154
  bodyParts.join('\n'),
155
+ hasRoutes ? this._renderRoutingScript(doc) : '',
166
156
  '</body>',
167
157
  '</html>',
168
158
  ].join('\n');
169
159
  }
170
160
 
171
- // ── element HTML ─────────────────────────────────────────
161
+ // ── Reactive runtime script ──────────────────────────────────────
162
+
163
+ _renderRuntime(doc) {
164
+ const stateJson = JSON.stringify(doc.state || {});
165
+ const computedJson = JSON.stringify((doc.computed || []).map(c => ({ name: c.name, initial: c.initialValue })));
166
+ const effectsJson = JSON.stringify((doc.effects || []).map(e => ({ deps: e.deps, code: e.code })));
167
+
168
+ return ` <script>
169
+ // ── NVML Reactive Runtime ─────────────────────────────────────────────
170
+ (function(){
171
+ 'use strict';
172
+
173
+ // ── Signal store ──────────────────────────────────────────────────────
174
+ const _state = ${stateJson};
175
+ const _computed = ${computedJson};
176
+ const _effects = ${effectsJson};
177
+ const _subs = {}; // signal → Set of subscriber functions
178
+ const _bindings = []; // { el, prop, signal, twoWay }
179
+ const _conds = []; // { el, signal, display }
180
+ const _eaches = []; // { container, signal, template, itemVar }
181
+
182
+ function _get(name) { return _state[name]; }
183
+
184
+ function _set(name, value, silent) {
185
+ if (_state[name] === value && typeof value !== 'object') return;
186
+ _state[name] = value;
187
+ if (!silent) _notify(name);
188
+ }
189
+
190
+ function _notify(name) {
191
+ (_subs[name] || []).forEach(fn => { try { fn(_state[name]); } catch(e) { console.error('[nvml signal]', name, e); } });
192
+ // recompute computed signals that depend on this one
193
+ _computed.forEach(c => {
194
+ if (c._deps && c._deps.includes(name)) {
195
+ // server computes these; client re-fetches via /_nvml/compute if needed
196
+ _notifyComputed(c.name);
197
+ }
198
+ });
199
+ // run effects that depend on this signal
200
+ _effects.forEach(e => {
201
+ if (e.deps.includes('*') || e.deps.includes(name)) _runEffect(e);
202
+ });
203
+ }
204
+
205
+ function _notifyComputed(name) {
206
+ (_subs[name] || []).forEach(fn => { try { fn(_state[name]); } catch(e) {} });
207
+ }
208
+
209
+ function _subscribe(name, fn) {
210
+ if (!_subs[name]) _subs[name] = new Set();
211
+ _subs[name].add(fn);
212
+ return () => _subs[name].delete(fn);
213
+ }
214
+
215
+ // ── Effects ────────────────────────────────────────────────────────────
216
+ function _runEffect(effect) {
217
+ if (!effect.code || !effect.code.trim()) return;
218
+ // Effects with Nova code run via /_nvml/run server round-trip
219
+ fetch('/_nvml/run', {
220
+ method: 'POST',
221
+ headers: { 'Content-Type': 'application/json' },
222
+ body: JSON.stringify({ code: effect.code, live: _liveSnapshot(), state: _state })
223
+ }).then(r => r.json()).then(({ mutations, error }) => {
224
+ if (error) { console.error('[nvml effect]', error); return; }
225
+ _applyMutations(mutations || []);
226
+ }).catch(e => console.error('[nvml effect fetch]', e));
227
+ }
228
+
229
+ // ── DOM patch / diff ────────────────────────────────────────────────────
230
+ function _applyMutations(mutations) {
231
+ for (const m of mutations) {
232
+ const el = m.id ? document.getElementById(m.id) : null;
233
+ switch (m.type) {
234
+ case 'setText': if (el) el.textContent = m.value; break;
235
+ case 'setHTML': if (el) _patchHTML(el, m.value); break;
236
+ case 'setProp': if (el) el.setAttribute(m.key, m.value); break;
237
+ case 'removeClass': if (el) el.classList.remove(m.value); break;
238
+ case 'addClass': if (el) el.classList.add(m.value); break;
239
+ case 'toggleClass': if (el) el.classList.toggle(m.value, m.force); break;
240
+ case 'setClass': if (el) el.className = m.value; break;
241
+ case 'hide': if (el) _transitionOut(el, m.transition); break;
242
+ case 'show': if (el) _transitionIn(el, m.value || 'block', m.transition); break;
243
+ case 'remove': if (el) { _transitionOut(el, m.transition, () => el.remove()); } break;
244
+ case 'insertBefore': if (el) { const n = _createEl(m.html); el.parentNode.insertBefore(n, el); } break;
245
+ case 'insertAfter': if (el) { const n = _createEl(m.html); el.parentNode.insertBefore(n, el.nextSibling); } break;
246
+ case 'appendChild': if (el) { el.appendChild(_createEl(m.html)); } break;
247
+ case 'setStyle': if (el) { el.style[m.key] = m.value; } break;
248
+ case 'setCSSVar': document.documentElement.style.setProperty(m.name, m.value); break;
249
+ case 'addStyle': { const s = document.createElement('style'); s.textContent = m.value; document.head.appendChild(s); } break;
250
+ case 'setAttr': if (el) el.setAttribute(m.key, m.value); break;
251
+ case 'removeAttr': if (el) el.removeAttribute(m.key); break;
252
+ case 'focus': if (el) el.focus(); break;
253
+ case 'blur': if (el) el.blur(); break;
254
+ case 'scroll': if (el) el.scrollIntoView({ behavior: m.behavior || 'smooth' }); break;
255
+ case 'setSignal': _set(m.name, m.value); break;
256
+ case 'navigate': _nvmlNavigate(m.path); break;
257
+ case 'reload': location.reload(); break;
258
+ case 'redirect': location.href = m.url; break;
259
+ case 'alert': alert(m.value); break;
260
+ case 'console': console[m.level || 'log'](m.value); break;
261
+ case 'setConfig': if (m.key === 'title') document.title = m.value; break;
262
+ case 'toast': _nvmlToast(m.value, m.duration, m.type); break;
263
+ case 'patchList': _patchList(m.id, m.items, m.template, m.key); break;
264
+ }
265
+ }
266
+ }
267
+
268
+ // ── Virtual DOM patch for innerHTML ────────────────────────────────────
269
+ function _patchHTML(container, newHTML) {
270
+ const tmp = document.createElement('div');
271
+ tmp.innerHTML = newHTML;
272
+ _diffChildren(container, tmp);
273
+ }
274
+
275
+ function _diffChildren(parent, newParent) {
276
+ const oldChildren = Array.from(parent.childNodes);
277
+ const newChildren = Array.from(newParent.childNodes);
278
+ const max = Math.max(oldChildren.length, newChildren.length);
279
+ for (let i = 0; i < max; i++) {
280
+ const oldC = oldChildren[i], newC = newChildren[i];
281
+ if (!oldC && newC) { parent.appendChild(newC.cloneNode(true)); }
282
+ else if (oldC && !newC) { parent.removeChild(oldC); }
283
+ else if (oldC.nodeType !== newC.nodeType || oldC.nodeName !== newC.nodeName) {
284
+ parent.replaceChild(newC.cloneNode(true), oldC);
285
+ } else if (oldC.nodeType === Node.TEXT_NODE) {
286
+ if (oldC.textContent !== newC.textContent) oldC.textContent = newC.textContent;
287
+ } else if (oldC.nodeType === Node.ELEMENT_NODE) {
288
+ _diffAttrs(oldC, newC);
289
+ _diffChildren(oldC, newC);
290
+ }
291
+ }
292
+ }
293
+
294
+ function _diffAttrs(oldEl, newEl) {
295
+ for (const attr of Array.from(newEl.attributes)) {
296
+ if (oldEl.getAttribute(attr.name) !== attr.value) oldEl.setAttribute(attr.name, attr.value);
297
+ }
298
+ for (const attr of Array.from(oldEl.attributes)) {
299
+ if (!newEl.hasAttribute(attr.name)) oldEl.removeAttribute(attr.name);
300
+ }
301
+ }
302
+
303
+ // ── Keyed list patch ────────────────────────────────────────────────────
304
+ function _patchList(containerId, items, templateFn, keyField) {
305
+ const container = document.getElementById(containerId);
306
+ if (!container) return;
307
+ const existing = {};
308
+ Array.from(container.children).forEach(c => { const k = c.dataset.nvmlKey; if (k) existing[k] = c; });
309
+ const seen = new Set();
310
+ items.forEach((item, i) => {
311
+ const key = keyField ? String(item[keyField]) : String(i);
312
+ seen.add(key);
313
+ if (existing[key]) {
314
+ // update existing — diff attrs
315
+ const tmp = document.createElement('div');
316
+ tmp.innerHTML = (typeof templateFn === 'function' ? templateFn(item, i) : templateFn.replace(/\{\{item\}\}/g, item));
317
+ const newChild = tmp.firstElementChild;
318
+ if (newChild) { _diffAttrs(existing[key], newChild); _diffChildren(existing[key], newChild); }
319
+ } else {
320
+ // insert new
321
+ const tmp = document.createElement('div');
322
+ tmp.innerHTML = (typeof templateFn === 'function' ? templateFn(item, i) : templateFn.replace(/\{\{item\}\}/g, item));
323
+ const newChild = tmp.firstElementChild;
324
+ if (newChild) { newChild.dataset.nvmlKey = key; container.appendChild(newChild); }
325
+ }
326
+ });
327
+ // remove stale
328
+ Object.keys(existing).forEach(k => { if (!seen.has(k)) _transitionOut(existing[k], null, () => existing[k].remove()); });
329
+ }
330
+
331
+ // ── Transitions ─────────────────────────────────────────────────────────
332
+ function _transitionIn(el, display, transName) {
333
+ el.style.display = display || 'block';
334
+ if (transName) { el.classList.add('nvml-enter-' + transName); el.addEventListener('animationend', () => el.classList.remove('nvml-enter-' + transName), { once: true }); }
335
+ }
336
+
337
+ function _transitionOut(el, transName, done) {
338
+ if (transName) {
339
+ el.classList.add('nvml-leave-' + transName);
340
+ el.addEventListener('animationend', () => { el.style.display = 'none'; el.classList.remove('nvml-leave-' + transName); if (done) done(); }, { once: true });
341
+ } else { el.style.display = 'none'; if (done) done(); }
342
+ }
343
+
344
+ function _createEl(html) { const d = document.createElement('div'); d.innerHTML = html; return d.firstElementChild || d; }
345
+
346
+ // ── Toast ───────────────────────────────────────────────────────────────
347
+ function _nvmlToast(msg, duration, type) {
348
+ const t = document.createElement('div');
349
+ t.textContent = msg;
350
+ t.style.cssText = 'position:fixed;bottom:1.5rem;right:1.5rem;background:'+(type==='error'?'#e53e3e':type==='success'?'#38a169':'#2d3748')+';color:#fff;padding:0.65rem 1.2rem;border-radius:6px;font-size:0.9rem;z-index:9999;box-shadow:0 4px 12px #0004;animation:nvml-enter-toast 0.2s ease both';
351
+ document.body.appendChild(t);
352
+ setTimeout(() => { t.style.animation = 'nvml-leave-toast 0.2s ease both'; t.addEventListener('animationend', () => t.remove(), { once: true }); }, duration || 3000);
353
+ }
354
+
355
+ // ── Signal bindings (one-way: -> and two-way: <->) ──────────────────────
356
+ function _initBindings() {
357
+ document.querySelectorAll('[data-nvml-bind]').forEach(el => {
358
+ const binding = el.dataset.nvmlBind; // "prop:signal" or "prop:signal:2way"
359
+ binding.split(';').forEach(b => {
360
+ const parts = b.split(':');
361
+ const prop = parts[0], signal = parts[1], twoWay = parts[2] === '2way';
362
+ // apply initial value
363
+ _applyBinding(el, prop, _get(signal));
364
+ // subscribe to signal changes
365
+ _subscribe(signal, val => _applyBinding(el, prop, val));
366
+ // two-way: listen for input/change and push back to signal
367
+ if (twoWay) {
368
+ const evName = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') ? 'input' : 'change';
369
+ el.addEventListener(evName, () => _set(signal, el.type === 'checkbox' ? el.checked : el.value));
370
+ }
371
+ });
372
+ });
373
+ }
374
+
375
+ function _applyBinding(el, prop, val) {
376
+ if (prop === 'text') el.textContent = val ?? '';
377
+ else if (prop === 'html') _patchHTML(el, String(val ?? ''));
378
+ else if (prop === 'value') el.value = val ?? '';
379
+ else if (prop === 'checked') el.checked = !!val;
380
+ else if (prop === 'class') el.className = val ?? '';
381
+ else if (prop === 'style') el.style.cssText = val ?? '';
382
+ else if (prop === 'href') el.href = val ?? '';
383
+ else if (prop === 'src') el.src = val ?? '';
384
+ else if (prop === 'disabled') el.disabled = !!val;
385
+ else if (prop === 'hidden') { if (val) el.style.display = 'none'; else el.style.display = ''; }
386
+ else if (prop === 'placeholder') el.placeholder = val ?? '';
387
+ else el.setAttribute(prop, val ?? '');
388
+ }
389
+
390
+ // ── Conditional renders ─────────────────────────────────────────────────
391
+ function _initConditionals() {
392
+ document.querySelectorAll('[data-nvml-if]').forEach(el => {
393
+ const signal = el.dataset.nvmlIf;
394
+ const display = el.dataset.nvmlDisplay || 'block';
395
+ const trans = el.dataset.nvmlTransition || null;
396
+ const apply = val => val ? _transitionIn(el, display, trans) : _transitionOut(el, trans);
397
+ apply(_get(signal));
398
+ _subscribe(signal, apply);
399
+ });
400
+ }
401
+
402
+ // ── @each reactive lists ─────────────────────────────────────────────────
403
+ function _initEach() {
404
+ document.querySelectorAll('[data-nvml-each]').forEach(container => {
405
+ const signal = container.dataset.nvmlEach;
406
+ const template = container.dataset.nvmlTemplate || '';
407
+ const key = container.dataset.nvmlKey || null;
408
+ const render = items => _patchList(container.id, Array.isArray(items) ? items : [], template, key);
409
+ render(_get(signal) || []);
410
+ _subscribe(signal, render);
411
+ });
412
+ }
413
+
414
+ // ── Server-Sent Events for server-push signal updates ───────────────────
415
+ function _initSSE() {
416
+ if (!window.EventSource) return;
417
+ const es = new EventSource('/_nvml/sse');
418
+ es.addEventListener('signal', e => {
419
+ try { const { name, value } = JSON.parse(e.data); _set(name, value); } catch(_) {}
420
+ });
421
+ es.addEventListener('mutations', e => {
422
+ try { _applyMutations(JSON.parse(e.data)); } catch(_) {}
423
+ });
424
+ es.onerror = () => { es.close(); setTimeout(_initSSE, 3000); }; // reconnect
425
+ }
426
+
427
+ // ── Live snapshot ────────────────────────────────────────────────────────
428
+ function _liveSnapshot() {
429
+ const elements = {};
430
+ document.querySelectorAll('[id]').forEach(e => {
431
+ elements[e.id] = { text: e.textContent, value: e.value ?? null, class: e.className };
432
+ });
433
+ return { title: document.title, url: location.href, query: Object.fromEntries(new URLSearchParams(location.search)), elements, state: _state };
434
+ }
435
+
436
+ // ── Triggered server script handler ─────────────────────────────────────
437
+ window.__nvmlRun = function(code) {
438
+ return fetch('/_nvml/run', {
439
+ method: 'POST',
440
+ headers: { 'Content-Type': 'application/json' },
441
+ body: JSON.stringify({ code, live: _liveSnapshot() })
442
+ }).then(r => r.json()).then(({ mutations, error }) => {
443
+ if (error) { console.error('[nvml]', error); return; }
444
+ _applyMutations(mutations || []);
445
+ }).catch(e => console.error('[nvml fetch]', e));
446
+ };
447
+
448
+ // ── Expose signal API ────────────────────────────────────────────────────
449
+ window.__nvml = {
450
+ get: _get, set: _set, subscribe: _subscribe,
451
+ state: _state, notify: _notify,
452
+ applyMutations: _applyMutations,
453
+ };
454
+
455
+ // ── Computed: set initial values ─────────────────────────────────────────
456
+ _computed.forEach(c => { if (c.initial !== null && c.initial !== undefined) _state[c.name] = c.initial; });
457
+
458
+ // ── Boot ─────────────────────────────────────────────────────────────────
459
+ document.addEventListener('DOMContentLoaded', function() {
460
+ _initBindings();
461
+ _initConditionals();
462
+ _initEach();
463
+ _initSSE();
464
+ // run wildcard effects on load
465
+ _effects.forEach(e => { if (e.deps.includes('*')) _runEffect(e); });
466
+ });
467
+
468
+ })();
469
+ </script>`;
470
+ }
471
+
472
+ // ── bf client runtime ────────────────────────────────────────────
473
+ // A compact Brainfuck runtime exposed as window.__nvml_bf on every page.
474
+ // Also provides window.__nvmlRunNode() for triggered nodejs scripts.
475
+
476
+ _renderBfRuntime() {
477
+ return ` <script>
478
+ // ── NVML bf (Brainfuck) Runtime ───────────────────────────────────────
479
+ (function(){
480
+ 'use strict';
481
+ const TAPE = 30000;
482
+ function _makeBf() {
483
+ let tape = new Uint8Array(TAPE), ptr = 0, output = '', input = '', inPtr = 0;
484
+ const obj = {
485
+ get tape() { return tape; },
486
+ get pointer() { return ptr; },
487
+ get output() { return output; },
488
+ get input() { return input; },
489
+ set input(v) { input = String(v); inPtr = 0; },
490
+ cell(n, v) { const i = n === undefined ? ptr : +n; if (v !== undefined) tape[i] = +v & 0xFF; return tape[i]; },
491
+ reset() { tape = new Uint8Array(TAPE); ptr = 0; output = ''; input = ''; inPtr = 0; return obj; },
492
+ run(code, inp) {
493
+ if (inp !== undefined) { input = String(inp); inPtr = 0; }
494
+ const src = String(code).split('').filter(c => '><+-.,[]'.includes(c));
495
+ const bk = {};
496
+ const stk = [];
497
+ for (let i = 0; i < src.length; i++) {
498
+ if (src[i] === '[') stk.push(i);
499
+ else if (src[i] === ']') { const o = stk.pop(); bk[o] = i; bk[i] = o; }
500
+ }
501
+ let ip = 0, ops = 0;
502
+ while (ip < src.length) {
503
+ if (++ops > 10000000) throw new Error('[bf] max ops exceeded');
504
+ switch (src[ip]) {
505
+ case '>': ptr = (ptr+1) % TAPE; break;
506
+ case '<': ptr = (ptr-1+TAPE) % TAPE; break;
507
+ case '+': tape[ptr] = (tape[ptr]+1) & 0xFF; break;
508
+ case '-': tape[ptr] = (tape[ptr]-1+256) & 0xFF; break;
509
+ case '.': output += String.fromCharCode(tape[ptr]); break;
510
+ case ',': tape[ptr] = inPtr < input.length ? input.charCodeAt(inPtr++) & 0xFF : 0; break;
511
+ case '[': if (!tape[ptr]) ip = bk[ip]; break;
512
+ case ']': if (tape[ptr]) ip = bk[ip]; break;
513
+ }
514
+ ip++;
515
+ }
516
+ return output;
517
+ },
518
+ };
519
+ return obj;
520
+ }
521
+ window.__nvml_bf_make = _makeBf;
522
+ window.__nvml_bf = _makeBf();
523
+
524
+ // ── __nvmlRunNode — triggered nodejs server script runner ────────────
525
+ window.__nvmlRunNode = function(code) {
526
+ return fetch('/_nvml/run-node', {
527
+ method: 'POST',
528
+ headers: { 'Content-Type': 'application/json' },
529
+ body: JSON.stringify({ code, live: window.__nvml ? { state: window.__nvml.state } : {} })
530
+ }).then(r => r.json()).then(({ mutations, error }) => {
531
+ if (error) { console.error('[nvml nodejs]', error); return; }
532
+ if (window.__nvml) window.__nvml.applyMutations(mutations || []);
533
+ }).catch(e => console.error('[nvml nodejs fetch]', e));
534
+ };
535
+ })();
536
+ </script>`;
537
+ }
172
538
 
173
- renderElement(el, depth) {
539
+ // ── Client-side router script ────────────────────────────────────
540
+
541
+ _renderRoutingScript(doc) {
542
+ const routes = (doc.routes || []).map(r => ({
543
+ path: typeof r.path === 'object' ? (r.path.value || r.path) : r.path,
544
+ html: r.body.map(n => this.renderElement(this._bodyToEl(n), 1, doc)).join(''),
545
+ }));
546
+ const routesJson = JSON.stringify(routes);
547
+ return `<script>
548
+ // ── NVML Client Router ────────────────────────────────────────────────
549
+ (function(){
550
+ const _routes = ${routesJson};
551
+ const _outlet = document.getElementById('nvml-router-outlet') || document.body;
552
+
553
+ function _match(path) {
554
+ for (const r of _routes) {
555
+ const pattern = new RegExp('^' + r.path.replace(/:([^/]+)/g, '([^/]+)') + '$');
556
+ const m = path.match(pattern);
557
+ if (m) {
558
+ const keys = [...r.path.matchAll(/:([^/]+)/g)].map(x => x[1]);
559
+ const params = {};
560
+ keys.forEach((k, i) => { params[k] = m[i + 1]; });
561
+ return { route: r, params };
562
+ }
563
+ }
564
+ return null;
565
+ }
566
+
567
+ function _nvmlNavigate(path) {
568
+ window.history.pushState({}, '', path);
569
+ _render(path);
570
+ }
571
+ window._nvmlNavigate = _nvmlNavigate;
572
+
573
+ function _render(path) {
574
+ const found = _match(path);
575
+ if (!found) return;
576
+ if (window.__nvml) {
577
+ // inject route params as signals
578
+ Object.entries(found.params).forEach(([k, v]) => window.__nvml.set(k, v));
579
+ }
580
+ _outlet.innerHTML = found.route.html;
581
+ }
582
+
583
+ window.addEventListener('popstate', () => _render(location.pathname));
584
+ document.addEventListener('click', e => {
585
+ const a = e.target.closest('[data-nvml-link]');
586
+ if (a) { e.preventDefault(); _nvmlNavigate(a.dataset.nvmlLink || a.getAttribute('href')); }
587
+ });
588
+
589
+ document.addEventListener('DOMContentLoaded', () => _render(location.pathname));
590
+ })();
591
+ </script>`;
592
+ }
593
+
594
+ _bodyToEl(node) {
595
+ // Dummy wrapper for route body nodes that need rendering
596
+ if (!node) return null;
597
+ return node;
598
+ }
599
+
600
+ // ── Element → HTML ───────────────────────────────────────────────
601
+
602
+ renderElement(el, depth, doc) {
603
+ if (!el) return '';
174
604
  const ind = ' '.repeat(depth);
175
605
 
176
- // ── script element ──────────────────────────────────────
177
- if (el.tag === 'script') {
178
- return this._renderScript(el, ind);
606
+ // slot outlet
607
+ if (el.isSlotOutlet) return this._renderSlotOutlet(el, depth, doc);
608
+
609
+ // each block
610
+ if (el.tag === 'each-block') return this._renderEachBlock(el, ind, doc);
611
+
612
+ // script
613
+ if (el.tag === 'script') return this._renderScript(el, ind);
614
+
615
+ // component placeholder (external)
616
+ if (el.props && el.props['data-component'] && !el.children.length && !el.textValue) {
617
+ return this._renderComponentPlaceholder(el, ind);
179
618
  }
180
619
 
181
- const tag = el.tag;
620
+ let tag = el.tag;
621
+ if (!tag || tag === '..') return '';
182
622
  const isVoid = VOID_ELEMENTS.has(tag);
183
623
 
184
- // build attributes
185
- const attrs = this._buildAttrs(el);
624
+ // build attrs
625
+ const attrs = this._buildAttrs(el, doc);
626
+
627
+ // conditional render: data-nvml-if
628
+ if (el.cond) {
629
+ attrs['data-nvml-if'] = el.cond;
630
+ attrs['data-nvml-display'] = el.props.style?.includes('inline') ? 'inline' : 'block';
631
+ }
632
+
633
+ // bindings: data-nvml-bind="prop:signal;prop2:signal2:2way"
634
+ if (el.bindings && el.bindings.length > 0) {
635
+ attrs['data-nvml-bind'] = el.bindings.map(b => `${b.prop}:${b.signal}${b.twoWay ? ':2way' : ''}`).join(';');
636
+ }
637
+
638
+ // transitions
639
+ if (el.transitions && el.transitions.length > 0) {
640
+ el.transitions.forEach(t => {
641
+ this._transitionNames.add(t.name);
642
+ attrs['data-nvml-transition'] = t.name;
643
+ });
644
+ }
186
645
 
187
- // inline style from [..]::ss
188
- if (el.ss && el.ss.trim()) {
189
- const styleId = el.props.id || el.id;
190
- // inject as a <style> block right before the element
191
- // (scoped via data-nvml-id if no id given)
192
- attrs['data-nvml-id'] = el.id;
646
+ // signal text (inline signal ref → data-nvml-bind="text:signal")
647
+ if (el._signalText) {
648
+ const existing = attrs['data-nvml-bind'] || '';
649
+ attrs['data-nvml-bind'] = (existing ? existing + ';' : '') + `text:${el._signalText}`;
193
650
  }
194
651
 
195
652
  const attrStr = this._attrsToString(attrs);
196
653
  const openTag = `<${tag}${attrStr}>`;
197
654
 
198
- if (isVoid) {
199
- let html = ind + openTag;
200
- if (el.ss && el.ss.trim()) {
201
- html = ind + this._scopedStyle(el) + '\n' + html;
202
- }
203
- return html;
204
- }
655
+ // scoped style block
656
+ const scopedStyleBlock = el.ss && el.ss.trim() ? ind + this._scopedStyle(el) + '\n' : '';
657
+
658
+ if (isVoid) return scopedStyleBlock + ind + openTag;
205
659
 
206
- // children
207
660
  const childLines = [];
208
661
 
209
- if (el.textValue !== null && el.textValue !== undefined) {
210
- childLines.push(ind + ' ' + escHtml(String(el.textValue)));
662
+ if (el._rawHTML) {
663
+ childLines.push(ind + ' ' + el._rawHTML);
664
+ } else if (el.textValue !== null && el.textValue !== undefined) {
665
+ // check for signal reference in text value
666
+ const tv = String(el.textValue);
667
+ if (tv.startsWith('__sig:')) {
668
+ // bind text to signal — leave text empty; runtime fills in
669
+ attrs['data-nvml-bind'] = (attrs['data-nvml-bind'] ? attrs['data-nvml-bind'] + ';' : '') + `text:${tv.slice(6)}`;
670
+ } else {
671
+ childLines.push(ind + ' ' + escHtml(tv));
672
+ }
211
673
  }
212
674
 
213
- for (const child of el.children) {
214
- childLines.push(this.renderElement(child, depth + 1));
675
+ for (const child of (el.children || [])) {
676
+ childLines.push(this.renderElement(child, depth + 1, doc));
215
677
  }
216
678
 
217
- const inner = childLines.length ? '\n' + childLines.join('\n') + '\n' + ind : '';
679
+ const inner = childLines.length ? '\n' + childLines.filter(Boolean).join('\n') + '\n' + ind : '';
218
680
  const closeTag = `</${tag}>`;
219
681
 
220
- let html = ind + openTag + inner + closeTag;
682
+ return scopedStyleBlock + ind + openTag + inner + closeTag;
683
+ }
221
684
 
222
- // prepend scoped <style> block if element has ::ss
223
- if (el.ss && el.ss.trim()) {
224
- html = ind + this._scopedStyle(el) + '\n' + html;
225
- }
685
+ // ── @each block ──────────────────────────────────────────────────
686
+
687
+ _renderEachBlock(el, ind, doc) {
688
+ // Generate a template string and a container div.
689
+ // The reactive runtime will clone the template per item.
690
+ // For SSR: render with the initial state value if available.
691
+ const signal = el.eachSignal;
692
+ const itemVar = el.eachItemVar || 'item';
693
+
694
+ // Build the per-item template as an HTML string with {{item}} placeholder
695
+ const templateParts = (el._eachBody || el.children || []).map(child => {
696
+ if (child && child.kind) {
697
+ // raw AST child — skip (handled at parse time)
698
+ return '';
699
+ }
700
+ return this.renderElement(child, 0, doc);
701
+ }).filter(Boolean);
702
+
703
+ const templateHTML = templateParts.join('') || `<div>{{item}}</div>`;
704
+ const escapedTemplate = esc(templateHTML);
226
705
 
227
- return html;
706
+ return `${ind}<div id="${escHtml(el.id)}" data-nvml-each="${escHtml(signal)}" data-nvml-template=\`${escapedTemplate}\`></div>`;
228
707
  }
229
708
 
230
- // ── script rendering ───────────────────────────────────────
709
+ // ── Slot outlet ──────────────────────────────────────────────────
710
+
711
+ _renderSlotOutlet(el, depth, doc) {
712
+ const slotName = el.slotName || 'default';
713
+ const slotEls = (this._slots || {})[slotName] || [];
714
+ if (!slotEls.length) return '';
715
+ return slotEls.map(s => this.renderElement(s, depth, doc)).join('\n');
716
+ }
717
+
718
+ // ── Component placeholder (external) ─────────────────────────────
719
+
720
+ _renderComponentPlaceholder(el, ind) {
721
+ const name = el.props['data-component'];
722
+ const attrs = this._attrsToString(this._buildAttrs(el, null));
723
+ return `${ind}<div${attrs}></div>`;
724
+ }
725
+
726
+ // ── script rendering ─────────────────────────────────────────────
231
727
 
232
728
  _renderScript(el, ind) {
233
729
  const scope = el._scriptScope || el.props.scope || 'client';
234
730
  const lang = el._scriptLang || el.props.language || el.props.lang || 'js';
235
731
 
236
- // nv/novac server scripts emit a fetch block that:
237
- // 1. snapshots live DOM state (text, value, class of every id'd element)
238
- // 2. POSTs the Nova code + live snapshot to /_nvml/run
239
- // 3. receives { mutations: [...] } and applies them to the DOM
732
+ // Nova server-side: already ran, leave a comment
240
733
  if ((lang === 'novac' || lang === 'nv') && scope === 'server') {
241
734
  return this._renderNvFetch(el, ind);
242
735
  }
243
736
 
244
- // client-side scripts (js / plain)
245
- let code = el._scriptCode || el.code || el.textValue || '';
737
+ // Node.js server-side: already ran at executor time, leave a comment
738
+ if ((lang === 'nodejs' || lang === 'node') && el._ranOnServer) {
739
+ return `${ind}<!-- nodejs server script executed at render time -->`;
740
+ }
246
741
 
247
- // Nova JS compilation via emitter (only if emitter provided and lang is nv)
742
+ // Node.js server-side with trigger: generate a fetch-based trigger wrapper
743
+ if ((lang === 'nodejs' || lang === 'node') && el.props.trigger) {
744
+ return this._renderNodejsFetch(el, ind);
745
+ }
746
+
747
+ // Custom lang (registered via @lang)
748
+ if (this._langDefs && this._langDefs[lang]) {
749
+ return this._renderCustomLangScript(el, ind, this._langDefs[lang]);
750
+ }
751
+
752
+ // Regular JS / Nova-compiled-to-JS
753
+ let code = el._scriptCode || el.code || el.textValue || '';
248
754
  if ((lang === 'novac' || lang === 'nv') && scope !== 'server' && this.novaEmitter) {
249
- try {
250
- code = this.novaEmitter(code);
251
- } catch (e) {
252
- code = `/* Nova compilation error: ${e.message} */`;
253
- }
755
+ try { code = this.novaEmitter(code); }
756
+ catch (e) { code = `/* Nova compilation error: ${e.message} */`; }
254
757
  }
255
758
 
256
759
  const extraAttrs = [];
257
760
  if (el.props.src) extraAttrs.push(`src="${escHtml(el.props.src)}"`);
258
761
  if (el.props.defer) extraAttrs.push('defer');
259
762
  if (el.props.async) extraAttrs.push('async');
260
- if (el.props.type && lang !== 'novac' && lang !== 'nv') {
261
- extraAttrs.push(`type="${escHtml(el.props.type)}"`);
262
- }
263
-
763
+ if (el.props.type && lang !== 'novac' && lang !== 'nv') extraAttrs.push(`type="${escHtml(el.props.type)}"`);
264
764
  const attrStr = extraAttrs.length ? ' ' + extraAttrs.join(' ') : '';
265
765
 
266
- if (!code.trim() && el.props.src) {
267
- return `${ind}<script${attrStr}></script>`;
766
+ if (!code.trim() && el.props.src) return `${ind}<script${attrStr}></script>`;
767
+ return `${ind}<script${attrStr}>\n${code}\n${ind}</script>`;
768
+ }
769
+
770
+ // ── nodejs triggered server script ───────────────────────────────
771
+ // Sends code to /_nvml/run-node endpoint; the server runs it in Node.js VM.
772
+
773
+ _renderNodejsFetch(el, ind) {
774
+ const code = (el._scriptCode || el.code || el.textValue || '').trim();
775
+ const trigger = el.props.trigger || null;
776
+ const target = el.props.target || null;
777
+ const escaped = esc(code);
778
+
779
+ const fetchCall = `window.__nvmlRunNode(\`${escaped}\`)`;
780
+
781
+ if (trigger && target) {
782
+ const evList = trigger.split(',').map(t => t.trim());
783
+ const handlers = evList.map(ev => `_t.addEventListener('${ev}', function(_e){ ${fetchCall}; });`).join('\n ');
784
+ return `${ind}<script>
785
+ ${ind}document.addEventListener('DOMContentLoaded', function() {
786
+ ${ind} const _t = document.getElementById('${target}');
787
+ ${ind} if (!_t) { console.error('[nvml] trigger target not found: ${target}'); return; }
788
+ ${ind} ${handlers}
789
+ ${ind}});
790
+ ${ind}</script>`;
268
791
  }
269
792
 
270
- return `${ind}<script${attrStr}>\n${code}\n${ind}</script>`;
793
+ return `${ind}<script>(async () => { ${fetchCall}; })();</script>`;
271
794
  }
272
795
 
273
- // ── nv server fetch block ──────────────────────────────────
796
+ // ── custom lang script rendering ─────────────────────────────────
274
797
 
275
- _renderNvFetch(el, ind) {
276
- // Script already ran server-side (no trigger, novaRunner was available).
277
- // Its mutations are already baked into the document — emit nothing.
798
+ _renderCustomLangScript(el, ind, langDef) {
278
799
  if (el._ranOnServer) {
279
- return `${ind}<!-- nv server script executed at render time -->`;
800
+ return `${ind}<!-- @lang ${langDef.name} (${langDef.runtimeLanguage}) script executed at render time -->`;
280
801
  }
281
802
 
282
- const code = (el._scriptCode || el.code || el.textValue || '').trim();
283
- const trigger = el.props.trigger || null; // 'click' | 'change' | …
284
- const target = el.props.target || null; // element id to attach to
285
-
286
- // Escape code safely for template literal embedding
287
- const escaped = code.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
288
-
289
- // Shared fetch + mutation-apply body (indented consistently)
290
- const i2 = ind + ' ';
291
- const i3 = ind + ' ';
292
- const i4 = ind + ' ';
293
- const fetchBody =
294
- `${i2}const _live = { title: document.title, url: location.href,
295
- ${i2} query: Object.fromEntries(new URLSearchParams(location.search)),
296
- ${i2} elements: {} };
297
- ${i2}document.querySelectorAll('[id]').forEach(function(_e){
298
- ${i2} _live.elements[_e.id] = { text: _e.textContent, value: _e.value ?? null, class: _e.className };
299
- ${i2}});
300
- ${i2}try {
301
- ${i3}const _res = await fetch('/_nvml/run', {
302
- ${i4}method: 'POST',
303
- ${i4}headers: { 'Content-Type': 'application/json' },
304
- ${i4}body: JSON.stringify({ code: \`${escaped}\`, live: _live })
305
- ${i3}});
306
- ${i3}if (!_res.ok) { console.error('[nvml] server script HTTP', _res.status); return; }
307
- ${i3}const { mutations, error } = await _res.json();
308
- ${i3}if (error) { console.error('[nvml] server script error:', error); return; }
309
- ${i3}for (const m of mutations) {
310
- ${i4}const _el = m.id ? document.getElementById(m.id) : null;
311
- ${i4}if (m.type === 'setText') { if (_el) _el.textContent = m.value; }
312
- ${i4}else if (m.type === 'setHTML') { if (_el) _el.innerHTML = m.value; }
313
- ${i4}else if (m.type === 'setProp') { if (_el) _el.setAttribute(m.key, m.value); }
314
- ${i4}else if (m.type === 'addClass') { if (_el) _el.classList.add(m.value); }
315
- ${i4}else if (m.type === 'setClass') { if (_el) _el.className = m.value; }
316
- ${i4}else if (m.type === 'hide') { if (_el) _el.style.display = 'none'; }
317
- ${i4}else if (m.type === 'show') { if (_el) _el.style.display = m.value || 'block'; }
318
- ${i4}else if (m.type === 'alert') { alert(m.value); }
319
- ${i4}else if (m.type === 'addStyle') { const _s = document.createElement('style'); _s.textContent = m.value; document.head.appendChild(_s); }
320
- ${i4}else if (m.type === 'setConfig' && m.key === 'title') { document.title = m.value; }
321
- ${i3}}
322
- ${i2}} catch (_e) { console.error('[nvml] nv fetch error:', _e); }`;
323
-
324
- // trigger mode: attach as event listener on target element
325
- if (trigger && target) {
326
- return `${ind}<script>
803
+ const code = el._scriptCode || el.code || el.textValue || '';
804
+ const trigger = el.props.trigger || null;
805
+ const target = el.props.target || null;
806
+
807
+ // Server-side triggered custom lang: route through /_nvml/run (Nova) or /_nvml/run-node
808
+ if (langDef.scope === 'server-nova' || langDef.scope === 'server-node') {
809
+ const endpoint = langDef.scope === 'server-node' ? '/_nvml/run-node' : '/_nvml/run';
810
+ const wrapped = langDef.code ? langDef.code + '\n' + code : code;
811
+ const escaped = esc(wrapped);
812
+ const fetchCall = `fetch('${endpoint}', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: \`${escaped}\`, live: window.__nvml ? { state: window.__nvml.state } : {} }) }).then(r => r.json()).then(({ mutations }) => { if (window.__nvml) window.__nvml.applyMutations(mutations || []); })`;
813
+
814
+ if (trigger && target) {
815
+ const evList = trigger.split(',').map(t => t.trim());
816
+ const handlers = evList.map(ev => `_t.addEventListener('${ev}', function() { ${fetchCall}; });`).join('\n ');
817
+ return `${ind}<script>
327
818
  ${ind}document.addEventListener('DOMContentLoaded', function() {
328
- ${ind} const _target = document.getElementById('${target}');
329
- ${ind} if (!_target) { console.error('[nvml] trigger target not found: ${target}'); return; }
330
- ${ind} _target.addEventListener('${trigger}', async function() {
331
- ${fetchBody}
332
- ${ind} });
819
+ ${ind} const _t = document.getElementById('${target}');
820
+ ${ind} if (!_t) { console.error('[nvml] trigger target not found: ${target}'); return; }
821
+ ${ind} ${handlers}
333
822
  ${ind}});
334
823
  ${ind}</script>`;
824
+ }
825
+ return `${ind}<script>(async () => { ${fetchCall}; })();</script>`;
335
826
  }
336
827
 
337
- // default: run immediately on page load (IIFE)
828
+ // Client-scope custom lang: embed implementation + user code + bf runtime
829
+ const implCode = langDef.code || '';
830
+ const userCode = code;
338
831
  return `${ind}<script>
339
- ${ind}(async () => {
340
- ${fetchBody}
832
+ ${ind}// @lang ${langDef.name} — client runtime
833
+ ${ind}(function() {
834
+ ${ind} var bf = window.__nvml_bf || window.__nvml_bf_make();
835
+ ${implCode ? implCode + '\n' : ''}${userCode}
341
836
  ${ind}})();
342
837
  ${ind}</script>`;
343
838
  }
344
839
 
345
- // ── attribute builder ──────────────────────────────────────
840
+ // ── Triggered / IIFE nv server script ────────────────────────────
841
+
842
+ _renderNvFetch(el, ind) {
843
+ if (el._ranOnServer) return `${ind}<!-- nv server script executed at render time -->`;
844
+
845
+ const code = (el._scriptCode || el.code || el.textValue || '').trim();
846
+ const trigger = el.props.trigger || null;
847
+ const target = el.props.target || null;
848
+ const escaped = esc(code);
849
+
850
+ const fetchCall = `window.__nvmlRun(\`${escaped}\`)`;
851
+
852
+ if (trigger && target) {
853
+ const evList = trigger.split(',').map(t => t.trim());
854
+ const handlers = evList.map(ev => `_t.addEventListener('${ev}', function(_e){ ${fetchCall}; });`).join('\n ');
855
+ return `${ind}<script>
856
+ ${ind}document.addEventListener('DOMContentLoaded', function() {
857
+ ${ind} const _t = document.getElementById('${target}');
858
+ ${ind} if (!_t) { console.error('[nvml] trigger target not found: ${target}'); return; }
859
+ ${ind} ${handlers}
860
+ ${ind}});
861
+ ${ind}</script>`;
862
+ }
863
+
864
+ return `${ind}<script>(async () => { ${fetchCall}; })();</script>`;
865
+ }
866
+
867
+ // ── Attributes ───────────────────────────────────────────────────
346
868
 
347
- _buildAttrs(el) {
869
+ _buildAttrs(el, doc) {
348
870
  const attrs = {};
349
871
 
350
- // standard HTML attributes from props
351
- const HTML_ATTRS = new Set([
352
- 'id','class','style','href','src','alt','title',
353
- 'placeholder','type','value','name','action','method',
354
- 'target','rel','for','checked','disabled','readonly',
355
- 'required','autofocus','autocomplete','multiple','size',
356
- 'rows','cols','maxlength','minlength','min','max','step',
357
- 'pattern','tabindex','accesskey','dir','draggable','hidden',
358
- 'spellcheck','translate','contenteditable','data',
359
- 'width','height','loading','decoding','crossorigin',
360
- 'integrity','referrerpolicy','charset','media','onload',
361
- 'onclick','onchange','oninput','onsubmit','onkeydown',
362
- 'onkeyup','onmouseover','onmouseout','onfocus','onblur',
363
- 'role','aria-label','aria-hidden','aria-expanded',
364
- 'aria-controls','aria-describedby','aria-labelledby',
365
- ]);
366
-
367
- for (const [key, val] of Object.entries(el.props)) {
368
- // skip internal props
369
- if (['language','lang','scope','code','ss','src'].includes(key) && el.tag === 'script') continue;
370
- if (val === true) {
371
- // boolean attribute
372
- attrs[safeAttr(key)] = true;
373
- } else {
374
- attrs[safeAttr(key)] = val;
872
+ for (const [key, val] of Object.entries(el.props || {})) {
873
+ if (el.tag === 'script' && ['language','lang','scope','code','ss','trigger','target'].includes(key)) continue;
874
+ if (val === true) attrs[safeAttr(key)] = true;
875
+ else if (val === false || val === null || val === undefined) continue;
876
+ else {
877
+ // check for signal ref value (prefixed __sig: by executor)
878
+ if (typeof val === 'string' && val.startsWith('__sig:')) {
879
+ const sigName = val.slice(6);
880
+ const existing = attrs['data-nvml-bind'] || '';
881
+ attrs['data-nvml-bind'] = (existing ? existing + ';' : '') + `${key}:${sigName}`;
882
+ } else {
883
+ attrs[safeAttr(key)] = val;
884
+ }
375
885
  }
376
886
  }
377
887
 
378
- // extra props (non-standard — emit as data-* if not known)
379
888
  for (const [key, val] of Object.entries(el.extraProps || {})) {
380
889
  if (typeof val === 'string' || typeof val === 'number') {
381
- const attrName = HTML_ATTRS.has(key) ? key : `data-${safeAttr(key)}`;
382
- attrs[attrName] = val;
890
+ attrs[HTML_ATTRS.has(key) ? key : `data-${safeAttr(key)}`] = val;
383
891
  }
384
892
  }
385
893
 
894
+ if (el.ss && el.ss.trim()) attrs['data-nvml-id'] = el.id;
895
+
386
896
  return attrs;
387
897
  }
388
898
 
@@ -395,30 +905,20 @@ ${ind}</script>`;
395
905
  return parts.length ? ' ' + parts.join(' ') : '';
396
906
  }
397
907
 
398
- // ── scoped style block ────────────────────────────────────
908
+ // ── Scoped style ─────────────────────────────────────────────────
399
909
 
400
910
  _scopedStyle(el) {
401
- const selector = el.props.id
402
- ? `#${el.props.id}`
403
- : `[data-nvml-id="${el.id}"]`;
404
-
911
+ const selector = el.props.id ? `#${el.props.id}` : `[data-nvml-id="${el.id}"]`;
405
912
  const raw = el.ss.trim();
406
913
  let css;
407
914
  if (raw.includes('{')) {
408
- // full rules — pass through as-is
409
915
  css = raw;
410
916
  } else {
411
- // bare declarations: normalise indentation and scope under our selector
412
- const decls = raw
413
- .split('\n')
414
- .map(l => ' ' + l.trim())
415
- .filter(l => l.trim())
416
- .join('\n');
917
+ const decls = raw.split('\n').map(l => ' ' + l.trim()).filter(l => l.trim()).join('\n');
417
918
  css = `${selector} {\n${decls}\n}`;
418
919
  }
419
-
420
920
  return `<style>\n${css}\n</style>`;
421
921
  }
422
922
  }
423
923
 
424
- module.exports = { Renderer };
924
+ module.exports = { Renderer };