/** * 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; } // --- 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 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["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; }; // --- 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