novac 2.2.2 → 2.3.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/bin/novac +49 -12
- package/bin/novac.cmd +2 -0
- package/bin/nvc.cmd +2 -0
- package/bin/nvml +267 -224
- package/bin/nvml.cmd +2 -0
- package/examples/bf.nv +3 -39
- package/kits/kitmisc/kitdef.js +2037 -0
- package/kits/kitnovacweb/nvml/index.js +9 -3
- package/kits/kitnovacweb/nvml/renderer.js +113 -83
- package/kits/libtasker/kitdef.js +1395 -106
- package/package.json +1 -1
|
@@ -38,10 +38,16 @@ function executeAst(ast, opts = {}) {
|
|
|
38
38
|
* Render a live NvmlDocument to an HTML string.
|
|
39
39
|
* @param {NvmlDocument} doc
|
|
40
40
|
* @param {object} opts
|
|
41
|
-
* novaEmitter(code)
|
|
41
|
+
* novaEmitter(code) — compiles Nova code to JS for client scripts
|
|
42
|
+
* registerAction(name,code,type) — registers a named server action
|
|
43
|
+
* csrfToken — CSRF token embedded in page JS
|
|
42
44
|
*/
|
|
43
45
|
function renderDoc(doc, opts = {}) {
|
|
44
|
-
const renderer = new Renderer({
|
|
46
|
+
const renderer = new Renderer({
|
|
47
|
+
novaEmitter: opts.novaEmitter || null,
|
|
48
|
+
registerAction: opts.registerAction || null,
|
|
49
|
+
csrfToken: opts.csrfToken || '',
|
|
50
|
+
});
|
|
45
51
|
return renderer.render(doc);
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -64,4 +70,4 @@ function run(source, opts = {}) {
|
|
|
64
70
|
return { doc, html, ast };
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
module.exports = { parse, executeAst, renderDoc, compile, run, NvmlDocument, makeBfObject };
|
|
73
|
+
module.exports = { parse, executeAst, renderDoc, compile, run, NvmlDocument, makeBfObject };
|
|
@@ -29,70 +29,72 @@
|
|
|
29
29
|
const { makeBfObject } = require('./executor');
|
|
30
30
|
|
|
31
31
|
const VOID_ELEMENTS = new Set([
|
|
32
|
-
'area','base','br','col','embed','hr','img','input',
|
|
33
|
-
'link','meta','param','source','track','wbr',
|
|
32
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
33
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
34
34
|
]);
|
|
35
35
|
|
|
36
36
|
function safeAttr(name) { return String(name).replace(/[^a-zA-Z0-9\-_:.]/g, ''); }
|
|
37
|
-
function escHtml(str)
|
|
38
|
-
function esc(str)
|
|
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
39
|
|
|
40
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',
|
|
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
60
|
]);
|
|
61
61
|
|
|
62
62
|
class Renderer {
|
|
63
63
|
constructor(options = {}) {
|
|
64
64
|
this.novaEmitter = options.novaEmitter || null;
|
|
65
|
+
this.registerAction = options.registerAction || null; // (name, code, type) => void
|
|
66
|
+
this._csrfToken = options.csrfToken || '';
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
render(doc) {
|
|
68
70
|
const config = doc.config || {};
|
|
69
|
-
const hasState
|
|
71
|
+
const hasState = Object.keys(doc.state || {}).length > 0;
|
|
70
72
|
const hasComputed = (doc.computed || []).length > 0;
|
|
71
|
-
const hasEffects
|
|
72
|
-
const hasRoutes
|
|
73
|
+
const hasEffects = (doc.effects || []).length > 0;
|
|
74
|
+
const hasRoutes = (doc.routes || []).length > 0;
|
|
73
75
|
|
|
74
76
|
// ── <head> ──────────────────────────────────────────────────────
|
|
75
77
|
const head = [];
|
|
76
78
|
|
|
77
79
|
head.push(` <meta charset="${escHtml(config.charset || 'UTF-8')}">`);
|
|
78
80
|
head.push(` <meta name="viewport" content="${escHtml(config.viewport || 'width=device-width, initial-scale=1.0')}">`);
|
|
79
|
-
if (config.title)
|
|
81
|
+
if (config.title) head.push(` <title>${escHtml(config.title)}</title>`);
|
|
80
82
|
if (config.description) head.push(` <meta name="description" content="${escHtml(config.description)}">`);
|
|
81
|
-
if (config.author)
|
|
83
|
+
if (config.author) head.push(` <meta name="author" content="${escHtml(config.author)}">`);
|
|
82
84
|
if (config.keywords) {
|
|
83
85
|
const kw = Array.isArray(config.keywords) ? config.keywords.join(', ') : config.keywords;
|
|
84
86
|
head.push(` <meta name="keywords" content="${escHtml(kw)}">`);
|
|
85
87
|
}
|
|
86
88
|
if (config['theme-color']) head.push(` <meta name="theme-color" content="${escHtml(config['theme-color'])}">`);
|
|
87
|
-
if (config.robots)
|
|
89
|
+
if (config.robots) head.push(` <meta name="robots" content="${escHtml(config.robots)}">`);
|
|
88
90
|
if (config.canonical) head.push(` <link rel="canonical" href="${escHtml(config.canonical)}">`);
|
|
89
|
-
if (config.favicon)
|
|
90
|
-
if (config.base)
|
|
91
|
+
if (config.favicon) head.push(` <link rel="icon" href="${escHtml(config.favicon)}">`);
|
|
92
|
+
if (config.base) head.push(` <base href="${escHtml(config.base)}">`);
|
|
91
93
|
|
|
92
|
-
for (const k of ['og:title','og:description','og:image','og:url','og:type']) {
|
|
94
|
+
for (const k of ['og:title', 'og:description', 'og:image', 'og:url', 'og:type']) {
|
|
93
95
|
if (config[k]) head.push(` <meta property="${escHtml(k)}" content="${escHtml(config[k])}">`);
|
|
94
96
|
}
|
|
95
|
-
for (const k of ['twitter:card','twitter:title','twitter:description','twitter:image']) {
|
|
97
|
+
for (const k of ['twitter:card', 'twitter:title', 'twitter:description', 'twitter:image']) {
|
|
96
98
|
if (config[k]) head.push(` <meta name="${escHtml(k)}" content="${escHtml(config[k])}">`);
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -109,9 +111,9 @@ class Renderer {
|
|
|
109
111
|
|
|
110
112
|
// Transition CSS (generated from ~ hints collected during element render)
|
|
111
113
|
this._transitionNames = new Set();
|
|
112
|
-
this._componentDefs
|
|
113
|
-
this._slots
|
|
114
|
-
this._langDefs
|
|
114
|
+
this._componentDefs = doc.components || {};
|
|
115
|
+
this._slots = doc.slots || {};
|
|
116
|
+
this._langDefs = doc.langs || {};
|
|
115
117
|
|
|
116
118
|
// Render body first to collect transition names and component templates
|
|
117
119
|
this._componentTemplates = {};
|
|
@@ -139,9 +141,9 @@ class Renderer {
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
// ── <body> ──────────────────────────────────────────────────────
|
|
142
|
-
const lang
|
|
144
|
+
const lang = config.lang || 'en';
|
|
143
145
|
const bodyClass = config.bodyClass ? ` class="${escHtml(config.bodyClass)}"` : '';
|
|
144
|
-
const bodyId
|
|
146
|
+
const bodyId = config.bodyId ? ` id="${escHtml(config.bodyId)}"` : '';
|
|
145
147
|
const bodyStyle = config.bodyStyle ? ` style="${escHtml(config.bodyStyle)}"` : '';
|
|
146
148
|
|
|
147
149
|
return [
|
|
@@ -161,15 +163,27 @@ class Renderer {
|
|
|
161
163
|
// ── Reactive runtime script ──────────────────────────────────────
|
|
162
164
|
|
|
163
165
|
_renderRuntime(doc) {
|
|
164
|
-
const stateJson
|
|
166
|
+
const stateJson = JSON.stringify(doc.state || {});
|
|
165
167
|
const computedJson = JSON.stringify((doc.computed || []).map(c => ({ name: c.name, initial: c.initialValue })));
|
|
166
|
-
|
|
168
|
+
// Effects: register named action at compile time, embed action name only
|
|
169
|
+
const effectsJson = JSON.stringify((doc.effects || []).map((e, i) => {
|
|
170
|
+
if (this.registerAction && e.code && e.code.trim()) {
|
|
171
|
+
const name = `effect_${i}`;
|
|
172
|
+
this.registerAction(name, e.code, 'nova');
|
|
173
|
+
return { deps: e.deps, action: name };
|
|
174
|
+
}
|
|
175
|
+
return { deps: e.deps, action: null }; // no-op if no server
|
|
176
|
+
}));
|
|
177
|
+
const csrf = this._csrfToken;
|
|
167
178
|
|
|
168
179
|
return ` <script>
|
|
169
180
|
// ── NVML Reactive Runtime ─────────────────────────────────────────────
|
|
170
181
|
(function(){
|
|
171
182
|
'use strict';
|
|
172
183
|
|
|
184
|
+
// ── CSRF token (embedded at compile time) ─────────────────────────────
|
|
185
|
+
const __csrf = '${csrf}';
|
|
186
|
+
|
|
173
187
|
// ── Signal store ──────────────────────────────────────────────────────
|
|
174
188
|
const _state = ${stateJson};
|
|
175
189
|
const _computed = ${computedJson};
|
|
@@ -214,12 +228,11 @@ function _subscribe(name, fn) {
|
|
|
214
228
|
|
|
215
229
|
// ── Effects ────────────────────────────────────────────────────────────
|
|
216
230
|
function _runEffect(effect) {
|
|
217
|
-
if (!effect.
|
|
218
|
-
|
|
219
|
-
fetch('/_nvml/run', {
|
|
231
|
+
if (!effect.action) return;
|
|
232
|
+
fetch('/_nvml/action/' + effect.action, {
|
|
220
233
|
method: 'POST',
|
|
221
|
-
headers: { 'Content-Type': 'application/json' },
|
|
222
|
-
body: JSON.stringify({
|
|
234
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': __csrf },
|
|
235
|
+
body: JSON.stringify({ live: _liveSnapshot() })
|
|
223
236
|
}).then(r => r.json()).then(({ mutations, error }) => {
|
|
224
237
|
if (error) { console.error('[nvml effect]', error); return; }
|
|
225
238
|
_applyMutations(mutations || []);
|
|
@@ -434,11 +447,11 @@ function _liveSnapshot() {
|
|
|
434
447
|
}
|
|
435
448
|
|
|
436
449
|
// ── Triggered server script handler ─────────────────────────────────────
|
|
437
|
-
window.__nvmlRun = function(
|
|
438
|
-
return fetch('/_nvml/
|
|
450
|
+
window.__nvmlRun = function(actionName) {
|
|
451
|
+
return fetch('/_nvml/action/' + actionName, {
|
|
439
452
|
method: 'POST',
|
|
440
|
-
headers: { 'Content-Type': 'application/json' },
|
|
441
|
-
body: JSON.stringify({
|
|
453
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': __csrf },
|
|
454
|
+
body: JSON.stringify({ live: _liveSnapshot() })
|
|
442
455
|
}).then(r => r.json()).then(({ mutations, error }) => {
|
|
443
456
|
if (error) { console.error('[nvml]', error); return; }
|
|
444
457
|
_applyMutations(mutations || []);
|
|
@@ -522,11 +535,11 @@ window.__nvml_bf_make = _makeBf;
|
|
|
522
535
|
window.__nvml_bf = _makeBf();
|
|
523
536
|
|
|
524
537
|
// ── __nvmlRunNode — triggered nodejs server script runner ────────────
|
|
525
|
-
window.__nvmlRunNode = function(
|
|
526
|
-
return fetch('/_nvml/
|
|
538
|
+
window.__nvmlRunNode = function(actionName) {
|
|
539
|
+
return fetch('/_nvml/action/' + actionName, {
|
|
527
540
|
method: 'POST',
|
|
528
|
-
headers: { 'Content-Type': 'application/json' },
|
|
529
|
-
body: JSON.stringify({
|
|
541
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': __csrf },
|
|
542
|
+
body: JSON.stringify({ live: window.__nvml ? { state: window.__nvml.state } : {} })
|
|
530
543
|
}).then(r => r.json()).then(({ mutations, error }) => {
|
|
531
544
|
if (error) { console.error('[nvml nodejs]', error); return; }
|
|
532
545
|
if (window.__nvml) window.__nvml.applyMutations(mutations || []);
|
|
@@ -540,8 +553,8 @@ window.__nvmlRunNode = function(code) {
|
|
|
540
553
|
|
|
541
554
|
_renderRoutingScript(doc) {
|
|
542
555
|
const routes = (doc.routes || []).map(r => ({
|
|
543
|
-
path:
|
|
544
|
-
html:
|
|
556
|
+
path: typeof r.path === 'object' ? (r.path.value || r.path) : r.path,
|
|
557
|
+
html: r.body.map(n => this.renderElement(this._bodyToEl(n), 1, doc)).join(''),
|
|
545
558
|
}));
|
|
546
559
|
const routesJson = JSON.stringify(routes);
|
|
547
560
|
return `<script>
|
|
@@ -626,7 +639,7 @@ document.addEventListener('DOMContentLoaded', () => _render(location.pathname));
|
|
|
626
639
|
|
|
627
640
|
// conditional render: data-nvml-if
|
|
628
641
|
if (el.cond) {
|
|
629
|
-
attrs['data-nvml-if']
|
|
642
|
+
attrs['data-nvml-if'] = el.cond;
|
|
630
643
|
attrs['data-nvml-display'] = el.props.style?.includes('inline') ? 'inline' : 'block';
|
|
631
644
|
}
|
|
632
645
|
|
|
@@ -676,7 +689,7 @@ document.addEventListener('DOMContentLoaded', () => _render(location.pathname));
|
|
|
676
689
|
childLines.push(this.renderElement(child, depth + 1, doc));
|
|
677
690
|
}
|
|
678
691
|
|
|
679
|
-
const inner
|
|
692
|
+
const inner = childLines.length ? '\n' + childLines.filter(Boolean).join('\n') + '\n' + ind : '';
|
|
680
693
|
const closeTag = `</${tag}>`;
|
|
681
694
|
|
|
682
695
|
return scopedStyleBlock + ind + openTag + inner + closeTag;
|
|
@@ -688,7 +701,7 @@ document.addEventListener('DOMContentLoaded', () => _render(location.pathname));
|
|
|
688
701
|
// Generate a template string and a container div.
|
|
689
702
|
// The reactive runtime will clone the template per item.
|
|
690
703
|
// For SSR: render with the initial state value if available.
|
|
691
|
-
const signal
|
|
704
|
+
const signal = el.eachSignal;
|
|
692
705
|
const itemVar = el.eachItemVar || 'item';
|
|
693
706
|
|
|
694
707
|
// Build the per-item template as an HTML string with {{item}} placeholder
|
|
@@ -710,7 +723,7 @@ document.addEventListener('DOMContentLoaded', () => _render(location.pathname));
|
|
|
710
723
|
|
|
711
724
|
_renderSlotOutlet(el, depth, doc) {
|
|
712
725
|
const slotName = el.slotName || 'default';
|
|
713
|
-
const slotEls
|
|
726
|
+
const slotEls = (this._slots || {})[slotName] || [];
|
|
714
727
|
if (!slotEls.length) return '';
|
|
715
728
|
return slotEls.map(s => this.renderElement(s, depth, doc)).join('\n');
|
|
716
729
|
}
|
|
@@ -718,7 +731,7 @@ document.addEventListener('DOMContentLoaded', () => _render(location.pathname));
|
|
|
718
731
|
// ── Component placeholder (external) ─────────────────────────────
|
|
719
732
|
|
|
720
733
|
_renderComponentPlaceholder(el, ind) {
|
|
721
|
-
const name
|
|
734
|
+
const name = el.props['data-component'];
|
|
722
735
|
const attrs = this._attrsToString(this._buildAttrs(el, null));
|
|
723
736
|
return `${ind}<div${attrs}></div>`;
|
|
724
737
|
}
|
|
@@ -727,7 +740,7 @@ document.addEventListener('DOMContentLoaded', () => _render(location.pathname));
|
|
|
727
740
|
|
|
728
741
|
_renderScript(el, ind) {
|
|
729
742
|
const scope = el._scriptScope || el.props.scope || 'client';
|
|
730
|
-
const lang
|
|
743
|
+
const lang = el._scriptLang || el.props.language || el.props.lang || 'js';
|
|
731
744
|
|
|
732
745
|
// Nova server-side: already ran, leave a comment
|
|
733
746
|
if ((lang === 'novac' || lang === 'nv') && scope === 'server') {
|
|
@@ -757,7 +770,7 @@ document.addEventListener('DOMContentLoaded', () => _render(location.pathname));
|
|
|
757
770
|
}
|
|
758
771
|
|
|
759
772
|
const extraAttrs = [];
|
|
760
|
-
if (el.props.src)
|
|
773
|
+
if (el.props.src) extraAttrs.push(`src="${escHtml(el.props.src)}"`);
|
|
761
774
|
if (el.props.defer) extraAttrs.push('defer');
|
|
762
775
|
if (el.props.async) extraAttrs.push('async');
|
|
763
776
|
if (el.props.type && lang !== 'novac' && lang !== 'nv') extraAttrs.push(`type="${escHtml(el.props.type)}"`);
|
|
@@ -771,15 +784,21 @@ document.addEventListener('DOMContentLoaded', () => _render(location.pathname));
|
|
|
771
784
|
// Sends code to /_nvml/run-node endpoint; the server runs it in Node.js VM.
|
|
772
785
|
|
|
773
786
|
_renderNodejsFetch(el, ind) {
|
|
774
|
-
const code
|
|
787
|
+
const code = (el._scriptCode || el.code || el.textValue || '').trim();
|
|
775
788
|
const trigger = el.props.trigger || null;
|
|
776
|
-
const target
|
|
777
|
-
const
|
|
789
|
+
const target = el.props.target || null;
|
|
790
|
+
const actionName = el.props.action || el.props.id || null;
|
|
791
|
+
|
|
792
|
+
if (!actionName) {
|
|
793
|
+
process.stderr.write(`[nvml] Warning: nodejs script block missing 'action' prop — skipping. Add action="my-action-name".\n`);
|
|
794
|
+
return `${ind}<!-- nvml: nodejs script skipped — no action name -->`;
|
|
795
|
+
}
|
|
778
796
|
|
|
779
|
-
|
|
797
|
+
if (this.registerAction) this.registerAction(actionName, code, 'nodejs');
|
|
798
|
+
const fetchCall = `window.__nvmlRunNode('${actionName}')`;
|
|
780
799
|
|
|
781
800
|
if (trigger && target) {
|
|
782
|
-
const evList
|
|
801
|
+
const evList = trigger.split(',').map(t => t.trim());
|
|
783
802
|
const handlers = evList.map(ev => `_t.addEventListener('${ev}', function(_e){ ${fetchCall}; });`).join('\n ');
|
|
784
803
|
return `${ind}<script>
|
|
785
804
|
${ind}document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -800,19 +819,24 @@ ${ind}</script>`;
|
|
|
800
819
|
return `${ind}<!-- @lang ${langDef.name} (${langDef.runtimeLanguage}) script executed at render time -->`;
|
|
801
820
|
}
|
|
802
821
|
|
|
803
|
-
const code
|
|
822
|
+
const code = el._scriptCode || el.code || el.textValue || '';
|
|
804
823
|
const trigger = el.props.trigger || null;
|
|
805
|
-
const target
|
|
824
|
+
const target = el.props.target || null;
|
|
806
825
|
|
|
807
|
-
// Server-side triggered custom lang:
|
|
826
|
+
// Server-side triggered custom lang: dispatch via /_nvml/action/:name
|
|
808
827
|
if (langDef.scope === 'server-nova' || langDef.scope === 'server-node') {
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
828
|
+
const actionName = el.props.action || el.props.id || null;
|
|
829
|
+
if (!actionName) {
|
|
830
|
+
process.stderr.write(`[nvml] Warning: @lang server script missing 'action' prop — skipping.\n`);
|
|
831
|
+
return `${ind}<!-- nvml: @lang server script skipped — no action name -->`;
|
|
832
|
+
}
|
|
833
|
+
const wrapped = langDef.code ? langDef.code + '\n' + code : code;
|
|
834
|
+
const type = langDef.scope === 'server-node' ? 'nodejs' : 'nova';
|
|
835
|
+
if (this.registerAction) this.registerAction(actionName, wrapped, type);
|
|
836
|
+
const fetchCall = `fetch('/_nvml/action/${actionName}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': __csrf }, body: JSON.stringify({ live: window.__nvml ? { state: window.__nvml.state } : {} }) }).then(r => r.json()).then(({ mutations }) => { if (window.__nvml) window.__nvml.applyMutations(mutations || []); })`;
|
|
813
837
|
|
|
814
838
|
if (trigger && target) {
|
|
815
|
-
const evList
|
|
839
|
+
const evList = trigger.split(',').map(t => t.trim());
|
|
816
840
|
const handlers = evList.map(ev => `_t.addEventListener('${ev}', function() { ${fetchCall}; });`).join('\n ');
|
|
817
841
|
return `${ind}<script>
|
|
818
842
|
${ind}document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -842,15 +866,21 @@ ${ind}</script>`;
|
|
|
842
866
|
_renderNvFetch(el, ind) {
|
|
843
867
|
if (el._ranOnServer) return `${ind}<!-- nv server script executed at render time -->`;
|
|
844
868
|
|
|
845
|
-
const code
|
|
869
|
+
const code = (el._scriptCode || el.code || el.textValue || '').trim();
|
|
846
870
|
const trigger = el.props.trigger || null;
|
|
847
|
-
const target
|
|
848
|
-
const
|
|
871
|
+
const target = el.props.target || null;
|
|
872
|
+
const actionName = el.props.action || el.props.id || null;
|
|
873
|
+
|
|
874
|
+
if (!actionName) {
|
|
875
|
+
process.stderr.write(`[nvml] Warning: server-side script block missing 'action' prop — skipping. Add action="my-action-name".\n`);
|
|
876
|
+
return `${ind}<!-- nvml: server script skipped — no action name -->`;
|
|
877
|
+
}
|
|
849
878
|
|
|
850
|
-
|
|
879
|
+
if (this.registerAction) this.registerAction(actionName, code, 'nova');
|
|
880
|
+
const fetchCall = `window.__nvmlRun('${actionName}')`;
|
|
851
881
|
|
|
852
882
|
if (trigger && target) {
|
|
853
|
-
const evList
|
|
883
|
+
const evList = trigger.split(',').map(t => t.trim());
|
|
854
884
|
const handlers = evList.map(ev => `_t.addEventListener('${ev}', function(_e){ ${fetchCall}; });`).join('\n ');
|
|
855
885
|
return `${ind}<script>
|
|
856
886
|
${ind}document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -870,8 +900,8 @@ ${ind}</script>`;
|
|
|
870
900
|
const attrs = {};
|
|
871
901
|
|
|
872
902
|
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)
|
|
903
|
+
if (el.tag === 'script' && ['language', 'lang', 'scope', 'code', 'ss', 'trigger', 'target'].includes(key)) continue;
|
|
904
|
+
if (val === true) attrs[safeAttr(key)] = true;
|
|
875
905
|
else if (val === false || val === null || val === undefined) continue;
|
|
876
906
|
else {
|
|
877
907
|
// check for signal ref value (prefixed __sig: by executor)
|
|
@@ -921,4 +951,4 @@ ${ind}</script>`;
|
|
|
921
951
|
}
|
|
922
952
|
}
|
|
923
953
|
|
|
924
|
-
module.exports = { Renderer };
|
|
954
|
+
module.exports = { Renderer };
|