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.
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/bin/novac +6 -3
- package/bin/nvc +0 -0
- package/bin/nvml +0 -0
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +5 -13
- package/examples/math.nv +2 -2
- package/kits/kitffmpeg/kitdef.js +1174 -0
- package/kits/libos/kitdef.js +3135 -0
- package/kits/libtasker/kitdef.js +125 -0
- package/package.json +1 -1
- package/scripts/update-bin.js +0 -0
- package/src/core/executor.js +7 -4
- package/src/core/lexer.js +2 -2
- package/src/index.js +0 -0
- package/novac/LICENSE +0 -21
- package/novac/README.md +0 -1823
- package/novac/bin/novac +0 -950
- package/novac/bin/nvc +0 -522
- package/novac/bin/nvml +0 -542
- package/novac/demo.nv +0 -245
- package/novac/demo_builtins.nv +0 -209
- package/novac/demo_http.nv +0 -62
- package/novac/examples/bf.nv +0 -69
- package/novac/examples/math.nv +0 -21
- package/novac/kits/kitai/kitdef.js +0 -2185
- package/novac/kits/kitansi/kitdef.js +0 -1402
- package/novac/kits/kitformat/kitdef.js +0 -1485
- package/novac/kits/kitgps/kitdef.js +0 -1862
- package/novac/kits/kitlibfs/kitdef.js +0 -231
- package/novac/kits/kitlibproc/kitdef.js +0 -78
- package/novac/kits/kitmatrix/ex.js +0 -19
- package/novac/kits/kitmatrix/kitdef.js +0 -960
- package/novac/kits/kitmpatch/kitdef.js +0 -906
- package/novac/kits/kitnovacweb/README.md +0 -1572
- package/novac/kits/kitnovacweb/demo.nv +0 -12
- package/novac/kits/kitnovacweb/demo.nvml +0 -71
- package/novac/kits/kitnovacweb/index.nova +0 -12
- package/novac/kits/kitnovacweb/kitdef.js +0 -692
- package/novac/kits/kitnovacweb/nova.kit.json +0 -8
- package/novac/kits/kitnovacweb/nvml/executor.js +0 -739
- package/novac/kits/kitnovacweb/nvml/index.js +0 -67
- package/novac/kits/kitnovacweb/nvml/lexer.js +0 -263
- package/novac/kits/kitnovacweb/nvml/parser.js +0 -508
- package/novac/kits/kitnovacweb/nvml/renderer.js +0 -924
- package/novac/kits/kitparse/kitdef.js +0 -1688
- package/novac/kits/kitregex++/kitdef.js +0 -1353
- package/novac/kits/kitrequire/kitdef.js +0 -1599
- package/novac/kits/kitx11/kitdef.js +0 -1
- package/novac/kits/kitx11/kitx11.js +0 -2472
- package/novac/kits/kitx11/kitx11_conn.js +0 -948
- package/novac/kits/kitx11/kitx11_worker.js +0 -121
- package/novac/kits/libtea/tf.js +0 -2691
- package/novac/kits/libterm/ex.js +0 -285
- package/novac/kits/libterm/kitdef.js +0 -1927
- package/novac/node_modules/chalk/license +0 -9
- package/novac/node_modules/chalk/package.json +0 -83
- package/novac/node_modules/chalk/readme.md +0 -297
- package/novac/node_modules/chalk/source/index.d.ts +0 -325
- package/novac/node_modules/chalk/source/index.js +0 -225
- package/novac/node_modules/chalk/source/utilities.js +0 -33
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +0 -236
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +0 -223
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +0 -1
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +0 -34
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +0 -55
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +0 -190
- package/novac/node_modules/commander/LICENSE +0 -22
- package/novac/node_modules/commander/Readme.md +0 -1176
- package/novac/node_modules/commander/esm.mjs +0 -16
- package/novac/node_modules/commander/index.js +0 -24
- package/novac/node_modules/commander/lib/argument.js +0 -150
- package/novac/node_modules/commander/lib/command.js +0 -2777
- package/novac/node_modules/commander/lib/error.js +0 -39
- package/novac/node_modules/commander/lib/help.js +0 -747
- package/novac/node_modules/commander/lib/option.js +0 -380
- package/novac/node_modules/commander/lib/suggestSimilar.js +0 -101
- package/novac/node_modules/commander/package-support.json +0 -19
- package/novac/node_modules/commander/package.json +0 -82
- package/novac/node_modules/commander/typings/esm.d.mts +0 -3
- package/novac/node_modules/commander/typings/index.d.ts +0 -1113
- package/novac/node_modules/node-addon-api/LICENSE.md +0 -9
- package/novac/node_modules/node-addon-api/README.md +0 -95
- package/novac/node_modules/node-addon-api/common.gypi +0 -21
- package/novac/node_modules/node-addon-api/except.gypi +0 -25
- package/novac/node_modules/node-addon-api/index.js +0 -14
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +0 -186
- package/novac/node_modules/node-addon-api/napi-inl.h +0 -7165
- package/novac/node_modules/node-addon-api/napi.h +0 -3364
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +0 -42
- package/novac/node_modules/node-addon-api/node_api.gyp +0 -9
- package/novac/node_modules/node-addon-api/noexcept.gypi +0 -26
- package/novac/node_modules/node-addon-api/nothing.c +0 -0
- package/novac/node_modules/node-addon-api/package-support.json +0 -21
- package/novac/node_modules/node-addon-api/package.json +0 -480
- package/novac/node_modules/node-addon-api/tools/README.md +0 -73
- package/novac/node_modules/node-addon-api/tools/check-napi.js +0 -99
- package/novac/node_modules/node-addon-api/tools/clang-format.js +0 -71
- package/novac/node_modules/node-addon-api/tools/conversion.js +0 -301
- package/novac/node_modules/serialize-javascript/LICENSE +0 -27
- package/novac/node_modules/serialize-javascript/README.md +0 -149
- package/novac/node_modules/serialize-javascript/index.js +0 -297
- package/novac/node_modules/serialize-javascript/package.json +0 -33
- package/novac/package.json +0 -27
- package/novac/scripts/update-bin.js +0 -24
- package/novac/src/core/bstd.js +0 -1035
- package/novac/src/core/config.js +0 -155
- package/novac/src/core/describe.js +0 -187
- package/novac/src/core/emitter.js +0 -499
- package/novac/src/core/error.js +0 -86
- package/novac/src/core/executor.js +0 -5606
- package/novac/src/core/formatter.js +0 -686
- package/novac/src/core/lexer.js +0 -1026
- package/novac/src/core/nova_builtins.js +0 -717
- package/novac/src/core/nova_thread_worker.js +0 -166
- package/novac/src/core/parser.js +0 -2181
- package/novac/src/core/types.js +0 -112
- package/novac/src/index.js +0 -28
- 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
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 };
|