From 2de4ba8c572bac49f4870972cd0eaeb27d88404f Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 13 Mar 2026 15:41:32 +0000 Subject: [PATCH] Refactor spread to use provide/emit! internally Spreads now emit their attrs into the nearest element's provide scope instead of requiring per-child spread? checks at every intermediate layer. emit! is tolerant (no-op when no provider), so spreads in non-element contexts silently vanish. - adapter-html: element/lake/marsh wrap children in provide, collect emitted; removed 14 spread filters from fragment, forms, components - adapter-sx: aser wraps result to catch spread values from fn calls; aser-call uses provide with attr-parts/child-parts ordering - adapter-async: same pattern for both render and aser paths - adapter-dom: added emit! in spread dispatch + provide in element rendering; kept spread? checks for reactive/island and DOM safety - platform: emit! returns NIL when no provider instead of erroring - 3 new aser tests: stored spread, nested element, silent drop Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 138 ++++++------- shared/sx/ref/adapter-async.sx | 177 ++++++++-------- shared/sx/ref/adapter-dom.sx | 54 ++--- shared/sx/ref/adapter-html.sx | 178 +++++++--------- shared/sx/ref/adapter-sx.sx | 98 +++++---- shared/sx/ref/platform_js.py | 2 - shared/sx/ref/platform_py.py | 4 +- shared/sx/ref/sx_ref.py | 302 +++++++++++----------------- shared/sx/ref/test-aser.sx | 17 +- 9 files changed, 444 insertions(+), 526 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 3eeb539..7cfd36f 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-13T12:16:43Z"; + var SX_VERSION = "2026-03-13T15:35:20Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -180,8 +180,6 @@ function sxEmit(name, value) { if (_provideStacks[name] && _provideStacks[name].length) { _provideStacks[name][_provideStacks[name].length - 1].emitted.push(value); - } else { - throw new Error("No provider for emit!: " + name); } return NIL; } @@ -1523,10 +1521,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); if (_m == "spread") return 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 (sxEmit("element-attrs", spreadAttrs(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); if (_m == "spread") return 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 (sxEmit("element-attrs", spreadAttrs(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", "provide"]; @@ -1537,10 +1535,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("", filter(function(x) { return !isSxTruthy(isSpread(x)); }, map(function(x) { return renderValueToHtml(x, env); }, expr))) : (function() { + return (isSxTruthy(!isSxTruthy((typeOf(head) == "symbol"))) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() { var name = symbolName(head); var args = rest(expr); - 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() { + 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() { 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)))))))))); @@ -1551,33 +1549,24 @@ 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)))) ? "" : (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() { +})() : (isSxTruthy((name == "when")) ? (isSxTruthy(!isSxTruthy(trampoline(evalExpr(nth(expr, 1), env)))) ? "" : (isSxTruthy((len(expr) == 3)) ? renderToHtml(nth(expr, 2), env) : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(2, len(expr)))))) : (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 (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() { + return (isSxTruthy((len(expr) == 3)) ? renderToHtml(nth(expr, 2), local) : join("", map(function(i) { return renderToHtml(nth(expr, i), local); }, range(2, len(expr))))); +})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? (isSxTruthy((len(expr) == 2)) ? renderToHtml(nth(expr, 1), env) : 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() { var f = trampoline(evalExpr(nth(expr, 1), env)); var coll = trampoline(evalExpr(nth(expr, 2), env)); - 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))); + return join("", 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("", 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))); + return join("", 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("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll))); + return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll)); })() : (isSxTruthy((name == "provide")) ? (function() { var provName = trampoline(evalExpr(nth(expr, 1), env)); var provVal = trampoline(evalExpr(nth(expr, 2), env)); @@ -1585,7 +1574,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = var bodyCount = (len(expr) - 3); providePush(provName, provVal); return (function() { - var result = (isSxTruthy((bodyCount == 1)) ? renderToHtml(nth(expr, bodyStart), env) : join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, map(function(i) { return renderToHtml(nth(expr, i), env); }, range(bodyStart, (bodyStart + bodyCount)))))); + var result = (isSxTruthy((bodyCount == 1)) ? renderToHtml(nth(expr, bodyStart), env) : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(bodyStart, (bodyStart + bodyCount))))); providePop(provName); return result; })(); @@ -1614,14 +1603,7 @@ 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))) { - (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))); -})(); + envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)))); } return renderToHtml(componentBody(comp), local); })(); @@ -1633,14 +1615,12 @@ 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 (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("")); -})()); + return (isSxTruthy(isVoid) ? (String("<") + String(tag) + String(renderAttrs(attrs)) + String(" />")) : (providePush("element-attrs", NIL), (function() { + var content = join("", map(function(c) { return renderToHtml(c, env); }, children)); + { var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; mergeSpreadAttrs(attrs, spreadDict); } } + providePop("element-attrs"); + return (String("<") + String(tag) + String(renderAttrs(attrs)) + String(">") + String(content) + String("")); +})())); })(); }; // render-html-lake @@ -1659,12 +1639,13 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = })(); }, {["i"]: 0, ["skip"]: false}, args); 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("")); + providePush("element-attrs", NIL); + return (function() { + var content = join("", map(function(c) { return renderToHtml(c, env); }, children)); + { var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; mergeSpreadAttrs(lakeAttrs, spreadDict); } } + providePop("element-attrs"); + return (String("<") + String(lakeTag) + String(renderAttrs(lakeAttrs)) + String(">") + String(content) + String("")); +})(); })(); })(); }; @@ -1684,12 +1665,13 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = })(); }, {["i"]: 0, ["skip"]: false}, args); 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("")); + providePush("element-attrs", NIL); + return (function() { + var content = join("", map(function(c) { return renderToHtml(c, env); }, children)); + { var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; mergeSpreadAttrs(marshAttrs, spreadDict); } } + providePop("element-attrs"); + return (String("<") + String(marshTag) + String(renderAttrs(marshAttrs)) + String(">") + String(content) + String("")); +})(); })(); })(); }; @@ -1710,14 +1692,7 @@ 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))) { - (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))); -})(); + envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)))); } return (function() { var bodyHtml = renderToHtml(componentBody(island), local); @@ -1741,10 +1716,13 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = // aser var aser = function(expr, env) { setRenderActiveB(true); -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() { +return (function() { + var result = (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)); if (_m == "spread") return expr; return expr; })(); }; +})(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(expr)), NIL); return expr; })(); + return (isSxTruthy(isSpread(result)) ? (sxEmit("element-attrs", spreadAttrs(result)), NIL) : result); +})(); }; // aser-list var aserList = function(expr, env) { return (function() { @@ -1772,29 +1750,36 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if // aser-call var aserCall = function(name, args, env) { return (function() { - var parts = [name]; + var attrParts = []; + var childParts = []; var skip = false; var i = 0; + providePush("element-attrs", NIL); { var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (isSxTruthy(skip) ? ((skip = false), (i = (i + 1))) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((i + 1) < len(args)))) ? (function() { var val = aser(nth(args, (i + 1)), env); if (isSxTruthy(!isSxTruthy(isNil(val)))) { - parts.push((String(":") + String(keywordName(arg)))); - parts.push(serialize(val)); + attrParts.push((String(":") + String(keywordName(arg)))); + attrParts.push(serialize(val)); } skip = true; return (i = (i + 1)); })() : (function() { var val = aser(arg, env); if (isSxTruthy(!isSxTruthy(isNil(val)))) { - (isSxTruthy(isSpread(val)) ? forEach(function(k) { return (function() { - var v = dictGet(spreadAttrs(val), k); - parts.push((String(":") + String(k))); - return append_b(parts, serialize(v)); -})(); }, keys(spreadAttrs(val))) : (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val)))); + (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(childParts, serialize(item)) : NIL); }, val) : append_b(childParts, serialize(val))); } return (i = (i + 1)); })())); } } + { var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; { var _c = keys(spreadDict); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; (function() { + var v = dictGet(spreadDict, k); + attrParts.push((String(":") + String(k))); + return append_b(attrParts, serialize(v)); +})(); } } } } + providePop("element-attrs"); + return (function() { + var parts = concat([name], attrParts, childParts); return (String("(") + String(join(" ", parts)) + String(")")); +})(); })(); }; // SPECIAL_FORM_NAMES @@ -1898,7 +1883,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 == "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)))); })(); }; +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 (sxEmit("element-attrs", spreadAttrs(expr)), 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() { @@ -1927,6 +1912,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme var renderDomElement = function(tag, args, env, ns) { return (function() { var newNs = (isSxTruthy((tag == "svg")) ? SVG_NS : (isSxTruthy((tag == "math")) ? MATH_NS : ns)); var el = domCreateElement(tag, newNs); + providePush("element-attrs", NIL); reduce(function(state, arg) { return (function() { var skip = get(state, "skip"); return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() { @@ -1951,8 +1937,11 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme return assoc(state, "skip", true, "i", (get(state, "i") + 1)); })() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? (function() { var child = renderToDom(arg, env, newNs); - return (isSxTruthy((isSxTruthy(isSpread(child)) && _islandScope)) ? reactiveSpread(el, function() { return renderToDom(arg, env, newNs); }) : (isSxTruthy(isSpread(child)) ? forEach(function(key) { return (function() { - var val = dictGet(spreadAttrs(child), key); + return (isSxTruthy((isSxTruthy(isSpread(child)) && _islandScope)) ? reactiveSpread(el, function() { return renderToDom(arg, env, newNs); }) : (isSxTruthy(isSpread(child)) ? NIL : domAppend(el, child))); +})() : NIL), assoc(state, "i", (get(state, "i") + 1))))); +})(); }, {["i"]: 0, ["skip"]: false}, args); + { var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; { var _c = keys(spreadDict); for (var _i = 0; _i < _c.length; _i++) { var key = _c[_i]; (function() { + var val = dictGet(spreadDict, 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)); @@ -1960,9 +1949,8 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme 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); +})(); } } } } + providePop("element-attrs"); return el; })(); }; diff --git a/shared/sx/ref/adapter-async.sx b/shared/sx/ref/adapter-async.sx index 605fe20..413c270 100644 --- a/shared/sx/ref/adapter-async.sx +++ b/shared/sx/ref/adapter-async.sx @@ -48,7 +48,7 @@ "string" (escape-html expr) "number" (escape-html (str expr)) "raw-html" (raw-html-content expr) - "spread" expr + "spread" (do (emit! "element-attrs" (spread-attrs expr)) "") "symbol" (let ((val (async-eval expr env ctx))) (async-render val env ctx)) "keyword" (escape-html (keyword-name expr)) @@ -80,10 +80,9 @@ (= name "raw!") (async-render-raw args env ctx) - ;; Fragment (spreads filtered — no parent element) + ;; Fragment (= name "<>") - (join "" (filter (fn (r) (not (spread? r))) - (async-map-render args env ctx))) + (join "" (async-map-render args env ctx)) ;; html: prefix (starts-with? name "html:") @@ -171,18 +170,19 @@ (css-class-collect! (str class-val)))) (if (contains? VOID_ELEMENTS tag) (str "<" tag (render-attrs attrs) ">") - ;; Render children, collecting spreads and content separately + ;; Provide scope for spread emit! (let ((token (if (or (= tag "svg") (= tag "math")) (svg-context-set! true) nil)) (content-parts (list))) + (provide-push! "element-attrs" nil) (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)))) + (fn (c) (append! content-parts (async-render c env ctx))) children) + (for-each + (fn (spread-dict) (merge-spread-attrs attrs spread-dict)) + (emitted "element-attrs")) + (provide-pop! "element-attrs") (when token (svg-context-reset! token)) (str "<" tag (render-attrs attrs) ">" (join "" content-parts) @@ -231,14 +231,11 @@ (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) + ;; Pre-render children to raw HTML (when (component-has-children? comp) (let ((parts (list))) (for-each - (fn (c) - (let ((r (async-render c env ctx))) - (when (not (spread? r)) - (append! parts r)))) + (fn (c) (append! parts (async-render c env ctx))) children) (env-set! local "children" (make-raw-html (join "" parts))))) @@ -259,14 +256,11 @@ (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) + ;; Pre-render children (when (component-has-children? island) (let ((parts (list))) (for-each - (fn (c) - (let ((r (async-render c env ctx))) - (when (not (spread? r)) - (append! parts r)))) + (fn (c) (append! parts (async-render c env ctx))) children) (env-set! local "children" (make-raw-html (join "" parts))))) @@ -367,14 +361,13 @@ (async-render (nth expr 3) env ctx) ""))) - ;; when — single body: pass through (spread propagates). Multi: join strings. + ;; when — single body: pass through. Multi: join strings. (= name "when") (if (not (async-eval (nth expr 1) env ctx)) "" (if (= (len expr) 3) (async-render (nth expr 2) env ctx) - (let ((results (async-map-render (slice expr 2) env ctx))) - (join "" (filter (fn (r) (not (spread? r))) results))))) + (join "" (async-map-render (slice expr 2) env ctx)))) ;; cond — uses cond-scheme? (every? check) from eval.sx (= name "cond") @@ -392,47 +385,39 @@ (let ((local (async-process-bindings (nth expr 1) env ctx))) (if (= (len expr) 3) (async-render (nth expr 2) local ctx) - (let ((results (async-map-render (slice expr 2) local ctx))) - (join "" (filter (fn (r) (not (spread? r))) results))))) + (join "" (async-map-render (slice expr 2) local ctx)))) ;; begin / do — single body: pass through. Multi: join strings. (or (= name "begin") (= name "do")) (if (= (len expr) 2) (async-render (nth expr 1) env ctx) - (let ((results (async-map-render (rest expr) env ctx))) - (join "" (filter (fn (r) (not (spread? r))) results)))) + (join "" (async-map-render (rest expr) env ctx))) ;; Definition forms (definition-form? name) (do (async-eval expr env ctx) "") - ;; map — spreads filtered + ;; map (= name "map") (let ((f (async-eval (nth expr 1) env ctx)) (coll (async-eval (nth expr 2) env ctx))) - (join "" - (filter (fn (r) (not (spread? r))) - (async-map-fn-render f coll env ctx)))) + (join "" (async-map-fn-render f coll env ctx))) - ;; map-indexed — spreads filtered + ;; map-indexed (= name "map-indexed") (let ((f (async-eval (nth expr 1) env ctx)) (coll (async-eval (nth expr 2) env ctx))) - (join "" - (filter (fn (r) (not (spread? r))) - (async-map-indexed-fn-render f coll env ctx)))) + (join "" (async-map-indexed-fn-render f coll env ctx))) ;; filter — eval fully then render (= name "filter") (async-render (async-eval expr env ctx) env ctx) - ;; for-each (render variant) — spreads filtered + ;; for-each (render variant) (= name "for-each") (let ((f (async-eval (nth expr 1) env ctx)) (coll (async-eval (nth expr 2) env ctx))) - (join "" - (filter (fn (r) (not (spread? r))) - (async-map-fn-render f coll env ctx)))) + (join "" (async-map-fn-render f coll env ctx))) ;; provide — render-time dynamic scope (= name "provide") @@ -443,8 +428,7 @@ (provide-push! prov-name prov-val) (let ((result (if (= body-count 1) (async-render (nth expr body-start) env ctx) - (let ((results (async-map-render (slice expr body-start) env ctx))) - (join "" (filter (fn (r) (not (spread? r))) results)))))) + (join "" (async-map-render (slice expr body-start) env ctx))))) (provide-pop! prov-name) result)) @@ -595,35 +579,34 @@ (define-async async-aser :effects [render io] (fn (expr (env :as dict) ctx) - (case (type-of expr) - "number" expr - "string" expr - "boolean" expr - "nil" nil - - "symbol" - (let ((name (symbol-name expr))) - (cond - (env-has? env name) (env-get env name) - (primitive? name) (get-primitive name) - (= name "true") true - (= name "false") false - (= name "nil") nil - :else (error (str "Undefined symbol: " name)))) - - "keyword" (keyword-name expr) - - "dict" (async-aser-dict expr env ctx) - - ;; Spread — pass through for client rendering - "spread" expr - - "list" - (if (empty? expr) - (list) - (async-aser-list expr env ctx)) - - :else expr))) + (let ((t (type-of expr)) + (result nil)) + (cond + (= t "number") (set! result expr) + (= t "string") (set! result expr) + (= t "boolean") (set! result expr) + (= t "nil") (set! result nil) + (= t "symbol") + (let ((name (symbol-name expr))) + (set! result + (cond + (env-has? env name) (env-get env name) + (primitive? name) (get-primitive name) + (= name "true") true + (= name "false") false + (= name "nil") nil + :else (error (str "Undefined symbol: " name))))) + (= t "keyword") (set! result (keyword-name expr)) + (= t "dict") (set! result (async-aser-dict expr env ctx)) + ;; Spread — emit attrs to nearest element provider + (= t "spread") (do (emit! "element-attrs" (spread-attrs expr)) + (set! result nil)) + (= t "list") (set! result (if (empty? expr) (list) (async-aser-list expr env ctx))) + :else (set! result expr)) + ;; Catch spread values from function calls and symbol lookups + (if (spread? result) + (do (emit! "element-attrs" (spread-attrs result)) nil) + result)))) (define-async async-aser-dict :effects [render io] @@ -775,7 +758,6 @@ (define-async async-aser-fragment :effects [render io] (fn ((children :as list) (env :as dict) ctx) - ;; Spreads are filtered — fragments have no parent element to merge into (let ((parts (list))) (for-each (fn (c) @@ -783,10 +765,10 @@ (if (= (type-of result) "list") (for-each (fn (item) - (when (and (not (nil? item)) (not (spread? item))) + (when (not (nil? item)) (append! parts (serialize item)))) result) - (when (and (not (nil? result)) (not (spread? result))) + (when (not (nil? result)) (append! parts (serialize result)))))) children) (if (empty? parts) @@ -860,9 +842,12 @@ (let ((token (if (or (= name "svg") (= name "math")) (svg-context-set! true) nil)) - (parts (list name)) + (attr-parts (list)) + (child-parts (list)) (skip false) (i 0)) + ;; Provide scope for spread emit! + (provide-push! "element-attrs" nil) (for-each (fn (arg) (if skip @@ -872,39 +857,43 @@ (< (inc i) (len args))) (let ((val (async-aser (nth args (inc i)) env ctx))) (when (not (nil? val)) - (append! parts (str ":" (keyword-name arg))) + (append! attr-parts (str ":" (keyword-name arg))) (if (= (type-of val) "list") (let ((live (filter (fn (v) (not (nil? v))) val))) (if (empty? live) - (append! parts "nil") + (append! attr-parts "nil") (let ((items (map serialize live))) (if (some (fn (v) (sx-expr? v)) live) - (append! parts (str "(<> " (join " " items) ")")) - (append! parts (str "(list " (join " " items) ")")))))) - (append! parts (serialize val)))) + (append! attr-parts (str "(<> " (join " " items) ")")) + (append! attr-parts (str "(list " (join " " items) ")")))))) + (append! attr-parts (serialize val)))) (set! skip true) (set! i (inc i))) (let ((result (async-aser arg env ctx))) (when (not (nil? result)) - (if (spread? result) - ;; Spread child — merge attrs as keyword args into parent element + (if (= (type-of result) "list") (for-each - (fn (k) - (let ((v (dict-get (spread-attrs result) k))) - (append! parts (str ":" k)) - (append! parts (serialize v)))) - (keys (spread-attrs result))) - (if (= (type-of result) "list") - (for-each - (fn (item) - (when (not (nil? item)) - (append! parts (serialize item)))) - result) - (append! parts (serialize result))))) + (fn (item) + (when (not (nil? item)) + (append! child-parts (serialize item)))) + result) + (append! child-parts (serialize result)))) (set! i (inc i)))))) args) + ;; Collect emitted spread attrs — after explicit attrs, before children + (for-each + (fn (spread-dict) + (for-each + (fn (k) + (let ((v (dict-get spread-dict k))) + (append! attr-parts (str ":" k)) + (append! attr-parts (serialize v)))) + (keys spread-dict))) + (emitted "element-attrs")) + (provide-pop! "element-attrs") (when token (svg-context-reset! token)) - (make-sx-expr (str "(" (join " " parts) ")"))))) + (let ((parts (concat (list name) attr-parts child-parts))) + (make-sx-expr (str "(" (join " " parts) ")")))))) ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx index 40edf4f..042f9b7 100644 --- a/shared/sx/ref/adapter-dom.sx +++ b/shared/sx/ref/adapter-dom.sx @@ -44,8 +44,8 @@ ;; Pre-rendered DOM node → pass through "dom-node" expr - ;; Spread → pass through (parent element handles it) - "spread" expr + ;; Spread → emit attrs to nearest element provider, pass through for reactive-spread + "spread" (do (emit! "element-attrs" (spread-attrs expr)) expr) ;; Dict → empty "dict" (create-fragment) @@ -180,6 +180,9 @@ :else ns)) (el (dom-create-element tag new-ns))) + ;; Provide scope for spread emit! — deeply nested spreads emit here + (provide-push! "element-attrs" nil) + ;; Process args: keywords → attrs, others → children (reduce (fn (state arg) @@ -236,28 +239,8 @@ ;; Reactive spread: track signal deps, update attrs on change (and (spread? child) *island-scope*) (reactive-spread el (fn () (render-to-dom arg env new-ns))) - ;; Static spread: one-shot merge attrs onto parent element - (spread? child) - (for-each - (fn ((key :as string)) - (let ((val (dict-get (spread-attrs child) key))) - (if (= key "class") - ;; Class: append to existing - (let ((existing (dom-get-attr el "class"))) - (dom-set-attr el "class" - (if (and existing (not (= existing ""))) - (str existing " " val) - val))) - (if (= key "style") - ;; Style: append with semicolon - (let ((existing (dom-get-attr el "style"))) - (dom-set-attr el "style" - (if (and existing (not (= existing ""))) - (str existing ";" val) - val))) - ;; Other attrs: overwrite - (dom-set-attr el key (str val)))))) - (keys (spread-attrs child))) + ;; Static spread: already emitted via provide, skip + (spread? child) nil ;; Normal child: append to element :else (dom-append el child)))) @@ -265,6 +248,29 @@ (dict "i" 0 "skip" false) args) + ;; Collect emitted spread attrs and merge onto DOM element + (for-each + (fn (spread-dict) + (for-each + (fn ((key :as string)) + (let ((val (dict-get spread-dict key))) + (if (= key "class") + (let ((existing (dom-get-attr el "class"))) + (dom-set-attr el "class" + (if (and existing (not (= existing ""))) + (str existing " " val) + val))) + (if (= key "style") + (let ((existing (dom-get-attr el "style"))) + (dom-set-attr el "style" + (if (and existing (not (= existing ""))) + (str existing ";" val) + val))) + (dom-set-attr el key (str val)))))) + (keys spread-dict))) + (emitted "element-attrs")) + (provide-pop! "element-attrs") + el))) diff --git a/shared/sx/ref/adapter-html.sx b/shared/sx/ref/adapter-html.sx index 3ad5f9b..bc5d833 100644 --- a/shared/sx/ref/adapter-html.sx +++ b/shared/sx/ref/adapter-html.sx @@ -30,8 +30,8 @@ "keyword" (escape-html (keyword-name expr)) ;; Raw HTML passthrough "raw-html" (raw-html-content expr) - ;; Spread — pass through as-is (parent element will merge attrs) - "spread" expr + ;; Spread — emit attrs to nearest element provider + "spread" (do (emit! "element-attrs" (spread-attrs expr)) "") ;; Everything else — evaluate first :else (render-value-to-html (trampoline (eval-expr expr env)) env)))) @@ -44,7 +44,7 @@ "boolean" (if val "true" "false") "list" (render-list-to-html val env) "raw-html" (raw-html-content val) - "spread" val + "spread" (do (emit! "element-attrs" (spread-attrs val)) "") :else (escape-html (str val))))) @@ -73,16 +73,14 @@ "" (let ((head (first expr))) (if (not (= (type-of head) "symbol")) - ;; Data list — render each item (spreads filtered — no parent element) - (join "" (filter (fn (x) (not (spread? x))) - (map (fn (x) (render-value-to-html x env)) expr))) + ;; Data list — render each item + (join "" (map (fn (x) (render-value-to-html x env)) expr)) (let ((name (symbol-name head)) (args (rest expr))) (cond - ;; Fragment (spreads filtered — no parent element) + ;; Fragment (= name "<>") - (join "" (filter (fn (x) (not (spread? x))) - (map (fn (x) (render-to-html x env)) args))) + (join "" (map (fn (x) (render-to-html x env)) args)) ;; Raw HTML passthrough (= name "raw!") @@ -152,15 +150,14 @@ (render-to-html (nth expr 3) env) ""))) - ;; when — single body: pass through (spread propagates). Multi: join strings. + ;; when — single body: pass through. Multi: join strings. (= name "when") (if (not (trampoline (eval-expr (nth expr 1) env))) "" (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))))) + (join "" (map (fn (i) (render-to-html (nth expr i) env)) + (range 2 (len expr)))))) ;; cond (= name "cond") @@ -178,64 +175,59 @@ (let ((local (process-bindings (nth expr 1) env))) (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))))) + (join "" (map (fn (i) (render-to-html (nth expr i) local)) + (range 2 (len expr)))))) ;; begin / do — single body: pass through. Multi: join strings. (or (= name "begin") (= name "do")) (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)))) + (join "" (map (fn (i) (render-to-html (nth expr i) env)) + (range 1 (len expr))))) ;; Definition forms — eval for side effects (definition-form? name) (do (trampoline (eval-expr expr env)) "") - ;; map — spreads filtered (no parent element in list context) + ;; map (= name "map") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env)))) (join "" - (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 + (fn (item) + (if (lambda? f) + (render-lambda-html f (list item) env) + (render-to-html (apply f (list item)) env))) + coll))) - ;; map-indexed — spreads filtered + ;; map-indexed (= name "map-indexed") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env)))) (join "" - (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)))) + (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) — spreads filtered + ;; for-each (render variant) (= name "for-each") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env)))) (join "" - (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 + (fn (item) + (if (lambda? f) + (render-lambda-html f (list item) env) + (render-to-html (apply f (list item)) env))) + coll))) ;; provide — render-time dynamic scope (= name "provide") @@ -246,9 +238,8 @@ (provide-push! prov-name prov-val) (let ((result (if (= body-count 1) (render-to-html (nth expr body-start) env) - (join "" (filter (fn (r) (not (spread? r))) - (map (fn (i) (render-to-html (nth expr i) env)) - (range body-start (+ body-start body-count)))))))) + (join "" (map (fn (i) (render-to-html (nth expr i) env)) + (range body-start (+ body-start body-count))))))) (provide-pop! prov-name) result)) @@ -307,17 +298,9 @@ (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) - (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))))) + (env-set! local "children" + (make-raw-html (join "" (map (fn (c) (render-to-html c env)) children))))) (render-to-html (component-body comp) local))))) @@ -329,18 +312,17 @@ (is-void (contains? VOID_ELEMENTS tag))) (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) - "")))))) + ;; Provide scope for spread emit! + (do + (provide-push! "element-attrs" nil) + (let ((content (join "" (map (fn (c) (render-to-html c env)) children)))) + (for-each + (fn (spread-dict) (merge-spread-attrs attrs spread-dict)) + (emitted "element-attrs")) + (provide-pop! "element-attrs") + (str "<" tag (render-attrs attrs) ">" + content + ""))))))) ;; -------------------------------------------------------------------------- @@ -375,19 +357,17 @@ (assoc state "i" (inc (get state "i")))))))) (dict "i" 0 "skip" false) args) - ;; 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) - ""))))) + ;; Provide scope for spread emit! + (let ((lake-attrs (dict "data-sx-lake" (or lake-id "")))) + (provide-push! "element-attrs" nil) + (let ((content (join "" (map (fn (c) (render-to-html c env)) children)))) + (for-each + (fn (spread-dict) (merge-spread-attrs lake-attrs spread-dict)) + (emitted "element-attrs")) + (provide-pop! "element-attrs") + (str "<" lake-tag (render-attrs lake-attrs) ">" + content + "")))))) ;; -------------------------------------------------------------------------- @@ -425,19 +405,17 @@ (assoc state "i" (inc (get state "i")))))))) (dict "i" 0 "skip" false) args) - ;; 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) - ""))))) + ;; Provide scope for spread emit! + (let ((marsh-attrs (dict "data-sx-marsh" (or marsh-id "")))) + (provide-push! "element-attrs" nil) + (let ((content (join "" (map (fn (c) (render-to-html c env)) children)))) + (for-each + (fn (spread-dict) (merge-spread-attrs marsh-attrs spread-dict)) + (emitted "element-attrs")) + (provide-pop! "element-attrs") + (str "<" marsh-tag (render-attrs marsh-attrs) ">" + content + "")))))) ;; -------------------------------------------------------------------------- @@ -487,17 +465,9 @@ (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) - (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))))) + (env-set! local "children" + (make-raw-html (join "" (map (fn (c) (render-to-html c env)) children))))) ;; 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 ac9e7a1..47f2738 100644 --- a/shared/sx/ref/adapter-sx.sx +++ b/shared/sx/ref/adapter-sx.sx @@ -25,33 +25,38 @@ ;; Evaluate for SX wire format — serialize rendering forms, ;; evaluate control flow and function calls. (set-render-active! true) - (case (type-of expr) - "number" expr - "string" expr - "boolean" expr - "nil" nil + (let ((result + (case (type-of expr) + "number" expr + "string" expr + "boolean" expr + "nil" nil - "symbol" - (let ((name (symbol-name expr))) - (cond - (env-has? env name) (env-get env name) - (primitive? name) (get-primitive name) - (= name "true") true - (= name "false") false - (= name "nil") nil - :else (error (str "Undefined symbol: " name)))) + "symbol" + (let ((name (symbol-name expr))) + (cond + (env-has? env name) (env-get env name) + (primitive? name) (get-primitive name) + (= name "true") true + (= name "false") false + (= name "nil") nil + :else (error (str "Undefined symbol: " name)))) - "keyword" (keyword-name expr) + "keyword" (keyword-name expr) - "list" - (if (empty? expr) - (list) - (aser-list expr env)) + "list" + (if (empty? expr) + (list) + (aser-list expr env)) - ;; Spread — pass through for client rendering - "spread" expr + ;; Spread — emit attrs to nearest element provider + "spread" (do (emit! "element-attrs" (spread-attrs expr)) nil) - :else expr))) + :else expr))) + ;; Catch spread values from function calls and symbol lookups + (if (spread? result) + (do (emit! "element-attrs" (spread-attrs result)) nil) + result)))) (define aser-list :effects [render] @@ -110,7 +115,6 @@ (fn ((children :as list) (env :as dict)) ;; Serialize (<> child1 child2 ...) to sx source string ;; Must flatten list results (e.g. from map/filter) to avoid nested parens - ;; Spreads are filtered — fragments have no parent element to merge into (let ((parts (list))) (for-each (fn (c) @@ -118,10 +122,10 @@ (if (= (type-of result) "list") (for-each (fn (item) - (when (and (not (nil? item)) (not (spread? item))) + (when (not (nil? item)) (append! parts (serialize item)))) result) - (when (and (not (nil? result)) (not (spread? result))) + (when (not (nil? result)) (append! parts (serialize result)))))) children) (if (empty? parts) @@ -134,9 +138,13 @@ ;; Serialize (name :key val child ...) — evaluate args but keep as sx ;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops ;; that can contain nested for-each for list flattening. - (let ((parts (list name)) + ;; Separate attrs and children so emitted spread attrs go before children. + (let ((attr-parts (list)) + (child-parts (list)) (skip false) (i 0)) + ;; Provide scope for spread emit! + (provide-push! "element-attrs" nil) (for-each (fn (arg) (if skip @@ -146,30 +154,34 @@ (< (inc i) (len args))) (let ((val (aser (nth args (inc i)) env))) (when (not (nil? val)) - (append! parts (str ":" (keyword-name arg))) - (append! parts (serialize val))) + (append! attr-parts (str ":" (keyword-name arg))) + (append! attr-parts (serialize val))) (set! skip true) (set! i (inc i))) (let ((val (aser arg env))) (when (not (nil? val)) - (if (spread? val) - ;; Spread child — merge attrs as keyword args into parent element + (if (= (type-of val) "list") (for-each - (fn (k) - (let ((v (dict-get (spread-attrs val) k))) - (append! parts (str ":" k)) - (append! parts (serialize v)))) - (keys (spread-attrs val))) - (if (= (type-of val) "list") - (for-each - (fn (item) - (when (not (nil? item)) - (append! parts (serialize item)))) - val) - (append! parts (serialize val))))) + (fn (item) + (when (not (nil? item)) + (append! child-parts (serialize item)))) + val) + (append! child-parts (serialize val)))) (set! i (inc i)))))) args) - (str "(" (join " " parts) ")")))) + ;; Collect emitted spread attrs — goes after explicit attrs, before children + (for-each + (fn (spread-dict) + (for-each + (fn (k) + (let ((v (dict-get spread-dict k))) + (append! attr-parts (str ":" k)) + (append! attr-parts (serialize v)))) + (keys spread-dict))) + (emitted "element-attrs")) + (provide-pop! "element-attrs") + (let ((parts (concat (list name) attr-parts child-parts))) + (str "(" (join " " parts) ")"))))) ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index 9e10be3..909be04 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -1203,8 +1203,6 @@ PLATFORM_JS_PRE = ''' function sxEmit(name, value) { if (_provideStacks[name] && _provideStacks[name].length) { _provideStacks[name][_provideStacks[name].length - 1].emitted.push(value); - } else { - throw new Error("No provider for emit!: " + name); } return NIL; } diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py index 0744671..78be9fb 100644 --- a/shared/sx/ref/platform_py.py +++ b/shared/sx/ref/platform_py.py @@ -126,11 +126,9 @@ def sx_context(name, *default): def sx_emit(name, value): - """Append value to nearest enclosing provider's accumulator. Error if no provider.""" + """Append value to nearest enclosing provider's accumulator. No-op if no provider.""" if name in _provide_stacks and _provide_stacks[name]: _provide_stacks[name][-1]["emitted"].append(value) - else: - raise RuntimeError(f"No provider for emit!: {name}") return NIL diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 747f98b..3b2c1a4 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -1,5 +1,3 @@ -# WARNING: special-forms.sx declares forms not in eval.sx: reset, shift -# WARNING: eval.sx dispatches forms not in special-forms.sx: form?, provide """ sx_ref.py -- Generated from reference SX evaluator specification. @@ -87,11 +85,9 @@ def sx_context(name, *default): def sx_emit(name, value): - """Append value to nearest enclosing provider's accumulator. Error if no provider.""" + """Append value to nearest enclosing provider's accumulator. No-op if no provider.""" if name in _provide_stacks and _provide_stacks[name]: _provide_stacks[name][-1]["emitted"].append(value) - else: - raise RuntimeError(f"No provider for emit!: {name}") return NIL @@ -2225,7 +2221,8 @@ def render_to_html(expr, env): elif _match == 'raw-html': return raw_html_content(expr) elif _match == 'spread': - return expr + sx_emit('element-attrs', spread_attrs(expr)) + return '' else: return render_value_to_html(trampoline(eval_expr(expr, env)), env) @@ -2248,7 +2245,8 @@ def render_value_to_html(val, env): elif _match == 'raw-html': return raw_html_content(val) elif _match == 'spread': - return val + sx_emit('element-attrs', spread_attrs(val)) + return '' else: return escape_html(sx_str(val)) @@ -2266,12 +2264,12 @@ def render_list_to_html(expr, env): else: head = first(expr) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))): - return join('', filter(lambda x: (not sx_truthy(is_spread(x))), map(lambda x: render_value_to_html(x, env), expr))) + return join('', map(lambda x: render_value_to_html(x, env), expr)) else: name = symbol_name(head) args = rest(expr) if sx_truthy((name == '<>')): - return join('', filter(lambda x: (not sx_truthy(is_spread(x))), map(lambda x: render_to_html(x, env), args))) + return join('', map(lambda x: render_to_html(x, env), args)) elif sx_truthy((name == 'raw!')): return join('', map(lambda x: sx_str(trampoline(eval_expr(x, env))), args)) elif sx_truthy((name == 'lake')): @@ -2315,8 +2313,7 @@ def dispatch_html_form(name, expr, env): if sx_truthy((len(expr) == 3)): return render_to_html(nth(expr, 2), env) else: - results = map(lambda i: render_to_html(nth(expr, i), env), range(2, len(expr))) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)) + return join('', map(lambda i: render_to_html(nth(expr, i), env), range(2, len(expr)))) elif sx_truthy((name == 'cond')): branch = eval_cond(rest(expr), env) if sx_truthy(branch): @@ -2330,38 +2327,36 @@ def dispatch_html_form(name, expr, env): if sx_truthy((len(expr) == 3)): return render_to_html(nth(expr, 2), local) else: - results = map(lambda i: render_to_html(nth(expr, i), local), range(2, len(expr))) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)) + return join('', map(lambda i: render_to_html(nth(expr, i), local), range(2, len(expr)))) elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))): if sx_truthy((len(expr) == 2)): return render_to_html(nth(expr, 1), env) else: - results = map(lambda i: render_to_html(nth(expr, i), env), range(1, len(expr))) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)) + return join('', map(lambda i: render_to_html(nth(expr, i), env), range(1, len(expr)))) elif sx_truthy(is_definition_form(name)): trampoline(eval_expr(expr, env)) return '' elif sx_truthy((name == 'map')): f = trampoline(eval_expr(nth(expr, 1), env)) coll = trampoline(eval_expr(nth(expr, 2), env)) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll))) + return join('', map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll)) elif sx_truthy((name == 'map-indexed')): f = trampoline(eval_expr(nth(expr, 1), env)) coll = trampoline(eval_expr(nth(expr, 2), env)) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), map_indexed(lambda i, item: (render_lambda_html(f, [i, item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [i, item]), env)), coll))) + return join('', map_indexed(lambda i, item: (render_lambda_html(f, [i, item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [i, item]), env)), coll)) elif sx_truthy((name == 'filter')): return render_to_html(trampoline(eval_expr(expr, env)), env) elif sx_truthy((name == 'for-each')): f = trampoline(eval_expr(nth(expr, 1), env)) coll = trampoline(eval_expr(nth(expr, 2), env)) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll))) + return join('', map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll)) elif sx_truthy((name == 'provide')): prov_name = trampoline(eval_expr(nth(expr, 1), env)) prov_val = trampoline(eval_expr(nth(expr, 2), env)) body_start = 3 body_count = (len(expr) - 3) provide_push(prov_name, prov_val) - result = (render_to_html(nth(expr, body_start), env) if sx_truthy((body_count == 1)) else join('', filter(lambda r: (not sx_truthy(is_spread(r))), map(lambda i: render_to_html(nth(expr, i), env), range(body_start, (body_start + body_count)))))) + result = (render_to_html(nth(expr, body_start), env) if sx_truthy((body_count == 1)) else join('', map(lambda i: render_to_html(nth(expr, i), env), range(body_start, (body_start + body_count))))) provide_pop(prov_name) return result else: @@ -2382,12 +2377,7 @@ def render_html_component(comp, args, env): for p in component_params(comp): local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL) if sx_truthy(component_has_children(comp)): - parts = [] - for c in children: - r = render_to_html(c, env) - if sx_truthy((not sx_truthy(is_spread(r)))): - parts.append(r) - local['children'] = make_raw_html(join('', parts)) + local['children'] = make_raw_html(join('', map(lambda c: render_to_html(c, env), children))) return render_to_html(component_body(comp), local) # render-html-element @@ -2399,14 +2389,12 @@ def render_html_element(tag, args, env): if sx_truthy(is_void): return sx_str('<', tag, render_attrs(attrs), ' />') else: - content_parts = [] - for c in children: - result = render_to_html(c, env) - if sx_truthy(is_spread(result)): - merge_spread_attrs(attrs, spread_attrs(result)) - else: - content_parts.append(result) - return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '') + provide_push('element-attrs', NIL) + content = join('', map(lambda c: render_to_html(c, env), children)) + for spread_dict in sx_emitted('element-attrs'): + merge_spread_attrs(attrs, spread_dict) + provide_pop('element-attrs') + return sx_str('<', tag, render_attrs(attrs), '>', content, '') # render-html-lake def render_html_lake(args, env): @@ -2416,14 +2404,12 @@ def render_html_lake(args, env): children = [] reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda kname: (lambda kval: _sx_begin((_sx_cell_set(_cells, 'lake_id', kval) if sx_truthy((kname == 'id')) else (_sx_cell_set(_cells, 'lake_tag', kval) if sx_truthy((kname == 'tag')) else NIL)), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))))(keyword_name(arg)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args) lake_attrs = {'data-sx-lake': (_cells['lake_id'] if sx_truthy(_cells['lake_id']) else '')} - content_parts = [] - for c in children: - result = render_to_html(c, env) - if sx_truthy(is_spread(result)): - merge_spread_attrs(lake_attrs, spread_attrs(result)) - else: - content_parts.append(result) - return sx_str('<', _cells['lake_tag'], render_attrs(lake_attrs), '>', join('', content_parts), '') + provide_push('element-attrs', NIL) + content = join('', map(lambda c: render_to_html(c, env), children)) + for spread_dict in sx_emitted('element-attrs'): + merge_spread_attrs(lake_attrs, spread_dict) + provide_pop('element-attrs') + return sx_str('<', _cells['lake_tag'], render_attrs(lake_attrs), '>', content, '') # render-html-marsh def render_html_marsh(args, env): @@ -2433,14 +2419,12 @@ def render_html_marsh(args, env): children = [] reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda kname: (lambda kval: _sx_begin((_sx_cell_set(_cells, 'marsh_id', kval) if sx_truthy((kname == 'id')) else (_sx_cell_set(_cells, 'marsh_tag', kval) if sx_truthy((kname == 'tag')) else (NIL if sx_truthy((kname == 'transform')) else NIL))), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))))(keyword_name(arg)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args) marsh_attrs = {'data-sx-marsh': (_cells['marsh_id'] if sx_truthy(_cells['marsh_id']) else '')} - content_parts = [] - for c in children: - result = render_to_html(c, env) - if sx_truthy(is_spread(result)): - merge_spread_attrs(marsh_attrs, spread_attrs(result)) - else: - content_parts.append(result) - return sx_str('<', _cells['marsh_tag'], render_attrs(marsh_attrs), '>', join('', content_parts), '') + provide_push('element-attrs', NIL) + content = join('', map(lambda c: render_to_html(c, env), children)) + for spread_dict in sx_emitted('element-attrs'): + merge_spread_attrs(marsh_attrs, spread_dict) + provide_pop('element-attrs') + return sx_str('<', _cells['marsh_tag'], render_attrs(marsh_attrs), '>', content, '') # render-html-island def render_html_island(island, args, env): @@ -2452,12 +2436,7 @@ def render_html_island(island, args, env): for p in component_params(island): local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL) if sx_truthy(component_has_children(island)): - parts = [] - for c in children: - r = render_to_html(c, env) - if sx_truthy((not sx_truthy(is_spread(r)))): - parts.append(r) - local['children'] = make_raw_html(join('', parts)) + local['children'] = make_raw_html(join('', map(lambda c: render_to_html(c, env), children))) body_html = render_to_html(component_body(island), local) state_sx = serialize_island_state(kwargs) return sx_str('', body_html, '') @@ -2483,40 +2462,12 @@ def render_to_sx(expr, env): # aser def aser(expr, env): set_render_active_b(True) - _match = type_of(expr) - if _match == 'number': - return expr - elif _match == 'string': - return expr - elif _match == 'boolean': - return expr - elif _match == 'nil': + result = _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else aser_list(expr, env))), ('spread', lambda: _sx_begin(sx_emit('element-attrs', spread_attrs(expr)), NIL)), (None, lambda: expr)]) + if sx_truthy(is_spread(result)): + sx_emit('element-attrs', spread_attrs(result)) return NIL - elif _match == 'symbol': - name = symbol_name(expr) - if sx_truthy(env_has(env, name)): - return env_get(env, name) - elif sx_truthy(is_primitive(name)): - return get_primitive(name) - elif sx_truthy((name == 'true')): - return True - elif sx_truthy((name == 'false')): - return False - elif sx_truthy((name == 'nil')): - return NIL - else: - return error(sx_str('Undefined symbol: ', name)) - elif _match == 'keyword': - return keyword_name(expr) - elif _match == 'list': - if sx_truthy(empty_p(expr)): - return [] - else: - return aser_list(expr, env) - elif _match == 'spread': - return expr else: - return expr + return result # aser-list def aser_list(expr, env): @@ -2561,10 +2512,10 @@ def aser_fragment(children, env): result = aser(c, env) if sx_truthy((type_of(result) == 'list')): for item in result: - if sx_truthy(((not sx_truthy(is_nil(item))) if not sx_truthy((not sx_truthy(is_nil(item)))) else (not sx_truthy(is_spread(item))))): + if sx_truthy((not sx_truthy(is_nil(item)))): parts.append(serialize(item)) else: - if sx_truthy(((not sx_truthy(is_nil(result))) if not sx_truthy((not sx_truthy(is_nil(result)))) else (not sx_truthy(is_spread(result))))): + if sx_truthy((not sx_truthy(is_nil(result)))): parts.append(serialize(result)) if sx_truthy(empty_p(parts)): return '' @@ -2574,9 +2525,11 @@ def aser_fragment(children, env): # aser-call def aser_call(name, args, env): _cells = {} - parts = [name] + attr_parts = [] + child_parts = [] _cells['skip'] = False _cells['i'] = 0 + provide_push('element-attrs', NIL) for arg in args: if sx_truthy(_cells['skip']): _cells['skip'] = False @@ -2585,26 +2538,27 @@ def aser_call(name, args, env): if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))): val = aser(nth(args, (_cells['i'] + 1)), env) if sx_truthy((not sx_truthy(is_nil(val)))): - parts.append(sx_str(':', keyword_name(arg))) - parts.append(serialize(val)) + attr_parts.append(sx_str(':', keyword_name(arg))) + attr_parts.append(serialize(val)) _cells['skip'] = True _cells['i'] = (_cells['i'] + 1) else: val = aser(arg, env) if sx_truthy((not sx_truthy(is_nil(val)))): - if sx_truthy(is_spread(val)): - for k in keys(spread_attrs(val)): - v = dict_get(spread_attrs(val), k) - parts.append(sx_str(':', k)) - parts.append(serialize(v)) + if sx_truthy((type_of(val) == 'list')): + for item in val: + if sx_truthy((not sx_truthy(is_nil(item)))): + child_parts.append(serialize(item)) else: - if sx_truthy((type_of(val) == 'list')): - for item in val: - if sx_truthy((not sx_truthy(is_nil(item)))): - parts.append(serialize(item)) - else: - parts.append(serialize(val)) + child_parts.append(serialize(val)) _cells['i'] = (_cells['i'] + 1) + for spread_dict in sx_emitted('element-attrs'): + for k in keys(spread_dict): + v = dict_get(spread_dict, k) + attr_parts.append(sx_str(':', k)) + attr_parts.append(serialize(v)) + provide_pop('element-attrs') + parts = concat([name], attr_parts, child_parts) return sx_str('(', join(' ', parts), ')') # SPECIAL_FORM_NAMES @@ -3659,7 +3613,8 @@ async def async_render(expr, env, ctx): elif _match == 'raw-html': return raw_html_content(expr) elif _match == 'spread': - return expr + sx_emit('element-attrs', spread_attrs(expr)) + return '' elif _match == 'symbol': val = (await async_eval(expr, env, ctx)) return (await async_render(val, env, ctx)) @@ -3691,7 +3646,7 @@ async def async_render_list(expr, env, ctx): elif sx_truthy((name == 'raw!')): return (await async_render_raw(args, env, ctx)) elif sx_truthy((name == '<>')): - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_render(args, env, ctx)))) + return join('', (await async_map_render(args, env, ctx))) elif sx_truthy(starts_with_p(name, 'html:')): return (await async_render_element(slice(name, 5), args, env, ctx)) elif sx_truthy(async_render_form_p(name)): @@ -3746,12 +3701,12 @@ async def async_render_element(tag, args, env, ctx): else: token = (svg_context_set(True) if sx_truthy(((tag == 'svg') if sx_truthy((tag == 'svg')) else (tag == 'math'))) else NIL) content_parts = [] + provide_push('element-attrs', NIL) for c in children: - result = (await async_render(c, env, ctx)) - if sx_truthy(is_spread(result)): - merge_spread_attrs(attrs, spread_attrs(result)) - else: - content_parts.append(result) + content_parts.append((await async_render(c, env, ctx))) + for spread_dict in sx_emitted('element-attrs'): + merge_spread_attrs(attrs, spread_dict) + provide_pop('element-attrs') if sx_truthy(token): svg_context_reset(token) return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '') @@ -3787,9 +3742,7 @@ async def async_render_component(comp, args, env, ctx): if sx_truthy(component_has_children(comp)): parts = [] for c in children: - r = (await async_render(c, env, ctx)) - if sx_truthy((not sx_truthy(is_spread(r)))): - parts.append(r) + parts.append((await async_render(c, env, ctx))) local['children'] = make_raw_html(join('', parts)) return (await async_render(component_body(comp), local, ctx)) @@ -3805,9 +3758,7 @@ async def async_render_island(island, args, env, ctx): if sx_truthy(component_has_children(island)): parts = [] for c in children: - r = (await async_render(c, env, ctx)) - if sx_truthy((not sx_truthy(is_spread(r)))): - parts.append(r) + parts.append((await async_render(c, env, ctx))) local['children'] = make_raw_html(join('', parts)) body_html = (await async_render(component_body(island), local, ctx)) state_json = serialize_island_state(kwargs) @@ -3871,8 +3822,7 @@ async def dispatch_async_render_form(name, expr, env, ctx): if sx_truthy((len(expr) == 3)): return (await async_render(nth(expr, 2), env, ctx)) else: - results = (await async_map_render(slice(expr, 2), env, ctx)) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)) + return join('', (await async_map_render(slice(expr, 2), env, ctx))) elif sx_truthy((name == 'cond')): clauses = rest(expr) if sx_truthy(cond_scheme_p(clauses)): @@ -3886,38 +3836,36 @@ async def dispatch_async_render_form(name, expr, env, ctx): if sx_truthy((len(expr) == 3)): return (await async_render(nth(expr, 2), local, ctx)) else: - results = (await async_map_render(slice(expr, 2), local, ctx)) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)) + return join('', (await async_map_render(slice(expr, 2), local, ctx))) elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))): if sx_truthy((len(expr) == 2)): return (await async_render(nth(expr, 1), env, ctx)) else: - results = (await async_map_render(rest(expr), env, ctx)) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)) + return join('', (await async_map_render(rest(expr), env, ctx))) elif sx_truthy(is_definition_form(name)): (await async_eval(expr, env, ctx)) return '' elif sx_truthy((name == 'map')): f = (await async_eval(nth(expr, 1), env, ctx)) coll = (await async_eval(nth(expr, 2), env, ctx)) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_fn_render(f, coll, env, ctx)))) + return join('', (await async_map_fn_render(f, coll, env, ctx))) elif sx_truthy((name == 'map-indexed')): f = (await async_eval(nth(expr, 1), env, ctx)) coll = (await async_eval(nth(expr, 2), env, ctx)) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_indexed_fn_render(f, coll, env, ctx)))) + return join('', (await async_map_indexed_fn_render(f, coll, env, ctx))) elif sx_truthy((name == 'filter')): return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) elif sx_truthy((name == 'for-each')): f = (await async_eval(nth(expr, 1), env, ctx)) coll = (await async_eval(nth(expr, 2), env, ctx)) - return join('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_fn_render(f, coll, env, ctx)))) + return join('', (await async_map_fn_render(f, coll, env, ctx))) elif sx_truthy((name == 'provide')): prov_name = (await async_eval(nth(expr, 1), env, ctx)) prov_val = (await async_eval(nth(expr, 2), env, ctx)) body_start = 3 body_count = (len(expr) - 3) provide_push(prov_name, prov_val) - result = ((await async_render(nth(expr, body_start), env, ctx)) if sx_truthy((body_count == 1)) else (lambda results: join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)))((await async_map_render(slice(expr, body_start), env, ctx)))) + result = ((await async_render(nth(expr, body_start), env, ctx)) if sx_truthy((body_count == 1)) else join('', (await async_map_render(slice(expr, body_start), env, ctx)))) provide_pop(prov_name) return result else: @@ -4019,42 +3967,35 @@ async def async_invoke(f, *args): # async-aser async def async_aser(expr, env, ctx): - _match = type_of(expr) - if _match == 'number': - return expr - elif _match == 'string': - return expr - elif _match == 'boolean': - return expr - elif _match == 'nil': - return NIL - elif _match == 'symbol': + t = type_of(expr) + result = NIL + if sx_truthy((t == 'number')): + result = expr + elif sx_truthy((t == 'string')): + result = expr + elif sx_truthy((t == 'boolean')): + result = expr + elif sx_truthy((t == 'nil')): + result = NIL + elif sx_truthy((t == 'symbol')): name = symbol_name(expr) - if sx_truthy(env_has(env, name)): - return env_get(env, name) - elif sx_truthy(is_primitive(name)): - return get_primitive(name) - elif sx_truthy((name == 'true')): - return True - elif sx_truthy((name == 'false')): - return False - elif sx_truthy((name == 'nil')): - return NIL - else: - return error(sx_str('Undefined symbol: ', name)) - elif _match == 'keyword': - return keyword_name(expr) - elif _match == 'dict': - return (await async_aser_dict(expr, env, ctx)) - elif _match == 'spread': - return expr - elif _match == 'list': - if sx_truthy(empty_p(expr)): - return [] - else: - return (await async_aser_list(expr, env, ctx)) + result = (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))) + elif sx_truthy((t == 'keyword')): + result = keyword_name(expr) + elif sx_truthy((t == 'dict')): + result = (await async_aser_dict(expr, env, ctx)) + elif sx_truthy((t == 'spread')): + sx_emit('element-attrs', spread_attrs(expr)) + result = NIL + elif sx_truthy((t == 'list')): + result = ([] if sx_truthy(empty_p(expr)) else (await async_aser_list(expr, env, ctx))) else: - return expr + result = expr + if sx_truthy(is_spread(result)): + sx_emit('element-attrs', spread_attrs(result)) + return NIL + else: + return result # async-aser-dict async def async_aser_dict(expr, env, ctx): @@ -4148,10 +4089,10 @@ async def async_aser_fragment(children, env, ctx): result = (await async_aser(c, env, ctx)) if sx_truthy((type_of(result) == 'list')): for item in result: - if sx_truthy(((not sx_truthy(is_nil(item))) if not sx_truthy((not sx_truthy(is_nil(item)))) else (not sx_truthy(is_spread(item))))): + if sx_truthy((not sx_truthy(is_nil(item)))): parts.append(serialize(item)) else: - if sx_truthy(((not sx_truthy(is_nil(result))) if not sx_truthy((not sx_truthy(is_nil(result)))) else (not sx_truthy(is_spread(result))))): + if sx_truthy((not sx_truthy(is_nil(result)))): parts.append(serialize(result)) if sx_truthy(empty_p(parts)): return make_sx_expr('') @@ -4204,9 +4145,11 @@ async def async_parse_aser_kw_args(args, kwargs, children, env, ctx): async def async_aser_call(name, args, env, ctx): _cells = {} token = (svg_context_set(True) if sx_truthy(((name == 'svg') if sx_truthy((name == 'svg')) else (name == 'math'))) else NIL) - parts = [name] + attr_parts = [] + child_parts = [] _cells['skip'] = False _cells['i'] = 0 + provide_push('element-attrs', NIL) for arg in args: if sx_truthy(_cells['skip']): _cells['skip'] = False @@ -4215,39 +4158,40 @@ async def async_aser_call(name, args, env, ctx): if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))): val = (await async_aser(nth(args, (_cells['i'] + 1)), env, ctx)) if sx_truthy((not sx_truthy(is_nil(val)))): - parts.append(sx_str(':', keyword_name(arg))) + attr_parts.append(sx_str(':', keyword_name(arg))) if sx_truthy((type_of(val) == 'list')): live = filter(lambda v: (not sx_truthy(is_nil(v))), val) if sx_truthy(empty_p(live)): - parts.append('nil') + attr_parts.append('nil') else: items = map(serialize, live) if sx_truthy(some(lambda v: is_sx_expr(v), live)): - parts.append(sx_str('(<> ', join(' ', items), ')')) + attr_parts.append(sx_str('(<> ', join(' ', items), ')')) else: - parts.append(sx_str('(list ', join(' ', items), ')')) + attr_parts.append(sx_str('(list ', join(' ', items), ')')) else: - parts.append(serialize(val)) + attr_parts.append(serialize(val)) _cells['skip'] = True _cells['i'] = (_cells['i'] + 1) else: result = (await async_aser(arg, env, ctx)) if sx_truthy((not sx_truthy(is_nil(result)))): - if sx_truthy(is_spread(result)): - for k in keys(spread_attrs(result)): - v = dict_get(spread_attrs(result), k) - parts.append(sx_str(':', k)) - parts.append(serialize(v)) + if sx_truthy((type_of(result) == 'list')): + for item in result: + if sx_truthy((not sx_truthy(is_nil(item)))): + child_parts.append(serialize(item)) else: - if sx_truthy((type_of(result) == 'list')): - for item in result: - if sx_truthy((not sx_truthy(is_nil(item)))): - parts.append(serialize(item)) - else: - parts.append(serialize(result)) + child_parts.append(serialize(result)) _cells['i'] = (_cells['i'] + 1) + for spread_dict in sx_emitted('element-attrs'): + for k in keys(spread_dict): + v = dict_get(spread_dict, k) + attr_parts.append(sx_str(':', k)) + attr_parts.append(serialize(v)) + provide_pop('element-attrs') if sx_truthy(token): svg_context_reset(token) + parts = concat([name], attr_parts, child_parts) return make_sx_expr(sx_str('(', join(' ', parts), ')')) # ASYNC_ASER_FORM_NAMES diff --git a/shared/sx/ref/test-aser.sx b/shared/sx/ref/test-aser.sx index 765b6e6..432a14e 100644 --- a/shared/sx/ref/test-aser.sx +++ b/shared/sx/ref/test-aser.sx @@ -285,6 +285,19 @@ (assert-equal "(div :class \"card\" :style \"color:red\" \"hello\")" (render-sx "(div (make-spread {:class \"card\"}) (make-spread {:style \"color:red\"}) \"hello\")"))) - (deftest "spread in fragment is filtered" + (deftest "spread in fragment is silently dropped" (assert-equal "(<> \"hello\")" - (render-sx "(<> (make-spread {:class \"card\"}) \"hello\")")))) + (render-sx "(<> (make-spread {:class \"card\"}) \"hello\")"))) + + (deftest "stored spread in let binding" + (assert-equal "(div :class \"card\" \"hello\")" + (render-sx "(let ((card (make-spread {:class \"card\"}))) + (div card \"hello\"))"))) + + (deftest "spread in nested element" + (assert-equal "(div (span :class \"inner\" \"hi\"))" + (render-sx "(div (span (make-spread {:class \"inner\"}) \"hi\"))"))) + + (deftest "spread in non-element context silently drops" + (assert-equal "hello" + (render-sx "(do (make-spread {:class \"card\"}) \"hello\")"))))