Rebrand sexp → sx across web platform (173 files)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m37s

Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 11:06:57 +00:00
parent 17cebe07e7
commit e8bc228c7f
174 changed files with 3126 additions and 2952 deletions

View File

@@ -1,12 +1,12 @@
/**
* sexp.js S-expression parser, evaluator, and DOM renderer.
* sx.js S-expression parser, evaluator, and DOM renderer.
*
* Client-side counterpart to shared/sexp/ Python modules.
* Client-side counterpart to shared/sx/ Python modules.
* Parses s-expression text, evaluates it, and renders to DOM nodes.
*
* Usage:
* Sexp.loadComponents('(defcomp ~card (&key title) (div :class "c" title))');
* const node = Sexp.render('(~card :title "Hello")');
* Sx.loadComponents('(defcomp ~card (&key title) (div :class "c" title))');
* const node = Sx.render('(~card :title "Hello")');
* document.body.appendChild(node);
*/
;(function (global) {
@@ -21,9 +21,9 @@
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 sexp but we match Python semantics where
// 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 isSexpTruthy(x) { return x !== false && !isNil(x); }
function isSxTruthy(x) { return x !== false && !isNil(x); }
function Symbol(name) { this.name = name; }
Symbol.prototype.toString = function () { return this.name; };
@@ -243,7 +243,7 @@
PRIMITIVES["pow"] = Math.pow;
// Comparison
PRIMITIVES["="] = function (a, b) { return a == b; }; // loose, matches Python sexp
PRIMITIVES["="] = function (a, b) { return a == b; }; // loose, matches Python sx
PRIMITIVES["!="] = function (a, b) { return a != b; };
PRIMITIVES["<"] = function (a, b) { return a < b; };
PRIMITIVES[">"] = function (a, b) { return a > b; };
@@ -251,7 +251,7 @@
PRIMITIVES[">="] = function (a, b) { return a >= b; };
// Logic
PRIMITIVES["not"] = function (x) { return !isSexpTruthy(x); };
PRIMITIVES["not"] = function (x) { return !isSxTruthy(x); };
// String
PRIMITIVES["str"] = function () {
@@ -326,7 +326,7 @@
// Evaluator
// =========================================================================
function sexpEval(expr, env) {
function sxEval(expr, env) {
// Literals
if (typeof expr === "number" || typeof expr === "string" || typeof expr === "boolean") return expr;
if (isNil(expr)) return NIL;
@@ -348,7 +348,7 @@
// Dict literal
if (expr && typeof expr === "object" && !Array.isArray(expr) && !expr._sym && !expr._kw && !expr._raw) {
var d = {};
for (var dk in expr) d[dk] = sexpEval(expr[dk], env);
for (var dk in expr) d[dk] = sxEval(expr[dk], env);
return d;
}
@@ -360,7 +360,7 @@
// Non-callable head → data list
if (!isSym(head) && !isLambda(head) && !Array.isArray(head)) {
return expr.map(function (x) { return sexpEval(x, env); });
return expr.map(function (x) { return sxEval(x, env); });
}
// Special forms
@@ -372,9 +372,9 @@
}
// Function call
var fn = sexpEval(head, env);
var fn = sxEval(head, env);
var args = [];
for (var ai = 1; ai < expr.length; ai++) args.push(sexpEval(expr[ai], env));
for (var ai = 1; ai < expr.length; ai++) args.push(sxEval(expr[ai], env));
if (typeof fn === "function") return fn.apply(null, args);
if (isLambda(fn)) return callLambda(fn, args, env);
@@ -388,7 +388,7 @@
}
var local = merge({}, fn.closure, callerEnv);
for (var i = 0; i < fn.params.length; i++) local[fn.params[i]] = args[i];
return sexpEval(fn.body, local);
return sxEval(fn.body, local);
}
function callComponent(comp, rawArgs, env) {
@@ -396,10 +396,10 @@
var i = 0;
while (i < rawArgs.length) {
if (isKw(rawArgs[i]) && i + 1 < rawArgs.length) {
kwargs[rawArgs[i].name] = sexpEval(rawArgs[i + 1], env);
kwargs[rawArgs[i].name] = sxEval(rawArgs[i + 1], env);
i += 2;
} else {
children.push(sexpEval(rawArgs[i], env));
children.push(sxEval(rawArgs[i], env));
i++;
}
}
@@ -409,7 +409,7 @@
local[p] = (p in kwargs) ? kwargs[p] : NIL;
}
if (comp.hasChildren) local["children"] = children;
return sexpEval(comp.body, local);
return sxEval(comp.body, local);
}
// --- Special forms -------------------------------------------------------
@@ -417,15 +417,15 @@
var SPECIAL_FORMS = {};
SPECIAL_FORMS["if"] = function (expr, env) {
var cond = sexpEval(expr[1], env);
if (isSexpTruthy(cond)) return sexpEval(expr[2], env);
return expr.length > 3 ? sexpEval(expr[3], env) : NIL;
var cond = sxEval(expr[1], env);
if (isSxTruthy(cond)) return sxEval(expr[2], env);
return expr.length > 3 ? sxEval(expr[3], env) : NIL;
};
SPECIAL_FORMS["when"] = function (expr, env) {
if (!isSexpTruthy(sexpEval(expr[1], env))) return NIL;
if (!isSxTruthy(sxEval(expr[1], env))) return NIL;
var result = NIL;
for (var i = 2; i < expr.length; i++) result = sexpEval(expr[i], env);
for (var i = 2; i < expr.length; i++) result = sxEval(expr[i], env);
return result;
};
@@ -437,28 +437,28 @@
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 sexpEval(clauses[i][1], env);
if (isSexpTruthy(sexpEval(test, env))) return sexpEval(clauses[i][1], env);
(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 sexpEval(clauses[j + 1], env);
if (isSexpTruthy(sexpEval(t, env))) return sexpEval(clauses[j + 1], env);
return sxEval(clauses[j + 1], env);
if (isSxTruthy(sxEval(t, env))) return sxEval(clauses[j + 1], env);
}
}
return NIL;
};
SPECIAL_FORMS["case"] = function (expr, env) {
var val = sexpEval(expr[1], env);
var val = sxEval(expr[1], env);
for (var i = 2; i < expr.length - 1; i += 2) {
var t = expr[i];
if ((isKw(t) && t.name === "else") || (isSym(t) && (t.name === ":else" || t.name === "else")))
return sexpEval(expr[i + 1], env);
if (val == sexpEval(t, env)) return sexpEval(expr[i + 1], env);
return sxEval(expr[i + 1], env);
if (val == sxEval(t, env)) return sxEval(expr[i + 1], env);
}
return NIL;
};
@@ -466,8 +466,8 @@
SPECIAL_FORMS["and"] = function (expr, env) {
var result = true;
for (var i = 1; i < expr.length; i++) {
result = sexpEval(expr[i], env);
if (!isSexpTruthy(result)) return result;
result = sxEval(expr[i], env);
if (!isSxTruthy(result)) return result;
}
return result;
};
@@ -475,8 +475,8 @@
SPECIAL_FORMS["or"] = function (expr, env) {
var result = false;
for (var i = 1; i < expr.length; i++) {
result = sexpEval(expr[i], env);
if (isSexpTruthy(result)) return result;
result = sxEval(expr[i], env);
if (isSxTruthy(result)) return result;
}
return result;
};
@@ -488,18 +488,18 @@
// 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] = sexpEval(bindings[i][1], local);
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] = sexpEval(bindings[j + 1], local);
local[vn] = sxEval(bindings[j + 1], local);
}
}
}
var result = NIL;
for (var k = 2; k < expr.length; k++) result = sexpEval(expr[k], local);
for (var k = 2; k < expr.length; k++) result = sxEval(expr[k], local);
return result;
};
@@ -514,7 +514,7 @@
SPECIAL_FORMS["define"] = function (expr, env) {
var name = expr[1].name;
var value = sexpEval(expr[2], env);
var value = sxEval(expr[2], env);
if (isLambda(value) && !value.name) value.name = name;
env[name] = value;
return value;
@@ -541,29 +541,29 @@
SPECIAL_FORMS["begin"] = SPECIAL_FORMS["do"] = function (expr, env) {
var result = NIL;
for (var i = 1; i < expr.length; i++) result = sexpEval(expr[i], env);
for (var i = 1; i < expr.length; i++) result = sxEval(expr[i], env);
return result;
};
SPECIAL_FORMS["quote"] = function (expr) { return expr[1]; };
SPECIAL_FORMS["set!"] = function (expr, env) {
var v = sexpEval(expr[2], env);
var v = sxEval(expr[2], env);
env[expr[1].name] = v;
return v;
};
SPECIAL_FORMS["->"] = function (expr, env) {
var result = sexpEval(expr[1], env);
var result = sxEval(expr[1], env);
for (var i = 2; i < expr.length; i++) {
var form = expr[i];
var fn, args;
if (Array.isArray(form)) {
fn = sexpEval(form[0], env);
fn = sxEval(form[0], env);
args = [result];
for (var j = 1; j < form.length; j++) args.push(sexpEval(form[j], env));
for (var j = 1; j < form.length; j++) args.push(sxEval(form[j], env));
} else {
fn = sexpEval(form, env);
fn = sxEval(form, env);
args = [result];
}
if (typeof fn === "function") result = fn.apply(null, args);
@@ -578,48 +578,48 @@
var HO_FORMS = {};
HO_FORMS["map"] = function (expr, env) {
var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env);
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
return coll.map(function (item) { return isLambda(fn) ? callLambda(fn, [item], env) : fn(item); });
};
HO_FORMS["map-indexed"] = function (expr, env) {
var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env);
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
return coll.map(function (item, i) { return isLambda(fn) ? callLambda(fn, [i, item], env) : fn(i, item); });
};
HO_FORMS["filter"] = function (expr, env) {
var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env);
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
return coll.filter(function (item) {
var r = isLambda(fn) ? callLambda(fn, [item], env) : fn(item);
return isSexpTruthy(r);
return isSxTruthy(r);
});
};
HO_FORMS["reduce"] = function (expr, env) {
var fn = sexpEval(expr[1], env), acc = sexpEval(expr[2], env), coll = sexpEval(expr[3], env);
var fn = sxEval(expr[1], env), acc = sxEval(expr[2], env), coll = sxEval(expr[3], env);
for (var i = 0; i < coll.length; i++) acc = isLambda(fn) ? callLambda(fn, [acc, coll[i]], env) : fn(acc, coll[i]);
return acc;
};
HO_FORMS["some"] = function (expr, env) {
var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env);
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
for (var i = 0; i < coll.length; i++) {
var r = isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]);
if (isSexpTruthy(r)) return r;
if (isSxTruthy(r)) return r;
}
return NIL;
};
HO_FORMS["every?"] = function (expr, env) {
var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env);
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
for (var i = 0; i < coll.length; i++) {
if (!isSexpTruthy(isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]))) return false;
if (!isSxTruthy(isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]))) return false;
}
return true;
};
HO_FORMS["for-each"] = function (expr, env) {
var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env);
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
for (var i = 0; i < coll.length; i++) isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]);
return NIL;
};
@@ -686,7 +686,7 @@
if (typeof expr === "number") return document.createTextNode(String(expr));
// Symbol → evaluate then render
if (isSym(expr)) return renderDOM(sexpEval(expr, env), env);
if (isSym(expr)) return renderDOM(sxEval(expr, env), env);
// Keyword → text
if (isKw(expr)) return document.createTextNode(expr.name);
@@ -707,13 +707,13 @@
var RENDER_FORMS = {};
RENDER_FORMS["if"] = function (expr, env) {
var cond = sexpEval(expr[1], env);
if (isSexpTruthy(cond)) return renderDOM(expr[2], env);
var cond = sxEval(expr[1], env);
if (isSxTruthy(cond)) return renderDOM(expr[2], env);
return expr.length > 3 ? renderDOM(expr[3], env) : document.createDocumentFragment();
};
RENDER_FORMS["when"] = function (expr, env) {
if (!isSexpTruthy(sexpEval(expr[1], env))) return document.createDocumentFragment();
if (!isSxTruthy(sxEval(expr[1], env))) return document.createDocumentFragment();
var frag = document.createDocumentFragment();
for (var i = 2; i < expr.length; i++) frag.appendChild(renderDOM(expr[i], env));
return frag;
@@ -727,14 +727,14 @@
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 (isSexpTruthy(sexpEval(test, env))) 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 (isSexpTruthy(sexpEval(t, env))) return renderDOM(clauses[j + 1], env);
if (isSxTruthy(sxEval(t, env))) return renderDOM(clauses[j + 1], env);
}
}
return document.createDocumentFragment();
@@ -745,11 +745,11 @@
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]] = sexpEval(bindings[i][1], local);
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]] = sexpEval(bindings[j + 1], local);
local[isSym(bindings[j]) ? bindings[j].name : bindings[j]] = sxEval(bindings[j + 1], local);
}
}
}
@@ -764,11 +764,11 @@
return frag;
};
RENDER_FORMS["define"] = function (expr, env) { sexpEval(expr, env); return document.createDocumentFragment(); };
RENDER_FORMS["defcomp"] = function (expr, env) { sexpEval(expr, env); return document.createDocumentFragment(); };
RENDER_FORMS["define"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
RENDER_FORMS["defcomp"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
RENDER_FORMS["map"] = function (expr, env) {
var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env);
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
var frag = document.createDocumentFragment();
for (var i = 0; i < coll.length; i++) {
var val = isLambda(fn) ? renderLambdaDOM(fn, [coll[i]], env) : renderDOM(fn(coll[i]), env);
@@ -778,7 +778,7 @@
};
RENDER_FORMS["map-indexed"] = function (expr, env) {
var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env);
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
var frag = document.createDocumentFragment();
for (var i = 0; i < coll.length; i++) {
var val = isLambda(fn) ? renderLambdaDOM(fn, [i, coll[i]], env) : renderDOM(fn(i, coll[i]), env);
@@ -788,12 +788,12 @@
};
RENDER_FORMS["filter"] = function (expr, env) {
var result = sexpEval(expr, env);
var result = sxEval(expr, env);
return renderDOM(result, env);
};
RENDER_FORMS["for-each"] = function (expr, env) {
var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env);
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
var frag = document.createDocumentFragment();
for (var i = 0; i < coll.length; i++) {
var val = isLambda(fn) ? renderLambdaDOM(fn, [coll[i]], env) : renderDOM(fn(coll[i]), env);
@@ -819,7 +819,7 @@
var v = args[i + 1];
kwargs[args[i].name] = (typeof v === "string" || typeof v === "number" ||
typeof v === "boolean" || isNil(v) || isKw(v))
? v : (isSym(v) ? sexpEval(v, env) : v);
? v : (isSym(v) ? sxEval(v, env) : v);
i += 2;
} else {
children.push(args[i]);
@@ -850,7 +850,7 @@
if (name === "raw!") {
var frag = document.createDocumentFragment();
for (var ri = 1; ri < expr.length; ri++) {
var val = sexpEval(expr[ri], env);
var val = sxEval(expr[ri], env);
if (typeof val === "string") {
var tpl = document.createElement("template");
tpl.innerHTML = val;
@@ -883,7 +883,7 @@
var comp = env[name];
if (isComponent(comp)) return renderComponentDOM(comp, expr.slice(1), env);
// Unknown component — render a visible warning, don't crash
console.warn("sexp.js: unknown component " + name);
console.warn("sx.js: unknown component " + name);
var warn = document.createElement("div");
warn.setAttribute("style",
"background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;" +
@@ -893,11 +893,11 @@
}
// Fallback: evaluate then render
return renderDOM(sexpEval(expr, env), env);
return renderDOM(sxEval(expr, env), env);
}
// Lambda/list head → evaluate
if (isLambda(head) || Array.isArray(head)) return renderDOM(sexpEval(expr, env), env);
if (isLambda(head) || Array.isArray(head)) return renderDOM(sxEval(expr, env), env);
// Data list
var dl = document.createDocumentFragment();
@@ -915,7 +915,7 @@
var arg = args[i];
if (isKw(arg) && i + 1 < args.length) {
var attrName = arg.name;
var attrVal = sexpEval(args[i + 1], env);
var attrVal = sxEval(args[i + 1], env);
i += 2;
if (isNil(attrVal) || attrVal === false) continue;
if (BOOLEAN_ATTRS[attrName]) {
@@ -949,7 +949,7 @@
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(sexpEval(expr, env), env);
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 "";
@@ -968,7 +968,7 @@
if (name === "raw!") {
var ps = [];
for (var ri = 1; ri < expr.length; ri++) {
var v = sexpEval(expr[ri], env);
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));
@@ -981,12 +981,12 @@
return fs.join("");
}
if (name === "if") {
return isSexpTruthy(sexpEval(expr[1], env))
return isSxTruthy(sxEval(expr[1], env))
? renderStr(expr[2], env)
: (expr.length > 3 ? renderStr(expr[3], env) : "");
}
if (name === "when") {
if (!isSexpTruthy(sexpEval(expr[1], env))) return "";
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("");
@@ -996,11 +996,11 @@
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]] = sexpEval(bindings[li][1], local);
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]] = sexpEval(bindings[lj + 1], local);
local[isSym(bindings[lj]) ? bindings[lj].name : bindings[lj]] = sxEval(bindings[lj + 1], local);
}
}
}
@@ -1013,11 +1013,11 @@
for (var bi = 1; bi < expr.length; bi++) bs.push(renderStr(expr[bi], env));
return bs.join("");
}
if (name === "define" || name === "defcomp") { sexpEval(expr, env); return ""; }
if (name === "define" || name === "defcomp") { sxEval(expr, env); return ""; }
// Higher-order forms — render-aware (lambda bodies may contain HTML/components)
if (name === "map") {
var mapFn = sexpEval(expr[1], env), mapColl = sexpEval(expr[2], env);
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++) {
@@ -1027,7 +1027,7 @@
return mapParts.join("");
}
if (name === "map-indexed") {
var mixFn = sexpEval(expr[1], env), mixColl = sexpEval(expr[2], env);
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++) {
@@ -1037,12 +1037,12 @@
return mixParts.join("");
}
if (name === "filter") {
var filtFn = sexpEval(expr[1], env), filtColl = sexpEval(expr[2], env);
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 (isSexpTruthy(keep)) filtParts.push(renderStr(filtColl[fli], env));
if (isSxTruthy(keep)) filtParts.push(renderStr(filtColl[fli], env));
}
return filtParts.join("");
}
@@ -1053,13 +1053,13 @@
var comp = env[name];
if (isComponent(comp)) return renderStrComponent(comp, expr.slice(1), env);
// Unknown component — return visible warning
console.warn("sexp.js: unknown component " + name);
console.warn("sx.js: unknown component " + name);
return '<div style="background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;' +
'padding:4px 8px;margin:2px;border-radius:4px;font-size:12px;font-family:monospace">' +
'Unknown component: ' + escapeText(name) + '</div>';
}
return renderStr(sexpEval(expr, env), env);
return renderStr(sxEval(expr, env), env);
}
function renderStrElement(tag, args, env) {
@@ -1067,7 +1067,7 @@
var i = 0;
while (i < args.length) {
if (isKw(args[i]) && i + 1 < args.length) {
var aname = args[i].name, aval = sexpEval(args[i + 1], env);
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); }
@@ -1099,7 +1099,7 @@
var v = args[i + 1];
kwargs[args[i].name] = (typeof v === "string" || typeof v === "number" ||
typeof v === "boolean" || isNil(v) || isKw(v))
? v : (isSym(v) ? sexpEval(v, env) : v);
? v : (isSym(v) ? sxEval(v, env) : v);
i += 2;
} else { children.push(args[i]); i++; }
}
@@ -1134,7 +1134,7 @@
return s;
}
/** Convert snake_case kwargs to kebab-case for sexp conventions. */
/** Convert snake_case kwargs to kebab-case for sx conventions. */
function toKebab(s) { return s.replace(/_/g, "-"); }
// =========================================================================
@@ -1187,7 +1187,7 @@
}
}
var Sexp = {
var Sx = {
// Types
NIL: NIL,
Symbol: Symbol,
@@ -1198,7 +1198,7 @@
parseAll: parseAll,
// Evaluator
eval: function (expr, env) { return sexpEval(expr, env || _componentEnv); },
eval: function (expr, env) { return sxEval(expr, env || _componentEnv); },
// DOM Renderer
render: function (exprOrText, extraEnv) {
@@ -1216,7 +1216,7 @@
/**
* Render a named component with keyword args (Python-style API).
* Sexp.renderComponent("card", {title: "Hi"})
* Sx.renderComponent("card", {title: "Hi"})
*/
renderComponent: function (name, kwargs, extraEnv) {
var fullName = name.charAt(0) === "~" ? name : "~" + name;
@@ -1237,52 +1237,52 @@
// Component management
loadComponents: function (text) {
var exprs = parseAll(text);
for (var i = 0; i < exprs.length; i++) sexpEval(exprs[i], _componentEnv);
for (var i = 0; i < exprs.length; i++) sxEval(exprs[i], _componentEnv);
},
getEnv: function () { return _componentEnv; },
// Utility
isTruthy: isSexpTruthy,
isTruthy: isSxTruthy,
isNil: isNil,
/**
* Mount a sexp expression into a DOM element, replacing its contents.
* Sexp.mount(el, '(~card :title "Hi")')
* Sexp.mount("#target", '(~card :title "Hi")')
* Sexp.mount(el, '(~card :title name)', {name: "Jo"})
* Mount a sx expression into a DOM element, replacing its contents.
* Sx.mount(el, '(~card :title "Hi")')
* Sx.mount("#target", '(~card :title "Hi")')
* Sx.mount(el, '(~card :title name)', {name: "Jo"})
*/
mount: function (target, exprOrText, extraEnv) {
var el = typeof target === "string" ? document.querySelector(target) : target;
if (!el) return;
var node = Sexp.render(exprOrText, extraEnv);
var node = Sx.render(exprOrText, extraEnv);
el.textContent = "";
el.appendChild(node);
// Auto-hoist head elements (meta, title, link, script[ld+json]) to <head>
_hoistHeadElements(el);
// Process sx- attributes and hydrate the newly mounted content
if (typeof SxEngine !== "undefined") SxEngine.process(el);
Sexp.hydrate(el);
Sx.hydrate(el);
},
/**
* Process all <script type="text/sexp"> tags in the document.
* Process all <script type="text/sx"> tags in the document.
* Tags with data-components load component definitions.
* Tags with data-mount="<selector>" render into that element.
*/
processScripts: function (root) {
var scripts = (root || document).querySelectorAll('script[type="text/sexp"]');
var scripts = (root || document).querySelectorAll('script[type="text/sx"]');
for (var i = 0; i < scripts.length; i++) {
var s = scripts[i];
if (s._sexpProcessed) continue;
s._sexpProcessed = true;
if (s._sxProcessed) continue;
s._sxProcessed = true;
var text = s.textContent;
if (!text || !text.trim()) continue;
// data-components: load as component definitions
if (s.hasAttribute("data-components")) {
Sexp.loadComponents(text);
Sx.loadComponents(text);
continue;
}
@@ -1290,32 +1290,32 @@
var mountSel = s.getAttribute("data-mount");
if (mountSel) {
var target = document.querySelector(mountSel);
if (target) Sexp.mount(target, text);
if (target) Sx.mount(target, text);
continue;
}
// Default: load as components
Sexp.loadComponents(text);
Sx.loadComponents(text);
}
},
/**
* Bind client-side sexp rendering to elements with data-sexp-* attrs.
* Bind client-side sx rendering to elements with data-sx-* attrs.
*
* Pattern:
* <div data-sexp="(~card :title title)" data-sexp-env='{"title":"Hi"}'>
* <div data-sx="(~card :title title)" data-sx-env='{"title":"Hi"}'>
* <!-- server-rendered HTML (hydration target) -->
* </div>
*
* Call Sexp.update(el, {title: "New"}) to re-render with new data.
* Call Sx.update(el, {title: "New"}) to re-render with new data.
*/
update: function (target, newEnv) {
var el = typeof target === "string" ? document.querySelector(target) : target;
if (!el) return;
var source = el.getAttribute("data-sexp");
var source = el.getAttribute("data-sx");
if (!source) return;
var baseEnv = {};
var envAttr = el.getAttribute("data-sexp-env");
var envAttr = el.getAttribute("data-sx-env");
if (envAttr) {
try { baseEnv = JSON.parse(envAttr); } catch (e) { /* ignore */ }
}
@@ -1325,31 +1325,31 @@
el.appendChild(node);
if (newEnv) {
merge(baseEnv, newEnv);
el.setAttribute("data-sexp-env", JSON.stringify(baseEnv));
el.setAttribute("data-sx-env", JSON.stringify(baseEnv));
}
},
/**
* Find all [data-sexp] elements within root and render them.
* Useful after HTMX swaps bring in new sexp-enabled elements.
* Find all [data-sx] elements within root and render them.
* Useful after HTMX swaps bring in new sx-enabled elements.
*/
hydrate: function (root) {
var els = (root || document).querySelectorAll("[data-sexp]");
var els = (root || document).querySelectorAll("[data-sx]");
for (var i = 0; i < els.length; i++) {
if (els[i]._sexpHydrated) continue;
els[i]._sexpHydrated = true;
Sexp.update(els[i]);
if (els[i]._sxHydrated) continue;
els[i]._sxHydrated = true;
Sx.update(els[i]);
}
},
// For testing
_types: { NIL: NIL, Symbol: Symbol, Keyword: Keyword, Lambda: Lambda, Component: Component, RawHTML: RawHTML },
_eval: sexpEval,
_eval: sxEval,
_renderStr: renderStr,
_renderDOM: renderDOM,
};
global.Sexp = Sexp;
global.Sx = Sx;
// =========================================================================
// SxEngine — native fetch/swap/history engine (replaces HTMX)
@@ -1573,12 +1573,12 @@
return resp.text().then(function (text) {
dispatch(el, "sx:afterRequest", { response: resp });
// Check for text/sexp content type
// Check for text/sx content type
var ct = resp.headers.get("Content-Type") || "";
if (ct.indexOf("text/sexp") >= 0) {
try { text = Sexp.renderToString(text); }
if (ct.indexOf("text/sx") >= 0) {
try { text = Sx.renderToString(text); }
catch (err) {
console.error("sexp.js render error:", err);
console.error("sx.js render error:", err);
return;
}
}
@@ -1594,8 +1594,8 @@
var parser = new DOMParser();
var doc = parser.parseFromString(text, "text/html");
// Process any sexp script blocks in the response (e.g. cross-domain component defs)
Sexp.processScripts(doc);
// Process any sx script blocks in the response (e.g. cross-domain component defs)
Sx.processScripts(doc);
// OOB processing: extract elements with sx-swap-oob
var oobs = doc.querySelectorAll("[sx-swap-oob]");
@@ -1679,8 +1679,8 @@
tgt.insertAdjacentHTML("afterend", html);
parent.removeChild(tgt);
// Process parent to catch all newly inserted siblings
Sexp.processScripts(parent);
Sexp.hydrate(parent);
Sx.processScripts(parent);
Sx.hydrate(parent);
SxEngine.process(parent);
return; // early return — afterSwap handling done inline
case "afterend":
@@ -1701,8 +1701,8 @@
default:
target.innerHTML = html;
}
Sexp.processScripts(target);
Sexp.hydrate(target);
Sx.processScripts(target);
Sx.hydrate(target);
SxEngine.process(target);
}
@@ -1863,8 +1863,8 @@
var main = document.getElementById("main-panel");
if (main) {
main.innerHTML = _historyCache[url];
Sexp.processScripts(main);
Sexp.hydrate(main);
Sx.processScripts(main);
Sx.hydrate(main);
SxEngine.process(main);
dispatch(document.body, "sx:afterSettle", { target: main });
return;
@@ -1885,9 +1885,9 @@
return resp.text();
}).then(function (text) {
var ct = "";
// Response content-type is lost here, check for sexp
// Response content-type is lost here, check for sx
if (text.charAt(0) === "(") {
try { text = Sexp.renderToString(text); } catch (e) { /* not sexp */ }
try { text = Sx.renderToString(text); } catch (e) { /* not sx */ }
}
var parser = new DOMParser();
var doc = parser.parseFromString(text, "text/html");
@@ -1895,8 +1895,8 @@
var main = document.getElementById("main-panel");
if (main && newMain) {
main.innerHTML = newMain.innerHTML;
Sexp.processScripts(main);
Sexp.hydrate(main);
Sx.processScripts(main);
Sx.hydrate(main);
SxEngine.process(main);
dispatch(document.body, "sx:afterSettle", { target: main });
}
@@ -1975,13 +1975,13 @@
// Auto-init in browser
// =========================================================================
Sexp.VERSION = "2026-03-01a";
Sx.VERSION = "2026-03-01a";
if (typeof document !== "undefined") {
var init = function () {
console.log("[sexp.js] v" + Sexp.VERSION + " init");
Sexp.processScripts();
Sexp.hydrate();
console.log("[sx.js] v" + Sx.VERSION + " init");
Sx.processScripts();
Sx.hydrate();
SxEngine.process();
};
if (document.readyState === "loading") {