diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index a314584..4e33d60 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-12T22:55:39Z"; + var SX_VERSION = "2026-03-13T02:18:19Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -83,6 +83,11 @@ function RawHTML(html) { this.html = html; } RawHTML.prototype._raw = true; + function SxSpread(attrs) { this.attrs = attrs || {}; } + SxSpread.prototype._spread = true; + + var _collectBuckets = {}; + function isSym(x) { return x != null && x._sym === true; } function isKw(x) { return x != null && x._kw === true; } @@ -118,6 +123,7 @@ if (x._component) return "component"; if (x._island) return "island"; if (x._signal) return "signal"; + if (x._spread) return "spread"; if (x._macro) return "macro"; if (x._raw) return "raw-html"; if (typeof Node !== "undefined" && x instanceof Node) return "dom-node"; @@ -140,6 +146,22 @@ } function makeThunk(expr, env) { return new Thunk(expr, env); } + function makeSpread(attrs) { return new SxSpread(attrs || {}); } + function isSpread(x) { return x != null && x._spread === true; } + function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; } + + function sxCollect(bucket, value) { + if (!_collectBuckets[bucket]) _collectBuckets[bucket] = []; + var items = _collectBuckets[bucket]; + if (items.indexOf(value) === -1) items.push(value); + } + function sxCollected(bucket) { + return _collectBuckets[bucket] ? _collectBuckets[bucket].slice() : []; + } + function sxClearCollected(bucket) { + if (_collectBuckets[bucket]) _collectBuckets[bucket] = []; + } + function lambdaParams(f) { return f.params; } function lambdaBody(f) { return f.body; } function lambdaClosure(f) { return f.closure; } @@ -466,6 +488,15 @@ }; + // stdlib.spread — spread + collect primitives + PRIMITIVES["make-spread"] = makeSpread; + PRIMITIVES["spread?"] = isSpread; + PRIMITIVES["spread-attrs"] = spreadAttrs; + PRIMITIVES["collect!"] = sxCollect; + PRIMITIVES["collected"] = sxCollected; + PRIMITIVES["clear-collected!"] = sxClearCollected; + + function isPrimitive(name) { return name in PRIMITIVES; } function getPrimitive(name) { return PRIMITIVES[name]; } @@ -1277,6 +1308,18 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai })()); })()); }; + // merge-spread-attrs + var mergeSpreadAttrs = function(target, spreadDict) { return forEach(function(key) { return (function() { + var val = dictGet(spreadDict, key); + return (isSxTruthy((key == "class")) ? (function() { + var existing = dictGet(target, "class"); + return dictSet(target, "class", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(" ") + String(val)) : val)); +})() : (isSxTruthy((key == "style")) ? (function() { + var existing = dictGet(target, "style"); + return dictSet(target, "style", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(";") + String(val)) : val)); +})() : dictSet(target, key, val))); +})(); }, keys(spreadDict)); }; + // === Transpiled from parser === @@ -1419,10 +1462,10 @@ continue; } else { return NIL; } } }; // render-to-html var renderToHtml = function(expr, env) { setRenderActiveB(true); -return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); }; +return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); if (_m == "spread") return expr; return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); }; // render-value-to-html - var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); return escapeHtml((String(val))); })(); }; + var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); if (_m == "spread") return val; return escapeHtml((String(val))); })(); }; // RENDER_HTML_FORMS var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "deftype", "defeffect", "map", "map-indexed", "filter", "for-each"]; @@ -1433,10 +1476,10 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = // render-list-to-html var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() { var head = first(expr); - return (isSxTruthy(!isSxTruthy((typeOf(head) == "symbol"))) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() { + return (isSxTruthy(!isSxTruthy((typeOf(head) == "symbol"))) ? join("", filter(function(x) { return !isSxTruthy(isSpread(x)); }, map(function(x) { return renderValueToHtml(x, env); }, expr))) : (function() { var name = symbolName(head); var args = rest(expr); - return (isSxTruthy((name == "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy((name == "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy((name == "lake")) ? renderHtmlLake(args, env) : (isSxTruthy((name == "marsh")) ? renderHtmlMarsh(args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderHtmlIsland(envGet(env, name), args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() { + return (isSxTruthy((name == "<>")) ? join("", filter(function(x) { return !isSxTruthy(isSpread(x)); }, map(function(x) { return renderToHtml(x, env); }, args))) : (isSxTruthy((name == "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy((name == "lake")) ? renderHtmlLake(args, env) : (isSxTruthy((name == "marsh")) ? renderHtmlMarsh(args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderHtmlIsland(envGet(env, name), args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() { var val = envGet(env, name); return (isSxTruthy(isComponent(val)) ? renderHtmlComponent(val, args, env) : (isSxTruthy(isMacro(val)) ? renderToHtml(expandMacro(val, args, env), env) : error((String("Unknown component: ") + String(name))))); })() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))); @@ -1447,24 +1490,33 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = var dispatchHtmlForm = function(name, expr, env) { return (isSxTruthy((name == "if")) ? (function() { var condVal = trampoline(evalExpr(nth(expr, 1), env)); return (isSxTruthy(condVal) ? renderToHtml(nth(expr, 2), env) : (isSxTruthy((len(expr) > 3)) ? renderToHtml(nth(expr, 3), env) : "")); -})() : (isSxTruthy((name == "when")) ? (isSxTruthy(!isSxTruthy(trampoline(evalExpr(nth(expr, 1), env)))) ? "" : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(2, len(expr))))) : (isSxTruthy((name == "cond")) ? (function() { +})() : (isSxTruthy((name == "when")) ? (isSxTruthy(!isSxTruthy(trampoline(evalExpr(nth(expr, 1), env)))) ? "" : (isSxTruthy((len(expr) == 3)) ? renderToHtml(nth(expr, 2), env) : (function() { + var results = map(function(i) { return renderToHtml(nth(expr, i), env); }, range(2, len(expr))); + return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, results)); +})())) : (isSxTruthy((name == "cond")) ? (function() { var branch = evalCond(rest(expr), env); return (isSxTruthy(branch) ? renderToHtml(branch, env) : ""); })() : (isSxTruthy((name == "case")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() { var local = processBindings(nth(expr, 1), env); - return join("", map(function(i) { return renderToHtml(nth(expr, i), local); }, range(2, len(expr)))); -})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(1, len(expr)))) : (isSxTruthy(isDefinitionForm(name)) ? (trampoline(evalExpr(expr, env)), "") : (isSxTruthy((name == "map")) ? (function() { + return (isSxTruthy((len(expr) == 3)) ? renderToHtml(nth(expr, 2), local) : (function() { + var results = map(function(i) { return renderToHtml(nth(expr, i), local); }, range(2, len(expr))); + return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, results)); +})()); +})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? (isSxTruthy((len(expr) == 2)) ? renderToHtml(nth(expr, 1), env) : (function() { + var results = map(function(i) { return renderToHtml(nth(expr, i), env); }, range(1, len(expr))); + return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, results)); +})()) : (isSxTruthy(isDefinitionForm(name)) ? (trampoline(evalExpr(expr, env)), "") : (isSxTruthy((name == "map")) ? (function() { var f = trampoline(evalExpr(nth(expr, 1), env)); var coll = trampoline(evalExpr(nth(expr, 2), env)); - return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll)); + return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll))); })() : (isSxTruthy((name == "map-indexed")) ? (function() { var f = trampoline(evalExpr(nth(expr, 1), env)); var coll = trampoline(evalExpr(nth(expr, 2), env)); - return join("", mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [i, item], env) : renderToHtml(apply(f, [i, item]), env)); }, coll)); + return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [i, item], env) : renderToHtml(apply(f, [i, item]), env)); }, coll))); })() : (isSxTruthy((name == "filter")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy((name == "for-each")) ? (function() { var f = trampoline(evalExpr(nth(expr, 1), env)); var coll = trampoline(evalExpr(nth(expr, 2), env)); - return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll)); + return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll))); })() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))); }; // render-lambda-html @@ -1490,7 +1542,14 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = var local = envMerge(componentClosure(comp), env); { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } if (isSxTruthy(componentHasChildren(comp))) { - envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)))); + (function() { + var parts = []; + { var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() { + var r = renderToHtml(c, env); + return (isSxTruthy(!isSxTruthy(isSpread(r))) ? append_b(parts, r) : NIL); +})(); } } + return envSet(local, "children", makeRawHtml(join("", parts))); +})(); } return renderToHtml(componentBody(comp), local); })(); @@ -1502,7 +1561,14 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = var attrs = first(parsed); var children = nth(parsed, 1); var isVoid = contains(VOID_ELEMENTS, tag); - return (String("<") + String(tag) + String(renderAttrs(attrs)) + String((isSxTruthy(isVoid) ? " />" : (String(">") + String(join("", map(function(c) { return renderToHtml(c, env); }, children))) + String(""))))); + return (isSxTruthy(isVoid) ? (String("<") + String(tag) + String(renderAttrs(attrs)) + String(" />")) : (function() { + var contentParts = []; + { var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() { + var result = renderToHtml(c, env); + return (isSxTruthy(isSpread(result)) ? mergeSpreadAttrs(attrs, spreadAttrs(result)) : append_b(contentParts, result)); +})(); } } + return (String("<") + String(tag) + String(renderAttrs(attrs)) + String(">") + String(join("", contentParts)) + String("")); +})()); })(); }; // render-html-lake @@ -1519,7 +1585,15 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = return assoc(state, "skip", true, "i", (get(state, "i") + 1)); })() : (append_b(children, arg), assoc(state, "i", (get(state, "i") + 1))))); })(); }, {["i"]: 0, ["skip"]: false}, args); - return (String("<") + String(lakeTag) + String(" data-sx-lake=\"") + String(escapeAttr(sxOr(lakeId, ""))) + String("\">") + String(join("", map(function(c) { return renderToHtml(c, env); }, children))) + String("")); + return (function() { + var lakeAttrs = {["data-sx-lake"]: sxOr(lakeId, "")}; + var contentParts = []; + { var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() { + var result = renderToHtml(c, env); + return (isSxTruthy(isSpread(result)) ? mergeSpreadAttrs(lakeAttrs, spreadAttrs(result)) : append_b(contentParts, result)); +})(); } } + return (String("<") + String(lakeTag) + String(renderAttrs(lakeAttrs)) + String(">") + String(join("", contentParts)) + String("")); +})(); })(); }; // render-html-marsh @@ -1536,7 +1610,15 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = return assoc(state, "skip", true, "i", (get(state, "i") + 1)); })() : (append_b(children, arg), assoc(state, "i", (get(state, "i") + 1))))); })(); }, {["i"]: 0, ["skip"]: false}, args); - return (String("<") + String(marshTag) + String(" data-sx-marsh=\"") + String(escapeAttr(sxOr(marshId, ""))) + String("\">") + String(join("", map(function(c) { return renderToHtml(c, env); }, children))) + String("")); + return (function() { + var marshAttrs = {["data-sx-marsh"]: sxOr(marshId, "")}; + var contentParts = []; + { var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() { + var result = renderToHtml(c, env); + return (isSxTruthy(isSpread(result)) ? mergeSpreadAttrs(marshAttrs, spreadAttrs(result)) : append_b(contentParts, result)); +})(); } } + return (String("<") + String(marshTag) + String(renderAttrs(marshAttrs)) + String(">") + String(join("", contentParts)) + String("")); +})(); })(); }; // render-html-island @@ -1556,7 +1638,14 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = var islandName = componentName(island); { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } if (isSxTruthy(componentHasChildren(island))) { - envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)))); + (function() { + var parts = []; + { var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() { + var r = renderToHtml(c, env); + return (isSxTruthy(!isSxTruthy(isSpread(r))) ? append_b(parts, r) : NIL); +})(); } } + return envSet(local, "children", makeRawHtml(join("", parts))); +})(); } return (function() { var bodyHtml = renderToHtml(componentBody(island), local); @@ -1583,7 +1672,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { var name = symbolName(expr); return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name)))))))); -})(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); return expr; })(); }; +})(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); if (_m == "spread") return expr; return expr; })(); }; // aser-list var aserList = function(expr, env) { return (function() { @@ -1725,7 +1814,7 @@ return result; }, args); // render-to-dom var renderToDom = function(expr, env, ns) { setRenderActiveB(true); -return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); return (isSxTruthy(isSignal(expr)) ? (isSxTruthy(_islandScope) ? reactiveText(expr) : createTextNode((String(deref(expr))))) : createTextNode((String(expr)))); })(); }; +return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "spread") return expr; if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); return (isSxTruthy(isSignal(expr)) ? (isSxTruthy(_islandScope) ? reactiveText(expr) : createTextNode((String(deref(expr))))) : createTextNode((String(expr)))); })(); }; // render-dom-list var renderDomList = function(expr, env, ns) { return (function() { @@ -1773,7 +1862,19 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme return (isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))); })()))))); return assoc(state, "skip", true, "i", (get(state, "i") + 1)); -})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1))))); +})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? (function() { + var child = renderToDom(arg, env, newNs); + return (isSxTruthy(isSpread(child)) ? forEach(function(key) { return (function() { + var val = dictGet(spreadAttrs(child), key); + return (isSxTruthy((key == "class")) ? (function() { + var existing = domGetAttr(el, "class"); + return domSetAttr(el, "class", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(" ") + String(val)) : val)); +})() : (isSxTruthy((key == "style")) ? (function() { + var existing = domGetAttr(el, "style"); + return domSetAttr(el, "style", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(";") + String(val)) : val)); +})() : domSetAttr(el, key, (String(val))))); +})(); }, keys(spreadAttrs(child))) : domAppend(el, child)); +})() : NIL), assoc(state, "i", (get(state, "i") + 1))))); })(); }, {["i"]: 0, ["skip"]: false}, args); return el; })(); }; @@ -6346,6 +6447,12 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { emitEvent: emitEvent, onEvent: onEvent, bridgeEvent: bridgeEvent, + makeSpread: makeSpread, + isSpread: isSpread, + spreadAttrs: spreadAttrs, + collect: sxCollect, + collected: sxCollected, + clearCollected: sxClearCollected, _version: "ref-2.0 (boot+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)" }; diff --git a/shared/sx/ref/adapter-async.sx b/shared/sx/ref/adapter-async.sx index 0922111..2e56652 100644 --- a/shared/sx/ref/adapter-async.sx +++ b/shared/sx/ref/adapter-async.sx @@ -48,6 +48,7 @@ "string" (escape-html expr) "number" (escape-html (str expr)) "raw-html" (raw-html-content expr) + "spread" expr "symbol" (let ((val (async-eval expr env ctx))) (async-render val env ctx)) "keyword" (escape-html (keyword-name expr)) @@ -79,9 +80,10 @@ (= name "raw!") (async-render-raw args env ctx) - ;; Fragment + ;; Fragment (spreads filtered — no parent element) (= name "<>") - (join "" (async-map-render args env ctx)) + (join "" (filter (fn (r) (not (spread? r))) + (async-map-render args env ctx))) ;; html: prefix (starts-with? name "html:") @@ -167,16 +169,24 @@ (let ((class-val (dict-get attrs "class"))) (when (and (not (nil? class-val)) (not (= class-val false))) (css-class-collect! (str class-val)))) - ;; Build opening tag - (let ((opening (str "<" tag (render-attrs attrs) ">"))) - (if (contains? VOID_ELEMENTS tag) - opening - (let ((token (if (or (= tag "svg") (= tag "math")) - (svg-context-set! true) - nil)) - (child-html (join "" (async-map-render children env ctx)))) - (when token (svg-context-reset! token)) - (str opening child-html ""))))))) + (if (contains? VOID_ELEMENTS tag) + (str "<" tag (render-attrs attrs) ">") + ;; Render children, collecting spreads and content separately + (let ((token (if (or (= tag "svg") (= tag "math")) + (svg-context-set! true) + nil)) + (content-parts (list))) + (for-each + (fn (c) + (let ((result (async-render c env ctx))) + (if (spread? result) + (merge-spread-attrs attrs (spread-attrs result)) + (append! content-parts result)))) + children) + (when token (svg-context-reset! token)) + (str "<" tag (render-attrs attrs) ">" + (join "" content-parts) + "")))))) ;; -------------------------------------------------------------------------- @@ -221,10 +231,17 @@ (for-each (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) (component-params comp)) + ;; Pre-render children to raw HTML (filter spreads — no parent element) (when (component-has-children? comp) - (env-set! local "children" - (make-raw-html - (join "" (async-map-render children env ctx))))) + (let ((parts (list))) + (for-each + (fn (c) + (let ((r (async-render c env ctx))) + (when (not (spread? r)) + (append! parts r)))) + children) + (env-set! local "children" + (make-raw-html (join "" parts))))) (async-render (component-body comp) local ctx))))) @@ -242,10 +259,17 @@ (for-each (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) (component-params island)) + ;; Pre-render children (filter spreads — no parent element) (when (component-has-children? island) - (env-set! local "children" - (make-raw-html - (join "" (async-map-render children env ctx))))) + (let ((parts (list))) + (for-each + (fn (c) + (let ((r (async-render c env ctx))) + (when (not (spread? r)) + (append! parts r)))) + children) + (env-set! local "children" + (make-raw-html (join "" parts))))) (let ((body-html (async-render (component-body island) local ctx)) (state-json (serialize-island-state kwargs))) (str "") - (join "" (map (fn (x) (render-to-html x env)) args)) + (join "" (filter (fn (x) (not (spread? x))) + (map (fn (x) (render-to-html x env)) args))) ;; Raw HTML passthrough (= name "raw!") @@ -147,14 +152,15 @@ (render-to-html (nth expr 3) env) ""))) - ;; when + ;; when — single body: pass through (spread propagates). Multi: join strings. (= name "when") (if (not (trampoline (eval-expr (nth expr 1) env))) "" - (join "" - (map - (fn (i) (render-to-html (nth expr i) env)) - (range 2 (len expr))))) + (if (= (len expr) 3) + (render-to-html (nth expr 2) env) + (let ((results (map (fn (i) (render-to-html (nth expr i) env)) + (range 2 (len expr))))) + (join "" (filter (fn (r) (not (spread? r))) results))))) ;; cond (= name "cond") @@ -167,64 +173,69 @@ (= name "case") (render-to-html (trampoline (eval-expr expr env)) env) - ;; let / let* + ;; let / let* — single body: pass through. Multi: join strings. (or (= name "let") (= name "let*")) (let ((local (process-bindings (nth expr 1) env))) - (join "" - (map - (fn (i) (render-to-html (nth expr i) local)) - (range 2 (len expr))))) + (if (= (len expr) 3) + (render-to-html (nth expr 2) local) + (let ((results (map (fn (i) (render-to-html (nth expr i) local)) + (range 2 (len expr))))) + (join "" (filter (fn (r) (not (spread? r))) results))))) - ;; begin / do + ;; begin / do — single body: pass through. Multi: join strings. (or (= name "begin") (= name "do")) - (join "" - (map - (fn (i) (render-to-html (nth expr i) env)) - (range 1 (len expr)))) + (if (= (len expr) 2) + (render-to-html (nth expr 1) env) + (let ((results (map (fn (i) (render-to-html (nth expr i) env)) + (range 1 (len expr))))) + (join "" (filter (fn (r) (not (spread? r))) results)))) ;; Definition forms — eval for side effects (definition-form? name) (do (trampoline (eval-expr expr env)) "") - ;; map + ;; map — spreads filtered (no parent element in list context) (= name "map") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env)))) (join "" - (map - (fn (item) - (if (lambda? f) - (render-lambda-html f (list item) env) - (render-to-html (apply f (list item)) env))) - coll))) + (filter (fn (r) (not (spread? r))) + (map + (fn (item) + (if (lambda? f) + (render-lambda-html f (list item) env) + (render-to-html (apply f (list item)) env))) + coll)))) - ;; map-indexed + ;; map-indexed — spreads filtered (= name "map-indexed") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env)))) (join "" - (map-indexed - (fn (i item) - (if (lambda? f) - (render-lambda-html f (list i item) env) - (render-to-html (apply f (list i item)) env))) - coll))) + (filter (fn (r) (not (spread? r))) + (map-indexed + (fn (i item) + (if (lambda? f) + (render-lambda-html f (list i item) env) + (render-to-html (apply f (list i item)) env))) + coll)))) ;; filter — evaluate fully then render (= name "filter") (render-to-html (trampoline (eval-expr expr env)) env) - ;; for-each (render variant) + ;; for-each (render variant) — spreads filtered (= name "for-each") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env)))) (join "" - (map - (fn (item) - (if (lambda? f) - (render-lambda-html f (list item) env) - (render-to-html (apply f (list item)) env))) - coll))) + (filter (fn (r) (not (spread? r))) + (map + (fn (item) + (if (lambda? f) + (render-lambda-html f (list item) env) + (render-to-html (apply f (list item)) env))) + coll)))) ;; Fallback :else @@ -281,10 +292,17 @@ (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) (component-params comp)) ;; If component accepts children, pre-render them to raw HTML + ;; Spread values are filtered out (no parent element to merge onto) (when (component-has-children? comp) - (env-set! local "children" - (make-raw-html - (join "" (map (fn (c) (render-to-html c env)) children))))) + (let ((parts (list))) + (for-each + (fn (c) + (let ((r (render-to-html c env))) + (when (not (spread? r)) + (append! parts r)))) + children) + (env-set! local "children" + (make-raw-html (join "" parts))))) (render-to-html (component-body comp) local))))) @@ -294,12 +312,19 @@ (attrs (first parsed)) (children (nth parsed 1)) (is-void (contains? VOID_ELEMENTS tag))) - (str "<" tag - (render-attrs attrs) - (if is-void - " />" - (str ">" - (join "" (map (fn (c) (render-to-html c env)) children)) + (if is-void + (str "<" tag (render-attrs attrs) " />") + ;; Render children, collecting spreads and content separately + (let ((content-parts (list))) + (for-each + (fn (c) + (let ((result (render-to-html c env))) + (if (spread? result) + (merge-spread-attrs attrs (spread-attrs result)) + (append! content-parts result)))) + children) + (str "<" tag (render-attrs attrs) ">" + (join "" content-parts) "")))))) @@ -335,9 +360,19 @@ (assoc state "i" (inc (get state "i")))))))) (dict "i" 0 "skip" false) args) - (str "<" lake-tag " data-sx-lake=\"" (escape-attr (or lake-id "")) "\">" - (join "" (map (fn (c) (render-to-html c env)) children)) - "")))) + ;; Render children, handling spreads + (let ((lake-attrs (dict "data-sx-lake" (or lake-id ""))) + (content-parts (list))) + (for-each + (fn (c) + (let ((result (render-to-html c env))) + (if (spread? result) + (merge-spread-attrs lake-attrs (spread-attrs result)) + (append! content-parts result)))) + children) + (str "<" lake-tag (render-attrs lake-attrs) ">" + (join "" content-parts) + ""))))) ;; -------------------------------------------------------------------------- @@ -375,9 +410,19 @@ (assoc state "i" (inc (get state "i")))))))) (dict "i" 0 "skip" false) args) - (str "<" marsh-tag " data-sx-marsh=\"" (escape-attr (or marsh-id "")) "\">" - (join "" (map (fn (c) (render-to-html c env)) children)) - "")))) + ;; Render children, handling spreads + (let ((marsh-attrs (dict "data-sx-marsh" (or marsh-id ""))) + (content-parts (list))) + (for-each + (fn (c) + (let ((result (render-to-html c env))) + (if (spread? result) + (merge-spread-attrs marsh-attrs (spread-attrs result)) + (append! content-parts result)))) + children) + (str "<" marsh-tag (render-attrs marsh-attrs) ">" + (join "" content-parts) + ""))))) ;; -------------------------------------------------------------------------- @@ -427,10 +472,17 @@ (component-params island)) ;; If island accepts children, pre-render them to raw HTML + ;; Spread values filtered out (no parent element) (when (component-has-children? island) - (env-set! local "children" - (make-raw-html - (join "" (map (fn (c) (render-to-html c env)) children))))) + (let ((parts (list))) + (for-each + (fn (c) + (let ((r (render-to-html c env))) + (when (not (spread? r)) + (append! parts r)))) + children) + (env-set! local "children" + (make-raw-html (join "" parts))))) ;; Render the island body as HTML (let ((body-html (render-to-html (component-body island) local)) diff --git a/shared/sx/ref/adapter-sx.sx b/shared/sx/ref/adapter-sx.sx index f55da20..677ed5a 100644 --- a/shared/sx/ref/adapter-sx.sx +++ b/shared/sx/ref/adapter-sx.sx @@ -48,6 +48,9 @@ (list) (aser-list expr env)) + ;; Spread — pass through for client rendering + "spread" expr + :else expr))) diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py index bd77a4b..afae46e 100644 --- a/shared/sx/ref/bootstrap_py.py +++ b/shared/sx/ref/bootstrap_py.py @@ -285,6 +285,14 @@ class PyEmitter: "svg-context-set!": "svg_context_set", "svg-context-reset!": "svg_context_reset", "css-class-collect!": "css_class_collect", + # spread + collect primitives + "make-spread": "make_spread", + "spread?": "is_spread", + "spread-attrs": "spread_attrs", + "merge-spread-attrs": "merge_spread_attrs", + "collect!": "sx_collect", + "collected": "sx_collected", + "clear-collected!": "sx_clear_collected", "is-raw-html?": "is_raw_html", "async-coroutine?": "is_async_coroutine", "async-await!": "async_await", diff --git a/shared/sx/ref/boundary.sx b/shared/sx/ref/boundary.sx index 95edfd8..bc1d66d 100644 --- a/shared/sx/ref/boundary.sx +++ b/shared/sx/ref/boundary.sx @@ -313,3 +313,61 @@ :effects [mutation] :doc "Group multiple signal writes. Subscribers are notified once at the end, after all values have been updated.") + + +;; -------------------------------------------------------------------------- +;; Tier 4: Spread + Collect — render-time attribute injection and accumulation +;; +;; `spread` is a new type: a dict of attributes that, when returned as a child +;; of an HTML element, merges its attrs onto the parent element rather than +;; rendering as content. This enables components like `~cssx/tw` to inject +;; classes and styles onto their parent from inside the child list. +;; +;; `collect!` / `collected` are render-time accumulators. Values are collected +;; into named buckets (with deduplication) during rendering and retrieved at +;; flush points (e.g. a single ")))))