novac 2.2.0 → 2.2.2

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 (122) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/bin/novac +6 -3
  4. package/bin/nvc +0 -0
  5. package/bin/nvml +0 -0
  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 +5 -13
  10. package/examples/math.nv +2 -2
  11. package/kits/kitffmpeg/kitdef.js +1174 -0
  12. package/kits/libos/kitdef.js +3135 -0
  13. package/kits/libtasker/kitdef.js +125 -0
  14. package/package.json +1 -1
  15. package/scripts/update-bin.js +0 -0
  16. package/src/core/executor.js +7 -4
  17. package/src/core/lexer.js +2 -2
  18. package/src/index.js +0 -0
  19. package/novac/LICENSE +0 -21
  20. package/novac/README.md +0 -1823
  21. package/novac/bin/novac +0 -950
  22. package/novac/bin/nvc +0 -522
  23. package/novac/bin/nvml +0 -542
  24. package/novac/demo.nv +0 -245
  25. package/novac/demo_builtins.nv +0 -209
  26. package/novac/demo_http.nv +0 -62
  27. package/novac/examples/bf.nv +0 -69
  28. package/novac/examples/math.nv +0 -21
  29. package/novac/kits/kitai/kitdef.js +0 -2185
  30. package/novac/kits/kitansi/kitdef.js +0 -1402
  31. package/novac/kits/kitformat/kitdef.js +0 -1485
  32. package/novac/kits/kitgps/kitdef.js +0 -1862
  33. package/novac/kits/kitlibfs/kitdef.js +0 -231
  34. package/novac/kits/kitlibproc/kitdef.js +0 -78
  35. package/novac/kits/kitmatrix/ex.js +0 -19
  36. package/novac/kits/kitmatrix/kitdef.js +0 -960
  37. package/novac/kits/kitmpatch/kitdef.js +0 -906
  38. package/novac/kits/kitnovacweb/README.md +0 -1572
  39. package/novac/kits/kitnovacweb/demo.nv +0 -12
  40. package/novac/kits/kitnovacweb/demo.nvml +0 -71
  41. package/novac/kits/kitnovacweb/index.nova +0 -12
  42. package/novac/kits/kitnovacweb/kitdef.js +0 -692
  43. package/novac/kits/kitnovacweb/nova.kit.json +0 -8
  44. package/novac/kits/kitnovacweb/nvml/executor.js +0 -739
  45. package/novac/kits/kitnovacweb/nvml/index.js +0 -67
  46. package/novac/kits/kitnovacweb/nvml/lexer.js +0 -263
  47. package/novac/kits/kitnovacweb/nvml/parser.js +0 -508
  48. package/novac/kits/kitnovacweb/nvml/renderer.js +0 -924
  49. package/novac/kits/kitparse/kitdef.js +0 -1688
  50. package/novac/kits/kitregex++/kitdef.js +0 -1353
  51. package/novac/kits/kitrequire/kitdef.js +0 -1599
  52. package/novac/kits/kitx11/kitdef.js +0 -1
  53. package/novac/kits/kitx11/kitx11.js +0 -2472
  54. package/novac/kits/kitx11/kitx11_conn.js +0 -948
  55. package/novac/kits/kitx11/kitx11_worker.js +0 -121
  56. package/novac/kits/libtea/tf.js +0 -2691
  57. package/novac/kits/libterm/ex.js +0 -285
  58. package/novac/kits/libterm/kitdef.js +0 -1927
  59. package/novac/node_modules/chalk/license +0 -9
  60. package/novac/node_modules/chalk/package.json +0 -83
  61. package/novac/node_modules/chalk/readme.md +0 -297
  62. package/novac/node_modules/chalk/source/index.d.ts +0 -325
  63. package/novac/node_modules/chalk/source/index.js +0 -225
  64. package/novac/node_modules/chalk/source/utilities.js +0 -33
  65. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +0 -236
  66. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +0 -223
  67. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +0 -1
  68. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +0 -34
  69. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +0 -55
  70. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +0 -190
  71. package/novac/node_modules/commander/LICENSE +0 -22
  72. package/novac/node_modules/commander/Readme.md +0 -1176
  73. package/novac/node_modules/commander/esm.mjs +0 -16
  74. package/novac/node_modules/commander/index.js +0 -24
  75. package/novac/node_modules/commander/lib/argument.js +0 -150
  76. package/novac/node_modules/commander/lib/command.js +0 -2777
  77. package/novac/node_modules/commander/lib/error.js +0 -39
  78. package/novac/node_modules/commander/lib/help.js +0 -747
  79. package/novac/node_modules/commander/lib/option.js +0 -380
  80. package/novac/node_modules/commander/lib/suggestSimilar.js +0 -101
  81. package/novac/node_modules/commander/package-support.json +0 -19
  82. package/novac/node_modules/commander/package.json +0 -82
  83. package/novac/node_modules/commander/typings/esm.d.mts +0 -3
  84. package/novac/node_modules/commander/typings/index.d.ts +0 -1113
  85. package/novac/node_modules/node-addon-api/LICENSE.md +0 -9
  86. package/novac/node_modules/node-addon-api/README.md +0 -95
  87. package/novac/node_modules/node-addon-api/common.gypi +0 -21
  88. package/novac/node_modules/node-addon-api/except.gypi +0 -25
  89. package/novac/node_modules/node-addon-api/index.js +0 -14
  90. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +0 -186
  91. package/novac/node_modules/node-addon-api/napi-inl.h +0 -7165
  92. package/novac/node_modules/node-addon-api/napi.h +0 -3364
  93. package/novac/node_modules/node-addon-api/node_addon_api.gyp +0 -42
  94. package/novac/node_modules/node-addon-api/node_api.gyp +0 -9
  95. package/novac/node_modules/node-addon-api/noexcept.gypi +0 -26
  96. package/novac/node_modules/node-addon-api/nothing.c +0 -0
  97. package/novac/node_modules/node-addon-api/package-support.json +0 -21
  98. package/novac/node_modules/node-addon-api/package.json +0 -480
  99. package/novac/node_modules/node-addon-api/tools/README.md +0 -73
  100. package/novac/node_modules/node-addon-api/tools/check-napi.js +0 -99
  101. package/novac/node_modules/node-addon-api/tools/clang-format.js +0 -71
  102. package/novac/node_modules/node-addon-api/tools/conversion.js +0 -301
  103. package/novac/node_modules/serialize-javascript/LICENSE +0 -27
  104. package/novac/node_modules/serialize-javascript/README.md +0 -149
  105. package/novac/node_modules/serialize-javascript/index.js +0 -297
  106. package/novac/node_modules/serialize-javascript/package.json +0 -33
  107. package/novac/package.json +0 -27
  108. package/novac/scripts/update-bin.js +0 -24
  109. package/novac/src/core/bstd.js +0 -1035
  110. package/novac/src/core/config.js +0 -155
  111. package/novac/src/core/describe.js +0 -187
  112. package/novac/src/core/emitter.js +0 -499
  113. package/novac/src/core/error.js +0 -86
  114. package/novac/src/core/executor.js +0 -5606
  115. package/novac/src/core/formatter.js +0 -686
  116. package/novac/src/core/lexer.js +0 -1026
  117. package/novac/src/core/nova_builtins.js +0 -717
  118. package/novac/src/core/nova_thread_worker.js +0 -166
  119. package/novac/src/core/parser.js +0 -2181
  120. package/novac/src/core/types.js +0 -112
  121. package/novac/src/index.js +0 -28
  122. package/novac/src/runtime/stdlib.js +0 -244
@@ -1,924 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * NVML Renderer v2
5
- *
6
- * Converts a NvmlDocument into a complete HTML string.
7
- *
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
27
- */
28
-
29
- const { makeBfObject } = require('./executor');
30
-
31
- const VOID_ELEMENTS = new Set([
32
- 'area','base','br','col','embed','hr','img','input',
33
- 'link','meta','param','source','track','wbr',
34
- ]);
35
-
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
- ]);
61
-
62
- class Renderer {
63
- constructor(options = {}) {
64
- this.novaEmitter = options.novaEmitter || null;
65
- }
66
-
67
- render(doc) {
68
- const config = doc.config || {};
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)}">`);
82
- if (config.keywords) {
83
- const kw = Array.isArray(config.keywords) ? config.keywords.join(', ') : config.keywords;
84
- head.push(` <meta name="keywords" content="${escHtml(kw)}">`);
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)}">`);
91
-
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])}">`);
94
- }
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])}">`);
97
- }
98
-
99
- const stylesheets = config.stylesheet ? [].concat(config.stylesheet) : [];
100
- for (const href of stylesheets) head.push(` <link rel="stylesheet" href="${escHtml(href)}">`);
101
-
102
- const scripts = config.scripts ? [].concat(config.scripts) : [];
103
- for (const src of scripts) head.push(` <script src="${escHtml(src)}"></script>`);
104
-
105
- // @ss global styles
106
- if (doc.globalStyles && doc.globalStyles.trim()) {
107
- head.push(` <style>\n${doc.globalStyles}\n </style>`);
108
- }
109
-
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 || {};
115
-
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));
120
-
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>`);
129
- }
130
-
131
- if (config.head) head.push(` ${config.head}`);
132
-
133
- // ── bf client runtime ────────────────────────────────────────────
134
- head.push(this._renderBfRuntime());
135
-
136
- // ── Reactive runtime ────────────────────────────────────────────
137
- if (hasState || hasComputed || hasEffects || hasRoutes) {
138
- head.push(this._renderRuntime(doc));
139
- }
140
-
141
- // ── <body> ──────────────────────────────────────────────────────
142
- const lang = config.lang || 'en';
143
- const bodyClass = config.bodyClass ? ` class="${escHtml(config.bodyClass)}"` : '';
144
- const bodyId = config.bodyId ? ` id="${escHtml(config.bodyId)}"` : '';
145
- const bodyStyle = config.bodyStyle ? ` style="${escHtml(config.bodyStyle)}"` : '';
146
-
147
- return [
148
- '<!DOCTYPE html>',
149
- `<html lang="${escHtml(lang)}">`,
150
- '<head>',
151
- head.join('\n'),
152
- '</head>',
153
- `<body${bodyClass}${bodyId}${bodyStyle}>`,
154
- bodyParts.join('\n'),
155
- hasRoutes ? this._renderRoutingScript(doc) : '',
156
- '</body>',
157
- '</html>',
158
- ].join('\n');
159
- }
160
-
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
- }
538
-
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 '';
604
- const ind = ' '.repeat(depth);
605
-
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);
618
- }
619
-
620
- let tag = el.tag;
621
- if (!tag || tag === '..') return '';
622
- const isVoid = VOID_ELEMENTS.has(tag);
623
-
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
- }
645
-
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}`;
650
- }
651
-
652
- const attrStr = this._attrsToString(attrs);
653
- const openTag = `<${tag}${attrStr}>`;
654
-
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;
659
-
660
- const childLines = [];
661
-
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
- }
673
- }
674
-
675
- for (const child of (el.children || [])) {
676
- childLines.push(this.renderElement(child, depth + 1, doc));
677
- }
678
-
679
- const inner = childLines.length ? '\n' + childLines.filter(Boolean).join('\n') + '\n' + ind : '';
680
- const closeTag = `</${tag}>`;
681
-
682
- return scopedStyleBlock + ind + openTag + inner + closeTag;
683
- }
684
-
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);
705
-
706
- return `${ind}<div id="${escHtml(el.id)}" data-nvml-each="${escHtml(signal)}" data-nvml-template=\`${escapedTemplate}\`></div>`;
707
- }
708
-
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 ─────────────────────────────────────────────
727
-
728
- _renderScript(el, ind) {
729
- const scope = el._scriptScope || el.props.scope || 'client';
730
- const lang = el._scriptLang || el.props.language || el.props.lang || 'js';
731
-
732
- // Nova server-side: already ran, leave a comment
733
- if ((lang === 'novac' || lang === 'nv') && scope === 'server') {
734
- return this._renderNvFetch(el, ind);
735
- }
736
-
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
- }
741
-
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 || '';
754
- if ((lang === 'novac' || lang === 'nv') && scope !== 'server' && this.novaEmitter) {
755
- try { code = this.novaEmitter(code); }
756
- catch (e) { code = `/* Nova compilation error: ${e.message} */`; }
757
- }
758
-
759
- const extraAttrs = [];
760
- if (el.props.src) extraAttrs.push(`src="${escHtml(el.props.src)}"`);
761
- if (el.props.defer) extraAttrs.push('defer');
762
- if (el.props.async) extraAttrs.push('async');
763
- if (el.props.type && lang !== 'novac' && lang !== 'nv') extraAttrs.push(`type="${escHtml(el.props.type)}"`);
764
- const attrStr = extraAttrs.length ? ' ' + extraAttrs.join(' ') : '';
765
-
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>`;
791
- }
792
-
793
- return `${ind}<script>(async () => { ${fetchCall}; })();</script>`;
794
- }
795
-
796
- // ── custom lang script rendering ─────────────────────────────────
797
-
798
- _renderCustomLangScript(el, ind, langDef) {
799
- if (el._ranOnServer) {
800
- return `${ind}<!-- @lang ${langDef.name} (${langDef.runtimeLanguage}) script executed at render time -->`;
801
- }
802
-
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>
818
- ${ind}document.addEventListener('DOMContentLoaded', function() {
819
- ${ind} const _t = document.getElementById('${target}');
820
- ${ind} if (!_t) { console.error('[nvml] trigger target not found: ${target}'); return; }
821
- ${ind} ${handlers}
822
- ${ind}});
823
- ${ind}</script>`;
824
- }
825
- return `${ind}<script>(async () => { ${fetchCall}; })();</script>`;
826
- }
827
-
828
- // Client-scope custom lang: embed implementation + user code + bf runtime
829
- const implCode = langDef.code || '';
830
- const userCode = code;
831
- return `${ind}<script>
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}
836
- ${ind}})();
837
- ${ind}</script>`;
838
- }
839
-
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 ───────────────────────────────────────────────────
868
-
869
- _buildAttrs(el, doc) {
870
- const attrs = {};
871
-
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
- }
885
- }
886
- }
887
-
888
- for (const [key, val] of Object.entries(el.extraProps || {})) {
889
- if (typeof val === 'string' || typeof val === 'number') {
890
- attrs[HTML_ATTRS.has(key) ? key : `data-${safeAttr(key)}`] = val;
891
- }
892
- }
893
-
894
- if (el.ss && el.ss.trim()) attrs['data-nvml-id'] = el.id;
895
-
896
- return attrs;
897
- }
898
-
899
- _attrsToString(attrs) {
900
- const parts = [];
901
- for (const [k, v] of Object.entries(attrs)) {
902
- if (v === true) parts.push(k);
903
- else parts.push(`${k}="${escHtml(String(v))}"`);
904
- }
905
- return parts.length ? ' ' + parts.join(' ') : '';
906
- }
907
-
908
- // ── Scoped style ─────────────────────────────────────────────────
909
-
910
- _scopedStyle(el) {
911
- const selector = el.props.id ? `#${el.props.id}` : `[data-nvml-id="${el.id}"]`;
912
- const raw = el.ss.trim();
913
- let css;
914
- if (raw.includes('{')) {
915
- css = raw;
916
- } else {
917
- const decls = raw.split('\n').map(l => ' ' + l.trim()).filter(l => l.trim()).join('\n');
918
- css = `${selector} {\n${decls}\n}`;
919
- }
920
- return `<style>\n${css}\n</style>`;
921
- }
922
- }
923
-
924
- module.exports = { Renderer };