/**
* sx.js — S-expression parser, evaluator, and DOM renderer. [v2-debug]
*
* Client-side counterpart to shared/sx/ Python modules.
* Parses s-expression text, evaluates it, and renders to DOM nodes.
*
* Usage:
* Sx.loadComponents('(defcomp ~card (&key title) (div :class "c" title))');
* const node = Sx.render('(~card :title "Hello")');
* document.body.appendChild(node);
*/
;(function (global) {
"use strict";
// --- 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 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) {
this.name = name;
this.params = params;
this.hasChildren = hasChildren;
this.body = body;
this.closure = closure || {};
}
Component.prototype._component = 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;
/** Thunk — deferred evaluation for tail-call optimization. */
function _Thunk(expr, env) { this.expr = expr; this.env = env; }
_Thunk.prototype._thunk = true;
function isThunk(x) { return x && x._thunk; }
/** Marker for pre-rendered HTML that bypasses escaping. */
function RawHTML(html) { this.html = html; }
RawHTML.prototype._raw = true;
/** CSSX StyleValue — generated CSS class with rules. */
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
this.className = className;
this.declarations = declarations || "";
this.mediaRules = mediaRules || [];
this.pseudoRules = pseudoRules || [];
this.keyframes = keyframes || [];
}
StyleValue.prototype._styleValue = true;
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 isStyleValue(x) { return x && x._styleValue === true; }
// --- Parser ---
var RE_WS = /\s+/y;
var RE_COMMENT = /;[^\n]*/y;
var RE_STRING = /"(?:[^"\\]|\\[\s\S])*"/y;
var RE_NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y;
var RE_KEYWORD = /:[a-zA-Z_~*+\-><=/!?&\[][a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]*/y;
var RE_SYMBOL = /[a-zA-Z_~*+\-><=/!?&][a-zA-Z0-9_~*+\-><=/!?.:&]*/y;
function Tokenizer(text) {
this.text = text;
this.pos = 0;
this.line = 1;
this.col = 1;
}
Tokenizer.prototype._advance = function (count) {
for (var i = 0; i < count; i++) {
if (this.pos < this.text.length) {
if (this.text[this.pos] === "\n") { this.line++; this.col = 1; }
else { this.col++; }
this.pos++;
}
}
};
Tokenizer.prototype._skip = function () {
while (this.pos < this.text.length) {
RE_WS.lastIndex = this.pos;
var m = RE_WS.exec(this.text);
if (m && m.index === this.pos) { this._advance(m[0].length); continue; }
RE_COMMENT.lastIndex = this.pos;
m = RE_COMMENT.exec(this.text);
if (m && m.index === this.pos) { this._advance(m[0].length); continue; }
break;
}
};
Tokenizer.prototype.peek = function () {
this._skip();
return this.pos < this.text.length ? this.text[this.pos] : null;
};
Tokenizer.prototype.next = function () {
this._skip();
if (this.pos >= this.text.length) return null;
var ch = this.text[this.pos];
// Delimiters
if ("()[]{}".indexOf(ch) !== -1) { this._advance(1); return ch; }
// String
if (ch === '"') {
RE_STRING.lastIndex = this.pos;
var m = RE_STRING.exec(this.text);
if (!m || m.index !== this.pos) throw parseErr("Unterminated string", this);
this._advance(m[0].length);
var raw = m[0].slice(1, -1);
return raw.replace(/\\n/g, "\n").replace(/\\t/g, "\t")
.replace(/\\"/g, '"').replace(/\\[/]/g, "/").replace(/\\\\/g, "\\");
}
// Keyword
if (ch === ":") {
RE_KEYWORD.lastIndex = this.pos;
m = RE_KEYWORD.exec(this.text);
if (!m || m.index !== this.pos) throw parseErr("Invalid keyword", this);
this._advance(m[0].length);
return new Keyword(m[0].slice(1));
}
// Number (before symbol due to leading -)
if (isDigit(ch) || (ch === "-" && this.pos + 1 < this.text.length &&
(isDigit(this.text[this.pos + 1]) || this.text[this.pos + 1] === "."))) {
RE_NUMBER.lastIndex = this.pos;
m = RE_NUMBER.exec(this.text);
if (m && m.index === this.pos) {
this._advance(m[0].length);
var s = m[0];
return (s.indexOf(".") !== -1 || s.indexOf("e") !== -1 || s.indexOf("E") !== -1)
? parseFloat(s) : parseInt(s, 10);
}
}
// Symbol
RE_SYMBOL.lastIndex = this.pos;
m = RE_SYMBOL.exec(this.text);
if (m && m.index === this.pos) {
this._advance(m[0].length);
var name = m[0];
if (name === "true") return true;
if (name === "false") return false;
if (name === "nil") return NIL;
return new Symbol(name);
}
var ctx = this.text.substring(Math.max(0, this.pos - 40), this.pos + 40);
throw parseErr("Unexpected character: " + ch + " | context: «" + ctx.replace(/\n/g, "\\n") + "»", this);
};
function isDigit(c) { return c >= "0" && c <= "9"; }
function parseErr(msg, tok) {
return new Error(msg + " at line " + tok.line + ", col " + tok.col);
}
function parseExpr(tok) {
// Use peek() (raw character) for structural decisions so that string
// values like ")" or "(" don't get confused with actual delimiters.
var raw = tok.peek();
if (raw === null) throw parseErr("Unexpected end of input", tok);
if (raw === ")" || raw === "]" || raw === "}") {
tok.next(); // consume the delimiter
throw parseErr("Unexpected " + raw, tok);
}
if (raw === "(") { tok.next(); return parseList(tok, ")"); }
if (raw === "[") { tok.next(); return parseList(tok, "]"); }
if (raw === "{") { tok.next(); return parseMap(tok); }
// Quasiquote syntax
if (raw === "`") { tok._advance(1); return [new Symbol("quasiquote"), parseExpr(tok)]; }
if (raw === ",") {
tok._advance(1);
if (tok.pos < tok.text.length && tok.text[tok.pos] === "@") {
tok._advance(1);
return [new Symbol("splice-unquote"), parseExpr(tok)];
}
return [new Symbol("unquote"), parseExpr(tok)];
}
return tok.next();
}
function parseList(tok, closer) {
var items = [];
while (true) {
var c = tok.peek();
if (c === null) throw parseErr("Unterminated list, expected " + closer, tok);
if (c === closer) { tok.next(); return items; }
items.push(parseExpr(tok));
}
}
function parseMap(tok) {
var result = {};
while (true) {
var c = tok.peek();
if (c === null) throw parseErr("Unterminated map", tok);
if (c === "}") { tok.next(); return result; }
var key = parseExpr(tok);
var keyStr = isKw(key) ? key.name : String(key);
result[keyStr] = parseExpr(tok);
}
}
/** Parse a single s-expression. */
function parse(text) {
var tok = new Tokenizer(text);
var result = parseExpr(tok);
if (tok.peek() !== null) throw parseErr("Unexpected content after expression", tok);
return result;
}
/** Parse zero or more s-expressions. */
function parseAll(text) {
var tok = new Tokenizer(text);
var results = [];
while (tok.peek() !== null) results.push(parseExpr(tok));
return results;
}
/** Serialize a JS object as SX dict {:key "val" ...} for attribute values. */
function _serializeDict(obj) {
var parts = [];
for (var k in obj) {
if (!obj.hasOwnProperty(k)) continue;
var v = obj[k];
var vs = typeof v === "string" ? '"' + v.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"' : String(v);
parts.push(":" + k + " " + vs);
}
return "{" + parts.join(" ") + "}";
}
// --- Primitives ---
var PRIMITIVES = {};
// 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"] = Math.round;
PRIMITIVES["min"] = Math.min;
PRIMITIVES["max"] = Math.max;
PRIMITIVES["sqrt"] = Math.sqrt;
PRIMITIVES["pow"] = Math.pow;
// Comparison
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; };
PRIMITIVES["<="] = function (a, b) { return a <= b; };
PRIMITIVES[">="] = function (a, b) { return a >= b; };
// Logic
PRIMITIVES["not"] = function (x) { return !isSxTruthy(x); };
// String
PRIMITIVES["str"] = function () {
var parts = [];
for (var i = 0; i < arguments.length; i++) {
var v = arguments[i];
if (isNil(v)) continue;
parts.push(String(v));
}
return parts.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["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["concat"] = function () {
var out = [];
for (var i = 0; i < arguments.length; i++) out = out.concat(arguments[i]);
return out;
};
// Predicates
PRIMITIVES["nil?"] = function (x) { return isNil(x); };
PRIMITIVES["number?"] = function (x) { return typeof x === "number"; };
PRIMITIVES["string?"] = function (x) { return typeof x === "string"; };
PRIMITIVES["list?"] = function (x) { return Array.isArray(x); };
PRIMITIVES["dict?"] = function (x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
PRIMITIVES["empty?"] = function (c) { return !c || (Array.isArray(c) ? c.length === 0 : Object.keys(c).length === 0); };
PRIMITIVES["contains?"] = function (c, k) { return typeof c === "string" ? c.indexOf(k) !== -1 : Array.isArray(c) ? c.indexOf(k) !== -1 : 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; };
// 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["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 : 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) { return c ? c.slice(1) : []; };
PRIMITIVES["nth"] = function (c, n) { return c && n < c.length ? c[n] : NIL; };
PRIMITIVES["slice"] = function (c, start, end) { return c ? (end !== undefined && end !== NIL ? c.slice(start, end) : c.slice(start)) : c; };
PRIMITIVES["cons"] = function (x, c) { return [x].concat(c || []); };
PRIMITIVES["append"] = function (c, x) { return (c || []).concat([x]); };
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]; for (var k in d) out[k] = d[k]; }
return out;
};
PRIMITIVES["assoc"] = function (d) {
var out = {}; 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["range"] = function (a, b, step) {
var r = []; step = step || 1;
if (b === undefined) { b = a; a = 0; }
for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i);
return r;
};
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["into"] = function (target, src) {
if (Array.isArray(target)) return target.concat(src || []);
var out = {}; for (var k in target) out[k] = target[k]; for (var k2 in src) out[k2] = src[k2]; return out;
};
// String operations
PRIMITIVES["replace"] = function (s, from, to) { return s ? String(s).split(from).join(to) : ""; };
PRIMITIVES["upper"] = function (s) { return s ? String(s).toUpperCase() : ""; };
PRIMITIVES["lower"] = function (s) { return s ? String(s).toLowerCase() : ""; };
PRIMITIVES["trim"] = function (s) { return s ? String(s).trim() : ""; };
PRIMITIVES["starts-with?"] = function (s, pfx) { return s ? String(s).indexOf(pfx) === 0 : false; };
PRIMITIVES["ends-with?"] = function (s, sfx) { var str = String(s || ""); return str.indexOf(sfx, str.length - sfx.length) !== -1; };
PRIMITIVES["escape"] = function (s) {
if (!s) return "";
return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
};
PRIMITIVES["strip-tags"] = function (s) { return s ? String(s).replace(/<[^>]*>/g, "") : ""; };
PRIMITIVES["split"] = function (s, sep) { return s ? String(s).split(sep) : []; };
PRIMITIVES["join"] = function (lst, sep) { return (lst || []).join(sep !== undefined ? sep : ""); };
PRIMITIVES["pluralize"] = function (n, singular, plural) { return n === 1 ? singular : (plural || singular + "s"); };
// Numeric
PRIMITIVES["clamp"] = function (val, lo, hi) { return Math.max(lo, Math.min(hi, val)); };
PRIMITIVES["parse-int"] = function (s, def) { var n = parseInt(s, 10); return isNaN(n) ? (def !== undefined ? def : 0) : n; };
PRIMITIVES["format-decimal"] = function (n, places) { return Number(n || 0).toFixed(places !== undefined ? places : 2); };
// Date formatting (basic)
PRIMITIVES["format-date"] = function (s, fmt) {
if (!s) return "";
try {
var d = new Date(s);
if (isNaN(d.getTime())) return String(s);
// Basic strftime-like: %Y %m %d %H %M %B %b %-d
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; };
PRIMITIVES["split-ids"] = function (s) {
if (!s) return [];
return String(s).split(",").map(function(x) { return x.trim(); }).filter(function(x) { return x; });
};
// --- CSSX Style Dictionary + Resolver ---
var _styleAtoms = {}; // atom → CSS declarations
var _pseudoVariants = {}; // variant → CSS pseudo-selector
var _responsiveBreakpoints = {}; // variant → media query
var _styleKeyframes = {}; // name → @keyframes rule
var _arbitraryPatterns = []; // [{re: RegExp, tmpl: string}, ...]
var _childSelectorPrefixes = []; // ["space-x-", "space-y-", ...]
var _styleCache = {}; // atoms-key → StyleValue
var _injectedStyles = {}; // className → true (already in blocks and inject into