Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Documents and demonstrates serializable CEK state. Type an expression, step to any point, click Freeze to see the frozen SX. Click Thaw to resume from the frozen state and get the result. - New page at /sx/(geography.(cek.freeze)) - Nav entry under CEK Machine - Interactive island demo with step/run/freeze/thaw buttons - Documentation: the idea, freeze format, thaw/resume, what it enables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3379 lines
126 KiB
Python
3379 lines
126 KiB
Python
"""
|
|
JS platform constants and functions for the SX bootstrap compiler.
|
|
|
|
This module contains all platform-specific JS code (string constants, helper
|
|
functions, and configuration dicts) shared by bootstrap_js.py and run_js_sx.py.
|
|
The JSEmitter class, compile_ref_to_js function, and main entry point remain
|
|
in bootstrap_js.py.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from shared.sx.parser import parse_all
|
|
from shared.sx.types import Symbol
|
|
|
|
|
|
def extract_defines(source: str) -> list[tuple[str, list]]:
|
|
"""Parse .sx source, return list of (name, define-expr) for top-level defines."""
|
|
exprs = parse_all(source)
|
|
defines = []
|
|
for expr in exprs:
|
|
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
|
|
if expr[0].name == "define":
|
|
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
|
defines.append((name, expr))
|
|
return defines
|
|
|
|
ADAPTER_FILES = {
|
|
"parser": ("parser.sx", "parser"),
|
|
"html": ("adapter-html.sx", "adapter-html"),
|
|
"sx": ("adapter-sx.sx", "adapter-sx"),
|
|
"dom": ("adapter-dom.sx", "adapter-dom"),
|
|
"engine": ("engine.sx", "engine"),
|
|
"orchestration": ("orchestration.sx","orchestration"),
|
|
"boot": ("boot.sx", "boot"),
|
|
}
|
|
|
|
# Dependencies
|
|
ADAPTER_DEPS = {
|
|
"engine": ["dom"],
|
|
"orchestration": ["engine", "dom"],
|
|
"boot": ["dom", "engine", "orchestration", "parser"],
|
|
"parser": [],
|
|
}
|
|
|
|
SPEC_MODULES = {
|
|
"deps": ("deps.sx", "deps (component dependency analysis)"),
|
|
"router": ("router.sx", "router (client-side route matching)"),
|
|
"signals": ("signals.sx", "signals (reactive signal runtime)"),
|
|
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
|
|
"frames": ("frames.sx", "frames (CEK continuation frames)"),
|
|
"cek": ("cek.sx", "cek (explicit CEK machine evaluator)"),
|
|
}
|
|
|
|
# Explicit ordering for spec modules with dependencies.
|
|
# Modules listed here are emitted in this order; any not listed use alphabetical.
|
|
SPEC_MODULE_ORDER = ["deps", "frames", "page-helpers", "router", "cek", "signals"]
|
|
|
|
|
|
EXTENSION_NAMES = {"continuations"}
|
|
CONTINUATIONS_JS = '''
|
|
// =========================================================================
|
|
// Extension: Delimited continuations (shift/reset)
|
|
// =========================================================================
|
|
|
|
function Continuation(fn) { this.fn = fn; }
|
|
Continuation.prototype._continuation = true;
|
|
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
|
|
|
|
function ShiftSignal(kName, body, env) {
|
|
this.kName = kName;
|
|
this.body = body;
|
|
this.env = env;
|
|
}
|
|
|
|
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
|
|
|
|
var _resetResume = [];
|
|
|
|
function sfReset(args, env) {
|
|
var body = args[0];
|
|
try {
|
|
return trampoline(evalExpr(body, env));
|
|
} catch (e) {
|
|
if (e instanceof ShiftSignal) {
|
|
var sig = e;
|
|
var cont = new Continuation(function(value) {
|
|
if (value === undefined) value = NIL;
|
|
_resetResume.push(value);
|
|
try {
|
|
return trampoline(evalExpr(body, env));
|
|
} finally {
|
|
_resetResume.pop();
|
|
}
|
|
});
|
|
var sigEnv = merge(sig.env);
|
|
sigEnv[sig.kName] = cont;
|
|
return trampoline(evalExpr(sig.body, sigEnv));
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function sfShift(args, env) {
|
|
if (_resetResume.length > 0) {
|
|
return _resetResume[_resetResume.length - 1];
|
|
}
|
|
var kName = symbolName(args[0]);
|
|
var body = args[1];
|
|
throw new ShiftSignal(kName, body, env);
|
|
}
|
|
|
|
// Wrap evalList to intercept reset/shift
|
|
var _baseEvalList = evalList;
|
|
evalList = function(expr, env) {
|
|
var head = expr[0];
|
|
if (isSym(head)) {
|
|
var name = head.name;
|
|
if (name === "reset") return sfReset(expr.slice(1), env);
|
|
if (name === "shift") return sfShift(expr.slice(1), env);
|
|
}
|
|
return _baseEvalList(expr, env);
|
|
};
|
|
|
|
// Wrap aserSpecial to handle reset/shift in SX wire mode
|
|
if (typeof aserSpecial === "function") {
|
|
var _baseAserSpecial = aserSpecial;
|
|
aserSpecial = function(name, expr, env) {
|
|
if (name === "reset") return sfReset(expr.slice(1), env);
|
|
if (name === "shift") return sfShift(expr.slice(1), env);
|
|
return _baseAserSpecial(name, expr, env);
|
|
};
|
|
}
|
|
|
|
// Wrap typeOf to recognize continuations
|
|
var _baseTypeOf = typeOf;
|
|
typeOf = function(x) {
|
|
if (x != null && x._continuation) return "continuation";
|
|
return _baseTypeOf(x);
|
|
};
|
|
'''
|
|
|
|
ASYNC_IO_JS = '''
|
|
// =========================================================================
|
|
// Async IO: Promise-aware rendering for client-side IO primitives
|
|
// =========================================================================
|
|
//
|
|
// IO primitives (query, current-user, etc.) return Promises on the client.
|
|
// asyncRenderToDom walks the component tree; when it encounters an IO
|
|
// primitive, it awaits the Promise and continues rendering.
|
|
//
|
|
// The sync evaluator/renderer is untouched. This is a separate async path
|
|
// used only when a page's component tree contains IO references.
|
|
|
|
var IO_PRIMITIVES = {};
|
|
|
|
function registerIoPrimitive(name, fn) {
|
|
IO_PRIMITIVES[name] = fn;
|
|
}
|
|
|
|
function isPromise(x) {
|
|
return x != null && typeof x === "object" && typeof x.then === "function";
|
|
}
|
|
|
|
// Async trampoline: resolves thunks, awaits Promises
|
|
function asyncTrampoline(val) {
|
|
if (isPromise(val)) return val.then(asyncTrampoline);
|
|
if (isThunk(val)) return asyncTrampoline(evalExpr(thunkExpr(val), thunkEnv(val)));
|
|
return val;
|
|
}
|
|
|
|
// Async eval: like trampoline(evalExpr(...)) but handles IO primitives
|
|
function asyncEval(expr, env) {
|
|
// Intercept IO primitive calls at the AST level
|
|
if (Array.isArray(expr) && expr.length > 0) {
|
|
var head = expr[0];
|
|
if (head && head._sym) {
|
|
var name = head.name;
|
|
if (IO_PRIMITIVES[name]) {
|
|
// Evaluate args, then call the IO primitive
|
|
return asyncEvalIoCall(name, expr.slice(1), env);
|
|
}
|
|
}
|
|
}
|
|
// Non-IO: use sync eval, but result might be a thunk
|
|
var result = evalExpr(expr, env);
|
|
return asyncTrampoline(result);
|
|
}
|
|
|
|
function asyncEvalIoCall(name, rawArgs, env) {
|
|
// Parse keyword args and positional args, evaluating each (may be async)
|
|
var kwargs = {};
|
|
var args = [];
|
|
var promises = [];
|
|
var i = 0;
|
|
while (i < rawArgs.length) {
|
|
var arg = rawArgs[i];
|
|
if (arg && arg._kw && (i + 1) < rawArgs.length) {
|
|
var kName = arg.name;
|
|
var kVal = asyncEval(rawArgs[i + 1], env);
|
|
if (isPromise(kVal)) {
|
|
(function(k) { promises.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName);
|
|
} else {
|
|
kwargs[kName] = kVal;
|
|
}
|
|
i += 2;
|
|
} else {
|
|
var aVal = asyncEval(arg, env);
|
|
if (isPromise(aVal)) {
|
|
(function(idx) { promises.push(aVal.then(function(v) { args[idx] = v; })); })(args.length);
|
|
args.push(null); // placeholder
|
|
} else {
|
|
args.push(aVal);
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
var ioFn = IO_PRIMITIVES[name];
|
|
if (promises.length > 0) {
|
|
return Promise.all(promises).then(function() { return ioFn(args, kwargs); });
|
|
}
|
|
return ioFn(args, kwargs);
|
|
}
|
|
|
|
// Async render-to-dom: returns Promise<Node> or Node
|
|
function asyncRenderToDom(expr, env, ns) {
|
|
// Literals
|
|
if (expr === NIL || expr === null || expr === undefined) return null;
|
|
if (expr === true || expr === false) return null;
|
|
if (typeof expr === "string") return document.createTextNode(expr);
|
|
if (typeof expr === "number") return document.createTextNode(String(expr));
|
|
|
|
// Symbol -> async eval then render
|
|
if (expr && expr._sym) {
|
|
var val = asyncEval(expr, env);
|
|
if (isPromise(val)) return val.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
|
return asyncRenderToDom(val, env, ns);
|
|
}
|
|
|
|
// Keyword
|
|
if (expr && expr._kw) return document.createTextNode(expr.name);
|
|
|
|
// DocumentFragment / DOM nodes pass through
|
|
if (expr instanceof DocumentFragment || (expr && expr.nodeType)) return expr;
|
|
|
|
// Dict -> skip
|
|
if (expr && typeof expr === "object" && !Array.isArray(expr)) return null;
|
|
|
|
// List
|
|
if (!Array.isArray(expr) || expr.length === 0) return null;
|
|
|
|
var head = expr[0];
|
|
if (!head) return null;
|
|
|
|
// Symbol head
|
|
if (head._sym) {
|
|
var hname = head.name;
|
|
|
|
// IO primitive
|
|
if (IO_PRIMITIVES[hname]) {
|
|
var ioResult = asyncEval(expr, env);
|
|
if (isPromise(ioResult)) return ioResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
|
return asyncRenderToDom(ioResult, env, ns);
|
|
}
|
|
|
|
// Fragment
|
|
if (hname === "<>") return asyncRenderChildren(expr.slice(1), env, ns);
|
|
|
|
// raw!
|
|
if (hname === "raw!") {
|
|
return asyncEvalRaw(expr.slice(1), env);
|
|
}
|
|
|
|
// Special forms that need async handling
|
|
if (hname === "if") return asyncRenderIf(expr, env, ns);
|
|
if (hname === "when") return asyncRenderWhen(expr, env, ns);
|
|
if (hname === "cond") return asyncRenderCond(expr, env, ns);
|
|
if (hname === "case") return asyncRenderCase(expr, env, ns);
|
|
if (hname === "let" || hname === "let*") return asyncRenderLet(expr, env, ns);
|
|
if (hname === "begin" || hname === "do") return asyncRenderChildren(expr.slice(1), env, ns);
|
|
if (hname === "map") return asyncRenderMap(expr, env, ns);
|
|
if (hname === "map-indexed") return asyncRenderMapIndexed(expr, env, ns);
|
|
if (hname === "for-each") return asyncRenderMap(expr, env, ns);
|
|
|
|
// define/defcomp/defmacro — eval for side effects
|
|
if (hname === "define" || hname === "defcomp" || hname === "defmacro" ||
|
|
hname === "defstyle" || hname === "defhandler") {
|
|
trampoline(evalExpr(expr, env));
|
|
return null;
|
|
}
|
|
|
|
// quote
|
|
if (hname === "quote") return null;
|
|
|
|
// lambda/fn
|
|
if (hname === "lambda" || hname === "fn") {
|
|
trampoline(evalExpr(expr, env));
|
|
return null;
|
|
}
|
|
|
|
// and/or — eval and render result
|
|
if (hname === "and" || hname === "or" || hname === "->") {
|
|
var aoResult = asyncEval(expr, env);
|
|
if (isPromise(aoResult)) return aoResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
|
return asyncRenderToDom(aoResult, env, ns);
|
|
}
|
|
|
|
// set!
|
|
if (hname === "set!") {
|
|
asyncEval(expr, env);
|
|
return null;
|
|
}
|
|
|
|
// Component or Island
|
|
if (hname.charAt(0) === "~") {
|
|
var comp = env[hname];
|
|
if (comp && comp._island) return renderDomIsland(comp, expr.slice(1), env, ns);
|
|
if (comp && comp._component) return asyncRenderComponent(comp, expr.slice(1), env, ns);
|
|
if (comp && comp._macro) {
|
|
var expanded = trampoline(expandMacro(comp, expr.slice(1), env));
|
|
return asyncRenderToDom(expanded, env, ns);
|
|
}
|
|
}
|
|
|
|
// Macro
|
|
if (env[hname] && env[hname]._macro) {
|
|
var mac = env[hname];
|
|
var expanded = trampoline(expandMacro(mac, expr.slice(1), env));
|
|
return asyncRenderToDom(expanded, env, ns);
|
|
}
|
|
|
|
// HTML tag
|
|
if (typeof renderDomElement === "function" && contains(HTML_TAGS, hname)) {
|
|
return asyncRenderElement(hname, expr.slice(1), env, ns);
|
|
}
|
|
|
|
// html: prefix
|
|
if (hname.indexOf("html:") === 0) {
|
|
return asyncRenderElement(hname.slice(5), expr.slice(1), env, ns);
|
|
}
|
|
|
|
// Custom element
|
|
if (hname.indexOf("-") >= 0 && expr.length > 1 && expr[1] && expr[1]._kw) {
|
|
return asyncRenderElement(hname, expr.slice(1), env, ns);
|
|
}
|
|
|
|
// SVG context
|
|
if (ns) return asyncRenderElement(hname, expr.slice(1), env, ns);
|
|
|
|
// Fallback: eval and render
|
|
var fResult = asyncEval(expr, env);
|
|
if (isPromise(fResult)) return fResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
|
return asyncRenderToDom(fResult, env, ns);
|
|
}
|
|
|
|
// Non-symbol head: eval call
|
|
var cResult = asyncEval(expr, env);
|
|
if (isPromise(cResult)) return cResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
|
|
return asyncRenderToDom(cResult, env, ns);
|
|
}
|
|
|
|
function asyncRenderChildren(exprs, env, ns) {
|
|
var frag = document.createDocumentFragment();
|
|
var pending = [];
|
|
for (var i = 0; i < exprs.length; i++) {
|
|
var result = asyncRenderToDom(exprs[i], env, ns);
|
|
if (isPromise(result)) {
|
|
// Insert placeholder, replace when resolved
|
|
var placeholder = document.createComment("async");
|
|
frag.appendChild(placeholder);
|
|
(function(ph) {
|
|
pending.push(result.then(function(node) {
|
|
if (node) ph.parentNode.replaceChild(node, ph);
|
|
else ph.parentNode.removeChild(ph);
|
|
}));
|
|
})(placeholder);
|
|
} else if (result && !result._spread) {
|
|
frag.appendChild(result);
|
|
}
|
|
}
|
|
if (pending.length > 0) {
|
|
return Promise.all(pending).then(function() { return frag; });
|
|
}
|
|
return frag;
|
|
}
|
|
|
|
function asyncRenderElement(tag, args, env, ns) {
|
|
var newNs = tag === "svg" ? SVG_NS : tag === "math" ? MATH_NS : ns;
|
|
var el = domCreateElement(tag, newNs);
|
|
var pending = [];
|
|
var isVoid = contains(VOID_ELEMENTS, tag);
|
|
for (var i = 0; i < args.length; i++) {
|
|
var arg = args[i];
|
|
if (arg && arg._kw && (i + 1) < args.length) {
|
|
var attrName = arg.name;
|
|
var attrVal = asyncEval(args[i + 1], env);
|
|
i++;
|
|
if (isPromise(attrVal)) {
|
|
(function(an, av) {
|
|
pending.push(av.then(function(v) {
|
|
if (!isNil(v) && v !== false) {
|
|
if (contains(BOOLEAN_ATTRS, an)) { if (isSxTruthy(v)) el.setAttribute(an, ""); }
|
|
else if (v === true) el.setAttribute(an, "");
|
|
else el.setAttribute(an, String(v));
|
|
}
|
|
}));
|
|
})(attrName, attrVal);
|
|
} else {
|
|
if (!isNil(attrVal) && attrVal !== false) {
|
|
if (contains(BOOLEAN_ATTRS, attrName)) {
|
|
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
|
|
} else if (attrVal === true) {
|
|
el.setAttribute(attrName, "");
|
|
} else {
|
|
el.setAttribute(attrName, String(attrVal));
|
|
}
|
|
}
|
|
}
|
|
} else if (!isVoid) {
|
|
var child = asyncRenderToDom(arg, env, newNs);
|
|
if (isPromise(child)) {
|
|
var placeholder = document.createComment("async");
|
|
el.appendChild(placeholder);
|
|
(function(ph) {
|
|
pending.push(child.then(function(node) {
|
|
if (node) ph.parentNode.replaceChild(node, ph);
|
|
else ph.parentNode.removeChild(ph);
|
|
}));
|
|
})(placeholder);
|
|
} else if (child) {
|
|
if (child._spread) {
|
|
// Spread: merge attrs onto parent element
|
|
var sa = child.attrs || {};
|
|
for (var sk in sa) {
|
|
if (sk === "class") {
|
|
var ec = el.getAttribute("class") || "";
|
|
el.setAttribute("class", ec ? ec + " " + sa[sk] : sa[sk]);
|
|
} else if (sk === "style") {
|
|
var es = el.getAttribute("style") || "";
|
|
el.setAttribute("style", es ? es + ";" + sa[sk] : sa[sk]);
|
|
} else {
|
|
el.setAttribute(sk, String(sa[sk]));
|
|
}
|
|
}
|
|
} else {
|
|
el.appendChild(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (pending.length > 0) return Promise.all(pending).then(function() { return el; });
|
|
return el;
|
|
}
|
|
|
|
function asyncRenderComponent(comp, args, env, ns) {
|
|
var kwargs = {};
|
|
var children = [];
|
|
var pending = [];
|
|
for (var i = 0; i < args.length; i++) {
|
|
var arg = args[i];
|
|
if (arg && arg._kw && (i + 1) < args.length) {
|
|
var kName = arg.name;
|
|
var kVal = asyncEval(args[i + 1], env);
|
|
if (isPromise(kVal)) {
|
|
(function(k) { pending.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName);
|
|
} else {
|
|
kwargs[kName] = kVal;
|
|
}
|
|
i++;
|
|
} else {
|
|
children.push(arg);
|
|
}
|
|
}
|
|
|
|
function doRender() {
|
|
var local = Object.create(componentClosure(comp));
|
|
for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k];
|
|
var params = componentParams(comp);
|
|
for (var j = 0; j < params.length; j++) {
|
|
local[params[j]] = params[j] in kwargs ? kwargs[params[j]] : NIL;
|
|
}
|
|
if (componentHasChildren(comp)) {
|
|
var childResult = asyncRenderChildren(children, env, ns);
|
|
if (isPromise(childResult)) {
|
|
return childResult.then(function(childFrag) {
|
|
local["children"] = childFrag;
|
|
return asyncRenderToDom(componentBody(comp), local, ns);
|
|
});
|
|
}
|
|
local["children"] = childResult;
|
|
}
|
|
return asyncRenderToDom(componentBody(comp), local, ns);
|
|
}
|
|
|
|
if (pending.length > 0) return Promise.all(pending).then(doRender);
|
|
return doRender();
|
|
}
|
|
|
|
function asyncRenderIf(expr, env, ns) {
|
|
var cond = asyncEval(expr[1], env);
|
|
if (isPromise(cond)) {
|
|
return cond.then(function(v) {
|
|
return isSxTruthy(v)
|
|
? asyncRenderToDom(expr[2], env, ns)
|
|
: (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null);
|
|
});
|
|
}
|
|
return isSxTruthy(cond)
|
|
? asyncRenderToDom(expr[2], env, ns)
|
|
: (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null);
|
|
}
|
|
|
|
function asyncRenderWhen(expr, env, ns) {
|
|
var cond = asyncEval(expr[1], env);
|
|
if (isPromise(cond)) {
|
|
return cond.then(function(v) {
|
|
return isSxTruthy(v) ? asyncRenderChildren(expr.slice(2), env, ns) : null;
|
|
});
|
|
}
|
|
return isSxTruthy(cond) ? asyncRenderChildren(expr.slice(2), env, ns) : null;
|
|
}
|
|
|
|
function asyncRenderCond(expr, env, ns) {
|
|
var clauses = expr.slice(1);
|
|
function step(idx) {
|
|
if (idx >= clauses.length) return null;
|
|
var clause = clauses[idx];
|
|
if (!Array.isArray(clause) || clause.length < 2) return step(idx + 1);
|
|
var test = clause[0];
|
|
if ((test && test._sym && (test.name === "else" || test.name === ":else")) ||
|
|
(test && test._kw && test.name === "else")) {
|
|
return asyncRenderToDom(clause[1], env, ns);
|
|
}
|
|
var v = asyncEval(test, env);
|
|
if (isPromise(v)) return v.then(function(r) { return isSxTruthy(r) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1); });
|
|
return isSxTruthy(v) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1);
|
|
}
|
|
return step(0);
|
|
}
|
|
|
|
function asyncRenderCase(expr, env, ns) {
|
|
var matchVal = asyncEval(expr[1], env);
|
|
function doCase(mv) {
|
|
var clauses = expr.slice(2);
|
|
for (var i = 0; i < clauses.length - 1; i += 2) {
|
|
var test = clauses[i];
|
|
if ((test && test._kw && test.name === "else") ||
|
|
(test && test._sym && (test.name === "else" || test.name === ":else"))) {
|
|
return asyncRenderToDom(clauses[i + 1], env, ns);
|
|
}
|
|
var tv = trampoline(evalExpr(test, env));
|
|
if (mv === tv || (typeof mv === "string" && typeof tv === "string" && mv === tv)) {
|
|
return asyncRenderToDom(clauses[i + 1], env, ns);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
if (isPromise(matchVal)) return matchVal.then(doCase);
|
|
return doCase(matchVal);
|
|
}
|
|
|
|
function asyncRenderLet(expr, env, ns) {
|
|
var bindings = expr[1];
|
|
var local = Object.create(env);
|
|
for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k];
|
|
function bindStep(idx) {
|
|
if (!Array.isArray(bindings)) return asyncRenderChildren(expr.slice(2), local, ns);
|
|
// Nested pairs: ((a 1) (b 2))
|
|
if (bindings.length > 0 && Array.isArray(bindings[0])) {
|
|
if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns);
|
|
var b = bindings[idx];
|
|
var vname = b[0]._sym ? b[0].name : String(b[0]);
|
|
var val = asyncEval(b[1], local);
|
|
if (isPromise(val)) return val.then(function(v) { local[vname] = v; return bindStep(idx + 1); });
|
|
local[vname] = val;
|
|
return bindStep(idx + 1);
|
|
}
|
|
// Flat pairs: (a 1 b 2)
|
|
if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns);
|
|
var vn = bindings[idx]._sym ? bindings[idx].name : String(bindings[idx]);
|
|
var vv = asyncEval(bindings[idx + 1], local);
|
|
if (isPromise(vv)) return vv.then(function(v) { local[vn] = v; return bindStep(idx + 2); });
|
|
local[vn] = vv;
|
|
return bindStep(idx + 2);
|
|
}
|
|
return bindStep(0);
|
|
}
|
|
|
|
function asyncRenderMap(expr, env, ns) {
|
|
var fn = asyncEval(expr[1], env);
|
|
var coll = asyncEval(expr[2], env);
|
|
function doMap(f, c) {
|
|
if (!Array.isArray(c)) return null;
|
|
var frag = document.createDocumentFragment();
|
|
var pending = [];
|
|
for (var i = 0; i < c.length; i++) {
|
|
var item = c[i];
|
|
var result;
|
|
if (f && f._lambda) {
|
|
var lenv = Object.create(f.closure || env);
|
|
for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k];
|
|
lenv[f.params[0]] = item;
|
|
result = asyncRenderToDom(f.body, lenv, null);
|
|
} else if (typeof f === "function") {
|
|
var r = f(item);
|
|
result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null);
|
|
} else {
|
|
result = asyncRenderToDom(item, env, null);
|
|
}
|
|
if (isPromise(result)) {
|
|
var ph = document.createComment("async");
|
|
frag.appendChild(ph);
|
|
(function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph);
|
|
} else if (result && !result._spread) {
|
|
frag.appendChild(result);
|
|
}
|
|
}
|
|
if (pending.length) return Promise.all(pending).then(function() { return frag; });
|
|
return frag;
|
|
}
|
|
if (isPromise(fn) || isPromise(coll)) {
|
|
return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)])
|
|
.then(function(r) { return doMap(r[0], r[1]); });
|
|
}
|
|
return doMap(fn, coll);
|
|
}
|
|
|
|
function asyncRenderMapIndexed(expr, env, ns) {
|
|
var fn = asyncEval(expr[1], env);
|
|
var coll = asyncEval(expr[2], env);
|
|
function doMap(f, c) {
|
|
if (!Array.isArray(c)) return null;
|
|
var frag = document.createDocumentFragment();
|
|
var pending = [];
|
|
for (var i = 0; i < c.length; i++) {
|
|
var item = c[i];
|
|
var result;
|
|
if (f && f._lambda) {
|
|
var lenv = Object.create(f.closure || env);
|
|
for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k];
|
|
lenv[f.params[0]] = i;
|
|
lenv[f.params[1]] = item;
|
|
result = asyncRenderToDom(f.body, lenv, null);
|
|
} else if (typeof f === "function") {
|
|
var r = f(i, item);
|
|
result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null);
|
|
} else {
|
|
result = asyncRenderToDom(item, env, null);
|
|
}
|
|
if (isPromise(result)) {
|
|
var ph = document.createComment("async");
|
|
frag.appendChild(ph);
|
|
(function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph);
|
|
} else if (result && !result._spread) {
|
|
frag.appendChild(result);
|
|
}
|
|
}
|
|
if (pending.length) return Promise.all(pending).then(function() { return frag; });
|
|
return frag;
|
|
}
|
|
if (isPromise(fn) || isPromise(coll)) {
|
|
return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)])
|
|
.then(function(r) { return doMap(r[0], r[1]); });
|
|
}
|
|
return doMap(fn, coll);
|
|
}
|
|
|
|
function asyncEvalRaw(args, env) {
|
|
var parts = [];
|
|
var pending = [];
|
|
for (var i = 0; i < args.length; i++) {
|
|
var val = asyncEval(args[i], env);
|
|
if (isPromise(val)) {
|
|
(function(idx) {
|
|
pending.push(val.then(function(v) { parts[idx] = v; }));
|
|
})(parts.length);
|
|
parts.push(null);
|
|
} else {
|
|
parts.push(val);
|
|
}
|
|
}
|
|
function assemble() {
|
|
var html = "";
|
|
for (var j = 0; j < parts.length; j++) {
|
|
var p = parts[j];
|
|
if (p && p._rawHtml) html += p.html;
|
|
else if (typeof p === "string") html += p;
|
|
else if (p != null && !isNil(p)) html += String(p);
|
|
}
|
|
var el = document.createElement("span");
|
|
el.innerHTML = html;
|
|
var frag = document.createDocumentFragment();
|
|
while (el.firstChild) frag.appendChild(el.firstChild);
|
|
return frag;
|
|
}
|
|
if (pending.length) return Promise.all(pending).then(assemble);
|
|
return assemble();
|
|
}
|
|
|
|
// Async version of sxRenderWithEnv — returns Promise<DocumentFragment>
|
|
function asyncSxRenderWithEnv(source, extraEnv) {
|
|
var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
|
|
var exprs = parse(source);
|
|
if (!_hasDom) return Promise.resolve(null);
|
|
return asyncRenderChildren(exprs, env, null);
|
|
}
|
|
|
|
// IO proxy cache: key → { value, expires }
|
|
var _ioCache = {};
|
|
var IO_CACHE_TTL = 300000; // 5 minutes
|
|
|
|
// Register a server-proxied IO primitive: fetches from /sx/io/<name>
|
|
// Uses GET for short args, POST for long payloads (URL length safety).
|
|
// Results are cached client-side by (name + args) with a TTL.
|
|
function registerProxiedIo(name) {
|
|
registerIoPrimitive(name, function(args, kwargs) {
|
|
// Cache key: name + serialized args
|
|
var cacheKey = name;
|
|
for (var ci = 0; ci < args.length; ci++) cacheKey += "\0" + String(args[ci]);
|
|
for (var ck in kwargs) {
|
|
if (kwargs.hasOwnProperty(ck)) cacheKey += "\0" + ck + "=" + String(kwargs[ck]);
|
|
}
|
|
var cached = _ioCache[cacheKey];
|
|
if (cached && cached.expires > Date.now()) return cached.value;
|
|
|
|
var url = "/sx/io/" + encodeURIComponent(name);
|
|
var qs = [];
|
|
for (var i = 0; i < args.length; i++) {
|
|
qs.push("_arg" + i + "=" + encodeURIComponent(String(args[i])));
|
|
}
|
|
for (var k in kwargs) {
|
|
if (kwargs.hasOwnProperty(k)) {
|
|
qs.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(kwargs[k])));
|
|
}
|
|
}
|
|
var queryStr = qs.join("&");
|
|
var fetchOpts;
|
|
if (queryStr.length > 1500) {
|
|
// POST with JSON body for long payloads
|
|
var sArgs = [];
|
|
for (var j = 0; j < args.length; j++) sArgs.push(String(args[j]));
|
|
var sKwargs = {};
|
|
for (var kk in kwargs) {
|
|
if (kwargs.hasOwnProperty(kk)) sKwargs[kk] = String(kwargs[kk]);
|
|
}
|
|
var postHeaders = { "SX-Request": "true", "Content-Type": "application/json" };
|
|
var csrf = csrfToken();
|
|
if (csrf && csrf !== NIL) postHeaders["X-CSRFToken"] = csrf;
|
|
fetchOpts = {
|
|
method: "POST",
|
|
headers: postHeaders,
|
|
body: JSON.stringify({ args: sArgs, kwargs: sKwargs })
|
|
};
|
|
} else {
|
|
if (queryStr) url += "?" + queryStr;
|
|
fetchOpts = { headers: { "SX-Request": "true" } };
|
|
}
|
|
var result = fetch(url, fetchOpts)
|
|
.then(function(resp) {
|
|
if (!resp.ok) {
|
|
logWarn("sx:io " + name + " failed " + resp.status);
|
|
return NIL;
|
|
}
|
|
return resp.text();
|
|
})
|
|
.then(function(text) {
|
|
if (!text || text === "nil") return NIL;
|
|
try {
|
|
var exprs = parse(text);
|
|
var val = exprs.length === 1 ? exprs[0] : exprs;
|
|
_ioCache[cacheKey] = { value: val, expires: Date.now() + IO_CACHE_TTL };
|
|
return val;
|
|
} catch (e) {
|
|
logWarn("sx:io " + name + " parse error: " + (e && e.message ? e.message : e));
|
|
return NIL;
|
|
}
|
|
})
|
|
.catch(function(e) {
|
|
logWarn("sx:io " + name + " network error: " + (e && e.message ? e.message : e));
|
|
return NIL;
|
|
});
|
|
// Cache the in-flight promise too (dedup concurrent calls for same args)
|
|
_ioCache[cacheKey] = { value: result, expires: Date.now() + IO_CACHE_TTL };
|
|
return result;
|
|
});
|
|
}
|
|
|
|
// Register IO deps as proxied primitives (idempotent, called per-page)
|
|
function registerIoDeps(names) {
|
|
if (!names || !names.length) return;
|
|
var registered = 0;
|
|
for (var i = 0; i < names.length; i++) {
|
|
var name = names[i];
|
|
if (!IO_PRIMITIVES[name]) {
|
|
registerProxiedIo(name);
|
|
registered++;
|
|
}
|
|
}
|
|
if (registered > 0) {
|
|
logInfo("sx:io registered " + registered + " proxied primitives: " + names.join(", "));
|
|
}
|
|
}
|
|
'''
|
|
|
|
PREAMBLE = '''\
|
|
/**
|
|
* sx-ref.js — Generated from reference SX evaluator specification.
|
|
*
|
|
* Bootstrap-compiled from shared/sx/ref/{eval,render,primitives}.sx
|
|
* Compare against hand-written sx.js for correctness verification.
|
|
*
|
|
* DO NOT EDIT — regenerate with: python bootstrap_js.py
|
|
*/
|
|
;(function(global) {
|
|
"use strict";
|
|
|
|
// =========================================================================
|
|
// Types
|
|
// =========================================================================
|
|
|
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
|
var SX_VERSION = "BUILD_TIMESTAMP";
|
|
|
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
|
|
|
function Symbol(name) { this.name = name; }
|
|
Symbol.prototype.toString = function() { return this.name; };
|
|
Symbol.prototype._sym = true;
|
|
|
|
function Keyword(name) { this.name = name; }
|
|
Keyword.prototype.toString = function() { return ":" + this.name; };
|
|
Keyword.prototype._kw = true;
|
|
|
|
function Lambda(params, body, closure, name) {
|
|
this.params = params;
|
|
this.body = body;
|
|
this.closure = closure || {};
|
|
this.name = name || null;
|
|
}
|
|
Lambda.prototype._lambda = true;
|
|
|
|
function Component(name, params, hasChildren, body, closure, affinity) {
|
|
this.name = name;
|
|
this.params = params;
|
|
this.hasChildren = hasChildren;
|
|
this.body = body;
|
|
this.closure = closure || {};
|
|
this.affinity = affinity || "auto";
|
|
}
|
|
Component.prototype._component = true;
|
|
|
|
function Island(name, params, hasChildren, body, closure) {
|
|
this.name = name;
|
|
this.params = params;
|
|
this.hasChildren = hasChildren;
|
|
this.body = body;
|
|
this.closure = closure || {};
|
|
}
|
|
Island.prototype._island = true;
|
|
|
|
function Macro(params, restParam, body, closure, name) {
|
|
this.params = params;
|
|
this.restParam = restParam;
|
|
this.body = body;
|
|
this.closure = closure || {};
|
|
this.name = name || null;
|
|
}
|
|
Macro.prototype._macro = true;
|
|
|
|
function Thunk(expr, env) { this.expr = expr; this.env = env; }
|
|
Thunk.prototype._thunk = true;
|
|
|
|
function RawHTML(html) { this.html = html; }
|
|
RawHTML.prototype._raw = true;
|
|
|
|
function SxSpread(attrs) { this.attrs = attrs || {}; }
|
|
SxSpread.prototype._spread = true;
|
|
|
|
var _scopeStacks = {};
|
|
|
|
function isSym(x) { return x != null && x._sym === true; }
|
|
function isKw(x) { return x != null && x._kw === true; }
|
|
|
|
function merge() {
|
|
var out = {};
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
var d = arguments[i];
|
|
if (d) for (var k in d) out[k] = d[k];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function sxOr() {
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
if (isSxTruthy(arguments[i])) return arguments[i];
|
|
}
|
|
return arguments.length ? arguments[arguments.length - 1] : false;
|
|
}'''
|
|
PRIMITIVES_JS_MODULES: dict[str, str] = {
|
|
"core.arithmetic": '''
|
|
// core.arithmetic
|
|
PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; };
|
|
PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; };
|
|
PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; };
|
|
PRIMITIVES["/"] = function(a, b) { return a / b; };
|
|
PRIMITIVES["mod"] = function(a, b) { return a % b; };
|
|
PRIMITIVES["inc"] = function(n) { return n + 1; };
|
|
PRIMITIVES["dec"] = function(n) { return n - 1; };
|
|
PRIMITIVES["abs"] = Math.abs;
|
|
PRIMITIVES["floor"] = Math.floor;
|
|
PRIMITIVES["ceil"] = Math.ceil;
|
|
PRIMITIVES["round"] = function(x, n) {
|
|
if (n === undefined || n === 0) return Math.round(x);
|
|
var f = Math.pow(10, n); return Math.round(x * f) / f;
|
|
};
|
|
PRIMITIVES["min"] = Math.min;
|
|
PRIMITIVES["max"] = Math.max;
|
|
PRIMITIVES["sqrt"] = Math.sqrt;
|
|
PRIMITIVES["pow"] = Math.pow;
|
|
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
|
|
''',
|
|
|
|
"core.comparison": '''
|
|
// core.comparison
|
|
PRIMITIVES["="] = function(a, b) { return a === b; };
|
|
PRIMITIVES["!="] = function(a, b) { return a !== b; };
|
|
PRIMITIVES["<"] = function(a, b) { return a < b; };
|
|
PRIMITIVES[">"] = function(a, b) { return a > b; };
|
|
PRIMITIVES["<="] = function(a, b) { return a <= b; };
|
|
PRIMITIVES[">="] = function(a, b) { return a >= b; };
|
|
''',
|
|
|
|
"core.logic": '''
|
|
// core.logic
|
|
PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); };
|
|
''',
|
|
|
|
"core.predicates": '''
|
|
// core.predicates
|
|
PRIMITIVES["nil?"] = isNil;
|
|
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
|
|
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
|
|
PRIMITIVES["list?"] = Array.isArray;
|
|
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
|
|
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
|
|
PRIMITIVES["contains?"] = function(c, k) {
|
|
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
|
|
if (Array.isArray(c)) return c.indexOf(k) !== -1;
|
|
return k in c;
|
|
};
|
|
PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
|
|
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
|
|
PRIMITIVES["zero?"] = function(n) { return n === 0; };
|
|
PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; };
|
|
PRIMITIVES["symbol?"] = function(x) { return x != null && x._sym === true; };
|
|
PRIMITIVES["keyword?"] = function(x) { return x != null && x._kw === true; };
|
|
PRIMITIVES["component-affinity"] = componentAffinity;
|
|
''',
|
|
|
|
"core.strings": '''
|
|
// core.strings
|
|
PRIMITIVES["str"] = function() {
|
|
var p = [];
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
var v = arguments[i]; if (isNil(v)) continue; p.push(String(v));
|
|
}
|
|
return p.join("");
|
|
};
|
|
PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); };
|
|
PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); };
|
|
PRIMITIVES["trim"] = function(s) { return String(s).trim(); };
|
|
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
|
|
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
|
|
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
|
|
PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); };
|
|
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
|
|
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
|
|
PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); };
|
|
PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); };
|
|
PRIMITIVES["char-from-code"] = function(n) { return String.fromCharCode(n); };
|
|
PRIMITIVES["string-length"] = function(s) { return String(s).length; };
|
|
PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; };
|
|
PRIMITIVES["concat"] = function() {
|
|
var out = [];
|
|
for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]);
|
|
return out;
|
|
};
|
|
''',
|
|
|
|
"core.collections": '''
|
|
// core.collections
|
|
PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); };
|
|
PRIMITIVES["dict"] = function() {
|
|
var d = {};
|
|
for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1];
|
|
return d;
|
|
};
|
|
PRIMITIVES["range"] = function(a, b, step) {
|
|
var r = []; step = step || 1;
|
|
for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i);
|
|
return r;
|
|
};
|
|
PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); };
|
|
PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; };
|
|
PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; };
|
|
PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; };
|
|
PRIMITIVES["rest"] = function(c) { if (c && typeof c.slice !== "function") { console.error("[sx-debug] rest called on non-sliceable:", typeof c, c, new Error().stack); return []; } return c ? c.slice(1) : []; };
|
|
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
|
|
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
|
|
PRIMITIVES["append"] = function(c, x) { return (c || []).concat(Array.isArray(x) ? x : [x]); };
|
|
PRIMITIVES["append!"] = function(arr, x) { arr.push(x); return arr; };
|
|
PRIMITIVES["chunk-every"] = function(c, n) {
|
|
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
|
|
};
|
|
PRIMITIVES["zip-pairs"] = function(c) {
|
|
var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
|
|
};
|
|
PRIMITIVES["reverse"] = function(c) { return Array.isArray(c) ? c.slice().reverse() : String(c).split("").reverse().join(""); };
|
|
PRIMITIVES["flatten"] = function(c) {
|
|
var out = [];
|
|
function walk(a) { for (var i = 0; i < a.length; i++) Array.isArray(a[i]) ? walk(a[i]) : out.push(a[i]); }
|
|
walk(c || []); return out;
|
|
};
|
|
''',
|
|
|
|
"core.dict": '''
|
|
// core.dict
|
|
PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); };
|
|
PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; };
|
|
PRIMITIVES["merge"] = function() {
|
|
var out = {};
|
|
for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; }
|
|
return out;
|
|
};
|
|
PRIMITIVES["assoc"] = function(d) {
|
|
var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k];
|
|
for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1];
|
|
return out;
|
|
};
|
|
PRIMITIVES["dissoc"] = function(d) {
|
|
var out = {}; for (var k in d) out[k] = d[k];
|
|
for (var i = 1; i < arguments.length; i++) delete out[arguments[i]];
|
|
return out;
|
|
};
|
|
PRIMITIVES["dict-set!"] = function(d, k, v) { d[k] = v; return v; };
|
|
PRIMITIVES["has-key?"] = function(d, k) { return d !== null && d !== undefined && k in d; };
|
|
PRIMITIVES["into"] = function(target, coll) {
|
|
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
|
|
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
|
|
return r;
|
|
};
|
|
''',
|
|
|
|
"stdlib.format": '''
|
|
// stdlib.format
|
|
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
|
|
PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; };
|
|
PRIMITIVES["format-date"] = function(s, fmt) {
|
|
if (!s) return "";
|
|
try {
|
|
var d = new Date(s);
|
|
if (isNaN(d.getTime())) return String(s);
|
|
var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
|
|
.replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
|
|
.replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
|
|
.replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
|
|
} catch (e) { return String(s); }
|
|
};
|
|
PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; };
|
|
''',
|
|
|
|
"stdlib.text": '''
|
|
// stdlib.text
|
|
PRIMITIVES["pluralize"] = function(n, s, p) {
|
|
if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s");
|
|
return n == 1 ? "" : "s";
|
|
};
|
|
PRIMITIVES["escape"] = function(s) {
|
|
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
|
|
};
|
|
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
|
|
''',
|
|
|
|
"stdlib.debug": '''
|
|
// stdlib.debug
|
|
PRIMITIVES["assert"] = function(cond, msg) {
|
|
if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed"));
|
|
return true;
|
|
};
|
|
''',
|
|
|
|
"stdlib.spread": '''
|
|
// stdlib.spread — spread + collect + scope primitives
|
|
PRIMITIVES["make-spread"] = makeSpread;
|
|
PRIMITIVES["spread?"] = isSpread;
|
|
PRIMITIVES["spread-attrs"] = spreadAttrs;
|
|
PRIMITIVES["collect!"] = sxCollect;
|
|
PRIMITIVES["collected"] = sxCollected;
|
|
PRIMITIVES["clear-collected!"] = sxClearCollected;
|
|
// scope — unified render-time dynamic scope
|
|
PRIMITIVES["scope-push!"] = scopePush;
|
|
PRIMITIVES["scope-pop!"] = scopePop;
|
|
// provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
|
|
PRIMITIVES["provide-push!"] = providePush;
|
|
PRIMITIVES["provide-pop!"] = providePop;
|
|
PRIMITIVES["context"] = sxContext;
|
|
PRIMITIVES["emit!"] = sxEmit;
|
|
PRIMITIVES["emitted"] = sxEmitted;
|
|
''',
|
|
}
|
|
# Modules to include by default (all)
|
|
_ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys())
|
|
|
|
def _assemble_primitives_js(modules: list[str] | None = None) -> str:
|
|
"""Assemble JS primitive code from selected modules.
|
|
|
|
If modules is None, all modules are included.
|
|
Core modules are always included regardless of the list.
|
|
"""
|
|
if modules is None:
|
|
modules = _ALL_JS_MODULES
|
|
parts = []
|
|
for mod in modules:
|
|
if mod in PRIMITIVES_JS_MODULES:
|
|
parts.append(PRIMITIVES_JS_MODULES[mod])
|
|
return "\n".join(parts)
|
|
|
|
PLATFORM_JS_PRE = '''
|
|
// =========================================================================
|
|
// Platform interface — JS implementation
|
|
// =========================================================================
|
|
|
|
function typeOf(x) {
|
|
if (isNil(x)) return "nil";
|
|
if (typeof x === "number") return "number";
|
|
if (typeof x === "string") return "string";
|
|
if (typeof x === "boolean") return "boolean";
|
|
if (x._sym) return "symbol";
|
|
if (x._kw) return "keyword";
|
|
if (x._thunk) return "thunk";
|
|
if (x._lambda) return "lambda";
|
|
if (x._component) return "component";
|
|
if (x._island) return "island";
|
|
if (x._spread) return "spread";
|
|
if (x._macro) return "macro";
|
|
if (x._raw) return "raw-html";
|
|
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
|
if (Array.isArray(x)) return "list";
|
|
if (typeof x === "object") return "dict";
|
|
return "unknown";
|
|
}
|
|
|
|
function symbolName(s) { return s.name; }
|
|
function keywordName(k) { return k.name; }
|
|
function makeSymbol(n) { return new Symbol(n); }
|
|
function makeKeyword(n) { return new Keyword(n); }
|
|
|
|
function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); }
|
|
function makeComponent(name, params, hasChildren, body, env, affinity) {
|
|
return new Component(name, params, hasChildren, body, merge(env), affinity);
|
|
}
|
|
function makeMacro(params, restParam, body, env, name) {
|
|
return new Macro(params, restParam, body, merge(env), name);
|
|
}
|
|
function makeThunk(expr, env) { return new Thunk(expr, env); }
|
|
|
|
function makeSpread(attrs) { return new SxSpread(attrs || {}); }
|
|
function isSpread(x) { return x != null && x._spread === true; }
|
|
function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; }
|
|
|
|
function scopePush(name, value) {
|
|
if (!_scopeStacks[name]) _scopeStacks[name] = [];
|
|
_scopeStacks[name].push({value: value !== undefined ? value : NIL, emitted: [], dedup: false});
|
|
}
|
|
function scopePop(name) {
|
|
if (_scopeStacks[name] && _scopeStacks[name].length) _scopeStacks[name].pop();
|
|
}
|
|
// Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
|
|
var providePush = scopePush;
|
|
var providePop = scopePop;
|
|
|
|
function sxContext(name) {
|
|
if (_scopeStacks[name] && _scopeStacks[name].length) {
|
|
return _scopeStacks[name][_scopeStacks[name].length - 1].value;
|
|
}
|
|
if (arguments.length > 1) return arguments[1];
|
|
throw new Error("No provider for: " + name);
|
|
}
|
|
function sxEmit(name, value) {
|
|
if (_scopeStacks[name] && _scopeStacks[name].length) {
|
|
var entry = _scopeStacks[name][_scopeStacks[name].length - 1];
|
|
if (entry.dedup && entry.emitted.indexOf(value) !== -1) return NIL;
|
|
entry.emitted.push(value);
|
|
}
|
|
return NIL;
|
|
}
|
|
function sxEmitted(name) {
|
|
if (_scopeStacks[name] && _scopeStacks[name].length) {
|
|
return _scopeStacks[name][_scopeStacks[name].length - 1].emitted.slice();
|
|
}
|
|
return [];
|
|
}
|
|
function sxCollect(bucket, value) {
|
|
if (!_scopeStacks[bucket] || !_scopeStacks[bucket].length) {
|
|
if (!_scopeStacks[bucket]) _scopeStacks[bucket] = [];
|
|
_scopeStacks[bucket].push({value: NIL, emitted: [], dedup: true});
|
|
}
|
|
var entry = _scopeStacks[bucket][_scopeStacks[bucket].length - 1];
|
|
if (entry.emitted.indexOf(value) === -1) entry.emitted.push(value);
|
|
}
|
|
function sxCollected(bucket) {
|
|
return sxEmitted(bucket);
|
|
}
|
|
function sxClearCollected(bucket) {
|
|
if (_scopeStacks[bucket] && _scopeStacks[bucket].length) {
|
|
_scopeStacks[bucket][_scopeStacks[bucket].length - 1].emitted = [];
|
|
}
|
|
}
|
|
|
|
function lambdaParams(f) { return f.params; }
|
|
function lambdaBody(f) { return f.body; }
|
|
function lambdaClosure(f) { return f.closure; }
|
|
function lambdaName(f) { return f.name; }
|
|
function setLambdaName(f, n) { f.name = n; }
|
|
|
|
function componentParams(c) { return c.params; }
|
|
function componentBody(c) { return c.body; }
|
|
function componentClosure(c) { return c.closure; }
|
|
function componentHasChildren(c) { return c.hasChildren; }
|
|
function componentName(c) { return c.name; }
|
|
function componentAffinity(c) { return c.affinity || "auto"; }
|
|
|
|
function macroParams(m) { return m.params; }
|
|
function macroRestParam(m) { return m.restParam; }
|
|
function macroBody(m) { return m.body; }
|
|
function macroClosure(m) { return m.closure; }
|
|
|
|
function isThunk(x) { return x != null && x._thunk === true; }
|
|
function thunkExpr(t) { return t.expr; }
|
|
function thunkEnv(t) { return t.env; }
|
|
|
|
function isCallable(x) { return typeof x === "function" || (x != null && x._lambda === true); }
|
|
function isLambda(x) { return x != null && x._lambda === true; }
|
|
function isComponent(x) { return x != null && x._component === true; }
|
|
function isIsland(x) { return x != null && x._island === true; }
|
|
function isMacro(x) { return x != null && x._macro === true; }
|
|
function isIdentical(a, b) { return a === b; }
|
|
|
|
// Island platform
|
|
function makeIsland(name, params, hasChildren, body, env) {
|
|
return new Island(name, params, hasChildren, body, merge(env));
|
|
}
|
|
|
|
// JSON / dict helpers for island state serialization
|
|
function jsonSerialize(obj) {
|
|
return JSON.stringify(obj);
|
|
}
|
|
function isEmptyDict(d) {
|
|
if (!d || typeof d !== "object") return true;
|
|
for (var k in d) if (d.hasOwnProperty(k)) return false;
|
|
return true;
|
|
}
|
|
|
|
function envHas(env, name) { return name in env; }
|
|
function envGet(env, name) { return env[name]; }
|
|
function envSet(env, name, val) {
|
|
// Walk prototype chain to find where the variable is defined (for set!)
|
|
var obj = env;
|
|
while (obj !== null && obj !== Object.prototype) {
|
|
if (obj.hasOwnProperty(name)) { obj[name] = val; return; }
|
|
obj = Object.getPrototypeOf(obj);
|
|
}
|
|
// Not found in any parent scope — set on the immediate env
|
|
env[name] = val;
|
|
}
|
|
function envExtend(env) { return Object.create(env); }
|
|
function envMerge(base, overlay) {
|
|
// Same env or overlay is descendant of base — just extend, no copy.
|
|
// This prevents set! inside lambdas from modifying shadow copies.
|
|
if (base === overlay) return Object.create(base);
|
|
var p = overlay;
|
|
for (var d = 0; p && p !== Object.prototype && d < 100; d++) {
|
|
if (p === base) return Object.create(base);
|
|
p = Object.getPrototypeOf(p);
|
|
}
|
|
// General case: extend base, copy ONLY overlay properties that don't
|
|
// exist in the base chain (avoids shadowing closure bindings).
|
|
var child = Object.create(base);
|
|
if (overlay) {
|
|
for (var k in overlay) {
|
|
if (overlay.hasOwnProperty(k) && !(k in base)) child[k] = overlay[k];
|
|
}
|
|
}
|
|
return child;
|
|
}
|
|
|
|
function dictSet(d, k, v) { d[k] = v; return v; }
|
|
function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; }
|
|
|
|
// Render-expression detection — lets the evaluator delegate to the active adapter.
|
|
// Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements.
|
|
// Placeholder — overridden by transpiled version from render.sx
|
|
function isRenderExpr(expr) { return false; }
|
|
|
|
// Render dispatch — call the active adapter's render function.
|
|
// Set by each adapter when loaded; defaults to identity (no rendering).
|
|
var _renderExprFn = null;
|
|
|
|
// Render mode flag — set by render-to-html/aser, checked by eval-list.
|
|
// When false, render expressions fall through to evalCall.
|
|
var _renderMode = false;
|
|
function renderActiveP() { return _renderMode; }
|
|
function setRenderActiveB(val) { _renderMode = !!val; }
|
|
|
|
function renderExpr(expr, env) {
|
|
if (_renderExprFn) return _renderExprFn(expr, env);
|
|
// No adapter loaded — fall through to evalCall
|
|
return evalCall(first(expr), rest(expr), env);
|
|
}
|
|
|
|
function stripPrefix(s, prefix) {
|
|
return s.indexOf(prefix) === 0 ? s.slice(prefix.length) : s;
|
|
}
|
|
|
|
function error(msg) { throw new Error(msg); }
|
|
function inspect(x) { return JSON.stringify(x); }
|
|
function debugLog() { console.error.apply(console, ["[sx-debug]"].concat(Array.prototype.slice.call(arguments))); }
|
|
|
|
'''
|
|
|
|
|
|
PLATFORM_JS_POST = '''
|
|
function isPrimitive(name) { return name in PRIMITIVES; }
|
|
function getPrimitive(name) { return PRIMITIVES[name]; }
|
|
|
|
// Higher-order helpers used by the transpiled code
|
|
function map(fn, coll) { return coll.map(fn); }
|
|
function mapIndexed(fn, coll) { return coll.map(function(item, i) { return fn(i, item); }); }
|
|
function filter(fn, coll) { return coll.filter(function(x) { return isSxTruthy(fn(x)); }); }
|
|
function reduce(fn, init, coll) {
|
|
var acc = init;
|
|
for (var i = 0; i < coll.length; i++) acc = fn(acc, coll[i]);
|
|
return acc;
|
|
}
|
|
function some(fn, coll) {
|
|
for (var i = 0; i < coll.length; i++) { var r = fn(coll[i]); if (isSxTruthy(r)) return r; }
|
|
return NIL;
|
|
}
|
|
function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; }
|
|
function isEvery(fn, coll) {
|
|
for (var i = 0; i < coll.length; i++) { if (!isSxTruthy(fn(coll[i]))) return false; }
|
|
return true;
|
|
}
|
|
function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; }
|
|
|
|
// Predicate aliases used by transpiled code
|
|
// Both naming conventions: isX (from js-renames) and x_p (from js-mangle of x?)
|
|
var isNumber = PRIMITIVES["number?"]; var number_p = isNumber;
|
|
var isString = PRIMITIVES["string?"]; var string_p = isString;
|
|
var isBoolean = PRIMITIVES["boolean?"]; var boolean_p = isBoolean;
|
|
var isDict = PRIMITIVES["dict?"];
|
|
var isList = PRIMITIVES["list?"]; var list_p = isList;
|
|
var isKeyword = PRIMITIVES["keyword?"]; var keyword_p = isKeyword;
|
|
var isSymbol = PRIMITIVES["symbol?"]; var symbol_p = isSymbol;
|
|
|
|
// List primitives used directly by transpiled code
|
|
var len = PRIMITIVES["len"];
|
|
var first = PRIMITIVES["first"];
|
|
var last = PRIMITIVES["last"];
|
|
var rest = PRIMITIVES["rest"];
|
|
var nth = PRIMITIVES["nth"];
|
|
var cons = PRIMITIVES["cons"];
|
|
var append = PRIMITIVES["append"];
|
|
var isEmpty = PRIMITIVES["empty?"];
|
|
var contains = PRIMITIVES["contains?"];
|
|
var startsWith = PRIMITIVES["starts-with?"];
|
|
var slice = PRIMITIVES["slice"];
|
|
var concat = PRIMITIVES["concat"];
|
|
var str = PRIMITIVES["str"];
|
|
var join = PRIMITIVES["join"];
|
|
var keys = PRIMITIVES["keys"];
|
|
var get = PRIMITIVES["get"];
|
|
var assoc = PRIMITIVES["assoc"];
|
|
var range = PRIMITIVES["range"];
|
|
function zip(a, b) { var r = []; for (var i = 0; i < Math.min(a.length, b.length); i++) r.push([a[i], b[i]]); return r; }
|
|
function append_b(arr, x) { arr.push(x); return arr; }
|
|
var apply = function(f, args) {
|
|
if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f)));
|
|
return f.apply(null, args);
|
|
};
|
|
|
|
// Additional primitive aliases used by adapter/engine transpiled code
|
|
var split = PRIMITIVES["split"];
|
|
var trim = PRIMITIVES["trim"];
|
|
var upper = PRIMITIVES["upper"];
|
|
var lower = PRIMITIVES["lower"];
|
|
var replace_ = function(s, old, nw) { return s.split(old).join(nw); };
|
|
var endsWith = PRIMITIVES["ends-with?"];
|
|
var parseInt_ = PRIMITIVES["parse-int"];
|
|
var dict_fn = PRIMITIVES["dict"];
|
|
|
|
// HTML rendering helpers
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
}
|
|
function escapeAttr(s) { return escapeHtml(s); }
|
|
function rawHtmlContent(r) { return r.html; }
|
|
function makeRawHtml(s) { return { _raw: true, html: s }; }
|
|
function sxExprSource(x) { return x && x.source ? x.source : String(x); }
|
|
|
|
// Placeholders — overridden by transpiled spec from parser.sx / adapter-sx.sx
|
|
function serialize(val) { return String(val); }
|
|
function isSpecialForm(n) { return false; }
|
|
function isHoForm(n) { return false; }
|
|
|
|
// processBindings and evalCond — now specced in render.sx, bootstrapped above
|
|
|
|
function isDefinitionForm(name) {
|
|
return name === "define" || name === "defcomp" || name === "defmacro" ||
|
|
name === "defstyle" || name === "defhandler";
|
|
}
|
|
|
|
function indexOf_(s, ch) {
|
|
return typeof s === "string" ? s.indexOf(ch) : -1;
|
|
}
|
|
|
|
function dictHas(d, k) { return d != null && k in d; }
|
|
function dictDelete(d, k) { delete d[k]; }
|
|
|
|
function forEachIndexed(fn, coll) {
|
|
for (var i = 0; i < coll.length; i++) fn(i, coll[i]);
|
|
return NIL;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Performance overrides — evaluator hot path
|
|
// =========================================================================
|
|
|
|
// Override parseKeywordArgs: imperative loop instead of reduce+assoc
|
|
parseKeywordArgs = function(rawArgs, env) {
|
|
var kwargs = {};
|
|
var children = [];
|
|
for (var i = 0; i < rawArgs.length; i++) {
|
|
var arg = rawArgs[i];
|
|
if (arg && arg._kw && (i + 1) < rawArgs.length) {
|
|
kwargs[arg.name] = trampoline(evalExpr(rawArgs[i + 1], env));
|
|
i++;
|
|
} else {
|
|
children.push(trampoline(evalExpr(arg, env)));
|
|
}
|
|
}
|
|
return [kwargs, children];
|
|
};
|
|
|
|
// Override callComponent: use prototype chain env, imperative kwarg binding
|
|
callComponent = function(comp, rawArgs, env) {
|
|
var kwargs = {};
|
|
var children = [];
|
|
for (var i = 0; i < rawArgs.length; i++) {
|
|
var arg = rawArgs[i];
|
|
if (arg && arg._kw && (i + 1) < rawArgs.length) {
|
|
kwargs[arg.name] = trampoline(evalExpr(rawArgs[i + 1], env));
|
|
i++;
|
|
} else {
|
|
children.push(trampoline(evalExpr(arg, env)));
|
|
}
|
|
}
|
|
var local = Object.create(componentClosure(comp));
|
|
for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k];
|
|
var params = componentParams(comp);
|
|
for (var j = 0; j < params.length; j++) {
|
|
var p = params[j];
|
|
local[p] = p in kwargs ? kwargs[p] : NIL;
|
|
}
|
|
if (componentHasChildren(comp)) {
|
|
local["children"] = children;
|
|
}
|
|
return makeThunk(componentBody(comp), local);
|
|
};'''
|
|
|
|
|
|
PLATFORM_CEK_JS = '''
|
|
// =========================================================================
|
|
// Platform: CEK module — explicit CEK machine
|
|
// =========================================================================
|
|
|
|
// Continuation type (needed by CEK even without the tree-walk shift/reset extension)
|
|
if (typeof Continuation === "undefined") {
|
|
function Continuation(fn) { this.fn = fn; }
|
|
Continuation.prototype._continuation = true;
|
|
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
|
|
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
|
|
}
|
|
|
|
// Standalone aliases for primitives used by cek.sx / frames.sx
|
|
var inc = PRIMITIVES["inc"];
|
|
var dec = PRIMITIVES["dec"];
|
|
var zip_pairs = PRIMITIVES["zip-pairs"];
|
|
|
|
var continuation_p = PRIMITIVES["continuation?"];
|
|
|
|
function makeCekContinuation(captured, restKont) {
|
|
var c = new Continuation(function(v) { return v !== undefined ? v : NIL; });
|
|
c._cek_data = {"captured": captured, "rest-kont": restKont};
|
|
return c;
|
|
}
|
|
function continuationData(c) {
|
|
return (c && c._cek_data) ? c._cek_data : {};
|
|
}
|
|
'''
|
|
|
|
# Iterative override for cek_run — replaces transpiled recursive version
|
|
CEK_FIXUPS_JS = '''
|
|
// Override recursive cekRun with iterative loop (avoids stack overflow)
|
|
cekRun = function(state) {
|
|
while (!cekTerminal_p(state)) { state = cekStep(state); }
|
|
return cekValue(state);
|
|
};
|
|
|
|
// Platform functions — defined in platform_js.py, not in .sx spec files.
|
|
// Spec defines self-register via js-emit-define; these are the platform interface.
|
|
PRIMITIVES["type-of"] = typeOf;
|
|
PRIMITIVES["symbol-name"] = symbolName;
|
|
PRIMITIVES["keyword-name"] = keywordName;
|
|
PRIMITIVES["callable?"] = isCallable;
|
|
PRIMITIVES["lambda?"] = isLambda;
|
|
PRIMITIVES["lambda-name"] = lambdaName;
|
|
PRIMITIVES["component?"] = isComponent;
|
|
PRIMITIVES["island?"] = isIsland;
|
|
PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); };
|
|
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
|
|
PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); };
|
|
'''
|
|
|
|
|
|
PLATFORM_DEPS_JS = '''
|
|
// =========================================================================
|
|
// Platform: deps module — component dependency analysis
|
|
// =========================================================================
|
|
|
|
function componentDeps(c) {
|
|
return c.deps ? c.deps.slice() : [];
|
|
}
|
|
|
|
function componentSetDeps(c, deps) {
|
|
c.deps = deps;
|
|
}
|
|
|
|
function componentCssClasses(c) {
|
|
return c.cssClasses ? c.cssClasses.slice() : [];
|
|
}
|
|
|
|
function envComponents(env) {
|
|
var names = [];
|
|
for (var k in env) {
|
|
var v = env[k];
|
|
if (v && (v._component || v._macro)) names.push(k);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
function regexFindAll(pattern, source) {
|
|
var re = new RegExp(pattern, "g");
|
|
var results = [];
|
|
var m;
|
|
while ((m = re.exec(source)) !== null) {
|
|
if (m[1] !== undefined) results.push(m[1]);
|
|
else results.push(m[0]);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function scanCssClasses(source) {
|
|
var classes = {};
|
|
var result = [];
|
|
var m;
|
|
var re1 = /:class\\s+"([^"]*)"/g;
|
|
while ((m = re1.exec(source)) !== null) {
|
|
var parts = m[1].split(/\\s+/);
|
|
for (var i = 0; i < parts.length; i++) {
|
|
if (parts[i] && !classes[parts[i]]) {
|
|
classes[parts[i]] = true;
|
|
result.push(parts[i]);
|
|
}
|
|
}
|
|
}
|
|
var re2 = /:class\\s+\\(str\\s+((?:"[^"]*"\\s*)+)\\)/g;
|
|
while ((m = re2.exec(source)) !== null) {
|
|
var re3 = /"([^"]*)"/g;
|
|
var m2;
|
|
while ((m2 = re3.exec(m[1])) !== null) {
|
|
var parts2 = m2[1].split(/\\s+/);
|
|
for (var j = 0; j < parts2.length; j++) {
|
|
if (parts2[j] && !classes[parts2[j]]) {
|
|
classes[parts2[j]] = true;
|
|
result.push(parts2[j]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var re4 = /;;\\s*@css\\s+(.+)/g;
|
|
while ((m = re4.exec(source)) !== null) {
|
|
var parts3 = m[1].split(/\\s+/);
|
|
for (var k = 0; k < parts3.length; k++) {
|
|
if (parts3[k] && !classes[parts3[k]]) {
|
|
classes[parts3[k]] = true;
|
|
result.push(parts3[k]);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function componentIoRefs(c) {
|
|
return c.ioRefs ? c.ioRefs.slice() : [];
|
|
}
|
|
|
|
function componentSetIoRefs(c, refs) {
|
|
c.ioRefs = refs;
|
|
}
|
|
'''
|
|
|
|
|
|
PLATFORM_PARSER_JS = r"""
|
|
// =========================================================================
|
|
// Platform interface — Parser
|
|
// =========================================================================
|
|
// Character classification derived from the grammar:
|
|
// ident-start → [a-zA-Z_~*+\-><=/!?&]
|
|
// ident-char → ident-start + [0-9.:\/\#,]
|
|
|
|
var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/;
|
|
var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/#,]/;
|
|
|
|
function isIdentStart(ch) { return _identStartRe.test(ch); }
|
|
function isIdentChar(ch) { return _identCharRe.test(ch); }
|
|
function parseNumber(s) { return Number(s); }
|
|
function escapeString(s) {
|
|
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
}
|
|
function sxExprSource(e) { return typeof e === "string" ? e : String(e); }
|
|
var charFromCode = PRIMITIVES["char-from-code"];
|
|
"""
|
|
|
|
|
|
PLATFORM_DOM_JS = """
|
|
// =========================================================================
|
|
// Platform interface — DOM adapter (browser-only)
|
|
// =========================================================================
|
|
|
|
var _hasDom = typeof document !== "undefined";
|
|
|
|
// Register DOM adapter as the render dispatch target for the evaluator.
|
|
_renderExprFn = function(expr, env) { return renderToDom(expr, env, null); };
|
|
_renderMode = true; // Browser always evaluates in render context.
|
|
|
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
|
|
|
|
function domCreateElement(tag, ns) {
|
|
if (!_hasDom) return null;
|
|
if (ns && ns !== NIL) return document.createElementNS(ns, tag);
|
|
return document.createElement(tag);
|
|
}
|
|
|
|
function createTextNode(s) {
|
|
return _hasDom ? document.createTextNode(s) : null;
|
|
}
|
|
|
|
function createComment(s) {
|
|
return _hasDom ? document.createComment(s || "") : null;
|
|
}
|
|
|
|
function createFragment() {
|
|
return _hasDom ? document.createDocumentFragment() : null;
|
|
}
|
|
|
|
function domAppend(parent, child) {
|
|
if (parent && child && !child._spread) parent.appendChild(child);
|
|
}
|
|
|
|
function domPrepend(parent, child) {
|
|
if (parent && child) parent.insertBefore(child, parent.firstChild);
|
|
}
|
|
|
|
function domSetAttr(el, name, val) {
|
|
if (el && el.setAttribute) el.setAttribute(name, val);
|
|
}
|
|
|
|
function domGetAttr(el, name) {
|
|
if (!el || !el.getAttribute) return NIL;
|
|
var v = el.getAttribute(name);
|
|
return v === null ? NIL : v;
|
|
}
|
|
|
|
function domRemoveAttr(el, name) {
|
|
if (el && el.removeAttribute) el.removeAttribute(name);
|
|
}
|
|
|
|
function domHasAttr(el, name) {
|
|
return !!(el && el.hasAttribute && el.hasAttribute(name));
|
|
}
|
|
|
|
function domParseHtml(html) {
|
|
if (!_hasDom) return null;
|
|
var tpl = document.createElement("template");
|
|
tpl.innerHTML = html;
|
|
return tpl.content;
|
|
}
|
|
|
|
function domClone(node) {
|
|
return node && node.cloneNode ? node.cloneNode(true) : node;
|
|
}
|
|
|
|
function domParent(el) { return el ? el.parentNode : null; }
|
|
function domId(el) { return el && el.id ? el.id : NIL; }
|
|
function domNodeType(el) { return el ? el.nodeType : 0; }
|
|
function domNodeName(el) { return el ? el.nodeName : ""; }
|
|
function domTextContent(el) { return el ? el.textContent || el.nodeValue || "" : ""; }
|
|
function domSetTextContent(el, s) { if (el) { if (el.nodeType === 3 || el.nodeType === 8) el.nodeValue = s; else el.textContent = s; } }
|
|
function domIsFragment(el) { return el ? el.nodeType === 11 : false; }
|
|
function domIsChildOf(child, parent) { return !!(parent && child && child.parentNode === parent); }
|
|
function domIsActiveElement(el) { return _hasDom && el === document.activeElement; }
|
|
function domIsInputElement(el) {
|
|
if (!el || !el.tagName) return false;
|
|
var t = el.tagName;
|
|
return t === "INPUT" || t === "TEXTAREA" || t === "SELECT";
|
|
}
|
|
function domFirstChild(el) { return el ? el.firstChild : null; }
|
|
function domNextSibling(el) { return el ? el.nextSibling : null; }
|
|
|
|
function domChildList(el) {
|
|
if (!el || !el.childNodes) return [];
|
|
return Array.prototype.slice.call(el.childNodes);
|
|
}
|
|
|
|
function domAttrList(el) {
|
|
if (!el || !el.attributes) return [];
|
|
var r = [];
|
|
for (var i = 0; i < el.attributes.length; i++) {
|
|
r.push([el.attributes[i].name, el.attributes[i].value]);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
function domInsertBefore(parent, node, ref) {
|
|
if (parent && node) parent.insertBefore(node, ref || null);
|
|
}
|
|
|
|
function domInsertAfter(ref, node) {
|
|
if (ref && ref.parentNode && node && !node._spread) {
|
|
ref.parentNode.insertBefore(node, ref.nextSibling);
|
|
}
|
|
}
|
|
|
|
function domRemoveChild(parent, child) {
|
|
if (parent && child && child.parentNode === parent) parent.removeChild(child);
|
|
}
|
|
|
|
function domReplaceChild(parent, newChild, oldChild) {
|
|
if (parent && newChild && oldChild) parent.replaceChild(newChild, oldChild);
|
|
}
|
|
|
|
function domSetInnerHtml(el, html) {
|
|
if (el) el.innerHTML = html;
|
|
}
|
|
|
|
function domInsertAdjacentHtml(el, pos, html) {
|
|
if (el && el.insertAdjacentHTML) el.insertAdjacentHTML(pos, html);
|
|
}
|
|
|
|
function domGetStyle(el, prop) {
|
|
return el && el.style ? el.style[prop] || "" : "";
|
|
}
|
|
|
|
function domSetStyle(el, prop, val) {
|
|
if (el && el.style) el.style[prop] = val;
|
|
}
|
|
|
|
function domGetProp(el, name) { return el ? el[name] : NIL; }
|
|
function domSetProp(el, name, val) { if (el) el[name] = val; }
|
|
// Call a method on an object with correct this binding: (dom-call-method obj "methodName" arg1 arg2 ...)
|
|
function domCallMethod() {
|
|
var obj = arguments[0], method = arguments[1];
|
|
var args = Array.prototype.slice.call(arguments, 2);
|
|
if (obj && typeof obj[method] === 'function') {
|
|
try { return obj[method].apply(obj, args); }
|
|
catch(e) { console.error("[sx] dom-call-method error:", e); return NIL; }
|
|
}
|
|
return NIL;
|
|
}
|
|
// Post a message to an iframe's contentWindow without exposing the cross-origin
|
|
// Window object to the SX evaluator (which would trigger _thunk access errors).
|
|
function domPostMessage(iframe, msg, origin) {
|
|
try {
|
|
if (iframe && iframe.contentWindow) {
|
|
iframe.contentWindow.postMessage(msg, origin || '*');
|
|
}
|
|
} catch(e) { console.error("[sx] domPostMessage error:", e); }
|
|
return NIL;
|
|
}
|
|
|
|
function domAddClass(el, cls) {
|
|
if (el && el.classList) el.classList.add(cls);
|
|
}
|
|
|
|
function domRemoveClass(el, cls) {
|
|
if (el && el.classList) el.classList.remove(cls);
|
|
}
|
|
|
|
function domDispatch(el, name, detail) {
|
|
if (!_hasDom || !el) return false;
|
|
var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} });
|
|
return el.dispatchEvent(evt);
|
|
}
|
|
|
|
function domListen(el, name, handler) {
|
|
if (!_hasDom || !el) return function() {};
|
|
// Wrap SX lambdas from runtime-evaluated island code into native fns
|
|
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
|
|
var wrapped = isLambda(handler)
|
|
? (lambdaParams(handler).length === 0
|
|
? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } finally { runPostRenderHooks(); } }
|
|
: function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } finally { runPostRenderHooks(); } })
|
|
: handler;
|
|
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
|
el.addEventListener(name, wrapped);
|
|
return function() { el.removeEventListener(name, wrapped); };
|
|
}
|
|
|
|
function eventDetail(e) {
|
|
return (e && e.detail != null) ? e.detail : NIL;
|
|
}
|
|
|
|
function domQuery(sel) {
|
|
return _hasDom ? document.querySelector(sel) : null;
|
|
}
|
|
|
|
function domEnsureElement(sel) {
|
|
if (!_hasDom) return null;
|
|
var el = document.querySelector(sel);
|
|
if (el) return el;
|
|
// Parse #id selector → create div with that id, append to body
|
|
if (sel.charAt(0) === '#') {
|
|
el = document.createElement('div');
|
|
el.id = sel.slice(1);
|
|
document.body.appendChild(el);
|
|
return el;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function domQueryAll(root, sel) {
|
|
if (!root || !root.querySelectorAll) return [];
|
|
return Array.prototype.slice.call(root.querySelectorAll(sel));
|
|
}
|
|
|
|
function domTagName(el) { return el && el.tagName ? el.tagName : ""; }
|
|
|
|
// Island DOM helpers
|
|
function domRemove(node) {
|
|
if (node && node.parentNode) node.parentNode.removeChild(node);
|
|
}
|
|
function domChildNodes(el) {
|
|
if (!el || !el.childNodes) return [];
|
|
return Array.prototype.slice.call(el.childNodes);
|
|
}
|
|
function domRemoveChildrenAfter(marker) {
|
|
if (!marker || !marker.parentNode) return;
|
|
var parent = marker.parentNode;
|
|
while (marker.nextSibling) parent.removeChild(marker.nextSibling);
|
|
}
|
|
function domSetData(el, key, val) {
|
|
if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; }
|
|
}
|
|
function domGetData(el, key) {
|
|
return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : NIL) : NIL;
|
|
}
|
|
function domInnerHtml(el) {
|
|
return (el && el.innerHTML != null) ? el.innerHTML : "";
|
|
}
|
|
function jsonParse(s) {
|
|
try { return JSON.parse(s); } catch(e) { return {}; }
|
|
}
|
|
|
|
// renderDomComponent and renderDomElement are transpiled from
|
|
// adapter-dom.sx — no imperative overrides needed.
|
|
"""
|
|
|
|
|
|
PLATFORM_ENGINE_PURE_JS = """
|
|
// =========================================================================
|
|
// Platform interface — Engine pure logic (browser + node compatible)
|
|
// =========================================================================
|
|
|
|
function browserLocationHref() {
|
|
return typeof location !== "undefined" ? location.href : "";
|
|
}
|
|
|
|
function browserSameOrigin(url) {
|
|
try { return new URL(url, location.href).origin === location.origin; }
|
|
catch (e) { return true; }
|
|
}
|
|
|
|
function browserPushState(url) {
|
|
if (typeof history !== "undefined") {
|
|
try { history.pushState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); }
|
|
catch (e) {}
|
|
}
|
|
}
|
|
|
|
function browserReplaceState(url) {
|
|
if (typeof history !== "undefined") {
|
|
try { history.replaceState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); }
|
|
catch (e) {}
|
|
}
|
|
}
|
|
|
|
function nowMs() { return (typeof performance !== "undefined") ? performance.now() : Date.now(); }
|
|
|
|
function parseHeaderValue(s) {
|
|
if (!s) return null;
|
|
try {
|
|
if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
|
|
return JSON.parse(s);
|
|
} catch (e) { return null; }
|
|
}
|
|
"""
|
|
|
|
|
|
PLATFORM_ORCHESTRATION_JS = """
|
|
// =========================================================================
|
|
// Platform interface — Orchestration (browser-only)
|
|
// =========================================================================
|
|
|
|
// --- Browser/Network ---
|
|
|
|
function browserNavigate(url) {
|
|
if (typeof location !== "undefined") location.assign(url);
|
|
}
|
|
|
|
function browserReload() {
|
|
if (typeof location !== "undefined") location.reload();
|
|
}
|
|
|
|
function browserScrollTo(x, y) {
|
|
if (typeof window !== "undefined") window.scrollTo(x, y);
|
|
}
|
|
|
|
function browserMediaMatches(query) {
|
|
if (typeof window === "undefined") return false;
|
|
return window.matchMedia(query).matches;
|
|
}
|
|
|
|
function browserConfirm(msg) {
|
|
if (typeof window === "undefined") return false;
|
|
return window.confirm(msg);
|
|
}
|
|
|
|
function browserPrompt(msg) {
|
|
if (typeof window === "undefined") return NIL;
|
|
var r = window.prompt(msg);
|
|
return r === null ? NIL : r;
|
|
}
|
|
|
|
function csrfToken() {
|
|
if (!_hasDom) return NIL;
|
|
var m = document.querySelector('meta[name="csrf-token"]');
|
|
return m ? m.getAttribute("content") : NIL;
|
|
}
|
|
|
|
function isCrossOrigin(url) {
|
|
try {
|
|
var h = new URL(url, location.href).hostname;
|
|
return h !== location.hostname &&
|
|
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0);
|
|
} catch (e) { return false; }
|
|
}
|
|
|
|
// --- Promises ---
|
|
|
|
function promiseResolve(val) { return Promise.resolve(val); }
|
|
|
|
function promiseThen(p, onResolve, onReject) {
|
|
if (!p || !p.then) return p;
|
|
return onReject ? p.then(onResolve, onReject) : p.then(onResolve);
|
|
}
|
|
|
|
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
|
|
|
function promiseDelayed(ms, value) {
|
|
return new Promise(function(resolve) {
|
|
setTimeout(function() { resolve(value); }, ms);
|
|
});
|
|
}
|
|
|
|
// --- Abort controllers ---
|
|
|
|
var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null;
|
|
|
|
function abortPrevious(el) {
|
|
if (_controllers) {
|
|
var prev = _controllers.get(el);
|
|
if (prev) prev.abort();
|
|
}
|
|
}
|
|
|
|
function trackController(el, ctrl) {
|
|
if (_controllers) _controllers.set(el, ctrl);
|
|
}
|
|
|
|
var _targetControllers = typeof WeakMap !== "undefined" ? new WeakMap() : null;
|
|
|
|
function abortPreviousTarget(el) {
|
|
if (_targetControllers) {
|
|
var prev = _targetControllers.get(el);
|
|
if (prev) prev.abort();
|
|
}
|
|
}
|
|
|
|
function trackControllerTarget(el, ctrl) {
|
|
if (_targetControllers) _targetControllers.set(el, ctrl);
|
|
}
|
|
|
|
function newAbortController() {
|
|
return typeof AbortController !== "undefined" ? new AbortController() : { signal: null, abort: function() {} };
|
|
}
|
|
|
|
function controllerSignal(ctrl) { return ctrl ? ctrl.signal : null; }
|
|
|
|
function isAbortError(err) { return err && err.name === "AbortError"; }
|
|
|
|
// --- Timers ---
|
|
|
|
function _wrapSxFn(fn) {
|
|
if (fn && fn._lambda) {
|
|
return function() { return trampoline(callLambda(fn, [], lambdaClosure(fn))); };
|
|
}
|
|
return fn;
|
|
}
|
|
function setTimeout_(fn, ms) { return setTimeout(_wrapSxFn(fn), ms || 0); }
|
|
function setInterval_(fn, ms) { return setInterval(_wrapSxFn(fn), ms || 1000); }
|
|
function clearTimeout_(id) { clearTimeout(id); }
|
|
function clearInterval_(id) { clearInterval(id); }
|
|
function requestAnimationFrame_(fn) {
|
|
var cb = _wrapSxFn(fn);
|
|
if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(cb);
|
|
else setTimeout(cb, 16);
|
|
}
|
|
|
|
// --- Fetch ---
|
|
|
|
function fetchRequest(config, successFn, errorFn) {
|
|
var opts = { method: config.method, headers: config.headers };
|
|
if (config.signal) opts.signal = config.signal;
|
|
if (config.body && config.method !== "GET") opts.body = config.body;
|
|
if (config["cross-origin"]) opts.credentials = "include";
|
|
|
|
var p = (config.preloaded && config.preloaded !== NIL)
|
|
? Promise.resolve({
|
|
ok: true, status: 200,
|
|
headers: new Headers({ "Content-Type": config.preloaded["content-type"] || "" }),
|
|
text: function() { return Promise.resolve(config.preloaded.text); }
|
|
})
|
|
: fetch(config.url, opts);
|
|
|
|
return p.then(function(resp) {
|
|
return resp.text().then(function(text) {
|
|
var getHeader = function(name) {
|
|
var v = resp.headers.get(name);
|
|
return v === null ? NIL : v;
|
|
};
|
|
return successFn(resp.ok, resp.status, getHeader, text);
|
|
});
|
|
}).catch(function(err) {
|
|
return errorFn(err);
|
|
});
|
|
}
|
|
|
|
function fetchLocation(headerVal) {
|
|
if (!_hasDom) return;
|
|
var locUrl = headerVal;
|
|
try { var obj = JSON.parse(headerVal); locUrl = obj.path || obj; } catch (e) {}
|
|
fetch(locUrl, { headers: { "SX-Request": "true" } }).then(function(r) {
|
|
return r.text().then(function(t) {
|
|
var main = document.getElementById("main-panel");
|
|
if (main) {
|
|
main.innerHTML = t;
|
|
postSwap(main);
|
|
try { history.pushState({ sxUrl: locUrl }, "", locUrl); } catch (e) {}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function fetchAndRestore(main, url, headers, scrollY) {
|
|
var opts = { headers: headers };
|
|
try {
|
|
var h = new URL(url, location.href).hostname;
|
|
if (h !== location.hostname &&
|
|
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) {
|
|
opts.credentials = "include";
|
|
}
|
|
} catch (e) {}
|
|
|
|
fetch(url, opts).then(function(resp) {
|
|
return resp.text().then(function(text) {
|
|
text = stripComponentScripts(text);
|
|
text = extractResponseCss(text);
|
|
text = text.trim();
|
|
if (text.charAt(0) === "(") {
|
|
try {
|
|
var dom = sxRender(text);
|
|
var container = document.createElement("div");
|
|
container.appendChild(dom);
|
|
processOobSwaps(container, function(t, oob, s) {
|
|
swapDomNodes(t, oob, s);
|
|
sxHydrate(t);
|
|
processElements(t);
|
|
});
|
|
var newMain = container.querySelector("#main-panel");
|
|
morphChildren(main, newMain || container);
|
|
postSwap(main);
|
|
if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
|
|
} catch (err) {
|
|
console.error("sx-ref popstate error:", err);
|
|
location.reload();
|
|
}
|
|
} else {
|
|
var parser = new DOMParser();
|
|
var doc = parser.parseFromString(text, "text/html");
|
|
var newMain = doc.getElementById("main-panel");
|
|
if (newMain) {
|
|
morphChildren(main, newMain);
|
|
postSwap(main);
|
|
if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
|
|
} else {
|
|
location.reload();
|
|
}
|
|
}
|
|
});
|
|
}).catch(function() { location.reload(); });
|
|
}
|
|
|
|
function fetchStreaming(target, url, headers) {
|
|
// Streaming fetch for multi-stream pages.
|
|
// First chunk = OOB SX swap (shell with skeletons).
|
|
// Subsequent chunks = __sxResolve script tags filling suspense slots.
|
|
var opts = { headers: headers };
|
|
try {
|
|
var h = new URL(url, location.href).hostname;
|
|
if (h !== location.hostname &&
|
|
(h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) {
|
|
opts.credentials = "include";
|
|
}
|
|
} catch (e) {}
|
|
|
|
fetch(url, opts).then(function(resp) {
|
|
if (!resp.ok || !resp.body) {
|
|
// Fallback: non-streaming
|
|
return resp.text().then(function(text) {
|
|
text = stripComponentScripts(text);
|
|
text = extractResponseCss(text);
|
|
text = text.trim();
|
|
if (text.charAt(0) === "(") {
|
|
var dom = sxRender(text);
|
|
var container = document.createElement("div");
|
|
container.appendChild(dom);
|
|
processOobSwaps(container, function(t, oob, s) {
|
|
swapDomNodes(t, oob, s);
|
|
sxHydrate(t);
|
|
processElements(t);
|
|
});
|
|
var newMain = container.querySelector("#main-panel");
|
|
morphChildren(target, newMain || container);
|
|
postSwap(target);
|
|
}
|
|
});
|
|
}
|
|
|
|
var reader = resp.body.getReader();
|
|
var decoder = new TextDecoder();
|
|
var buffer = "";
|
|
var initialSwapDone = false;
|
|
// Regex to match __sxResolve script tags
|
|
var RESOLVE_START = "<script>window.__sxResolve&&window.__sxResolve(";
|
|
var RESOLVE_END = ")</script>";
|
|
|
|
function processResolveScripts() {
|
|
// Strip and load any extra component defs before resolve scripts
|
|
buffer = stripSxScripts(buffer);
|
|
var idx;
|
|
while ((idx = buffer.indexOf(RESOLVE_START)) >= 0) {
|
|
var endIdx = buffer.indexOf(RESOLVE_END, idx);
|
|
if (endIdx < 0) break; // incomplete, wait for more data
|
|
var argsStr = buffer.substring(idx + RESOLVE_START.length, endIdx);
|
|
buffer = buffer.substring(endIdx + RESOLVE_END.length);
|
|
// argsStr is: "stream-id","sx source"
|
|
var commaIdx = argsStr.indexOf(",");
|
|
if (commaIdx >= 0) {
|
|
try {
|
|
var id = JSON.parse(argsStr.substring(0, commaIdx));
|
|
var sx = JSON.parse(argsStr.substring(commaIdx + 1));
|
|
if (typeof Sx !== "undefined" && Sx.resolveSuspense) {
|
|
Sx.resolveSuspense(id, sx);
|
|
}
|
|
} catch (e) {
|
|
console.error("[sx-ref] resolve parse error:", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function pump() {
|
|
return reader.read().then(function(result) {
|
|
buffer += decoder.decode(result.value || new Uint8Array(), { stream: !result.done });
|
|
|
|
if (!initialSwapDone) {
|
|
// Look for the first resolve script — everything before it is OOB content
|
|
var scriptIdx = buffer.indexOf("<script>window.__sxResolve");
|
|
// If we found a script tag, or the stream is done, process OOB
|
|
var oobEnd = scriptIdx >= 0 ? scriptIdx : (result.done ? buffer.length : -1);
|
|
if (oobEnd >= 0) {
|
|
var oobContent = buffer.substring(0, oobEnd);
|
|
buffer = buffer.substring(oobEnd);
|
|
initialSwapDone = true;
|
|
|
|
// Process OOB SX content (same as fetchAndRestore)
|
|
oobContent = stripComponentScripts(oobContent);
|
|
// Also strip bare <script type="text/sx"> (extra defs from resolve chunks)
|
|
oobContent = stripSxScripts(oobContent);
|
|
oobContent = extractResponseCss(oobContent);
|
|
oobContent = oobContent.trim();
|
|
if (oobContent.charAt(0) === "(") {
|
|
try {
|
|
var dom = sxRender(oobContent);
|
|
var container = document.createElement("div");
|
|
container.appendChild(dom);
|
|
processOobSwaps(container, function(t, oob, s) {
|
|
swapDomNodes(t, oob, s);
|
|
sxHydrate(t);
|
|
processElements(t);
|
|
});
|
|
var newMain = container.querySelector("#main-panel");
|
|
morphChildren(target, newMain || container);
|
|
postSwap(target);
|
|
// Dispatch clientRoute so nav links update active state
|
|
domDispatch(target, "sx:clientRoute",
|
|
{ pathname: new URL(url, location.href).pathname });
|
|
} catch (err) {
|
|
console.error("[sx-ref] streaming OOB swap error:", err);
|
|
}
|
|
}
|
|
// Process any resolve scripts already in buffer
|
|
processResolveScripts();
|
|
}
|
|
} else {
|
|
// Process resolve scripts as they arrive
|
|
processResolveScripts();
|
|
}
|
|
|
|
if (!result.done) return pump();
|
|
});
|
|
}
|
|
|
|
return pump();
|
|
}).catch(function(err) {
|
|
console.error("[sx-ref] streaming fetch error:", err);
|
|
location.reload();
|
|
});
|
|
}
|
|
|
|
function fetchPreload(url, headers, cache) {
|
|
fetch(url, { headers: headers }).then(function(resp) {
|
|
if (!resp.ok) return;
|
|
var ct = resp.headers.get("Content-Type") || "";
|
|
return resp.text().then(function(text) {
|
|
preloadCacheSet(cache, url, text, ct);
|
|
});
|
|
}).catch(function() { /* ignore */ });
|
|
}
|
|
|
|
// --- Request body building ---
|
|
|
|
function buildRequestBody(el, method, url) {
|
|
if (!_hasDom) return { body: null, url: url, "content-type": NIL };
|
|
var body = null;
|
|
var ct = NIL;
|
|
var finalUrl = url;
|
|
var isJson = el.getAttribute("sx-encoding") === "json";
|
|
|
|
if (method !== "GET") {
|
|
var form = el.closest("form") || (el.tagName === "FORM" ? el : null);
|
|
if (form) {
|
|
if (isJson) {
|
|
var fd = new FormData(form);
|
|
var obj = {};
|
|
fd.forEach(function(v, k) {
|
|
if (obj[k] !== undefined) {
|
|
if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
|
|
obj[k].push(v);
|
|
} else { obj[k] = v; }
|
|
});
|
|
body = JSON.stringify(obj);
|
|
ct = "application/json";
|
|
} else {
|
|
body = new URLSearchParams(new FormData(form));
|
|
ct = "application/x-www-form-urlencoded";
|
|
}
|
|
}
|
|
}
|
|
|
|
// sx-params
|
|
var paramsSpec = el.getAttribute("sx-params");
|
|
if (paramsSpec && body instanceof URLSearchParams) {
|
|
if (paramsSpec === "none") {
|
|
body = new URLSearchParams();
|
|
} else if (paramsSpec.indexOf("not ") === 0) {
|
|
paramsSpec.substring(4).split(",").forEach(function(k) { body.delete(k.trim()); });
|
|
} else if (paramsSpec !== "*") {
|
|
var allowed = paramsSpec.split(",").map(function(s) { return s.trim(); });
|
|
var filtered = new URLSearchParams();
|
|
allowed.forEach(function(k) {
|
|
body.getAll(k).forEach(function(v) { filtered.append(k, v); });
|
|
});
|
|
body = filtered;
|
|
}
|
|
}
|
|
|
|
// sx-include
|
|
var includeSel = el.getAttribute("sx-include");
|
|
if (includeSel && method !== "GET") {
|
|
if (!body) body = new URLSearchParams();
|
|
document.querySelectorAll(includeSel).forEach(function(inp) {
|
|
if (inp.name) body.append(inp.name, inp.value);
|
|
});
|
|
}
|
|
|
|
// sx-vals
|
|
var valsAttr = el.getAttribute("sx-vals");
|
|
if (valsAttr) {
|
|
try {
|
|
var vals = valsAttr.charAt(0) === "{" && valsAttr.charAt(1) === ":" ? parse(valsAttr) : JSON.parse(valsAttr);
|
|
if (method === "GET") {
|
|
for (var vk in vals) finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(vk) + "=" + encodeURIComponent(vals[vk]);
|
|
} else if (body instanceof URLSearchParams) {
|
|
for (var vk2 in vals) body.append(vk2, vals[vk2]);
|
|
} else if (!body) {
|
|
body = new URLSearchParams();
|
|
for (var vk3 in vals) body.append(vk3, vals[vk3]);
|
|
ct = "application/x-www-form-urlencoded";
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
// GET form data → URL
|
|
if (method === "GET") {
|
|
var form2 = el.closest("form") || (el.tagName === "FORM" ? el : null);
|
|
if (form2) {
|
|
var qs = new URLSearchParams(new FormData(form2)).toString();
|
|
if (qs) finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + qs;
|
|
}
|
|
if ((el.tagName === "INPUT" || el.tagName === "SELECT" || el.tagName === "TEXTAREA") && el.name) {
|
|
finalUrl += (finalUrl.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(el.name) + "=" + encodeURIComponent(el.value);
|
|
}
|
|
}
|
|
|
|
return { body: body, url: finalUrl, "content-type": ct };
|
|
}
|
|
|
|
// --- Loading state ---
|
|
|
|
function showIndicator(el) {
|
|
if (!_hasDom) return NIL;
|
|
var sel = el.getAttribute("sx-indicator");
|
|
var ind = sel ? (document.querySelector(sel) || el.closest(sel)) : null;
|
|
if (ind) { ind.classList.add("sx-request"); ind.style.display = ""; }
|
|
return ind || NIL;
|
|
}
|
|
|
|
function disableElements(el) {
|
|
if (!_hasDom) return [];
|
|
var sel = el.getAttribute("sx-disabled-elt");
|
|
if (!sel) return [];
|
|
var elts = Array.prototype.slice.call(document.querySelectorAll(sel));
|
|
elts.forEach(function(e) { e.disabled = true; });
|
|
return elts;
|
|
}
|
|
|
|
function clearLoadingState(el, indicator, disabledElts) {
|
|
el.classList.remove("sx-request");
|
|
el.removeAttribute("aria-busy");
|
|
if (indicator && !isNil(indicator)) {
|
|
indicator.classList.remove("sx-request");
|
|
indicator.style.display = "none";
|
|
}
|
|
if (disabledElts) {
|
|
for (var i = 0; i < disabledElts.length; i++) disabledElts[i].disabled = false;
|
|
}
|
|
}
|
|
|
|
// --- DOM extras ---
|
|
|
|
function domQueryById(id) {
|
|
return _hasDom ? document.getElementById(id) : null;
|
|
}
|
|
|
|
function domMatches(el, sel) {
|
|
return el && el.matches ? el.matches(sel) : false;
|
|
}
|
|
|
|
function domClosest(el, sel) {
|
|
return el && el.closest ? el.closest(sel) : null;
|
|
}
|
|
|
|
function domBody() {
|
|
return _hasDom ? document.body : null;
|
|
}
|
|
|
|
function domHasClass(el, cls) {
|
|
return el && el.classList ? el.classList.contains(cls) : false;
|
|
}
|
|
|
|
function domAppendToHead(el) {
|
|
if (_hasDom && document.head) document.head.appendChild(el);
|
|
}
|
|
|
|
function domParseHtmlDocument(text) {
|
|
if (!_hasDom) return null;
|
|
return new DOMParser().parseFromString(text, "text/html");
|
|
}
|
|
|
|
function domOuterHtml(el) {
|
|
return el ? el.outerHTML : "";
|
|
}
|
|
|
|
function domBodyInnerHtml(doc) {
|
|
return doc && doc.body ? doc.body.innerHTML : "";
|
|
}
|
|
|
|
// --- Events ---
|
|
|
|
function preventDefault_(e) { if (e && e.preventDefault) e.preventDefault(); }
|
|
function stopPropagation_(e) { if (e && e.stopPropagation) e.stopPropagation(); }
|
|
function domFocus(el) { if (el && el.focus) el.focus(); }
|
|
function tryCatch(tryFn, catchFn) {
|
|
var t = _wrapSxFn(tryFn);
|
|
var c = catchFn && catchFn._lambda
|
|
? function(e) { return trampoline(callLambda(catchFn, [e], lambdaClosure(catchFn))); }
|
|
: catchFn;
|
|
try { return t(); } catch (e) { return c(e); }
|
|
}
|
|
function errorMessage(e) {
|
|
return e && e.message ? e.message : String(e);
|
|
}
|
|
function scheduleIdle(fn) {
|
|
var cb = _wrapSxFn(fn);
|
|
if (typeof cb !== "function") {
|
|
console.error("[sx-ref] scheduleIdle: callback not callable, fn type:", typeof fn, "fn:", fn, "_lambda:", fn && fn._lambda);
|
|
return;
|
|
}
|
|
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(cb);
|
|
else setTimeout(cb, 0);
|
|
}
|
|
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
|
|
|
function domAddListener(el, event, fn, opts) {
|
|
if (!el || !el.addEventListener) return;
|
|
var o = {};
|
|
if (opts && !isNil(opts)) {
|
|
if (opts.once || opts["once"]) o.once = true;
|
|
}
|
|
el.addEventListener(event, function(e) {
|
|
try { fn(e); } catch (err) { logInfo("EVENT ERROR: " + event + " " + (err && err.message ? err.message : err)); console.error("[sx-ref] event handler error:", event, err); }
|
|
}, o);
|
|
}
|
|
|
|
// --- Validation ---
|
|
|
|
function validateForRequest(el) {
|
|
if (!_hasDom) return true;
|
|
var attr = el.getAttribute("sx-validate");
|
|
if (attr === null) {
|
|
var vForm = el.closest("[sx-validate]");
|
|
if (vForm) attr = vForm.getAttribute("sx-validate");
|
|
}
|
|
if (attr === null) return true; // no validation configured
|
|
var form = el.tagName === "FORM" ? el : el.closest("form");
|
|
if (form && !form.reportValidity()) return false;
|
|
if (attr && attr !== "true" && attr !== "") {
|
|
var fn = window[attr];
|
|
if (typeof fn === "function" && !fn(el)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// --- View Transitions ---
|
|
|
|
function withTransition(enabled, fn) {
|
|
if (enabled && _hasDom && document.startViewTransition) {
|
|
document.startViewTransition(fn);
|
|
} else {
|
|
fn();
|
|
}
|
|
}
|
|
|
|
// --- IntersectionObserver ---
|
|
|
|
function observeIntersection(el, fn, once, delay) {
|
|
if (!_hasDom || !("IntersectionObserver" in window)) { fn(); return; }
|
|
var fired = false;
|
|
var d = isNil(delay) ? 0 : delay;
|
|
var obs = new IntersectionObserver(function(entries) {
|
|
entries.forEach(function(entry) {
|
|
if (!entry.isIntersecting) return;
|
|
if (once && fired) return;
|
|
fired = true;
|
|
if (once) obs.unobserve(el);
|
|
if (d) setTimeout(fn, d); else fn();
|
|
});
|
|
});
|
|
obs.observe(el);
|
|
}
|
|
|
|
// --- EventSource ---
|
|
|
|
function eventSourceConnect(url, el) {
|
|
var source = new EventSource(url);
|
|
source.addEventListener("error", function() { domDispatch(el, "sx:sseError", {}); });
|
|
source.addEventListener("open", function() { domDispatch(el, "sx:sseOpen", {}); });
|
|
if (typeof MutationObserver !== "undefined") {
|
|
var obs = new MutationObserver(function() {
|
|
if (!document.body.contains(el)) { source.close(); obs.disconnect(); }
|
|
});
|
|
obs.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
return source;
|
|
}
|
|
|
|
function eventSourceListen(source, event, fn) {
|
|
source.addEventListener(event, function(e) { fn(e.data); });
|
|
}
|
|
|
|
// --- Boost bindings ---
|
|
|
|
function bindBoostLink(el, _href) {
|
|
el.addEventListener("click", function(e) {
|
|
e.preventDefault();
|
|
// Re-read href from element at click time (not closed-over value)
|
|
var liveHref = el.getAttribute("href") || _href;
|
|
console.log("[sx-debug] bindBoostLink click:", liveHref, "el:", el.tagName, el.textContent.slice(0,30));
|
|
executeRequest(el, { method: "GET", url: liveHref }).then(function() {
|
|
console.log("[sx-debug] boost fetch OK, pushState:", liveHref);
|
|
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
|
|
}).catch(function(err) {
|
|
console.error("[sx-debug] boost fetch ERROR:", err);
|
|
});
|
|
});
|
|
}
|
|
|
|
function bindBoostForm(form, _method, _action) {
|
|
form.addEventListener("submit", function(e) {
|
|
e.preventDefault();
|
|
// Re-read from element at submit time
|
|
var liveMethod = (form.getAttribute("method") || _method || "GET").toUpperCase();
|
|
var liveAction = form.getAttribute("action") || _action || location.href;
|
|
executeRequest(form, { method: liveMethod, url: liveAction }).then(function() {
|
|
try { history.pushState({ sxUrl: liveAction, scrollY: window.scrollY }, "", liveAction); } catch (err) {}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- Client-side route bindings ---
|
|
|
|
function bindClientRouteClick(link, _href, fallbackFn) {
|
|
link.addEventListener("click", function(e) {
|
|
e.preventDefault();
|
|
// Re-read href from element at click time (not closed-over value)
|
|
var liveHref = link.getAttribute("href") || _href;
|
|
var pathname = urlPathname(liveHref);
|
|
console.log("[sx-debug] bindClientRouteClick:", pathname, "el:", link.tagName, link.textContent.slice(0,30));
|
|
// Find target selector: sx-boost ancestor, explicit sx-target, or #main-panel
|
|
var boostEl = link.closest("[sx-boost]");
|
|
var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null;
|
|
if (!targetSel || targetSel === "true") {
|
|
targetSel = link.getAttribute("sx-target") || "#main-panel";
|
|
}
|
|
console.log("[sx-debug] targetSel:", targetSel, "trying client route...");
|
|
if (tryClientRoute(pathname, targetSel)) {
|
|
console.log("[sx-debug] client route SUCCESS, pushState:", liveHref);
|
|
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
|
|
if (typeof window !== "undefined") window.scrollTo(0, 0);
|
|
} else {
|
|
console.log("[sx-debug] client route FAILED, server fetch:", liveHref);
|
|
executeRequest(link, { method: "GET", url: liveHref }).then(function() {
|
|
console.log("[sx-debug] server fetch OK, pushState:", liveHref);
|
|
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
|
|
}).catch(function(err) {
|
|
console.error("[sx-debug] server fetch ERROR:", err);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function tryEvalContent(source, env) {
|
|
try {
|
|
var merged = merge(componentEnv);
|
|
if (env && !isNil(env)) {
|
|
var ks = Object.keys(env);
|
|
for (var i = 0; i < ks.length; i++) merged[ks[i]] = env[ks[i]];
|
|
}
|
|
return sxRenderWithEnv(source, merged);
|
|
} catch (e) {
|
|
logInfo("sx:route eval miss: " + (e && e.message ? e.message : e));
|
|
return NIL;
|
|
}
|
|
}
|
|
|
|
// Async eval with callback — used for pages with IO deps.
|
|
// Calls callback(rendered) when done, callback(null) on failure.
|
|
function tryAsyncEvalContent(source, env, callback) {
|
|
var merged = merge(componentEnv);
|
|
if (env && !isNil(env)) {
|
|
var ks = Object.keys(env);
|
|
for (var i = 0; i < ks.length; i++) merged[ks[i]] = env[ks[i]];
|
|
}
|
|
try {
|
|
var result = asyncSxRenderWithEnv(source, merged);
|
|
if (isPromise(result)) {
|
|
result.then(function(rendered) {
|
|
callback(rendered);
|
|
}).catch(function(e) {
|
|
logWarn("sx:async eval miss: " + (e && e.message ? e.message : e));
|
|
callback(null);
|
|
});
|
|
} else {
|
|
callback(result);
|
|
}
|
|
} catch (e) {
|
|
logInfo("sx:async eval miss: " + (e && e.message ? e.message : e));
|
|
callback(null);
|
|
}
|
|
}
|
|
|
|
function resolvePageData(pageName, params, callback) {
|
|
// Platform implementation: fetch page data via HTTP from /sx/data/ endpoint.
|
|
// The spec only knows about resolve-page-data(name, params, callback) —
|
|
// this function provides the concrete transport.
|
|
var url = "/sx/data/" + encodeURIComponent(pageName);
|
|
if (params && !isNil(params)) {
|
|
var qs = [];
|
|
var ks = Object.keys(params);
|
|
for (var i = 0; i < ks.length; i++) {
|
|
var v = params[ks[i]];
|
|
if (v !== null && v !== undefined && v !== NIL) {
|
|
qs.push(encodeURIComponent(ks[i]) + "=" + encodeURIComponent(v));
|
|
}
|
|
}
|
|
if (qs.length) url += "?" + qs.join("&");
|
|
}
|
|
var headers = { "SX-Request": "true" };
|
|
fetch(url, { headers: headers }).then(function(resp) {
|
|
if (!resp.ok) {
|
|
logWarn("sx:data resolve failed " + resp.status + " for " + pageName);
|
|
return;
|
|
}
|
|
return resp.text().then(function(text) {
|
|
try {
|
|
var exprs = parse(text);
|
|
var data = exprs.length === 1 ? exprs[0] : {};
|
|
callback(data || {});
|
|
} catch (e) {
|
|
logWarn("sx:data parse error for " + pageName + ": " + (e && e.message ? e.message : e));
|
|
}
|
|
});
|
|
}).catch(function(err) {
|
|
logWarn("sx:data resolve error for " + pageName + ": " + (err && err.message ? err.message : err));
|
|
});
|
|
}
|
|
|
|
function parseSxData(text) {
|
|
// Parse SX text into a data value. Returns the first parsed expression,
|
|
// or NIL on error. Used by cache update directives.
|
|
try {
|
|
var exprs = parse(text);
|
|
return exprs.length >= 1 ? exprs[0] : NIL;
|
|
} catch (e) {
|
|
logWarn("sx:cache parse error: " + (e && e.message ? e.message : e));
|
|
return NIL;
|
|
}
|
|
}
|
|
|
|
function swPostMessage(msg) {
|
|
// Send a message to the active service worker (if registered).
|
|
// Used to notify SW of cache invalidation.
|
|
if (typeof navigator !== "undefined" && navigator.serviceWorker &&
|
|
navigator.serviceWorker.controller) {
|
|
navigator.serviceWorker.controller.postMessage(msg);
|
|
}
|
|
}
|
|
|
|
function urlPathname(href) {
|
|
try {
|
|
return new URL(href, location.href).pathname;
|
|
} catch (e) {
|
|
// Fallback: strip query/hash
|
|
var idx = href.indexOf("?");
|
|
if (idx >= 0) href = href.substring(0, idx);
|
|
idx = href.indexOf("#");
|
|
if (idx >= 0) href = href.substring(0, idx);
|
|
return href;
|
|
}
|
|
}
|
|
|
|
// --- Preload binding ---
|
|
|
|
function bindPreload(el, events, debounceMs, fn) {
|
|
var timer = null;
|
|
events.forEach(function(evt) {
|
|
el.addEventListener(evt, function() {
|
|
if (debounceMs) {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(fn, debounceMs);
|
|
} else {
|
|
fn();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- Processing markers ---
|
|
|
|
var PROCESSED = "_sxBound";
|
|
|
|
function markProcessed(el, key) { el[PROCESSED + key] = true; }
|
|
function isProcessed(el, key) { return !!el[PROCESSED + key]; }
|
|
|
|
// --- Script cloning ---
|
|
|
|
function createScriptClone(dead) {
|
|
var live = document.createElement("script");
|
|
for (var i = 0; i < dead.attributes.length; i++)
|
|
live.setAttribute(dead.attributes[i].name, dead.attributes[i].value);
|
|
live.textContent = dead.textContent;
|
|
return live;
|
|
}
|
|
|
|
// --- SX API references ---
|
|
|
|
function sxRender(source) {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
if (SxObj && SxObj.render) return SxObj.render(source);
|
|
throw new Error("No SX renderer available");
|
|
}
|
|
|
|
function sxProcessScripts(root) {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
var r = (root && root !== NIL) ? root : undefined;
|
|
if (SxObj && SxObj.processScripts) SxObj.processScripts(r);
|
|
}
|
|
|
|
function sxHydrate(root) {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
var r = (root && root !== NIL) ? root : undefined;
|
|
if (SxObj && SxObj.hydrate) SxObj.hydrate(r);
|
|
}
|
|
|
|
function loadedComponentNames() {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
if (!SxObj) return [];
|
|
var env = SxObj.componentEnv || (SxObj.getEnv ? SxObj.getEnv() : {});
|
|
return Object.keys(env).filter(function(k) { return k.charAt(0) === "~"; });
|
|
}
|
|
|
|
// --- Response processing ---
|
|
|
|
function stripComponentScripts(text) {
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
return text.replace(/<script[^>]*type="text\\/sx"[^>]*data-components[^>]*>([\\s\\S]*?)<\\/script>/gi,
|
|
function(_, defs) { if (SxObj && SxObj.loadComponents) SxObj.loadComponents(defs); return ""; });
|
|
}
|
|
|
|
function stripSxScripts(text) {
|
|
// Strip <script type="text/sx">...</script> (without data-components or data-init).
|
|
// These contain extra component defs from streaming resolve chunks.
|
|
// data-init scripts are preserved for process-sx-scripts to evaluate as side effects.
|
|
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
|
return text.replace(/<script[^>]*type="text\\/sx"[^>]*>[\\s\\S]*?<\\/script>/gi,
|
|
function(match) {
|
|
if (/data-init/.test(match)) return match; // preserve data-init scripts
|
|
var m = match.match(/<script[^>]*>([\\s\\S]*?)<\\/script>/i);
|
|
if (m && SxObj && SxObj.loadComponents) SxObj.loadComponents(m[1]);
|
|
return "";
|
|
});
|
|
}
|
|
|
|
function extractResponseCss(text) {
|
|
if (!_hasDom) return text;
|
|
var target = document.getElementById("sx-css");
|
|
if (!target) return text;
|
|
return text.replace(/<style[^>]*data-sx-css[^>]*>([\\s\\S]*?)<\\/style>/gi,
|
|
function(_, css) { target.textContent += css; return ""; });
|
|
}
|
|
|
|
function selectFromContainer(container, sel) {
|
|
var frag = document.createDocumentFragment();
|
|
sel.split(",").forEach(function(s) {
|
|
container.querySelectorAll(s.trim()).forEach(function(m) { frag.appendChild(m); });
|
|
});
|
|
return frag;
|
|
}
|
|
|
|
function childrenToFragment(container) {
|
|
var frag = document.createDocumentFragment();
|
|
while (container.firstChild) frag.appendChild(container.firstChild);
|
|
return frag;
|
|
}
|
|
|
|
function selectHtmlFromDoc(doc, sel) {
|
|
var parts = sel.split(",").map(function(s) { return s.trim(); });
|
|
var frags = [];
|
|
parts.forEach(function(s) {
|
|
doc.querySelectorAll(s).forEach(function(m) { frags.push(m.outerHTML); });
|
|
});
|
|
return frags.join("");
|
|
}
|
|
|
|
// --- Parsing ---
|
|
|
|
function tryParseJson(s) {
|
|
if (!s) return NIL;
|
|
try { return JSON.parse(s); } catch (e) { return NIL; }
|
|
}
|
|
"""
|
|
|
|
|
|
PLATFORM_BOOT_JS = """
|
|
// =========================================================================
|
|
// Platform interface — Boot (mount, hydrate, scripts, cookies)
|
|
// =========================================================================
|
|
|
|
function resolveMountTarget(target) {
|
|
if (typeof target === "string") return _hasDom ? document.querySelector(target) : null;
|
|
return target;
|
|
}
|
|
|
|
function sxRenderWithEnv(source, extraEnv) {
|
|
var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
|
|
var exprs = parse(source);
|
|
if (!_hasDom) return null;
|
|
var frag = document.createDocumentFragment();
|
|
for (var i = 0; i < exprs.length; i++) {
|
|
var node = renderToDom(exprs[i], env, null);
|
|
if (node && !node._spread) frag.appendChild(node);
|
|
}
|
|
return frag;
|
|
}
|
|
|
|
function getRenderEnv(extraEnv) {
|
|
return extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
|
|
}
|
|
|
|
function mergeEnvs(base, newEnv) {
|
|
return newEnv ? merge(componentEnv, base, newEnv) : merge(componentEnv, base);
|
|
}
|
|
|
|
function sxLoadComponents(text) {
|
|
try {
|
|
var exprs = parse(text);
|
|
for (var i = 0; i < exprs.length; i++) trampoline(evalExpr(exprs[i], componentEnv));
|
|
} catch (err) {
|
|
logParseError("loadComponents", text, err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function setDocumentTitle(s) {
|
|
if (_hasDom) document.title = s || "";
|
|
}
|
|
|
|
function removeHeadElement(sel) {
|
|
if (!_hasDom) return;
|
|
var old = document.head.querySelector(sel);
|
|
if (old) old.parentNode.removeChild(old);
|
|
}
|
|
|
|
function querySxScripts(root) {
|
|
if (!_hasDom) return [];
|
|
var r = (root && root !== NIL) ? root : document;
|
|
return Array.prototype.slice.call(
|
|
r.querySelectorAll('script[type="text/sx"]'));
|
|
}
|
|
|
|
function queryPageScripts() {
|
|
if (!_hasDom) return [];
|
|
return Array.prototype.slice.call(
|
|
document.querySelectorAll('script[type="text/sx-pages"]'));
|
|
}
|
|
|
|
// --- localStorage ---
|
|
|
|
function localStorageGet(key) {
|
|
try { var v = localStorage.getItem(key); return v === null ? NIL : v; }
|
|
catch (e) { return NIL; }
|
|
}
|
|
|
|
function localStorageSet(key, val) {
|
|
try { localStorage.setItem(key, val); } catch (e) {}
|
|
}
|
|
|
|
function localStorageRemove(key) {
|
|
try { localStorage.removeItem(key); } catch (e) {}
|
|
}
|
|
|
|
// --- Cookies ---
|
|
|
|
function setSxCompCookie(hash) {
|
|
if (_hasDom) document.cookie = "sx-comp-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
|
}
|
|
|
|
function clearSxCompCookie() {
|
|
if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
|
|
}
|
|
|
|
// --- Env helpers ---
|
|
|
|
function parseEnvAttr(el) {
|
|
var attr = el && el.getAttribute ? el.getAttribute("data-sx-env") : null;
|
|
if (!attr) return {};
|
|
try { return JSON.parse(attr); } catch (e) { return {}; }
|
|
}
|
|
|
|
function storeEnvAttr(el, base, newEnv) {
|
|
var merged = merge(base, newEnv);
|
|
if (el && el.setAttribute) el.setAttribute("data-sx-env", JSON.stringify(merged));
|
|
}
|
|
|
|
function toKebab(s) { return s.replace(/_/g, "-"); }
|
|
|
|
// --- Logging ---
|
|
|
|
function logInfo(msg) {
|
|
if (typeof console !== "undefined") console.log("[sx-ref] " + msg);
|
|
}
|
|
|
|
function logWarn(msg) {
|
|
if (typeof console !== "undefined") console.warn("[sx-ref] " + msg);
|
|
}
|
|
|
|
function logParseError(label, text, err) {
|
|
if (typeof console === "undefined") return;
|
|
var msg = err && err.message ? err.message : String(err);
|
|
var colMatch = msg.match(/col (\\d+)/);
|
|
var lineMatch = msg.match(/line (\\d+)/);
|
|
if (colMatch && text) {
|
|
var errLine = lineMatch ? parseInt(lineMatch[1]) : 1;
|
|
var errCol = parseInt(colMatch[1]);
|
|
var lines = text.split("\\n");
|
|
var pos = 0;
|
|
for (var i = 0; i < errLine - 1 && i < lines.length; i++) pos += lines[i].length + 1;
|
|
pos += errCol;
|
|
var ws = 80;
|
|
var start = Math.max(0, pos - ws);
|
|
var end = Math.min(text.length, pos + ws);
|
|
console.error("[sx-ref] " + label + ":", msg,
|
|
"\\n around error (pos ~" + pos + "):",
|
|
"\\n \\u00ab" + text.substring(start, pos) + "\\u26d4" + text.substring(pos, end) + "\\u00bb");
|
|
} else {
|
|
console.error("[sx-ref] " + label + ":", msg);
|
|
}
|
|
}
|
|
|
|
"""
|
|
|
|
|
|
def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_page_helpers=False):
|
|
lines = ['''
|
|
// =========================================================================
|
|
// Post-transpilation fixups
|
|
// =========================================================================
|
|
// The reference spec's call-lambda only handles Lambda objects, but HO forms
|
|
// (map, reduce, etc.) may receive native primitives. Wrap to handle both.
|
|
var _rawCallLambda = callLambda;
|
|
callLambda = function(f, args, callerEnv) {
|
|
if (typeof f === "function") return f.apply(null, args);
|
|
return _rawCallLambda(f, args, callerEnv);
|
|
};
|
|
|
|
// Expose render functions as primitives so SX code can call them''']
|
|
if has_html:
|
|
lines.append(' if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml;')
|
|
if has_sx:
|
|
lines.append(' if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx;')
|
|
lines.append(' if (typeof aser === "function") PRIMITIVES["aser"] = aser;')
|
|
if has_dom:
|
|
lines.append(' if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;')
|
|
if has_signals:
|
|
lines.append('''
|
|
// Expose signal functions as primitives so runtime-evaluated SX code
|
|
// (e.g. island bodies from .sx files) can call them
|
|
PRIMITIVES["signal"] = signal;
|
|
PRIMITIVES["signal?"] = isSignal;
|
|
PRIMITIVES["deref"] = deref;
|
|
PRIMITIVES["reset!"] = reset_b;
|
|
PRIMITIVES["swap!"] = swap_b;
|
|
PRIMITIVES["computed"] = computed;
|
|
PRIMITIVES["effect"] = effect;
|
|
PRIMITIVES["batch"] = batch;
|
|
// Timer primitives for island code
|
|
PRIMITIVES["set-interval"] = setInterval_;
|
|
PRIMITIVES["clear-interval"] = clearInterval_;
|
|
// Reactive DOM helpers for island code
|
|
PRIMITIVES["reactive-text"] = reactiveText;
|
|
PRIMITIVES["create-text-node"] = createTextNode;
|
|
PRIMITIVES["dom-set-text-content"] = domSetTextContent;
|
|
PRIMITIVES["dom-listen"] = domListen;
|
|
PRIMITIVES["dom-dispatch"] = domDispatch;
|
|
PRIMITIVES["event-detail"] = eventDetail;
|
|
PRIMITIVES["resource"] = resource;
|
|
PRIMITIVES["promise-delayed"] = promiseDelayed;
|
|
PRIMITIVES["promise-then"] = promiseThen;
|
|
PRIMITIVES["def-store"] = defStore;
|
|
PRIMITIVES["use-store"] = useStore;
|
|
PRIMITIVES["emit-event"] = emitEvent;
|
|
PRIMITIVES["on-event"] = onEvent;
|
|
PRIMITIVES["bridge-event"] = bridgeEvent;
|
|
// DOM primitives for island code
|
|
PRIMITIVES["dom-focus"] = domFocus;
|
|
PRIMITIVES["dom-tag-name"] = domTagName;
|
|
PRIMITIVES["dom-get-prop"] = domGetProp;
|
|
PRIMITIVES["dom-set-prop"] = domSetProp;
|
|
PRIMITIVES["dom-call-method"] = domCallMethod;
|
|
PRIMITIVES["dom-post-message"] = domPostMessage;
|
|
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
|
PRIMITIVES["error-message"] = errorMessage;
|
|
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
|
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
|
PRIMITIVES["filter"] = filter;
|
|
// DOM primitives for sx-on:* handlers and data-init scripts
|
|
if (typeof domBody === "function") PRIMITIVES["dom-body"] = domBody;
|
|
if (typeof domQuery === "function") PRIMITIVES["dom-query"] = domQuery;
|
|
if (typeof domQueryAll === "function") PRIMITIVES["dom-query-all"] = domQueryAll;
|
|
if (typeof domQueryById === "function") PRIMITIVES["dom-query-by-id"] = domQueryById;
|
|
if (typeof domSetAttr === "function") PRIMITIVES["dom-set-attr"] = domSetAttr;
|
|
if (typeof domGetAttr === "function") PRIMITIVES["dom-get-attr"] = domGetAttr;
|
|
if (typeof domRemoveAttr === "function") PRIMITIVES["dom-remove-attr"] = domRemoveAttr;
|
|
if (typeof domHasAttr === "function") PRIMITIVES["dom-has-attr?"] = domHasAttr;
|
|
if (typeof domAddClass === "function") PRIMITIVES["dom-add-class"] = domAddClass;
|
|
if (typeof domRemoveClass === "function") PRIMITIVES["dom-remove-class"] = domRemoveClass;
|
|
if (typeof domHasClass === "function") PRIMITIVES["dom-has-class?"] = domHasClass;
|
|
if (typeof domClosest === "function") PRIMITIVES["dom-closest"] = domClosest;
|
|
if (typeof domMatches === "function") PRIMITIVES["dom-matches?"] = domMatches;
|
|
if (typeof preventDefault_ === "function") PRIMITIVES["prevent-default"] = preventDefault_;
|
|
if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue;
|
|
if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml;
|
|
if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml;
|
|
if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent;
|
|
if (typeof domCreateElement === "function") PRIMITIVES["dom-create-element"] = domCreateElement;
|
|
if (typeof domAppend === "function") PRIMITIVES["dom-append"] = domAppend;
|
|
if (typeof domAppendToHead === "function") PRIMITIVES["dom-append-to-head"] = domAppendToHead;
|
|
if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse;
|
|
if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs;
|
|
PRIMITIVES["sx-parse"] = sxParse;
|
|
PRIMITIVES["console-log"] = function() { console.log.apply(console, ["[sx]"].concat(Array.prototype.slice.call(arguments))); return arguments.length > 0 ? arguments[0] : NIL; };''')
|
|
if has_deps:
|
|
lines.append('''
|
|
// Expose deps module functions as primitives so runtime-evaluated SX code
|
|
// (e.g. test-deps.sx in browser) can call them
|
|
// Platform functions (from PLATFORM_DEPS_JS)
|
|
PRIMITIVES["component-deps"] = componentDeps;
|
|
PRIMITIVES["component-set-deps!"] = componentSetDeps;
|
|
PRIMITIVES["component-css-classes"] = componentCssClasses;
|
|
PRIMITIVES["env-components"] = envComponents;
|
|
PRIMITIVES["regex-find-all"] = regexFindAll;
|
|
PRIMITIVES["scan-css-classes"] = scanCssClasses;
|
|
// Transpiled functions (from deps.sx)
|
|
PRIMITIVES["scan-refs"] = scanRefs;
|
|
PRIMITIVES["scan-refs-walk"] = scanRefsWalk;
|
|
PRIMITIVES["transitive-deps"] = transitiveDeps;
|
|
PRIMITIVES["transitive-deps-walk"] = transitiveDepsWalk;
|
|
PRIMITIVES["compute-all-deps"] = computeAllDeps;
|
|
PRIMITIVES["scan-components-from-source"] = scanComponentsFromSource;
|
|
PRIMITIVES["components-needed"] = componentsNeeded;
|
|
PRIMITIVES["page-component-bundle"] = pageComponentBundle;
|
|
PRIMITIVES["page-css-classes"] = pageCssClasses;
|
|
PRIMITIVES["scan-io-refs-walk"] = scanIoRefsWalk;
|
|
PRIMITIVES["scan-io-refs"] = scanIoRefs;
|
|
PRIMITIVES["transitive-io-refs-walk"] = transitiveIoRefsWalk;
|
|
PRIMITIVES["transitive-io-refs"] = transitiveIoRefs;
|
|
PRIMITIVES["compute-all-io-refs"] = computeAllIoRefs;
|
|
PRIMITIVES["component-io-refs-cached"] = componentIoRefsCached;
|
|
PRIMITIVES["component-pure?"] = componentPure_p;
|
|
PRIMITIVES["render-target"] = renderTarget;
|
|
PRIMITIVES["page-render-plan"] = pageRenderPlan;''')
|
|
if has_page_helpers:
|
|
lines.append('''
|
|
// Expose page-helper functions as primitives
|
|
PRIMITIVES["categorize-special-forms"] = categorizeSpecialForms;
|
|
PRIMITIVES["extract-define-kwargs"] = extractDefineKwargs;
|
|
PRIMITIVES["build-reference-data"] = buildReferenceData;
|
|
PRIMITIVES["build-ref-items-with-href"] = buildRefItemsWithHref;
|
|
PRIMITIVES["build-attr-detail"] = buildAttrDetail;
|
|
PRIMITIVES["build-header-detail"] = buildHeaderDetail;
|
|
PRIMITIVES["build-event-detail"] = buildEventDetail;
|
|
PRIMITIVES["build-component-source"] = buildComponentSource;
|
|
PRIMITIVES["build-bundle-analysis"] = buildBundleAnalysis;
|
|
PRIMITIVES["build-routing-analysis"] = buildRoutingAnalysis;
|
|
PRIMITIVES["build-affinity-analysis"] = buildAffinityAnalysis;''')
|
|
return "\n".join(lines)
|
|
|
|
|
|
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False, has_signals=False, has_page_helpers=False, has_cek=False):
|
|
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
|
|
if has_parser:
|
|
parser = '''
|
|
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
|
|
var parse = sxParse;'''
|
|
else:
|
|
parser = r'''
|
|
// Minimal fallback parser (no parser adapter)
|
|
function parse(text) {
|
|
throw new Error("Parser adapter not included — cannot parse SX source at runtime");
|
|
}'''
|
|
|
|
# Public API — conditional on adapters
|
|
api_lines = [parser, '''
|
|
// =========================================================================
|
|
// Public API
|
|
// =========================================================================
|
|
|
|
var componentEnv = {};
|
|
|
|
function loadComponents(source) {
|
|
var exprs = parse(source);
|
|
for (var i = 0; i < exprs.length; i++) {
|
|
trampoline(evalExpr(exprs[i], componentEnv));
|
|
}
|
|
}''']
|
|
|
|
# render() — auto-dispatches based on available adapters
|
|
if has_html and has_dom:
|
|
api_lines.append('''
|
|
function render(source) {
|
|
if (!_hasDom) {
|
|
var exprs = parse(source);
|
|
var parts = [];
|
|
for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv)));
|
|
return parts.join("");
|
|
}
|
|
var exprs = parse(source);
|
|
var frag = document.createDocumentFragment();
|
|
for (var i = 0; i < exprs.length; i++) { var _r = renderToDom(exprs[i], merge(componentEnv), null); if (_r && !_r._spread) frag.appendChild(_r); }
|
|
return frag;
|
|
}''')
|
|
elif has_dom:
|
|
api_lines.append('''
|
|
function render(source) {
|
|
var exprs = parse(source);
|
|
var frag = document.createDocumentFragment();
|
|
for (var i = 0; i < exprs.length; i++) { var _r = renderToDom(exprs[i], merge(componentEnv), null); if (_r && !_r._spread) frag.appendChild(_r); }
|
|
return frag;
|
|
}''')
|
|
elif has_html:
|
|
api_lines.append('''
|
|
function render(source) {
|
|
var exprs = parse(source);
|
|
var parts = [];
|
|
for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv)));
|
|
return parts.join("");
|
|
}''')
|
|
else:
|
|
api_lines.append('''
|
|
function render(source) {
|
|
var exprs = parse(source);
|
|
var results = [];
|
|
for (var i = 0; i < exprs.length; i++) results.push(trampoline(evalExpr(exprs[i], merge(componentEnv))));
|
|
return results.length === 1 ? results[0] : results;
|
|
}''')
|
|
|
|
# renderToString helper
|
|
if has_html:
|
|
api_lines.append('''
|
|
function renderToString(source) {
|
|
var exprs = parse(source);
|
|
var parts = [];
|
|
for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv)));
|
|
return parts.join("");
|
|
}''')
|
|
|
|
# Build Sx object
|
|
version = f"ref-2.0 ({adapter_label}, bootstrap-compiled)"
|
|
api_lines.append(f'''
|
|
var Sx = {{
|
|
VERSION: "ref-2.0",
|
|
parse: parse,
|
|
parseAll: parse,
|
|
eval: function(expr, env) {{ return trampoline(evalExpr(expr, env || merge(componentEnv))); }},
|
|
loadComponents: loadComponents,
|
|
render: render,{"" if has_html else ""}
|
|
{"renderToString: renderToString," if has_html else ""}
|
|
serialize: serialize,
|
|
NIL: NIL,
|
|
Symbol: Symbol,
|
|
Keyword: Keyword,
|
|
isTruthy: isSxTruthy,
|
|
isNil: isNil,
|
|
componentEnv: componentEnv,''')
|
|
|
|
if has_html:
|
|
api_lines.append(' renderToHtml: function(expr, env) { return renderToHtml(expr, env || merge(componentEnv)); },')
|
|
if has_sx:
|
|
api_lines.append(' renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); },')
|
|
if has_dom:
|
|
api_lines.append(' renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,')
|
|
if has_engine:
|
|
api_lines.append(' parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,')
|
|
api_lines.append(' parseTime: typeof parseTime === "function" ? parseTime : null,')
|
|
api_lines.append(' defaultTrigger: typeof defaultTrigger === "function" ? defaultTrigger : null,')
|
|
api_lines.append(' parseSwapSpec: typeof parseSwapSpec === "function" ? parseSwapSpec : null,')
|
|
api_lines.append(' parseRetrySpec: typeof parseRetrySpec === "function" ? parseRetrySpec : null,')
|
|
api_lines.append(' nextRetryMs: typeof nextRetryMs === "function" ? nextRetryMs : null,')
|
|
api_lines.append(' filterParams: typeof filterParams === "function" ? filterParams : null,')
|
|
api_lines.append(' morphNode: typeof morphNode === "function" ? morphNode : null,')
|
|
api_lines.append(' morphChildren: typeof morphChildren === "function" ? morphChildren : null,')
|
|
api_lines.append(' swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,')
|
|
if has_orch:
|
|
api_lines.append(' process: typeof processElements === "function" ? processElements : null,')
|
|
api_lines.append(' executeRequest: typeof executeRequest === "function" ? executeRequest : null,')
|
|
api_lines.append(' postSwap: typeof postSwap === "function" ? postSwap : null,')
|
|
if has_boot:
|
|
api_lines.append(' processScripts: typeof processSxScripts === "function" ? processSxScripts : null,')
|
|
api_lines.append(' mount: typeof sxMount === "function" ? sxMount : null,')
|
|
api_lines.append(' hydrate: typeof sxHydrateElements === "function" ? sxHydrateElements : null,')
|
|
api_lines.append(' update: typeof sxUpdateElement === "function" ? sxUpdateElement : null,')
|
|
api_lines.append(' renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,')
|
|
api_lines.append(' getEnv: function() { return componentEnv; },')
|
|
api_lines.append(' resolveSuspense: typeof resolveSuspense === "function" ? resolveSuspense : null,')
|
|
api_lines.append(' hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null,')
|
|
api_lines.append(' disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null,')
|
|
api_lines.append(' init: typeof bootInit === "function" ? bootInit : null,')
|
|
elif has_orch:
|
|
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
|
|
if has_deps:
|
|
api_lines.append(' scanRefs: scanRefs,')
|
|
api_lines.append(' scanComponentsFromSource: scanComponentsFromSource,')
|
|
api_lines.append(' transitiveDeps: transitiveDeps,')
|
|
api_lines.append(' computeAllDeps: computeAllDeps,')
|
|
api_lines.append(' componentsNeeded: componentsNeeded,')
|
|
api_lines.append(' pageComponentBundle: pageComponentBundle,')
|
|
api_lines.append(' pageCssClasses: pageCssClasses,')
|
|
api_lines.append(' scanIoRefs: scanIoRefs,')
|
|
api_lines.append(' transitiveIoRefs: transitiveIoRefs,')
|
|
api_lines.append(' computeAllIoRefs: computeAllIoRefs,')
|
|
api_lines.append(' componentPure_p: componentPure_p,')
|
|
if has_page_helpers:
|
|
api_lines.append(' categorizeSpecialForms: categorizeSpecialForms,')
|
|
api_lines.append(' buildReferenceData: buildReferenceData,')
|
|
api_lines.append(' buildAttrDetail: buildAttrDetail,')
|
|
api_lines.append(' buildHeaderDetail: buildHeaderDetail,')
|
|
api_lines.append(' buildEventDetail: buildEventDetail,')
|
|
api_lines.append(' buildComponentSource: buildComponentSource,')
|
|
api_lines.append(' buildBundleAnalysis: buildBundleAnalysis,')
|
|
api_lines.append(' buildRoutingAnalysis: buildRoutingAnalysis,')
|
|
api_lines.append(' buildAffinityAnalysis: buildAffinityAnalysis,')
|
|
if has_router:
|
|
api_lines.append(' splitPathSegments: splitPathSegments,')
|
|
api_lines.append(' parseRoutePattern: parseRoutePattern,')
|
|
api_lines.append(' matchRoute: matchRoute,')
|
|
api_lines.append(' findMatchingRoute: findMatchingRoute,')
|
|
api_lines.append(' urlToExpr: urlToExpr,')
|
|
api_lines.append(' autoQuoteUnknowns: autoQuoteUnknowns,')
|
|
api_lines.append(' prepareUrlExpr: prepareUrlExpr,')
|
|
|
|
if has_dom:
|
|
api_lines.append(' registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,')
|
|
api_lines.append(' registerIoDeps: typeof registerIoDeps === "function" ? registerIoDeps : null,')
|
|
api_lines.append(' asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,')
|
|
api_lines.append(' asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,')
|
|
if has_signals:
|
|
api_lines.append(' signal: signal,')
|
|
api_lines.append(' deref: deref,')
|
|
api_lines.append(' reset: reset_b,')
|
|
api_lines.append(' swap: swap_b,')
|
|
api_lines.append(' computed: computed,')
|
|
api_lines.append(' effect: effect,')
|
|
api_lines.append(' batch: batch,')
|
|
api_lines.append(' isSignal: isSignal,')
|
|
api_lines.append(' makeSignal: makeSignal,')
|
|
api_lines.append(' defStore: defStore,')
|
|
api_lines.append(' useStore: useStore,')
|
|
api_lines.append(' clearStores: clearStores,')
|
|
api_lines.append(' emitEvent: emitEvent,')
|
|
api_lines.append(' onEvent: onEvent,')
|
|
api_lines.append(' bridgeEvent: bridgeEvent,')
|
|
api_lines.append(' makeSpread: makeSpread,')
|
|
api_lines.append(' isSpread: isSpread,')
|
|
api_lines.append(' spreadAttrs: spreadAttrs,')
|
|
api_lines.append(' collect: sxCollect,')
|
|
api_lines.append(' collected: sxCollected,')
|
|
api_lines.append(' clearCollected: sxClearCollected,')
|
|
api_lines.append(' scopePush: scopePush,')
|
|
api_lines.append(' scopePop: scopePop,')
|
|
api_lines.append(' providePush: providePush,')
|
|
api_lines.append(' providePop: providePop,')
|
|
api_lines.append(' context: sxContext,')
|
|
api_lines.append(' emit: sxEmit,')
|
|
api_lines.append(' emitted: sxEmitted,')
|
|
if has_cek:
|
|
api_lines.append(' cekRun: cekRun,')
|
|
api_lines.append(' makeCekState: makeCekState,')
|
|
api_lines.append(' makeCekValue: makeCekValue,')
|
|
api_lines.append(' cekStep: cekStep,')
|
|
api_lines.append(' cekTerminal: cekTerminal_p,')
|
|
api_lines.append(' cekValue: cekValue,')
|
|
api_lines.append(' makeReactiveResetFrame: makeReactiveResetFrame,')
|
|
api_lines.append(' evalExpr: evalExpr,')
|
|
api_lines.append(f' _version: "{version}"')
|
|
api_lines.append(' };')
|
|
api_lines.append('')
|
|
if has_orch:
|
|
api_lines.append('''
|
|
// --- Popstate listener ---
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("popstate", function(e) {
|
|
handlePopstate(e && e.state ? e.state.scrollY || 0 : 0);
|
|
});
|
|
}''')
|
|
if has_boot:
|
|
api_lines.append('''
|
|
// --- Auto-init ---
|
|
if (typeof document !== "undefined") {
|
|
var _sxInit = function() {
|
|
bootInit();
|
|
// Process any suspense resolutions that arrived before init
|
|
if (global.__sxPending) {
|
|
for (var pi = 0; pi < global.__sxPending.length; pi++) {
|
|
resolveSuspense(global.__sxPending[pi].id, global.__sxPending[pi].sx);
|
|
}
|
|
global.__sxPending = null;
|
|
}
|
|
// Set up direct resolution for future chunks
|
|
global.__sxResolve = function(id, sx) { resolveSuspense(id, sx); };
|
|
// Register service worker for offline data caching
|
|
if ("serviceWorker" in navigator) {
|
|
navigator.serviceWorker.register("/sx-sw.js", { scope: "/" }).then(function(reg) {
|
|
logInfo("sx:sw registered (scope: " + reg.scope + ")");
|
|
}).catch(function(err) {
|
|
logWarn("sx:sw registration failed: " + (err && err.message ? err.message : err));
|
|
});
|
|
}
|
|
};
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", _sxInit);
|
|
} else {
|
|
_sxInit();
|
|
}
|
|
}''')
|
|
elif has_orch:
|
|
api_lines.append('''
|
|
// --- Auto-init ---
|
|
if (typeof document !== "undefined") {
|
|
var _sxInit = function() { engineInit(); };
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", _sxInit);
|
|
} else {
|
|
_sxInit();
|
|
}
|
|
}''')
|
|
|
|
api_lines.append(' if (typeof module !== "undefined" && module.exports) module.exports = Sx;')
|
|
api_lines.append(' else global.Sx = Sx;')
|
|
|
|
return "\n".join(api_lines)
|
|
|
|
|
|
EPILOGUE = '''
|
|
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);'''
|