/** * Browser reactive runtime (Point 2: reactive directives). * * Served verbatim at `/__wrnexus/reactive.js` for any page that contains a * `data-scope`. It is plain browser JS (no build step) and self-contained: it * inlines a tiny `signal()` so it has no imports to resolve. * * Supported directives (this is exactly what the `.wrn` compiler emits): * data-scope="count: 0, name: 'x'" declare reactive state on a subtree * data-on-="count++" run a statement in scope on an event * data-text="expr" element textContent follows an expression * data-wrn-loop-locals="base64-json" preserves SSR {#each} item/index values * data-wrnexus-csr="id" target for generated CSR fetch bindings * {{expr}} or {expr} interpolation inside text nodes * * Expressions are evaluated by a tiny parser instead of `eval`/`new Function`, * so production can use a strong CSP without `unsafe-eval`. */ export const REACTIVE_RUNTIME = String.raw` (function () { var behaviorInstances = new WeakMap(); var mountedBehaviorRoots = new Set(); var pendingUpdateHooks = new Map(); var updateHooksScheduled = false; var behaviorObserver; function scheduleUpdateHook(element, callback) { pendingUpdateHooks.set(element, callback); if (updateHooksScheduled) return; updateHooksScheduled = true; queueMicrotask(function () { updateHooksScheduled = false; var pending = Array.from(pendingUpdateHooks.entries()); pendingUpdateHooks.clear(); pending.forEach(function (entry) { var root = entry[0]; var hook = entry[1]; if (!root.isConnected) return; try { hook(); } catch (error) { console.error("[wrnexus] component update hook failed", error); } }); }); } function decodeScopePayload(value) { var binary = window.atob(value); var bytes = new Uint8Array(binary.length); for ( var index = 0; index < binary.length; index++ ) { bytes[index] = binary.charCodeAt(index); } var decoded = new TextDecoder( "utf-8", ).decode(bytes); var parsed = JSON.parse(decoded); return parsed && typeof parsed === "object" ? parsed : {}; } function decodeBehaviorPayload(value) { var binary = window.atob(value); var bytes = new Uint8Array(binary.length); for (var index = 0; index < binary.length; index++) { bytes[index] = binary.charCodeAt(index); } return new TextDecoder("utf-8").decode(bytes); } function parseBehavior(element) { var raw = element.getAttribute( "data-wrn-behavior", ); if (!raw) { return null; } try { var decoded = decodeBehaviorPayload(raw); var parsed = JSON.parse(decoded); if ( !parsed || typeof parsed !== "object" ) { return null; } return parsed; } catch (error) { console.error( "[wrnexus] failed to parse component behavior", error, ); return null; } } function extractBehaviorFunctions(source) { var functions = []; var re = /(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g; var match; while ((match = re.exec(source || ""))) { var start = re.lastIndex; var depth = 1; var quote = ""; var i = start; for (; i < source.length; i++) { var ch = source[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'" || ch === "\`") { quote = ch; continue; } if (ch === "{") depth++; else if (ch === "}" && --depth === 0) break; } if (depth !== 0) break; functions.push({ name: match[1], args: match[2].split(",").map(function (arg) { return arg.trim(); }).filter(Boolean), body: source.slice(start, i), }); re.lastIndex = i + 1; } return functions; } function splitStatements(input) { var parts = []; var start = 0; var depth = 0; var quote = ""; for (var i = 0; i < input.length; i++) { var ch = input[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'" || ch === "\`") { quote = ch; continue; } if (ch === "(" || ch === "[" || ch === "{") depth++; else if (ch === ")" || ch === "]" || ch === "}") depth--; else if ( ( ch === ";" || ch === "\n" || ch === "\r" ) && depth === 0 ) { var nextIndex = i + 1; while ( nextIndex < input.length && /\s/.test(input[nextIndex]) ) { nextIndex++; } if ( input.slice( nextIndex, nextIndex + 4, ) === "else" ) { continue; } var part = input .slice(start, i) .trim(); if (part) { parts.push(part); } start = i + 1; } } var tail = input.slice(start).trim(); if (tail) parts.push(tail); return parts; } function signal(initial) { var value = initial; var subscribers = new Set(); var notifying = false; return { get: function () { return value; }, set: function (nextValue) { if ( Object.is( nextValue, value, ) ) { return; } var previous = value; value = nextValue; /* * Prevent nested synchronous notification of the * same signal. */ if (notifying) { return; } notifying = true; try { /* * Always iterate a snapshot. A renderer can read * this signal and subscribe again while it runs. */ var snapshot = Array.from(subscribers); for ( var index = 0; index < snapshot.length; index++ ) { var subscriber = snapshot[index]; if ( subscribers.has( subscriber, ) ) { subscriber( value, previous, ); } } } finally { notifying = false; } }, subscribe: function ( subscriber, ) { subscribers.add( subscriber, ); return function () { subscribers.delete( subscriber, ); }; }, }; } function applyReactiveAttribute(node, name, value) { var lowerName = String(name || "").toLowerCase(); if ( lowerName === "value" && ( node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement ) ) { var nextValue = value == null ? "" : String(value); if (node.value !== nextValue) { node.value = nextValue; } if (value == null) { node.removeAttribute("value"); } else { node.setAttribute( "value", nextValue, ); } return; } if ( lowerName === "checked" && node instanceof HTMLInputElement ) { var checked = !!value; if (node.checked !== checked) { node.checked = checked; } if (checked) { node.setAttribute( "checked", "", ); } else { node.removeAttribute( "checked", ); } return; } if ( lowerName === "selected" && node instanceof HTMLOptionElement ) { var selected = !!value; node.selected = selected; if (selected) { node.setAttribute( "selected", "", ); } else { node.removeAttribute( "selected", ); } return; } if ( lowerName === "disabled" || lowerName === "required" || lowerName === "readonly" || lowerName === "multiple" ) { var enabled = !!value; if (lowerName in node) { try { node[lowerName] = enabled; } catch (_) { // Fall through to attribute handling. } } if (enabled) { node.setAttribute( name, "", ); } else { node.removeAttribute(name); } return; } if (value === false || value == null) { node.removeAttribute(name); } else { node.setAttribute( name, value === true ? "" : String(value), ); } } function setupScope(el) { if (el.__wrnexusScope) return; el.__wrnexusScope = true; el.__wrnexusHydrated = true; var encodedScope = el.getAttribute( "data-wrn-scope", ); var initial; if (encodedScope) { try { initial = decodeScopePayload( encodedScope, ); } catch (error) { console.error( "[wrnexus] failed to decode scope payload", error, ); initial = {}; } } else { var declaration = el.getAttribute( "data-scope", ) || ""; initial = parseScopeDecl( declaration, ); } var signals = {}; Object.keys(initial).forEach(function (k) { signals[k] = signal(initial[k]); }); var renderers = []; // Dependency-tracked rendering: while a renderer runs, every signal it reads // subscribes THAT renderer (not a blanket "re-render everything"). A signal // change then re-runs only the renderers that actually read it. The Set in // signal.subscribe dedupes, so re-subscribing each run is cheap and bounded. var currentRenderer = null; var currentRenderer = null; function reactive(fn) { var running = false; function run() { if (running) { return; } running = true; var previousRenderer = currentRenderer; currentRenderer = run; try { fn(); } finally { currentRenderer = previousRenderer; running = false; } } renderers.push(run); return run; } function renderAll() { renderers.forEach(function (f) { f(); }); } function decodeLoopLocals(node) { if (!node) { return {}; } var element = node.nodeType === 1 ? node : node.parentElement; var owner = element && element.closest && element.closest( "[data-wrn-loop-locals]", ); if (!owner) { return {}; } var encoded = owner.getAttribute( "data-wrn-loop-locals", ); if (!encoded) { return {}; } try { var binary = window.atob(encoded); var bytes = new Uint8Array(binary.length); for ( var index = 0; index < binary.length; index++ ) { bytes[index] = binary.charCodeAt(index); } var decoded = new TextDecoder("utf-8").decode( bytes, ); var parsed = JSON.parse(decoded); return parsed && typeof parsed === "object" ? parsed : {}; } catch (error) { console.error( "[wrnexus] failed to decode loop locals", error, ); return {}; } } var behaviorFunctions = {}; var stateWatchers = {}; var anyStateListeners = new Set(); var cleanupCallbacks = []; var disposed = false; function readGlobal(name) { if (name === "window") return window; if (name === "document") return document; if (name === "console") return console; if (name === "Math") return Math; if (name === "JSON") return JSON; if (name === "Date") return Date; if (name === "URL") return URL; if (name === "location") return window.location; if (name === "history") return window.history; if (name === "navigator") return window.navigator; if (name === "setTimeout") return window.setTimeout.bind(window); if (name === "clearTimeout") return window.clearTimeout.bind(window); if (name === "requestAnimationFrame") return window.requestAnimationFrame.bind(window); if (name === "cancelAnimationFrame") return window.cancelAnimationFrame.bind(window); return undefined; } function readScope(name) { var sig = signals[name]; if (sig) { if (currentRenderer) sig.subscribe(currentRenderer); return sig.get(); } if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) { return behaviorFunctions[name]; } return readGlobal(name); } function peekScope(name) { if (signals[name]) return signals[name].get(); if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) { return behaviorFunctions[name]; } return readGlobal(name); } function notifyState(name, value, previous) { var watchers = stateWatchers[name]; if (watchers) { watchers.slice().forEach(function (watcher) { try { watcher(value, previous); } catch (error) { console.error("[wrnexus] watcher for '" + name + "' failed", error); } }); } anyStateListeners.forEach(function (listener) { try { listener(name, value, previous); } catch (error) { console.error("[wrnexus] state subscriber failed", error); } }); } function writeScope( name, value, ) { if (!signals[name]) { signals[name] = signal(value); notifyState( name, value, undefined, ); renderAll(); return; } var previous = signals[name].get(); if ( Object.is( previous, value, ) ) { return; } signals[name].set(value); notifyState( name, value, previous, ); } function evalExpr(expr, locals) { return evaluateExpression(expr, function (name) { if (locals && Object.prototype.hasOwnProperty.call(locals, name)) { return locals[name]; } return readScope(name); }); } function runStmt( source, locals, ) { var statements = splitStatements(source); for ( var statementIndex = 0; statementIndex < statements.length; statementIndex++ ) { var result = runStatement( statements[statementIndex], function (expression) { return evalExpr( expression, locals, ); }, function (name) { if ( locals && Object.prototype .hasOwnProperty.call( locals, name, ) ) { return locals[name]; } return peekScope(name); }, function (name, value) { if ( locals && Object.prototype .hasOwnProperty.call( locals, name, ) ) { locals[name] = value; } else { writeScope(name, value); } }, function (body) { return runStmt( body, locals, ); }, ); if (result.returned) { return result; } } return { returned: false, value: undefined, }; } function installBehaviorFunctions(source) { extractBehaviorFunctions(source || "").forEach(function (definition) { behaviorFunctions[ definition.name ] = function () { var locals = {}; for ( var i = 0; i < definition.args.length; i++ ) { locals[definition.args[i]] = arguments[i]; } var result = runStmt( definition.body, locals, ); return result.returned ? result.value : undefined; }; }); } // A binding belongs to THIS scope only when el is the node's nearest // [data-scope] ancestor. Otherwise a nested scope owns it and we skip it, // so an outer scope never clobbers an inner one's values. function owns(node) { var host = node.nodeType === 1 ? node : node.parentNode; return ( !!host && host.closest && host.closest( "[data-scope], [data-wrn-scope]", ) === el ); } // --- data-for list rendering ------------------------------------------- // Each [data-for="item in list"] element is a per-item template. On any // change to the list (or a dependency an item reads), the list re-renders. function parseFor(value) { var m = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)\s*$/.exec( value || "", ); return m ? { item: m[1], index: m[2], list: m[3] } : null; } function fillMustache(str, itemEval) { return str.replace(/\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function (_, d, s) { var e = (d || s).trim(); try { return String(itemEval(e)); } catch (err) { return ""; } }); } function hydrateItem( root, locals, ) { function localRead(name) { if ( Object.prototype.hasOwnProperty.call( locals, name, ) ) { return locals[name]; } return readScope(name); } function itemEval(expression) { return evaluateExpression( expression, localRead, ); } function applyItemTemplate( template, ) { return String(template).replace( /\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function ( _, doubleExpression, singleExpression, ) { var expression = ( doubleExpression || singleExpression ).trim(); try { var value = itemEval(expression); return value == null ? "" : String(value); } catch (_) { return ""; } }, ); } var elements = [root]; if (root.querySelectorAll) { Array.prototype.push.apply( elements, root.querySelectorAll("*"), ); } elements.forEach(function (node) { if ( !node || node.nodeType !== 1 ) { return; } var attributes = Array.prototype.slice.call( node.attributes, ); attributes.forEach(function ( attribute, ) { /* * data-text must be handled using itemEval(), * because item/index only exist in loop locals. */ if ( attribute.name === "data-text" ) { try { var textValue = itemEval(attribute.value); node.textContent = textValue == null ? "" : String(textValue); } catch (error) { console.error( "[wrnexus] data-for text binding failed for '" + attribute.value + "'", error, ); node.textContent = ""; } return; } /* * Conditional classes: * data-wrn-class-*='["class","expression"]' */ if ( attribute.name.indexOf( "data-wrn-class-", ) === 0 ) { var classBinding; try { classBinding = JSON.parse( attribute.value, ); } catch (_) { return; } if ( !classBinding || classBinding.length !== 2 ) { return; } var className = classBinding[0]; var classExpression = classBinding[1]; var classEnabled = false; try { classEnabled = !!itemEval( classExpression, ); } catch (_) { classEnabled = false; } node.classList.toggle( className, classEnabled, ); return; } /* * Reactive ordinary attributes: * data-wrn-bind-*='["aria-expanded","{isOpen(index)}"]' */ if ( attribute.name.indexOf( "data-wrn-bind-", ) === 0 ) { var binding; try { binding = JSON.parse( attribute.value, ); } catch (_) { return; } if ( !binding || binding.length !== 2 ) { return; } var attributeName = binding[0]; var attributeTemplate = binding[1]; var exact = /^\{([^{}]+)\}$/.exec( attributeTemplate, ); var rawValue; if (exact) { try { rawValue = itemEval( exact[1].trim(), ); } catch (_) { return; } } else { rawValue = attributeTemplate.replace( /\{([^{}]+)\}/g, function ( _, expression, ) { try { var value = itemEval( expression.trim(), ); return value == null ? "" : String(value); } catch (_) { return ""; } }, ); } applyReactiveAttribute( node, attributeName, rawValue, ); return; } /* * Event handlers inside data-for. */ if ( attribute.name.indexOf( "data-on-", ) === 0 ) { var eventName = attribute.name.slice( "data-on-".length, ); var statement = attribute.value; node.addEventListener( eventName, function (event) { var eventLocals = {}; Object.keys( locals, ).forEach(function (key) { eventLocals[key] = locals[key]; }); eventLocals.event = event; eventLocals.$event = event; try { runStmt( statement, eventLocals, ); } catch (error) { console.error( "[wrnexus] data-for handler error in '" + statement + "'", error, ); } }, ); return; } /* * Static attributes containing interpolation. */ if ( attribute.value && attribute.value.indexOf("{") !== -1 ) { node.setAttribute( attribute.name, applyItemTemplate( attribute.value, ), ); } }); }); /* * Text interpolation inside loop content: * * {item.title} */ var walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, null, ); var textNode; while ( (textNode = walker.nextNode()) ) { var template = textNode.nodeValue || ""; if ( template.indexOf("{") === -1 ) { continue; } textNode.nodeValue = applyItemTemplate(template); } } Array.prototype.slice .call( el.querySelectorAll( "[data-for]", ), ) .forEach(function (tpl) { if ( !tpl.parentNode || !owns(tpl) ) { return; } var spec = parseFor( tpl.getAttribute( "data-for", ), ); if (!spec) { return; } var template = tpl.cloneNode(true); template.removeAttribute( "data-for", ); var parent = tpl.parentNode; var marker = document.createComment( "wrn-for", ); parent.insertBefore( marker, tpl, ); parent.removeChild(tpl); var clones = []; reactive(function () { var list = evalExpr(spec.list); if (!Array.isArray(list)) { console.error( "[wrnexus] data-for expected an array for '" + spec.list + "', received", list, ); list = []; } for ( var cloneIndex = 0; cloneIndex < clones.length; cloneIndex++ ) { var existingClone = clones[cloneIndex]; if ( existingClone.parentNode ) { existingClone.parentNode .removeChild( existingClone, ); } } clones = []; var fragment = document.createDocumentFragment(); for ( var itemIndex = 0; itemIndex < list.length; itemIndex++ ) { var clone = template.cloneNode(true); var locals = {}; locals[spec.item] = list[itemIndex]; if (spec.index) { locals[spec.index] = itemIndex; } hydrateItem( clone, locals, ); fragment.appendChild( clone, ); clones.push(clone); } parent.insertBefore( fragment, marker.nextSibling, ); }); }); // data-text bindings. Server-rendered {#each} nodes recover their item/index // values from the nearest data-wrn-loop-locals marker. el.querySelectorAll("[data-text]").forEach(function (node) { if (!owns(node)) return; var expr = node.getAttribute("data-text"); reactive(function () { try { var value = evalExpr( expr, decodeLoopLocals(node), ); node.textContent = value == null ? "" : String(value); } catch (_) { // Keep the server-rendered value when evaluation is unavailable. } }); }); // data-show="expr" — toggle visibility on truthiness. el.querySelectorAll("[data-show]").forEach(function (node) { if (!owns(node)) return; var showExpr = node.getAttribute("data-show"); reactive(function () { var visible = true; try { visible = !!evalExpr( showExpr, decodeLoopLocals(node), ); } catch (_) { // Keep visible when evaluation is unavailable. } node.style.display = visible ? "" : "none"; }); }); // Conditional class bindings emitted as: // data-wrn-class-*='["class-name","expression"]' var classBindNodes = [el].concat( Array.prototype.slice.call(el.querySelectorAll("*")), ); classBindNodes.forEach(function (node) { if (!owns(node)) return; Array.prototype.slice.call(node.attributes).forEach(function (marker) { if (marker.name.indexOf("data-wrn-class-") !== 0) return; var binding; try { binding = JSON.parse(marker.value); } catch (e) { return; } if (!binding || binding.length !== 2) return; var className = binding[0]; var expression = binding[1]; reactive(function () { var enabled = false; try { enabled = !!evalExpr( expression, decodeLoopLocals(node), ); } catch (error) { enabled = false; } node.classList.toggle( className, enabled, ); }); }); }); // Reactive ordinary attributes emitted by the compiler. Each marker stores // [attributeName, originalTemplate], preserving an SSR value while allowing // state changes to update type, aria-*, class, href, and other attributes. var bindNodes = [el].concat( Array.prototype.slice.call( el.querySelectorAll("*"), ), ); bindNodes.forEach(function (node) { if (!owns(node)) return; Array.prototype.slice .call(node.attributes) .forEach(function (marker) { if ( marker.name.indexOf( "data-wrn-bind-", ) !== 0 ) { return; } var binding; try { binding = JSON.parse( marker.value, ); } catch (error) { return; } if ( !binding || binding.length !== 2 ) { return; } var name = binding[0]; var template = binding[1]; reactive(function () { var locals = decodeLoopLocals(node); var exact = /^\{([^{}]+)\}$/.exec( template, ); var raw; if (exact) { try { raw = evalExpr( exact[1].trim(), locals, ); } catch (error) { return; } } else { raw = template.replace( /\{([^{}]+)\}/g, function ( _, expression, ) { try { var value = evalExpr( expression.trim(), locals, ); return value == null ? "" : String(value); } catch (error) { return ""; } }, ); } applyReactiveAttribute( node, name, raw, ); }); }); }); // {{expr}} / {expr} interpolation in text nodes var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); var textNode; while ((textNode = walker.nextNode())) { var template = textNode.nodeValue; if (template.indexOf("{") === -1) continue; if (!owns(textNode)) continue; (function (node, tpl) { reactive(function () { node.nodeValue = tpl.replace(/\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function (_, doubleExpr, singleExpr) { var expr = doubleExpr || singleExpr; try { var value = evalExpr( expr.trim(), decodeLoopLocals( node.parentNode, ), ); return value == null ? "" : String(value); } catch (err) { return ""; } }); }); })(textNode, template); } // Event handlers on elements, window, and document. var nodes = [el].concat(Array.prototype.slice.call(el.querySelectorAll("*"))); nodes.forEach(function (node) { if (!owns(node)) return; Array.prototype.slice.call(node.attributes).forEach(function (attr) { if (attr.name.indexOf("data-on-") !== 0) return; var rawName = attr.name.slice("data-on-".length); var target = node; var evt = rawName; if (rawName.indexOf("window-") === 0) { target = window; evt = rawName.slice("window-".length); } else if (rawName.indexOf("document-") === 0) { target = document; evt = rawName.slice("document-".length); } var stmt = attr.value; var listener = function (event) { var locals = decodeLoopLocals(node); locals.event = event; locals.$event = event; try { runStmt( stmt, locals, ); } catch (error) { console.error( "[wrnexus] handler error in '" + stmt + "'", error, ); } }; var options = target === window && (evt === "scroll" || evt === "touchstart" || evt === "touchmove") ? { passive: true } : undefined; target.addEventListener(evt, listener, options); cleanupCallbacks.push(function () { target.removeEventListener(evt, listener, options); }); }); }); var behavior = parseBehavior(el); if (behavior) { installBehaviorFunctions(behavior.functions); (behavior.watches || []).forEach(function (watch) { if (!watch || typeof watch.state !== "string" || typeof watch.body !== "string") return; if (!stateWatchers[watch.state]) stateWatchers[watch.state] = []; stateWatchers[watch.state].push(function (value, previous) { runStmt(watch.body, { value: value, previous: previous }); }); }); var updateSource = behavior.lifecycle && typeof behavior.lifecycle.update === "string" ? behavior.lifecycle.update.trim() : ""; if (updateSource) { anyStateListeners.add(function () { scheduleUpdateHook(el, function () { if (!disposed) runStmt(updateSource); }); }); } var mountSource = behavior.lifecycle && typeof behavior.lifecycle.mount === "string" ? behavior.lifecycle.mount.trim() : ""; var unmountSource = behavior.lifecycle && typeof behavior.lifecycle.unmount === "string" ? behavior.lifecycle.unmount.trim() : ""; var instance = { dispose: function () { if (disposed) return; disposed = true; cleanupCallbacks.splice(0).reverse().forEach(function (cleanup) { try { cleanup(); } catch (_) { /* ignore cleanup errors */ } }); pendingUpdateHooks.delete(el); if (unmountSource) { try { runStmt(unmountSource); } catch (error) { console.error("[wrnexus] component unmount hook failed", error); } } mountedBehaviorRoots.delete(el); behaviorInstances.delete(el); el.__wrnexusScope = false; }, }; behaviorInstances.set(el, instance); mountedBehaviorRoots.add(el); if (mountSource) { queueMicrotask(function () { if (!disposed && el.isConnected) { try { runStmt(mountSource); } catch (error) { console.error("[wrnexus] component mount hook failed", error); } } }); } } renderAll(); } function disposeBehaviors(root) { var elements = []; if (root && root.nodeType === 1 && root.hasAttribute("data-wrn-behavior")) { elements.push(root); } if (root && root.querySelectorAll) { Array.prototype.push.apply(elements, root.querySelectorAll("[data-wrn-behavior]")); } elements.reverse().forEach(function (element) { var instance = behaviorInstances.get(element); if (instance) instance.dispose(); }); } function ensureBehaviorObserver() { if (behaviorObserver || typeof MutationObserver === "undefined") return; behaviorObserver = new MutationObserver(function (records) { records.forEach(function (record) { Array.prototype.forEach.call(record.removedNodes, function (node) { if (node && node.nodeType === 1) disposeBehaviors(node); }); }); }); behaviorObserver.observe(document.documentElement, { childList: true, subtree: true }); } function hydrateScopes(root) { var host = root || document; var scopeSelector = "[data-scope], [data-wrn-scope]"; if ( host.nodeType === 1 && host.matches && host.matches(scopeSelector) ) { setupScope(host); } if (host.querySelectorAll) { host .querySelectorAll(scopeSelector) .forEach(setupScope); } ensureBehaviorObserver(); } function parseScopeDecl(decl) { var initial = {}; splitTopLevel(decl, ",").forEach(function (part) { var idx = findTopLevel(part, ":"); if (idx < 0) return; var name = part.slice(0, idx).trim(); var expr = part.slice(idx + 1).trim(); if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) return; try { initial[name] = evaluateExpression(expr, function () { return undefined; }); } catch (e) { console.error("[wrnexus] invalid data-scope value for '" + name + "'", e); } }); return initial; } function skipStatementWhitespace( source, index, ) { while ( index < source.length && /\s/.test(source[index]) ) { index++; } return index; } function findClosingDelimiter( source, start, opening, closing, ) { if (source[start] !== opening) { return -1; } var depth = 1; var quote = ""; var escaped = false; for ( var index = start + 1; index < source.length; index++ ) { var character = source[index]; if (quote) { if (escaped) { escaped = false; continue; } if (character === "\\") { escaped = true; continue; } if (character === quote) { quote = ""; } continue; } if ( character === '"' || character === "'" || character === "\`" ) { quote = character; continue; } if (character === opening) { depth++; continue; } if (character === closing) { depth--; if (depth === 0) { return index; } } } return -1; } function parseIfStatement(source) { source = String(source || "").trim(); if ( source.slice(0, 2) !== "if" || /[A-Za-z0-9_$]/.test( source.charAt(2), ) ) { return null; } var branches = []; var index = 0; var firstBranch = true; while (index < source.length) { index = skipStatementWhitespace( source, index, ); var branchType; if ( firstBranch && source.slice(index, index + 2) === "if" && !/[A-Za-z0-9_$]/.test( source.charAt(index + 2), ) ) { branchType = "if"; index += 2; } else if ( source.slice(index, index + 7) === "else if" && !/[A-Za-z0-9_$]/.test( source.charAt(index + 7), ) ) { branchType = "else-if"; index += 7; } else if ( source.slice(index, index + 4) === "else" && !/[A-Za-z0-9_$]/.test( source.charAt(index + 4), ) ) { branchType = "else"; index += 4; } else { break; } firstBranch = false; index = skipStatementWhitespace( source, index, ); var condition = null; if (branchType !== "else") { if (source.charAt(index) !== "(") { throw new Error( "Expected '(' after " + ( branchType === "if" ? "if" : "else if" ), ); } var conditionEnd = findClosingDelimiter( source, index, "(", ")", ); if (conditionEnd < 0) { throw new Error( "Unclosed " + ( branchType === "if" ? "if" : "else if" ) + " condition", ); } condition = source .slice( index + 1, conditionEnd, ) .trim(); index = skipStatementWhitespace( source, conditionEnd + 1, ); } if (source.charAt(index) !== "{") { throw new Error( "Expected '{' after " + branchType, ); } var bodyEnd = findClosingDelimiter( source, index, "{", "}", ); if (bodyEnd < 0) { throw new Error( "Unclosed " + branchType + " body", ); } branches.push({ condition: condition, body: source.slice( index + 1, bodyEnd, ), }); index = skipStatementWhitespace( source, bodyEnd + 1, ); if ( source.slice(index, index + 7) === "else if" || source.slice(index, index + 4) === "else" ) { continue; } break; } return branches.length > 0 ? branches : null; } function runStatement( stmt, evalExpr, read, write, runBlock, ) { stmt = String(stmt || "").trim(); if (!stmt) { return { returned: false, value: undefined, }; } var returnMatch = /^return(?:\s+([\s\S]+))?$/.exec( stmt, ); if (returnMatch) { return { returned: true, value: returnMatch[1] ? evalExpr(returnMatch[1]) : undefined, }; } var ifBranches = parseIfStatement(stmt); if (ifBranches) { for ( var branchIndex = 0; branchIndex < ifBranches.length; branchIndex++ ) { var branch = ifBranches[branchIndex]; if ( branch.condition === null || !!evalExpr(branch.condition) ) { return runBlock(branch.body); } } return { returned: false, value: undefined, }; } var increment = stmt.match( /^([A-Za-z_$][A-Za-z0-9_$]*)\s*(\+\+|--)$/, ); if (increment) { write( increment[1], Number(read(increment[1]) || 0) + (increment[2] === "++" ? 1 : -1), ); return { returned: false, value: undefined, }; } var assignment = stmt.match( /^([A-Za-z_$][A-Za-z0-9_$]*)\s*(\+=|-=|\*=|\/=|%=|=)\s*([\s\S]+)$/, ); if (!assignment) { evalExpr(stmt); return { returned: false, value: undefined, }; } var name = assignment[1]; var operator = assignment[2]; var nextValue = evalExpr(assignment[3]); var currentValue = read(name); if (operator === "+=") { nextValue = currentValue + nextValue; } else if (operator === "-=") { nextValue = Number(currentValue || 0) - Number(nextValue || 0); } else if (operator === "*=") { nextValue = Number(currentValue || 0) * Number(nextValue || 0); } else if (operator === "/=") { nextValue = Number(currentValue || 0) / Number(nextValue || 0); } else if (operator === "%=") { nextValue = Number(currentValue || 0) % Number(nextValue || 0); } write(name, nextValue); return { returned: false, value: nextValue, }; } function evaluateSpecialExpression( expression, read, ) { var source = String( expression || "", ).trim(); // Remove an optional trailing semicolon. if ( source.charAt( source.length - 1, ) === ";" ) { source = source .slice(0, -1) .trim(); } /* * Array literals containing spreads: * * [...openIndexes, index] * [first, ...otherItems] */ if ( source.charAt(0) === "[" && source.charAt( source.length - 1, ) === "]" && source.indexOf("...") !== -1 ) { var arraySource = source.slice( 1, -1, ); var arrayParts = splitTopLevel( arraySource, ",", ); var arrayResult = []; arrayParts.forEach(function ( part, ) { var partSource = part.trim(); if (!partSource) { return; } if ( partSource.slice(0, 3) === "..." ) { var spreadResult = evaluateExpression( partSource .slice(3) .trim(), read, ); if ( Array.isArray( spreadResult, ) ) { Array.prototype.push.apply( arrayResult, spreadResult, ); } return; } arrayResult.push( evaluateExpression( partSource, read, ), ); }); return { matched: true, value: arrayResult, }; } /* * First recognize the array method call without * trying to parse the callback in the same regex. */ var methodMatch = /^([\s\S]+)\.(filter|map|some|every|find|findIndex)\(\s*([\s\S]*)\s*\)$/.exec( source, ); if (!methodMatch) { return { matched: false, value: undefined, }; } var collectionSource = methodMatch[1].trim(); var method = methodMatch[2]; var callbackSource = methodMatch[3].trim(); /* * Supported callbacks: * * item => item.active * (item) => item.active * (item, index) => index > 0 */ var callbackMatch = /^(?:\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:,\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*)?\)|([A-Za-z_$][A-Za-z0-9_$]*))\s*=>\s*([\s\S]+)$/.exec( callbackSource, ); if (!callbackMatch) { return { matched: false, value: undefined, }; } var itemName = callbackMatch[1] || callbackMatch[3]; var indexName = callbackMatch[2]; var callbackBody = callbackMatch[4].trim(); /* * Support expression bodies wrapped in parentheses: * * item => ( * item.active * ) */ if ( callbackBody.charAt(0) === "(" && callbackBody.charAt( callbackBody.length - 1, ) === ")" ) { callbackBody = callbackBody .slice(1, -1) .trim(); } var collection = evaluateExpression( collectionSource, read, ); if (!Array.isArray(collection)) { collection = []; } var callback = function ( itemValue, itemIndex, ) { return evaluateExpression( callbackBody, function (name) { if (name === itemName) { return itemValue; } if ( indexName && name === indexName ) { return itemIndex; } return read(name); }, ); }; var result; if (method === "filter") { result = collection.filter(callback); } else if ( method === "map" ) { result = collection.map(callback); } else if ( method === "some" ) { result = collection.some(callback); } else if ( method === "every" ) { result = collection.every(callback); } else if ( method === "find" ) { result = collection.find(callback); } else { result = collection.findIndex( callback, ); } return { matched: true, value: result, }; } // A small, eval-free expression evaluator (so a strict CSP needs no // 'unsafe-eval'). Supports: literals, identifiers, member access (a.b, a[b]), // function/method calls, arrays, objects, arithmetic, comparison, equality, // logical (&& ||), unary (! - +), and the ternary operator. function evaluateExpression( expr, read, ) { expr = String( expr || "", ).trim(); var special = evaluateSpecialExpression( expr, read, ); if (special.matched) { return special.value; } var tokens = tokenizeExpression(expr); var index = 0; function peek() { return tokens[index]; } function next() { return tokens[index++]; } function is(v) { return peek() && peek().value === v; } function match(v) { if (is(v)) { index++; return true; } return false; } function expect(v) { if (!match(v)) throw new Error("Expected '" + v + "'"); } function parsePrimary() { var t = next(); if (!t) throw new Error("Unexpected end of expression"); if (t.type === "number" || t.type === "string") return { value: t.value }; if (t.type === "ident") { if (t.value === "true") return { value: true }; if (t.value === "false") return { value: false }; if (t.value === "null") return { value: null }; if (t.value === "undefined") return { value: undefined }; return { value: read(t.value) }; } if (t.value === "(") { var v = parseTernary(); expect(")"); return { value: v }; } if (t.value === "[") { var arr = []; if (!is("]")) { arr.push(parseTernary()); while (match(",")) arr.push(parseTernary()); } expect("]"); return { value: arr }; } if (t.value === "{") { var obj = {}; if (!is("}")) { do { var kt = next(); var key = kt.value; expect(":"); obj[key] = parseTernary(); } while (match(",")); } expect("}"); return { value: obj }; } throw new Error("Unexpected token '" + t.value + "'"); } function parsePostfix() { var node = parsePrimary(); for (;;) { if (match(".")) { var prop = next().value; node = { value: node.value == null ? undefined : node.value[prop], obj: node.value }; } else if (match("[")) { var key = parseTernary(); expect("]"); node = { value: node.value == null ? undefined : node.value[key], obj: node.value }; } else if (is("(")) { next(); var args = []; if (!is(")")) { args.push(parseTernary()); while (match(",")) args.push(parseTernary()); } expect(")"); var fn = node.value; node = { value: typeof fn === "function" ? fn.apply(node.obj, args) : undefined }; } else break; } return node; } function parseUnary() { if (match("!")) return !parseUnary(); if (match("-")) return -parseUnary(); if (match("+")) return +parseUnary(); return parsePostfix().value; } function parseMul() { var l = parseUnary(); while (peek() && (is("*") || is("/") || is("%"))) { var op = next().value, r = parseUnary(); l = op === "*" ? l * r : op === "/" ? l / r : l % r; } return l; } function parseAdd() { var l = parseMul(); while (peek() && (is("+") || is("-"))) { var op = next().value, r = parseMul(); l = op === "+" ? l + r : l - r; } return l; } function parseCmp() { var l = parseAdd(); while (peek() && (is("<") || is(">") || is("<=") || is(">="))) { var op = next().value, r = parseAdd(); l = op === "<" ? l < r : op === ">" ? l > r : op === "<=" ? l <= r : l >= r; } return l; } function parseEq() { var l = parseCmp(); while (peek() && (is("==") || is("!=") || is("===") || is("!=="))) { var op = next().value, r = parseCmp(); l = op === "==" ? l == r : op === "!=" ? l != r : op === "===" ? l === r : l !== r; } return l; } function parseAnd() { var l = parseEq(); while (match("&&")) l = l && parseEq(); return l; } function parseOr() { var l = parseAnd(); while (match("||")) l = l || parseAnd(); return l; } function parseTernary() { var c = parseOr(); if (match("?")) { var a = parseTernary(); expect(":"); var b = parseTernary(); return c ? a : b; } return c; } var value = parseTernary(); if (index < tokens.length) throw new Error("Unexpected token '" + tokens[index].value + "'"); return value; } function tokenizeExpression(input) { var tokens = []; var i = 0; while (i < input.length) { var ch = input[i]; if (/\s/.test(ch)) { i++; continue; } if (/[0-9]/.test(ch) || (ch === "." && /[0-9]/.test(input[i + 1]))) { var start = i++; while (i < input.length && /[0-9.]/.test(input[i])) i++; tokens.push({ type: "number", value: Number(input.slice(start, i)) }); continue; } if (ch === '"' || ch === "'") { var quote = ch, value = ""; i++; while (i < input.length) { ch = input[i++]; if (ch === quote) break; if (ch === "\\") { var esc = input[i++]; value += esc === "n" ? "\n" : esc === "t" ? "\t" : esc || ""; } else value += ch; } tokens.push({ type: "string", value: value }); continue; } if (/[A-Za-z_$]/.test(ch)) { var s = i++; while (i < input.length && /[A-Za-z0-9_$]/.test(input[i])) i++; tokens.push({ type: "ident", value: input.slice(s, i) }); continue; } var three = input.slice(i, i + 3); if ( three === "===" || three === "!==" ) { tokens.push({ type: "operator", value: three, }); i += 3; continue; } var two = input.slice(i, i + 2); if ( two === "==" || two === "!=" || two === ">=" || two === "<=" || two === "&&" || two === "||" ) { tokens.push({ type: "operator", value: two, }); i += 2; continue; } if ("()+-*/%!<>.,?:[]{}".indexOf(ch) !== -1) { tokens.push({ type: "op", value: ch }); i++; continue; } throw new Error("Unexpected character '" + ch + "'"); } return tokens; } function splitTopLevel(input, separator) { var parts = []; var start = 0; var depth = 0; var quote = ""; for (var i = 0; i < input.length; i++) { var ch = input[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'") { quote = ch; continue; } if (ch === "(" || ch === "[" || ch === "{") depth++; else if (ch === ")" || ch === "]" || ch === "}") depth--; else if (ch === separator && depth === 0) { parts.push(input.slice(start, i).trim()); start = i + 1; } } parts.push(input.slice(start).trim()); return parts.filter(Boolean); } function findTopLevel(input, needle) { var depth = 0; var quote = ""; for (var i = 0; i < input.length; i++) { var ch = input[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'") quote = ch; else if (ch === "(" || ch === "[" || ch === "{") depth++; else if (ch === ")" || ch === "]" || ch === "}") depth--; else if (ch === needle && depth === 0) return i; } return -1; } function safeCsrUrl(id) { try { if (!/^[A-Za-z0-9_-]+$/.test(id)) return null; return "/__wrnexus/csr?route=" + encodeURIComponent(location.pathname) + "&id=" + encodeURIComponent(id); } catch (_) { return null; } } function setupCsrFetch(el) { if (el.__wrnexusCsrFetch) return; el.__wrnexusCsrFetch = true; var id = el.getAttribute("data-wrnexus-csr") || ""; var url = safeCsrUrl(id); if (!url) { console.error("[wrnexus] blocked unsafe CSR binding '" + id + "'"); return; } var headers = { "accept": "text/plain" }; var storage = localStorageSnapshotHeader(); if (storage) headers["x-wrnexus-local-storage"] = storage; fetch(url, { headers: headers }) .then(function (res) { if (!res.ok) throw new Error("HTTP " + res.status); return res.text(); }) .then(function (text) { el.textContent = text; }) .catch(function (err) { console.error("[wrnexus] CSR fetch failed for binding '" + id + "'", err); el.textContent = "Failed to load"; }); } function hydrateCsrFetches(root) { (root || document).querySelectorAll("[data-wrnexus-csr]").forEach(setupCsrFetch); } function localStorageSnapshotHeader() { try { if (!("localStorage" in window)) return ""; var values = {}; for (var i = 0; i < window.localStorage.length; i++) { var key = window.localStorage.key(i); if (!key) continue; var value = window.localStorage.getItem(key); if (typeof value === "string") values[key] = value; } var encoded = encodeURIComponent(JSON.stringify(values)); return encoded.length <= 12000 ? encoded : ""; } catch (_) { return ""; } } window.__wrnexusHydrateScopes = hydrateScopes; window.__wrnexusHydrateCsrFetches = hydrateCsrFetches; window.__wrnexusDisposeBehaviors = disposeBehaviors; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { hydrateScopes(document); hydrateCsrFetches(document); }); } else { hydrateScopes(document); hydrateCsrFetches(document); } })(); `.trim();