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 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 15:41:32 +00:00
parent 859aad4333
commit 2de4ba8c57
9 changed files with 444 additions and 526 deletions

View File

@@ -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("</") + String(tag) + 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("</") + String(tag) + 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("</") + String(lakeTag) + 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("</") + String(lakeTag) + 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("</") + String(marshTag) + 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("</") + String(marshTag) + 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;
})(); };

View File

@@ -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) ")"))))))
;; --------------------------------------------------------------------------

View File

@@ -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)))

View File

@@ -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)
"</" tag ">"))))))
;; 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
"</" tag ">")))))))
;; --------------------------------------------------------------------------
@@ -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)
"</" lake-tag ">")))))
;; 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
"</" lake-tag ">"))))))
;; --------------------------------------------------------------------------
@@ -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)
"</" marsh-tag ">")))))
;; 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
"</" marsh-tag ">"))))))
;; --------------------------------------------------------------------------
@@ -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))

View File

@@ -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) ")")))))
;; --------------------------------------------------------------------------

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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), '</', tag, '>')
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, '</', tag, '>')
# 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), '</', _cells['lake_tag'], '>')
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, '</', _cells['lake_tag'], '>')
# 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), '</', _cells['marsh_tag'], '>')
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, '</', _cells['marsh_tag'], '>')
# 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('<span data-sx-island="', escape_attr(island_name), '"', (sx_str(' data-sx-state="', escape_attr(state_sx), '"') if sx_truthy(state_sx) else ''), '>', body_html, '</span>')
@@ -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), '</', tag, '>')
@@ -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

View File

@@ -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\")"))))