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