Compare commits
7 Commits
ee969a343c
...
11fdd1a840
| Author | SHA1 | Date | |
|---|---|---|---|
| 11fdd1a840 | |||
| 6ca46bb295 | |||
| e1a5e3eb89 | |||
| aef990735f | |||
| 04d3b2ecaf | |||
| c4a999d0d0 | |||
| 2de4ba8c57 |
@@ -7,6 +7,7 @@ on:
|
||||
env:
|
||||
REGISTRY: registry.rose-ash.com:5000
|
||||
APP_DIR: /root/rose-ash
|
||||
BUILD_DIR: /root/rose-ash-ci
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
@@ -33,23 +34,26 @@ jobs:
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
run: |
|
||||
ssh "root@$DEPLOY_HOST" "
|
||||
cd ${{ env.APP_DIR }}
|
||||
|
||||
# Save current HEAD before updating
|
||||
OLD_HEAD=\$(git rev-parse HEAD 2>/dev/null || echo none)
|
||||
|
||||
git fetch origin ${{ github.ref_name }}
|
||||
# --- Build in isolated CI directory (never touch dev working tree) ---
|
||||
BUILD=${{ env.BUILD_DIR }}
|
||||
ORIGIN=\$(git -C ${{ env.APP_DIR }} remote get-url origin)
|
||||
if [ ! -d \"\$BUILD/.git\" ]; then
|
||||
git clone \"\$ORIGIN\" \"\$BUILD\"
|
||||
fi
|
||||
cd \"\$BUILD\"
|
||||
git fetch origin
|
||||
git reset --hard origin/${{ github.ref_name }}
|
||||
|
||||
NEW_HEAD=\$(git rev-parse HEAD)
|
||||
# Detect changes using push event SHAs (not local checkout state)
|
||||
BEFORE='${{ github.event.before }}'
|
||||
AFTER='${{ github.sha }}'
|
||||
|
||||
# Detect what changed
|
||||
REBUILD_ALL=false
|
||||
if [ \"\$OLD_HEAD\" = \"none\" ] || [ \"\$OLD_HEAD\" = \"\$NEW_HEAD\" ]; then
|
||||
# First deploy or CI re-run on same commit — rebuild all
|
||||
if [ -z \"\$BEFORE\" ] || [ \"\$BEFORE\" = '0000000000000000000000000000000000000000' ] || ! git cat-file -e \"\$BEFORE\" 2>/dev/null; then
|
||||
# New branch, force push, or unreachable parent — rebuild all
|
||||
REBUILD_ALL=true
|
||||
else
|
||||
CHANGED=\$(git diff --name-only \$OLD_HEAD \$NEW_HEAD)
|
||||
CHANGED=\$(git diff --name-only \$BEFORE \$AFTER)
|
||||
if echo \"\$CHANGED\" | grep -q '^shared/'; then
|
||||
REBUILD_ALL=true
|
||||
fi
|
||||
@@ -86,8 +90,8 @@ jobs:
|
||||
|
||||
# Deploy swarm stacks only on main branch
|
||||
if [ '${{ github.ref_name }}' = 'main' ]; then
|
||||
source .env
|
||||
docker stack deploy -c docker-compose.yml rose-ash
|
||||
source ${{ env.APP_DIR }}/.env
|
||||
docker stack deploy --resolve-image always -c docker-compose.yml rose-ash
|
||||
echo 'Waiting for swarm services to update...'
|
||||
sleep 10
|
||||
docker stack services rose-ash
|
||||
@@ -99,17 +103,17 @@ jobs:
|
||||
fi
|
||||
if [ \"\$SX_REBUILT\" = true ]; then
|
||||
echo 'Deploying sx-web stack (sx-web.org)...'
|
||||
docker stack deploy -c /root/sx-web/docker-compose.yml sx-web
|
||||
docker stack deploy --resolve-image always -c /root/sx-web/docker-compose.yml sx-web
|
||||
sleep 5
|
||||
docker stack services sx-web
|
||||
# Reload Caddy to pick up any Caddyfile changes
|
||||
docker service update --force caddy_caddy 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
echo 'Skipping swarm deploy (branch: ${{ github.ref_name }})'
|
||||
fi
|
||||
|
||||
# Dev stack always deployed (bind-mounted source + auto-reload)
|
||||
# Dev stack uses working tree (bind-mounted source + auto-reload)
|
||||
cd ${{ env.APP_DIR }}
|
||||
echo 'Deploying dev stack...'
|
||||
docker compose -p rose-ash-dev -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||
echo 'Dev stack deployed'
|
||||
|
||||
@@ -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-13T16:48:03Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -86,8 +86,7 @@
|
||||
function SxSpread(attrs) { this.attrs = attrs || {}; }
|
||||
SxSpread.prototype._spread = true;
|
||||
|
||||
var _collectBuckets = {};
|
||||
var _provideStacks = {};
|
||||
var _scopeStacks = {};
|
||||
|
||||
function isSym(x) { return x != null && x._sym === true; }
|
||||
function isKw(x) { return x != null && x._kw === true; }
|
||||
@@ -151,46 +150,54 @@
|
||||
function isSpread(x) { return x != null && x._spread === true; }
|
||||
function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; }
|
||||
|
||||
function sxCollect(bucket, value) {
|
||||
if (!_collectBuckets[bucket]) _collectBuckets[bucket] = [];
|
||||
var items = _collectBuckets[bucket];
|
||||
if (items.indexOf(value) === -1) items.push(value);
|
||||
function scopePush(name, value) {
|
||||
if (!_scopeStacks[name]) _scopeStacks[name] = [];
|
||||
_scopeStacks[name].push({value: value !== undefined ? value : NIL, emitted: [], dedup: false});
|
||||
}
|
||||
function sxCollected(bucket) {
|
||||
return _collectBuckets[bucket] ? _collectBuckets[bucket].slice() : [];
|
||||
}
|
||||
function sxClearCollected(bucket) {
|
||||
if (_collectBuckets[bucket]) _collectBuckets[bucket] = [];
|
||||
function scopePop(name) {
|
||||
if (_scopeStacks[name] && _scopeStacks[name].length) _scopeStacks[name].pop();
|
||||
}
|
||||
// Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
|
||||
var providePush = scopePush;
|
||||
var providePop = scopePop;
|
||||
|
||||
function providePush(name, value) {
|
||||
if (!_provideStacks[name]) _provideStacks[name] = [];
|
||||
_provideStacks[name].push({value: value !== undefined ? value : NIL, emitted: []});
|
||||
}
|
||||
function providePop(name) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) _provideStacks[name].pop();
|
||||
}
|
||||
function sxContext(name) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) {
|
||||
return _provideStacks[name][_provideStacks[name].length - 1].value;
|
||||
if (_scopeStacks[name] && _scopeStacks[name].length) {
|
||||
return _scopeStacks[name][_scopeStacks[name].length - 1].value;
|
||||
}
|
||||
if (arguments.length > 1) return arguments[1];
|
||||
throw new Error("No provider for: " + name);
|
||||
}
|
||||
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);
|
||||
if (_scopeStacks[name] && _scopeStacks[name].length) {
|
||||
var entry = _scopeStacks[name][_scopeStacks[name].length - 1];
|
||||
if (entry.dedup && entry.emitted.indexOf(value) !== -1) return NIL;
|
||||
entry.emitted.push(value);
|
||||
}
|
||||
return NIL;
|
||||
}
|
||||
function sxEmitted(name) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) {
|
||||
return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice();
|
||||
if (_scopeStacks[name] && _scopeStacks[name].length) {
|
||||
return _scopeStacks[name][_scopeStacks[name].length - 1].emitted.slice();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function sxCollect(bucket, value) {
|
||||
if (!_scopeStacks[bucket] || !_scopeStacks[bucket].length) {
|
||||
if (!_scopeStacks[bucket]) _scopeStacks[bucket] = [];
|
||||
_scopeStacks[bucket].push({value: NIL, emitted: [], dedup: true});
|
||||
}
|
||||
var entry = _scopeStacks[bucket][_scopeStacks[bucket].length - 1];
|
||||
if (entry.emitted.indexOf(value) === -1) entry.emitted.push(value);
|
||||
}
|
||||
function sxCollected(bucket) {
|
||||
return sxEmitted(bucket);
|
||||
}
|
||||
function sxClearCollected(bucket) {
|
||||
if (_scopeStacks[bucket] && _scopeStacks[bucket].length) {
|
||||
_scopeStacks[bucket][_scopeStacks[bucket].length - 1].emitted = [];
|
||||
}
|
||||
}
|
||||
|
||||
function lambdaParams(f) { return f.params; }
|
||||
function lambdaBody(f) { return f.body; }
|
||||
@@ -519,14 +526,17 @@
|
||||
};
|
||||
|
||||
|
||||
// stdlib.spread — spread + collect primitives
|
||||
// stdlib.spread — spread + collect + scope primitives
|
||||
PRIMITIVES["make-spread"] = makeSpread;
|
||||
PRIMITIVES["spread?"] = isSpread;
|
||||
PRIMITIVES["spread-attrs"] = spreadAttrs;
|
||||
PRIMITIVES["collect!"] = sxCollect;
|
||||
PRIMITIVES["collected"] = sxCollected;
|
||||
PRIMITIVES["clear-collected!"] = sxClearCollected;
|
||||
// provide/context/emit! — render-time dynamic scope
|
||||
// scope — unified render-time dynamic scope
|
||||
PRIMITIVES["scope-push!"] = scopePush;
|
||||
PRIMITIVES["scope-pop!"] = scopePop;
|
||||
// provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
|
||||
PRIMITIVES["provide-push!"] = providePush;
|
||||
PRIMITIVES["provide-pop!"] = providePop;
|
||||
PRIMITIVES["context"] = sxContext;
|
||||
@@ -798,10 +808,10 @@
|
||||
var args = rest(expr);
|
||||
return (isSxTruthy(!isSxTruthy(sxOr((typeOf(head) == "symbol"), (typeOf(head) == "lambda"), (typeOf(head) == "list")))) ? map(function(x) { return trampoline(evalExpr(x, env)); }, expr) : (isSxTruthy((typeOf(head) == "symbol")) ? (function() {
|
||||
var name = symbolName(head);
|
||||
return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "letrec")) ? sfLetrec(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defisland")) ? sfDefisland(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefhandler(args, env) : (isSxTruthy((name == "defpage")) ? sfDefpage(args, env) : (isSxTruthy((name == "defquery")) ? sfDefquery(args, env) : (isSxTruthy((name == "defaction")) ? sfDefaction(args, env) : (isSxTruthy((name == "deftype")) ? sfDeftype(args, env) : (isSxTruthy((name == "defeffect")) ? sfDefeffect(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "reset")) ? sfReset(args, env) : (isSxTruthy((name == "shift")) ? sfShift(args, env) : (isSxTruthy((name == "dynamic-wind")) ? sfDynamicWind(args, env) : (isSxTruthy((name == "provide")) ? sfProvide(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
|
||||
return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "letrec")) ? sfLetrec(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defisland")) ? sfDefisland(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefhandler(args, env) : (isSxTruthy((name == "defpage")) ? sfDefpage(args, env) : (isSxTruthy((name == "defquery")) ? sfDefquery(args, env) : (isSxTruthy((name == "defaction")) ? sfDefaction(args, env) : (isSxTruthy((name == "deftype")) ? sfDeftype(args, env) : (isSxTruthy((name == "defeffect")) ? sfDefeffect(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "reset")) ? sfReset(args, env) : (isSxTruthy((name == "shift")) ? sfShift(args, env) : (isSxTruthy((name == "dynamic-wind")) ? sfDynamicWind(args, env) : (isSxTruthy((name == "scope")) ? sfScope(args, env) : (isSxTruthy((name == "provide")) ? sfProvide(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
|
||||
var mac = envGet(env, name);
|
||||
return makeThunk(expandMacro(mac, args, env), env);
|
||||
})() : (isSxTruthy((isSxTruthy(renderActiveP()) && isRenderExpr(expr))) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))))))))))))));
|
||||
})() : (isSxTruthy((isSxTruthy(renderActiveP()) && isRenderExpr(expr))) ? renderExpr(expr, env) : evalCall(head, args, env)))))))))))))))))))))))))))))))))))))))))));
|
||||
})() : evalCall(head, args, env)));
|
||||
})(); };
|
||||
|
||||
@@ -1206,6 +1216,22 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
callThunk(after, env);
|
||||
return result;
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// sf-scope
|
||||
var sfScope = function(args, env) { return (function() {
|
||||
var name = trampoline(evalExpr(first(args), env));
|
||||
var rest = slice(args, 1);
|
||||
var val = NIL;
|
||||
var bodyExprs = NIL;
|
||||
(isSxTruthy((isSxTruthy((len(rest) >= 2)) && isSxTruthy((typeOf(first(rest)) == "keyword")) && (keywordName(first(rest)) == "value"))) ? ((val = trampoline(evalExpr(nth(rest, 1), env))), (bodyExprs = slice(rest, 2))) : (bodyExprs = rest));
|
||||
scopePush(name, val);
|
||||
return (function() {
|
||||
var result = NIL;
|
||||
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; result = trampoline(evalExpr(e, env)); } }
|
||||
scopePop(name);
|
||||
return result;
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// sf-provide
|
||||
@@ -1214,9 +1240,9 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var val = trampoline(evalExpr(nth(args, 1), env));
|
||||
var bodyExprs = slice(args, 2);
|
||||
var result = NIL;
|
||||
providePush(name, val);
|
||||
scopePush(name, val);
|
||||
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; result = trampoline(evalExpr(e, env)); } }
|
||||
providePop(name);
|
||||
scopePop(name);
|
||||
return result;
|
||||
})(); };
|
||||
|
||||
@@ -1523,13 +1549,13 @@ 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"];
|
||||
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", "scope", "provide"];
|
||||
|
||||
// render-html-form?
|
||||
var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); };
|
||||
@@ -1537,10 +1563,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,45 +1577,48 @@ 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 == "scope")) ? (function() {
|
||||
var scopeName = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var restArgs = slice(expr, 2);
|
||||
var scopeVal = NIL;
|
||||
var bodyExprs = NIL;
|
||||
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((scopeVal = trampoline(evalExpr(nth(restArgs, 1), env))), (bodyExprs = slice(restArgs, 2))) : (bodyExprs = restArgs));
|
||||
scopePush(scopeName, scopeVal);
|
||||
return (function() {
|
||||
var result = (isSxTruthy((len(bodyExprs) == 1)) ? renderToHtml(first(bodyExprs), env) : join("", map(function(e) { return renderToHtml(e, env); }, bodyExprs)));
|
||||
scopePop(scopeName);
|
||||
return result;
|
||||
})();
|
||||
})() : (isSxTruthy((name == "provide")) ? (function() {
|
||||
var provName = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var provVal = trampoline(evalExpr(nth(expr, 2), env));
|
||||
var bodyStart = 3;
|
||||
var bodyCount = (len(expr) - 3);
|
||||
providePush(provName, provVal);
|
||||
scopePush(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))))));
|
||||
providePop(provName);
|
||||
var result = (isSxTruthy((bodyCount == 1)) ? renderToHtml(nth(expr, bodyStart), env) : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(bodyStart, (bodyStart + bodyCount)))));
|
||||
scopePop(provName);
|
||||
return result;
|
||||
})();
|
||||
})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))))))))); };
|
||||
})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))))); };
|
||||
|
||||
// render-lambda-html
|
||||
var renderLambdaHtml = function(f, args, env) { return (function() {
|
||||
@@ -1614,14 +1643,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 +1655,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(" />")) : (scopePush("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); } }
|
||||
scopePop("element-attrs");
|
||||
return (String("<") + String(tag) + String(renderAttrs(attrs)) + String(">") + String(content) + String("</") + String(tag) + String(">"));
|
||||
})()));
|
||||
})(); };
|
||||
|
||||
// render-html-lake
|
||||
@@ -1659,12 +1679,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(">"));
|
||||
scopePush("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); } }
|
||||
scopePop("element-attrs");
|
||||
return (String("<") + String(lakeTag) + String(renderAttrs(lakeAttrs)) + String(">") + String(content) + String("</") + String(lakeTag) + String(">"));
|
||||
})();
|
||||
})();
|
||||
})(); };
|
||||
|
||||
@@ -1684,12 +1705,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(">"));
|
||||
scopePush("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); } }
|
||||
scopePop("element-attrs");
|
||||
return (String("<") + String(marshTag) + String(renderAttrs(marshAttrs)) + String(">") + String(content) + String("</") + String(marshTag) + String(">"));
|
||||
})();
|
||||
})();
|
||||
})(); };
|
||||
|
||||
@@ -1710,14 +1732,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 +1756,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,33 +1790,40 @@ 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;
|
||||
scopePush("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));
|
||||
})(); } } } }
|
||||
scopePop("element-attrs");
|
||||
return (function() {
|
||||
var parts = concat([name], attrParts, childParts);
|
||||
return (String("(") + String(join(" ", parts)) + String(")"));
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// SPECIAL_FORM_NAMES
|
||||
var SPECIAL_FORM_NAMES = ["if", "when", "cond", "case", "and", "or", "let", "let*", "lambda", "fn", "define", "defcomp", "defmacro", "defstyle", "defhandler", "defpage", "defquery", "defaction", "defrelation", "begin", "do", "quote", "quasiquote", "->", "set!", "letrec", "dynamic-wind", "defisland", "deftype", "defeffect", "provide"];
|
||||
var SPECIAL_FORM_NAMES = ["if", "when", "cond", "case", "and", "or", "let", "let*", "lambda", "fn", "define", "defcomp", "defmacro", "defstyle", "defhandler", "defpage", "defquery", "defaction", "defrelation", "begin", "do", "quote", "quasiquote", "->", "set!", "letrec", "dynamic-wind", "defisland", "deftype", "defeffect", "scope", "provide"];
|
||||
|
||||
// HO_FORM_NAMES
|
||||
var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"];
|
||||
@@ -1869,15 +1894,28 @@ return result; }, args);
|
||||
return append_b(results, aser(lambdaBody(f), local));
|
||||
})() : invoke(f, item)); } }
|
||||
return (isSxTruthy(isEmpty(results)) ? NIL : results);
|
||||
})() : (isSxTruthy((name == "defisland")) ? (trampoline(evalExpr(expr, env)), serialize(expr)) : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler"), (name == "defpage"), (name == "defquery"), (name == "defaction"), (name == "defrelation"), (name == "deftype"), (name == "defeffect"))) ? (trampoline(evalExpr(expr, env)), NIL) : (isSxTruthy((name == "provide")) ? (function() {
|
||||
})() : (isSxTruthy((name == "defisland")) ? (trampoline(evalExpr(expr, env)), serialize(expr)) : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler"), (name == "defpage"), (name == "defquery"), (name == "defaction"), (name == "defrelation"), (name == "deftype"), (name == "defeffect"))) ? (trampoline(evalExpr(expr, env)), NIL) : (isSxTruthy((name == "scope")) ? (function() {
|
||||
var scopeName = trampoline(evalExpr(first(args), env));
|
||||
var restArgs = rest(args);
|
||||
var scopeVal = NIL;
|
||||
var bodyArgs = NIL;
|
||||
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((scopeVal = trampoline(evalExpr(nth(restArgs, 1), env))), (bodyArgs = slice(restArgs, 2))) : (bodyArgs = restArgs));
|
||||
scopePush(scopeName, scopeVal);
|
||||
return (function() {
|
||||
var result = NIL;
|
||||
{ var _c = bodyArgs; for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } }
|
||||
scopePop(scopeName);
|
||||
return result;
|
||||
})();
|
||||
})() : (isSxTruthy((name == "provide")) ? (function() {
|
||||
var provName = trampoline(evalExpr(first(args), env));
|
||||
var provVal = trampoline(evalExpr(nth(args, 1), env));
|
||||
var result = NIL;
|
||||
providePush(provName, provVal);
|
||||
scopePush(provName, provVal);
|
||||
{ var _c = slice(args, 2); for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } }
|
||||
providePop(provName);
|
||||
scopePop(provName);
|
||||
return result;
|
||||
})() : trampoline(evalExpr(expr, env))))))))))))))));
|
||||
})() : trampoline(evalExpr(expr, env)))))))))))))))));
|
||||
})(); };
|
||||
|
||||
// eval-case-aser
|
||||
@@ -1898,7 +1936,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 +1965,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);
|
||||
scopePush("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 +1990,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 +2002,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);
|
||||
})(); } } } }
|
||||
scopePop("element-attrs");
|
||||
return el;
|
||||
})(); };
|
||||
|
||||
@@ -2019,7 +2060,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
|
||||
|
||||
// RENDER_DOM_FORMS
|
||||
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary", "provide"];
|
||||
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary", "scope", "provide"];
|
||||
|
||||
// render-dom-form?
|
||||
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
|
||||
@@ -2161,15 +2202,26 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
return domAppend(frag, val);
|
||||
})(); } }
|
||||
return frag;
|
||||
})() : (isSxTruthy((name == "scope")) ? (function() {
|
||||
var scopeName = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var restArgs = slice(expr, 2);
|
||||
var scopeVal = NIL;
|
||||
var bodyExprs = NIL;
|
||||
var frag = createFragment();
|
||||
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((scopeVal = trampoline(evalExpr(nth(restArgs, 1), env))), (bodyExprs = slice(restArgs, 2))) : (bodyExprs = restArgs));
|
||||
scopePush(scopeName, scopeVal);
|
||||
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; domAppend(frag, renderToDom(e, env, ns)); } }
|
||||
scopePop(scopeName);
|
||||
return frag;
|
||||
})() : (isSxTruthy((name == "provide")) ? (function() {
|
||||
var provName = trampoline(evalExpr(nth(expr, 1), env));
|
||||
var provVal = trampoline(evalExpr(nth(expr, 2), env));
|
||||
var frag = createFragment();
|
||||
providePush(provName, provVal);
|
||||
scopePush(provName, provVal);
|
||||
{ var _c = range(3, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } }
|
||||
providePop(provName);
|
||||
scopePop(provName);
|
||||
return frag;
|
||||
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); };
|
||||
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))))); };
|
||||
|
||||
// render-lambda-dom
|
||||
var renderLambdaDom = function(f, args, env, ns) { return (function() {
|
||||
@@ -6634,6 +6686,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
collect: sxCollect,
|
||||
collected: sxCollected,
|
||||
clearCollected: sxClearCollected,
|
||||
scopePush: scopePush,
|
||||
scopePop: scopePop,
|
||||
providePush: providePush,
|
||||
providePop: providePop,
|
||||
context: sxContext,
|
||||
|
||||
@@ -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)))
|
||||
(scope-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"))
|
||||
(scope-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)))))
|
||||
@@ -341,7 +335,7 @@
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"deftype" "defeffect"
|
||||
"map" "map-indexed" "filter" "for-each" "provide"))
|
||||
"map" "map-indexed" "filter" "for-each" "scope" "provide"))
|
||||
|
||||
(define async-render-form? :effects []
|
||||
(fn ((name :as string))
|
||||
@@ -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,60 +385,71 @@
|
||||
(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
|
||||
;; scope — unified render-time dynamic scope
|
||||
(= name "scope")
|
||||
(let ((scope-name (async-eval (nth expr 1) env ctx))
|
||||
(rest-args (slice expr 2))
|
||||
(scope-val nil)
|
||||
(body-exprs nil))
|
||||
;; Check for :value keyword
|
||||
(if (and (>= (len rest-args) 2)
|
||||
(= (type-of (first rest-args)) "keyword")
|
||||
(= (keyword-name (first rest-args)) "value"))
|
||||
(do (set! scope-val (async-eval (nth rest-args 1) env ctx))
|
||||
(set! body-exprs (slice rest-args 2)))
|
||||
(set! body-exprs rest-args))
|
||||
(scope-push! scope-name scope-val)
|
||||
(let ((result (if (= (len body-exprs) 1)
|
||||
(async-render (first body-exprs) env ctx)
|
||||
(join "" (async-map-render body-exprs env ctx)))))
|
||||
(scope-pop! scope-name)
|
||||
result))
|
||||
|
||||
;; provide — sugar for scope with value
|
||||
(= name "provide")
|
||||
(let ((prov-name (async-eval (nth expr 1) env ctx))
|
||||
(prov-val (async-eval (nth expr 2) env ctx))
|
||||
(body-start 3)
|
||||
(body-count (- (len expr) 3)))
|
||||
(provide-push! prov-name prov-val)
|
||||
(scope-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))))))
|
||||
(provide-pop! prov-name)
|
||||
(join "" (async-map-render (slice expr body-start) env ctx)))))
|
||||
(scope-pop! prov-name)
|
||||
result))
|
||||
|
||||
;; Fallback
|
||||
@@ -595,35 +599,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 +778,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 +785,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 +862,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!
|
||||
(scope-push! "element-attrs" nil)
|
||||
(for-each
|
||||
(fn (arg)
|
||||
(if skip
|
||||
@@ -872,39 +877,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"))
|
||||
(scope-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) ")"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -917,7 +926,7 @@
|
||||
"define" "defcomp" "defmacro" "defstyle"
|
||||
"defhandler" "defpage" "defquery" "defaction"
|
||||
"begin" "do" "quote" "->" "set!" "defisland"
|
||||
"deftype" "defeffect" "provide"))
|
||||
"deftype" "defeffect" "scope" "provide"))
|
||||
|
||||
(define ASYNC_ASER_HO_NAMES
|
||||
(list "map" "map-indexed" "filter" "for-each"))
|
||||
@@ -1055,15 +1064,35 @@
|
||||
(= name "deftype") (= name "defeffect"))
|
||||
(do (async-eval expr env ctx) nil)
|
||||
|
||||
;; provide — render-time dynamic scope
|
||||
;; scope — unified render-time dynamic scope
|
||||
(= name "scope")
|
||||
(let ((scope-name (async-eval (first args) env ctx))
|
||||
(rest-args (rest args))
|
||||
(scope-val nil)
|
||||
(body-args nil))
|
||||
;; Check for :value keyword
|
||||
(if (and (>= (len rest-args) 2)
|
||||
(= (type-of (first rest-args)) "keyword")
|
||||
(= (keyword-name (first rest-args)) "value"))
|
||||
(do (set! scope-val (async-eval (nth rest-args 1) env ctx))
|
||||
(set! body-args (slice rest-args 2)))
|
||||
(set! body-args rest-args))
|
||||
(scope-push! scope-name scope-val)
|
||||
(let ((result nil))
|
||||
(for-each (fn (body) (set! result (async-aser body env ctx)))
|
||||
body-args)
|
||||
(scope-pop! scope-name)
|
||||
result))
|
||||
|
||||
;; provide — sugar for scope with value
|
||||
(= name "provide")
|
||||
(let ((prov-name (async-eval (first args) env ctx))
|
||||
(prov-val (async-eval (nth args 1) env ctx))
|
||||
(result nil))
|
||||
(provide-push! prov-name prov-val)
|
||||
(scope-push! prov-name prov-val)
|
||||
(for-each (fn (body) (set! result (async-aser body env ctx)))
|
||||
(slice args 2))
|
||||
(provide-pop! prov-name)
|
||||
(scope-pop! prov-name)
|
||||
result)
|
||||
|
||||
;; Fallback
|
||||
|
||||
@@ -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
|
||||
(scope-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"))
|
||||
(scope-pop! "element-attrs")
|
||||
|
||||
el)))
|
||||
|
||||
|
||||
@@ -375,7 +381,7 @@
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each" "portal"
|
||||
"error-boundary" "provide"))
|
||||
"error-boundary" "scope" "provide"))
|
||||
|
||||
(define render-dom-form? :effects []
|
||||
(fn ((name :as string))
|
||||
@@ -631,17 +637,39 @@
|
||||
coll)
|
||||
frag)
|
||||
|
||||
;; provide — render-time dynamic scope
|
||||
;; scope — unified render-time dynamic scope
|
||||
(= name "scope")
|
||||
(let ((scope-name (trampoline (eval-expr (nth expr 1) env)))
|
||||
(rest-args (slice expr 2))
|
||||
(scope-val nil)
|
||||
(body-exprs nil)
|
||||
(frag (create-fragment)))
|
||||
;; Check for :value keyword
|
||||
(if (and (>= (len rest-args) 2)
|
||||
(= (type-of (first rest-args)) "keyword")
|
||||
(= (keyword-name (first rest-args)) "value"))
|
||||
(do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))
|
||||
(set! body-exprs (slice rest-args 2)))
|
||||
(set! body-exprs rest-args))
|
||||
(scope-push! scope-name scope-val)
|
||||
(for-each
|
||||
(fn (e)
|
||||
(dom-append frag (render-to-dom e env ns)))
|
||||
body-exprs)
|
||||
(scope-pop! scope-name)
|
||||
frag)
|
||||
|
||||
;; provide — sugar for scope with value
|
||||
(= name "provide")
|
||||
(let ((prov-name (trampoline (eval-expr (nth expr 1) env)))
|
||||
(prov-val (trampoline (eval-expr (nth expr 2) env)))
|
||||
(frag (create-fragment)))
|
||||
(provide-push! prov-name prov-val)
|
||||
(scope-push! prov-name prov-val)
|
||||
(for-each
|
||||
(fn (i)
|
||||
(dom-append frag (render-to-dom (nth expr i) env ns)))
|
||||
(range 3 (len expr)))
|
||||
(provide-pop! prov-name)
|
||||
(scope-pop! prov-name)
|
||||
frag)
|
||||
|
||||
;; Fallback
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"deftype" "defeffect"
|
||||
"map" "map-indexed" "filter" "for-each" "provide"))
|
||||
"map" "map-indexed" "filter" "for-each" "scope" "provide"))
|
||||
|
||||
(define render-html-form? :effects []
|
||||
(fn ((name :as string))
|
||||
@@ -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,78 +175,92 @@
|
||||
(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
|
||||
;; scope — unified render-time dynamic scope
|
||||
(= name "scope")
|
||||
(let ((scope-name (trampoline (eval-expr (nth expr 1) env)))
|
||||
(rest-args (slice expr 2))
|
||||
(scope-val nil)
|
||||
(body-exprs nil))
|
||||
;; Check for :value keyword
|
||||
(if (and (>= (len rest-args) 2)
|
||||
(= (type-of (first rest-args)) "keyword")
|
||||
(= (keyword-name (first rest-args)) "value"))
|
||||
(do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))
|
||||
(set! body-exprs (slice rest-args 2)))
|
||||
(set! body-exprs rest-args))
|
||||
(scope-push! scope-name scope-val)
|
||||
(let ((result (if (= (len body-exprs) 1)
|
||||
(render-to-html (first body-exprs) env)
|
||||
(join "" (map (fn (e) (render-to-html e env)) body-exprs)))))
|
||||
(scope-pop! scope-name)
|
||||
result))
|
||||
|
||||
;; provide — sugar for scope with value
|
||||
(= name "provide")
|
||||
(let ((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)
|
||||
(scope-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))))))))
|
||||
(provide-pop! prov-name)
|
||||
(join "" (map (fn (i) (render-to-html (nth expr i) env))
|
||||
(range body-start (+ body-start body-count)))))))
|
||||
(scope-pop! prov-name)
|
||||
result))
|
||||
|
||||
;; Fallback
|
||||
@@ -307,17 +318,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 +332,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
|
||||
(scope-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"))
|
||||
(scope-pop! "element-attrs")
|
||||
(str "<" tag (render-attrs attrs) ">"
|
||||
content
|
||||
"</" tag ">")))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -375,19 +377,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 ""))))
|
||||
(scope-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"))
|
||||
(scope-pop! "element-attrs")
|
||||
(str "<" lake-tag (render-attrs lake-attrs) ">"
|
||||
content
|
||||
"</" lake-tag ">"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -425,19 +425,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 ""))))
|
||||
(scope-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"))
|
||||
(scope-pop! "element-attrs")
|
||||
(str "<" marsh-tag (render-attrs marsh-attrs) ">"
|
||||
content
|
||||
"</" marsh-tag ">"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -487,17 +485,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))
|
||||
|
||||
@@ -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!
|
||||
(scope-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"))
|
||||
(scope-pop! "element-attrs")
|
||||
(let ((parts (concat (list name) attr-parts child-parts)))
|
||||
(str "(" (join " " parts) ")")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -183,7 +195,7 @@
|
||||
"defhandler" "defpage" "defquery" "defaction" "defrelation"
|
||||
"begin" "do" "quote" "quasiquote"
|
||||
"->" "set!" "letrec" "dynamic-wind" "defisland"
|
||||
"deftype" "defeffect" "provide"))
|
||||
"deftype" "defeffect" "scope" "provide"))
|
||||
|
||||
(define HO_FORM_NAMES
|
||||
(list "map" "map-indexed" "filter" "reduce"
|
||||
@@ -321,15 +333,35 @@
|
||||
(= name "deftype") (= name "defeffect"))
|
||||
(do (trampoline (eval-expr expr env)) nil)
|
||||
|
||||
;; provide — render-time dynamic scope
|
||||
;; scope — unified render-time dynamic scope
|
||||
(= name "scope")
|
||||
(let ((scope-name (trampoline (eval-expr (first args) env)))
|
||||
(rest-args (rest args))
|
||||
(scope-val nil)
|
||||
(body-args nil))
|
||||
;; Check for :value keyword
|
||||
(if (and (>= (len rest-args) 2)
|
||||
(= (type-of (first rest-args)) "keyword")
|
||||
(= (keyword-name (first rest-args)) "value"))
|
||||
(do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))
|
||||
(set! body-args (slice rest-args 2)))
|
||||
(set! body-args rest-args))
|
||||
(scope-push! scope-name scope-val)
|
||||
(let ((result nil))
|
||||
(for-each (fn (body) (set! result (aser body env)))
|
||||
body-args)
|
||||
(scope-pop! scope-name)
|
||||
result))
|
||||
|
||||
;; provide — sugar for scope with value
|
||||
(= name "provide")
|
||||
(let ((prov-name (trampoline (eval-expr (first args) env)))
|
||||
(prov-val (trampoline (eval-expr (nth args 1) env)))
|
||||
(result nil))
|
||||
(provide-push! prov-name prov-val)
|
||||
(scope-push! prov-name prov-val)
|
||||
(for-each (fn (body) (set! result (aser body env)))
|
||||
(slice args 2))
|
||||
(provide-pop! prov-name)
|
||||
(scope-pop! prov-name)
|
||||
result)
|
||||
|
||||
;; Everything else — evaluate normally
|
||||
|
||||
@@ -293,6 +293,8 @@ class PyEmitter:
|
||||
"collect!": "sx_collect",
|
||||
"collected": "sx_collected",
|
||||
"clear-collected!": "sx_clear_collected",
|
||||
"scope-push!": "scope_push",
|
||||
"scope-pop!": "scope_pop",
|
||||
"provide-push!": "provide_push",
|
||||
"provide-pop!": "provide_pop",
|
||||
"context": "sx_context",
|
||||
|
||||
@@ -374,30 +374,44 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Tier 5: Dynamic scope — render-time provide/context/emit!
|
||||
;; Tier 5: Scoped effects — unified render-time dynamic scope
|
||||
;;
|
||||
;; `provide` is a special form (not a primitive) that creates a named scope
|
||||
;; with a value and an empty accumulator. `context` reads the value from the
|
||||
;; nearest enclosing provider. `emit!` appends to the accumulator, `emitted`
|
||||
;; reads the accumulated values.
|
||||
;; `scope` is the general primitive. `provide` is sugar for scope-with-value.
|
||||
;; Both `provide` and `scope` are special forms in the evaluator.
|
||||
;;
|
||||
;; The platform must implement per-name stacks. Each entry has a value and
|
||||
;; an emitted list. `provide-push!`/`provide-pop!` manage the stack.
|
||||
;; The platform must implement per-name stacks. Each entry has a value,
|
||||
;; an emitted list, and a dedup flag. `scope-push!`/`scope-pop!` manage
|
||||
;; the stack. `provide-push!`/`provide-pop!` are aliases.
|
||||
;;
|
||||
;; `collect!`/`collected`/`clear-collected!` (Tier 4) are backed by scopes:
|
||||
;; collect! lazily creates a root scope with dedup=true, then emits into it.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(declare-tier :dynamic-scope :source "eval.sx")
|
||||
(declare-tier :scoped-effects :source "eval.sx")
|
||||
|
||||
(declare-spread-primitive "scope-push!"
|
||||
:params (name value)
|
||||
:returns "nil"
|
||||
:effects [mutation]
|
||||
:doc "Push a scope with name and value. General form — provide-push! is an alias.")
|
||||
|
||||
(declare-spread-primitive "scope-pop!"
|
||||
:params (name)
|
||||
:returns "nil"
|
||||
:effects [mutation]
|
||||
:doc "Pop the most recent scope for name. General form — provide-pop! is an alias.")
|
||||
|
||||
(declare-spread-primitive "provide-push!"
|
||||
:params (name value)
|
||||
:returns "nil"
|
||||
:effects [mutation]
|
||||
:doc "Push a provider scope with name and value (platform internal).")
|
||||
:doc "Alias for scope-push!. Push a scope with name and value.")
|
||||
|
||||
(declare-spread-primitive "provide-pop!"
|
||||
:params (name)
|
||||
:returns "nil"
|
||||
:effects [mutation]
|
||||
:doc "Pop the most recent provider scope for name (platform internal).")
|
||||
:doc "Alias for scope-pop!. Pop the most recent scope for name.")
|
||||
|
||||
(declare-spread-primitive "context"
|
||||
:params (name &rest default)
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
(= name "reset") (sf-reset args env)
|
||||
(= name "shift") (sf-shift args env)
|
||||
(= name "dynamic-wind") (sf-dynamic-wind args env)
|
||||
(= name "scope") (sf-scope args env)
|
||||
(= name "provide") (sf-provide args env)
|
||||
|
||||
;; Higher-order forms
|
||||
@@ -951,11 +952,35 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6a2. provide — render-time dynamic scope
|
||||
;; 6a2. scope — unified render-time dynamic scope primitive
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; (provide name value body...) — push a named scope with value and empty
|
||||
;; accumulator, evaluate body, pop scope. Returns last body result.
|
||||
;; (scope name body...) or (scope name :value v body...)
|
||||
;; Push a named scope with optional value and empty accumulator,
|
||||
;; evaluate body, pop scope. Returns last body result.
|
||||
;;
|
||||
;; `provide` is sugar: (provide name value body...) = (scope name :value value body...)
|
||||
|
||||
(define sf-scope
|
||||
(fn ((args :as list) (env :as dict))
|
||||
(let ((name (trampoline (eval-expr (first args) env)))
|
||||
(rest (slice args 1))
|
||||
(val nil)
|
||||
(body-exprs nil))
|
||||
;; Check for :value keyword
|
||||
(if (and (>= (len rest) 2) (= (type-of (first rest)) "keyword") (= (keyword-name (first rest)) "value"))
|
||||
(do (set! val (trampoline (eval-expr (nth rest 1) env)))
|
||||
(set! body-exprs (slice rest 2)))
|
||||
(set! body-exprs rest))
|
||||
(scope-push! name val)
|
||||
(let ((result nil))
|
||||
(for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs)
|
||||
(scope-pop! name)
|
||||
result))))
|
||||
|
||||
|
||||
;; provide — sugar for scope with a value
|
||||
;; (provide name value body...) → (scope name :value value body...)
|
||||
|
||||
(define sf-provide
|
||||
(fn ((args :as list) (env :as dict))
|
||||
@@ -963,9 +988,9 @@
|
||||
(val (trampoline (eval-expr (nth args 1) env)))
|
||||
(body-exprs (slice args 2))
|
||||
(result nil))
|
||||
(provide-push! name val)
|
||||
(scope-push! name val)
|
||||
(for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs)
|
||||
(provide-pop! name)
|
||||
(scope-pop! name)
|
||||
result)))
|
||||
|
||||
|
||||
|
||||
@@ -527,6 +527,8 @@
|
||||
"collect!" "sxCollect"
|
||||
"collected" "sxCollected"
|
||||
"clear-collected!" "sxClearCollected"
|
||||
"scope-push!" "scopePush"
|
||||
"scope-pop!" "scopePop"
|
||||
"provide-push!" "providePush"
|
||||
"provide-pop!" "providePop"
|
||||
"context" "sxContext"
|
||||
|
||||
@@ -883,8 +883,7 @@ PREAMBLE = '''\
|
||||
function SxSpread(attrs) { this.attrs = attrs || {}; }
|
||||
SxSpread.prototype._spread = true;
|
||||
|
||||
var _collectBuckets = {};
|
||||
var _provideStacks = {};
|
||||
var _scopeStacks = {};
|
||||
|
||||
function isSym(x) { return x != null && x._sym === true; }
|
||||
function isKw(x) { return x != null && x._kw === true; }
|
||||
@@ -1098,14 +1097,17 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
''',
|
||||
|
||||
"stdlib.spread": '''
|
||||
// stdlib.spread — spread + collect primitives
|
||||
// stdlib.spread — spread + collect + scope primitives
|
||||
PRIMITIVES["make-spread"] = makeSpread;
|
||||
PRIMITIVES["spread?"] = isSpread;
|
||||
PRIMITIVES["spread-attrs"] = spreadAttrs;
|
||||
PRIMITIVES["collect!"] = sxCollect;
|
||||
PRIMITIVES["collected"] = sxCollected;
|
||||
PRIMITIVES["clear-collected!"] = sxClearCollected;
|
||||
// provide/context/emit! — render-time dynamic scope
|
||||
// scope — unified render-time dynamic scope
|
||||
PRIMITIVES["scope-push!"] = scopePush;
|
||||
PRIMITIVES["scope-pop!"] = scopePop;
|
||||
// provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
|
||||
PRIMITIVES["provide-push!"] = providePush;
|
||||
PRIMITIVES["provide-pop!"] = providePop;
|
||||
PRIMITIVES["context"] = sxContext;
|
||||
@@ -1174,46 +1176,54 @@ PLATFORM_JS_PRE = '''
|
||||
function isSpread(x) { return x != null && x._spread === true; }
|
||||
function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; }
|
||||
|
||||
function sxCollect(bucket, value) {
|
||||
if (!_collectBuckets[bucket]) _collectBuckets[bucket] = [];
|
||||
var items = _collectBuckets[bucket];
|
||||
if (items.indexOf(value) === -1) items.push(value);
|
||||
function scopePush(name, value) {
|
||||
if (!_scopeStacks[name]) _scopeStacks[name] = [];
|
||||
_scopeStacks[name].push({value: value !== undefined ? value : NIL, emitted: [], dedup: false});
|
||||
}
|
||||
function sxCollected(bucket) {
|
||||
return _collectBuckets[bucket] ? _collectBuckets[bucket].slice() : [];
|
||||
}
|
||||
function sxClearCollected(bucket) {
|
||||
if (_collectBuckets[bucket]) _collectBuckets[bucket] = [];
|
||||
function scopePop(name) {
|
||||
if (_scopeStacks[name] && _scopeStacks[name].length) _scopeStacks[name].pop();
|
||||
}
|
||||
// Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
|
||||
var providePush = scopePush;
|
||||
var providePop = scopePop;
|
||||
|
||||
function providePush(name, value) {
|
||||
if (!_provideStacks[name]) _provideStacks[name] = [];
|
||||
_provideStacks[name].push({value: value !== undefined ? value : NIL, emitted: []});
|
||||
}
|
||||
function providePop(name) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) _provideStacks[name].pop();
|
||||
}
|
||||
function sxContext(name) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) {
|
||||
return _provideStacks[name][_provideStacks[name].length - 1].value;
|
||||
if (_scopeStacks[name] && _scopeStacks[name].length) {
|
||||
return _scopeStacks[name][_scopeStacks[name].length - 1].value;
|
||||
}
|
||||
if (arguments.length > 1) return arguments[1];
|
||||
throw new Error("No provider for: " + name);
|
||||
}
|
||||
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);
|
||||
if (_scopeStacks[name] && _scopeStacks[name].length) {
|
||||
var entry = _scopeStacks[name][_scopeStacks[name].length - 1];
|
||||
if (entry.dedup && entry.emitted.indexOf(value) !== -1) return NIL;
|
||||
entry.emitted.push(value);
|
||||
}
|
||||
return NIL;
|
||||
}
|
||||
function sxEmitted(name) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) {
|
||||
return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice();
|
||||
if (_scopeStacks[name] && _scopeStacks[name].length) {
|
||||
return _scopeStacks[name][_scopeStacks[name].length - 1].emitted.slice();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function sxCollect(bucket, value) {
|
||||
if (!_scopeStacks[bucket] || !_scopeStacks[bucket].length) {
|
||||
if (!_scopeStacks[bucket]) _scopeStacks[bucket] = [];
|
||||
_scopeStacks[bucket].push({value: NIL, emitted: [], dedup: true});
|
||||
}
|
||||
var entry = _scopeStacks[bucket][_scopeStacks[bucket].length - 1];
|
||||
if (entry.emitted.indexOf(value) === -1) entry.emitted.push(value);
|
||||
}
|
||||
function sxCollected(bucket) {
|
||||
return sxEmitted(bucket);
|
||||
}
|
||||
function sxClearCollected(bucket) {
|
||||
if (_scopeStacks[bucket] && _scopeStacks[bucket].length) {
|
||||
_scopeStacks[bucket][_scopeStacks[bucket].length - 1].emitted = [];
|
||||
}
|
||||
}
|
||||
|
||||
function lambdaParams(f) { return f.params; }
|
||||
function lambdaBody(f) { return f.body; }
|
||||
@@ -3246,6 +3256,8 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
|
||||
api_lines.append(' collect: sxCollect,')
|
||||
api_lines.append(' collected: sxCollected,')
|
||||
api_lines.append(' clearCollected: sxClearCollected,')
|
||||
api_lines.append(' scopePush: scopePush,')
|
||||
api_lines.append(' scopePop: scopePop,')
|
||||
api_lines.append(' providePush: providePush,')
|
||||
api_lines.append(' providePop: providePop,')
|
||||
api_lines.append(' context: sxContext,')
|
||||
|
||||
@@ -91,53 +91,56 @@ class _Spread:
|
||||
self.attrs = dict(attrs) if attrs else {}
|
||||
|
||||
|
||||
# Render-time accumulator buckets (per render pass)
|
||||
_collect_buckets: dict[str, list] = {}
|
||||
# Unified scope stacks — backing store for provide/context/emit!/collect!
|
||||
# Each entry: {"value": v, "emitted": [], "dedup": bool}
|
||||
_scope_stacks: dict[str, list[dict]] = {}
|
||||
|
||||
|
||||
def _collect_reset():
|
||||
"""Reset all collect buckets (call at start of each render pass)."""
|
||||
global _collect_buckets
|
||||
_collect_buckets = {}
|
||||
"""Reset all scope stacks (call at start of each render pass)."""
|
||||
global _scope_stacks
|
||||
_scope_stacks = {}
|
||||
|
||||
|
||||
# Render-time dynamic scope stacks (provide/context/emit!)
|
||||
_provide_stacks: dict[str, list[dict]] = {}
|
||||
def scope_push(name, value=None):
|
||||
"""Push a scope with name, value, and empty accumulator."""
|
||||
_scope_stacks.setdefault(name, []).append({"value": value, "emitted": [], "dedup": False})
|
||||
|
||||
|
||||
def provide_push(name, value=None):
|
||||
"""Push a provider scope with name, value, and empty emitted list."""
|
||||
_provide_stacks.setdefault(name, []).append({"value": value, "emitted": []})
|
||||
def scope_pop(name):
|
||||
"""Pop the most recent scope for name."""
|
||||
if name in _scope_stacks and _scope_stacks[name]:
|
||||
_scope_stacks[name].pop()
|
||||
|
||||
|
||||
def provide_pop(name):
|
||||
"""Pop the most recent provider scope for name."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
_provide_stacks[name].pop()
|
||||
# Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
|
||||
provide_push = scope_push
|
||||
provide_pop = scope_pop
|
||||
|
||||
|
||||
def sx_context(name, *default):
|
||||
"""Read value from nearest enclosing provider. Error if no provider and no default."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
return _provide_stacks[name][-1]["value"]
|
||||
"""Read value from nearest enclosing scope. Error if no scope and no default."""
|
||||
if name in _scope_stacks and _scope_stacks[name]:
|
||||
return _scope_stacks[name][-1]["value"]
|
||||
if default:
|
||||
return default[0]
|
||||
raise RuntimeError(f"No provider for: {name}")
|
||||
|
||||
|
||||
def sx_emit(name, value):
|
||||
"""Append value to nearest enclosing provider's accumulator. Error 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}")
|
||||
"""Append value to nearest enclosing scope's accumulator. Respects dedup flag."""
|
||||
if name in _scope_stacks and _scope_stacks[name]:
|
||||
entry = _scope_stacks[name][-1]
|
||||
if entry["dedup"] and value in entry["emitted"]:
|
||||
return NIL
|
||||
entry["emitted"].append(value)
|
||||
return NIL
|
||||
|
||||
|
||||
def sx_emitted(name):
|
||||
"""Return list of values emitted into nearest matching provider."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
return list(_provide_stacks[name][-1]["emitted"])
|
||||
"""Return list of values emitted into nearest matching scope."""
|
||||
if name in _scope_stacks and _scope_stacks[name]:
|
||||
return list(_scope_stacks[name][-1]["emitted"])
|
||||
return []
|
||||
|
||||
|
||||
@@ -342,23 +345,23 @@ def spread_attrs(s):
|
||||
|
||||
|
||||
def sx_collect(bucket, value):
|
||||
"""Add value to named render-time accumulator (deduplicated)."""
|
||||
if bucket not in _collect_buckets:
|
||||
_collect_buckets[bucket] = []
|
||||
items = _collect_buckets[bucket]
|
||||
if value not in items:
|
||||
items.append(value)
|
||||
"""Add value to named scope accumulator (deduplicated). Lazily creates root scope."""
|
||||
if bucket not in _scope_stacks or not _scope_stacks[bucket]:
|
||||
_scope_stacks.setdefault(bucket, []).append({"value": None, "emitted": [], "dedup": True})
|
||||
entry = _scope_stacks[bucket][-1]
|
||||
if value not in entry["emitted"]:
|
||||
entry["emitted"].append(value)
|
||||
|
||||
|
||||
def sx_collected(bucket):
|
||||
"""Return all values in named render-time accumulator."""
|
||||
return list(_collect_buckets.get(bucket, []))
|
||||
"""Return all values collected in named scope accumulator."""
|
||||
return sx_emitted(bucket)
|
||||
|
||||
|
||||
def sx_clear_collected(bucket):
|
||||
"""Clear a named render-time accumulator bucket."""
|
||||
if bucket in _collect_buckets:
|
||||
_collect_buckets[bucket] = []
|
||||
"""Clear nearest scope's accumulator for name."""
|
||||
if bucket in _scope_stacks and _scope_stacks[bucket]:
|
||||
_scope_stacks[bucket][-1]["emitted"] = []
|
||||
|
||||
|
||||
def lambda_params(f):
|
||||
@@ -976,14 +979,17 @@ PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).thro
|
||||
''',
|
||||
|
||||
"stdlib.spread": '''
|
||||
# stdlib.spread — spread + collect primitives
|
||||
# stdlib.spread — spread + collect + scope primitives
|
||||
PRIMITIVES["make-spread"] = make_spread
|
||||
PRIMITIVES["spread?"] = is_spread
|
||||
PRIMITIVES["spread-attrs"] = spread_attrs
|
||||
PRIMITIVES["collect!"] = sx_collect
|
||||
PRIMITIVES["collected"] = sx_collected
|
||||
PRIMITIVES["clear-collected!"] = sx_clear_collected
|
||||
# provide/context/emit! — render-time dynamic scope
|
||||
# scope — unified render-time dynamic scope
|
||||
PRIMITIVES["scope-push!"] = scope_push
|
||||
PRIMITIVES["scope-pop!"] = scope_pop
|
||||
# provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
|
||||
PRIMITIVES["provide-push!"] = provide_push
|
||||
PRIMITIVES["provide-pop!"] = provide_pop
|
||||
PRIMITIVES["context"] = sx_context
|
||||
|
||||
@@ -252,6 +252,8 @@
|
||||
"collect!" "sx_collect"
|
||||
"collected" "sx_collected"
|
||||
"clear-collected!" "sx_clear_collected"
|
||||
"scope-push!" "scope_push"
|
||||
"scope-pop!" "scope_pop"
|
||||
"provide-push!" "provide_push"
|
||||
"provide-pop!" "provide_pop"
|
||||
"context" "sx_context"
|
||||
|
||||
@@ -269,11 +269,13 @@
|
||||
;; (collected bucket) → list
|
||||
;; (clear-collected! bucket) → void
|
||||
;;
|
||||
;; Dynamic scope (provide/context/emit!):
|
||||
;; (provide-push! name val) → void
|
||||
;; (provide-pop! name) → void
|
||||
;; (context name &rest def) → value from nearest provider
|
||||
;; (emit! name value) → void (append to provider accumulator)
|
||||
;; Scoped effects (scope/provide/context/emit!):
|
||||
;; (scope-push! name val) → void (general form)
|
||||
;; (scope-pop! name) → void (general form)
|
||||
;; (provide-push! name val) → alias for scope-push!
|
||||
;; (provide-pop! name) → alias for scope-pop!
|
||||
;; (context name &rest def) → value from nearest scope
|
||||
;; (emit! name value) → void (append to scope accumulator)
|
||||
;; (emitted name) → list of emitted values
|
||||
;;
|
||||
;; From parser.sx:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -52,53 +50,56 @@ class _Spread:
|
||||
self.attrs = dict(attrs) if attrs else {}
|
||||
|
||||
|
||||
# Render-time accumulator buckets (per render pass)
|
||||
_collect_buckets: dict[str, list] = {}
|
||||
# Unified scope stacks — backing store for provide/context/emit!/collect!
|
||||
# Each entry: {"value": v, "emitted": [], "dedup": bool}
|
||||
_scope_stacks: dict[str, list[dict]] = {}
|
||||
|
||||
|
||||
def _collect_reset():
|
||||
"""Reset all collect buckets (call at start of each render pass)."""
|
||||
global _collect_buckets
|
||||
_collect_buckets = {}
|
||||
"""Reset all scope stacks (call at start of each render pass)."""
|
||||
global _scope_stacks
|
||||
_scope_stacks = {}
|
||||
|
||||
|
||||
# Render-time dynamic scope stacks (provide/context/emit!)
|
||||
_provide_stacks: dict[str, list[dict]] = {}
|
||||
def scope_push(name, value=None):
|
||||
"""Push a scope with name, value, and empty accumulator."""
|
||||
_scope_stacks.setdefault(name, []).append({"value": value, "emitted": [], "dedup": False})
|
||||
|
||||
|
||||
def provide_push(name, value=None):
|
||||
"""Push a provider scope with name, value, and empty emitted list."""
|
||||
_provide_stacks.setdefault(name, []).append({"value": value, "emitted": []})
|
||||
def scope_pop(name):
|
||||
"""Pop the most recent scope for name."""
|
||||
if name in _scope_stacks and _scope_stacks[name]:
|
||||
_scope_stacks[name].pop()
|
||||
|
||||
|
||||
def provide_pop(name):
|
||||
"""Pop the most recent provider scope for name."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
_provide_stacks[name].pop()
|
||||
# Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
|
||||
provide_push = scope_push
|
||||
provide_pop = scope_pop
|
||||
|
||||
|
||||
def sx_context(name, *default):
|
||||
"""Read value from nearest enclosing provider. Error if no provider and no default."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
return _provide_stacks[name][-1]["value"]
|
||||
"""Read value from nearest enclosing scope. Error if no scope and no default."""
|
||||
if name in _scope_stacks and _scope_stacks[name]:
|
||||
return _scope_stacks[name][-1]["value"]
|
||||
if default:
|
||||
return default[0]
|
||||
raise RuntimeError(f"No provider for: {name}")
|
||||
|
||||
|
||||
def sx_emit(name, value):
|
||||
"""Append value to nearest enclosing provider's accumulator. Error 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}")
|
||||
"""Append value to nearest enclosing scope's accumulator. Respects dedup flag."""
|
||||
if name in _scope_stacks and _scope_stacks[name]:
|
||||
entry = _scope_stacks[name][-1]
|
||||
if entry["dedup"] and value in entry["emitted"]:
|
||||
return NIL
|
||||
entry["emitted"].append(value)
|
||||
return NIL
|
||||
|
||||
|
||||
def sx_emitted(name):
|
||||
"""Return list of values emitted into nearest matching provider."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
return list(_provide_stacks[name][-1]["emitted"])
|
||||
"""Return list of values emitted into nearest matching scope."""
|
||||
if name in _scope_stacks and _scope_stacks[name]:
|
||||
return list(_scope_stacks[name][-1]["emitted"])
|
||||
return []
|
||||
|
||||
|
||||
@@ -303,23 +304,23 @@ def spread_attrs(s):
|
||||
|
||||
|
||||
def sx_collect(bucket, value):
|
||||
"""Add value to named render-time accumulator (deduplicated)."""
|
||||
if bucket not in _collect_buckets:
|
||||
_collect_buckets[bucket] = []
|
||||
items = _collect_buckets[bucket]
|
||||
if value not in items:
|
||||
items.append(value)
|
||||
"""Add value to named scope accumulator (deduplicated). Lazily creates root scope."""
|
||||
if bucket not in _scope_stacks or not _scope_stacks[bucket]:
|
||||
_scope_stacks.setdefault(bucket, []).append({"value": None, "emitted": [], "dedup": True})
|
||||
entry = _scope_stacks[bucket][-1]
|
||||
if value not in entry["emitted"]:
|
||||
entry["emitted"].append(value)
|
||||
|
||||
|
||||
def sx_collected(bucket):
|
||||
"""Return all values in named render-time accumulator."""
|
||||
return list(_collect_buckets.get(bucket, []))
|
||||
"""Return all values collected in named scope accumulator."""
|
||||
return sx_emitted(bucket)
|
||||
|
||||
|
||||
def sx_clear_collected(bucket):
|
||||
"""Clear a named render-time accumulator bucket."""
|
||||
if bucket in _collect_buckets:
|
||||
_collect_buckets[bucket] = []
|
||||
"""Clear nearest scope's accumulator for name."""
|
||||
if bucket in _scope_stacks and _scope_stacks[bucket]:
|
||||
_scope_stacks[bucket][-1]["emitted"] = []
|
||||
|
||||
|
||||
def lambda_params(f):
|
||||
@@ -941,14 +942,17 @@ def _strip_tags(s):
|
||||
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
|
||||
|
||||
|
||||
# stdlib.spread — spread + collect primitives
|
||||
# stdlib.spread — spread + collect + scope primitives
|
||||
PRIMITIVES["make-spread"] = make_spread
|
||||
PRIMITIVES["spread?"] = is_spread
|
||||
PRIMITIVES["spread-attrs"] = spread_attrs
|
||||
PRIMITIVES["collect!"] = sx_collect
|
||||
PRIMITIVES["collected"] = sx_collected
|
||||
PRIMITIVES["clear-collected!"] = sx_clear_collected
|
||||
# provide/context/emit! — render-time dynamic scope
|
||||
# scope — unified render-time dynamic scope
|
||||
PRIMITIVES["scope-push!"] = scope_push
|
||||
PRIMITIVES["scope-pop!"] = scope_pop
|
||||
# provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
|
||||
PRIMITIVES["provide-push!"] = provide_push
|
||||
PRIMITIVES["provide-pop!"] = provide_pop
|
||||
PRIMITIVES["context"] = sx_context
|
||||
@@ -1398,6 +1402,8 @@ def eval_list(expr, env):
|
||||
return sf_shift(args, env)
|
||||
elif sx_truthy((name == 'dynamic-wind')):
|
||||
return sf_dynamic_wind(args, env)
|
||||
elif sx_truthy((name == 'scope')):
|
||||
return sf_scope(args, env)
|
||||
elif sx_truthy((name == 'provide')):
|
||||
return sf_provide(args, env)
|
||||
elif sx_truthy((name == 'map')):
|
||||
@@ -1891,6 +1897,25 @@ def sf_dynamic_wind(args, env):
|
||||
call_thunk(after, env)
|
||||
return result
|
||||
|
||||
# sf-scope
|
||||
def sf_scope(args, env):
|
||||
_cells = {}
|
||||
name = trampoline(eval_expr(first(args), env))
|
||||
rest = slice(args, 1)
|
||||
val = NIL
|
||||
body_exprs = NIL
|
||||
if sx_truthy(((len(rest) >= 2) if not sx_truthy((len(rest) >= 2)) else ((type_of(first(rest)) == 'keyword') if not sx_truthy((type_of(first(rest)) == 'keyword')) else (keyword_name(first(rest)) == 'value')))):
|
||||
val = trampoline(eval_expr(nth(rest, 1), env))
|
||||
body_exprs = slice(rest, 2)
|
||||
else:
|
||||
body_exprs = rest
|
||||
scope_push(name, val)
|
||||
_cells['result'] = NIL
|
||||
for e in body_exprs:
|
||||
_cells['result'] = trampoline(eval_expr(e, env))
|
||||
scope_pop(name)
|
||||
return _cells['result']
|
||||
|
||||
# sf-provide
|
||||
def sf_provide(args, env):
|
||||
_cells = {}
|
||||
@@ -1898,10 +1923,10 @@ def sf_provide(args, env):
|
||||
val = trampoline(eval_expr(nth(args, 1), env))
|
||||
body_exprs = slice(args, 2)
|
||||
_cells['result'] = NIL
|
||||
provide_push(name, val)
|
||||
scope_push(name, val)
|
||||
for e in body_exprs:
|
||||
_cells['result'] = trampoline(eval_expr(e, env))
|
||||
provide_pop(name)
|
||||
scope_pop(name)
|
||||
return _cells['result']
|
||||
|
||||
# expand-macro
|
||||
@@ -2225,7 +2250,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,12 +2274,13 @@ 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))
|
||||
|
||||
# RENDER_HTML_FORMS
|
||||
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']
|
||||
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', 'scope', 'provide']
|
||||
|
||||
# render-html-form?
|
||||
def is_render_html_form(name):
|
||||
@@ -2266,12 +2293,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 +2342,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,39 +2356,51 @@ 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 == 'scope')):
|
||||
scope_name = trampoline(eval_expr(nth(expr, 1), env))
|
||||
rest_args = slice(expr, 2)
|
||||
scope_val = NIL
|
||||
body_exprs = NIL
|
||||
if sx_truthy(((len(rest_args) >= 2) if not sx_truthy((len(rest_args) >= 2)) else ((type_of(first(rest_args)) == 'keyword') if not sx_truthy((type_of(first(rest_args)) == 'keyword')) else (keyword_name(first(rest_args)) == 'value')))):
|
||||
scope_val = trampoline(eval_expr(nth(rest_args, 1), env))
|
||||
body_exprs = slice(rest_args, 2)
|
||||
else:
|
||||
body_exprs = rest_args
|
||||
scope_push(scope_name, scope_val)
|
||||
result = (render_to_html(first(body_exprs), env) if sx_truthy((len(body_exprs) == 1)) else join('', map(lambda e: render_to_html(e, env), body_exprs)))
|
||||
scope_pop(scope_name)
|
||||
return result
|
||||
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))))))
|
||||
provide_pop(prov_name)
|
||||
scope_push(prov_name, prov_val)
|
||||
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)))))
|
||||
scope_pop(prov_name)
|
||||
return result
|
||||
else:
|
||||
return render_value_to_html(trampoline(eval_expr(expr, env)), env)
|
||||
@@ -2382,12 +2420,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 +2432,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, '>')
|
||||
scope_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)
|
||||
scope_pop('element-attrs')
|
||||
return sx_str('<', tag, render_attrs(attrs), '>', content, '</', tag, '>')
|
||||
|
||||
# render-html-lake
|
||||
def render_html_lake(args, env):
|
||||
@@ -2416,14 +2447,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'], '>')
|
||||
scope_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)
|
||||
scope_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 +2462,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'], '>')
|
||||
scope_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)
|
||||
scope_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 +2479,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 +2505,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 +2555,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 +2568,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
|
||||
scope_push('element-attrs', NIL)
|
||||
for arg in args:
|
||||
if sx_truthy(_cells['skip']):
|
||||
_cells['skip'] = False
|
||||
@@ -2585,30 +2581,31 @@ 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))
|
||||
scope_pop('element-attrs')
|
||||
parts = concat([name], attr_parts, child_parts)
|
||||
return sx_str('(', join(' ', parts), ')')
|
||||
|
||||
# SPECIAL_FORM_NAMES
|
||||
SPECIAL_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'defrelation', 'begin', 'do', 'quote', 'quasiquote', '->', 'set!', 'letrec', 'dynamic-wind', 'defisland', 'deftype', 'defeffect', 'provide']
|
||||
SPECIAL_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'defrelation', 'begin', 'do', 'quote', 'quasiquote', '->', 'set!', 'letrec', 'dynamic-wind', 'defisland', 'deftype', 'defeffect', 'scope', 'provide']
|
||||
|
||||
# HO_FORM_NAMES
|
||||
HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each']
|
||||
@@ -2705,14 +2702,30 @@ def aser_special(name, expr, env):
|
||||
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'defrelation') if sx_truthy((name == 'defrelation')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect')))))))))))):
|
||||
trampoline(eval_expr(expr, env))
|
||||
return NIL
|
||||
elif sx_truthy((name == 'scope')):
|
||||
scope_name = trampoline(eval_expr(first(args), env))
|
||||
rest_args = rest(args)
|
||||
scope_val = NIL
|
||||
body_args = NIL
|
||||
if sx_truthy(((len(rest_args) >= 2) if not sx_truthy((len(rest_args) >= 2)) else ((type_of(first(rest_args)) == 'keyword') if not sx_truthy((type_of(first(rest_args)) == 'keyword')) else (keyword_name(first(rest_args)) == 'value')))):
|
||||
scope_val = trampoline(eval_expr(nth(rest_args, 1), env))
|
||||
body_args = slice(rest_args, 2)
|
||||
else:
|
||||
body_args = rest_args
|
||||
scope_push(scope_name, scope_val)
|
||||
_cells['result'] = NIL
|
||||
for body in body_args:
|
||||
_cells['result'] = aser(body, env)
|
||||
scope_pop(scope_name)
|
||||
return _cells['result']
|
||||
elif sx_truthy((name == 'provide')):
|
||||
prov_name = trampoline(eval_expr(first(args), env))
|
||||
prov_val = trampoline(eval_expr(nth(args, 1), env))
|
||||
_cells['result'] = NIL
|
||||
provide_push(prov_name, prov_val)
|
||||
scope_push(prov_name, prov_val)
|
||||
for body in slice(args, 2):
|
||||
_cells['result'] = aser(body, env)
|
||||
provide_pop(prov_name)
|
||||
scope_pop(prov_name)
|
||||
return _cells['result']
|
||||
else:
|
||||
return trampoline(eval_expr(expr, env))
|
||||
@@ -3659,7 +3672,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 +3705,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 +3760,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 = []
|
||||
scope_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)
|
||||
scope_pop('element-attrs')
|
||||
if sx_truthy(token):
|
||||
svg_context_reset(token)
|
||||
return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '</', tag, '>')
|
||||
@@ -3787,9 +3801,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 +3817,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)
|
||||
@@ -3847,7 +3857,7 @@ async def async_map_render(exprs, env, ctx):
|
||||
return results
|
||||
|
||||
# ASYNC_RENDER_FORMS
|
||||
ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each', 'provide']
|
||||
ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each', 'scope', 'provide']
|
||||
|
||||
# async-render-form?
|
||||
def async_render_form_p(name):
|
||||
@@ -3871,8 +3881,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,39 +3895,51 @@ 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 == 'scope')):
|
||||
scope_name = (await async_eval(nth(expr, 1), env, ctx))
|
||||
rest_args = slice(expr, 2)
|
||||
scope_val = NIL
|
||||
body_exprs = NIL
|
||||
if sx_truthy(((len(rest_args) >= 2) if not sx_truthy((len(rest_args) >= 2)) else ((type_of(first(rest_args)) == 'keyword') if not sx_truthy((type_of(first(rest_args)) == 'keyword')) else (keyword_name(first(rest_args)) == 'value')))):
|
||||
scope_val = (await async_eval(nth(rest_args, 1), env, ctx))
|
||||
body_exprs = slice(rest_args, 2)
|
||||
else:
|
||||
body_exprs = rest_args
|
||||
scope_push(scope_name, scope_val)
|
||||
result = ((await async_render(first(body_exprs), env, ctx)) if sx_truthy((len(body_exprs) == 1)) else join('', (await async_map_render(body_exprs, env, ctx))))
|
||||
scope_pop(scope_name)
|
||||
return result
|
||||
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))))
|
||||
provide_pop(prov_name)
|
||||
scope_push(prov_name, prov_val)
|
||||
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))))
|
||||
scope_pop(prov_name)
|
||||
return result
|
||||
else:
|
||||
return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
|
||||
@@ -4019,42 +4040,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 +4162,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 +4218,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
|
||||
scope_push('element-attrs', NIL)
|
||||
for arg in args:
|
||||
if sx_truthy(_cells['skip']):
|
||||
_cells['skip'] = False
|
||||
@@ -4215,43 +4231,44 @@ 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))
|
||||
scope_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
|
||||
ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland', 'deftype', 'defeffect', 'provide']
|
||||
ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland', 'deftype', 'defeffect', 'scope', 'provide']
|
||||
|
||||
# ASYNC_ASER_HO_NAMES
|
||||
ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each']
|
||||
@@ -4345,14 +4362,30 @@ async def dispatch_async_aser_form(name, expr, env, ctx):
|
||||
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect'))))))))))):
|
||||
(await async_eval(expr, env, ctx))
|
||||
return NIL
|
||||
elif sx_truthy((name == 'scope')):
|
||||
scope_name = (await async_eval(first(args), env, ctx))
|
||||
rest_args = rest(args)
|
||||
scope_val = NIL
|
||||
body_args = NIL
|
||||
if sx_truthy(((len(rest_args) >= 2) if not sx_truthy((len(rest_args) >= 2)) else ((type_of(first(rest_args)) == 'keyword') if not sx_truthy((type_of(first(rest_args)) == 'keyword')) else (keyword_name(first(rest_args)) == 'value')))):
|
||||
scope_val = (await async_eval(nth(rest_args, 1), env, ctx))
|
||||
body_args = slice(rest_args, 2)
|
||||
else:
|
||||
body_args = rest_args
|
||||
scope_push(scope_name, scope_val)
|
||||
_cells['result'] = NIL
|
||||
for body in body_args:
|
||||
_cells['result'] = (await async_aser(body, env, ctx))
|
||||
scope_pop(scope_name)
|
||||
return _cells['result']
|
||||
elif sx_truthy((name == 'provide')):
|
||||
prov_name = (await async_eval(first(args), env, ctx))
|
||||
prov_val = (await async_eval(nth(args, 1), env, ctx))
|
||||
_cells['result'] = NIL
|
||||
provide_push(prov_name, prov_val)
|
||||
scope_push(prov_name, prov_val)
|
||||
for body in slice(args, 2):
|
||||
_cells['result'] = (await async_aser(body, env, ctx))
|
||||
provide_pop(prov_name)
|
||||
scope_pop(prov_name)
|
||||
return _cells['result']
|
||||
else:
|
||||
return (await async_eval(expr, env, ctx))
|
||||
|
||||
@@ -285,6 +285,62 @@
|
||||
(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\")"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Scope tests — unified scope primitive
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "scope"
|
||||
|
||||
(deftest "scope with value and context"
|
||||
(assert-equal "dark"
|
||||
(render-sx "(scope \"sc-theme\" :value \"dark\" (context \"sc-theme\"))")))
|
||||
|
||||
(deftest "scope without value defaults to nil"
|
||||
(assert-equal ""
|
||||
(render-sx "(scope \"sc-nil\" (str (context \"sc-nil\")))")))
|
||||
|
||||
(deftest "scope with emit!/emitted"
|
||||
(assert-equal "a,b"
|
||||
(render-sx "(scope \"sc-emit\" (emit! \"sc-emit\" \"a\") (emit! \"sc-emit\" \"b\") (join \",\" (emitted \"sc-emit\")))")))
|
||||
|
||||
(deftest "provide is equivalent to scope with value"
|
||||
(assert-equal "42"
|
||||
(render-sx "(provide \"sc-prov\" 42 (str (context \"sc-prov\")))")))
|
||||
|
||||
(deftest "collect! works via scope (lazy root scope)"
|
||||
(assert-equal "x,y"
|
||||
(render-sx "(do (collect! \"sc-coll\" \"x\") (collect! \"sc-coll\" \"y\") (join \",\" (collected \"sc-coll\")))")))
|
||||
|
||||
(deftest "collect! deduplicates"
|
||||
(assert-equal "a"
|
||||
(render-sx "(do (collect! \"sc-dedup\" \"a\") (collect! \"sc-dedup\" \"a\") (join \",\" (collected \"sc-dedup\")))")))
|
||||
|
||||
(deftest "clear-collected! clears scope accumulator"
|
||||
(assert-equal ""
|
||||
(render-sx "(do (collect! \"sc-clear\" \"x\") (clear-collected! \"sc-clear\") (join \",\" (collected \"sc-clear\")))")))
|
||||
|
||||
(deftest "nested scope shadows outer"
|
||||
(assert-equal "inner"
|
||||
(render-sx "(scope \"sc-nest\" :value \"outer\" (scope \"sc-nest\" :value \"inner\" (context \"sc-nest\")))")))
|
||||
|
||||
(deftest "scope pops correctly after body"
|
||||
(assert-equal "outer"
|
||||
(render-sx "(scope \"sc-pop\" :value \"outer\" (scope \"sc-pop\" :value \"inner\" \"ignore\") (context \"sc-pop\"))"))))
|
||||
|
||||
@@ -374,8 +374,12 @@
|
||||
:children (list
|
||||
{:label "Reference" :href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items}
|
||||
{:label "Examples" :href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items})}
|
||||
{:label "Scopes" :href "/sx/(geography.(scopes))"
|
||||
:summary "The unified primitive beneath provide, collect!, spreads, and islands. Named scope with downward value, upward accumulation, and a dedup flag."}
|
||||
{:label "Provide / Emit!" :href "/sx/(geography.(provide))"
|
||||
:summary "Sugar for scope-with-value. Render-time dynamic scope — the substrate beneath spreads, CSSX, and script collection."}
|
||||
{:label "Spreads" :href "/sx/(geography.(spreads))"
|
||||
:summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, and the path to provide/context/emit!."}
|
||||
:summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes."}
|
||||
{:label "Marshes" :href "/sx/(geography.(marshes))"
|
||||
:summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted."}
|
||||
{:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items})}
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content)
|
||||
:else '(~reactive-islands/index/reactive-islands-index-content)))))
|
||||
|
||||
(define scopes
|
||||
(fn (content)
|
||||
(if (nil? content) '(~geography/scopes-content) content)))
|
||||
|
||||
(define spreads
|
||||
(fn (content)
|
||||
(if (nil? content) '(~geography/spreads-content) content)))
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
(p "Every layer is definable in terms of the one below. "
|
||||
"No layer can be decomposed without the layer beneath it.")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str "Layer 0: CEK machine (expression + environment + continuation)\n"
|
||||
"Layer 1: Continuations (shift / reset \u2014 delimited capture)\n"
|
||||
"Layer 2: Algebraic effects (operations + handlers)\n"
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
(p "SX already implements CEK. It just doesn't name it:")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str ";; eval-expr IS the CEK transition function\n"
|
||||
";; C = expr, E = env, K = implicit (call stack / trampoline)\n"
|
||||
"(define eval-expr\n"
|
||||
@@ -105,7 +105,7 @@
|
||||
(p "Delimited continuations (Felleisen 1988, Danvy & Filinski 1990) "
|
||||
"expose the K register as a first-class value:")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str ";; reset marks a point in the continuation\n"
|
||||
"(reset\n"
|
||||
" (+ 1 (shift k ;; k = \"the rest up to reset\"\n"
|
||||
@@ -148,7 +148,7 @@
|
||||
"an operation (\"perform this effect\") and a handler (\"here's what that effect means\"). "
|
||||
"The handler receives the operation's argument and a continuation to resume the program.")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str ";; Pseudocode \u2014 algebraic effect style\n"
|
||||
"(handle\n"
|
||||
" (fn () (+ 1 (perform :ask \"what number?\")))\n"
|
||||
@@ -317,7 +317,7 @@
|
||||
(p "The deepest primitive is not a single thing. "
|
||||
"It's a point in a three-dimensional space:")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str "depth: CEK \u2192 continuations \u2192 algebraic effects \u2192 scoped effects\n"
|
||||
"topology: sequential \u2192 concurrent \u2192 distributed\n"
|
||||
"linearity: unrestricted \u2192 affine \u2192 linear"))
|
||||
@@ -378,7 +378,7 @@
|
||||
(p "If C, E, and K are all data structures (not host stack frames), "
|
||||
"the entire computation state is serializable:")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str ";; Freeze a computation mid-flight\n"
|
||||
"(let ((state (capture-cek)))\n"
|
||||
" (send-to-worker state) ;; ship to another machine\n"
|
||||
@@ -454,7 +454,7 @@
|
||||
|
||||
(p "Add optional effect annotations to function definitions:")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str ";; Declare what effects a function uses\n"
|
||||
"(define fetch-user :effects [io auth]\n"
|
||||
" (fn (id) ...))\n"
|
||||
@@ -477,7 +477,7 @@
|
||||
|
||||
(p "Refactor eval.sx to expose the CEK registers as data:")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str ";; The CEK state is a value\n"
|
||||
"(define-record CEK\n"
|
||||
" :control expr ;; the expression\n"
|
||||
@@ -514,7 +514,7 @@
|
||||
|
||||
(p "Extend the CEK machine to support multiple concurrent computations:")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str ";; Fork: create two CEK states from one\n"
|
||||
"(define fork :effects [concurrency]\n"
|
||||
" (fn (cek)\n"
|
||||
@@ -539,7 +539,7 @@
|
||||
|
||||
(p "Add resource-safety constraints:")
|
||||
|
||||
(~docs/code-block :code
|
||||
(~docs/code :code
|
||||
(str ";; Linear scope: must be entered, must complete\n"
|
||||
"(define-linear open-file :effects [io linear]\n"
|
||||
" (fn (path)\n"
|
||||
|
||||
@@ -340,24 +340,27 @@
|
||||
(p "The path from current SX to the scope primitive follows the existing plan "
|
||||
"and adds two phases:")
|
||||
|
||||
(~docs/subsection :title "Phase 1: provide/context/emit! (immediate)"
|
||||
(p "Already planned. Implement render-time dynamic scope. Four primitives: "
|
||||
(~docs/subsection :title "Phase 1: provide/context/emit! ✓"
|
||||
(p (strong "Complete. ") "Render-time dynamic scope. Four primitives: "
|
||||
(code "provide") " (special form), " (code "context") ", " (code "emit!") ", "
|
||||
(code "emitted") ". Platform provides " (code "provide-push!/provide-pop!") ".")
|
||||
(p "This is " (code "scope") " with " (code ":propagation :render") " only. "
|
||||
"No change to islands or lakes. Pure addition.")
|
||||
(p (strong "Delivers: ") "render-time context, scoped accumulation, "
|
||||
"spread and collect reimplemented as sugar over provide/emit."))
|
||||
(code "emitted") ". Platform provides " (code "scope-push!/scope-pop!") ". "
|
||||
"Spreads reimplemented on provide/emit!.")
|
||||
(p "See "
|
||||
(a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline" "provide article")
|
||||
" and "
|
||||
(a :href "/sx/(geography.(spreads))" :class "text-violet-600 hover:underline" "spreads article")
|
||||
"."))
|
||||
|
||||
(~docs/subsection :title "Phase 2: scope as the common form (next)"
|
||||
(p "Introduce " (code "scope") " as the general form. "
|
||||
(code "provide") " becomes sugar for " (code "(scope ... :propagation :render)") ". "
|
||||
(code "defisland") " becomes sugar for " (code "(scope ... :propagation :reactive)") ". "
|
||||
(code "lake") " becomes sugar for " (code "(scope ... :propagation :morph)") ".")
|
||||
(p "The sugar forms remain — nobody writes " (code "scope") " directly in page code. "
|
||||
"But the evaluator, adapters, and bootstrappers all dispatch through one mechanism.")
|
||||
(p (strong "Delivers: ") "unified internal representation, reactive context (the new cell), "
|
||||
"simplified adapter code (one scope handler instead of three separate paths)."))
|
||||
(~docs/subsection :title "Phase 2: scope as the common form ✓"
|
||||
(p (strong "Complete. ") (code "scope") " is now the general form. "
|
||||
(code "provide") " is sugar for " (code "(scope name :value v body...)") ". "
|
||||
(code "collect!") " creates a lazy root scope with deduplication. "
|
||||
"All adapters use " (code "scope-push!/scope-pop!") " directly.")
|
||||
(p "The unified platform structure:")
|
||||
(~docs/code :code (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python"))
|
||||
(p "See "
|
||||
(a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes article")
|
||||
"."))
|
||||
|
||||
(~docs/subsection :title "Phase 3: effect handlers (future)"
|
||||
(p "Make propagation modes extensible. A " (code ":propagation") " value is a "
|
||||
@@ -437,8 +440,10 @@
|
||||
"and composable. It's the last primitive SX needs.")
|
||||
|
||||
(~docs/note
|
||||
(p (strong "Status: ") "Phase 1 (" (code "provide/context/emit!") ") is specced and "
|
||||
"ready to build. Phase 2 (" (code "scope") " unification) follows naturally once "
|
||||
"provide is working. Phase 3 (extensible handlers) is the research frontier — "
|
||||
(p (strong "Status: ") "Phase 1 (" (code "provide/context/emit!") ") and "
|
||||
"Phase 2 (" (code "scope") " unification) are complete. "
|
||||
"574 tests pass. All four adapters use " (code "scope-push!/scope-pop!") ". "
|
||||
(code "collect!") " is backed by lazy scopes with dedup. "
|
||||
"Phase 3 (extensible handlers) is the research frontier — "
|
||||
"it may turn out that three modes are sufficient, or it may turn out that "
|
||||
"user-defined modes unlock something unexpected.")))))
|
||||
|
||||
250
sx/sx/provide.sx
Normal file
250
sx/sx/provide.sx
Normal file
@@ -0,0 +1,250 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Provide / Context / Emit! — render-time dynamic scope
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
;; ---- Demo components ----
|
||||
|
||||
(defcomp ~geography/demo-provide-basic ()
|
||||
(div :class "space-y-2"
|
||||
(provide "theme" {:primary "violet" :accent "rose"}
|
||||
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
|
||||
(p :class "text-sm text-violet-800 font-semibold" "Inside provider: theme.primary = violet")
|
||||
(p :class "text-xs text-stone-500" "Child reads context value without prop threading.")))
|
||||
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
|
||||
(p :class "text-sm text-stone-600" "Outside provider: no theme context."))))
|
||||
|
||||
(defcomp ~geography/demo-emit-collect ()
|
||||
(div :class "space-y-2"
|
||||
(provide "scripts" nil
|
||||
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
|
||||
(p :class "text-sm text-stone-700"
|
||||
(emit! "scripts" "analytics.js")
|
||||
(emit! "scripts" "charts.js")
|
||||
"Page content renders here. Scripts emitted silently."))
|
||||
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
|
||||
(p :class "text-sm text-violet-800 font-semibold" "Emitted scripts:")
|
||||
(ul :class "text-xs text-stone-600 list-disc pl-5"
|
||||
(map (fn (s) (li (code s))) (emitted "scripts")))))))
|
||||
|
||||
(defcomp ~geography/demo-spread-mechanism ()
|
||||
(div :class "space-y-2"
|
||||
(div (make-spread {:class "rounded-lg p-3 bg-rose-50 border border-rose-200"})
|
||||
(p :class "text-sm text-rose-800 font-semibold" "Spread child styled this div")
|
||||
(p :class "text-xs text-stone-500" "The spread emitted into the element-attrs provider."))
|
||||
(let ((card (make-spread {:class "rounded-lg p-3 bg-amber-50 border border-amber-200"})))
|
||||
(div card
|
||||
(p :class "text-sm text-amber-800 font-semibold" "Stored spread, same mechanism")
|
||||
(p :class "text-xs text-stone-500" "Bound to a let variable, applied when rendered as child.")))))
|
||||
|
||||
(defcomp ~geography/demo-nested-provide ()
|
||||
(div :class "space-y-2"
|
||||
(provide "level" "outer"
|
||||
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
|
||||
(p :class "text-sm text-stone-700"
|
||||
(str "Level: " (context "level")))
|
||||
(provide "level" "inner"
|
||||
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200 ml-4"
|
||||
(p :class "text-sm text-violet-700"
|
||||
(str "Level: " (context "level")))))
|
||||
(p :class "text-sm text-stone-500 mt-1"
|
||||
(str "Back to: " (context "level")))))))
|
||||
|
||||
|
||||
;; ---- Layout helper (reuse from spreads article) ----
|
||||
|
||||
(defcomp ~geography/provide-demo-example (&key demo code)
|
||||
(div :class "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start"
|
||||
(div :class "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]"
|
||||
demo)
|
||||
(div :class "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto"
|
||||
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code)))))
|
||||
|
||||
|
||||
;; ---- Page content ----
|
||||
|
||||
(defcomp ~geography/provide-content ()
|
||||
(~docs/page :title "Provide / Context / Emit!"
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"Sugar for " (code "scope") " with a value. " (code "provide") " creates a named scope "
|
||||
"with a value and an accumulator. " (code "context") " reads the value downward. "
|
||||
(code "emit!") " appends to the accumulator upward. " (code "emitted") " retrieves what was emitted. "
|
||||
"See "
|
||||
(a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
|
||||
" for the unified primitive.")
|
||||
|
||||
;; =====================================================================
|
||||
;; I. The four primitives
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Four primitives" :id "primitives"
|
||||
|
||||
(~docs/subsection :title "provide (special form)"
|
||||
(p (code "provide") " creates a named scope with a value and an empty accumulator. "
|
||||
"The body expressions execute with the scope active. When the body completes, "
|
||||
"the scope is popped.")
|
||||
(~docs/code :code (highlight "(provide name value\n body...)\n\n;; Example: theme context\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"Title\") ;; can read (context \"theme\")\n (p \"Body\")) ;; scope active for all children" "lisp"))
|
||||
(p (code "provide") " is a special form, not a function — the body is evaluated "
|
||||
"inside the scope, not before it."))
|
||||
|
||||
(~docs/subsection :title "context"
|
||||
(p "Reads the value from the nearest enclosing " (code "provide") " with the given name. "
|
||||
"Errors if no provider and no default given.")
|
||||
(~docs/code :code (highlight "(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (get (context \"theme\") :primary)) ;; → \"violet\"\n\n;; With default (no error when missing):\n(context \"theme\" {:primary \"stone\"}) ;; → {:primary \"stone\"}" "lisp")))
|
||||
|
||||
(~docs/subsection :title "emit!"
|
||||
(p "Appends a value to the nearest enclosing provider's accumulator. "
|
||||
"Tolerant: returns nil silently when no provider exists.")
|
||||
(~docs/code :code (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"analytics.js\")\n (emit! \"scripts\" \"charts.js\")\n ;; accumulator now has both scripts\n )\n\n;; Outside any provider — silently does nothing:\n(emit! \"scripts\" \"orphan.js\") ;; → nil, no error" "lisp"))
|
||||
(p "Tolerance is critical. Spreads emit into " (code "\"element-attrs\"")
|
||||
" — but a spread might be evaluated in a fragment, a " (code "begin") " block, or a "
|
||||
(code "map") " call where no element provider exists. "
|
||||
"Tolerant " (code "emit!") " means these cases silently vanish instead of crashing."))
|
||||
|
||||
(~docs/subsection :title "emitted"
|
||||
(p "Returns the list of values emitted into the nearest provider with the given name. "
|
||||
"Empty list if no provider.")
|
||||
(~docs/code :code (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"a.js\")\n (emit! \"scripts\" \"b.js\")\n (emitted \"scripts\")) ;; → (\"a.js\" \"b.js\")" "lisp"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; II. Two directions, one mechanism
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Two directions, one mechanism" :id "directions"
|
||||
(p (code "provide") " serves both downward and upward communication through a single scope.")
|
||||
|
||||
(~docs/table
|
||||
:headers (list "Direction" "Read with" "Write with" "Example")
|
||||
:rows (list
|
||||
(list "Downward (scope → child)" "context" "provide value" "Theme, config, locale")
|
||||
(list "Upward (child → scope)" "emitted" "emit!" "Script collection, spread attrs")))
|
||||
|
||||
(~geography/provide-demo-example
|
||||
:demo (~geography/demo-provide-basic)
|
||||
:code (highlight ";; Downward: theme context\n(provide \"theme\"\n {:primary \"violet\" :accent \"rose\"}\n (h1 :style (str \"color:\"\n (get (context \"theme\") :primary))\n \"Themed heading\")\n (p \"inherits theme context\"))" "lisp"))
|
||||
|
||||
(~geography/provide-demo-example
|
||||
:demo (~geography/demo-emit-collect)
|
||||
:code (highlight ";; Upward: script accumulation\n(provide \"scripts\" nil\n (div\n (emit! \"scripts\" \"analytics.js\")\n (div\n (emit! \"scripts\" \"charts.js\")\n \"chart\"))\n ;; Collect at the boundary:\n (for-each (fn (s)\n (script :src s))\n (emitted \"scripts\")))" "lisp")))
|
||||
|
||||
;; =====================================================================
|
||||
;; III. How spreads use it
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "How spreads use provide/emit!" :id "spreads"
|
||||
(p "Every element rendering function wraps its children in a provider scope "
|
||||
"named " (code "\"element-attrs\"") ". When the adapter encounters a spread child, "
|
||||
"it emits the spread's attrs into this scope. After all children render, the "
|
||||
"element collects and merges the emitted attrs.")
|
||||
|
||||
(~geography/provide-demo-example
|
||||
:demo (~geography/demo-spread-mechanism)
|
||||
:code (highlight ";; Spread = emit! into element-attrs\n(div (make-spread {:class \"card\"})\n \"hello\")\n\n;; Internally:\n;; 1. div opens provider:\n;; (provide-push! \"element-attrs\" nil)\n;; 2. spread child emits:\n;; (emit! \"element-attrs\"\n;; {:class \"card\"})\n;; 3. div collects + merges:\n;; (emitted \"element-attrs\")\n;; → ({:class \"card\"})\n;; 4. (provide-pop! \"element-attrs\")\n;; Result: <div class=\"card\">hello</div>" "lisp"))
|
||||
|
||||
(~docs/subsection :title "Why this matters"
|
||||
(p "Before the refactor, every intermediate form in the render pipeline — "
|
||||
"fragments, " (code "let") ", " (code "begin") ", " (code "map") ", "
|
||||
(code "for-each") ", " (code "when") ", " (code "cond") ", component children — "
|
||||
"needed an explicit " (code "(filter (fn (r) (not (spread? r))) ...)") " to strip "
|
||||
"spread values from rendered output. Over 25 such filters existed across the four adapters.")
|
||||
(p "With provide/emit!, all of these disappear. Spreads emit into the nearest element's "
|
||||
"scope regardless of how many layers of control flow they pass through. Non-element "
|
||||
"contexts have no provider, so " (code "emit!") " is a silent no-op.")))
|
||||
|
||||
;; =====================================================================
|
||||
;; IV. Nested scoping
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Nested scoping" :id "nesting"
|
||||
(p "Providers stack. Each " (code "provide") " pushes onto a per-name stack; "
|
||||
"the closest one wins. This gives lexical-style scoping at render time.")
|
||||
|
||||
(~geography/provide-demo-example
|
||||
:demo (~geography/demo-nested-provide)
|
||||
:code (highlight "(provide \"level\" \"outer\"\n (context \"level\") ;; → \"outer\"\n (provide \"level\" \"inner\"\n (context \"level\")) ;; → \"inner\"\n (context \"level\")) ;; → \"outer\" again" "lisp"))
|
||||
|
||||
(p "For " (code "emit!") ", this means emissions go to the " (em "nearest") " provider. "
|
||||
"A spread inside a nested element emits to that element, not an ancestor.")
|
||||
(~docs/code :code (highlight ";; Nested elements = nested providers\n(div ;; provider A\n (span ;; provider B\n (make-spread {:class \"inner\"})) ;; emits to B\n (make-spread {:class \"outer\"})) ;; emits to A\n;; → <div class=\"outer\"><span class=\"inner\"></span></div>" "lisp")))
|
||||
|
||||
;; =====================================================================
|
||||
;; V. Across all adapters
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Across all adapters" :id "adapters"
|
||||
(p "The provide/emit! mechanism works identically across all four rendering adapters. "
|
||||
"The element rendering pattern is the same; only the output format differs.")
|
||||
|
||||
(~docs/table
|
||||
:headers (list "Adapter" "Element render" "Spread dispatch")
|
||||
:rows (list
|
||||
(list "HTML (server)" "provide-push! → render children → merge emitted → provide-pop!" "(emit! \"element-attrs\" (spread-attrs expr)) → \"\"")
|
||||
(list "Async (server)" "Same pattern, with await on child rendering" "Same dispatch")
|
||||
(list "SX wire (aser)" "provide-push! → serialize children → merge emitted as :key attrs → provide-pop!" "(emit! \"element-attrs\" (spread-attrs expr)) → nil")
|
||||
(list "DOM (browser)" "provide-push! → reduce children → merge emitted onto DOM element → provide-pop!" "emit! + keep value for reactive-spread detection")))
|
||||
|
||||
(~docs/subsection :title "DOM adapter: reactive-spread preserved"
|
||||
(p "In the DOM adapter, spread children inside islands are still checked individually "
|
||||
"for signal dependencies. " (code "reactive-spread") " tracks signal deps and "
|
||||
"surgically updates attributes when signals change. The static path uses provide/emit!; "
|
||||
"the reactive path wraps it in an effect.")
|
||||
(p "See the "
|
||||
(a :href "/sx/(geography.(spreads))" :class "text-violet-600 hover:underline" "spreads article")
|
||||
" for reactive-spread details.")))
|
||||
|
||||
;; =====================================================================
|
||||
;; VI. Comparison with collect!
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Comparison with collect! / collected" :id "comparison"
|
||||
(~docs/table
|
||||
:headers (list "" "provide / emit!" "collect! / collected")
|
||||
:rows (list
|
||||
(list "Scope" "Lexical (nearest enclosing provide)" "Global (render-wide)")
|
||||
(list "Deduplication" "None — every emit! appends" "Automatic (same value skipped)")
|
||||
(list "Multiple scopes" "Yes — nested provides shadow" "No — single global bucket per name")
|
||||
(list "Downward data" "Yes (context)" "No")
|
||||
(list "Used by" "Spreads (element-attrs)" "CSSX rule accumulation")))
|
||||
|
||||
(p (code "collect!") " remains the right tool for CSS rule accumulation — deduplication "
|
||||
"matters there, and rules need to reach the layout root regardless of nesting depth. "
|
||||
(code "emit!") " is right for spread attrs — no dedup needed, and each element only "
|
||||
"wants attrs from its direct children."))
|
||||
|
||||
;; =====================================================================
|
||||
;; VII. Platform implementation
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Platform implementation" :id "platform"
|
||||
(p (code "provide") " is sugar for " (code "scope") ". At the platform level, "
|
||||
(code "provide-push!") " and " (code "provide-pop!") " are aliases for "
|
||||
(code "scope-push!") " and " (code "scope-pop!") ". All operations work on a unified "
|
||||
(code "_scope_stacks") " data structure.")
|
||||
|
||||
(~docs/table
|
||||
:headers (list "Platform primitive" "Purpose")
|
||||
:rows (list
|
||||
(list "scope-push!(name, value)" "Push a new scope with value and empty accumulator")
|
||||
(list "scope-pop!(name)" "Pop the most recent scope")
|
||||
(list "context(name, ...default)" "Read value from nearest scope (error if missing and no default)")
|
||||
(list "emit!(name, value)" "Append to nearest scope's accumulator (tolerant: no-op if missing)")
|
||||
(list "emitted(name)" "Return accumulated values from nearest scope")))
|
||||
|
||||
(p (code "provide") " is a special form in "
|
||||
(a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx")
|
||||
" — it calls " (code "scope-push!") ", evaluates the body, "
|
||||
"then calls " (code "scope-pop!") ". See "
|
||||
(a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
|
||||
" for the full unified platform.")
|
||||
|
||||
(~docs/note
|
||||
(p (strong "Spec explorer: ") "See the provide/emit! primitives in "
|
||||
(a :href "/sx/(language.(spec.(explore.boundary)))" :class "font-mono text-violet-600 hover:underline text-sm" "boundary.sx explorer")
|
||||
". The " (code "provide") " special form is in "
|
||||
(a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx explorer")
|
||||
". Element rendering with provide/emit! is visible in "
|
||||
(a :href "/sx/(language.(spec.(explore.adapter-html)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-html")
|
||||
" and "
|
||||
(a :href "/sx/(language.(spec.(explore.adapter-async)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-async")
|
||||
".")))))
|
||||
@@ -11,73 +11,73 @@
|
||||
|
||||
(~docs/section :title "1. Signal + Computed + Effect" :id "demo-counter"
|
||||
(p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.")
|
||||
(~reactive-islands/demo/counter :initial 0)
|
||||
(~reactive-islands/index/demo-counter :initial 0)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"−\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp"))
|
||||
(p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing."))
|
||||
|
||||
(~docs/section :title "2. Temperature Converter" :id "demo-temperature"
|
||||
(p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.")
|
||||
(~reactive-islands/demo/temperature)
|
||||
(~reactive-islands/index/demo-temperature)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"−5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp"))
|
||||
(p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes."))
|
||||
|
||||
(~docs/section :title "3. Effect + Cleanup: Stopwatch" :id "demo-stopwatch"
|
||||
(p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.")
|
||||
(~reactive-islands/demo/stopwatch)
|
||||
(~reactive-islands/index/demo-stopwatch)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp"))
|
||||
(p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order."))
|
||||
|
||||
(~docs/section :title "4. Imperative Pattern" :id "demo-imperative"
|
||||
(p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".")
|
||||
(~reactive-islands/demo/imperative)
|
||||
(~reactive-islands/index/demo-imperative)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp"))
|
||||
(p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates."))
|
||||
|
||||
(~docs/section :title "5. Reactive List" :id "demo-reactive-list"
|
||||
(p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.")
|
||||
(~reactive-islands/demo/reactive-list)
|
||||
(~reactive-islands/index/demo-reactive-list)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp"))
|
||||
(p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass."))
|
||||
|
||||
(~docs/section :title "6. Input Binding" :id "demo-input-binding"
|
||||
(p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.")
|
||||
(~reactive-islands/demo/input-binding)
|
||||
(~reactive-islands/index/demo-input-binding)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp"))
|
||||
(p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump."))
|
||||
|
||||
(~docs/section :title "7. Portals" :id "demo-portal"
|
||||
(p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.")
|
||||
(~reactive-islands/demo/portal)
|
||||
(~reactive-islands/index/demo-portal)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
|
||||
(p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup."))
|
||||
|
||||
(~docs/section :title "8. Error Boundaries" :id "demo-error-boundary"
|
||||
(p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.")
|
||||
(~reactive-islands/demo/error-boundary)
|
||||
(~reactive-islands/index/demo-error-boundary)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp"))
|
||||
(p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") "."))
|
||||
|
||||
(~docs/section :title "9. Refs — Imperative DOM Access" :id "demo-refs"
|
||||
(p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.")
|
||||
(~reactive-islands/demo/refs)
|
||||
(~reactive-islands/index/demo-refs)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp"))
|
||||
(p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") "."))
|
||||
|
||||
(~docs/section :title "10. Dynamic Class and Style" :id "demo-dynamic-class"
|
||||
(p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.")
|
||||
(~reactive-islands/demo/dynamic-class)
|
||||
(~reactive-islands/index/demo-dynamic-class)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp"))
|
||||
(p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string."))
|
||||
|
||||
(~docs/section :title "11. Resource + Suspense Pattern" :id "demo-resource"
|
||||
(p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.")
|
||||
(~reactive-islands/demo/resource)
|
||||
(~reactive-islands/index/demo-resource)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp"))
|
||||
(p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically."))
|
||||
|
||||
(~docs/section :title "12. Transition Pattern" :id "demo-transition"
|
||||
(p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.")
|
||||
(~reactive-islands/demo/transition)
|
||||
(~reactive-islands/index/demo-transition)
|
||||
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp"))
|
||||
(p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations."))
|
||||
|
||||
|
||||
194
sx/sx/scopes.sx
Normal file
194
sx/sx/scopes.sx
Normal file
@@ -0,0 +1,194 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Scopes — the unified primitive beneath provide, collect!, and spreads
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
;; ---- Demo components ----
|
||||
|
||||
(defcomp ~geography/demo-scope-basic ()
|
||||
(div :class "space-y-2"
|
||||
(scope "demo-theme" :value "violet"
|
||||
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
|
||||
(p :class "text-sm text-violet-800 font-semibold"
|
||||
(str "Inside scope: theme = " (context "demo-theme")))
|
||||
(p :class "text-xs text-stone-500" "scope creates a named scope. context reads it.")))
|
||||
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
|
||||
(p :class "text-sm text-stone-600" "Outside scope: no context available."))))
|
||||
|
||||
(defcomp ~geography/demo-scope-emit ()
|
||||
(div :class "space-y-2"
|
||||
(scope "demo-deps"
|
||||
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
|
||||
(p :class "text-sm text-stone-700"
|
||||
(emit! "demo-deps" "lodash")
|
||||
(emit! "demo-deps" "react")
|
||||
"Components emit their dependencies upward."))
|
||||
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
|
||||
(p :class "text-sm text-violet-800 font-semibold" "Emitted:")
|
||||
(ul :class "text-xs text-stone-600 list-disc pl-5"
|
||||
(map (fn (d) (li (code d))) (emitted "demo-deps")))))))
|
||||
|
||||
(defcomp ~geography/demo-scope-dedup ()
|
||||
(div :class "space-y-2"
|
||||
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
|
||||
(p :class "text-sm text-stone-700"
|
||||
(collect! "demo-css-dedup" ".card { padding: 1rem }")
|
||||
(collect! "demo-css-dedup" ".card { padding: 1rem }")
|
||||
(collect! "demo-css-dedup" ".btn { color: blue }")
|
||||
"Three collect! calls, two identical. Only unique values kept."))
|
||||
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
|
||||
(p :class "text-sm text-violet-800 font-semibold"
|
||||
(str "Collected: " (len (collected "demo-css-dedup")) " rules"))
|
||||
(ul :class "text-xs text-stone-600 list-disc pl-5"
|
||||
(map (fn (r) (li (code r))) (collected "demo-css-dedup"))))))
|
||||
|
||||
|
||||
;; ---- Layout helper ----
|
||||
|
||||
(defcomp ~geography/scopes-demo-example (&key demo code)
|
||||
(div :class "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start"
|
||||
(div :class "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]"
|
||||
demo)
|
||||
(div :class "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto"
|
||||
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code)))))
|
||||
|
||||
|
||||
;; ---- Page content ----
|
||||
|
||||
(defcomp ~geography/scopes-content ()
|
||||
(~docs/page :title "Scopes"
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"The unified primitive. " (code "scope") " creates a named scope with an optional value "
|
||||
"and an accumulator. " (code "provide") ", " (code "collect!") ", spreads, islands — "
|
||||
"they all resolve to scope operations at the platform level.")
|
||||
|
||||
;; =====================================================================
|
||||
;; I. The primitive
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "The primitive" :id "primitive"
|
||||
|
||||
(p (code "scope") " is a special form that pushes a named scope, evaluates its body, "
|
||||
"then pops it. The scope has three properties: a name, a downward value, and an "
|
||||
"upward accumulator.")
|
||||
|
||||
(~docs/code :code (highlight "(scope name body...) ;; scope with no value\n(scope name :value v body...) ;; scope with downward value" "lisp"))
|
||||
|
||||
(p "Within the body, " (code "context") " reads the value, " (code "emit!") " appends "
|
||||
"to the accumulator, and " (code "emitted") " reads what was accumulated.")
|
||||
|
||||
(~geography/scopes-demo-example
|
||||
:demo (~geography/demo-scope-basic)
|
||||
:code (highlight "(scope \"theme\" :value \"violet\"\n (context \"theme\")) ;; → \"violet\"\n\n;; Nested scopes shadow:\n(scope \"x\" :value \"outer\"\n (scope \"x\" :value \"inner\"\n (context \"x\")) ;; → \"inner\"\n (context \"x\")) ;; → \"outer\"" "lisp")))
|
||||
|
||||
;; =====================================================================
|
||||
;; II. Sugar forms
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Sugar forms" :id "sugar"
|
||||
|
||||
(p "Nobody writes " (code "scope") " directly. The sugar forms are the API:")
|
||||
|
||||
(~docs/table
|
||||
:headers (list "Sugar" "Expands to" "Used for")
|
||||
:rows (list
|
||||
(list "provide" "(scope name :value v body...)" "Downward context passing")
|
||||
(list "collect!" "Lazy root scope + dedup emit" "CSS rule accumulation")
|
||||
(list "Spreads" "(scope \"element-attrs\" ...)" "Child-to-parent attrs (implicit)")))
|
||||
|
||||
(~docs/subsection :title "provide — scope with a value"
|
||||
(p (code "(provide name value body...)") " is exactly "
|
||||
(code "(scope name :value value body...)") ". It exists because "
|
||||
"the two-arg form is the common case.")
|
||||
(~docs/code :code (highlight ";; These are equivalent:\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"hello\"))\n\n(scope \"theme\" :value {:primary \"violet\"}\n (h1 \"hello\"))" "lisp")))
|
||||
|
||||
(~docs/subsection :title "collect! — lazy root scope with dedup"
|
||||
(p (code "collect!") " is the most interesting sugar. When called, if no scope exists "
|
||||
"for that name, it lazily creates a root scope with deduplication enabled. "
|
||||
"Then it emits into it.")
|
||||
(~geography/scopes-demo-example
|
||||
:demo (~geography/demo-scope-dedup)
|
||||
:code (highlight ";; collect! creates a lazy root scope:\n(collect! \"css\" \".card { pad: 1rem }\")\n(collect! \"css\" \".card { pad: 1rem }\") ;; deduped!\n(collect! \"css\" \".btn { color: blue }\")\n(collected \"css\") ;; → 2 rules\n\n;; Equivalent to:\n(scope \"css\" ;; with dedup\n (emit! \"css\" ...)\n (emitted \"css\"))" "lisp"))
|
||||
(p (code "collected") " is an alias for " (code "emitted") ". "
|
||||
(code "clear-collected!") " clears the accumulator."))
|
||||
|
||||
(~docs/subsection :title "Spreads — implicit element scope"
|
||||
(p "Every element rendering function wraps its children in "
|
||||
(code "(scope-push! \"element-attrs\" nil)") ". Spread children "
|
||||
(code "emit!") " their attrs into this scope. After rendering, the element "
|
||||
"merges the emitted attrs.")
|
||||
(p "See the "
|
||||
(a :href "/sx/(geography.(spreads))" :class "text-violet-600 hover:underline" "spreads article")
|
||||
" for the full mechanism.")))
|
||||
|
||||
;; =====================================================================
|
||||
;; III. Accumulator: upward data flow
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Upward data flow" :id "upward"
|
||||
|
||||
(~geography/scopes-demo-example
|
||||
:demo (~geography/demo-scope-emit)
|
||||
:code (highlight "(scope \"deps\"\n (emit! \"deps\" \"lodash\")\n (emit! \"deps\" \"react\")\n (emitted \"deps\")) ;; → (\"lodash\" \"react\")" "lisp"))
|
||||
|
||||
(p "Accumulation always goes to the " (em "nearest") " enclosing scope with that name. "
|
||||
"This is what makes nested elements work — a spread inside a nested "
|
||||
(code "span") " emits to the " (code "span") "'s scope, not an outer "
|
||||
(code "div") "'s scope."))
|
||||
|
||||
;; =====================================================================
|
||||
;; IV. Platform implementation
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Platform implementation" :id "platform"
|
||||
|
||||
(p "Each platform (Python, JavaScript) maintains a single data structure:")
|
||||
|
||||
(~docs/code :code (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python"))
|
||||
|
||||
(p "Six operations on this structure:")
|
||||
|
||||
(~docs/table
|
||||
:headers (list "Operation" "Purpose")
|
||||
:rows (list
|
||||
(list "scope-push!(name, value)" "Push {value, emitted: [], dedup: false}")
|
||||
(list "scope-pop!(name)" "Pop the most recent scope")
|
||||
(list "context(name, default?)" "Read value from nearest scope")
|
||||
(list "emit!(name, value)" "Append to nearest scope's accumulator (respects dedup)")
|
||||
(list "emitted(name)" "Read accumulated values from nearest scope")
|
||||
(list "collect!(name, value)" "Lazy push root scope with dedup, then emit")))
|
||||
|
||||
(p (code "provide-push!") " and " (code "provide-pop!") " are aliases for "
|
||||
(code "scope-push!") " and " (code "scope-pop!") ". "
|
||||
"All adapter code uses " (code "scope-push!") "/" (code "scope-pop!") " directly."))
|
||||
|
||||
;; =====================================================================
|
||||
;; V. Unification
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "What scope unifies" :id "unification"
|
||||
|
||||
(p "Before scopes, the platform had two separate mechanisms:")
|
||||
|
||||
(~docs/code :code (highlight ";; Before: two mechanisms\n_provide_stacks = {} ;; {name: [{value, emitted: []}]}\n_collect_buckets = {} ;; {name: [values...]}\n\n;; After: one mechanism\n_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python"))
|
||||
|
||||
(p "The unification is not just code cleanup. It means:")
|
||||
(ul :class "space-y-1"
|
||||
(li (code "collect!") " can be nested inside " (code "provide") " scopes — "
|
||||
"they share the same stack.")
|
||||
(li "A component can " (code "emit!") " and " (code "collect!") " into the same scope — "
|
||||
"they use the same accumulator.")
|
||||
(li "The dedup flag is per-scope, not per-mechanism — a " (code "provide") " scope "
|
||||
"has no dedup, a " (code "collect!") " root scope has dedup."))
|
||||
|
||||
(p "See the "
|
||||
(a :href "/sx/(etc.(plan.scoped-effects))" :class "text-violet-600 hover:underline" "scoped effects plan")
|
||||
" for the full design rationale and future phases (reactive scopes, morph scopes).")
|
||||
|
||||
(~docs/note
|
||||
(p (strong "Spec: ") "The " (code "scope") " special form is in "
|
||||
(a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx")
|
||||
". Platform primitives are declared in "
|
||||
(a :href "/sx/(language.(spec.(explore.boundary)))" :class "font-mono text-violet-600 hover:underline text-sm" "boundary.sx")
|
||||
" (Tier 5: Scoped effects).")))))
|
||||
153
sx/sx/spreads.sx
153
sx/sx/spreads.sx
@@ -89,39 +89,57 @@
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"A spread is a value that, when returned as a child of an element, "
|
||||
"injects attributes onto its parent instead of rendering as content. "
|
||||
"This inverts the normal direction of data flow: children tell parents how to look.")
|
||||
"Internally, spreads work through "
|
||||
(a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
|
||||
" — every element creates a scope, and spread children emit into it.")
|
||||
|
||||
;; =====================================================================
|
||||
;; I. The primitives
|
||||
;; I. How it works
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Three primitives" :id "primitives"
|
||||
(p "The spread system has three orthogonal primitives. Each operates at a "
|
||||
"different level of the render pipeline.")
|
||||
(~docs/section :title "How it works" :id "mechanism"
|
||||
(p "Every element wraps its children in a " (code "provide") " scope named "
|
||||
(code "\"element-attrs\"") ". When the renderer encounters a spread child, "
|
||||
"it calls " (code "emit!") " to push the spread's attrs into that scope. "
|
||||
"After all children render, the element collects the emitted attrs and merges them. "
|
||||
"This is the " (a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline"
|
||||
"provide/emit!") " mechanism — spreads are one instance of it.")
|
||||
(p "The user-facing API is " (code "make-spread") " — it creates a spread value from a dict. "
|
||||
(code "spread?") " tests for one. " (code "spread-attrs") " extracts the dict. "
|
||||
"But the actual delivery from child to parent goes through provide/emit!, not through "
|
||||
"return-value inspection.")
|
||||
|
||||
(~docs/subsection :title "1. make-spread / spread? / spread-attrs"
|
||||
(p "A spread is a value type. " (code "make-spread") " creates one from a dict of "
|
||||
"attributes. When the renderer encounters a spread as a child of an element, "
|
||||
"it merges the attrs onto the parent element instead of appending a DOM node.")
|
||||
(~geography/demo-example
|
||||
:demo (~geography/demo-spread-basic)
|
||||
:code (highlight "(defcomp ~callout (&key type)\n (make-spread\n (cond\n (= type \"info\")\n {\"style\" \"border-left:4px solid\n #3b82f6; background:#eff6ff\"}\n (= type \"warning\")\n {\"style\" \"border-left:4px solid\n #f59e0b; background:#fffbeb\"}\n (= type \"success\")\n {\"style\" \"border-left:4px solid\n #10b981; background:#ecfdf5\"})))\n\n;; Child injects attrs onto parent:\n(div (~callout :type \"info\")\n \"This div gets the callout style.\")" "lisp"))
|
||||
|
||||
(~geography/demo-example
|
||||
:demo (~geography/demo-spread-basic)
|
||||
:code (highlight "(defcomp ~callout (&key type)\n (make-spread\n (cond\n (= type \"info\")\n {\"style\" \"border-left:4px solid\n #3b82f6; background:#eff6ff\"}\n (= type \"warning\")\n {\"style\" \"border-left:4px solid\n #f59e0b; background:#fffbeb\"}\n (= type \"success\")\n {\"style\" \"border-left:4px solid\n #10b981; background:#ecfdf5\"})))\n\n;; Child injects attrs onto parent:\n(div (~callout :type \"info\")\n \"This div gets the callout style.\")" "lisp"))
|
||||
(p (code "class") " values are appended (space-joined). "
|
||||
(code "style") " values are appended (semicolon-joined). "
|
||||
"All other attributes overwrite.")
|
||||
|
||||
(p (code "class") " values are appended (space-joined). "
|
||||
(code "style") " values are appended (semicolon-joined). "
|
||||
"All other attributes overwrite."))
|
||||
(p "Tolerant " (code "emit!") " means a spread outside any element context "
|
||||
"(in a fragment, " (code "begin") ", or bare " (code "map") ") silently vanishes "
|
||||
"instead of crashing — there's no provider to emit into, so it's a no-op."))
|
||||
|
||||
(~docs/subsection :title "2. collect! / collected / clear-collected!"
|
||||
(p "Render-time accumulators. Values are collected into named buckets "
|
||||
"during rendering and retrieved at flush points. Deduplication is automatic.")
|
||||
(~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"<style>\" (join \"\" rules) \"</style>\")))" "lisp"))
|
||||
(p "This is upward communication through the render tree: "
|
||||
"a deeply nested component contributes a CSS rule, and the layout "
|
||||
"emits all accumulated rules as a single " (code "<style>") " tag. "
|
||||
"No prop threading, no context providers, no global state."))
|
||||
;; =====================================================================
|
||||
;; II. collect! — the other upward channel
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/subsection :title "3. reactive-spread (islands)"
|
||||
(~docs/section :title "collect! — the other upward channel" :id "collect"
|
||||
(p "Spreads use " (code "scope") "/" (code "emit!") " (scoped, no dedup). "
|
||||
(code "collect!") "/" (code "collected") " is also backed by scopes — "
|
||||
"a lazy root scope with automatic deduplication. Used for CSS rule accumulation.")
|
||||
(~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"<style>\" (join \"\" rules) \"</style>\")))" "lisp"))
|
||||
(p "Both are upward communication through the render tree, but with different "
|
||||
"semantics — " (code "emit!") " is scoped to the nearest provider, "
|
||||
(code "collect!") " is global and deduplicates."))
|
||||
|
||||
;; =====================================================================
|
||||
;; III. Reactive spreads (islands)
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Reactive spreads" :id "reactive"
|
||||
(~docs/subsection :title "reactive-spread (islands)"
|
||||
(p "Inside an island, when a spread's value depends on signals, "
|
||||
(code "reactive-spread") " tracks signal dependencies and surgically "
|
||||
"updates the parent element's attributes when signals change.")
|
||||
@@ -138,12 +156,12 @@
|
||||
(li "No re-render. No VDOM. No diffing. Just attr surgery."))))
|
||||
|
||||
;; =====================================================================
|
||||
;; II. Orthogonality
|
||||
;; IV. Orthogonality
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Orthogonality across boundaries" :id "orthogonality"
|
||||
(p "The three primitives operate at different levels of the render pipeline. "
|
||||
"They compose without knowing about each other.")
|
||||
(p "Spread (via provide/emit!), collect!, and reactive-spread operate at different "
|
||||
"levels of the render pipeline. They compose without knowing about each other.")
|
||||
|
||||
(~docs/table
|
||||
:headers (list "Primitive" "Direction" "Boundary" "When")
|
||||
@@ -179,11 +197,11 @@
|
||||
(list "Client rendering" "spread in wire format (aser)" "reactive-spread (live)")))))
|
||||
|
||||
;; =====================================================================
|
||||
;; III. CSSX as use case
|
||||
;; V. CSSX as use case
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "CSSX: the first application" :id "cssx"
|
||||
(p (code "~cssx/tw") " is a component that uses all three primitives:")
|
||||
(p (code "~cssx/tw") " is the primary consumer of spreads:")
|
||||
|
||||
(~geography/demo-example
|
||||
:demo (~geography/demo-cssx-tw)
|
||||
@@ -193,7 +211,7 @@
|
||||
(code "collect!") " to accumulate CSS rules for batch flushing, and "
|
||||
"when called inside an island with signal-dependent tokens, "
|
||||
(code "reactive-spread") " makes it live.")
|
||||
(p "But " (code "~cssx/tw") " is just one instance. The same primitives enable:")
|
||||
(p "But " (code "~cssx/tw") " is just one instance. The same mechanism enables:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
|
||||
(li (code "~aria") " — reactive accessibility attributes driven by UI state")
|
||||
(li (code "~data-attrs") " — signal-driven data attributes for coordination")
|
||||
@@ -201,7 +219,7 @@
|
||||
(li "Any component that needs to inject attributes onto its parent")))
|
||||
|
||||
;; =====================================================================
|
||||
;; IV. Semantic variables
|
||||
;; VI. Semantic variables
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Semantic style variables" :id "variables"
|
||||
@@ -218,7 +236,7 @@
|
||||
(code "~admin/heading") " are different components in different namespaces."))
|
||||
|
||||
;; =====================================================================
|
||||
;; V. What nothing else does
|
||||
;; VII. What nothing else does
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "What nothing else does" :id "unique"
|
||||
@@ -229,52 +247,43 @@
|
||||
(p "CSS-in-JS libraries (styled-components, Emotion) create " (em "wrapper elements")
|
||||
". They don't inject attrs onto an existing element from a child position. "
|
||||
"And they need a build step, a runtime, a theme provider.")
|
||||
(p "SX does it with three orthogonal primitives that already existed for other reasons:")
|
||||
(p "SX does it with " (code "provide") "/" (code "emit!") " — render-time dynamic scope "
|
||||
"that already existed for other reasons:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
|
||||
(li (strong "spread") " — child-to-parent attr injection (existed for component composition)")
|
||||
(li (strong "collect!") " — render-time accumulation (existed for CSS rule batching)")
|
||||
(li (strong "reactive-spread") " — just the obvious combination of spread + effect"))
|
||||
(li (strong "provide/emit!") " — scoped upward communication (the general mechanism)")
|
||||
(li (strong "make-spread") " — creates spread values that emit into element-attrs providers")
|
||||
(li (strong "reactive-spread") " — wraps emit! in a signal effect for live updates"))
|
||||
(p "No new concepts. No new runtime. No new API surface."))
|
||||
|
||||
;; =====================================================================
|
||||
;; VI. The deeper primitive
|
||||
;; VIII. The general primitive
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "The deeper primitive: provide / context / emit!" :id "provide"
|
||||
(p "Spread and collect are both instances of the same pattern: "
|
||||
(strong "child communicates upward through the render tree") ". "
|
||||
"The general form is " (code "provide") "/" (code "context") "/" (code "emit!")
|
||||
" — render-time dynamic scope.")
|
||||
(~docs/section :title "The general primitive" :id "provide"
|
||||
(p "Spreads, collect!, and context are all instances of one mechanism: "
|
||||
(code "provide") "/" (code "emit!") " — render-time dynamic scope. "
|
||||
(a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline"
|
||||
"See the full provide/context/emit! article")
|
||||
" for the general primitive, nested scoping, and its other uses.")
|
||||
|
||||
(~docs/subsection :title "The unification"
|
||||
(~docs/table
|
||||
:headers (list "Current" "General form" "Direction")
|
||||
:rows (list
|
||||
(list "collect! / collected" "emit! / emitted" "upward (child → scope)")
|
||||
(list "make-spread" "emit! into implicit parent-attrs provider" "upward (child → parent)")
|
||||
(list "(nothing yet)" "context" "downward (scope → child)")))
|
||||
(p (code "provide") " creates a named scope with a value (downward) and an accumulator (upward). "
|
||||
(code "context") " reads the value. " (code "emit!") " appends to the accumulator. "
|
||||
(code "emitted") " retrieves accumulated values.")
|
||||
(~docs/code :code (highlight ";; Downward: theme context\n(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (h1 :style (str \"color:\" (get (context \"theme\") :primary))\n \"Themed heading\"))\n\n;; Upward: script accumulation (like collect!)\n(provide \"scripts\" nil\n (div\n (emit! \"scripts\" \"analytics.js\")\n (div (emit! \"scripts\" \"charts.js\") \"chart\"))\n (for-each (fn (s) (script :src s)) (emitted \"scripts\")))\n\n;; Both at once\n(provide \"page\" {:title \"Home\"}\n (h1 (context \"page\" :title))\n (emit! \"page\" {:meta \"og:title\" :content \"Home\"})\n (for-each (fn (m) (meta :name (get m :meta) :content (get m :content)))\n (emitted \"page\")))" "lisp")))
|
||||
|
||||
(~docs/subsection :title "What this means"
|
||||
(p "Three mechanisms collapse into one:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
|
||||
(li (code "collect!") " = " (code "emit!") " with deduplication")
|
||||
(li (code "spread") " = " (code "emit!") " into implicit parent-attrs provider")
|
||||
(li (code "collected") " = " (code "emitted"))
|
||||
(li (code "context") " = downward data flow (new capability — no current equivalent)"))
|
||||
(p "The reactive-spread we just built would naturally follow — the effect tracks "
|
||||
"signal deps in the " (code "emit!") " call, the provider accumulates, the element applies.")
|
||||
(p :class "text-stone-500 italic"
|
||||
"This is the planned next step. The current primitives (spread, collect, reactive-spread) "
|
||||
"work and are fully orthogonal. " (code "provide/context/emit!") " will be the deeper "
|
||||
"foundation they are reimplemented on top of."))
|
||||
(~docs/table
|
||||
:headers (list "Mechanism" "General form" "Direction")
|
||||
:rows (list
|
||||
(list "spread" "emit! into element-attrs provider" "upward (child → parent)")
|
||||
(list "collect! / collected" "emit! / emitted (global, deduped)" "upward (child → scope)")
|
||||
(list "theme / config" "context" "downward (scope → child)")))
|
||||
|
||||
(~docs/note
|
||||
(p (strong "Plan: ") (code "provide") "/" (code "context") "/" (code "emit!") " is specced "
|
||||
"and ready to implement. Per-name stacks. Each entry has a value and an emitted list. "
|
||||
"Four primitives: " (code "provide") " (special form), " (code "context") ", "
|
||||
(code "emit!") ", " (code "emitted") ". Platform provides " (code "provide-push!")
|
||||
"/" (code "provide-pop!") ". See the implementation plan for details.")))))
|
||||
(p (strong "Spec: ") "The provide/emit! primitives are declared in "
|
||||
(a :href "/sx/(language.(spec.(explore.boundary)))" :class "font-mono text-violet-600 hover:underline text-sm" "boundary.sx")
|
||||
" (Tier 5: Dynamic scope). The " (code "provide") " special form is in "
|
||||
(a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx")
|
||||
". Element rendering with provide/emit! is visible in all four adapter specs: "
|
||||
(a :href "/sx/(language.(spec.(explore.adapter-html)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-html")
|
||||
", "
|
||||
(a :href "/sx/(language.(spec.(explore.adapter-async)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-async")
|
||||
", "
|
||||
(a :href "/sx/(language.(spec.(explore.adapter-sx)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-sx")
|
||||
", "
|
||||
(a :href "/sx/(language.(spec.(explore.adapter-dom)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-dom")
|
||||
".")))))
|
||||
|
||||
@@ -613,6 +613,22 @@
|
||||
"phase2" (~reactive-islands/phase2/reactive-islands-phase2-content)
|
||||
:else (~reactive-islands/index/reactive-islands-index-content))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Provide / Emit! section (under Geography)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage scopes-index
|
||||
:path "/geography/scopes/"
|
||||
:auth :public
|
||||
:layout :sx-docs
|
||||
:content (~layouts/doc :path "/sx/(geography.(scopes))" (~geography/scopes-content)))
|
||||
|
||||
(defpage provide-index
|
||||
:path "/geography/provide/"
|
||||
:auth :public
|
||||
:layout :sx-docs
|
||||
:content (~layouts/doc :path "/sx/(geography.(provide))" (~geography/provide-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Spreads section (under Geography)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user