diff --git a/shared/static/scripts/sx-test.js b/shared/static/scripts/sx-test.js
new file mode 100644
index 0000000..859cb20
--- /dev/null
+++ b/shared/static/scripts/sx-test.js
@@ -0,0 +1,292 @@
+/**
+ * sx-test.js — String renderer for sx.js (Node-only, used by test harness).
+ *
+ * Provides Sx.renderToString() for server-side / test rendering.
+ * Assumes sx.js is loaded first and Sx global is available.
+ */
+;(function (Sx) {
+ "use strict";
+
+ // Pull references from Sx internals
+ var NIL = Sx.NIL;
+ var _eval = Sx._eval;
+ var _types = Sx._types;
+ var RawHTML = _types.RawHTML;
+
+ function isNil(x) { return x === NIL || x === null || x === undefined; }
+ function isSym(x) { return x && x._sym === true; }
+ function isKw(x) { return x && x._kw === true; }
+ function isLambda(x) { return x && x._lambda === true; }
+ function isComponent(x) { return x && x._component === true; }
+ function isMacro(x) { return x && x._macro === true; }
+ function isRaw(x) { return x && x._raw === true; }
+ function isSxTruthy(x) { return x !== false && !isNil(x); }
+
+ function merge(target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var src = arguments[i];
+ if (src) for (var k in src) target[k] = src[k];
+ }
+ return target;
+ }
+
+ // Use the same tag/attr sets as sx.js
+ var HTML_TAGS = Sx._renderDOM ? null : null; // We'll use a local copy
+ var _HTML_TAGS_STR =
+ "html head body title meta link style script base noscript " +
+ "header footer main nav aside section article address hgroup " +
+ "h1 h2 h3 h4 h5 h6 " +
+ "div p blockquote pre figure figcaption ul ol li dl dt dd hr " +
+ "a span em strong small s cite q abbr code var samp kbd sub sup " +
+ "i b u mark ruby rt rp bdi bdo br wbr time data " +
+ "ins del " +
+ "img picture source iframe embed object param video audio track canvas map area " +
+ "table caption colgroup col thead tbody tfoot tr td th " +
+ "form input textarea button select option optgroup label fieldset legend " +
+ "details summary dialog " +
+ "svg path circle rect line ellipse polyline polygon text g defs use " +
+ "clippath lineargradient radialgradient stop pattern mask " +
+ "tspan textpath foreignobject";
+ var _VOID_STR = "area base br col embed hr img input link meta param source track wbr";
+ var _BOOL_STR = "disabled checked readonly required selected autofocus autoplay " +
+ "controls loop muted multiple hidden open novalidate";
+
+ function makeSet(str) {
+ var s = {}, parts = str.split(/\s+/);
+ for (var i = 0; i < parts.length; i++) if (parts[i]) s[parts[i]] = true;
+ return s;
+ }
+
+ HTML_TAGS = makeSet(_HTML_TAGS_STR);
+ var VOID_ELEMENTS = makeSet(_VOID_STR);
+ var BOOLEAN_ATTRS = makeSet(_BOOL_STR);
+
+ // Access expandMacro via Sx._eval on a defmacro — we need to replicate macro expansion
+ // Actually, we need the internal expandMacro. Let's check if Sx exposes it.
+ // Sx._eval handles macro expansion internally, so we can call sxEval for macro forms.
+ var sxEval = _eval;
+
+ // _isRenderExpr — check if an expression is a render-only form
+ function _isRenderExpr(v) {
+ if (!Array.isArray(v) || !v.length) return false;
+ var h = v[0];
+ if (!isSym(h)) return false;
+ var n = h.name;
+ if (n === "<>" || n === "raw!" || n === "if" || n === "when" || n === "cond" ||
+ n === "case" || n === "let" || n === "let*" || n === "begin" || n === "do" ||
+ n === "map" || n === "map-indexed" || n === "filter" || n === "for-each") return true;
+ if (n.charAt(0) === "~") return true;
+ if (HTML_TAGS[n]) return true;
+ return false;
+ }
+
+ // --- String Renderer ---
+
+ function escapeText(s) { return s.replace(/&/g, "&").replace(//g, ">"); }
+ function escapeAttr(s) { return s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); }
+
+ function renderStr(expr, env) {
+ if (isNil(expr) || expr === false || expr === true) return "";
+ if (isRaw(expr)) return expr.html;
+ if (typeof expr === "string") return escapeText(expr);
+ if (typeof expr === "number") return escapeText(String(expr));
+ if (isSym(expr)) return renderStr(sxEval(expr, env), env);
+ if (isKw(expr)) return escapeText(expr.name);
+ if (Array.isArray(expr)) { if (!expr.length) return ""; return renderStrList(expr, env); }
+ if (expr && typeof expr === "object") return "";
+ return escapeText(String(expr));
+ }
+
+ function renderStrList(expr, env) {
+ var head = expr[0];
+ if (!isSym(head)) {
+ var parts = [];
+ for (var i = 0; i < expr.length; i++) parts.push(renderStr(expr[i], env));
+ return parts.join("");
+ }
+ var name = head.name;
+
+ if (name === "raw!") {
+ var ps = [];
+ for (var ri = 1; ri < expr.length; ri++) {
+ var v = sxEval(expr[ri], env);
+ if (isRaw(v)) ps.push(v.html);
+ else if (typeof v === "string") ps.push(v);
+ else if (!isNil(v)) ps.push(String(v));
+ }
+ return ps.join("");
+ }
+ if (name === "<>") {
+ var fs = [];
+ for (var fi = 1; fi < expr.length; fi++) fs.push(renderStr(expr[fi], env));
+ return fs.join("");
+ }
+ if (name === "if") {
+ return isSxTruthy(sxEval(expr[1], env))
+ ? renderStr(expr[2], env)
+ : (expr.length > 3 ? renderStr(expr[3], env) : "");
+ }
+ if (name === "when") {
+ if (!isSxTruthy(sxEval(expr[1], env))) return "";
+ var ws = [];
+ for (var wi = 2; wi < expr.length; wi++) ws.push(renderStr(expr[wi], env));
+ return ws.join("");
+ }
+ if (name === "let" || name === "let*") {
+ var bindings = expr[1], local = merge({}, env);
+ if (Array.isArray(bindings)) {
+ if (bindings.length && Array.isArray(bindings[0])) {
+ for (var li = 0; li < bindings.length; li++) {
+ local[isSym(bindings[li][0]) ? bindings[li][0].name : bindings[li][0]] = sxEval(bindings[li][1], local);
+ }
+ } else {
+ for (var lj = 0; lj < bindings.length; lj += 2) {
+ local[isSym(bindings[lj]) ? bindings[lj].name : bindings[lj]] = sxEval(bindings[lj + 1], local);
+ }
+ }
+ }
+ var ls = [];
+ for (var lk = 2; lk < expr.length; lk++) ls.push(renderStr(expr[lk], local));
+ return ls.join("");
+ }
+ if (name === "begin" || name === "do") {
+ var bs = [];
+ for (var bi = 1; bi < expr.length; bi++) bs.push(renderStr(expr[bi], env));
+ return bs.join("");
+ }
+ if (name === "define" || name === "defcomp" || name === "defmacro" || name === "defhandler") { sxEval(expr, env); return ""; }
+
+ // Macro expansion in string renderer
+ if (name in env && isMacro(env[name])) {
+ var smExp = Sx._expandMacro(env[name], expr.slice(1), env);
+ return renderStr(smExp, env);
+ }
+
+ // Higher-order forms — render-aware
+ if (name === "map") {
+ var mapFn = sxEval(expr[1], env), mapColl = sxEval(expr[2], env);
+ if (!Array.isArray(mapColl)) return "";
+ var mapParts = [];
+ for (var mi = 0; mi < mapColl.length; mi++) {
+ if (isLambda(mapFn)) mapParts.push(renderLambdaStr(mapFn, [mapColl[mi]], env));
+ else mapParts.push(renderStr(mapFn(mapColl[mi]), env));
+ }
+ return mapParts.join("");
+ }
+ if (name === "map-indexed") {
+ var mixFn = sxEval(expr[1], env), mixColl = sxEval(expr[2], env);
+ if (!Array.isArray(mixColl)) return "";
+ var mixParts = [];
+ for (var mxi = 0; mxi < mixColl.length; mxi++) {
+ if (isLambda(mixFn)) mixParts.push(renderLambdaStr(mixFn, [mxi, mixColl[mxi]], env));
+ else mixParts.push(renderStr(mixFn(mxi, mixColl[mxi]), env));
+ }
+ return mixParts.join("");
+ }
+ if (name === "filter") {
+ var filtFn = sxEval(expr[1], env), filtColl = sxEval(expr[2], env);
+ if (!Array.isArray(filtColl)) return "";
+ var filtParts = [];
+ for (var fli = 0; fli < filtColl.length; fli++) {
+ var keep = isLambda(filtFn) ? Sx._callLambda(filtFn, [filtColl[fli]], env) : filtFn(filtColl[fli]);
+ if (isSxTruthy(keep)) filtParts.push(renderStr(filtColl[fli], env));
+ }
+ return filtParts.join("");
+ }
+
+ if (HTML_TAGS[name]) return renderStrElement(name, expr.slice(1), env);
+
+ if (name.charAt(0) === "~") {
+ var comp = env[name];
+ if (isComponent(comp)) return renderStrComponent(comp, expr.slice(1), env);
+ console.warn("sx.js: unknown component " + name);
+ return '
' +
+ 'Unknown component: ' + escapeText(name) + '
';
+ }
+
+ return renderStr(sxEval(expr, env), env);
+ }
+
+ function renderStrElement(tag, args, env) {
+ var attrs = [], children = [];
+ var i = 0;
+ while (i < args.length) {
+ if (isKw(args[i]) && i + 1 < args.length) {
+ var aname = args[i].name, aval = sxEval(args[i + 1], env);
+ i += 2;
+ if (isNil(aval) || aval === false) continue;
+ if (BOOLEAN_ATTRS[aname]) { if (aval) attrs.push(" " + aname); }
+ else if (aval === true) attrs.push(" " + aname);
+ else attrs.push(" " + aname + '="' + escapeAttr(String(aval)) + '"');
+ } else {
+ children.push(args[i]);
+ i++;
+ }
+ }
+ var open = "<" + tag + attrs.join("") + ">";
+ if (VOID_ELEMENTS[tag]) return open;
+ var isRawText = (tag === "script" || tag === "style");
+ var inner = [];
+ for (var ci = 0; ci < children.length; ci++) {
+ var child = children[ci];
+ if (isRawText && typeof child === "string") inner.push(child);
+ else if (isRawText && isSym(child)) inner.push(String(sxEval(child, env)));
+ else inner.push(renderStr(child, env));
+ }
+ return open + inner.join("") + "" + tag + ">";
+ }
+
+ function renderLambdaStr(fn, args, env) {
+ var local = merge({}, fn.closure, env);
+ for (var i = 0; i < fn.params.length; i++) local[fn.params[i]] = args[i];
+ return renderStr(fn.body, local);
+ }
+
+ function renderStrComponent(comp, args, env) {
+ var kwargs = {}, children = [];
+ var i = 0;
+ while (i < args.length) {
+ if (isKw(args[i]) && i + 1 < args.length) {
+ var v = args[i + 1];
+ if (typeof v === "string" || typeof v === "number" ||
+ typeof v === "boolean" || isNil(v) || isKw(v)) {
+ kwargs[args[i].name] = v;
+ } else if (isSym(v)) {
+ kwargs[args[i].name] = sxEval(v, env);
+ } else if (Array.isArray(v) && v.length && isSym(v[0])) {
+ if (_isRenderExpr(v)) {
+ kwargs[args[i].name] = new RawHTML(renderStr(v, env));
+ } else {
+ kwargs[args[i].name] = sxEval(v, env);
+ }
+ } else {
+ kwargs[args[i].name] = v;
+ }
+ i += 2;
+ } else { children.push(args[i]); i++; }
+ }
+ var local = merge({}, comp.closure, env);
+ for (var pi = 0; pi < comp.params.length; pi++) {
+ var p = comp.params[pi];
+ local[p] = (p in kwargs) ? kwargs[p] : NIL;
+ }
+ if (comp.hasChildren) {
+ var cs = [];
+ for (var ci = 0; ci < children.length; ci++) cs.push(renderStr(children[ci], env));
+ local["children"] = new RawHTML(cs.join(""));
+ }
+ return renderStr(comp.body, local);
+ }
+
+ // --- Public API ---
+
+ Sx.renderToString = function (exprOrText, extraEnv) {
+ var expr = typeof exprOrText === "string" ? Sx.parse(exprOrText) : exprOrText;
+ var env = extraEnv ? merge({}, Sx.getEnv(), extraEnv) : Sx.getEnv();
+ return renderStr(expr, env);
+ };
+
+ Sx._renderStr = renderStr;
+
+})(Sx);
diff --git a/shared/static/scripts/sx.js b/shared/static/scripts/sx.js
index c6769e7..3cb1758 100644
--- a/shared/static/scripts/sx.js
+++ b/shared/static/scripts/sx.js
@@ -12,17 +12,12 @@
;(function (global) {
"use strict";
- // =========================================================================
- // Types
- // =========================================================================
+ // --- Types ---
/** Singleton nil — falsy placeholder. */
var NIL = Object.freeze({ _nil: true, toString: function () { return "nil"; } });
function isNil(x) { return x === NIL || x === null || x === undefined; }
- function isTruthy(x) { return x !== false && !isNil(x) && x !== 0 && x !== ""; }
- // Note: 0 and "" are falsy in sx but we match Python semantics where
- // only nil/false/None are falsy for control flow. Revisit if needed.
function isSxTruthy(x) { return x !== false && !isNil(x); }
function Symbol(name) { this.name = name; }
@@ -70,9 +65,7 @@
function isMacro(x) { return x && x._macro === true; }
function isRaw(x) { return x && x._raw === true; }
- // =========================================================================
- // Parser
- // =========================================================================
+ // --- Parser ---
var RE_WS = /\s+/y;
var RE_COMMENT = /;[^\n]*/y;
@@ -242,9 +235,7 @@
return results;
}
- // =========================================================================
- // Primitives
- // =========================================================================
+ // --- Primitives ---
var PRIMITIVES = {};
@@ -345,9 +336,7 @@
return r;
};
- // =========================================================================
- // Evaluator
- // =========================================================================
+ // --- Evaluator ---
function sxEval(expr, env) {
// Literals
@@ -444,6 +433,68 @@
return sxEval(comp.body, local);
}
+ // --- Shared helpers for special/render forms ---
+
+ function _processBindings(bindings, env) {
+ var local = merge({}, env);
+ if (Array.isArray(bindings)) {
+ if (bindings.length && Array.isArray(bindings[0])) {
+ for (var i = 0; i < bindings.length; i++) {
+ var vname = isSym(bindings[i][0]) ? bindings[i][0].name : bindings[i][0];
+ local[vname] = sxEval(bindings[i][1], local);
+ }
+ } else {
+ for (var j = 0; j < bindings.length; j += 2) {
+ var vn = isSym(bindings[j]) ? bindings[j].name : bindings[j];
+ local[vn] = sxEval(bindings[j + 1], local);
+ }
+ }
+ }
+ return local;
+ }
+
+ function _evalCond(clauses, env) {
+ if (!clauses.length) return null;
+ if (Array.isArray(clauses[0]) && clauses[0].length === 2) {
+ for (var i = 0; i < clauses.length; i++) {
+ var test = clauses[i][0];
+ if ((isSym(test) && (test.name === "else" || test.name === ":else")) ||
+ (isKw(test) && test.name === "else")) return clauses[i][1];
+ if (isSxTruthy(sxEval(test, env))) return clauses[i][1];
+ }
+ } else {
+ for (var j = 0; j < clauses.length - 1; j += 2) {
+ var t = clauses[j];
+ if ((isKw(t) && t.name === "else") || (isSym(t) && (t.name === ":else" || t.name === "else")))
+ return clauses[j + 1];
+ if (isSxTruthy(sxEval(t, env))) return clauses[j + 1];
+ }
+ }
+ return null;
+ }
+
+ function _logParseError(label, text, err, windowSize) {
+ var colMatch = err.message && err.message.match(/col (\d+)/);
+ var lineMatch = err.message && err.message.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 start = Math.max(0, pos - windowSize);
+ var end = Math.min(text.length, pos + windowSize);
+ console.error("sx.js " + label + ":", err.message,
+ "\n total length:", text.length, "lines:", lines.length,
+ "\n error line " + errLine + ":", lines[errLine - 1] ? lines[errLine - 1].substring(0, 200) : "(no such line)",
+ "\n around error (pos ~" + pos + "):",
+ "\n «" + text.substring(start, pos) + "⛔" + text.substring(pos, end) + "»");
+ } else {
+ console.error("sx.js " + label + ":", err.message || err);
+ }
+ }
+
// --- Special forms -------------------------------------------------------
var SPECIAL_FORMS = {};
@@ -462,26 +513,8 @@
};
SPECIAL_FORMS["cond"] = function (expr, env) {
- var clauses = expr.slice(1);
- if (!clauses.length) return NIL;
- // Scheme-style
- if (Array.isArray(clauses[0]) && clauses[0].length === 2) {
- for (var i = 0; i < clauses.length; i++) {
- var test = clauses[i][0];
- if ((isSym(test) && (test.name === "else" || test.name === ":else")) ||
- (isKw(test) && test.name === "else")) return sxEval(clauses[i][1], env);
- if (isSxTruthy(sxEval(test, env))) return sxEval(clauses[i][1], env);
- }
- } else {
- // Clojure-style
- for (var j = 0; j < clauses.length - 1; j += 2) {
- var t = clauses[j];
- if ((isKw(t) && t.name === "else") || (isSym(t) && (t.name === ":else" || t.name === "else")))
- return sxEval(clauses[j + 1], env);
- if (isSxTruthy(sxEval(t, env))) return sxEval(clauses[j + 1], env);
- }
- }
- return NIL;
+ var branch = _evalCond(expr.slice(1), env);
+ return branch ? sxEval(branch, env) : NIL;
};
SPECIAL_FORMS["case"] = function (expr, env) {
@@ -514,22 +547,7 @@
};
SPECIAL_FORMS["let"] = SPECIAL_FORMS["let*"] = function (expr, env) {
- var bindings = expr[1], local = merge({}, env);
- if (Array.isArray(bindings)) {
- if (bindings.length && Array.isArray(bindings[0])) {
- // Scheme-style
- for (var i = 0; i < bindings.length; i++) {
- var vname = isSym(bindings[i][0]) ? bindings[i][0].name : bindings[i][0];
- local[vname] = sxEval(bindings[i][1], local);
- }
- } else {
- // Clojure-style
- for (var j = 0; j < bindings.length; j += 2) {
- var vn = isSym(bindings[j]) ? bindings[j].name : bindings[j];
- local[vn] = sxEval(bindings[j + 1], local);
- }
- }
- }
+ var local = _processBindings(expr[1], env);
var result = NIL;
for (var k = 2; k < expr.length; k++) result = sxEval(expr[k], local);
return result;
@@ -714,9 +732,7 @@
return NIL;
};
- // =========================================================================
- // DOM Renderer
- // =========================================================================
+ // --- DOM Renderer ---
var HTML_TAGS = makeSet(
"html head body title meta link style script base noscript " +
@@ -813,39 +829,12 @@
};
RENDER_FORMS["cond"] = function (expr, env) {
- var clauses = expr.slice(1);
- if (!clauses.length) return document.createDocumentFragment();
- if (Array.isArray(clauses[0]) && clauses[0].length === 2) {
- for (var i = 0; i < clauses.length; i++) {
- var test = clauses[i][0];
- if ((isSym(test) && (test.name === "else" || test.name === ":else")) ||
- (isKw(test) && test.name === "else")) return renderDOM(clauses[i][1], env);
- if (isSxTruthy(sxEval(test, env))) return renderDOM(clauses[i][1], env);
- }
- } else {
- for (var j = 0; j < clauses.length - 1; j += 2) {
- var t = clauses[j];
- if ((isKw(t) && t.name === "else") || (isSym(t) && (t.name === ":else" || t.name === "else")))
- return renderDOM(clauses[j + 1], env);
- if (isSxTruthy(sxEval(t, env))) return renderDOM(clauses[j + 1], env);
- }
- }
- return document.createDocumentFragment();
+ var branch = _evalCond(expr.slice(1), env);
+ return branch ? renderDOM(branch, env) : document.createDocumentFragment();
};
RENDER_FORMS["let"] = RENDER_FORMS["let*"] = function (expr, env) {
- var bindings = expr[1], local = merge({}, env);
- if (Array.isArray(bindings)) {
- if (bindings.length && Array.isArray(bindings[0])) {
- for (var i = 0; i < bindings.length; i++) {
- local[isSym(bindings[i][0]) ? bindings[i][0].name : bindings[i][0]] = sxEval(bindings[i][1], local);
- }
- } else {
- for (var j = 0; j < bindings.length; j += 2) {
- local[isSym(bindings[j]) ? bindings[j].name : bindings[j]] = sxEval(bindings[j + 1], local);
- }
- }
- }
+ var local = _processBindings(expr[1], env);
var frag = document.createDocumentFragment();
for (var k = 2; k < expr.length; k++) frag.appendChild(renderDOM(expr[k], local));
return frag;
@@ -1070,216 +1059,7 @@
return el;
}
- // =========================================================================
- // String Renderer (for SSR parity / testing)
- // =========================================================================
-
- function escapeText(s) { return s.replace(/&/g, "&").replace(//g, ">"); }
- function escapeAttr(s) { return s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); }
-
- function renderStr(expr, env) {
- if (isNil(expr) || expr === false || expr === true) return "";
- if (isRaw(expr)) return expr.html;
- if (typeof expr === "string") return escapeText(expr);
- if (typeof expr === "number") return escapeText(String(expr));
- if (isSym(expr)) return renderStr(sxEval(expr, env), env);
- if (isKw(expr)) return escapeText(expr.name);
- if (Array.isArray(expr)) { if (!expr.length) return ""; return renderStrList(expr, env); }
- if (expr && typeof expr === "object") return "";
- return escapeText(String(expr));
- }
-
- function renderStrList(expr, env) {
- var head = expr[0];
- if (!isSym(head)) {
- var parts = [];
- for (var i = 0; i < expr.length; i++) parts.push(renderStr(expr[i], env));
- return parts.join("");
- }
- var name = head.name;
-
- if (name === "raw!") {
- var ps = [];
- for (var ri = 1; ri < expr.length; ri++) {
- var v = sxEval(expr[ri], env);
- if (isRaw(v)) ps.push(v.html);
- else if (typeof v === "string") ps.push(v);
- else if (!isNil(v)) ps.push(String(v));
- }
- return ps.join("");
- }
- if (name === "<>") {
- var fs = [];
- for (var fi = 1; fi < expr.length; fi++) fs.push(renderStr(expr[fi], env));
- return fs.join("");
- }
- if (name === "if") {
- return isSxTruthy(sxEval(expr[1], env))
- ? renderStr(expr[2], env)
- : (expr.length > 3 ? renderStr(expr[3], env) : "");
- }
- if (name === "when") {
- if (!isSxTruthy(sxEval(expr[1], env))) return "";
- var ws = [];
- for (var wi = 2; wi < expr.length; wi++) ws.push(renderStr(expr[wi], env));
- return ws.join("");
- }
- if (name === "let" || name === "let*") {
- var bindings = expr[1], local = merge({}, env);
- if (Array.isArray(bindings)) {
- if (bindings.length && Array.isArray(bindings[0])) {
- for (var li = 0; li < bindings.length; li++) {
- local[isSym(bindings[li][0]) ? bindings[li][0].name : bindings[li][0]] = sxEval(bindings[li][1], local);
- }
- } else {
- for (var lj = 0; lj < bindings.length; lj += 2) {
- local[isSym(bindings[lj]) ? bindings[lj].name : bindings[lj]] = sxEval(bindings[lj + 1], local);
- }
- }
- }
- var ls = [];
- for (var lk = 2; lk < expr.length; lk++) ls.push(renderStr(expr[lk], local));
- return ls.join("");
- }
- if (name === "begin" || name === "do") {
- var bs = [];
- for (var bi = 1; bi < expr.length; bi++) bs.push(renderStr(expr[bi], env));
- return bs.join("");
- }
- if (name === "define" || name === "defcomp" || name === "defmacro" || name === "defhandler") { sxEval(expr, env); return ""; }
-
- // Macro expansion in string renderer
- if (name in env && isMacro(env[name])) {
- var smExp = expandMacro(env[name], expr.slice(1), env);
- return renderStr(smExp, env);
- }
-
- // Higher-order forms — render-aware (lambda bodies may contain HTML/components)
- if (name === "map") {
- var mapFn = sxEval(expr[1], env), mapColl = sxEval(expr[2], env);
- if (!Array.isArray(mapColl)) return "";
- var mapParts = [];
- for (var mi = 0; mi < mapColl.length; mi++) {
- if (isLambda(mapFn)) mapParts.push(renderLambdaStr(mapFn, [mapColl[mi]], env));
- else mapParts.push(renderStr(mapFn(mapColl[mi]), env));
- }
- return mapParts.join("");
- }
- if (name === "map-indexed") {
- var mixFn = sxEval(expr[1], env), mixColl = sxEval(expr[2], env);
- if (!Array.isArray(mixColl)) return "";
- var mixParts = [];
- for (var mxi = 0; mxi < mixColl.length; mxi++) {
- if (isLambda(mixFn)) mixParts.push(renderLambdaStr(mixFn, [mxi, mixColl[mxi]], env));
- else mixParts.push(renderStr(mixFn(mxi, mixColl[mxi]), env));
- }
- return mixParts.join("");
- }
- if (name === "filter") {
- var filtFn = sxEval(expr[1], env), filtColl = sxEval(expr[2], env);
- if (!Array.isArray(filtColl)) return "";
- var filtParts = [];
- for (var fli = 0; fli < filtColl.length; fli++) {
- var keep = isLambda(filtFn) ? callLambda(filtFn, [filtColl[fli]], env) : filtFn(filtColl[fli]);
- if (isSxTruthy(keep)) filtParts.push(renderStr(filtColl[fli], env));
- }
- return filtParts.join("");
- }
-
- if (HTML_TAGS[name]) return renderStrElement(name, expr.slice(1), env);
-
- if (name.charAt(0) === "~") {
- var comp = env[name];
- if (isComponent(comp)) return renderStrComponent(comp, expr.slice(1), env);
- // Unknown component — return visible warning
- console.warn("sx.js: unknown component " + name);
- return '' +
- 'Unknown component: ' + escapeText(name) + '
';
- }
-
- return renderStr(sxEval(expr, env), env);
- }
-
- function renderStrElement(tag, args, env) {
- var attrs = [], children = [];
- var i = 0;
- while (i < args.length) {
- if (isKw(args[i]) && i + 1 < args.length) {
- var aname = args[i].name, aval = sxEval(args[i + 1], env);
- i += 2;
- if (isNil(aval) || aval === false) continue;
- if (BOOLEAN_ATTRS[aname]) { if (aval) attrs.push(" " + aname); }
- else if (aval === true) attrs.push(" " + aname);
- else attrs.push(" " + aname + '="' + escapeAttr(String(aval)) + '"');
- } else {
- children.push(args[i]);
- i++;
- }
- }
- var open = "<" + tag + attrs.join("") + ">";
- if (VOID_ELEMENTS[tag]) return open;
- var isRawText = (tag === "script" || tag === "style");
- var inner = [];
- for (var ci = 0; ci < children.length; ci++) {
- var child = children[ci];
- if (isRawText && typeof child === "string") inner.push(child);
- else if (isRawText && isSym(child)) inner.push(String(sxEval(child, env)));
- else inner.push(renderStr(child, env));
- }
- return open + inner.join("") + "" + tag + ">";
- }
-
- function renderLambdaStr(fn, args, env) {
- var local = merge({}, fn.closure, env);
- for (var i = 0; i < fn.params.length; i++) local[fn.params[i]] = args[i];
- return renderStr(fn.body, local);
- }
-
- function renderStrComponent(comp, args, env) {
- var kwargs = {}, children = [];
- var i = 0;
- while (i < args.length) {
- if (isKw(args[i]) && i + 1 < args.length) {
- // Evaluate kwarg values eagerly in the caller's env so expressions
- // like (get t "src") resolve while lambda params are still bound.
- var v = args[i + 1];
- if (typeof v === "string" || typeof v === "number" ||
- typeof v === "boolean" || isNil(v) || isKw(v)) {
- kwargs[args[i].name] = v;
- } else if (isSym(v)) {
- kwargs[args[i].name] = sxEval(v, env);
- } else if (Array.isArray(v) && v.length && isSym(v[0])) {
- // Expression with Symbol head — evaluate in caller's env.
- // Render-only forms go through renderStr; data exprs through sxEval.
- if (_isRenderExpr(v)) {
- kwargs[args[i].name] = new RawHTML(renderStr(v, env));
- } else {
- kwargs[args[i].name] = sxEval(v, env);
- }
- } else {
- // Data arrays, dicts, etc — pass through as-is
- kwargs[args[i].name] = v;
- }
- i += 2;
- } else { children.push(args[i]); i++; }
- }
- var local = merge({}, comp.closure, env);
- for (var pi = 0; pi < comp.params.length; pi++) {
- var p = comp.params[pi];
- local[p] = (p in kwargs) ? kwargs[p] : NIL;
- }
- if (comp.hasChildren) {
- var cs = [];
- for (var ci = 0; ci < children.length; ci++) cs.push(renderStr(children[ci], env));
- local["children"] = new RawHTML(cs.join(""));
- }
- return renderStr(comp.body, local);
- }
-
- // =========================================================================
- // Helpers
- // =========================================================================
+ // --- Helpers ---
function merge(target) {
for (var i = 1; i < arguments.length; i++) {
@@ -1298,15 +1078,11 @@
/** Convert snake_case kwargs to kebab-case for sx conventions. */
function toKebab(s) { return s.replace(/_/g, "-"); }
- // =========================================================================
- // Public API
- // =========================================================================
+ // --- Public API ---
var _componentEnv = {};
- // =========================================================================
- // Head auto-hoist: move meta/title/link/script[ld+json] from body to
- // =========================================================================
+ // --- Head auto-hoist ---
var HEAD_HOIST_SELECTOR =
"meta, title, link[rel='canonical'], script[type='application/ld+json']";
@@ -1382,13 +1158,6 @@
return renderDOM(exprOrText, env);
},
- // String Renderer (matches Python html.render output)
- renderToString: function (exprOrText, extraEnv) {
- var expr = typeof exprOrText === "string" ? parse(exprOrText) : exprOrText;
- var env = extraEnv ? merge({}, _componentEnv, extraEnv) : _componentEnv;
- return renderStr(expr, env);
- },
-
/**
* Render a named component with keyword args (Python-style API).
* Sx.renderComponent("card", {title: "Hi"})
@@ -1415,26 +1184,7 @@
var exprs = parseAll(text);
for (var i = 0; i < exprs.length; i++) sxEval(exprs[i], _componentEnv);
} catch (err) {
- // Enhanced error logging: show context around parse failure
- var colMatch = err.message && err.message.match(/col (\d+)/);
- var lineMatch = err.message && err.message.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 li = 0; li < errLine - 1 && li < lines.length; li++) pos += lines[li].length + 1;
- pos += errCol;
- var start = Math.max(0, pos - 120);
- var end = Math.min(text.length, pos + 120);
- console.error("sx.js loadComponents PARSE ERROR:", err.message,
- "\n total length:", text.length, "lines:", lines.length,
- "\n error line " + errLine + ":", lines[errLine - 1] ? lines[errLine - 1].substring(0, 200) : "(no such line)",
- "\n around error (pos ~" + pos + "):",
- "\n «" + text.substring(start, pos) + "⛔" + text.substring(pos, end) + "»");
- } else {
- console.error("sx.js loadComponents error:", err, "\ntext first 500:", text ? text.substring(0, 500) : "(empty)");
- }
+ _logParseError("loadComponents PARSE ERROR", text, err, 120);
throw err;
}
},
@@ -1458,28 +1208,7 @@
try {
node = Sx.render(exprOrText, extraEnv);
} catch (e) {
- if (typeof exprOrText === "string") {
- var src = exprOrText;
- // Find approx position from error message
- var colMatch = e.message && e.message.match(/col (\d+)/);
- var lineMatch = e.message && e.message.match(/line (\d+)/);
- if (colMatch) {
- var errLine = lineMatch ? parseInt(lineMatch[1]) : 1;
- var errCol = parseInt(colMatch[1]);
- var lines = src.split("\n");
- var pos = 0;
- for (var li = 0; li < errLine - 1 && li < lines.length; li++) pos += lines[li].length + 1;
- pos += errCol;
- var start = Math.max(0, pos - 80);
- var end = Math.min(src.length, pos + 80);
- console.error("sx.js MOUNT PARSE ERROR:", e.message,
- "\n source length:", src.length,
- "\n around error (pos ~" + pos + "):",
- "\n «" + src.substring(start, pos) + "⛔" + src.substring(pos, end) + "»");
- } else {
- console.error("sx.js MOUNT PARSE ERROR:", e.message, "\n first 500:", src.substring(0, 500));
- }
- }
+ if (typeof exprOrText === "string") _logParseError("MOUNT PARSE ERROR", exprOrText, e, 80);
throw e;
}
el.textContent = "";
@@ -1619,18 +1348,17 @@
}
},
- // For testing
+ // For testing / sx-test.js
_types: { NIL: NIL, Symbol: Symbol, Keyword: Keyword, Lambda: Lambda, Component: Component, RawHTML: RawHTML },
_eval: sxEval,
- _renderStr: renderStr,
+ _expandMacro: expandMacro,
+ _callLambda: callLambda,
_renderDOM: renderDOM,
};
global.Sx = Sx;
- // =========================================================================
- // SxEngine — native fetch/swap/history engine (replaces HTMX)
- // =========================================================================
+ // --- SxEngine — native fetch/swap/history engine ---
var SxEngine = (function () {
if (typeof document === "undefined") return {};
@@ -1889,24 +1617,7 @@
container.appendChild(sxDom);
// OOB processing on live DOM nodes
- var oobs = container.querySelectorAll("[sx-swap-oob]");
- oobs.forEach(function (oob) {
- var oobSwap = oob.getAttribute("sx-swap-oob") || "outerHTML";
- var oobTarget = document.getElementById(oob.id);
- oob.removeAttribute("sx-swap-oob");
- oob.parentNode.removeChild(oob);
- if (oobTarget) _swapDOM(oobTarget, oob, oobSwap);
- });
-
- // hx-swap-oob compat
- var hxOobs = container.querySelectorAll("[hx-swap-oob]");
- hxOobs.forEach(function (oob) {
- var oobSwap = oob.getAttribute("hx-swap-oob") || "outerHTML";
- var oobTarget = document.getElementById(oob.id);
- oob.removeAttribute("hx-swap-oob");
- oob.parentNode.removeChild(oob);
- if (oobTarget) _swapDOM(oobTarget, oob, oobSwap);
- });
+ _processOOBSwaps(container, _swapDOM);
// sx-select filtering
var selectedDOM;
@@ -1941,27 +1652,7 @@
Sx.processScripts(doc);
// OOB processing
- var oobs = doc.querySelectorAll("[sx-swap-oob]");
- oobs.forEach(function (oob) {
- var oobSwap = oob.getAttribute("sx-swap-oob") || "outerHTML";
- var oobTarget = document.getElementById(oob.id);
- oob.removeAttribute("sx-swap-oob");
- if (oobTarget) {
- _swapContent(oobTarget, oob.outerHTML, oobSwap);
- }
- oob.parentNode.removeChild(oob);
- });
-
- var hxOobs = doc.querySelectorAll("[hx-swap-oob]");
- hxOobs.forEach(function (oob) {
- var oobSwap = oob.getAttribute("hx-swap-oob") || "outerHTML";
- var oobTarget = document.getElementById(oob.id);
- oob.removeAttribute("hx-swap-oob");
- if (oobTarget) {
- _swapContent(oobTarget, oob.outerHTML, oobSwap);
- }
- oob.parentNode.removeChild(oob);
- });
+ _processOOBSwaps(doc, function (t, o, s) { _swapContent(t, o.outerHTML, s); });
// Build final content
var content;
@@ -2150,10 +1841,7 @@
} else {
_morphDOM(target, newNodes);
}
- _activateScripts(parent);
- Sx.processScripts(parent);
- Sx.hydrate(parent);
- SxEngine.process(parent);
+ _postSwap(parent);
return; // early return like existing outerHTML
case "afterend":
target.parentNode.insertBefore(newNodes, target.nextSibling);
@@ -2179,14 +1867,26 @@
_morphChildren(target, wrapper);
}
}
- _activateScripts(target);
- Sx.processScripts(target);
- Sx.hydrate(target);
- SxEngine.process(target);
+ _postSwap(target);
}
// ---- Swap engine (string-based, kept as fallback) ----------------------
+ function _processOOBSwaps(container, swapFn, postSwapFn) {
+ ["sx-swap-oob", "hx-swap-oob"].forEach(function (attr) {
+ container.querySelectorAll("[" + attr + "]").forEach(function (oob) {
+ var swapType = oob.getAttribute(attr) || "outerHTML";
+ var target = document.getElementById(oob.id);
+ oob.removeAttribute(attr);
+ if (oob.parentNode) oob.parentNode.removeChild(oob);
+ if (target) {
+ swapFn(target, oob, swapType);
+ if (postSwapFn) postSwapFn(target);
+ }
+ });
+ });
+ }
+
/** Scripts inserted via innerHTML/insertAdjacentHTML don't execute.
* Recreate them as live elements so the browser fetches & runs them. */
function _activateScripts(root) {
@@ -2201,6 +1901,13 @@
}
}
+ function _postSwap(root) {
+ _activateScripts(root);
+ Sx.processScripts(root);
+ Sx.hydrate(root);
+ SxEngine.process(root);
+ }
+
function _swapContent(target, html, strategy) {
switch (strategy) {
case "innerHTML":
@@ -2211,11 +1918,7 @@
var parent = tgt.parentNode;
tgt.insertAdjacentHTML("afterend", html);
parent.removeChild(tgt);
- // Process parent to catch all newly inserted siblings
- _activateScripts(parent);
- Sx.processScripts(parent);
- Sx.hydrate(parent);
- SxEngine.process(parent);
+ _postSwap(parent);
return; // early return — afterSwap handling done inline
case "afterend":
target.insertAdjacentHTML("afterend", html);
@@ -2235,10 +1938,7 @@
default:
target.innerHTML = html;
}
- _activateScripts(target);
- Sx.processScripts(target);
- Sx.hydrate(target);
- SxEngine.process(target);
+ _postSwap(target);
}
// ---- Retry system -----------------------------------------------------
@@ -2413,37 +2113,11 @@
popContainer.appendChild(popDom);
// Process OOB swaps (sidebar, filter, menu, headers)
- var oobs = popContainer.querySelectorAll("[sx-swap-oob]");
- oobs.forEach(function (oob) {
- var oobSwap = oob.getAttribute("sx-swap-oob") || "outerHTML";
- var oobTarget = document.getElementById(oob.id);
- oob.removeAttribute("sx-swap-oob");
- oob.parentNode.removeChild(oob);
- if (oobTarget) {
- _swapDOM(oobTarget, oob, oobSwap);
- Sx.hydrate(oobTarget);
- SxEngine.process(oobTarget);
- }
- });
- var hxOobs = popContainer.querySelectorAll("[hx-swap-oob]");
- hxOobs.forEach(function (oob) {
- var oobSwap = oob.getAttribute("hx-swap-oob") || "outerHTML";
- var oobTarget = document.getElementById(oob.id);
- oob.removeAttribute("hx-swap-oob");
- oob.parentNode.removeChild(oob);
- if (oobTarget) {
- _swapDOM(oobTarget, oob, oobSwap);
- Sx.hydrate(oobTarget);
- SxEngine.process(oobTarget);
- }
- });
+ _processOOBSwaps(popContainer, _swapDOM, function (t) { Sx.hydrate(t); SxEngine.process(t); });
var newMain = popContainer.querySelector("#main-panel");
_morphChildren(main, newMain || popContainer);
- _activateScripts(main);
- Sx.processScripts(main);
- Sx.hydrate(main);
- SxEngine.process(main);
+ _postSwap(main);
dispatch(document.body, "sx:afterSettle", { target: main });
window.scrollTo(0, e.state && e.state.scrollY || 0);
} catch (err) {
@@ -2456,34 +2130,12 @@
var doc = parser.parseFromString(text, "text/html");
// Process OOB swaps from HTML response
- var hOobs = doc.querySelectorAll("[sx-swap-oob]");
- hOobs.forEach(function (oob) {
- var oobSwap = oob.getAttribute("sx-swap-oob") || "outerHTML";
- var oobTarget = document.getElementById(oob.id);
- oob.removeAttribute("sx-swap-oob");
- if (oobTarget) {
- _swapContent(oobTarget, oob.outerHTML, oobSwap);
- }
- oob.parentNode.removeChild(oob);
- });
- var hhOobs = doc.querySelectorAll("[hx-swap-oob]");
- hhOobs.forEach(function (oob) {
- var oobSwap = oob.getAttribute("hx-swap-oob") || "outerHTML";
- var oobTarget = document.getElementById(oob.id);
- oob.removeAttribute("hx-swap-oob");
- if (oobTarget) {
- _swapContent(oobTarget, oob.outerHTML, oobSwap);
- }
- oob.parentNode.removeChild(oob);
- });
+ _processOOBSwaps(doc, function (t, o, s) { _swapContent(t, o.outerHTML, s); });
var newMain = doc.getElementById("main-panel");
if (newMain) {
_morphChildren(main, newMain);
- _activateScripts(main);
- Sx.processScripts(main);
- Sx.hydrate(main);
- SxEngine.process(main);
+ _postSwap(main);
dispatch(document.body, "sx:afterSettle", { target: main });
window.scrollTo(0, e.state && e.state.scrollY || 0);
} else {
@@ -2561,52 +2213,29 @@
global.SxEngine = SxEngine;
- // =========================================================================
- // Auto-init in browser
- // =========================================================================
+ // --- Auto-init in browser ---
Sx.VERSION = "2026-03-01c-cssx";
// CSS class tracking for on-demand CSS delivery
- var _sxCssKnown = {};
var _sxCssHash = ""; // 8-char hex hash from server
function _initCssTracking() {
var meta = document.querySelector('meta[name="sx-css-classes"]');
if (meta) {
var content = meta.getAttribute("content");
- if (content) {
- // If content is short (≤16 chars), it's a hash from the server
- if (content.length <= 16) {
- _sxCssHash = content;
- } else {
- content.split(",").forEach(function (c) {
- if (c) _sxCssKnown[c] = true;
- });
- }
- }
+ if (content) _sxCssHash = content;
}
}
function _getSxCssHeader() {
- // Prefer sending the hash (compact) over the full class list
- if (_sxCssHash) return _sxCssHash;
- var names = Object.keys(_sxCssKnown);
- return names.length ? names.join(",") : "";
+ return _sxCssHash;
}
function _processCssResponse(text, resp) {
- // Read SX-Css-Hash response header — replaces local hash
var hashHeader = resp.headers.get("SX-Css-Hash");
if (hashHeader) _sxCssHash = hashHeader;
- // Merge SX-Css-Add header into known set (kept for debugging/fallback)
- var addHeader = resp.headers.get("SX-Css-Add");
- if (addHeader) {
- addHeader.split(",").forEach(function (c) {
- if (c) _sxCssKnown[c] = true;
- });
- }
// Extract blocks and inject into