Compare commits
9 Commits
81d8e55fb0
...
9a707dbe56
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a707dbe56 | |||
| 069d7e7090 | |||
| 09947262a5 | |||
| ec52e2116e | |||
| 657b631700 | |||
| 32ca059ed7 | |||
| 2da80c69ed | |||
| a8bfff9e0b | |||
| a70ff2b153 |
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-07T21:45:27Z";
|
||||
var SX_VERSION = "2026-03-08T00:10:07Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -35,12 +35,13 @@
|
||||
}
|
||||
Lambda.prototype._lambda = true;
|
||||
|
||||
function Component(name, params, hasChildren, body, closure) {
|
||||
function Component(name, params, hasChildren, body, closure, affinity) {
|
||||
this.name = name;
|
||||
this.params = params;
|
||||
this.hasChildren = hasChildren;
|
||||
this.body = body;
|
||||
this.closure = closure || {};
|
||||
this.affinity = affinity || "auto";
|
||||
}
|
||||
Component.prototype._component = true;
|
||||
|
||||
@@ -59,15 +60,6 @@
|
||||
function RawHTML(html) { this.html = html; }
|
||||
RawHTML.prototype._raw = true;
|
||||
|
||||
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
|
||||
this.className = className;
|
||||
this.declarations = declarations || "";
|
||||
this.mediaRules = mediaRules || [];
|
||||
this.pseudoRules = pseudoRules || [];
|
||||
this.keyframes = keyframes || [];
|
||||
}
|
||||
StyleValue.prototype._styleValue = true;
|
||||
|
||||
function isSym(x) { return x != null && x._sym === true; }
|
||||
function isKw(x) { return x != null && x._kw === true; }
|
||||
|
||||
@@ -103,7 +95,6 @@
|
||||
if (x._component) return "component";
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
if (x._styleValue) return "style-value";
|
||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||
if (Array.isArray(x)) return "list";
|
||||
if (typeof x === "object") return "dict";
|
||||
@@ -116,8 +107,8 @@
|
||||
function makeKeyword(n) { return new Keyword(n); }
|
||||
|
||||
function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); }
|
||||
function makeComponent(name, params, hasChildren, body, env) {
|
||||
return new Component(name, params, hasChildren, body, merge(env));
|
||||
function makeComponent(name, params, hasChildren, body, env, affinity) {
|
||||
return new Component(name, params, hasChildren, body, merge(env), affinity);
|
||||
}
|
||||
function makeMacro(params, restParam, body, env, name) {
|
||||
return new Macro(params, restParam, body, merge(env), name);
|
||||
@@ -135,6 +126,7 @@
|
||||
function componentClosure(c) { return c.closure; }
|
||||
function componentHasChildren(c) { return c.hasChildren; }
|
||||
function componentName(c) { return c.name; }
|
||||
function componentAffinity(c) { return c.affinity || "auto"; }
|
||||
|
||||
function macroParams(m) { return m.params; }
|
||||
function macroRestParam(m) { return m.restParam; }
|
||||
@@ -150,27 +142,6 @@
|
||||
function isComponent(x) { return x != null && x._component === true; }
|
||||
function isMacro(x) { return x != null && x._macro === true; }
|
||||
|
||||
function isStyleValue(x) { return x != null && x._styleValue === true; }
|
||||
function styleValueClass(x) { return x.className; }
|
||||
function styleValue_p(x) { return x != null && x._styleValue === true; }
|
||||
|
||||
function buildKeyframes(kfName, steps, env) {
|
||||
// Platform implementation of defkeyframes
|
||||
var parts = [];
|
||||
for (var i = 0; i < steps.length; i++) {
|
||||
var step = steps[i];
|
||||
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
|
||||
var body = trampoline(evalExpr(step[1], env));
|
||||
var decls = isStyleValue(body) ? body.declarations : String(body);
|
||||
parts.push(selector + "{" + decls + "}");
|
||||
}
|
||||
var kfRule = "@keyframes " + kfName + "{" + parts.join("") + "}";
|
||||
var cn = "sx-ref-kf-" + kfName;
|
||||
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
|
||||
env[kfName] = sv;
|
||||
return sv;
|
||||
}
|
||||
|
||||
function envHas(env, name) { return name in env; }
|
||||
function envGet(env, name) { return env[name]; }
|
||||
function envSet(env, name, val) { env[name] = val; }
|
||||
@@ -384,29 +355,6 @@
|
||||
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
|
||||
|
||||
|
||||
// stdlib.style
|
||||
PRIMITIVES["css"] = function() {
|
||||
var atoms = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (isNil(a) || a === false) continue;
|
||||
atoms.push(isKw(a) ? a.name : String(a));
|
||||
}
|
||||
if (!atoms.length) return NIL;
|
||||
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
|
||||
};
|
||||
PRIMITIVES["merge-styles"] = function() {
|
||||
var valid = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||
}
|
||||
if (!valid.length) return NIL;
|
||||
if (valid.length === 1) return valid[0];
|
||||
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
|
||||
return new StyleValue("sx-merged", allDecls, [], [], []);
|
||||
};
|
||||
|
||||
|
||||
// stdlib.debug
|
||||
PRIMITIVES["assert"] = function(cond, msg) {
|
||||
if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed"));
|
||||
@@ -493,7 +441,7 @@
|
||||
function isSpecialForm(n) { return n in {
|
||||
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
|
||||
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
|
||||
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
|
||||
"defhandler":1,"begin":1,"do":1,
|
||||
"quote":1,"quasiquote":1,"->":1,"set!":1
|
||||
}; }
|
||||
function isHoForm(n) { return n in {
|
||||
@@ -504,7 +452,7 @@
|
||||
|
||||
function isDefinitionForm(name) {
|
||||
return name === "define" || name === "defcomp" || name === "defmacro" ||
|
||||
name === "defstyle" || name === "defkeyframes" || name === "defhandler";
|
||||
name === "defstyle" || name === "defhandler";
|
||||
}
|
||||
|
||||
function indexOf_(s, ch) {
|
||||
@@ -604,10 +552,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 == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defkeyframes")) ? sfDefkeyframes(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 == "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 == "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 == "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 == "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 == "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(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env)))))))))))))))))))))))))))))))))))))));
|
||||
})() : (isSxTruthy(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))))))))));
|
||||
})() : evalCall(head, args, env)));
|
||||
})(); };
|
||||
|
||||
@@ -771,18 +719,32 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var sfDefcomp = function(args, env) { return (function() {
|
||||
var nameSym = first(args);
|
||||
var paramsRaw = nth(args, 1);
|
||||
var body = nth(args, 2);
|
||||
var body = last(args);
|
||||
var compName = stripPrefix(symbolName(nameSym), "~");
|
||||
var parsed = parseCompParams(paramsRaw);
|
||||
var params = first(parsed);
|
||||
var hasChildren = nth(parsed, 1);
|
||||
var affinity = defcompKwarg(args, "affinity", "auto");
|
||||
return (function() {
|
||||
var comp = makeComponent(compName, params, hasChildren, body, env);
|
||||
var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
|
||||
env[symbolName(nameSym)] = comp;
|
||||
return comp;
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// defcomp-kwarg
|
||||
var defcompKwarg = function(args, key, default_) { return (function() {
|
||||
var end = (len(args) - 1);
|
||||
var result = default_;
|
||||
{ var _c = range(2, end, 1); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(nth(args, i)) == "keyword")) && isSxTruthy((keywordName(nth(args, i)) == key)) && ((i + 1) < end)))) {
|
||||
(function() {
|
||||
var val = nth(args, (i + 1));
|
||||
return (result = (isSxTruthy((typeOf(val) == "keyword")) ? keywordName(val) : val));
|
||||
})();
|
||||
} } }
|
||||
return result;
|
||||
})(); };
|
||||
|
||||
// parse-comp-params
|
||||
var parseCompParams = function(paramsExpr) { return (function() {
|
||||
var params = [];
|
||||
@@ -828,13 +790,6 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
return value;
|
||||
})(); };
|
||||
|
||||
// sf-defkeyframes
|
||||
var sfDefkeyframes = function(args, env) { return (function() {
|
||||
var kfName = symbolName(first(args));
|
||||
var steps = rest(args);
|
||||
return buildKeyframes(kfName, steps, env);
|
||||
})(); };
|
||||
|
||||
// sf-begin
|
||||
var sfBegin = function(args, env) { return (isSxTruthy(isEmpty(args)) ? NIL : (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 0, (len(args) - 1))), makeThunk(last(args), env))); };
|
||||
|
||||
@@ -994,7 +949,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var BOOLEAN_ATTRS = ["async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "inert", "ismap", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected"];
|
||||
|
||||
// definition-form?
|
||||
var isDefinitionForm = function(name) { return sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defkeyframes"), (name == "defhandler")); };
|
||||
var isDefinitionForm = function(name) { return sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler")); };
|
||||
|
||||
// parse-element-args
|
||||
var parseElementArgs = function(args, env) { return (function() {
|
||||
@@ -1014,7 +969,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
// render-attrs
|
||||
var renderAttrs = function(attrs) { return join("", map(function(key) { return (function() {
|
||||
var val = dictGet(attrs, key);
|
||||
return (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && val)) ? (String(" ") + String(key)) : (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && !isSxTruthy(val))) ? "" : (isSxTruthy(isNil(val)) ? "" : (isSxTruthy((isSxTruthy((key == "style")) && isStyleValue(val))) ? (String(" class=\"") + String(styleValueClass(val)) + String("\"")) : (String(" ") + String(key) + String("=\"") + String(escapeAttr((String(val)))) + String("\""))))));
|
||||
return (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && val)) ? (String(" ") + String(key)) : (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && !isSxTruthy(val))) ? "" : (isSxTruthy(isNil(val)) ? "" : (String(" ") + String(key) + String("=\"") + String(escapeAttr((String(val)))) + String("\"")))));
|
||||
})(); }, keys(attrs))); };
|
||||
|
||||
// eval-cond
|
||||
@@ -1057,7 +1012,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var skipComment = function() { while(true) { if (isSxTruthy((isSxTruthy((pos < lenSrc)) && !isSxTruthy((nth(source, pos) == "\n"))))) { pos = (pos + 1);
|
||||
continue; } else { return NIL; } } };
|
||||
var skipWs = function() { while(true) { if (isSxTruthy((pos < lenSrc))) { { var ch = nth(source, pos);
|
||||
if (isSxTruthy(sxOr((ch == " "), (ch == "\t"), (ch == "\n"), (ch == "\\r")))) { pos = (pos + 1);
|
||||
if (isSxTruthy(sxOr((ch == " "), (ch == "\t"), (ch == "\n"), (ch == "\r")))) { pos = (pos + 1);
|
||||
continue; } else if (isSxTruthy((ch == ";"))) { pos = (pos + 1);
|
||||
skipComment();
|
||||
continue; } else { return NIL; } } } else { return NIL; } } };
|
||||
@@ -1068,7 +1023,7 @@ return (function() {
|
||||
if (isSxTruthy((ch == "\""))) { pos = (pos + 1);
|
||||
return NIL; } else if (isSxTruthy((ch == "\\"))) { pos = (pos + 1);
|
||||
{ var esc = nth(source, pos);
|
||||
buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\\r" : esc)))));
|
||||
buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\r" : esc)))));
|
||||
pos = (pos + 1);
|
||||
continue; } } else { buf = (String(buf) + String(ch));
|
||||
pos = (pos + 1);
|
||||
@@ -1165,10 +1120,10 @@ continue; } else { return NIL; } } };
|
||||
var renderToHtml = function(expr, 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); 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 == "style-value") return styleValueClass(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); return escapeHtml((String(val))); })(); };
|
||||
|
||||
// RENDER_HTML_FORMS
|
||||
var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defkeyframes", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
|
||||
// render-html-form?
|
||||
var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); };
|
||||
@@ -1316,7 +1271,7 @@ continue; } else { return NIL; } } };
|
||||
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
|
||||
|
||||
// render-to-dom
|
||||
var renderToDom = function(expr, env, ns) { return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); if (_m == "style-value") return createTextNode(styleValueClass(expr)); return createTextNode((String(expr))); })(); };
|
||||
var renderToDom = function(expr, env, ns) { return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); return createTextNode((String(expr))); })(); };
|
||||
|
||||
// render-dom-list
|
||||
var renderDomList = function(expr, env, ns) { return (function() {
|
||||
@@ -1339,22 +1294,15 @@ continue; } else { return NIL; } } };
|
||||
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);
|
||||
var extraClass = 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() {
|
||||
var attrName = keywordName(arg);
|
||||
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
|
||||
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy((attrName == "style")) && isStyleValue(attrVal))) ? (extraClass = styleValueClass(attrVal)) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))));
|
||||
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal))))));
|
||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1)))));
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
if (isSxTruthy(extraClass)) {
|
||||
(function() {
|
||||
var existing = domGetAttr(el, "class");
|
||||
return domSetAttr(el, "class", (isSxTruthy(existing) ? (String(existing) + String(" ") + String(extraClass)) : extraClass));
|
||||
})();
|
||||
}
|
||||
return el;
|
||||
})(); };
|
||||
|
||||
@@ -1405,7 +1353,7 @@ continue; } else { return NIL; } } };
|
||||
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", "defmacro", "defstyle", "defkeyframes", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
|
||||
// render-dom-form?
|
||||
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
|
||||
@@ -1818,9 +1766,9 @@ return forEach(function(attr) { return (isSxTruthy(!isSxTruthy(domHasAttr(newEl,
|
||||
var handleSxResponse = function(el, target, text, swapStyle, useTransition) { return (function() {
|
||||
var cleaned = stripComponentScripts(text);
|
||||
return (function() {
|
||||
var final = extractResponseCss(cleaned);
|
||||
var final_ = extractResponseCss(cleaned);
|
||||
return (function() {
|
||||
var trimmed = trim(final);
|
||||
var trimmed = trim(final_);
|
||||
return (isSxTruthy(!isSxTruthy(isEmpty(trimmed))) ? (function() {
|
||||
var rendered = sxRender(trimmed);
|
||||
var container = domCreateElement("div", NIL);
|
||||
@@ -2043,6 +1991,14 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
|
||||
return (isSxTruthy(isNil(target)) ? (logWarn((String("sx:route target not found: ") + String(targetSel))), false) : (isSxTruthy(!isSxTruthy(depsSatisfied_p(match))) ? (logInfo((String("sx:route deps miss for ") + String(pageName))), false) : (function() {
|
||||
var ioDeps = get(match, "io-deps");
|
||||
var hasIo = (isSxTruthy(ioDeps) && !isSxTruthy(isEmpty(ioDeps)));
|
||||
var renderPlan = get(match, "render-plan");
|
||||
if (isSxTruthy(renderPlan)) {
|
||||
(function() {
|
||||
var srv = sxOr(get(renderPlan, "server"), []);
|
||||
var cli = sxOr(get(renderPlan, "client"), []);
|
||||
return logInfo((String("sx:route plan ") + String(pageName) + String(" — ") + String(len(srv)) + String(" server, ") + String(len(cli)) + String(" client")));
|
||||
})();
|
||||
}
|
||||
if (isSxTruthy(hasIo)) {
|
||||
registerIoDeps(ioDeps);
|
||||
}
|
||||
@@ -2172,180 +2128,6 @@ return bindInlineHandlers(root); };
|
||||
var engineInit = function() { return (initCssTracking(), sxProcessScripts(NIL), sxHydrate(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// === Transpiled from cssx ===
|
||||
|
||||
// _style-atoms
|
||||
var _styleAtoms = {};
|
||||
|
||||
// _pseudo-variants
|
||||
var _pseudoVariants = {};
|
||||
|
||||
// _responsive-breakpoints
|
||||
var _responsiveBreakpoints = {};
|
||||
|
||||
// _style-keyframes
|
||||
var _styleKeyframes = {};
|
||||
|
||||
// _arbitrary-patterns
|
||||
var _arbitraryPatterns = [];
|
||||
|
||||
// _child-selector-prefixes
|
||||
var _childSelectorPrefixes = [];
|
||||
|
||||
// _style-cache
|
||||
var _styleCache = {};
|
||||
|
||||
// _injected-styles
|
||||
var _injectedStyles = {};
|
||||
|
||||
// load-style-dict
|
||||
var loadStyleDict = function(data) { _styleAtoms = sxOr(get(data, "a"), {});
|
||||
_pseudoVariants = sxOr(get(data, "v"), {});
|
||||
_responsiveBreakpoints = sxOr(get(data, "b"), {});
|
||||
_styleKeyframes = sxOr(get(data, "k"), {});
|
||||
_childSelectorPrefixes = sxOr(get(data, "c"), []);
|
||||
_arbitraryPatterns = map(function(pair) { return {["re"]: compileRegex((String("^") + String(first(pair)) + String("$"))), ["tmpl"]: nth(pair, 1)}; }, sxOr(get(data, "p"), []));
|
||||
return (_styleCache = {}); };
|
||||
|
||||
// split-variant
|
||||
var splitVariant = function(atom) { return (function() {
|
||||
var result = NIL;
|
||||
{ var _c = keys(_responsiveBreakpoints); for (var _i = 0; _i < _c.length; _i++) { var bp = _c[_i]; if (isSxTruthy(isNil(result))) {
|
||||
(function() {
|
||||
var prefix = (String(bp) + String(":"));
|
||||
return (isSxTruthy(startsWith(atom, prefix)) ? (function() {
|
||||
var restAtom = slice(atom, len(prefix));
|
||||
return (function() {
|
||||
var innerMatch = NIL;
|
||||
{ var _c = keys(_pseudoVariants); for (var _i = 0; _i < _c.length; _i++) { var pv = _c[_i]; if (isSxTruthy(isNil(innerMatch))) {
|
||||
(function() {
|
||||
var innerPrefix = (String(pv) + String(":"));
|
||||
return (isSxTruthy(startsWith(restAtom, innerPrefix)) ? (innerMatch = [(String(bp) + String(":") + String(pv)), slice(restAtom, len(innerPrefix))]) : NIL);
|
||||
})();
|
||||
} } }
|
||||
return (result = sxOr(innerMatch, [bp, restAtom]));
|
||||
})();
|
||||
})() : NIL);
|
||||
})();
|
||||
} } }
|
||||
if (isSxTruthy(isNil(result))) {
|
||||
{ var _c = keys(_pseudoVariants); for (var _i = 0; _i < _c.length; _i++) { var pv = _c[_i]; if (isSxTruthy(isNil(result))) {
|
||||
(function() {
|
||||
var prefix = (String(pv) + String(":"));
|
||||
return (isSxTruthy(startsWith(atom, prefix)) ? (result = [pv, slice(atom, len(prefix))]) : NIL);
|
||||
})();
|
||||
} } }
|
||||
}
|
||||
return sxOr(result, [NIL, atom]);
|
||||
})(); };
|
||||
|
||||
// resolve-atom
|
||||
var resolveAtom = function(atom) { return (function() {
|
||||
var decls = dictGet(_styleAtoms, atom);
|
||||
return (isSxTruthy(!isSxTruthy(isNil(decls))) ? decls : (isSxTruthy(startsWith(atom, "animate-")) ? (function() {
|
||||
var kfName = slice(atom, 8);
|
||||
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? (String("animation-name:") + String(kfName)) : NIL);
|
||||
})() : (function() {
|
||||
var matchResult = NIL;
|
||||
{ var _c = _arbitraryPatterns; for (var _i = 0; _i < _c.length; _i++) { var pat = _c[_i]; if (isSxTruthy(isNil(matchResult))) {
|
||||
(function() {
|
||||
var m = regexMatch(get(pat, "re"), atom);
|
||||
return (isSxTruthy(m) ? (matchResult = regexReplaceGroups(get(pat, "tmpl"), m)) : NIL);
|
||||
})();
|
||||
} } }
|
||||
return matchResult;
|
||||
})()));
|
||||
})(); };
|
||||
|
||||
// is-child-selector-atom?
|
||||
var isChildSelectorAtom = function(atom) { return some(function(prefix) { return startsWith(atom, prefix); }, _childSelectorPrefixes); };
|
||||
|
||||
// hash-style
|
||||
var hashStyle = function(input) { return fnv1aHash(input); };
|
||||
|
||||
// resolve-style
|
||||
var resolveStyle = function(atoms) { return (function() {
|
||||
var key = join("\\0", atoms);
|
||||
return (function() {
|
||||
var cached = dictGet(_styleCache, key);
|
||||
return (isSxTruthy(!isSxTruthy(isNil(cached))) ? cached : (function() {
|
||||
var baseDecls = [];
|
||||
var mediaRules = [];
|
||||
var pseudoRules = [];
|
||||
var kfNeeded = [];
|
||||
{ var _c = atoms; for (var _i = 0; _i < _c.length; _i++) { var a = _c[_i]; if (isSxTruthy(a)) {
|
||||
(function() {
|
||||
var clean = (isSxTruthy(startsWith(a, ":")) ? slice(a, 1) : a);
|
||||
return (function() {
|
||||
var parts = splitVariant(clean);
|
||||
return (function() {
|
||||
var variant = first(parts);
|
||||
var base = nth(parts, 1);
|
||||
var decls = resolveAtom(base);
|
||||
return (isSxTruthy(decls) ? ((isSxTruthy(startsWith(base, "animate-")) ? (function() {
|
||||
var kfName = slice(base, 8);
|
||||
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? append_b(kfNeeded, [kfName, dictGet(_styleKeyframes, kfName)]) : NIL);
|
||||
})() : NIL), (isSxTruthy(isNil(variant)) ? (isSxTruthy(isChildSelectorAtom(base)) ? append_b(pseudoRules, [">:not(:first-child)", decls]) : append_b(baseDecls, decls)) : (isSxTruthy(dictHas(_responsiveBreakpoints, variant)) ? append_b(mediaRules, [dictGet(_responsiveBreakpoints, variant), decls]) : (isSxTruthy(dictHas(_pseudoVariants, variant)) ? append_b(pseudoRules, [dictGet(_pseudoVariants, variant), decls]) : (function() {
|
||||
var vparts = split(variant, ":");
|
||||
var mediaPart = NIL;
|
||||
var pseudoPart = NIL;
|
||||
{ var _c = vparts; for (var _i = 0; _i < _c.length; _i++) { var vp = _c[_i]; (isSxTruthy(dictHas(_responsiveBreakpoints, vp)) ? (mediaPart = dictGet(_responsiveBreakpoints, vp)) : (isSxTruthy(dictHas(_pseudoVariants, vp)) ? (pseudoPart = dictGet(_pseudoVariants, vp)) : NIL)); } }
|
||||
if (isSxTruthy(mediaPart)) {
|
||||
mediaRules.push([mediaPart, decls]);
|
||||
}
|
||||
if (isSxTruthy(pseudoPart)) {
|
||||
pseudoRules.push([pseudoPart, decls]);
|
||||
}
|
||||
return (isSxTruthy((isSxTruthy(isNil(mediaPart)) && isNil(pseudoPart))) ? append_b(baseDecls, decls) : NIL);
|
||||
})())))) : NIL);
|
||||
})();
|
||||
})();
|
||||
})();
|
||||
} } }
|
||||
return (function() {
|
||||
var hashInput = join(";", baseDecls);
|
||||
{ var _c = mediaRules; for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
|
||||
{ var _c = pseudoRules; for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
|
||||
{ var _c = kfNeeded; for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
|
||||
return (function() {
|
||||
var cn = (String("sx-") + String(hashStyle(hashInput)));
|
||||
var sv = makeStyleValue_(cn, join(";", baseDecls), mediaRules, pseudoRules, kfNeeded);
|
||||
_styleCache[key] = sv;
|
||||
injectStyleValue(sv, atoms);
|
||||
return sv;
|
||||
})();
|
||||
})();
|
||||
})());
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// merge-style-values
|
||||
var mergeStyleValues = function(styles) { return (isSxTruthy((len(styles) == 1)) ? first(styles) : (function() {
|
||||
var allDecls = [];
|
||||
var allMedia = [];
|
||||
var allPseudo = [];
|
||||
var allKf = [];
|
||||
{ var _c = styles; for (var _i = 0; _i < _c.length; _i++) { var sv = _c[_i]; if (isSxTruthy(styleValueDeclarations(sv))) {
|
||||
allDecls.push(styleValueDeclarations(sv));
|
||||
}
|
||||
allMedia = concat(allMedia, styleValueMediaRules(sv));
|
||||
allPseudo = concat(allPseudo, styleValuePseudoRules(sv));
|
||||
allKf = concat(allKf, styleValueKeyframes_(sv)); } }
|
||||
return (function() {
|
||||
var hashInput = join(";", allDecls);
|
||||
{ var _c = allMedia; for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
|
||||
{ var _c = allPseudo; for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
|
||||
{ var _c = allKf; for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
|
||||
return (function() {
|
||||
var cn = (String("sx-") + String(hashStyle(hashInput)));
|
||||
var merged = makeStyleValue_(cn, join(";", allDecls), allMedia, allPseudo, allKf);
|
||||
injectStyleValue(merged, []);
|
||||
return merged;
|
||||
})();
|
||||
})();
|
||||
})()); };
|
||||
|
||||
|
||||
// === Transpiled from boot ===
|
||||
|
||||
// HEAD_HOIST_SELECTOR
|
||||
@@ -2461,26 +2243,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
})();
|
||||
return setSxCompCookie(hash);
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// init-style-dict
|
||||
var initStyleDict = function() { return (function() {
|
||||
var scripts = queryStyleScripts();
|
||||
return forEach(function(s) { return (isSxTruthy(!isSxTruthy(isProcessed(s, "styles"))) ? (markProcessed(s, "styles"), (function() {
|
||||
var text = domTextContent(s);
|
||||
var hash = domGetAttr(s, "data-hash");
|
||||
return (isSxTruthy(isNil(hash)) ? (isSxTruthy((isSxTruthy(text) && !isSxTruthy(isEmpty(trim(text))))) ? parseAndLoadStyleDict(text) : NIL) : (function() {
|
||||
var hasInline = (isSxTruthy(text) && !isSxTruthy(isEmpty(trim(text))));
|
||||
(function() {
|
||||
var cachedHash = localStorageGet("sx-styles-hash");
|
||||
return (isSxTruthy((cachedHash == hash)) ? (isSxTruthy(hasInline) ? (localStorageSet("sx-styles-src", text), parseAndLoadStyleDict(text), logInfo("styles: downloaded (cookie stale)")) : (function() {
|
||||
var cached = localStorageGet("sx-styles-src");
|
||||
return (isSxTruthy(cached) ? (parseAndLoadStyleDict(cached), logInfo((String("styles: cached (") + String(hash) + String(")")))) : (clearSxStylesCookie(), browserReload()));
|
||||
})()) : (isSxTruthy(hasInline) ? (localStorageSet("sx-styles-hash", hash), localStorageSet("sx-styles-src", text), parseAndLoadStyleDict(text), logInfo((String("styles: downloaded (") + String(hash) + String(")")))) : (localStorageRemove("sx-styles-hash"), localStorageRemove("sx-styles-src"), clearSxStylesCookie(), browserReload())));
|
||||
})();
|
||||
return setSxStylesCookie(hash);
|
||||
})());
|
||||
})()) : NIL); }, scripts);
|
||||
})(); };
|
||||
|
||||
// _page-routes
|
||||
@@ -2506,7 +2268,7 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
})(); };
|
||||
|
||||
// boot-init
|
||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), initStyleDict(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// === Transpiled from router (client-side route matching) ===
|
||||
@@ -2779,7 +2541,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
renderDomElement = function(tag, args, env, ns) {
|
||||
var newNs = tag === "svg" ? SVG_NS : tag === "math" ? MATH_NS : ns;
|
||||
var el = domCreateElement(tag, newNs);
|
||||
var extraClasses = [];
|
||||
var isVoid = contains(VOID_ELEMENTS, tag);
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var arg = args[i];
|
||||
@@ -2788,11 +2549,7 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
var attrVal = trampoline(evalExpr(args[i + 1], env));
|
||||
i++; // skip value
|
||||
if (isNil(attrVal) || attrVal === false) continue;
|
||||
if (attrName === "class" && attrVal && attrVal._styleValue) {
|
||||
extraClasses.push(attrVal.className);
|
||||
} else if (attrName === "style" && attrVal && attrVal._styleValue) {
|
||||
extraClasses.push(attrVal.className);
|
||||
} else if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
|
||||
} else if (attrVal === true) {
|
||||
el.setAttribute(attrName, "");
|
||||
@@ -2806,10 +2563,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (extraClasses.length) {
|
||||
var existing = el.getAttribute("class") || "";
|
||||
el.setAttribute("class", (existing ? existing + " " : "") + extraClasses.join(" "));
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
@@ -3703,70 +3456,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeStyleValue_(cn, decls, media, pseudo, kf) {
|
||||
return new StyleValue(cn, decls || "", media || [], pseudo || [], kf || []);
|
||||
}
|
||||
|
||||
function styleValueDeclarations(sv) { return sv.declarations; }
|
||||
function styleValueMediaRules(sv) { return sv.mediaRules; }
|
||||
function styleValuePseudoRules(sv) { return sv.pseudoRules; }
|
||||
function styleValueKeyframes_(sv) { return sv.keyframes; }
|
||||
|
||||
function injectStyleValue(sv, atoms) {
|
||||
if (_injectedStyles[sv.className]) return;
|
||||
_injectedStyles[sv.className] = true;
|
||||
|
||||
if (!_hasDom) return;
|
||||
var cssTarget = document.getElementById("sx-css");
|
||||
if (!cssTarget) return;
|
||||
|
||||
var rules = [];
|
||||
// Child-selector atoms are now routed to pseudoRules by the resolver
|
||||
// with selector ">:not(:first-child)", so base declarations are always
|
||||
// applied directly to the class.
|
||||
if (sv.declarations) {
|
||||
rules.push("." + sv.className + "{" + sv.declarations + "}");
|
||||
}
|
||||
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
|
||||
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
|
||||
if (sel.indexOf("&") >= 0) {
|
||||
rules.push(sel.replace(/&/g, "." + sv.className) + "{" + decls + "}");
|
||||
} else {
|
||||
rules.push("." + sv.className + sel + "{" + decls + "}");
|
||||
}
|
||||
}
|
||||
for (var mi = 0; mi < sv.mediaRules.length; mi++) {
|
||||
rules.push("@media " + sv.mediaRules[mi][0] + "{." + sv.className + "{" + sv.mediaRules[mi][1] + "}}");
|
||||
}
|
||||
for (var ki = 0; ki < sv.keyframes.length; ki++) {
|
||||
rules.push(sv.keyframes[ki][1]);
|
||||
}
|
||||
cssTarget.textContent += rules.join("");
|
||||
}
|
||||
|
||||
// Replace stub css primitive with real CSSX implementation
|
||||
PRIMITIVES["css"] = function() {
|
||||
var atoms = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (isNil(a) || a === false) continue;
|
||||
atoms.push(isKw(a) ? a.name : String(a));
|
||||
}
|
||||
if (!atoms.length) return NIL;
|
||||
return resolveStyle(atoms);
|
||||
};
|
||||
|
||||
PRIMITIVES["merge-styles"] = function() {
|
||||
var valid = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||
}
|
||||
if (!valid.length) return NIL;
|
||||
if (valid.length === 1) return valid[0];
|
||||
return mergeStyleValues(valid);
|
||||
};
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — Boot (mount, hydrate, scripts, cookies)
|
||||
// =========================================================================
|
||||
@@ -3823,12 +3512,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
r.querySelectorAll('script[type="text/sx"]'));
|
||||
}
|
||||
|
||||
function queryStyleScripts() {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
document.querySelectorAll('script[type="text/sx-styles"]'));
|
||||
}
|
||||
|
||||
function queryPageScripts() {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
@@ -3860,14 +3543,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
function setSxStylesCookie(hash) {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
||||
}
|
||||
|
||||
function clearSxStylesCookie() {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
// --- Env helpers ---
|
||||
|
||||
function parseEnvAttr(el) {
|
||||
@@ -3916,12 +3591,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
}
|
||||
}
|
||||
|
||||
function parseAndLoadStyleDict(text) {
|
||||
try { loadStyleDict(JSON.parse(text)); }
|
||||
catch (e) { if (typeof console !== "undefined") console.warn("[sx-ref] style dict parse error", e); }
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Post-transpilation fixups
|
||||
// =========================================================================
|
||||
@@ -4082,7 +3751,7 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
|
||||
// define/defcomp/defmacro — eval for side effects
|
||||
if (hname === "define" || hname === "defcomp" || hname === "defmacro" ||
|
||||
hname === "defstyle" || hname === "defkeyframes" || hname === "defhandler") {
|
||||
hname === "defstyle" || hname === "defhandler") {
|
||||
trampoline(evalExpr(expr, env));
|
||||
return null;
|
||||
}
|
||||
@@ -4204,11 +3873,7 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
})(attrName, attrVal);
|
||||
} else {
|
||||
if (!isNil(attrVal) && attrVal !== false) {
|
||||
if (attrName === "class" && attrVal && attrVal._styleValue) {
|
||||
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
|
||||
} else if (attrName === "style" && attrVal && attrVal._styleValue) {
|
||||
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
|
||||
} else if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
|
||||
} else if (attrVal === true) {
|
||||
el.setAttribute(attrName, "");
|
||||
@@ -4670,7 +4335,7 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
registerIoDeps: typeof registerIoDeps === "function" ? registerIoDeps : null,
|
||||
asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,
|
||||
asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,
|
||||
_version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
|
||||
_version: "ref-2.0 (boot+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -58,15 +58,6 @@
|
||||
function RawHTML(html) { this.html = html; }
|
||||
RawHTML.prototype._raw = true;
|
||||
|
||||
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
|
||||
this.className = className;
|
||||
this.declarations = declarations || "";
|
||||
this.mediaRules = mediaRules || [];
|
||||
this.pseudoRules = pseudoRules || [];
|
||||
this.keyframes = keyframes || [];
|
||||
}
|
||||
StyleValue.prototype._styleValue = true;
|
||||
|
||||
function isSym(x) { return x != null && x._sym === true; }
|
||||
function isKw(x) { return x != null && x._kw === true; }
|
||||
|
||||
@@ -102,7 +93,6 @@
|
||||
if (x._component) return "component";
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
if (x._styleValue) return "style-value";
|
||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||
if (Array.isArray(x)) return "list";
|
||||
if (typeof x === "object") return "dict";
|
||||
@@ -149,27 +139,6 @@
|
||||
function isComponent(x) { return x != null && x._component === true; }
|
||||
function isMacro(x) { return x != null && x._macro === true; }
|
||||
|
||||
function isStyleValue(x) { return x != null && x._styleValue === true; }
|
||||
function styleValueClass(x) { return x.className; }
|
||||
function styleValue_p(x) { return x != null && x._styleValue === true; }
|
||||
|
||||
function buildKeyframes(kfName, steps, env) {
|
||||
// Platform implementation of defkeyframes
|
||||
var parts = [];
|
||||
for (var i = 0; i < steps.length; i++) {
|
||||
var step = steps[i];
|
||||
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
|
||||
var body = trampoline(evalExpr(step[1], env));
|
||||
var decls = isStyleValue(body) ? body.declarations : String(body);
|
||||
parts.push(selector + "{" + decls + "}");
|
||||
}
|
||||
var kfRule = "@keyframes " + kfName + "{" + parts.join("") + "}";
|
||||
var cn = "sx-ref-kf-" + kfName;
|
||||
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
|
||||
env[kfName] = sv;
|
||||
return sv;
|
||||
}
|
||||
|
||||
function envHas(env, name) { return name in env; }
|
||||
function envGet(env, name) { return env[name]; }
|
||||
function envSet(env, name, val) { env[name] = val; }
|
||||
@@ -377,29 +346,6 @@
|
||||
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
|
||||
|
||||
|
||||
// stdlib.style
|
||||
PRIMITIVES["css"] = function() {
|
||||
var atoms = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (isNil(a) || a === false) continue;
|
||||
atoms.push(isKw(a) ? a.name : String(a));
|
||||
}
|
||||
if (!atoms.length) return NIL;
|
||||
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
|
||||
};
|
||||
PRIMITIVES["merge-styles"] = function() {
|
||||
var valid = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||
}
|
||||
if (!valid.length) return NIL;
|
||||
if (valid.length === 1) return valid[0];
|
||||
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
|
||||
return new StyleValue("sx-merged", allDecls, [], [], []);
|
||||
};
|
||||
|
||||
|
||||
// stdlib.debug
|
||||
PRIMITIVES["assert"] = function(cond, msg) {
|
||||
if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed"));
|
||||
@@ -486,7 +432,7 @@
|
||||
function isSpecialForm(n) { return n in {
|
||||
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
|
||||
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
|
||||
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
|
||||
"defhandler":1,"begin":1,"do":1,
|
||||
"quote":1,"quasiquote":1,"->":1,"set!":1
|
||||
}; }
|
||||
function isHoForm(n) { return n in {
|
||||
@@ -517,7 +463,7 @@
|
||||
|
||||
function isDefinitionForm(name) {
|
||||
return name === "define" || name === "defcomp" || name === "defmacro" ||
|
||||
name === "defstyle" || name === "defkeyframes" || name === "defhandler";
|
||||
name === "defstyle" || name === "defhandler";
|
||||
}
|
||||
|
||||
function indexOf_(s, ch) {
|
||||
@@ -657,10 +603,10 @@
|
||||
var args = rest(expr);
|
||||
return (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 == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defkeyframes")) ? sfDefkeyframes(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 == "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 == "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 == "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 == "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 == "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(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env)))))))))))))))))))))))))))))))))))))));
|
||||
})() : (isSxTruthy(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))))))))));
|
||||
})() : evalCall(head, args, env)));
|
||||
})(); };
|
||||
|
||||
@@ -881,13 +827,6 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
return value;
|
||||
})(); };
|
||||
|
||||
// sf-defkeyframes
|
||||
var sfDefkeyframes = function(args, env) { return (function() {
|
||||
var kfName = symbolName(first(args));
|
||||
var steps = rest(args);
|
||||
return buildKeyframes(kfName, steps, env);
|
||||
})(); };
|
||||
|
||||
// sf-begin
|
||||
var sfBegin = function(args, env) { return (isSxTruthy(isEmpty(args)) ? NIL : (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 0, (len(args) - 1))), makeThunk(last(args), env))); };
|
||||
|
||||
@@ -1047,7 +986,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var BOOLEAN_ATTRS = ["async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "inert", "ismap", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected"];
|
||||
|
||||
// definition-form?
|
||||
var isDefinitionForm = function(name) { return sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defkeyframes"), (name == "defhandler")); };
|
||||
var isDefinitionForm = function(name) { return sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler")); };
|
||||
|
||||
// parse-element-args
|
||||
var parseElementArgs = function(args, env) { return (function() {
|
||||
@@ -1067,7 +1006,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
// render-attrs
|
||||
var renderAttrs = function(attrs) { return join("", map(function(key) { return (function() {
|
||||
var val = dictGet(attrs, key);
|
||||
return (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && val)) ? (String(" ") + String(key)) : (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && !val)) ? "" : (isSxTruthy(isNil(val)) ? "" : (isSxTruthy((isSxTruthy((key == "style")) && isStyleValue(val))) ? (String(" class=\"") + String(styleValueClass(val)) + String("\"")) : (String(" ") + String(key) + String("=\"") + String(escapeAttr((String(val)))) + String("\""))))));
|
||||
return (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && val)) ? (String(" ") + String(key)) : (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && !val)) ? "" : (isSxTruthy(isNil(val)) ? "" : (String(" ") + String(key) + String("=\"") + String(escapeAttr((String(val)))) + String("\"")))));
|
||||
})(); }, keys(attrs))); };
|
||||
|
||||
|
||||
@@ -1188,10 +1127,10 @@ continue; } else { return NIL; } } };
|
||||
var renderToHtml = function(expr, 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); 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 == "style-value") return styleValueClass(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); return escapeHtml((String(val))); })(); };
|
||||
|
||||
// RENDER_HTML_FORMS
|
||||
var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defkeyframes", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
|
||||
// render-html-form?
|
||||
var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); };
|
||||
@@ -1339,7 +1278,7 @@ continue; } else { return NIL; } } };
|
||||
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
|
||||
|
||||
// render-to-dom
|
||||
var renderToDom = function(expr, env, ns) { return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); if (_m == "style-value") return createTextNode(styleValueClass(expr)); return createTextNode((String(expr))); })(); };
|
||||
var renderToDom = function(expr, env, ns) { return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); return createTextNode((String(expr))); })(); };
|
||||
|
||||
// render-dom-list
|
||||
var renderDomList = function(expr, env, ns) { return (function() {
|
||||
@@ -1362,22 +1301,15 @@ continue; } else { return NIL; } } };
|
||||
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);
|
||||
var extraClass = 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() {
|
||||
var attrName = keywordName(arg);
|
||||
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
|
||||
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy((attrName == "style")) && isStyleValue(attrVal))) ? (extraClass = styleValueClass(attrVal)) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))));
|
||||
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal))))));
|
||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||
})() : ((isSxTruthy(!contains(VOID_ELEMENTS, tag)) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1)))));
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
if (isSxTruthy(extraClass)) {
|
||||
(function() {
|
||||
var existing = domGetAttr(el, "class");
|
||||
return domSetAttr(el, "class", (isSxTruthy(existing) ? (String(existing) + String(" ") + String(extraClass)) : extraClass));
|
||||
})();
|
||||
}
|
||||
return el;
|
||||
})(); };
|
||||
|
||||
@@ -1433,7 +1365,7 @@ continue; } else { return NIL; } } };
|
||||
})(); };
|
||||
|
||||
// RENDER_DOM_FORMS
|
||||
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defkeyframes", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each"];
|
||||
|
||||
// render-dom-form?
|
||||
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
|
||||
@@ -2089,180 +2021,6 @@ return bindInlineHandlers(root); };
|
||||
var engineInit = function() { return (initCssTracking(), sxProcessScripts(NIL), sxHydrate(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// === Transpiled from cssx ===
|
||||
|
||||
// _style-atoms
|
||||
var _styleAtoms = {};
|
||||
|
||||
// _pseudo-variants
|
||||
var _pseudoVariants = {};
|
||||
|
||||
// _responsive-breakpoints
|
||||
var _responsiveBreakpoints = {};
|
||||
|
||||
// _style-keyframes
|
||||
var _styleKeyframes = {};
|
||||
|
||||
// _arbitrary-patterns
|
||||
var _arbitraryPatterns = [];
|
||||
|
||||
// _child-selector-prefixes
|
||||
var _childSelectorPrefixes = [];
|
||||
|
||||
// _style-cache
|
||||
var _styleCache = {};
|
||||
|
||||
// _injected-styles
|
||||
var _injectedStyles = {};
|
||||
|
||||
// load-style-dict
|
||||
var loadStyleDict = function(data) { _styleAtoms = sxOr(get(data, "a"), {});
|
||||
_pseudoVariants = sxOr(get(data, "v"), {});
|
||||
_responsiveBreakpoints = sxOr(get(data, "b"), {});
|
||||
_styleKeyframes = sxOr(get(data, "k"), {});
|
||||
_childSelectorPrefixes = sxOr(get(data, "c"), []);
|
||||
_arbitraryPatterns = map(function(pair) { return {["re"]: compileRegex((String("^") + String(first(pair)) + String("$"))), ["tmpl"]: nth(pair, 1)}; }, sxOr(get(data, "p"), []));
|
||||
return (_styleCache = {}); };
|
||||
|
||||
// split-variant
|
||||
var splitVariant = function(atom) { return (function() {
|
||||
var result = NIL;
|
||||
{ var _c = keys(_responsiveBreakpoints); for (var _i = 0; _i < _c.length; _i++) { var bp = _c[_i]; if (isSxTruthy(isNil(result))) {
|
||||
(function() {
|
||||
var prefix = (String(bp) + String(":"));
|
||||
return (isSxTruthy(startsWith(atom, prefix)) ? (function() {
|
||||
var restAtom = slice(atom, len(prefix));
|
||||
return (function() {
|
||||
var innerMatch = NIL;
|
||||
{ var _c = keys(_pseudoVariants); for (var _i = 0; _i < _c.length; _i++) { var pv = _c[_i]; if (isSxTruthy(isNil(innerMatch))) {
|
||||
(function() {
|
||||
var innerPrefix = (String(pv) + String(":"));
|
||||
return (isSxTruthy(startsWith(restAtom, innerPrefix)) ? (innerMatch = [(String(bp) + String(":") + String(pv)), slice(restAtom, len(innerPrefix))]) : NIL);
|
||||
})();
|
||||
} } }
|
||||
return (result = sxOr(innerMatch, [bp, restAtom]));
|
||||
})();
|
||||
})() : NIL);
|
||||
})();
|
||||
} } }
|
||||
if (isSxTruthy(isNil(result))) {
|
||||
{ var _c = keys(_pseudoVariants); for (var _i = 0; _i < _c.length; _i++) { var pv = _c[_i]; if (isSxTruthy(isNil(result))) {
|
||||
(function() {
|
||||
var prefix = (String(pv) + String(":"));
|
||||
return (isSxTruthy(startsWith(atom, prefix)) ? (result = [pv, slice(atom, len(prefix))]) : NIL);
|
||||
})();
|
||||
} } }
|
||||
}
|
||||
return sxOr(result, [NIL, atom]);
|
||||
})(); };
|
||||
|
||||
// resolve-atom
|
||||
var resolveAtom = function(atom) { return (function() {
|
||||
var decls = dictGet(_styleAtoms, atom);
|
||||
return (isSxTruthy(!isNil(decls)) ? decls : (isSxTruthy(startsWith(atom, "animate-")) ? (function() {
|
||||
var kfName = slice(atom, 8);
|
||||
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? (String("animation-name:") + String(kfName)) : NIL);
|
||||
})() : (function() {
|
||||
var matchResult = NIL;
|
||||
{ var _c = _arbitraryPatterns; for (var _i = 0; _i < _c.length; _i++) { var pat = _c[_i]; if (isSxTruthy(isNil(matchResult))) {
|
||||
(function() {
|
||||
var m = regexMatch(get(pat, "re"), atom);
|
||||
return (isSxTruthy(m) ? (matchResult = regexReplaceGroups(get(pat, "tmpl"), m)) : NIL);
|
||||
})();
|
||||
} } }
|
||||
return matchResult;
|
||||
})()));
|
||||
})(); };
|
||||
|
||||
// is-child-selector-atom?
|
||||
var isChildSelectorAtom = function(atom) { return some(function(prefix) { return startsWith(atom, prefix); }, _childSelectorPrefixes); };
|
||||
|
||||
// hash-style
|
||||
var hashStyle = function(input) { return fnv1aHash(input); };
|
||||
|
||||
// resolve-style
|
||||
var resolveStyle = function(atoms) { return (function() {
|
||||
var key = join("\\0", atoms);
|
||||
return (function() {
|
||||
var cached = dictGet(_styleCache, key);
|
||||
return (isSxTruthy(!isNil(cached)) ? cached : (function() {
|
||||
var baseDecls = [];
|
||||
var mediaRules = [];
|
||||
var pseudoRules = [];
|
||||
var kfNeeded = [];
|
||||
{ var _c = atoms; for (var _i = 0; _i < _c.length; _i++) { var a = _c[_i]; if (isSxTruthy(a)) {
|
||||
(function() {
|
||||
var clean = (isSxTruthy(startsWith(a, ":")) ? slice(a, 1) : a);
|
||||
return (function() {
|
||||
var parts = splitVariant(clean);
|
||||
return (function() {
|
||||
var variant = first(parts);
|
||||
var base = nth(parts, 1);
|
||||
var decls = resolveAtom(base);
|
||||
return (isSxTruthy(decls) ? ((isSxTruthy(startsWith(base, "animate-")) ? (function() {
|
||||
var kfName = slice(base, 8);
|
||||
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? append_b(kfNeeded, [kfName, dictGet(_styleKeyframes, kfName)]) : NIL);
|
||||
})() : NIL), (isSxTruthy(isNil(variant)) ? (isSxTruthy(isChildSelectorAtom(base)) ? append_b(pseudoRules, [">:not(:first-child)", decls]) : append_b(baseDecls, decls)) : (isSxTruthy(dictHas(_responsiveBreakpoints, variant)) ? append_b(mediaRules, [dictGet(_responsiveBreakpoints, variant), decls]) : (isSxTruthy(dictHas(_pseudoVariants, variant)) ? append_b(pseudoRules, [dictGet(_pseudoVariants, variant), decls]) : (function() {
|
||||
var vparts = split(variant, ":");
|
||||
var mediaPart = NIL;
|
||||
var pseudoPart = NIL;
|
||||
{ var _c = vparts; for (var _i = 0; _i < _c.length; _i++) { var vp = _c[_i]; (isSxTruthy(dictHas(_responsiveBreakpoints, vp)) ? (mediaPart = dictGet(_responsiveBreakpoints, vp)) : (isSxTruthy(dictHas(_pseudoVariants, vp)) ? (pseudoPart = dictGet(_pseudoVariants, vp)) : NIL)); } }
|
||||
if (isSxTruthy(mediaPart)) {
|
||||
mediaRules.push([mediaPart, decls]);
|
||||
}
|
||||
if (isSxTruthy(pseudoPart)) {
|
||||
pseudoRules.push([pseudoPart, decls]);
|
||||
}
|
||||
return (isSxTruthy((isSxTruthy(isNil(mediaPart)) && isNil(pseudoPart))) ? append_b(baseDecls, decls) : NIL);
|
||||
})())))) : NIL);
|
||||
})();
|
||||
})();
|
||||
})();
|
||||
} } }
|
||||
return (function() {
|
||||
var hashInput = join(";", baseDecls);
|
||||
{ var _c = mediaRules; for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
|
||||
{ var _c = pseudoRules; for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
|
||||
{ var _c = kfNeeded; for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
|
||||
return (function() {
|
||||
var cn = (String("sx-") + String(hashStyle(hashInput)));
|
||||
var sv = makeStyleValue_(cn, join(";", baseDecls), mediaRules, pseudoRules, kfNeeded);
|
||||
_styleCache[key] = sv;
|
||||
injectStyleValue(sv, atoms);
|
||||
return sv;
|
||||
})();
|
||||
})();
|
||||
})());
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// merge-style-values
|
||||
var mergeStyleValues = function(styles) { return (isSxTruthy((len(styles) == 1)) ? first(styles) : (function() {
|
||||
var allDecls = [];
|
||||
var allMedia = [];
|
||||
var allPseudo = [];
|
||||
var allKf = [];
|
||||
{ var _c = styles; for (var _i = 0; _i < _c.length; _i++) { var sv = _c[_i]; if (isSxTruthy(styleValueDeclarations(sv))) {
|
||||
allDecls.push(styleValueDeclarations(sv));
|
||||
}
|
||||
allMedia = concat(allMedia, styleValueMediaRules(sv));
|
||||
allPseudo = concat(allPseudo, styleValuePseudoRules(sv));
|
||||
allKf = concat(allKf, styleValueKeyframes_(sv)); } }
|
||||
return (function() {
|
||||
var hashInput = join(";", allDecls);
|
||||
{ var _c = allMedia; for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
|
||||
{ var _c = allPseudo; for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
|
||||
{ var _c = allKf; for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
|
||||
return (function() {
|
||||
var cn = (String("sx-") + String(hashStyle(hashInput)));
|
||||
var merged = makeStyleValue_(cn, join(";", allDecls), allMedia, allPseudo, allKf);
|
||||
injectStyleValue(merged, []);
|
||||
return merged;
|
||||
})();
|
||||
})();
|
||||
})()); };
|
||||
|
||||
|
||||
// === Transpiled from boot ===
|
||||
|
||||
// HEAD_HOIST_SELECTOR
|
||||
@@ -2363,30 +2121,10 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
})();
|
||||
return setSxCompCookie(hash);
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// init-style-dict
|
||||
var initStyleDict = function() { return (function() {
|
||||
var scripts = queryStyleScripts();
|
||||
return forEach(function(s) { return (isSxTruthy(!isProcessed(s, "styles")) ? (markProcessed(s, "styles"), (function() {
|
||||
var text = domTextContent(s);
|
||||
var hash = domGetAttr(s, "data-hash");
|
||||
return (isSxTruthy(isNil(hash)) ? (isSxTruthy((isSxTruthy(text) && !isEmpty(trim(text)))) ? parseAndLoadStyleDict(text) : NIL) : (function() {
|
||||
var hasInline = (isSxTruthy(text) && !isEmpty(trim(text)));
|
||||
(function() {
|
||||
var cachedHash = localStorageGet("sx-styles-hash");
|
||||
return (isSxTruthy((cachedHash == hash)) ? (isSxTruthy(hasInline) ? (localStorageSet("sx-styles-src", text), parseAndLoadStyleDict(text), logInfo("styles: downloaded (cookie stale)")) : (function() {
|
||||
var cached = localStorageGet("sx-styles-src");
|
||||
return (isSxTruthy(cached) ? (parseAndLoadStyleDict(cached), logInfo((String("styles: cached (") + String(hash) + String(")")))) : (clearSxStylesCookie(), browserReload()));
|
||||
})()) : (isSxTruthy(hasInline) ? (localStorageSet("sx-styles-hash", hash), localStorageSet("sx-styles-src", text), parseAndLoadStyleDict(text), logInfo((String("styles: downloaded (") + String(hash) + String(")")))) : (localStorageRemove("sx-styles-hash"), localStorageRemove("sx-styles-src"), clearSxStylesCookie(), browserReload())));
|
||||
})();
|
||||
return setSxStylesCookie(hash);
|
||||
})());
|
||||
})()) : NIL); }, scripts);
|
||||
})(); };
|
||||
|
||||
// boot-init
|
||||
var bootInit = function() { return (initCssTracking(), initStyleDict(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||
var bootInit = function() { return (initCssTracking(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// === Transpiled from deps (component dependency analysis) ===
|
||||
@@ -3290,70 +3028,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeStyleValue_(cn, decls, media, pseudo, kf) {
|
||||
return new StyleValue(cn, decls || "", media || [], pseudo || [], kf || []);
|
||||
}
|
||||
|
||||
function styleValueDeclarations(sv) { return sv.declarations; }
|
||||
function styleValueMediaRules(sv) { return sv.mediaRules; }
|
||||
function styleValuePseudoRules(sv) { return sv.pseudoRules; }
|
||||
function styleValueKeyframes_(sv) { return sv.keyframes; }
|
||||
|
||||
function injectStyleValue(sv, atoms) {
|
||||
if (_injectedStyles[sv.className]) return;
|
||||
_injectedStyles[sv.className] = true;
|
||||
|
||||
if (!_hasDom) return;
|
||||
var cssTarget = document.getElementById("sx-css");
|
||||
if (!cssTarget) return;
|
||||
|
||||
var rules = [];
|
||||
// Child-selector atoms are now routed to pseudoRules by the resolver
|
||||
// with selector ">:not(:first-child)", so base declarations are always
|
||||
// applied directly to the class.
|
||||
if (sv.declarations) {
|
||||
rules.push("." + sv.className + "{" + sv.declarations + "}");
|
||||
}
|
||||
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
|
||||
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
|
||||
if (sel.indexOf("&") >= 0) {
|
||||
rules.push(sel.replace(/&/g, "." + sv.className) + "{" + decls + "}");
|
||||
} else {
|
||||
rules.push("." + sv.className + sel + "{" + decls + "}");
|
||||
}
|
||||
}
|
||||
for (var mi = 0; mi < sv.mediaRules.length; mi++) {
|
||||
rules.push("@media " + sv.mediaRules[mi][0] + "{." + sv.className + "{" + sv.mediaRules[mi][1] + "}}");
|
||||
}
|
||||
for (var ki = 0; ki < sv.keyframes.length; ki++) {
|
||||
rules.push(sv.keyframes[ki][1]);
|
||||
}
|
||||
cssTarget.textContent += rules.join("");
|
||||
}
|
||||
|
||||
// Replace stub css primitive with real CSSX implementation
|
||||
PRIMITIVES["css"] = function() {
|
||||
var atoms = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (isNil(a) || a === false) continue;
|
||||
atoms.push(isKw(a) ? a.name : String(a));
|
||||
}
|
||||
if (!atoms.length) return NIL;
|
||||
return resolveStyle(atoms);
|
||||
};
|
||||
|
||||
PRIMITIVES["merge-styles"] = function() {
|
||||
var valid = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||
}
|
||||
if (!valid.length) return NIL;
|
||||
if (valid.length === 1) return valid[0];
|
||||
return mergeStyleValues(valid);
|
||||
};
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — Boot (mount, hydrate, scripts, cookies)
|
||||
// =========================================================================
|
||||
@@ -3410,12 +3084,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
r.querySelectorAll('script[type="text/sx"]'));
|
||||
}
|
||||
|
||||
function queryStyleScripts() {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
document.querySelectorAll('script[type="text/sx-styles"]'));
|
||||
}
|
||||
|
||||
// --- localStorage ---
|
||||
|
||||
function localStorageGet(key) {
|
||||
@@ -3441,14 +3109,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
function setSxStylesCookie(hash) {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
||||
}
|
||||
|
||||
function clearSxStylesCookie() {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
// --- Env helpers ---
|
||||
|
||||
function parseEnvAttr(el) {
|
||||
@@ -3493,12 +3153,6 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
}
|
||||
}
|
||||
|
||||
function parseAndLoadStyleDict(text) {
|
||||
try { loadStyleDict(JSON.parse(text)); }
|
||||
catch (e) { if (typeof console !== "undefined") console.warn("[sx-ref] style dict parse error", e); }
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Post-transpilation fixups
|
||||
// =========================================================================
|
||||
@@ -3675,7 +3329,7 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
transitiveIoRefs: transitiveIoRefs,
|
||||
computeAllIoRefs: computeAllIoRefs,
|
||||
componentPure_p: componentPure_p,
|
||||
_version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
|
||||
_version: "ref-2.0 (boot+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -63,23 +63,12 @@
|
||||
function RawHTML(html) { this.html = html; }
|
||||
RawHTML.prototype._raw = true;
|
||||
|
||||
/** CSSX StyleValue — generated CSS class with rules. */
|
||||
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
|
||||
this.className = className;
|
||||
this.declarations = declarations || "";
|
||||
this.mediaRules = mediaRules || [];
|
||||
this.pseudoRules = pseudoRules || [];
|
||||
this.keyframes = keyframes || [];
|
||||
}
|
||||
StyleValue.prototype._styleValue = true;
|
||||
|
||||
function isSym(x) { return x && x._sym === true; }
|
||||
function isKw(x) { return x && x._kw === true; }
|
||||
function isLambda(x) { return x && x._lambda === true; }
|
||||
function isComponent(x) { return x && x._component === true; }
|
||||
function isMacro(x) { return x && x._macro === true; }
|
||||
function isRaw(x) { return x && x._raw === true; }
|
||||
function isStyleValue(x) { return x && x._styleValue === true; }
|
||||
|
||||
// --- Parser ---
|
||||
|
||||
@@ -416,227 +405,6 @@
|
||||
return String(s).split(",").map(function(x) { return x.trim(); }).filter(function(x) { return x; });
|
||||
};
|
||||
|
||||
// --- CSSX Style Dictionary + Resolver ---
|
||||
|
||||
var _styleAtoms = {}; // atom → CSS declarations
|
||||
var _pseudoVariants = {}; // variant → CSS pseudo-selector
|
||||
var _responsiveBreakpoints = {}; // variant → media query
|
||||
var _styleKeyframes = {}; // name → @keyframes rule
|
||||
var _arbitraryPatterns = []; // [{re: RegExp, tmpl: string}, ...]
|
||||
var _childSelectorPrefixes = []; // ["space-x-", "space-y-", ...]
|
||||
var _styleCache = {}; // atoms-key → StyleValue
|
||||
var _injectedStyles = {}; // className → true (already in <style>)
|
||||
|
||||
function _loadStyleDict(data) {
|
||||
_styleAtoms = data.a || {};
|
||||
_pseudoVariants = data.v || {};
|
||||
_responsiveBreakpoints = data.b || {};
|
||||
_styleKeyframes = data.k || {};
|
||||
_childSelectorPrefixes = data.c || [];
|
||||
_arbitraryPatterns = [];
|
||||
var pats = data.p || [];
|
||||
for (var i = 0; i < pats.length; i++) {
|
||||
_arbitraryPatterns.push({ re: new RegExp("^" + pats[i][0] + "$"), tmpl: pats[i][1] });
|
||||
}
|
||||
_styleCache = {};
|
||||
}
|
||||
|
||||
function _splitVariant(atom) {
|
||||
// Check responsive prefix first
|
||||
for (var bp in _responsiveBreakpoints) {
|
||||
var prefix = bp + ":";
|
||||
if (atom.indexOf(prefix) === 0) {
|
||||
var rest = atom.substring(prefix.length);
|
||||
for (var pv in _pseudoVariants) {
|
||||
var inner = pv + ":";
|
||||
if (rest.indexOf(inner) === 0) return [bp + ":" + pv, rest.substring(inner.length)];
|
||||
}
|
||||
return [bp, rest];
|
||||
}
|
||||
}
|
||||
for (var pv2 in _pseudoVariants) {
|
||||
var prefix2 = pv2 + ":";
|
||||
if (atom.indexOf(prefix2) === 0) return [pv2, atom.substring(prefix2.length)];
|
||||
}
|
||||
return [null, atom];
|
||||
}
|
||||
|
||||
function _resolveAtom(atom) {
|
||||
var decls = _styleAtoms[atom];
|
||||
if (decls !== undefined) return decls;
|
||||
// Dynamic keyframes: animate-{name} → animation-name:{name}
|
||||
if (atom.indexOf("animate-") === 0) {
|
||||
var kfName = atom.substring(8);
|
||||
if (_styleKeyframes[kfName]) return "animation-name:" + kfName;
|
||||
}
|
||||
for (var i = 0; i < _arbitraryPatterns.length; i++) {
|
||||
var m = atom.match(_arbitraryPatterns[i].re);
|
||||
if (m) {
|
||||
var result = _arbitraryPatterns[i].tmpl;
|
||||
for (var j = 1; j < m.length; j++) result = result.replace("{" + (j - 1) + "}", m[j]);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _isChildSelectorAtom(atom) {
|
||||
for (var i = 0; i < _childSelectorPrefixes.length; i++) {
|
||||
if (atom.indexOf(_childSelectorPrefixes[i]) === 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** SHA-256 hash (first 6 hex chars) for content-addressed class names. */
|
||||
function _hashStyle(input) {
|
||||
// Simple FNV-1a 32-bit hash — fast, deterministic, good distribution
|
||||
var h = 0x811c9dc5;
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
h ^= input.charCodeAt(i);
|
||||
h = (h * 0x01000193) >>> 0;
|
||||
}
|
||||
return h.toString(16).padStart(8, "0").substring(0, 6);
|
||||
}
|
||||
|
||||
function _resolveStyle(atoms) {
|
||||
var key = atoms.join("\0");
|
||||
if (_styleCache[key]) return _styleCache[key];
|
||||
|
||||
var baseDecls = [], mediaRules = [], pseudoRules = [], kfNeeded = [];
|
||||
for (var i = 0; i < atoms.length; i++) {
|
||||
var a = atoms[i];
|
||||
if (!a) continue;
|
||||
if (a.charAt(0) === ":") a = a.substring(1);
|
||||
|
||||
var parts = _splitVariant(a);
|
||||
var variant = parts[0], base = parts[1];
|
||||
var decls = _resolveAtom(base);
|
||||
if (!decls) continue;
|
||||
|
||||
// Check keyframes
|
||||
if (base.indexOf("animate-") === 0) {
|
||||
var kfName = base.substring(8);
|
||||
if (_styleKeyframes[kfName]) kfNeeded.push([kfName, _styleKeyframes[kfName]]);
|
||||
}
|
||||
|
||||
if (variant === null) {
|
||||
baseDecls.push(decls);
|
||||
} else if (_responsiveBreakpoints[variant]) {
|
||||
mediaRules.push([_responsiveBreakpoints[variant], decls]);
|
||||
} else if (_pseudoVariants[variant]) {
|
||||
pseudoRules.push([_pseudoVariants[variant], decls]);
|
||||
} else {
|
||||
// Compound variant: "sm:hover" → split
|
||||
var vparts = variant.split(":");
|
||||
var mediaPart = null, pseudoPart = null;
|
||||
for (var vi = 0; vi < vparts.length; vi++) {
|
||||
if (_responsiveBreakpoints[vparts[vi]]) mediaPart = _responsiveBreakpoints[vparts[vi]];
|
||||
else if (_pseudoVariants[vparts[vi]]) pseudoPart = _pseudoVariants[vparts[vi]];
|
||||
}
|
||||
if (mediaPart) mediaRules.push([mediaPart, decls]);
|
||||
if (pseudoPart) pseudoRules.push([pseudoPart, decls]);
|
||||
if (!mediaPart && !pseudoPart) baseDecls.push(decls);
|
||||
}
|
||||
}
|
||||
|
||||
// Build hash input
|
||||
var hashInput = baseDecls.join(";");
|
||||
for (var mi = 0; mi < mediaRules.length; mi++) hashInput += "@" + mediaRules[mi][0] + "{" + mediaRules[mi][1] + "}";
|
||||
for (var pi = 0; pi < pseudoRules.length; pi++) hashInput += pseudoRules[pi][0] + "{" + pseudoRules[pi][1] + "}";
|
||||
for (var ki = 0; ki < kfNeeded.length; ki++) hashInput += kfNeeded[ki][1];
|
||||
|
||||
var cn = "sx-" + _hashStyle(hashInput);
|
||||
var sv = new StyleValue(cn, baseDecls.join(";"), mediaRules, pseudoRules, kfNeeded);
|
||||
_styleCache[key] = sv;
|
||||
|
||||
// Inject CSS rules into <style id="sx-css">
|
||||
_injectStyleValue(sv, atoms);
|
||||
return sv;
|
||||
}
|
||||
|
||||
function _injectStyleValue(sv, atoms) {
|
||||
if (_injectedStyles[sv.className]) return;
|
||||
_injectedStyles[sv.className] = true;
|
||||
|
||||
var cssTarget = document.getElementById("sx-css");
|
||||
if (!cssTarget) return;
|
||||
|
||||
var rules = [];
|
||||
if (sv.declarations) {
|
||||
// Check if any atoms need child selectors
|
||||
var hasChild = false;
|
||||
if (atoms) {
|
||||
for (var ai = 0; ai < atoms.length; ai++) {
|
||||
if (_isChildSelectorAtom(atoms[ai])) { hasChild = true; break; }
|
||||
}
|
||||
}
|
||||
if (hasChild) {
|
||||
rules.push("." + sv.className + ">:not(:first-child){" + sv.declarations + "}");
|
||||
} else {
|
||||
rules.push("." + sv.className + "{" + sv.declarations + "}");
|
||||
}
|
||||
}
|
||||
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
|
||||
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
|
||||
if (sel.indexOf("&") >= 0) {
|
||||
rules.push(sel.replace(/&/g, "." + sv.className) + "{" + decls + "}");
|
||||
} else {
|
||||
rules.push("." + sv.className + sel + "{" + decls + "}");
|
||||
}
|
||||
}
|
||||
for (var mi = 0; mi < sv.mediaRules.length; mi++) {
|
||||
rules.push("@media " + sv.mediaRules[mi][0] + "{." + sv.className + "{" + sv.mediaRules[mi][1] + "}}");
|
||||
}
|
||||
for (var ki = 0; ki < sv.keyframes.length; ki++) {
|
||||
rules.push(sv.keyframes[ki][1]);
|
||||
}
|
||||
cssTarget.textContent += rules.join("");
|
||||
}
|
||||
|
||||
function _mergeStyleValues(styles) {
|
||||
if (styles.length === 1) return styles[0];
|
||||
var allDecls = [], allMedia = [], allPseudo = [], allKf = [];
|
||||
var allAtoms = [];
|
||||
for (var i = 0; i < styles.length; i++) {
|
||||
var sv = styles[i];
|
||||
if (sv.declarations) allDecls.push(sv.declarations);
|
||||
allMedia = allMedia.concat(sv.mediaRules);
|
||||
allPseudo = allPseudo.concat(sv.pseudoRules);
|
||||
allKf = allKf.concat(sv.keyframes);
|
||||
}
|
||||
var hashInput = allDecls.join(";");
|
||||
for (var mi = 0; mi < allMedia.length; mi++) hashInput += "@" + allMedia[mi][0] + "{" + allMedia[mi][1] + "}";
|
||||
for (var pi = 0; pi < allPseudo.length; pi++) hashInput += allPseudo[pi][0] + "{" + allPseudo[pi][1] + "}";
|
||||
for (var ki = 0; ki < allKf.length; ki++) hashInput += allKf[ki][1];
|
||||
var cn = "sx-" + _hashStyle(hashInput);
|
||||
var merged = new StyleValue(cn, allDecls.join(";"), allMedia, allPseudo, allKf);
|
||||
_injectStyleValue(merged, allAtoms);
|
||||
return merged;
|
||||
}
|
||||
|
||||
// css primitive: (css :flex :gap-4 :hover:bg-sky-200) → StyleValue
|
||||
PRIMITIVES["css"] = function () {
|
||||
var atoms = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (isNil(a) || a === false) continue;
|
||||
atoms.push(isKw(a) ? a.name : String(a));
|
||||
}
|
||||
if (!atoms.length) return NIL;
|
||||
return _resolveStyle(atoms);
|
||||
};
|
||||
|
||||
// merge-styles: combine multiple StyleValues
|
||||
PRIMITIVES["merge-styles"] = function () {
|
||||
var valid = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||
}
|
||||
if (!valid.length) return NIL;
|
||||
if (valid.length === 1) return valid[0];
|
||||
return _mergeStyleValues(valid);
|
||||
};
|
||||
|
||||
// --- Evaluator ---
|
||||
|
||||
/** Unwrap thunks by re-entering the evaluator until we get an actual value. */
|
||||
@@ -892,30 +660,6 @@
|
||||
return value;
|
||||
};
|
||||
|
||||
SPECIAL_FORMS["defkeyframes"] = function (expr, env) {
|
||||
var kfName = expr[1].name;
|
||||
var steps = [];
|
||||
for (var i = 2; i < expr.length; i++) {
|
||||
var step = expr[i];
|
||||
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
|
||||
var body = sxEval(step[1], env);
|
||||
var decls = isStyleValue(body) ? body.declarations : String(body);
|
||||
steps.push(selector + "{" + decls + "}");
|
||||
}
|
||||
var kfRule = "@keyframes " + kfName + "{" + steps.join("") + "}";
|
||||
|
||||
// Register in keyframes dict for animate-{name} lookup
|
||||
_styleKeyframes[kfName] = kfRule;
|
||||
_styleCache = {}; // Clear cache so new keyframes are picked up
|
||||
|
||||
// Build a StyleValue for the animation
|
||||
var cn = "sx-" + _hashStyle(kfRule);
|
||||
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
|
||||
_injectStyleValue(sv, []);
|
||||
env[kfName] = sv;
|
||||
return sv;
|
||||
};
|
||||
|
||||
SPECIAL_FORMS["defcomp"] = function (expr, env) {
|
||||
var nameSym = expr[1];
|
||||
var compName = nameSym.name.replace(/^~/, "");
|
||||
@@ -1209,7 +953,6 @@
|
||||
|
||||
RENDER_FORMS["define"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defstyle"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defkeyframes"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defcomp"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defmacro"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
RENDER_FORMS["defhandler"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
|
||||
@@ -1413,7 +1156,6 @@
|
||||
? document.createElementNS(ns, tag)
|
||||
: (SVG_TAGS[tag] ? document.createElementNS(SVG_NS, tag) : document.createElement(tag));
|
||||
|
||||
var extraClass = null;
|
||||
var i = 0;
|
||||
while (i < args.length) {
|
||||
var arg = args[i];
|
||||
@@ -1422,11 +1164,6 @@
|
||||
var attrVal = sxEval(args[i + 1], env);
|
||||
i += 2;
|
||||
if (isNil(attrVal) || attrVal === false) continue;
|
||||
// :style StyleValue → convert to class
|
||||
if (attrName === "style" && isStyleValue(attrVal)) {
|
||||
extraClass = attrVal.className;
|
||||
continue;
|
||||
}
|
||||
if (BOOLEAN_ATTRS[attrName]) {
|
||||
if (attrVal) el.setAttribute(attrName, "");
|
||||
} else if (attrVal === true) {
|
||||
@@ -1443,12 +1180,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Merge StyleValue class into element's class attribute
|
||||
if (extraClass) {
|
||||
var existing = el.getAttribute("class");
|
||||
el.setAttribute("class", existing ? existing + " " + extraClass : extraClass);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -1770,7 +1501,7 @@
|
||||
},
|
||||
|
||||
// For testing / sx-test.js
|
||||
_types: { NIL: NIL, Symbol: Symbol, Keyword: Keyword, Lambda: Lambda, Component: Component, RawHTML: RawHTML, StyleValue: StyleValue },
|
||||
_types: { NIL: NIL, Symbol: Symbol, Keyword: Keyword, Lambda: Lambda, Component: Component, RawHTML: RawHTML },
|
||||
_eval: sxEval,
|
||||
_expandMacro: expandMacro,
|
||||
_callLambda: function (fn, args, env) { return trampoline(callLambda(fn, args, env)); },
|
||||
@@ -3116,79 +2847,10 @@
|
||||
document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
// --- sx-styles-hash cookie helpers ---
|
||||
|
||||
function _setSxStylesCookie(hash) {
|
||||
document.cookie = "sx-styles-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
||||
}
|
||||
|
||||
function _clearSxStylesCookie() {
|
||||
document.cookie = "sx-styles-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
function _initStyleDict() {
|
||||
var scripts = document.querySelectorAll('script[type="text/sx-styles"]');
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
var s = scripts[i];
|
||||
if (s._sxProcessed) continue;
|
||||
s._sxProcessed = true;
|
||||
|
||||
var text = s.textContent;
|
||||
var hash = s.getAttribute("data-hash");
|
||||
if (!hash) {
|
||||
if (text && text.trim()) {
|
||||
try { _loadStyleDict(JSON.parse(text)); } catch (e) { console.warn("[sx.js] style dict parse error", e); }
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasInline = text && text.trim();
|
||||
try {
|
||||
var cachedHash = localStorage.getItem("sx-styles-hash");
|
||||
if (cachedHash === hash) {
|
||||
if (hasInline) {
|
||||
localStorage.setItem("sx-styles-src", text);
|
||||
_loadStyleDict(JSON.parse(text));
|
||||
console.log("[sx.js] styles: downloaded (cookie stale)");
|
||||
} else {
|
||||
var cached = localStorage.getItem("sx-styles-src");
|
||||
if (cached) {
|
||||
_loadStyleDict(JSON.parse(cached));
|
||||
console.log("[sx.js] styles: cached (" + hash + ")");
|
||||
} else {
|
||||
_clearSxStylesCookie();
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (hasInline) {
|
||||
localStorage.setItem("sx-styles-hash", hash);
|
||||
localStorage.setItem("sx-styles-src", text);
|
||||
_loadStyleDict(JSON.parse(text));
|
||||
console.log("[sx.js] styles: downloaded (" + hash + ")");
|
||||
} else {
|
||||
localStorage.removeItem("sx-styles-hash");
|
||||
localStorage.removeItem("sx-styles-src");
|
||||
_clearSxStylesCookie();
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (hasInline) {
|
||||
try { _loadStyleDict(JSON.parse(text)); } catch (e2) { console.warn("[sx.js] style dict parse error", e2); }
|
||||
}
|
||||
}
|
||||
_setSxStylesCookie(hash);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
var init = function () {
|
||||
console.log("[sx.js] v" + Sx.VERSION + " init");
|
||||
_initCssTracking();
|
||||
_initStyleDict();
|
||||
Sx.processScripts();
|
||||
Sx.hydrate();
|
||||
SxEngine.process();
|
||||
|
||||
@@ -45,7 +45,7 @@ import contextvars
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
|
||||
# When True, _aser expands known components server-side instead of serializing
|
||||
# them for client rendering. Set during page slot evaluation so Python-only
|
||||
@@ -425,11 +425,6 @@ async def _asf_defstyle(expr, env, ctx):
|
||||
return _sf_defstyle(expr, env)
|
||||
|
||||
|
||||
async def _asf_defkeyframes(expr, env, ctx):
|
||||
from .evaluator import _sf_defkeyframes
|
||||
return _sf_defkeyframes(expr, env)
|
||||
|
||||
|
||||
async def _asf_defmacro(expr, env, ctx):
|
||||
from .evaluator import _sf_defmacro
|
||||
return _sf_defmacro(expr, env)
|
||||
@@ -568,7 +563,6 @@ _ASYNC_SPECIAL_FORMS: dict[str, Any] = {
|
||||
"fn": _asf_lambda,
|
||||
"define": _asf_define,
|
||||
"defstyle": _asf_defstyle,
|
||||
"defkeyframes": _asf_defkeyframes,
|
||||
"defcomp": _asf_defcomp,
|
||||
"defmacro": _asf_defmacro,
|
||||
"defhandler": _asf_defhandler,
|
||||
@@ -884,18 +878,6 @@ async def _arender_element(
|
||||
children.append(arg)
|
||||
i += 1
|
||||
|
||||
# Handle :style StyleValue — convert to class and register CSS rule
|
||||
style_val = attrs.get("style")
|
||||
if isinstance(style_val, StyleValue):
|
||||
from .css_registry import register_generated_rule
|
||||
register_generated_rule(style_val)
|
||||
existing_class = attrs.get("class")
|
||||
if existing_class and existing_class is not NIL and existing_class is not False:
|
||||
attrs["class"] = f"{existing_class} {style_val.class_name}"
|
||||
else:
|
||||
attrs["class"] = style_val.class_name
|
||||
del attrs["style"]
|
||||
|
||||
class_val = attrs.get("class")
|
||||
if class_val is not None and class_val is not NIL and class_val is not False:
|
||||
collector = css_class_collector.get(None)
|
||||
@@ -1120,7 +1102,6 @@ _ASYNC_RENDER_FORMS: dict[str, Any] = {
|
||||
"do": _arsf_begin,
|
||||
"define": _arsf_define,
|
||||
"defstyle": _arsf_define,
|
||||
"defkeyframes": _arsf_define,
|
||||
"defcomp": _arsf_define,
|
||||
"defmacro": _arsf_define,
|
||||
"defhandler": _arsf_define,
|
||||
@@ -1333,7 +1314,7 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
|
||||
expanded = _expand_macro(val, expr[1:], env)
|
||||
return await _aser(expanded, env, ctx)
|
||||
if isinstance(val, Component):
|
||||
if _expand_components.get() or not val.is_pure:
|
||||
if _expand_components.get() or val.render_target == "server":
|
||||
return await _aser_component(val, expr[1:], env, ctx)
|
||||
return await _aser_call(name, expr[1:], env, ctx)
|
||||
|
||||
@@ -1441,41 +1422,35 @@ async def _aser_call(
|
||||
|
||||
try:
|
||||
parts = [name]
|
||||
extra_class: str | None = None # from :style StyleValue conversion
|
||||
extra_class: str | None = None
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||
val = await _aser(args[i + 1], env, ctx)
|
||||
if val is not NIL and val is not None:
|
||||
# :style StyleValue → convert to :class and register CSS
|
||||
if arg.name == "style" and isinstance(val, StyleValue):
|
||||
from .css_registry import register_generated_rule
|
||||
register_generated_rule(val)
|
||||
extra_class = val.class_name
|
||||
else:
|
||||
parts.append(f":{arg.name}")
|
||||
# Plain list → serialize for the client.
|
||||
# Rendered items (SxExpr) → wrap in (<> ...) fragment.
|
||||
# Data items (dicts, strings, numbers) → (list ...)
|
||||
# so the client gets an iterable array, not a
|
||||
# DocumentFragment that breaks map/filter.
|
||||
if isinstance(val, list):
|
||||
live = [v for v in val
|
||||
if v is not NIL and v is not None]
|
||||
items = [serialize(v) for v in live]
|
||||
if not items:
|
||||
parts.append("nil")
|
||||
elif any(isinstance(v, SxExpr) for v in live):
|
||||
parts.append(
|
||||
"(<> " + " ".join(items) + ")"
|
||||
)
|
||||
else:
|
||||
parts.append(
|
||||
"(list " + " ".join(items) + ")"
|
||||
)
|
||||
parts.append(f":{arg.name}")
|
||||
# Plain list → serialize for the client.
|
||||
# Rendered items (SxExpr) → wrap in (<> ...) fragment.
|
||||
# Data items (dicts, strings, numbers) → (list ...)
|
||||
# so the client gets an iterable array, not a
|
||||
# DocumentFragment that breaks map/filter.
|
||||
if isinstance(val, list):
|
||||
live = [v for v in val
|
||||
if v is not NIL and v is not None]
|
||||
items = [serialize(v) for v in live]
|
||||
if not items:
|
||||
parts.append("nil")
|
||||
elif any(isinstance(v, SxExpr) for v in live):
|
||||
parts.append(
|
||||
"(<> " + " ".join(items) + ")"
|
||||
)
|
||||
else:
|
||||
parts.append(serialize(val))
|
||||
parts.append(
|
||||
"(list " + " ".join(items) + ")"
|
||||
)
|
||||
else:
|
||||
parts.append(serialize(val))
|
||||
i += 2
|
||||
else:
|
||||
result = await _aser(arg, env, ctx)
|
||||
@@ -1725,7 +1700,6 @@ _ASER_FORMS: dict[str, Any] = {
|
||||
"fn": _assf_lambda,
|
||||
"define": _assf_define,
|
||||
"defstyle": _assf_define,
|
||||
"defkeyframes": _assf_define,
|
||||
"defcomp": _assf_define,
|
||||
"defmacro": _assf_define,
|
||||
"defhandler": _assf_define,
|
||||
|
||||
@@ -94,10 +94,10 @@ def validate_helper(service: str, name: str) -> None:
|
||||
def validate_boundary_value(value: Any, context: str = "") -> None:
|
||||
"""Validate that a value is an allowed SX boundary type.
|
||||
|
||||
Allowed: int, float, str, bool, None/NIL, list, dict, SxExpr, StyleValue.
|
||||
Allowed: int, float, str, bool, None/NIL, list, dict, SxExpr.
|
||||
NOT allowed: datetime, ORM models, Quart objects, raw callables.
|
||||
"""
|
||||
from .types import NIL, StyleValue
|
||||
from .types import NIL
|
||||
from .parser import SxExpr
|
||||
|
||||
if value is None or value is NIL:
|
||||
@@ -106,8 +106,6 @@ def validate_boundary_value(value: Any, context: str = "") -> None:
|
||||
return
|
||||
if isinstance(value, SxExpr):
|
||||
return
|
||||
if isinstance(value, StyleValue):
|
||||
return
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
validate_boundary_value(item, context)
|
||||
|
||||
@@ -147,48 +147,6 @@ def scan_classes_from_sx(source: str) -> set[str]:
|
||||
return classes
|
||||
|
||||
|
||||
def register_generated_rule(style_val: Any) -> None:
|
||||
"""Register a generated StyleValue's CSS rules in the registry.
|
||||
|
||||
This allows generated class names (``sx-a3f2c1``) to flow through
|
||||
the existing ``lookup_rules()`` → ``SX-Css`` delta pipeline.
|
||||
"""
|
||||
from .style_dict import CHILD_SELECTOR_ATOMS
|
||||
cn = style_val.class_name
|
||||
if cn in _REGISTRY:
|
||||
return # already registered
|
||||
|
||||
parts: list[str] = []
|
||||
|
||||
# Base declarations
|
||||
if style_val.declarations:
|
||||
parts.append(f".{cn}{{{style_val.declarations}}}")
|
||||
|
||||
# Pseudo-class rules
|
||||
for sel, decls in style_val.pseudo_rules:
|
||||
if sel.startswith("::"):
|
||||
parts.append(f".{cn}{sel}{{{decls}}}")
|
||||
elif "&" in sel:
|
||||
# group-hover pattern: ":is(.group:hover) &" → .group:hover .sx-abc
|
||||
expanded = sel.replace("&", f".{cn}")
|
||||
parts.append(f"{expanded}{{{decls}}}")
|
||||
else:
|
||||
parts.append(f".{cn}{sel}{{{decls}}}")
|
||||
|
||||
# Media-query rules
|
||||
for query, decls in style_val.media_rules:
|
||||
parts.append(f"@media {query}{{.{cn}{{{decls}}}}}")
|
||||
|
||||
# Keyframes
|
||||
for _name, kf_rule in style_val.keyframes:
|
||||
parts.append(kf_rule)
|
||||
|
||||
rule_text = "".join(parts)
|
||||
order = len(_RULE_ORDER) + 10000 # after all tw.css rules
|
||||
_REGISTRY[cn] = rule_text
|
||||
_RULE_ORDER[cn] = order
|
||||
|
||||
|
||||
def registry_loaded() -> bool:
|
||||
"""True if the registry has been populated."""
|
||||
return bool(_REGISTRY)
|
||||
|
||||
@@ -198,6 +198,22 @@ def compute_all_io_refs(env: dict[str, Any], io_names: set[str]) -> None:
|
||||
_compute_all_io_refs_fallback(env, io_names)
|
||||
|
||||
|
||||
def page_render_plan(page_sx: str, env: dict[str, Any], io_names: set[str] | None = None) -> dict[str, Any]:
|
||||
"""Compute the render plan for a page.
|
||||
|
||||
Returns dict with:
|
||||
- "components": {name: "server"|"client", ...}
|
||||
- "server": [names rendered server-side]
|
||||
- "client": [names rendered client-side]
|
||||
- "io-deps": [IO primitive names needed by server components]
|
||||
"""
|
||||
if io_names is None:
|
||||
io_names = get_all_io_names()
|
||||
from .ref.sx_ref import page_render_plan as _ref_prp
|
||||
plan = _ref_prp(page_sx, env, list(io_names))
|
||||
return plan
|
||||
|
||||
|
||||
def get_all_io_names() -> set[str]:
|
||||
"""Build the complete set of IO primitive names from all boundary tiers.
|
||||
|
||||
|
||||
@@ -493,9 +493,9 @@ def _sf_define(expr: list, env: dict) -> Any:
|
||||
|
||||
|
||||
def _sf_defstyle(expr: list, env: dict) -> Any:
|
||||
"""``(defstyle card-base (css :rounded-xl :bg-white :shadow))``
|
||||
"""``(defstyle card-base ...)``
|
||||
|
||||
Evaluates body → StyleValue, binds to name in env.
|
||||
Evaluates body and binds to name in env.
|
||||
"""
|
||||
if len(expr) < 3:
|
||||
raise EvalError("defstyle requires name and body")
|
||||
@@ -507,65 +507,8 @@ def _sf_defstyle(expr: list, env: dict) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _sf_defkeyframes(expr: list, env: dict) -> Any:
|
||||
"""``(defkeyframes fade-in (from (css :opacity-0)) (to (css :opacity-100)))``
|
||||
|
||||
Builds @keyframes rule from steps, registers it, and binds the animation.
|
||||
"""
|
||||
from .types import StyleValue
|
||||
from .css_registry import register_generated_rule
|
||||
from .style_dict import KEYFRAMES
|
||||
|
||||
if len(expr) < 3:
|
||||
raise EvalError("defkeyframes requires name and at least one step")
|
||||
name_sym = expr[1]
|
||||
if not isinstance(name_sym, Symbol):
|
||||
raise EvalError(f"defkeyframes name must be symbol, got {type(name_sym).__name__}")
|
||||
|
||||
kf_name = name_sym.name
|
||||
|
||||
# Build @keyframes rule from steps
|
||||
steps: list[str] = []
|
||||
for step_expr in expr[2:]:
|
||||
if not isinstance(step_expr, list) or len(step_expr) < 2:
|
||||
raise EvalError("defkeyframes step must be (selector (css ...))")
|
||||
selector = step_expr[0]
|
||||
if isinstance(selector, Symbol):
|
||||
selector = selector.name
|
||||
else:
|
||||
selector = str(selector)
|
||||
body = _trampoline(_eval(step_expr[1], env))
|
||||
if isinstance(body, StyleValue):
|
||||
decls = body.declarations
|
||||
elif isinstance(body, str):
|
||||
decls = body
|
||||
else:
|
||||
raise EvalError(f"defkeyframes step body must be css/string, got {type(body).__name__}")
|
||||
steps.append(f"{selector}{{{decls}}}")
|
||||
|
||||
kf_rule = f"@keyframes {kf_name}{{{' '.join(steps)}}}"
|
||||
|
||||
# Register in KEYFRAMES so animate-{name} works
|
||||
KEYFRAMES[kf_name] = kf_rule
|
||||
# Clear resolver cache so new keyframes are picked up
|
||||
from .style_resolver import _resolve_cached
|
||||
_resolve_cached.cache_clear()
|
||||
|
||||
# Create a StyleValue for the animation property
|
||||
import hashlib
|
||||
h = hashlib.sha256(kf_rule.encode()).hexdigest()[:6]
|
||||
sv = StyleValue(
|
||||
class_name=f"sx-{h}",
|
||||
declarations=f"animation-name:{kf_name}",
|
||||
keyframes=((kf_name, kf_rule),),
|
||||
)
|
||||
register_generated_rule(sv)
|
||||
env[kf_name] = sv
|
||||
return sv
|
||||
|
||||
|
||||
def _sf_defcomp(expr: list, env: dict) -> Component:
|
||||
"""``(defcomp ~name (&key param1 param2 &rest children) body)``"""
|
||||
"""``(defcomp ~name (&key ...) [:affinity :client|:server] body)``"""
|
||||
if len(expr) < 4:
|
||||
raise EvalError("defcomp requires name, params, and body")
|
||||
name_sym = expr[1]
|
||||
@@ -593,21 +536,38 @@ def _sf_defcomp(expr: list, env: dict) -> Component:
|
||||
params.append(p.name)
|
||||
else:
|
||||
params.append(p.name)
|
||||
# Skip children param name after &rest
|
||||
elif isinstance(p, str):
|
||||
params.append(p)
|
||||
|
||||
# Body is always last element; keyword annotations between params and body
|
||||
body = expr[-1]
|
||||
affinity = _defcomp_kwarg(expr, "affinity", "auto")
|
||||
|
||||
comp = Component(
|
||||
name=comp_name,
|
||||
params=params,
|
||||
has_children=has_children,
|
||||
body=expr[3],
|
||||
body=body,
|
||||
closure=dict(env),
|
||||
affinity=affinity,
|
||||
)
|
||||
env[name_sym.name] = comp
|
||||
return comp
|
||||
|
||||
|
||||
def _defcomp_kwarg(expr: list, key: str, default: str) -> str:
|
||||
"""Extract a keyword annotation from defcomp, e.g. :affinity :client."""
|
||||
# Scan from index 3 to second-to-last for :key value pairs
|
||||
for i in range(3, len(expr) - 1):
|
||||
item = expr[i]
|
||||
if isinstance(item, Keyword) and item.name == key:
|
||||
val = expr[i + 1]
|
||||
if isinstance(val, Keyword):
|
||||
return val.name
|
||||
return str(val)
|
||||
return default
|
||||
|
||||
|
||||
def _sf_begin(expr: list, env: dict) -> Any:
|
||||
if len(expr) < 2:
|
||||
return NIL
|
||||
@@ -1060,7 +1020,6 @@ _SPECIAL_FORMS: dict[str, Any] = {
|
||||
"fn": _sf_lambda,
|
||||
"define": _sf_define,
|
||||
"defstyle": _sf_defstyle,
|
||||
"defkeyframes": _sf_defkeyframes,
|
||||
"defcomp": _sf_defcomp,
|
||||
"defrelation": _sf_defrelation,
|
||||
"begin": _sf_begin,
|
||||
|
||||
@@ -645,7 +645,6 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details.
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-stone-50 text-stone-900">
|
||||
<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>
|
||||
<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>
|
||||
<script type="text/sx-pages">{pages_sx}</script>
|
||||
<script type="text/sx" data-mount="body">{page_sx}</script>
|
||||
@@ -738,6 +737,15 @@ def _build_pages_sx(service: str) -> str:
|
||||
|
||||
stream = "true" if page_def.stream else "false"
|
||||
|
||||
# Render plan: which components render where
|
||||
plan = page_def.render_plan
|
||||
if plan:
|
||||
server_sx = "(" + " ".join(_sx_literal(n) for n in plan.get("server", [])) + ")"
|
||||
client_sx = "(" + " ".join(_sx_literal(n) for n in plan.get("client", [])) + ")"
|
||||
render_plan_sx = "{:server " + server_sx + " :client " + client_sx + "}"
|
||||
else:
|
||||
render_plan_sx = "{:server () :client ()}"
|
||||
|
||||
entry = (
|
||||
"{:name " + _sx_literal(page_def.name)
|
||||
+ " :path " + _sx_literal(page_def.path)
|
||||
@@ -746,6 +754,7 @@ def _build_pages_sx(service: str) -> str:
|
||||
+ " :stream " + stream
|
||||
+ " :layout " + _sx_literal(layout_id)
|
||||
+ " :io-deps " + io_deps_sx
|
||||
+ " :render-plan " + render_plan_sx
|
||||
+ " :content " + _sx_literal(content_src)
|
||||
+ " :deps " + deps_sx
|
||||
+ " :closure " + closure_sx + "}"
|
||||
@@ -820,14 +829,6 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
import logging
|
||||
logging.getLogger("sx").warning("Pretty-print page_sx failed: %s", e)
|
||||
|
||||
# Style dictionary for client-side css primitive
|
||||
styles_hash = _get_style_dict_hash()
|
||||
client_styles_hash = _get_sx_styles_cookie()
|
||||
if not _is_dev_mode() and client_styles_hash and client_styles_hash == styles_hash:
|
||||
styles_json = "" # Client has cached version
|
||||
else:
|
||||
styles_json = _build_style_dict_json()
|
||||
|
||||
# Page registry for client-side routing
|
||||
import logging
|
||||
_plog = logging.getLogger("sx.pages")
|
||||
@@ -844,8 +845,6 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
csrf=_html_escape(csrf),
|
||||
component_hash=component_hash,
|
||||
component_defs=component_defs,
|
||||
styles_hash=styles_hash,
|
||||
styles_json=styles_json,
|
||||
pages_sx=pages_sx,
|
||||
page_sx=page_sx,
|
||||
sx_css=sx_css,
|
||||
@@ -907,10 +906,6 @@ def sx_page_streaming_parts(ctx: dict, page_html: str, *,
|
||||
title = ctx.get("base_title", "Rose Ash")
|
||||
csrf = _get_csrf_token()
|
||||
|
||||
styles_hash = _get_style_dict_hash()
|
||||
client_styles_hash = _get_sx_styles_cookie()
|
||||
styles_json = "" if (not _is_dev_mode() and client_styles_hash == styles_hash) else _build_style_dict_json()
|
||||
|
||||
import logging
|
||||
from quart import current_app
|
||||
pages_sx = _build_pages_sx(current_app.name)
|
||||
@@ -953,7 +948,6 @@ def sx_page_streaming_parts(ctx: dict, page_html: str, *,
|
||||
'</style>\n'
|
||||
'</head>\n'
|
||||
'<body class="bg-stone-50 text-stone-900">\n'
|
||||
f'<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>\n'
|
||||
f'<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>\n'
|
||||
f'<script type="text/sx-pages">{pages_sx}</script>\n'
|
||||
# Server-rendered HTML — suspense placeholders are real DOM elements
|
||||
@@ -989,58 +983,6 @@ def sx_streaming_resolve_script(suspension_id: str, sx_source: str,
|
||||
|
||||
|
||||
_SCRIPT_HASH_CACHE: dict[str, str] = {}
|
||||
_STYLE_DICT_JSON: str = ""
|
||||
_STYLE_DICT_HASH: str = ""
|
||||
|
||||
|
||||
def _build_style_dict_json() -> str:
|
||||
"""Build compact JSON style dictionary for client-side css primitive."""
|
||||
global _STYLE_DICT_JSON, _STYLE_DICT_HASH
|
||||
if _STYLE_DICT_JSON:
|
||||
return _STYLE_DICT_JSON
|
||||
|
||||
import json
|
||||
from .style_dict import (
|
||||
STYLE_ATOMS, PSEUDO_VARIANTS, RESPONSIVE_BREAKPOINTS,
|
||||
KEYFRAMES, ARBITRARY_PATTERNS, CHILD_SELECTOR_ATOMS,
|
||||
)
|
||||
|
||||
# Derive child selector prefixes from CHILD_SELECTOR_ATOMS
|
||||
prefixes = set()
|
||||
for atom in CHILD_SELECTOR_ATOMS:
|
||||
# "space-y-4" → "space-y-", "divide-y" → "divide-"
|
||||
for sep in ("space-x-", "space-y-", "divide-x", "divide-y"):
|
||||
if atom.startswith(sep):
|
||||
prefixes.add(sep)
|
||||
break
|
||||
|
||||
data = {
|
||||
"a": STYLE_ATOMS,
|
||||
"v": PSEUDO_VARIANTS,
|
||||
"b": RESPONSIVE_BREAKPOINTS,
|
||||
"k": KEYFRAMES,
|
||||
"p": ARBITRARY_PATTERNS,
|
||||
"c": sorted(prefixes),
|
||||
}
|
||||
_STYLE_DICT_JSON = json.dumps(data, separators=(",", ":"))
|
||||
_STYLE_DICT_HASH = hashlib.md5(_STYLE_DICT_JSON.encode()).hexdigest()[:8]
|
||||
return _STYLE_DICT_JSON
|
||||
|
||||
|
||||
def _get_style_dict_hash() -> str:
|
||||
"""Get the hash of the style dictionary JSON."""
|
||||
if not _STYLE_DICT_HASH:
|
||||
_build_style_dict_json()
|
||||
return _STYLE_DICT_HASH
|
||||
|
||||
|
||||
def _get_sx_styles_cookie() -> str:
|
||||
"""Read the sx-styles-hash cookie from the current request."""
|
||||
try:
|
||||
from quart import request
|
||||
return request.cookies.get("sx-styles-hash", "")
|
||||
except RuntimeError:
|
||||
return ""
|
||||
|
||||
|
||||
def _script_hash(filename: str) -> str:
|
||||
|
||||
@@ -27,7 +27,7 @@ from __future__ import annotations
|
||||
import contextvars
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline
|
||||
|
||||
def _eval(expr, env):
|
||||
@@ -510,19 +510,6 @@ def _render_element(tag: str, args: list, env: dict[str, Any]) -> str:
|
||||
children.append(arg)
|
||||
i += 1
|
||||
|
||||
# Handle :style StyleValue — convert to class and register CSS rule
|
||||
style_val = attrs.get("style")
|
||||
if isinstance(style_val, StyleValue):
|
||||
from .css_registry import register_generated_rule
|
||||
register_generated_rule(style_val)
|
||||
# Merge into :class
|
||||
existing_class = attrs.get("class")
|
||||
if existing_class and existing_class is not NIL and existing_class is not False:
|
||||
attrs["class"] = f"{existing_class} {style_val.class_name}"
|
||||
else:
|
||||
attrs["class"] = style_val.class_name
|
||||
del attrs["style"]
|
||||
|
||||
# Collect CSS classes if collector is active
|
||||
class_val = attrs.get("class")
|
||||
if class_val is not None and class_val is not NIL and class_val is not False:
|
||||
|
||||
@@ -840,6 +840,25 @@ async def execute_page_streaming_oob(
|
||||
# Blueprint mounting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def compute_page_render_plans(service_name: str) -> None:
|
||||
"""Pre-compute and cache render plans for all pages in a service.
|
||||
|
||||
Must be called after components are loaded (compute_all_deps/io_refs done)
|
||||
and pages are registered. Stores plans on PageDef.render_plan.
|
||||
"""
|
||||
from .parser import serialize
|
||||
from .deps import page_render_plan, get_all_io_names
|
||||
from .jinja_bridge import _COMPONENT_ENV
|
||||
|
||||
io_names = get_all_io_names()
|
||||
pages = get_all_pages(service_name)
|
||||
for page_def in pages.values():
|
||||
if page_def.content_expr is not None:
|
||||
content_src = serialize(page_def.content_expr)
|
||||
page_def.render_plan = page_render_plan(content_src, _COMPONENT_ENV, io_names)
|
||||
logger.info("Computed render plans for %d pages in %s", len(pages), service_name)
|
||||
|
||||
|
||||
def auto_mount_pages(app: Any, service_name: str) -> None:
|
||||
"""Auto-mount all registered defpages for a service directly on the app.
|
||||
|
||||
@@ -849,6 +868,10 @@ def auto_mount_pages(app: Any, service_name: str) -> None:
|
||||
Also mounts the /sx/data/ endpoint for client-side data fetching.
|
||||
"""
|
||||
pages = get_all_pages(service_name)
|
||||
|
||||
# Pre-compute render plans (which components render where)
|
||||
compute_page_render_plans(service_name)
|
||||
|
||||
for page_def in pages.values():
|
||||
_mount_one_page(app, service_name, page_def)
|
||||
logger.info("Auto-mounted %d defpages for %s", len(pages), service_name)
|
||||
|
||||
@@ -62,7 +62,7 @@ class SxExpr(str):
|
||||
# Errors
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ESCAPE_MAP = {"n": "\n", "t": "\t", '"': '"', "\\": "\\", "/": "/"}
|
||||
_ESCAPE_MAP = {"n": "\n", "t": "\t", "r": "\r", "0": "\0", '"': '"', "\\": "\\", "/": "/"}
|
||||
|
||||
|
||||
def _unescape_string(s: str) -> str:
|
||||
@@ -359,7 +359,9 @@ def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str:
|
||||
expr.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
.replace("\0", "\\0")
|
||||
.replace("</script", "<\\/script")
|
||||
)
|
||||
return f'"{escaped}"'
|
||||
@@ -380,11 +382,6 @@ def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str:
|
||||
items.append(serialize(v, indent, pretty))
|
||||
return "{" + " ".join(items) + "}"
|
||||
|
||||
# StyleValue — serialize as class name string
|
||||
from .types import StyleValue
|
||||
if isinstance(expr, StyleValue):
|
||||
return f'"{expr.class_name}"'
|
||||
|
||||
# _RawHTML — pre-rendered HTML; wrap as (raw! "...") for SX wire format
|
||||
from .html import _RawHTML
|
||||
if isinstance(expr, _RawHTML):
|
||||
@@ -392,6 +389,8 @@ def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str:
|
||||
expr.html.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\0", "\\0")
|
||||
.replace("</script", "<\\/script")
|
||||
)
|
||||
return f'(raw! "{escaped}")'
|
||||
|
||||
@@ -89,37 +89,6 @@ def prim_strip_tags(s: str) -> str:
|
||||
return re.sub(r"<[^>]+>", "", s)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# stdlib.style
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@register_primitive("css")
|
||||
def prim_css(*args: Any) -> Any:
|
||||
"""``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue."""
|
||||
from .types import Keyword
|
||||
from .style_resolver import resolve_style
|
||||
atoms = tuple(
|
||||
(a.name if isinstance(a, Keyword) else str(a))
|
||||
for a in args if a is not None and a is not NIL and a is not False
|
||||
)
|
||||
if not atoms:
|
||||
return NIL
|
||||
return resolve_style(atoms)
|
||||
|
||||
|
||||
@register_primitive("merge-styles")
|
||||
def prim_merge_styles(*styles: Any) -> Any:
|
||||
"""``(merge-styles style1 style2)`` → merged StyleValue."""
|
||||
from .types import StyleValue
|
||||
from .style_resolver import merge_styles
|
||||
valid = [s for s in styles if isinstance(s, StyleValue)]
|
||||
if not valid:
|
||||
return NIL
|
||||
if len(valid) == 1:
|
||||
return valid[0]
|
||||
return merge_styles(valid)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# stdlib.debug
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -38,7 +38,6 @@ Only these types may cross the host-SX boundary:
|
||||
| list | `list` | `Array` | `Vec<SxValue>` |
|
||||
| dict | `dict` | `Object` / `Map` | `HashMap<String, SxValue>` |
|
||||
| sx-source | `SxExpr` wrapper | `string` | `String` |
|
||||
| style-value | `StyleValue` | `StyleValue` | `StyleValue` |
|
||||
|
||||
**NOT allowed:** ORM models, datetime objects, request objects, raw callables, framework types. Convert at the edge before crossing.
|
||||
|
||||
|
||||
@@ -52,9 +52,6 @@
|
||||
(create-fragment)
|
||||
(render-dom-list expr env ns))
|
||||
|
||||
;; Style value → text of class name
|
||||
"style-value" (create-text-node (style-value-class expr))
|
||||
|
||||
;; Fallback
|
||||
:else (create-text-node (str expr)))))
|
||||
|
||||
@@ -147,8 +144,7 @@
|
||||
(let ((new-ns (cond (= tag "svg") SVG_NS
|
||||
(= tag "math") MATH_NS
|
||||
:else ns))
|
||||
(el (dom-create-element tag new-ns))
|
||||
(extra-class nil))
|
||||
(el (dom-create-element tag new-ns)))
|
||||
|
||||
;; Process args: keywords → attrs, others → children
|
||||
(reduce
|
||||
@@ -168,9 +164,6 @@
|
||||
;; nil or false → skip
|
||||
(or (nil? attr-val) (= attr-val false))
|
||||
nil
|
||||
;; :style StyleValue → convert to class
|
||||
(and (= attr-name "style") (style-value? attr-val))
|
||||
(set! extra-class (style-value-class attr-val))
|
||||
;; Boolean attr
|
||||
(contains? BOOLEAN_ATTRS attr-name)
|
||||
(when attr-val (dom-set-attr el attr-name ""))
|
||||
@@ -190,12 +183,6 @@
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
|
||||
;; Merge StyleValue class
|
||||
(when extra-class
|
||||
(let ((existing (dom-get-attr el "class")))
|
||||
(dom-set-attr el "class"
|
||||
(if existing (str existing " " extra-class) extra-class))))
|
||||
|
||||
el)))
|
||||
|
||||
|
||||
@@ -297,7 +284,7 @@
|
||||
|
||||
(define RENDER_DOM_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
|
||||
(define render-dom-form?
|
||||
@@ -450,7 +437,6 @@
|
||||
;;
|
||||
;; From render.sx:
|
||||
;; HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, definition-form?
|
||||
;; style-value?, style-value-class
|
||||
;;
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"boolean" (if val "true" "false")
|
||||
"list" (render-list-to-html val env)
|
||||
"raw-html" (raw-html-content val)
|
||||
"style-value" (style-value-class val)
|
||||
:else (escape-html (str val)))))
|
||||
|
||||
|
||||
@@ -51,7 +50,7 @@
|
||||
|
||||
(define RENDER_HTML_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
|
||||
(define render-html-form?
|
||||
@@ -293,7 +292,7 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Inherited from render.sx:
|
||||
;; escape-html, escape-attr, raw-html-content, style-value?, style-value-class
|
||||
;; escape-html, escape-attr, raw-html-content
|
||||
;;
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
|
||||
|
||||
@@ -26,7 +26,7 @@ import contextvars
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from ..types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
|
||||
from ..types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from ..parser import SxExpr, serialize
|
||||
from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io
|
||||
from ..html import (
|
||||
@@ -250,18 +250,6 @@ async def _arender_element(tag, args, env, ctx):
|
||||
children.append(arg)
|
||||
i += 1
|
||||
|
||||
# StyleValue → class
|
||||
style_val = attrs.get("style")
|
||||
if isinstance(style_val, StyleValue):
|
||||
from ..css_registry import register_generated_rule
|
||||
register_generated_rule(style_val)
|
||||
existing = attrs.get("class")
|
||||
if existing and existing is not NIL and existing is not False:
|
||||
attrs["class"] = f"{existing} {style_val.class_name}"
|
||||
else:
|
||||
attrs["class"] = style_val.class_name
|
||||
del attrs["style"]
|
||||
|
||||
class_val = attrs.get("class")
|
||||
if class_val is not None and class_val is not NIL and class_val is not False:
|
||||
collector = css_class_collector.get(None)
|
||||
@@ -464,7 +452,6 @@ _ASYNC_RENDER_FORMS = {
|
||||
"do": _arsf_begin,
|
||||
"define": _arsf_define,
|
||||
"defstyle": _arsf_define,
|
||||
"defkeyframes": _arsf_define,
|
||||
"defcomp": _arsf_define,
|
||||
"defmacro": _arsf_define,
|
||||
"defhandler": _arsf_define,
|
||||
@@ -716,23 +703,18 @@ async def _aser_call(name, args, env, ctx):
|
||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||
val = await _aser(args[i + 1], env, ctx)
|
||||
if val is not NIL and val is not None:
|
||||
if arg.name == "style" and isinstance(val, StyleValue):
|
||||
from ..css_registry import register_generated_rule
|
||||
register_generated_rule(val)
|
||||
extra_class = val.class_name
|
||||
else:
|
||||
parts.append(f":{arg.name}")
|
||||
if isinstance(val, list):
|
||||
live = [v for v in val if v is not NIL and v is not None]
|
||||
items = [serialize(v) for v in live]
|
||||
if not items:
|
||||
parts.append("nil")
|
||||
elif any(isinstance(v, SxExpr) for v in live):
|
||||
parts.append("(<> " + " ".join(items) + ")")
|
||||
else:
|
||||
parts.append("(list " + " ".join(items) + ")")
|
||||
parts.append(f":{arg.name}")
|
||||
if isinstance(val, list):
|
||||
live = [v for v in val if v is not NIL and v is not None]
|
||||
items = [serialize(v) for v in live]
|
||||
if not items:
|
||||
parts.append("nil")
|
||||
elif any(isinstance(v, SxExpr) for v in live):
|
||||
parts.append("(<> " + " ".join(items) + ")")
|
||||
else:
|
||||
parts.append(serialize(val))
|
||||
parts.append("(list " + " ".join(items) + ")")
|
||||
else:
|
||||
parts.append(serialize(val))
|
||||
i += 2
|
||||
else:
|
||||
result = await _aser(arg, env, ctx)
|
||||
@@ -996,7 +978,6 @@ _ASER_FORMS = {
|
||||
"fn": _assf_lambda,
|
||||
"define": _assf_define,
|
||||
"defstyle": _assf_define,
|
||||
"defkeyframes": _assf_define,
|
||||
"defcomp": _assf_define,
|
||||
"defmacro": _assf_define,
|
||||
"defhandler": _assf_define,
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
;;
|
||||
;; Handles the browser startup lifecycle:
|
||||
;; 1. CSS tracking init
|
||||
;; 2. Style dictionary loading (from <script type="text/sx-styles">)
|
||||
;; 3. Component script processing (from <script type="text/sx">)
|
||||
;; 4. Hydration of [data-sx] elements
|
||||
;; 5. Engine element processing
|
||||
;; 2. Component script processing (from <script type="text/sx">)
|
||||
;; 3. Hydration of [data-sx] elements
|
||||
;; 4. Engine element processing
|
||||
;;
|
||||
;; Also provides the public mounting/hydration API:
|
||||
;; mount, hydrate, update, render-component
|
||||
;;
|
||||
;; Depends on:
|
||||
;; cssx.sx — load-style-dict
|
||||
;; orchestration.sx — process-elements, engine-init
|
||||
;; adapter-dom.sx — render-to-dom
|
||||
;; render.sx — shared registries
|
||||
@@ -275,58 +273,6 @@
|
||||
(set-sx-comp-cookie hash))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Style dictionary initialization
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define init-style-dict
|
||||
(fn ()
|
||||
;; Process <script type="text/sx-styles"> tags with caching.
|
||||
(let ((scripts (query-style-scripts)))
|
||||
(for-each
|
||||
(fn (s)
|
||||
(when (not (is-processed? s "styles"))
|
||||
(mark-processed! s "styles")
|
||||
(let ((text (dom-text-content s))
|
||||
(hash (dom-get-attr s "data-hash")))
|
||||
(if (nil? hash)
|
||||
;; No hash — just parse inline
|
||||
(when (and text (not (empty? (trim text))))
|
||||
(parse-and-load-style-dict text))
|
||||
;; Hash-based caching
|
||||
(let ((has-inline (and text (not (empty? (trim text))))))
|
||||
(let ((cached-hash (local-storage-get "sx-styles-hash")))
|
||||
(if (= cached-hash hash)
|
||||
;; Cache hit
|
||||
(if has-inline
|
||||
(do
|
||||
(local-storage-set "sx-styles-src" text)
|
||||
(parse-and-load-style-dict text)
|
||||
(log-info "styles: downloaded (cookie stale)"))
|
||||
(let ((cached (local-storage-get "sx-styles-src")))
|
||||
(if cached
|
||||
(do
|
||||
(parse-and-load-style-dict cached)
|
||||
(log-info (str "styles: cached (" hash ")")))
|
||||
(do
|
||||
(clear-sx-styles-cookie)
|
||||
(browser-reload)))))
|
||||
;; Cache miss
|
||||
(if has-inline
|
||||
(do
|
||||
(local-storage-set "sx-styles-hash" hash)
|
||||
(local-storage-set "sx-styles-src" text)
|
||||
(parse-and-load-style-dict text)
|
||||
(log-info (str "styles: downloaded (" hash ")")))
|
||||
(do
|
||||
(local-storage-remove "sx-styles-hash")
|
||||
(local-storage-remove "sx-styles-src")
|
||||
(clear-sx-styles-cookie)
|
||||
(browser-reload)))))
|
||||
(set-sx-styles-cookie hash))))))
|
||||
scripts))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Page registry for client-side routing
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -375,7 +321,6 @@
|
||||
(do
|
||||
(log-info (str "sx-browser " SX_VERSION))
|
||||
(init-css-tracking)
|
||||
(init-style-dict)
|
||||
(process-page-scripts)
|
||||
(process-sx-scripts nil)
|
||||
(sx-hydrate-elements nil)
|
||||
@@ -389,9 +334,6 @@
|
||||
;; From orchestration.sx:
|
||||
;; process-elements, init-css-tracking
|
||||
;;
|
||||
;; From cssx.sx:
|
||||
;; load-style-dict
|
||||
;;
|
||||
;; === DOM / Render ===
|
||||
;; (resolve-mount-target target) → Element (string → querySelector, else identity)
|
||||
;; (sx-render-with-env source extra-env) → DOM node (parse + render with componentEnv + extra)
|
||||
@@ -420,7 +362,6 @@
|
||||
;;
|
||||
;; === Script queries ===
|
||||
;; (query-sx-scripts root) → list of <script type="text/sx"> elements
|
||||
;; (query-style-scripts) → list of <script type="text/sx-styles"> elements
|
||||
;; (query-page-scripts) → list of <script type="text/sx-pages"> elements
|
||||
;;
|
||||
;; === localStorage ===
|
||||
@@ -431,8 +372,6 @@
|
||||
;; === Cookies ===
|
||||
;; (set-sx-comp-cookie hash) → void
|
||||
;; (clear-sx-comp-cookie) → void
|
||||
;; (set-sx-styles-cookie hash) → void
|
||||
;; (clear-sx-styles-cookie) → void
|
||||
;;
|
||||
;; === Env ===
|
||||
;; (parse-env-attr el) → dict (parse data-sx-env JSON attr)
|
||||
@@ -444,8 +383,6 @@
|
||||
;; (log-parse-error label text err) → void (diagnostic parse error)
|
||||
;;
|
||||
;; === JSON parsing ===
|
||||
;; (parse-and-load-style-dict text) → void (JSON.parse + load-style-dict)
|
||||
;;
|
||||
;; === Processing markers ===
|
||||
;; (mark-processed! el key) → void
|
||||
;; (is-processed? el key) → boolean
|
||||
|
||||
@@ -29,6 +29,20 @@ from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
|
||||
# SX → JavaScript transpiler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# JS reserved words — SX parameter/variable names that collide get _ suffix
|
||||
_JS_RESERVED = frozenset({
|
||||
"abstract", "arguments", "await", "boolean", "break", "byte", "case",
|
||||
"catch", "char", "class", "const", "continue", "debugger", "default",
|
||||
"delete", "do", "double", "else", "enum", "eval", "export", "extends",
|
||||
"final", "finally", "float", "for", "function", "goto", "if",
|
||||
"implements", "import", "in", "instanceof", "int", "interface", "let",
|
||||
"long", "native", "new", "package", "private", "protected", "public",
|
||||
"return", "short", "static", "super", "switch", "synchronized", "this",
|
||||
"throw", "throws", "transient", "try", "typeof", "var", "void",
|
||||
"volatile", "while", "with", "yield",
|
||||
})
|
||||
|
||||
|
||||
class JSEmitter:
|
||||
"""Transpile an SX AST node to JavaScript source code."""
|
||||
|
||||
@@ -114,6 +128,7 @@ class JSEmitter:
|
||||
"component-closure": "componentClosure",
|
||||
"component-has-children?": "componentHasChildren",
|
||||
"component-name": "componentName",
|
||||
"component-affinity": "componentAffinity",
|
||||
"macro-params": "macroParams",
|
||||
"macro-rest-param": "macroRestParam",
|
||||
"macro-body": "macroBody",
|
||||
@@ -176,6 +191,7 @@ class JSEmitter:
|
||||
"sf-lambda": "sfLambda",
|
||||
"sf-define": "sfDefine",
|
||||
"sf-defcomp": "sfDefcomp",
|
||||
"defcomp-kwarg": "defcompKwarg",
|
||||
"sf-defmacro": "sfDefmacro",
|
||||
"sf-begin": "sfBegin",
|
||||
"sf-quote": "sfQuote",
|
||||
@@ -191,10 +207,6 @@ class JSEmitter:
|
||||
"ho-every": "hoEvery",
|
||||
"ho-for-each": "hoForEach",
|
||||
"sf-defstyle": "sfDefstyle",
|
||||
"sf-defkeyframes": "sfDefkeyframes",
|
||||
"build-keyframes": "buildKeyframes",
|
||||
"style-value?": "isStyleValue",
|
||||
"style-value-class": "styleValueClass",
|
||||
"kf-name": "kfName",
|
||||
"special-form?": "isSpecialForm",
|
||||
"ho-form?": "isHoForm",
|
||||
@@ -278,6 +290,7 @@ class JSEmitter:
|
||||
"for-each-indexed": "forEachIndexed",
|
||||
"index-of": "indexOf_",
|
||||
"component-has-children?": "componentHasChildren",
|
||||
"component-affinity": "componentAffinity",
|
||||
# engine.sx
|
||||
"ENGINE_VERBS": "ENGINE_VERBS",
|
||||
"DEFAULT_SWAP": "DEFAULT_SWAP",
|
||||
@@ -443,32 +456,6 @@ class JSEmitter:
|
||||
"format-date": "formatDate",
|
||||
"format-decimal": "formatDecimal",
|
||||
"parse-int": "parseInt_",
|
||||
# cssx.sx
|
||||
"_style-atoms": "_styleAtoms",
|
||||
"_pseudo-variants": "_pseudoVariants",
|
||||
"_responsive-breakpoints": "_responsiveBreakpoints",
|
||||
"_style-keyframes": "_styleKeyframes",
|
||||
"_arbitrary-patterns": "_arbitraryPatterns",
|
||||
"_child-selector-prefixes": "_childSelectorPrefixes",
|
||||
"_style-cache": "_styleCache",
|
||||
"_injected-styles": "_injectedStyles",
|
||||
"load-style-dict": "loadStyleDict",
|
||||
"split-variant": "splitVariant",
|
||||
"resolve-atom": "resolveAtom",
|
||||
"is-child-selector-atom?": "isChildSelectorAtom",
|
||||
"hash-style": "hashStyle",
|
||||
"resolve-style": "resolveStyle",
|
||||
"merge-style-values": "mergeStyleValues",
|
||||
"fnv1a-hash": "fnv1aHash",
|
||||
"compile-regex": "compileRegex",
|
||||
"regex-match": "regexMatch",
|
||||
"regex-replace-groups": "regexReplaceGroups",
|
||||
"make-style-value": "makeStyleValue_",
|
||||
"style-value-declarations": "styleValueDeclarations",
|
||||
"style-value-media-rules": "styleValueMediaRules",
|
||||
"style-value-pseudo-rules": "styleValuePseudoRules",
|
||||
"style-value-keyframes": "styleValueKeyframes_",
|
||||
"inject-style-value": "injectStyleValue",
|
||||
# boot.sx
|
||||
"HEAD_HOIST_SELECTOR": "HEAD_HOIST_SELECTOR",
|
||||
"hoist-head-elements-full": "hoistHeadElementsFull",
|
||||
@@ -478,7 +465,6 @@ class JSEmitter:
|
||||
"sx-render-component": "sxRenderComponent",
|
||||
"process-sx-scripts": "processSxScripts",
|
||||
"process-component-script": "processComponentScript",
|
||||
"init-style-dict": "initStyleDict",
|
||||
"SX_VERSION": "SX_VERSION",
|
||||
"boot-init": "bootInit",
|
||||
"resolve-suspense": "resolveSuspense",
|
||||
@@ -490,21 +476,17 @@ class JSEmitter:
|
||||
"set-document-title": "setDocumentTitle",
|
||||
"remove-head-element": "removeHeadElement",
|
||||
"query-sx-scripts": "querySxScripts",
|
||||
"query-style-scripts": "queryStyleScripts",
|
||||
"local-storage-get": "localStorageGet",
|
||||
"local-storage-set": "localStorageSet",
|
||||
"local-storage-remove": "localStorageRemove",
|
||||
"set-sx-comp-cookie": "setSxCompCookie",
|
||||
"clear-sx-comp-cookie": "clearSxCompCookie",
|
||||
"set-sx-styles-cookie": "setSxStylesCookie",
|
||||
"clear-sx-styles-cookie": "clearSxStylesCookie",
|
||||
"parse-env-attr": "parseEnvAttr",
|
||||
"store-env-attr": "storeEnvAttr",
|
||||
"to-kebab": "toKebab",
|
||||
"log-info": "logInfo",
|
||||
"log-warn": "logWarn",
|
||||
"log-parse-error": "logParseError",
|
||||
"parse-and-load-style-dict": "parseAndLoadStyleDict",
|
||||
"_page-routes": "_pageRoutes",
|
||||
"process-page-scripts": "processPageScripts",
|
||||
"query-page-scripts": "queryPageScripts",
|
||||
@@ -531,6 +513,8 @@ class JSEmitter:
|
||||
"transitive-io-refs": "transitiveIoRefs",
|
||||
"compute-all-io-refs": "computeAllIoRefs",
|
||||
"component-pure?": "componentPure_p",
|
||||
"render-target": "renderTarget",
|
||||
"page-render-plan": "pageRenderPlan",
|
||||
# router.sx
|
||||
"split-path-segments": "splitPathSegments",
|
||||
"make-route-segment": "makeRouteSegment",
|
||||
@@ -552,6 +536,9 @@ class JSEmitter:
|
||||
parts = result.split("-")
|
||||
if len(parts) > 1:
|
||||
result = parts[0] + "".join(p.capitalize() for p in parts[1:])
|
||||
# Escape JS reserved words
|
||||
if result in _JS_RESERVED:
|
||||
result = result + "_"
|
||||
return result
|
||||
|
||||
# --- List emission ---
|
||||
@@ -1018,7 +1005,7 @@ class JSEmitter:
|
||||
return str(expr)
|
||||
|
||||
def _js_string(self, s: str) -> str:
|
||||
return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + '"'
|
||||
return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t").replace("\0", "\\0") + '"'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1044,7 +1031,6 @@ ADAPTER_FILES = {
|
||||
"dom": ("adapter-dom.sx", "adapter-dom"),
|
||||
"engine": ("engine.sx", "engine"),
|
||||
"orchestration": ("orchestration.sx","orchestration"),
|
||||
"cssx": ("cssx.sx", "cssx"),
|
||||
"boot": ("boot.sx", "boot"),
|
||||
}
|
||||
|
||||
@@ -1052,8 +1038,7 @@ ADAPTER_FILES = {
|
||||
ADAPTER_DEPS = {
|
||||
"engine": ["dom"],
|
||||
"orchestration": ["engine", "dom"],
|
||||
"cssx": [],
|
||||
"boot": ["dom", "engine", "orchestration", "cssx", "parser"],
|
||||
"boot": ["dom", "engine", "orchestration", "parser"],
|
||||
"parser": [],
|
||||
}
|
||||
|
||||
@@ -1292,7 +1277,7 @@ ASYNC_IO_JS = '''
|
||||
|
||||
// define/defcomp/defmacro — eval for side effects
|
||||
if (hname === "define" || hname === "defcomp" || hname === "defmacro" ||
|
||||
hname === "defstyle" || hname === "defkeyframes" || hname === "defhandler") {
|
||||
hname === "defstyle" || hname === "defhandler") {
|
||||
trampoline(evalExpr(expr, env));
|
||||
return null;
|
||||
}
|
||||
@@ -1414,11 +1399,7 @@ ASYNC_IO_JS = '''
|
||||
})(attrName, attrVal);
|
||||
} else {
|
||||
if (!isNil(attrVal) && attrVal !== false) {
|
||||
if (attrName === "class" && attrVal && attrVal._styleValue) {
|
||||
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
|
||||
} else if (attrName === "style" && attrVal && attrVal._styleValue) {
|
||||
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
|
||||
} else if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
|
||||
} else if (attrVal === true) {
|
||||
el.setAttribute(attrName, "");
|
||||
@@ -1829,7 +1810,6 @@ def compile_ref_to_js(
|
||||
"dom": PLATFORM_DOM_JS,
|
||||
"engine": PLATFORM_ENGINE_PURE_JS,
|
||||
"orchestration": PLATFORM_ORCHESTRATION_JS,
|
||||
"cssx": PLATFORM_CSSX_JS,
|
||||
"boot": PLATFORM_BOOT_JS,
|
||||
}
|
||||
|
||||
@@ -1864,7 +1844,7 @@ def compile_ref_to_js(
|
||||
("eval.sx", "eval"),
|
||||
("render.sx", "render (core)"),
|
||||
]
|
||||
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "cssx", "boot"):
|
||||
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"):
|
||||
if name in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES[name])
|
||||
for name in sorted(spec_mod_set):
|
||||
@@ -1895,7 +1875,6 @@ def compile_ref_to_js(
|
||||
has_dom = "dom" in adapter_set
|
||||
has_engine = "engine" in adapter_set
|
||||
has_orch = "orchestration" in adapter_set
|
||||
has_cssx = "cssx" in adapter_set
|
||||
has_boot = "boot" in adapter_set
|
||||
has_parser = "parser" in adapter_set
|
||||
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
||||
@@ -1937,7 +1916,7 @@ def compile_ref_to_js(
|
||||
# Platform JS for selected adapters
|
||||
if not has_dom:
|
||||
parts.append("\n var _hasDom = false;\n")
|
||||
for name in ("dom", "engine", "orchestration", "cssx", "boot"):
|
||||
for name in ("dom", "engine", "orchestration", "boot"):
|
||||
if name in adapter_set and name in adapter_platform:
|
||||
parts.append(adapter_platform[name])
|
||||
|
||||
@@ -1946,7 +1925,7 @@ def compile_ref_to_js(
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
if has_dom:
|
||||
parts.append(ASYNC_IO_JS)
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps, has_router))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router))
|
||||
parts.append(EPILOGUE)
|
||||
from datetime import datetime, timezone
|
||||
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
@@ -1995,12 +1974,13 @@ PREAMBLE = '''\
|
||||
}
|
||||
Lambda.prototype._lambda = true;
|
||||
|
||||
function Component(name, params, hasChildren, body, closure) {
|
||||
function Component(name, params, hasChildren, body, closure, affinity) {
|
||||
this.name = name;
|
||||
this.params = params;
|
||||
this.hasChildren = hasChildren;
|
||||
this.body = body;
|
||||
this.closure = closure || {};
|
||||
this.affinity = affinity || "auto";
|
||||
}
|
||||
Component.prototype._component = true;
|
||||
|
||||
@@ -2019,15 +1999,6 @@ PREAMBLE = '''\
|
||||
function RawHTML(html) { this.html = html; }
|
||||
RawHTML.prototype._raw = true;
|
||||
|
||||
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
|
||||
this.className = className;
|
||||
this.declarations = declarations || "";
|
||||
this.mediaRules = mediaRules || [];
|
||||
this.pseudoRules = pseudoRules || [];
|
||||
this.keyframes = keyframes || [];
|
||||
}
|
||||
StyleValue.prototype._styleValue = true;
|
||||
|
||||
function isSym(x) { return x != null && x._sym === true; }
|
||||
function isKw(x) { return x != null && x._kw === true; }
|
||||
|
||||
@@ -2224,30 +2195,6 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
|
||||
''',
|
||||
|
||||
"stdlib.style": '''
|
||||
// stdlib.style
|
||||
PRIMITIVES["css"] = function() {
|
||||
var atoms = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (isNil(a) || a === false) continue;
|
||||
atoms.push(isKw(a) ? a.name : String(a));
|
||||
}
|
||||
if (!atoms.length) return NIL;
|
||||
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
|
||||
};
|
||||
PRIMITIVES["merge-styles"] = function() {
|
||||
var valid = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||
}
|
||||
if (!valid.length) return NIL;
|
||||
if (valid.length === 1) return valid[0];
|
||||
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
|
||||
return new StyleValue("sx-merged", allDecls, [], [], []);
|
||||
};
|
||||
''',
|
||||
|
||||
"stdlib.debug": '''
|
||||
// stdlib.debug
|
||||
PRIMITIVES["assert"] = function(cond, msg) {
|
||||
@@ -2295,7 +2242,6 @@ PLATFORM_JS_PRE = '''
|
||||
if (x._component) return "component";
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
if (x._styleValue) return "style-value";
|
||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||
if (Array.isArray(x)) return "list";
|
||||
if (typeof x === "object") return "dict";
|
||||
@@ -2308,8 +2254,8 @@ PLATFORM_JS_PRE = '''
|
||||
function makeKeyword(n) { return new Keyword(n); }
|
||||
|
||||
function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); }
|
||||
function makeComponent(name, params, hasChildren, body, env) {
|
||||
return new Component(name, params, hasChildren, body, merge(env));
|
||||
function makeComponent(name, params, hasChildren, body, env, affinity) {
|
||||
return new Component(name, params, hasChildren, body, merge(env), affinity);
|
||||
}
|
||||
function makeMacro(params, restParam, body, env, name) {
|
||||
return new Macro(params, restParam, body, merge(env), name);
|
||||
@@ -2327,6 +2273,7 @@ PLATFORM_JS_PRE = '''
|
||||
function componentClosure(c) { return c.closure; }
|
||||
function componentHasChildren(c) { return c.hasChildren; }
|
||||
function componentName(c) { return c.name; }
|
||||
function componentAffinity(c) { return c.affinity || "auto"; }
|
||||
|
||||
function macroParams(m) { return m.params; }
|
||||
function macroRestParam(m) { return m.restParam; }
|
||||
@@ -2342,27 +2289,6 @@ PLATFORM_JS_PRE = '''
|
||||
function isComponent(x) { return x != null && x._component === true; }
|
||||
function isMacro(x) { return x != null && x._macro === true; }
|
||||
|
||||
function isStyleValue(x) { return x != null && x._styleValue === true; }
|
||||
function styleValueClass(x) { return x.className; }
|
||||
function styleValue_p(x) { return x != null && x._styleValue === true; }
|
||||
|
||||
function buildKeyframes(kfName, steps, env) {
|
||||
// Platform implementation of defkeyframes
|
||||
var parts = [];
|
||||
for (var i = 0; i < steps.length; i++) {
|
||||
var step = steps[i];
|
||||
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
|
||||
var body = trampoline(evalExpr(step[1], env));
|
||||
var decls = isStyleValue(body) ? body.declarations : String(body);
|
||||
parts.push(selector + "{" + decls + "}");
|
||||
}
|
||||
var kfRule = "@keyframes " + kfName + "{" + parts.join("") + "}";
|
||||
var cn = "sx-ref-kf-" + kfName;
|
||||
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
|
||||
env[kfName] = sv;
|
||||
return sv;
|
||||
}
|
||||
|
||||
function envHas(env, name) { return name in env; }
|
||||
function envGet(env, name) { return env[name]; }
|
||||
function envSet(env, name, val) { env[name] = val; }
|
||||
@@ -2488,7 +2414,7 @@ PLATFORM_JS_POST = '''
|
||||
function isSpecialForm(n) { return n in {
|
||||
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
|
||||
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
|
||||
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
|
||||
"defhandler":1,"begin":1,"do":1,
|
||||
"quote":1,"quasiquote":1,"->":1,"set!":1
|
||||
}; }
|
||||
function isHoForm(n) { return n in {
|
||||
@@ -2499,7 +2425,7 @@ PLATFORM_JS_POST = '''
|
||||
|
||||
function isDefinitionForm(name) {
|
||||
return name === "define" || name === "defcomp" || name === "defmacro" ||
|
||||
name === "defstyle" || name === "defkeyframes" || name === "defhandler";
|
||||
name === "defstyle" || name === "defhandler";
|
||||
}
|
||||
|
||||
function indexOf_(s, ch) {
|
||||
@@ -2877,11 +2803,7 @@ PLATFORM_DOM_JS = """
|
||||
var attrVal = trampoline(evalExpr(args[i + 1], env));
|
||||
i++; // skip value
|
||||
if (isNil(attrVal) || attrVal === false) continue;
|
||||
if (attrName === "class" && attrVal && attrVal._styleValue) {
|
||||
extraClasses.push(attrVal.className);
|
||||
} else if (attrName === "style" && attrVal && attrVal._styleValue) {
|
||||
extraClasses.push(attrVal.className);
|
||||
} else if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (contains(BOOLEAN_ATTRS, attrName)) {
|
||||
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
|
||||
} else if (attrVal === true) {
|
||||
el.setAttribute(attrName, "");
|
||||
@@ -3763,102 +3685,6 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
}
|
||||
"""
|
||||
|
||||
PLATFORM_CSSX_JS = """
|
||||
// =========================================================================
|
||||
// Platform interface — CSSX (style dictionary)
|
||||
// =========================================================================
|
||||
|
||||
function fnv1aHash(input) {
|
||||
var h = 0x811c9dc5;
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
h ^= input.charCodeAt(i);
|
||||
h = (h * 0x01000193) >>> 0;
|
||||
}
|
||||
return h.toString(16).padStart(8, "0").substring(0, 6);
|
||||
}
|
||||
|
||||
function compileRegex(pattern) {
|
||||
try { return new RegExp(pattern); } catch (e) { return null; }
|
||||
}
|
||||
|
||||
function regexMatch(re, s) {
|
||||
if (!re) return NIL;
|
||||
var m = s.match(re);
|
||||
return m ? Array.prototype.slice.call(m) : NIL;
|
||||
}
|
||||
|
||||
function regexReplaceGroups(tmpl, match) {
|
||||
var result = tmpl;
|
||||
for (var j = 1; j < match.length; j++) {
|
||||
result = result.split("{" + (j - 1) + "}").join(match[j]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeStyleValue_(cn, decls, media, pseudo, kf) {
|
||||
return new StyleValue(cn, decls || "", media || [], pseudo || [], kf || []);
|
||||
}
|
||||
|
||||
function styleValueDeclarations(sv) { return sv.declarations; }
|
||||
function styleValueMediaRules(sv) { return sv.mediaRules; }
|
||||
function styleValuePseudoRules(sv) { return sv.pseudoRules; }
|
||||
function styleValueKeyframes_(sv) { return sv.keyframes; }
|
||||
|
||||
function injectStyleValue(sv, atoms) {
|
||||
if (_injectedStyles[sv.className]) return;
|
||||
_injectedStyles[sv.className] = true;
|
||||
|
||||
if (!_hasDom) return;
|
||||
var cssTarget = document.getElementById("sx-css");
|
||||
if (!cssTarget) return;
|
||||
|
||||
var rules = [];
|
||||
// Child-selector atoms are now routed to pseudoRules by the resolver
|
||||
// with selector ">:not(:first-child)", so base declarations are always
|
||||
// applied directly to the class.
|
||||
if (sv.declarations) {
|
||||
rules.push("." + sv.className + "{" + sv.declarations + "}");
|
||||
}
|
||||
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
|
||||
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
|
||||
if (sel.indexOf("&") >= 0) {
|
||||
rules.push(sel.replace(/&/g, "." + sv.className) + "{" + decls + "}");
|
||||
} else {
|
||||
rules.push("." + sv.className + sel + "{" + decls + "}");
|
||||
}
|
||||
}
|
||||
for (var mi = 0; mi < sv.mediaRules.length; mi++) {
|
||||
rules.push("@media " + sv.mediaRules[mi][0] + "{." + sv.className + "{" + sv.mediaRules[mi][1] + "}}");
|
||||
}
|
||||
for (var ki = 0; ki < sv.keyframes.length; ki++) {
|
||||
rules.push(sv.keyframes[ki][1]);
|
||||
}
|
||||
cssTarget.textContent += rules.join("");
|
||||
}
|
||||
|
||||
// Replace stub css primitive with real CSSX implementation
|
||||
PRIMITIVES["css"] = function() {
|
||||
var atoms = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (isNil(a) || a === false) continue;
|
||||
atoms.push(isKw(a) ? a.name : String(a));
|
||||
}
|
||||
if (!atoms.length) return NIL;
|
||||
return resolveStyle(atoms);
|
||||
};
|
||||
|
||||
PRIMITIVES["merge-styles"] = function() {
|
||||
var valid = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
|
||||
}
|
||||
if (!valid.length) return NIL;
|
||||
if (valid.length === 1) return valid[0];
|
||||
return mergeStyleValues(valid);
|
||||
};
|
||||
"""
|
||||
|
||||
PLATFORM_BOOT_JS = """
|
||||
// =========================================================================
|
||||
// Platform interface — Boot (mount, hydrate, scripts, cookies)
|
||||
@@ -3916,12 +3742,6 @@ PLATFORM_BOOT_JS = """
|
||||
r.querySelectorAll('script[type="text/sx"]'));
|
||||
}
|
||||
|
||||
function queryStyleScripts() {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
document.querySelectorAll('script[type="text/sx-styles"]'));
|
||||
}
|
||||
|
||||
function queryPageScripts() {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
@@ -3953,14 +3773,6 @@ PLATFORM_BOOT_JS = """
|
||||
if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
function setSxStylesCookie(hash) {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
|
||||
}
|
||||
|
||||
function clearSxStylesCookie() {
|
||||
if (_hasDom) document.cookie = "sx-styles-hash=;path=/;max-age=0;SameSite=Lax";
|
||||
}
|
||||
|
||||
// --- Env helpers ---
|
||||
|
||||
function parseEnvAttr(el) {
|
||||
@@ -4009,10 +3821,6 @@ PLATFORM_BOOT_JS = """
|
||||
}
|
||||
}
|
||||
|
||||
function parseAndLoadStyleDict(text) {
|
||||
try { loadStyleDict(JSON.parse(text)); }
|
||||
catch (e) { if (typeof console !== "undefined") console.warn("[sx-ref] style dict parse error", e); }
|
||||
}
|
||||
"""
|
||||
|
||||
def fixups_js(has_html, has_sx, has_dom):
|
||||
@@ -4039,7 +3847,7 @@ def fixups_js(has_html, has_sx, has_dom):
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
|
||||
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
|
||||
if has_parser:
|
||||
parser = '''
|
||||
|
||||
@@ -31,6 +31,19 @@ from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
|
||||
# SX -> Python transpiler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Python reserved words — SX names that collide get _ suffix
|
||||
# Excludes names we intentionally shadow (list, dict, range, filter, map)
|
||||
_PY_RESERVED = frozenset({
|
||||
"False", "None", "True", "and", "as", "assert", "async", "await",
|
||||
"break", "class", "continue", "def", "del", "elif", "else", "except",
|
||||
"finally", "for", "from", "global", "if", "import", "in", "is",
|
||||
"lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try",
|
||||
"while", "with", "yield",
|
||||
# builtins we don't want to shadow
|
||||
"default", "type", "id", "input", "open", "print", "set", "super",
|
||||
})
|
||||
|
||||
|
||||
class PyEmitter:
|
||||
"""Transpile an SX AST node to Python source code."""
|
||||
|
||||
@@ -124,6 +137,7 @@ class PyEmitter:
|
||||
"component-closure": "component_closure",
|
||||
"component-has-children?": "component_has_children",
|
||||
"component-name": "component_name",
|
||||
"component-affinity": "component_affinity",
|
||||
"macro-params": "macro_params",
|
||||
"macro-rest-param": "macro_rest_param",
|
||||
"macro-body": "macro_body",
|
||||
@@ -182,6 +196,7 @@ class PyEmitter:
|
||||
"sf-lambda": "sf_lambda",
|
||||
"sf-define": "sf_define",
|
||||
"sf-defcomp": "sf_defcomp",
|
||||
"defcomp-kwarg": "defcomp_kwarg",
|
||||
"sf-defmacro": "sf_defmacro",
|
||||
"sf-begin": "sf_begin",
|
||||
"sf-quote": "sf_quote",
|
||||
@@ -199,11 +214,6 @@ class PyEmitter:
|
||||
"ho-every": "ho_every",
|
||||
"ho-for-each": "ho_for_each",
|
||||
"sf-defstyle": "sf_defstyle",
|
||||
"sf-defkeyframes": "sf_defkeyframes",
|
||||
"build-keyframes": "build_keyframes",
|
||||
"style-value?": "is_style_value",
|
||||
"style-value-class": "style_value_class",
|
||||
"kf-name": "kf_name",
|
||||
"special-form?": "is_special_form",
|
||||
"ho-form?": "is_ho_form",
|
||||
"strip-prefix": "strip_prefix",
|
||||
@@ -262,6 +272,8 @@ class PyEmitter:
|
||||
"transitive-io-refs": "transitive_io_refs",
|
||||
"compute-all-io-refs": "compute_all_io_refs",
|
||||
"component-pure?": "component_pure_p",
|
||||
"render-target": "render_target",
|
||||
"page-render-plan": "page_render_plan",
|
||||
# router.sx
|
||||
"split-path-segments": "split_path_segments",
|
||||
"make-route-segment": "make_route_segment",
|
||||
@@ -281,9 +293,9 @@ class PyEmitter:
|
||||
result = result[:-1] + "_b"
|
||||
# Kebab to snake_case
|
||||
result = result.replace("-", "_")
|
||||
# Avoid Python keyword conflicts
|
||||
if result in ("list", "dict", "range", "filter"):
|
||||
result = result # keep as-is, these are our SX aliases
|
||||
# Escape Python reserved words
|
||||
if result in _PY_RESERVED:
|
||||
result = result + "_"
|
||||
return result
|
||||
|
||||
# --- List emission ---
|
||||
@@ -1080,7 +1092,7 @@ from typing import Any
|
||||
# =========================================================================
|
||||
|
||||
from shared.sx.types import (
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro,
|
||||
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
|
||||
)
|
||||
from shared.sx.parser import SxExpr
|
||||
@@ -1189,8 +1201,6 @@ def type_of(x):
|
||||
return "macro"
|
||||
if isinstance(x, _RawHTML):
|
||||
return "raw-html"
|
||||
if isinstance(x, StyleValue):
|
||||
return "style-value"
|
||||
if isinstance(x, Continuation):
|
||||
return "continuation"
|
||||
if isinstance(x, list):
|
||||
@@ -1220,9 +1230,9 @@ def make_lambda(params, body, env):
|
||||
return Lambda(params=list(params), body=body, closure=dict(env))
|
||||
|
||||
|
||||
def make_component(name, params, has_children, body, env):
|
||||
def make_component(name, params, has_children, body, env, affinity="auto"):
|
||||
return Component(name=name, params=list(params), has_children=has_children,
|
||||
body=body, closure=dict(env))
|
||||
body=body, closure=dict(env), affinity=str(affinity) if affinity else "auto")
|
||||
|
||||
|
||||
def make_macro(params, rest_param, body, env, name=None):
|
||||
@@ -1311,6 +1321,10 @@ def component_name(c):
|
||||
return c.name
|
||||
|
||||
|
||||
def component_affinity(c):
|
||||
return getattr(c, 'affinity', 'auto')
|
||||
|
||||
|
||||
def macro_params(m):
|
||||
return m.params
|
||||
|
||||
@@ -1355,14 +1369,6 @@ def is_macro(x):
|
||||
return isinstance(x, Macro)
|
||||
|
||||
|
||||
def is_style_value(x):
|
||||
return isinstance(x, StyleValue)
|
||||
|
||||
|
||||
def style_value_class(x):
|
||||
return x.class_name
|
||||
|
||||
|
||||
def env_has(env, name):
|
||||
return name in env
|
||||
|
||||
@@ -1513,8 +1519,6 @@ def serialize(val):
|
||||
if t == "raw-html":
|
||||
escaped = escape_string(raw_html_content(val))
|
||||
return '(raw! "' + escaped + '")'
|
||||
if t == "style-value":
|
||||
return '"' + style_value_class(val) + '"'
|
||||
if t == "list":
|
||||
if not val:
|
||||
return "()"
|
||||
@@ -1534,7 +1538,7 @@ def serialize(val):
|
||||
_SPECIAL_FORM_NAMES = frozenset([
|
||||
"if", "when", "cond", "case", "and", "or",
|
||||
"let", "let*", "lambda", "fn",
|
||||
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
"define", "defcomp", "defmacro", "defstyle",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
||||
"begin", "do", "quote", "quasiquote",
|
||||
"->", "set!",
|
||||
@@ -1696,7 +1700,7 @@ def aser_special(name, expr, env):
|
||||
fn(item)
|
||||
return results if results else NIL
|
||||
# Definition forms — evaluate for side effects
|
||||
if name in ("define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
if name in ("define", "defcomp", "defmacro", "defstyle",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
||||
trampoline(eval_expr(expr, env))
|
||||
return NIL
|
||||
|
||||
@@ -123,4 +123,4 @@
|
||||
|
||||
(define-boundary-types
|
||||
(list "number" "string" "boolean" "nil" "keyword"
|
||||
"list" "dict" "sx-source" "style-value"))
|
||||
"list" "dict" "sx-source"))
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
;; ==========================================================================
|
||||
;; cssx.sx — On-demand CSS style dictionary
|
||||
;;
|
||||
;; Resolves keyword atoms (e.g. :flex, :gap-4, :hover:bg-sky-200) into
|
||||
;; StyleValue objects with content-addressed class names. CSS rules are
|
||||
;; injected into the document on first use.
|
||||
;;
|
||||
;; The style dictionary is loaded from a JSON blob (typically served
|
||||
;; inline in a <script type="text/sx-styles"> tag) containing:
|
||||
;; a — atom → CSS declarations map
|
||||
;; v — pseudo-variant → CSS pseudo-selector map
|
||||
;; b — responsive breakpoint → media query map
|
||||
;; k — keyframe name → @keyframes rule map
|
||||
;; p — arbitrary patterns: [[regex, template], ...]
|
||||
;; c — child selector prefixes: ["space-x-", "space-y-", ...]
|
||||
;;
|
||||
;; Depends on:
|
||||
;; render.sx — StyleValue type
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; State — populated by load-style-dict
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define _style-atoms (dict))
|
||||
(define _pseudo-variants (dict))
|
||||
(define _responsive-breakpoints (dict))
|
||||
(define _style-keyframes (dict))
|
||||
(define _arbitrary-patterns (list))
|
||||
(define _child-selector-prefixes (list))
|
||||
(define _style-cache (dict))
|
||||
(define _injected-styles (dict))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Load style dictionary from parsed JSON data
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define load-style-dict
|
||||
(fn (data)
|
||||
(set! _style-atoms (or (get data "a") (dict)))
|
||||
(set! _pseudo-variants (or (get data "v") (dict)))
|
||||
(set! _responsive-breakpoints (or (get data "b") (dict)))
|
||||
(set! _style-keyframes (or (get data "k") (dict)))
|
||||
(set! _child-selector-prefixes (or (get data "c") (list)))
|
||||
;; Compile arbitrary patterns from [regex, template] pairs
|
||||
(set! _arbitrary-patterns
|
||||
(map
|
||||
(fn (pair)
|
||||
(dict "re" (compile-regex (str "^" (first pair) "$"))
|
||||
"tmpl" (nth pair 1)))
|
||||
(or (get data "p") (list))))
|
||||
;; Clear cache on reload
|
||||
(set! _style-cache (dict))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Variant splitting
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define split-variant
|
||||
(fn (atom)
|
||||
;; Parse variant prefixes: "sm:hover:bg-sky-200" → ["sm:hover", "bg-sky-200"]
|
||||
;; Returns [variant, base] where variant is nil for no variant.
|
||||
|
||||
;; Check responsive prefix first
|
||||
(let ((result nil))
|
||||
(for-each
|
||||
(fn (bp)
|
||||
(when (nil? result)
|
||||
(let ((prefix (str bp ":")))
|
||||
(when (starts-with? atom prefix)
|
||||
(let ((rest-atom (slice atom (len prefix))))
|
||||
;; Check for compound variant (sm:hover:...)
|
||||
(let ((inner-match nil))
|
||||
(for-each
|
||||
(fn (pv)
|
||||
(when (nil? inner-match)
|
||||
(let ((inner-prefix (str pv ":")))
|
||||
(when (starts-with? rest-atom inner-prefix)
|
||||
(set! inner-match
|
||||
(list (str bp ":" pv)
|
||||
(slice rest-atom (len inner-prefix))))))))
|
||||
(keys _pseudo-variants))
|
||||
(set! result
|
||||
(or inner-match (list bp rest-atom)))))))))
|
||||
(keys _responsive-breakpoints))
|
||||
|
||||
(when (nil? result)
|
||||
;; Check pseudo variants
|
||||
(for-each
|
||||
(fn (pv)
|
||||
(when (nil? result)
|
||||
(let ((prefix (str pv ":")))
|
||||
(when (starts-with? atom prefix)
|
||||
(set! result (list pv (slice atom (len prefix))))))))
|
||||
(keys _pseudo-variants)))
|
||||
|
||||
(or result (list nil atom)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Atom resolution
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define resolve-atom
|
||||
(fn (atom)
|
||||
;; Look up atom → CSS declarations string, or nil
|
||||
(let ((decls (dict-get _style-atoms atom)))
|
||||
(if (not (nil? decls))
|
||||
decls
|
||||
;; Dynamic keyframes: animate-{name}
|
||||
(if (starts-with? atom "animate-")
|
||||
(let ((kf-name (slice atom 8)))
|
||||
(if (dict-has? _style-keyframes kf-name)
|
||||
(str "animation-name:" kf-name)
|
||||
nil))
|
||||
;; Try arbitrary patterns
|
||||
(let ((match-result nil))
|
||||
(for-each
|
||||
(fn (pat)
|
||||
(when (nil? match-result)
|
||||
(let ((m (regex-match (get pat "re") atom)))
|
||||
(when m
|
||||
(set! match-result
|
||||
(regex-replace-groups (get pat "tmpl") m))))))
|
||||
_arbitrary-patterns)
|
||||
match-result))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Child selector detection
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define is-child-selector-atom?
|
||||
(fn (atom)
|
||||
(some
|
||||
(fn (prefix) (starts-with? atom prefix))
|
||||
_child-selector-prefixes)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; FNV-1a 32-bit hash → 6 hex chars
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define hash-style
|
||||
(fn (input)
|
||||
;; FNV-1a 32-bit hash for content-addressed class names
|
||||
(fnv1a-hash input)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Full style resolution pipeline
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define resolve-style
|
||||
(fn (atoms)
|
||||
;; Resolve a list of atom strings into a StyleValue.
|
||||
;; Uses content-addressed caching.
|
||||
(let ((key (join "\0" atoms)))
|
||||
(let ((cached (dict-get _style-cache key)))
|
||||
(if (not (nil? cached))
|
||||
cached
|
||||
;; Resolve each atom
|
||||
(let ((base-decls (list))
|
||||
(media-rules (list))
|
||||
(pseudo-rules (list))
|
||||
(kf-needed (list)))
|
||||
(for-each
|
||||
(fn (a)
|
||||
(when a
|
||||
(let ((clean (if (starts-with? a ":") (slice a 1) a)))
|
||||
(let ((parts (split-variant clean)))
|
||||
(let ((variant (first parts))
|
||||
(base (nth parts 1))
|
||||
(decls (resolve-atom base)))
|
||||
(when decls
|
||||
;; Check keyframes
|
||||
(when (starts-with? base "animate-")
|
||||
(let ((kf-name (slice base 8)))
|
||||
(when (dict-has? _style-keyframes kf-name)
|
||||
(append! kf-needed
|
||||
(list kf-name (dict-get _style-keyframes kf-name))))))
|
||||
|
||||
(cond
|
||||
(nil? variant)
|
||||
(if (is-child-selector-atom? base)
|
||||
(append! pseudo-rules
|
||||
(list ">:not(:first-child)" decls))
|
||||
(append! base-decls decls))
|
||||
|
||||
(dict-has? _responsive-breakpoints variant)
|
||||
(append! media-rules
|
||||
(list (dict-get _responsive-breakpoints variant) decls))
|
||||
|
||||
(dict-has? _pseudo-variants variant)
|
||||
(append! pseudo-rules
|
||||
(list (dict-get _pseudo-variants variant) decls))
|
||||
|
||||
;; Compound variant: "sm:hover"
|
||||
:else
|
||||
(let ((vparts (split variant ":"))
|
||||
(media-part nil)
|
||||
(pseudo-part nil))
|
||||
(for-each
|
||||
(fn (vp)
|
||||
(cond
|
||||
(dict-has? _responsive-breakpoints vp)
|
||||
(set! media-part (dict-get _responsive-breakpoints vp))
|
||||
(dict-has? _pseudo-variants vp)
|
||||
(set! pseudo-part (dict-get _pseudo-variants vp))))
|
||||
vparts)
|
||||
(when media-part
|
||||
(append! media-rules (list media-part decls)))
|
||||
(when pseudo-part
|
||||
(append! pseudo-rules (list pseudo-part decls)))
|
||||
(when (and (nil? media-part) (nil? pseudo-part))
|
||||
(append! base-decls decls))))))))))
|
||||
atoms)
|
||||
|
||||
;; Build hash input
|
||||
(let ((hash-input (join ";" base-decls)))
|
||||
(for-each
|
||||
(fn (mr)
|
||||
(set! hash-input
|
||||
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
|
||||
media-rules)
|
||||
(for-each
|
||||
(fn (pr)
|
||||
(set! hash-input
|
||||
(str hash-input (first pr) "{" (nth pr 1) "}")))
|
||||
pseudo-rules)
|
||||
(for-each
|
||||
(fn (kf)
|
||||
(set! hash-input (str hash-input (nth kf 1))))
|
||||
kf-needed)
|
||||
|
||||
(let ((cn (str "sx-" (hash-style hash-input)))
|
||||
(sv (make-style-value cn
|
||||
(join ";" base-decls)
|
||||
media-rules
|
||||
pseudo-rules
|
||||
kf-needed)))
|
||||
(dict-set! _style-cache key sv)
|
||||
;; Inject CSS rules
|
||||
(inject-style-value sv atoms)
|
||||
sv))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Merge multiple StyleValues
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define merge-style-values
|
||||
(fn (styles)
|
||||
(if (= (len styles) 1)
|
||||
(first styles)
|
||||
(let ((all-decls (list))
|
||||
(all-media (list))
|
||||
(all-pseudo (list))
|
||||
(all-kf (list)))
|
||||
(for-each
|
||||
(fn (sv)
|
||||
(when (style-value-declarations sv)
|
||||
(append! all-decls (style-value-declarations sv)))
|
||||
(set! all-media (concat all-media (style-value-media-rules sv)))
|
||||
(set! all-pseudo (concat all-pseudo (style-value-pseudo-rules sv)))
|
||||
(set! all-kf (concat all-kf (style-value-keyframes sv))))
|
||||
styles)
|
||||
|
||||
(let ((hash-input (join ";" all-decls)))
|
||||
(for-each
|
||||
(fn (mr)
|
||||
(set! hash-input
|
||||
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
|
||||
all-media)
|
||||
(for-each
|
||||
(fn (pr)
|
||||
(set! hash-input
|
||||
(str hash-input (first pr) "{" (nth pr 1) "}")))
|
||||
all-pseudo)
|
||||
(for-each
|
||||
(fn (kf)
|
||||
(set! hash-input (str hash-input (nth kf 1))))
|
||||
all-kf)
|
||||
|
||||
(let ((cn (str "sx-" (hash-style hash-input)))
|
||||
(merged (make-style-value cn
|
||||
(join ";" all-decls)
|
||||
all-media all-pseudo all-kf)))
|
||||
(inject-style-value merged (list))
|
||||
merged))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — CSSX
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Hash:
|
||||
;; (fnv1a-hash input) → 6-char hex string (FNV-1a 32-bit)
|
||||
;;
|
||||
;; Regex:
|
||||
;; (compile-regex pattern) → compiled regex object
|
||||
;; (regex-match re str) → match array or nil
|
||||
;; (regex-replace-groups tmpl match) → string with {0},{1},... replaced
|
||||
;;
|
||||
;; StyleValue construction:
|
||||
;; (make-style-value cn decls media pseudo kf) → StyleValue object
|
||||
;; (style-value-declarations sv) → declarations string
|
||||
;; (style-value-media-rules sv) → list of [query, decls] pairs
|
||||
;; (style-value-pseudo-rules sv) → list of [selector, decls] pairs
|
||||
;; (style-value-keyframes sv) → list of [name, rule] pairs
|
||||
;;
|
||||
;; CSS injection:
|
||||
;; (inject-style-value sv atoms) → void (append CSS rules to <style id="sx-css">)
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -314,19 +314,91 @@
|
||||
(empty? (transitive-io-refs name env io-names))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 5. Render target — boundary decision per component
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Combines IO analysis with affinity annotations to decide where a
|
||||
;; component should render:
|
||||
;;
|
||||
;; :affinity :server → always "server" (auth-sensitive, secrets)
|
||||
;; :affinity :client → "client" even if IO-dependent (IO proxy)
|
||||
;; :affinity :auto → "server" if IO-dependent, "client" if pure
|
||||
;;
|
||||
;; Returns: "server" | "client"
|
||||
|
||||
(define render-target
|
||||
(fn (name env io-names)
|
||||
(let ((key (if (starts-with? name "~") name (str "~" name))))
|
||||
(let ((val (env-get env key)))
|
||||
(if (not (= (type-of val) "component"))
|
||||
"server"
|
||||
(let ((affinity (component-affinity val)))
|
||||
(cond
|
||||
(= affinity "server") "server"
|
||||
(= affinity "client") "client"
|
||||
;; auto: decide from IO analysis
|
||||
(not (component-pure? name env io-names)) "server"
|
||||
:else "client")))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. Page render plan — pre-computed boundary decisions for a page
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Given page source + env + IO names, returns a render plan dict:
|
||||
;;
|
||||
;; {:components {~name "server"|"client" ...}
|
||||
;; :server (list of ~names that render server-side)
|
||||
;; :client (list of ~names that render client-side)
|
||||
;; :io-deps (list of IO primitives needed by server components)}
|
||||
;;
|
||||
;; This is computed once at page registration and cached on the page def.
|
||||
;; The async evaluator and client router both use it to make decisions
|
||||
;; without recomputing at every request.
|
||||
|
||||
(define page-render-plan
|
||||
(fn (page-source env io-names)
|
||||
(let ((needed (components-needed page-source env))
|
||||
(comp-targets (dict))
|
||||
(server-list (list))
|
||||
(client-list (list))
|
||||
(io-deps (list)))
|
||||
|
||||
(for-each
|
||||
(fn (name)
|
||||
(let ((target (render-target name env io-names)))
|
||||
(dict-set! comp-targets name target)
|
||||
(if (= target "server")
|
||||
(do
|
||||
(append! server-list name)
|
||||
;; Collect IO deps from server components
|
||||
(for-each
|
||||
(fn (io-ref)
|
||||
(when (not (contains? io-deps io-ref))
|
||||
(append! io-deps io-ref)))
|
||||
(transitive-io-refs name env io-names)))
|
||||
(append! client-list name))))
|
||||
needed)
|
||||
|
||||
{:components comp-targets
|
||||
:server server-list
|
||||
:client client-list
|
||||
:io-deps io-deps})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Host obligation: selective expansion in async partial evaluation
|
||||
;; --------------------------------------------------------------------------
|
||||
;; The spec classifies components as pure or IO-dependent. Each host's
|
||||
;; async partial evaluator (the server-side rendering path that bridges
|
||||
;; sync evaluation with async IO) must use this classification:
|
||||
;; The spec classifies components as pure or IO-dependent and provides
|
||||
;; per-component render-target decisions. Each host's async partial
|
||||
;; evaluator (the server-side rendering path that bridges sync evaluation
|
||||
;; with async IO) must use this classification:
|
||||
;;
|
||||
;; IO-dependent component → expand server-side (IO must resolve)
|
||||
;; Pure component → serialize for client (can render anywhere)
|
||||
;; render-target "server" → expand server-side (IO must resolve)
|
||||
;; render-target "client" → serialize for client (can render anywhere)
|
||||
;; Layout slot context → expand all (server needs full HTML)
|
||||
;;
|
||||
;; The spec provides the data (component-io-refs, component-pure?).
|
||||
;; The host provides the async runtime that acts on it.
|
||||
;; The spec provides: component-io-refs, component-pure?, render-target,
|
||||
;; component-affinity. The host provides the async runtime that acts on it.
|
||||
;; This is not SX semantics — it is host infrastructure. Every host
|
||||
;; with a server-side async evaluator implements the same rule.
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -349,6 +421,7 @@
|
||||
;; (component-css-classes c)→ pre-scanned CSS class list
|
||||
;; (component-io-refs c) → cached IO ref list (may be empty)
|
||||
;; (component-set-io-refs! c r)→ cache IO refs on component
|
||||
;; (component-affinity c) → "auto" | "client" | "server"
|
||||
;; (macro-body m) → AST body of macro
|
||||
;; (env-components env) → list of component names in env
|
||||
;; (regex-find-all pat src) → list of capture group matches
|
||||
|
||||
@@ -143,7 +143,6 @@
|
||||
(= name "defcomp") (sf-defcomp args env)
|
||||
(= name "defmacro") (sf-defmacro args env)
|
||||
(= name "defstyle") (sf-defstyle args env)
|
||||
(= name "defkeyframes") (sf-defkeyframes args env)
|
||||
(= name "defhandler") (sf-defhandler args env)
|
||||
(= name "defpage") (sf-defpage args env)
|
||||
(= name "defquery") (sf-defquery args env)
|
||||
@@ -491,17 +490,37 @@
|
||||
|
||||
(define sf-defcomp
|
||||
(fn (args env)
|
||||
;; (defcomp ~name (params) [:affinity :client|:server] body)
|
||||
;; Body is always the last element. Optional keyword annotations
|
||||
;; may appear between the params list and the body.
|
||||
(let ((name-sym (first args))
|
||||
(params-raw (nth args 1))
|
||||
(body (nth args 2))
|
||||
(body (last args))
|
||||
(comp-name (strip-prefix (symbol-name name-sym) "~"))
|
||||
(parsed (parse-comp-params params-raw))
|
||||
(params (first parsed))
|
||||
(has-children (nth parsed 1)))
|
||||
(let ((comp (make-component comp-name params has-children body env)))
|
||||
(has-children (nth parsed 1))
|
||||
(affinity (defcomp-kwarg args "affinity" "auto")))
|
||||
(let ((comp (make-component comp-name params has-children body env affinity)))
|
||||
(env-set! env (symbol-name name-sym) comp)
|
||||
comp))))
|
||||
|
||||
(define defcomp-kwarg
|
||||
(fn (args key default)
|
||||
;; Search for :key value between params (index 2) and body (last).
|
||||
(let ((end (- (len args) 1))
|
||||
(result default))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(when (and (= (type-of (nth args i)) "keyword")
|
||||
(= (keyword-name (nth args i)) key)
|
||||
(< (+ i 1) end))
|
||||
(let ((val (nth args (+ i 1))))
|
||||
(set! result (if (= (type-of val) "keyword")
|
||||
(keyword-name val) val)))))
|
||||
(range 2 end 1))
|
||||
result)))
|
||||
|
||||
(define parse-comp-params
|
||||
(fn (params-expr)
|
||||
;; Parse (&key param1 param2 &children) → (params has-children)
|
||||
@@ -559,23 +578,13 @@
|
||||
|
||||
(define sf-defstyle
|
||||
(fn (args env)
|
||||
;; (defstyle name expr) — bind name to evaluated expr (typically a StyleValue)
|
||||
;; (defstyle name expr) — bind name to evaluated expr (string, function, etc.)
|
||||
(let ((name-sym (first args))
|
||||
(value (trampoline (eval-expr (nth args 1) env))))
|
||||
(env-set! env (symbol-name name-sym) value)
|
||||
value)))
|
||||
|
||||
|
||||
(define sf-defkeyframes
|
||||
(fn (args env)
|
||||
;; (defkeyframes name (selector body) ...) — build @keyframes rule,
|
||||
;; register in keyframes dict, return StyleValue.
|
||||
;; Delegates to platform: build-keyframes returns a StyleValue.
|
||||
(let ((kf-name (symbol-name (first args)))
|
||||
(steps (rest args)))
|
||||
(build-keyframes kf-name steps env))))
|
||||
|
||||
|
||||
(define sf-begin
|
||||
(fn (args env)
|
||||
(if (empty? args)
|
||||
@@ -879,7 +888,7 @@
|
||||
;;
|
||||
;; Constructors:
|
||||
;; (make-lambda params body env) → Lambda
|
||||
;; (make-component name params has-children body env) → Component
|
||||
;; (make-component name params has-children body env affinity) → Component
|
||||
;; (make-macro params rest-param body env name) → Macro
|
||||
;; (make-thunk expr env) → Thunk
|
||||
;;
|
||||
@@ -893,6 +902,7 @@
|
||||
;; (component-body c) → expr
|
||||
;; (component-closure c) → env
|
||||
;; (component-has-children? c) → boolean
|
||||
;; (component-affinity c) → "auto" | "client" | "server"
|
||||
;; (macro-params m) → list of strings
|
||||
;; (macro-rest-param m) → string or nil
|
||||
;; (macro-body m) → expr
|
||||
@@ -931,9 +941,6 @@
|
||||
;; (zip lists...) → list of tuples
|
||||
;;
|
||||
;;
|
||||
;; CSSX (style system):
|
||||
;; (build-keyframes name steps env) → StyleValue (platform builds @keyframes)
|
||||
;;
|
||||
;; Dynamic wind (for dynamic-wind):
|
||||
;; (push-wind! before after) → void (push wind record onto stack)
|
||||
;; (pop-wind!) → void (pop wind record from stack)
|
||||
|
||||
@@ -661,7 +661,14 @@
|
||||
(if (not (deps-satisfied? match))
|
||||
(do (log-info (str "sx:route deps miss for " page-name)) false)
|
||||
(let ((io-deps (get match "io-deps"))
|
||||
(has-io (and io-deps (not (empty? io-deps)))))
|
||||
(has-io (and io-deps (not (empty? io-deps))))
|
||||
(render-plan (get match "render-plan")))
|
||||
;; Log render plan for boundary visibility
|
||||
(when render-plan
|
||||
(let ((srv (or (get render-plan "server") (list)))
|
||||
(cli (or (get render-plan "client") (list))))
|
||||
(log-info (str "sx:route plan " page-name
|
||||
" — " (len srv) " server, " (len cli) " client"))))
|
||||
;; Ensure IO deps are registered as proxied primitives
|
||||
(when has-io (register-io-deps io-deps))
|
||||
(if (get match "stream")
|
||||
|
||||
@@ -541,18 +541,6 @@
|
||||
;; Stdlib — Style
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-module :stdlib.style)
|
||||
|
||||
(define-primitive "css"
|
||||
:params (&rest atoms)
|
||||
:returns "style-value"
|
||||
:doc "Resolve style atoms to a StyleValue with className and CSS declarations.
|
||||
Atoms are keywords or strings: (css :flex :gap-4 :hover:bg-sky-200).")
|
||||
|
||||
(define-primitive "merge-styles"
|
||||
:params (&rest styles)
|
||||
:returns "style-value"
|
||||
:doc "Merge multiple StyleValues into one combined StyleValue.")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
(define definition-form?
|
||||
(fn (name)
|
||||
(or (= name "define") (= name "defcomp") (= name "defmacro")
|
||||
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))))
|
||||
(= name "defstyle") (= name "defhandler"))))
|
||||
|
||||
|
||||
(define parse-element-args
|
||||
@@ -116,9 +116,6 @@
|
||||
""
|
||||
;; Nil values — skip
|
||||
(nil? val) ""
|
||||
;; StyleValue on :style → emit as class
|
||||
(and (= key "style") (style-value? val))
|
||||
(str " class=\"" (style-value-class val) "\"")
|
||||
;; Normal attr
|
||||
:else (str " " key "=\"" (escape-attr (str val)) "\""))))
|
||||
(keys attrs)))))
|
||||
@@ -202,10 +199,6 @@
|
||||
;; (escape-attr s) → attribute-value-escaped string
|
||||
;; (raw-html-content r) → unwrap RawHTML marker to string
|
||||
;;
|
||||
;; StyleValue:
|
||||
;; (style-value? x) → boolean (is x a StyleValue?)
|
||||
;; (style-value-class sv) → string (CSS class name)
|
||||
;;
|
||||
;; Serialization:
|
||||
;; (serialize val) → SX source string representation of val
|
||||
;;
|
||||
|
||||
@@ -363,20 +363,12 @@
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "defstyle"
|
||||
:syntax (defstyle name atoms ...)
|
||||
:doc "Define a named style. Evaluates atoms to a StyleValue and binds
|
||||
it to name in the environment."
|
||||
:syntax (defstyle name expr)
|
||||
:doc "Define a named style value. Evaluates expr and binds the result
|
||||
to name in the environment. The value is typically a class string
|
||||
or a function that returns class strings."
|
||||
:tail-position "none"
|
||||
:example "(defstyle card-style :rounded-lg :shadow-md :p-4 :bg-white)")
|
||||
|
||||
(define-special-form "defkeyframes"
|
||||
:syntax (defkeyframes name steps ...)
|
||||
:doc "Define a CSS @keyframes animation. Steps are (percentage properties ...)
|
||||
pairs. Produces a StyleValue with the animation name and keyframe rules."
|
||||
:tail-position "none"
|
||||
:example "(defkeyframes fade-in
|
||||
(0 :opacity-0)
|
||||
(100 :opacity-100))")
|
||||
:example "(defstyle card-style \"rounded-lg shadow-md p-4 bg-white\")")
|
||||
|
||||
(define-special-form "defhandler"
|
||||
:syntax (defhandler name (&key params ...) body)
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import Any
|
||||
# =========================================================================
|
||||
|
||||
from shared.sx.types import (
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro,
|
||||
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
|
||||
)
|
||||
from shared.sx.parser import SxExpr
|
||||
@@ -126,8 +126,6 @@ def type_of(x):
|
||||
return "macro"
|
||||
if isinstance(x, _RawHTML):
|
||||
return "raw-html"
|
||||
if isinstance(x, StyleValue):
|
||||
return "style-value"
|
||||
if isinstance(x, Continuation):
|
||||
return "continuation"
|
||||
if isinstance(x, list):
|
||||
@@ -157,9 +155,9 @@ def make_lambda(params, body, env):
|
||||
return Lambda(params=list(params), body=body, closure=dict(env))
|
||||
|
||||
|
||||
def make_component(name, params, has_children, body, env):
|
||||
def make_component(name, params, has_children, body, env, affinity="auto"):
|
||||
return Component(name=name, params=list(params), has_children=has_children,
|
||||
body=body, closure=dict(env))
|
||||
body=body, closure=dict(env), affinity=str(affinity) if affinity else "auto")
|
||||
|
||||
|
||||
def make_macro(params, rest_param, body, env, name=None):
|
||||
@@ -248,6 +246,10 @@ def component_name(c):
|
||||
return c.name
|
||||
|
||||
|
||||
def component_affinity(c):
|
||||
return getattr(c, 'affinity', 'auto')
|
||||
|
||||
|
||||
def macro_params(m):
|
||||
return m.params
|
||||
|
||||
@@ -292,14 +294,6 @@ def is_macro(x):
|
||||
return isinstance(x, Macro)
|
||||
|
||||
|
||||
def is_style_value(x):
|
||||
return isinstance(x, StyleValue)
|
||||
|
||||
|
||||
def style_value_class(x):
|
||||
return x.class_name
|
||||
|
||||
|
||||
def env_has(env, name):
|
||||
return name in env
|
||||
|
||||
@@ -450,8 +444,6 @@ def serialize(val):
|
||||
if t == "raw-html":
|
||||
escaped = escape_string(raw_html_content(val))
|
||||
return '(raw! "' + escaped + '")'
|
||||
if t == "style-value":
|
||||
return '"' + style_value_class(val) + '"'
|
||||
if t == "list":
|
||||
if not val:
|
||||
return "()"
|
||||
@@ -471,7 +463,7 @@ def serialize(val):
|
||||
_SPECIAL_FORM_NAMES = frozenset([
|
||||
"if", "when", "cond", "case", "and", "or",
|
||||
"let", "let*", "lambda", "fn",
|
||||
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
"define", "defcomp", "defmacro", "defstyle",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
||||
"begin", "do", "quote", "quasiquote",
|
||||
"->", "set!",
|
||||
@@ -633,7 +625,7 @@ def aser_special(name, expr, env):
|
||||
fn(item)
|
||||
return results if results else NIL
|
||||
# Definition forms — evaluate for side effects
|
||||
if name in ("define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
if name in ("define", "defcomp", "defmacro", "defstyle",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
||||
trampoline(eval_expr(expr, env))
|
||||
return NIL
|
||||
@@ -955,7 +947,7 @@ trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result
|
||||
eval_expr = lambda expr, env: _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)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)])
|
||||
|
||||
# eval-list
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env)))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
|
||||
# eval-call
|
||||
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
|
||||
@@ -1012,7 +1004,18 @@ sf_lambda = lambda args, env: (lambda params_expr: (lambda body: (lambda param_n
|
||||
sf_define = lambda args, env: (lambda name_sym: (lambda value: _sx_begin((_sx_set_attr(value, 'name', symbol_name(name_sym)) if sx_truthy((is_lambda(value) if not sx_truthy(is_lambda(value)) else is_nil(lambda_name(value)))) else NIL), _sx_dict_set(env, symbol_name(name_sym), value), value))(trampoline(eval_expr(nth(args, 1), env))))(first(args))
|
||||
|
||||
# sf-defcomp
|
||||
sf_defcomp = lambda args, env: (lambda name_sym: (lambda params_raw: (lambda body: (lambda comp_name: (lambda parsed: (lambda params: (lambda has_children: (lambda comp: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), comp), comp))(make_component(comp_name, params, has_children, body, env)))(nth(parsed, 1)))(first(parsed)))(parse_comp_params(params_raw)))(strip_prefix(symbol_name(name_sym), '~')))(nth(args, 2)))(nth(args, 1)))(first(args))
|
||||
sf_defcomp = lambda args, env: (lambda name_sym: (lambda params_raw: (lambda body: (lambda comp_name: (lambda parsed: (lambda params: (lambda has_children: (lambda affinity: (lambda comp: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), comp), comp))(make_component(comp_name, params, has_children, body, env, affinity)))(defcomp_kwarg(args, 'affinity', 'auto')))(nth(parsed, 1)))(first(parsed)))(parse_comp_params(params_raw)))(strip_prefix(symbol_name(name_sym), '~')))(last(args)))(nth(args, 1)))(first(args))
|
||||
|
||||
# defcomp-kwarg
|
||||
def defcomp_kwarg(args, key, default_):
|
||||
_cells = {}
|
||||
end = (len(args) - 1)
|
||||
_cells['result'] = default_
|
||||
for i in range(2, end, 1):
|
||||
if sx_truthy(((type_of(nth(args, i)) == 'keyword') if not sx_truthy((type_of(nth(args, i)) == 'keyword')) else ((keyword_name(nth(args, i)) == key) if not sx_truthy((keyword_name(nth(args, i)) == key)) else ((i + 1) < end)))):
|
||||
val = nth(args, (i + 1))
|
||||
_cells['result'] = (keyword_name(val) if sx_truthy((type_of(val) == 'keyword')) else val)
|
||||
return _cells['result']
|
||||
|
||||
# parse-comp-params
|
||||
def parse_comp_params(params_expr):
|
||||
@@ -1051,9 +1054,6 @@ def parse_macro_params(params_expr):
|
||||
# sf-defstyle
|
||||
sf_defstyle = lambda args, env: (lambda name_sym: (lambda value: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), value), value))(trampoline(eval_expr(nth(args, 1), env))))(first(args))
|
||||
|
||||
# sf-defkeyframes
|
||||
sf_defkeyframes = lambda args, env: (lambda kf_name: (lambda steps: build_keyframes(kf_name, steps, env))(rest(args)))(symbol_name(first(args)))
|
||||
|
||||
# sf-begin
|
||||
sf_begin = lambda args, env: (NIL if sx_truthy(empty_p(args)) else _sx_begin(for_each(lambda e: trampoline(eval_expr(e, env)), slice(args, 0, (len(args) - 1))), make_thunk(last(args), env)))
|
||||
|
||||
@@ -1164,13 +1164,13 @@ VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'li
|
||||
BOOLEAN_ATTRS = ['async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'inert', 'ismap', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected']
|
||||
|
||||
# definition-form?
|
||||
is_definition_form = lambda name: ((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 == 'defkeyframes') if sx_truthy((name == 'defkeyframes')) else (name == 'defhandler'))))))
|
||||
is_definition_form = lambda name: ((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')))))
|
||||
|
||||
# parse-element-args
|
||||
parse_element_args = lambda args, env: (lambda attrs: (lambda children: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin(_sx_dict_set(attrs, keyword_name(arg), val), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))) 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), [attrs, children]))([]))({})
|
||||
|
||||
# render-attrs
|
||||
render_attrs = lambda attrs: join('', map(lambda key: (lambda val: (sx_str(' ', key) if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else val)) else ('' if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else (not sx_truthy(val)))) else ('' if sx_truthy(is_nil(val)) else (sx_str(' class="', style_value_class(val), '"') if sx_truthy(((key == 'style') if not sx_truthy((key == 'style')) else is_style_value(val))) else sx_str(' ', key, '="', escape_attr(sx_str(val)), '"'))))))(dict_get(attrs, key)), keys(attrs)))
|
||||
render_attrs = lambda attrs: join('', map(lambda key: (lambda val: (sx_str(' ', key) if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else val)) else ('' if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else (not sx_truthy(val)))) else ('' if sx_truthy(is_nil(val)) else sx_str(' ', key, '="', escape_attr(sx_str(val)), '"')))))(dict_get(attrs, key)), keys(attrs)))
|
||||
|
||||
# eval-cond
|
||||
eval_cond = lambda clauses, env: (eval_cond_scheme(clauses, env) if sx_truthy(((not sx_truthy(empty_p(clauses))) if not sx_truthy((not sx_truthy(empty_p(clauses)))) else ((type_of(first(clauses)) == 'list') if not sx_truthy((type_of(first(clauses)) == 'list')) else (len(first(clauses)) == 2)))) else eval_cond_clojure(clauses, env))
|
||||
@@ -1191,10 +1191,10 @@ process_bindings = lambda bindings, env: (lambda local: _sx_begin(for_each(lambd
|
||||
render_to_html = lambda expr, env: _sx_case(type_of(expr), [('nil', lambda: ''), ('string', lambda: escape_html(expr)), ('number', lambda: sx_str(expr)), ('boolean', lambda: ('true' if sx_truthy(expr) else 'false')), ('list', lambda: ('' if sx_truthy(empty_p(expr)) else render_list_to_html(expr, env))), ('symbol', lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env)), ('keyword', lambda: escape_html(keyword_name(expr))), ('raw-html', lambda: raw_html_content(expr)), (None, lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env))])
|
||||
|
||||
# render-value-to-html
|
||||
render_value_to_html = lambda val, env: _sx_case(type_of(val), [('nil', lambda: ''), ('string', lambda: escape_html(val)), ('number', lambda: sx_str(val)), ('boolean', lambda: ('true' if sx_truthy(val) else 'false')), ('list', lambda: render_list_to_html(val, env)), ('raw-html', lambda: raw_html_content(val)), ('style-value', lambda: style_value_class(val)), (None, lambda: escape_html(sx_str(val)))])
|
||||
render_value_to_html = lambda val, env: _sx_case(type_of(val), [('nil', lambda: ''), ('string', lambda: escape_html(val)), ('number', lambda: sx_str(val)), ('boolean', lambda: ('true' if sx_truthy(val) else 'false')), ('list', lambda: render_list_to_html(val, env)), ('raw-html', lambda: raw_html_content(val)), (None, lambda: escape_html(sx_str(val)))])
|
||||
|
||||
# RENDER_HTML_FORMS
|
||||
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defmacro', 'defstyle', 'defkeyframes', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
|
||||
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
|
||||
|
||||
# render-html-form?
|
||||
is_render_html_form = lambda name: contains_p(RENDER_HTML_FORMS, name)
|
||||
@@ -1283,168 +1283,11 @@ compute_all_io_refs = lambda env, io_names: for_each(lambda name: (lambda val: (
|
||||
# component-pure?
|
||||
component_pure_p = lambda name, env, io_names: empty_p(transitive_io_refs(name, env, io_names))
|
||||
|
||||
# render-target
|
||||
render_target = lambda name, env, io_names: (lambda key: (lambda val: ('server' if sx_truthy((not sx_truthy((type_of(val) == 'component')))) else (lambda affinity: ('server' if sx_truthy((affinity == 'server')) else ('client' if sx_truthy((affinity == 'client')) else ('server' if sx_truthy((not sx_truthy(component_pure_p(name, env, io_names)))) else 'client'))))(component_affinity(val))))(env_get(env, key)))((name if sx_truthy(starts_with_p(name, '~')) else sx_str('~', name)))
|
||||
|
||||
# === Transpiled from engine (fetch/swap/trigger pure logic) ===
|
||||
|
||||
# ENGINE_VERBS
|
||||
ENGINE_VERBS = ['get', 'post', 'put', 'delete', 'patch']
|
||||
|
||||
# DEFAULT_SWAP
|
||||
DEFAULT_SWAP = 'outerHTML'
|
||||
|
||||
# parse-time
|
||||
parse_time = lambda s: (0 if sx_truthy(is_nil(s)) else (parse_int(s, 0) if sx_truthy(ends_with_p(s, 'ms')) else ((parse_int(replace(s, 's', ''), 0) * 1000) if sx_truthy(ends_with_p(s, 's')) else parse_int(s, 0))))
|
||||
|
||||
# parse-trigger-spec
|
||||
parse_trigger_spec = lambda spec: (NIL if sx_truthy(is_nil(spec)) else (lambda raw_parts: filter(lambda x: (not sx_truthy(is_nil(x))), map(lambda part: (lambda tokens: (NIL if sx_truthy(empty_p(tokens)) else ({'event': 'every', 'modifiers': {'interval': parse_time(nth(tokens, 1))}} if sx_truthy(((first(tokens) == 'every') if not sx_truthy((first(tokens) == 'every')) else (len(tokens) >= 2))) else (lambda mods: _sx_begin(for_each(lambda tok: (_sx_dict_set(mods, 'once', True) if sx_truthy((tok == 'once')) else (_sx_dict_set(mods, 'changed', True) if sx_truthy((tok == 'changed')) else (_sx_dict_set(mods, 'delay', parse_time(slice(tok, 6))) if sx_truthy(starts_with_p(tok, 'delay:')) else (_sx_dict_set(mods, 'from', slice(tok, 5)) if sx_truthy(starts_with_p(tok, 'from:')) else NIL)))), rest(tokens)), {'event': first(tokens), 'modifiers': mods}))({}))))(split(trim(part), ' ')), raw_parts)))(split(spec, ',')))
|
||||
|
||||
# default-trigger
|
||||
default_trigger = lambda tag_name: ([{'event': 'submit', 'modifiers': {}}] if sx_truthy((tag_name == 'FORM')) else ([{'event': 'change', 'modifiers': {}}] if sx_truthy(((tag_name == 'INPUT') if sx_truthy((tag_name == 'INPUT')) else ((tag_name == 'SELECT') if sx_truthy((tag_name == 'SELECT')) else (tag_name == 'TEXTAREA')))) else [{'event': 'click', 'modifiers': {}}]))
|
||||
|
||||
# get-verb-info
|
||||
get_verb_info = lambda el: some(lambda verb: (lambda url: ({'method': upper(verb), 'url': url} if sx_truthy(url) else NIL))(dom_get_attr(el, sx_str('sx-', verb))), ENGINE_VERBS)
|
||||
|
||||
# build-request-headers
|
||||
build_request_headers = lambda el, loaded_components, css_hash: (lambda headers: _sx_begin((lambda target_sel: (_sx_dict_set(headers, 'SX-Target', target_sel) if sx_truthy(target_sel) else NIL))(dom_get_attr(el, 'sx-target')), (_sx_dict_set(headers, 'SX-Components', join(',', loaded_components)) if sx_truthy((not sx_truthy(empty_p(loaded_components)))) else NIL), (_sx_dict_set(headers, 'SX-Css', css_hash) if sx_truthy(css_hash) else NIL), (lambda extra_h: ((lambda parsed: (for_each(lambda key: _sx_dict_set(headers, key, sx_str(get(parsed, key))), keys(parsed)) if sx_truthy(parsed) else NIL))(parse_header_value(extra_h)) if sx_truthy(extra_h) else NIL))(dom_get_attr(el, 'sx-headers')), headers))({'SX-Request': 'true', 'SX-Current-URL': browser_location_href()})
|
||||
|
||||
# process-response-headers
|
||||
process_response_headers = lambda get_header: {'redirect': get_header('SX-Redirect'), 'refresh': get_header('SX-Refresh'), 'trigger': get_header('SX-Trigger'), 'retarget': get_header('SX-Retarget'), 'reswap': get_header('SX-Reswap'), 'location': get_header('SX-Location'), 'replace-url': get_header('SX-Replace-Url'), 'css-hash': get_header('SX-Css-Hash'), 'trigger-swap': get_header('SX-Trigger-After-Swap'), 'trigger-settle': get_header('SX-Trigger-After-Settle'), 'content-type': get_header('Content-Type')}
|
||||
|
||||
# parse-swap-spec
|
||||
def parse_swap_spec(raw_swap, global_transitions_p):
|
||||
_cells = {}
|
||||
parts = split((raw_swap if sx_truthy(raw_swap) else DEFAULT_SWAP), ' ')
|
||||
style = first(parts)
|
||||
_cells['use_transition'] = global_transitions_p
|
||||
for p in rest(parts):
|
||||
if sx_truthy((p == 'transition:true')):
|
||||
_cells['use_transition'] = True
|
||||
elif sx_truthy((p == 'transition:false')):
|
||||
_cells['use_transition'] = False
|
||||
return {'style': style, 'transition': _cells['use_transition']}
|
||||
|
||||
# parse-retry-spec
|
||||
parse_retry_spec = lambda retry_attr: (NIL if sx_truthy(is_nil(retry_attr)) else (lambda parts: {'strategy': first(parts), 'start-ms': parse_int(nth(parts, 1), 1000), 'cap-ms': parse_int(nth(parts, 2), 30000)})(split(retry_attr, ':')))
|
||||
|
||||
# next-retry-ms
|
||||
next_retry_ms = lambda current_ms, cap_ms: min((current_ms * 2), cap_ms)
|
||||
|
||||
# filter-params
|
||||
filter_params = lambda params_spec, all_params: (all_params if sx_truthy(is_nil(params_spec)) else ([] if sx_truthy((params_spec == 'none')) else (all_params if sx_truthy((params_spec == '*')) else ((lambda excluded: filter(lambda p: (not sx_truthy(contains_p(excluded, first(p)))), all_params))(map(trim, split(slice(params_spec, 4), ','))) if sx_truthy(starts_with_p(params_spec, 'not ')) else (lambda allowed: filter(lambda p: contains_p(allowed, first(p)), all_params))(map(trim, split(params_spec, ',')))))))
|
||||
|
||||
# resolve-target
|
||||
resolve_target = lambda el: (lambda sel: (el if sx_truthy((is_nil(sel) if sx_truthy(is_nil(sel)) else (sel == 'this'))) else (dom_parent(el) if sx_truthy((sel == 'closest')) else dom_query(sel))))(dom_get_attr(el, 'sx-target'))
|
||||
|
||||
# apply-optimistic
|
||||
apply_optimistic = lambda el: (lambda directive: (NIL if sx_truthy(is_nil(directive)) else (lambda target: (lambda state: _sx_begin((_sx_begin(_sx_dict_set(state, 'opacity', dom_get_style(target, 'opacity')), dom_set_style(target, 'opacity', '0'), dom_set_style(target, 'pointer-events', 'none')) if sx_truthy((directive == 'remove')) else (_sx_begin(_sx_dict_set(state, 'disabled', dom_get_prop(target, 'disabled')), dom_set_prop(target, 'disabled', True)) if sx_truthy((directive == 'disable')) else ((lambda cls: _sx_begin(_sx_dict_set(state, 'add-class', cls), dom_add_class(target, cls)))(slice(directive, 10)) if sx_truthy(starts_with_p(directive, 'add-class:')) else NIL))), state))({'target': target, 'directive': directive}))((resolve_target(el) if sx_truthy(resolve_target(el)) else el))))(dom_get_attr(el, 'sx-optimistic'))
|
||||
|
||||
# revert-optimistic
|
||||
revert_optimistic = lambda state: ((lambda target: (lambda directive: (_sx_begin(dom_set_style(target, 'opacity', (get(state, 'opacity') if sx_truthy(get(state, 'opacity')) else '')), dom_set_style(target, 'pointer-events', '')) if sx_truthy((directive == 'remove')) else (dom_set_prop(target, 'disabled', (get(state, 'disabled') if sx_truthy(get(state, 'disabled')) else False)) if sx_truthy((directive == 'disable')) else (dom_remove_class(target, get(state, 'add-class')) if sx_truthy(get(state, 'add-class')) else NIL))))(get(state, 'directive')))(get(state, 'target')) if sx_truthy(state) else NIL)
|
||||
|
||||
# find-oob-swaps
|
||||
find_oob_swaps = lambda container: (lambda results: _sx_begin(for_each(lambda attr: (lambda oob_els: for_each(lambda oob: (lambda swap_type: (lambda target_id: _sx_begin(dom_remove_attr(oob, attr), (_sx_append(results, {'element': oob, 'swap-type': swap_type, 'target-id': target_id}) if sx_truthy(target_id) else NIL)))(dom_id(oob)))((dom_get_attr(oob, attr) if sx_truthy(dom_get_attr(oob, attr)) else 'outerHTML')), oob_els))(dom_query_all(container, sx_str('[', attr, ']'))), ['sx-swap-oob', 'hx-swap-oob']), results))([])
|
||||
|
||||
# morph-node
|
||||
morph_node = lambda old_node, new_node: (NIL if sx_truthy((dom_has_attr_p(old_node, 'sx-preserve') if sx_truthy(dom_has_attr_p(old_node, 'sx-preserve')) else dom_has_attr_p(old_node, 'sx-ignore'))) else (dom_replace_child(dom_parent(old_node), dom_clone(new_node), old_node) if sx_truthy(((not sx_truthy((dom_node_type(old_node) == dom_node_type(new_node)))) if sx_truthy((not sx_truthy((dom_node_type(old_node) == dom_node_type(new_node))))) else (not sx_truthy((dom_node_name(old_node) == dom_node_name(new_node)))))) else ((dom_set_text_content(old_node, dom_text_content(new_node)) if sx_truthy((not sx_truthy((dom_text_content(old_node) == dom_text_content(new_node))))) else NIL) if sx_truthy(((dom_node_type(old_node) == 3) if sx_truthy((dom_node_type(old_node) == 3)) else (dom_node_type(old_node) == 8))) else (_sx_begin(sync_attrs(old_node, new_node), (morph_children(old_node, new_node) if sx_truthy((not sx_truthy((dom_is_active_element_p(old_node) if not sx_truthy(dom_is_active_element_p(old_node)) else dom_is_input_element_p(old_node))))) else NIL)) if sx_truthy((dom_node_type(old_node) == 1)) else NIL))))
|
||||
|
||||
# sync-attrs
|
||||
sync_attrs = _sx_fn(lambda old_el, new_el: (
|
||||
for_each(lambda attr: (lambda name: (lambda val: (dom_set_attr(old_el, name, val) if sx_truthy((not sx_truthy((dom_get_attr(old_el, name) == val)))) else NIL))(nth(attr, 1)))(first(attr)), dom_attr_list(new_el)),
|
||||
for_each(lambda attr: (dom_remove_attr(old_el, first(attr)) if sx_truthy((not sx_truthy(dom_has_attr_p(new_el, first(attr))))) else NIL), dom_attr_list(old_el))
|
||||
)[-1])
|
||||
|
||||
# morph-children
|
||||
def morph_children(old_parent, new_parent):
|
||||
_cells = {}
|
||||
old_kids = dom_child_list(old_parent)
|
||||
new_kids = dom_child_list(new_parent)
|
||||
old_by_id = reduce(lambda acc, kid: (lambda id: (_sx_begin(_sx_dict_set(acc, id, kid), acc) if sx_truthy(id) else acc))(dom_id(kid)), {}, old_kids)
|
||||
_cells['oi'] = 0
|
||||
for new_child in new_kids:
|
||||
match_id = dom_id(new_child)
|
||||
match_by_id = (dict_get(old_by_id, match_id) if sx_truthy(match_id) else NIL)
|
||||
if sx_truthy((match_by_id if not sx_truthy(match_by_id) else (not sx_truthy(is_nil(match_by_id))))):
|
||||
if sx_truthy(((_cells['oi'] < len(old_kids)) if not sx_truthy((_cells['oi'] < len(old_kids))) else (not sx_truthy((match_by_id == nth(old_kids, _cells['oi'])))))):
|
||||
dom_insert_before(old_parent, match_by_id, (nth(old_kids, _cells['oi']) if sx_truthy((_cells['oi'] < len(old_kids))) else NIL))
|
||||
morph_node(match_by_id, new_child)
|
||||
_cells['oi'] = (_cells['oi'] + 1)
|
||||
elif sx_truthy((_cells['oi'] < len(old_kids))):
|
||||
old_child = nth(old_kids, _cells['oi'])
|
||||
if sx_truthy((dom_id(old_child) if not sx_truthy(dom_id(old_child)) else (not sx_truthy(match_id)))):
|
||||
dom_insert_before(old_parent, dom_clone(new_child), old_child)
|
||||
else:
|
||||
morph_node(old_child, new_child)
|
||||
_cells['oi'] = (_cells['oi'] + 1)
|
||||
else:
|
||||
dom_append(old_parent, dom_clone(new_child))
|
||||
return for_each(lambda i: ((lambda leftover: (dom_remove_child(old_parent, leftover) if sx_truthy((dom_is_child_of_p(leftover, old_parent) if not sx_truthy(dom_is_child_of_p(leftover, old_parent)) else ((not sx_truthy(dom_has_attr_p(leftover, 'sx-preserve'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(leftover, 'sx-preserve')))) else (not sx_truthy(dom_has_attr_p(leftover, 'sx-ignore')))))) else NIL))(nth(old_kids, i)) if sx_truthy((i >= _cells['oi'])) else NIL), range(_cells['oi'], len(old_kids)))
|
||||
|
||||
# swap-dom-nodes
|
||||
swap_dom_nodes = lambda target, new_nodes, strategy: _sx_case(strategy, [('innerHTML', lambda: (morph_children(target, new_nodes) if sx_truthy(dom_is_fragment_p(new_nodes)) else (lambda wrapper: _sx_begin(dom_append(wrapper, new_nodes), morph_children(target, wrapper)))(dom_create_element('div', NIL)))), ('outerHTML', lambda: (lambda parent: _sx_begin(((lambda fc: (_sx_begin(morph_node(target, fc), (lambda sib: insert_remaining_siblings(parent, target, sib))(dom_next_sibling(fc))) if sx_truthy(fc) else dom_remove_child(parent, target)))(dom_first_child(new_nodes)) if sx_truthy(dom_is_fragment_p(new_nodes)) else morph_node(target, new_nodes)), parent))(dom_parent(target))), ('afterend', lambda: dom_insert_after(target, new_nodes)), ('beforeend', lambda: dom_append(target, new_nodes)), ('afterbegin', lambda: dom_prepend(target, new_nodes)), ('beforebegin', lambda: dom_insert_before(dom_parent(target), new_nodes, target)), ('delete', lambda: dom_remove_child(dom_parent(target), target)), ('none', lambda: NIL), (None, lambda: (morph_children(target, new_nodes) if sx_truthy(dom_is_fragment_p(new_nodes)) else (lambda wrapper: _sx_begin(dom_append(wrapper, new_nodes), morph_children(target, wrapper)))(dom_create_element('div', NIL))))])
|
||||
|
||||
# insert-remaining-siblings
|
||||
insert_remaining_siblings = lambda parent, ref_node, sib: ((lambda next: _sx_begin(dom_insert_after(ref_node, sib), insert_remaining_siblings(parent, sib, next)))(dom_next_sibling(sib)) if sx_truthy(sib) else NIL)
|
||||
|
||||
# swap-html-string
|
||||
swap_html_string = lambda target, html, strategy: _sx_case(strategy, [('innerHTML', lambda: dom_set_inner_html(target, html)), ('outerHTML', lambda: (lambda parent: _sx_begin(dom_insert_adjacent_html(target, 'afterend', html), dom_remove_child(parent, target), parent))(dom_parent(target))), ('afterend', lambda: dom_insert_adjacent_html(target, 'afterend', html)), ('beforeend', lambda: dom_insert_adjacent_html(target, 'beforeend', html)), ('afterbegin', lambda: dom_insert_adjacent_html(target, 'afterbegin', html)), ('beforebegin', lambda: dom_insert_adjacent_html(target, 'beforebegin', html)), ('delete', lambda: dom_remove_child(dom_parent(target), target)), ('none', lambda: NIL), (None, lambda: dom_set_inner_html(target, html))])
|
||||
|
||||
# handle-history
|
||||
handle_history = lambda el, url, resp_headers: (lambda push_url: (lambda replace_url: (lambda hdr_replace: (browser_replace_state(hdr_replace) if sx_truthy(hdr_replace) else (browser_push_state((url if sx_truthy((push_url == 'true')) else push_url)) if sx_truthy((push_url if not sx_truthy(push_url) else (not sx_truthy((push_url == 'false'))))) else (browser_replace_state((url if sx_truthy((replace_url == 'true')) else replace_url)) if sx_truthy((replace_url if not sx_truthy(replace_url) else (not sx_truthy((replace_url == 'false'))))) else NIL))))(get(resp_headers, 'replace-url')))(dom_get_attr(el, 'sx-replace-url')))(dom_get_attr(el, 'sx-push-url'))
|
||||
|
||||
# PRELOAD_TTL
|
||||
PRELOAD_TTL = 30000
|
||||
|
||||
# preload-cache-get
|
||||
preload_cache_get = lambda cache, url: (lambda entry: (NIL if sx_truthy(is_nil(entry)) else (_sx_begin(dict_delete(cache, url), NIL) if sx_truthy(((now_ms() - get(entry, 'timestamp')) > PRELOAD_TTL)) else _sx_begin(dict_delete(cache, url), entry))))(dict_get(cache, url))
|
||||
|
||||
# preload-cache-set
|
||||
preload_cache_set = lambda cache, url, text, content_type: _sx_dict_set(cache, url, {'text': text, 'content-type': content_type, 'timestamp': now_ms()})
|
||||
|
||||
# classify-trigger
|
||||
classify_trigger = lambda trigger: (lambda event: ('poll' if sx_truthy((event == 'every')) else ('intersect' if sx_truthy((event == 'intersect')) else ('load' if sx_truthy((event == 'load')) else ('revealed' if sx_truthy((event == 'revealed')) else 'event')))))(get(trigger, 'event'))
|
||||
|
||||
# should-boost-link?
|
||||
should_boost_link_p = lambda link: (lambda href: (href if not sx_truthy(href) else ((not sx_truthy(starts_with_p(href, '#'))) if not sx_truthy((not sx_truthy(starts_with_p(href, '#')))) else ((not sx_truthy(starts_with_p(href, 'javascript:'))) if not sx_truthy((not sx_truthy(starts_with_p(href, 'javascript:')))) else ((not sx_truthy(starts_with_p(href, 'mailto:'))) if not sx_truthy((not sx_truthy(starts_with_p(href, 'mailto:')))) else (browser_same_origin_p(href) if not sx_truthy(browser_same_origin_p(href)) else ((not sx_truthy(dom_has_attr_p(link, 'sx-get'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(link, 'sx-get')))) else ((not sx_truthy(dom_has_attr_p(link, 'sx-post'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(link, 'sx-post')))) else (not sx_truthy(dom_has_attr_p(link, 'sx-disable')))))))))))(dom_get_attr(link, 'href'))
|
||||
|
||||
# should-boost-form?
|
||||
should_boost_form_p = lambda form: ((not sx_truthy(dom_has_attr_p(form, 'sx-get'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(form, 'sx-get')))) else ((not sx_truthy(dom_has_attr_p(form, 'sx-post'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(form, 'sx-post')))) else (not sx_truthy(dom_has_attr_p(form, 'sx-disable')))))
|
||||
|
||||
# parse-sse-swap
|
||||
parse_sse_swap = lambda el: (dom_get_attr(el, 'sx-sse-swap') if sx_truthy(dom_get_attr(el, 'sx-sse-swap')) else 'message')
|
||||
|
||||
|
||||
# === Transpiled from router (client-side route matching) ===
|
||||
|
||||
# split-path-segments
|
||||
split_path_segments = lambda path: (lambda trimmed: (lambda trimmed2: ([] if sx_truthy(empty_p(trimmed2)) else split(trimmed2, '/')))((slice(trimmed, 0, (len(trimmed) - 1)) if sx_truthy(((not sx_truthy(empty_p(trimmed))) if not sx_truthy((not sx_truthy(empty_p(trimmed)))) else ends_with_p(trimmed, '/'))) else trimmed)))((slice(path, 1) if sx_truthy(starts_with_p(path, '/')) else path))
|
||||
|
||||
# make-route-segment
|
||||
make_route_segment = lambda seg: ((lambda param_name: (lambda d: _sx_begin(_sx_dict_set(d, 'type', 'param'), _sx_dict_set(d, 'value', param_name), d))({}))(slice(seg, 1, (len(seg) - 1))) if sx_truthy((starts_with_p(seg, '<') if not sx_truthy(starts_with_p(seg, '<')) else ends_with_p(seg, '>'))) else (lambda d: _sx_begin(_sx_dict_set(d, 'type', 'literal'), _sx_dict_set(d, 'value', seg), d))({}))
|
||||
|
||||
# parse-route-pattern
|
||||
parse_route_pattern = lambda pattern: (lambda segments: map(make_route_segment, segments))(split_path_segments(pattern))
|
||||
|
||||
# match-route-segments
|
||||
def match_route_segments(path_segs, parsed_segs):
|
||||
_cells = {}
|
||||
return (NIL if sx_truthy((not sx_truthy((len(path_segs) == len(parsed_segs))))) else (lambda params: _sx_begin(_sx_cell_set(_cells, 'matched', True), _sx_begin(for_each_indexed(lambda i, parsed_seg: ((lambda path_seg: (lambda seg_type: ((_sx_cell_set(_cells, 'matched', False) if sx_truthy((not sx_truthy((path_seg == get(parsed_seg, 'value'))))) else NIL) if sx_truthy((seg_type == 'literal')) else (_sx_dict_set(params, get(parsed_seg, 'value'), path_seg) if sx_truthy((seg_type == 'param')) else _sx_cell_set(_cells, 'matched', False))))(get(parsed_seg, 'type')))(nth(path_segs, i)) if sx_truthy(_cells['matched']) else NIL), parsed_segs), (params if sx_truthy(_cells['matched']) else NIL))))({}))
|
||||
|
||||
# match-route
|
||||
match_route = lambda path, pattern: (lambda path_segs: (lambda parsed_segs: match_route_segments(path_segs, parsed_segs))(parse_route_pattern(pattern)))(split_path_segments(path))
|
||||
|
||||
# find-matching-route
|
||||
def find_matching_route(path, routes):
|
||||
_cells = {}
|
||||
path_segs = split_path_segments(path)
|
||||
_cells['result'] = NIL
|
||||
for route in routes:
|
||||
if sx_truthy(is_nil(_cells['result'])):
|
||||
params = match_route_segments(path_segs, get(route, 'parsed'))
|
||||
if sx_truthy((not sx_truthy(is_nil(params)))):
|
||||
matched = merge(route, {})
|
||||
matched['params'] = params
|
||||
_cells['result'] = matched
|
||||
return _cells['result']
|
||||
# page-render-plan
|
||||
page_render_plan = lambda page_source, env, io_names: (lambda needed: (lambda comp_targets: (lambda server_list: (lambda client_list: (lambda io_deps: _sx_begin(for_each(lambda name: (lambda target: _sx_begin(_sx_dict_set(comp_targets, name, target), (_sx_begin(_sx_append(server_list, name), for_each(lambda io_ref: (_sx_append(io_deps, io_ref) if sx_truthy((not sx_truthy(contains_p(io_deps, io_ref)))) else NIL), transitive_io_refs(name, env, io_names))) if sx_truthy((target == 'server')) else _sx_append(client_list, name))))(render_target(name, env, io_names)), needed), {'components': comp_targets, 'server': server_list, 'client': client_list, 'io-deps': io_deps}))([]))([]))([]))({}))(components_needed(page_source, env))
|
||||
|
||||
|
||||
# =========================================================================
|
||||
|
||||
@@ -223,3 +223,83 @@
|
||||
|
||||
(deftest "leaf component is pure"
|
||||
(assert-true (component-pure? "~dep-leaf" (test-env) (list "fetch-data")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. render-target — boundary decision with affinity
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
;; Components with explicit affinity annotations
|
||||
(defcomp ~dep-force-client (&key x)
|
||||
:affinity :client
|
||||
(div (fetch-data "/api") x))
|
||||
|
||||
(defcomp ~dep-force-server (&key x)
|
||||
:affinity :server
|
||||
(div x))
|
||||
|
||||
(defcomp ~dep-auto-pure (&key x)
|
||||
(div x))
|
||||
|
||||
(defcomp ~dep-auto-io (&key x)
|
||||
(div (fetch-data "/api")))
|
||||
|
||||
(defsuite "render-target"
|
||||
|
||||
(deftest "pure auto component targets client"
|
||||
(assert-equal "client" (render-target "~dep-auto-pure" (test-env) (list "fetch-data"))))
|
||||
|
||||
(deftest "IO auto component targets server"
|
||||
(assert-equal "server" (render-target "~dep-auto-io" (test-env) (list "fetch-data"))))
|
||||
|
||||
(deftest "affinity client overrides IO to client"
|
||||
(assert-equal "client" (render-target "~dep-force-client" (test-env) (list "fetch-data"))))
|
||||
|
||||
(deftest "affinity server overrides pure to server"
|
||||
(assert-equal "server" (render-target "~dep-force-server" (test-env) (list "fetch-data"))))
|
||||
|
||||
(deftest "leaf component targets client"
|
||||
(assert-equal "client" (render-target "~dep-leaf" (test-env) (list "fetch-data"))))
|
||||
|
||||
(deftest "unknown name targets server"
|
||||
(assert-equal "server" (render-target "~nonexistent" (test-env) (list "fetch-data")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 7. page-render-plan — per-page boundary plan
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
;; A page component that uses both pure and IO components
|
||||
(defcomp ~plan-page (&key data)
|
||||
(div
|
||||
(~dep-auto-pure :x "hello")
|
||||
(~dep-auto-io :x data)
|
||||
(~dep-force-client :x "interactive")))
|
||||
|
||||
(defsuite "page-render-plan"
|
||||
|
||||
(deftest "plan classifies components correctly"
|
||||
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
|
||||
;; ~plan-page has transitive IO deps (via ~dep-auto-io) so targets server
|
||||
(assert-equal "server" (dict-get (get plan :components) "~plan-page"))
|
||||
(assert-equal "client" (dict-get (get plan :components) "~dep-auto-pure"))
|
||||
(assert-equal "server" (dict-get (get plan :components) "~dep-auto-io"))
|
||||
(assert-equal "client" (dict-get (get plan :components) "~dep-force-client"))))
|
||||
|
||||
(deftest "plan server list contains IO components"
|
||||
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
|
||||
(assert-true (contains? (get plan :server) "~dep-auto-io"))))
|
||||
|
||||
(deftest "plan client list contains pure components"
|
||||
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
|
||||
(assert-true (contains? (get plan :client) "~dep-auto-pure"))
|
||||
(assert-true (contains? (get plan :client) "~dep-force-client"))))
|
||||
|
||||
(deftest "plan collects IO deps from server components"
|
||||
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
|
||||
(assert-true (contains? (get plan :io-deps) "fetch-data"))))
|
||||
|
||||
(deftest "pure-only page has empty server list"
|
||||
(let ((plan (page-render-plan "(~dep-auto-pure :x 1)" (test-env) (list "fetch-data"))))
|
||||
(assert-equal 0 (len (get plan :server)))
|
||||
(assert-true (> (len (get plan :client)) 0)))))
|
||||
|
||||
@@ -396,7 +396,32 @@
|
||||
(deftest "component with default via or"
|
||||
(defcomp ~label (&key text)
|
||||
(span (or text "default")))
|
||||
(assert-true (not (nil? ~label)))))
|
||||
(assert-true (not (nil? ~label))))
|
||||
|
||||
(deftest "defcomp default affinity is auto"
|
||||
(defcomp ~aff-default (&key x)
|
||||
(div x))
|
||||
(assert-equal "auto" (component-affinity ~aff-default)))
|
||||
|
||||
(deftest "defcomp affinity client"
|
||||
(defcomp ~aff-client (&key x)
|
||||
:affinity :client
|
||||
(div x))
|
||||
(assert-equal "client" (component-affinity ~aff-client)))
|
||||
|
||||
(deftest "defcomp affinity server"
|
||||
(defcomp ~aff-server (&key x)
|
||||
:affinity :server
|
||||
(div x))
|
||||
(assert-equal "server" (component-affinity ~aff-server)))
|
||||
|
||||
(deftest "defcomp affinity preserves body"
|
||||
(defcomp ~aff-body (&key val)
|
||||
:affinity :client
|
||||
(span val))
|
||||
;; Component should still render correctly
|
||||
(assert-equal "client" (component-affinity ~aff-body))
|
||||
(assert-true (not (nil? ~aff-body)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -1,782 +0,0 @@
|
||||
"""
|
||||
Style dictionary — maps keyword atoms to CSS declarations.
|
||||
|
||||
Pure data. Each key is a Tailwind-compatible class name (used as an sx keyword
|
||||
atom in ``(css :flex :gap-4 :p-2)``), and each value is the CSS declaration(s)
|
||||
that class produces. Declarations are self-contained — no ``--tw-*`` custom
|
||||
properties needed.
|
||||
|
||||
Generated from the codebase's tw.css via ``css_registry.py`` then simplified
|
||||
to remove Tailwind v3 variable indirection.
|
||||
|
||||
Used by:
|
||||
- ``style_resolver.py`` (server) — resolves ``(css ...)`` to StyleValue
|
||||
- ``sx.js`` (client) — same resolution, cached in localStorage
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Base atoms — keyword → CSS declarations
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# ~466 atoms covering all utilities used across the codebase.
|
||||
# Variants (hover:*, sm:*, focus:*, etc.) are NOT stored here — the
|
||||
# resolver splits "hover:bg-sky-200" into variant="hover" + atom="bg-sky-200"
|
||||
# and wraps the declaration in the appropriate pseudo/media rule.
|
||||
|
||||
STYLE_ATOMS: dict[str, str] = {
|
||||
# ── Display ──────────────────────────────────────────────────────────
|
||||
"block": "display:block",
|
||||
"inline-block": "display:inline-block",
|
||||
"inline": "display:inline",
|
||||
"flex": "display:flex",
|
||||
"inline-flex": "display:inline-flex",
|
||||
"table": "display:table",
|
||||
"table-row": "display:table-row",
|
||||
"grid": "display:grid",
|
||||
"contents": "display:contents",
|
||||
"hidden": "display:none",
|
||||
|
||||
# ── Position ─────────────────────────────────────────────────────────
|
||||
"static": "position:static",
|
||||
"fixed": "position:fixed",
|
||||
"absolute": "position:absolute",
|
||||
"relative": "position:relative",
|
||||
"inset-0": "inset:0",
|
||||
"top-0": "top:0",
|
||||
"top-1/2": "top:50%",
|
||||
"top-2": "top:.5rem",
|
||||
"top-20": "top:5rem",
|
||||
"top-[8px]": "top:8px",
|
||||
"top-full": "top:100%",
|
||||
"right-2": "right:.5rem",
|
||||
"right-[8px]": "right:8px",
|
||||
"bottom-full": "bottom:100%",
|
||||
"left-1/2": "left:50%",
|
||||
"left-2": "left:.5rem",
|
||||
"-right-2": "right:-.5rem",
|
||||
"-right-3": "right:-.75rem",
|
||||
"-top-1.5": "top:-.375rem",
|
||||
"-top-2": "top:-.5rem",
|
||||
|
||||
# ── Z-Index ──────────────────────────────────────────────────────────
|
||||
"z-10": "z-index:10",
|
||||
"z-40": "z-index:40",
|
||||
"z-50": "z-index:50",
|
||||
|
||||
# ── Grid ─────────────────────────────────────────────────────────────
|
||||
"grid-cols-1": "grid-template-columns:repeat(1,minmax(0,1fr))",
|
||||
"grid-cols-2": "grid-template-columns:repeat(2,minmax(0,1fr))",
|
||||
"grid-cols-3": "grid-template-columns:repeat(3,minmax(0,1fr))",
|
||||
"grid-cols-4": "grid-template-columns:repeat(4,minmax(0,1fr))",
|
||||
"grid-cols-5": "grid-template-columns:repeat(5,minmax(0,1fr))",
|
||||
"grid-cols-6": "grid-template-columns:repeat(6,minmax(0,1fr))",
|
||||
"grid-cols-7": "grid-template-columns:repeat(7,minmax(0,1fr))",
|
||||
"grid-cols-12": "grid-template-columns:repeat(12,minmax(0,1fr))",
|
||||
"col-span-2": "grid-column:span 2/span 2",
|
||||
"col-span-3": "grid-column:span 3/span 3",
|
||||
"col-span-4": "grid-column:span 4/span 4",
|
||||
"col-span-5": "grid-column:span 5/span 5",
|
||||
"col-span-12": "grid-column:span 12/span 12",
|
||||
"col-span-full": "grid-column:1/-1",
|
||||
|
||||
# ── Flexbox ──────────────────────────────────────────────────────────
|
||||
"flex-row": "flex-direction:row",
|
||||
"flex-col": "flex-direction:column",
|
||||
"flex-wrap": "flex-wrap:wrap",
|
||||
"flex-0": "flex:0",
|
||||
"flex-1": "flex:1 1 0%",
|
||||
"flex-shrink-0": "flex-shrink:0",
|
||||
"shrink-0": "flex-shrink:0",
|
||||
"flex-shrink": "flex-shrink:1",
|
||||
|
||||
# ── Alignment ────────────────────────────────────────────────────────
|
||||
"items-start": "align-items:flex-start",
|
||||
"items-end": "align-items:flex-end",
|
||||
"items-center": "align-items:center",
|
||||
"items-baseline": "align-items:baseline",
|
||||
"justify-start": "justify-content:flex-start",
|
||||
"justify-end": "justify-content:flex-end",
|
||||
"justify-center": "justify-content:center",
|
||||
"justify-between": "justify-content:space-between",
|
||||
"self-start": "align-self:flex-start",
|
||||
"self-center": "align-self:center",
|
||||
"place-items-center": "place-items:center",
|
||||
|
||||
# ── Gap ───────────────────────────────────────────────────────────────
|
||||
"gap-px": "gap:1px",
|
||||
"gap-0.5": "gap:.125rem",
|
||||
"gap-1": "gap:.25rem",
|
||||
"gap-1.5": "gap:.375rem",
|
||||
"gap-2": "gap:.5rem",
|
||||
"gap-3": "gap:.75rem",
|
||||
"gap-4": "gap:1rem",
|
||||
"gap-5": "gap:1.25rem",
|
||||
"gap-6": "gap:1.5rem",
|
||||
"gap-8": "gap:2rem",
|
||||
"gap-[4px]": "gap:4px",
|
||||
"gap-[8px]": "gap:8px",
|
||||
"gap-[16px]": "gap:16px",
|
||||
"gap-x-3": "column-gap:.75rem",
|
||||
"gap-y-1": "row-gap:.25rem",
|
||||
|
||||
# ── Margin ───────────────────────────────────────────────────────────
|
||||
"m-0": "margin:0",
|
||||
"m-2": "margin:.5rem",
|
||||
"mx-1": "margin-left:.25rem;margin-right:.25rem",
|
||||
"mx-2": "margin-left:.5rem;margin-right:.5rem",
|
||||
"mx-4": "margin-left:1rem;margin-right:1rem",
|
||||
"mx-auto": "margin-left:auto;margin-right:auto",
|
||||
"my-3": "margin-top:.75rem;margin-bottom:.75rem",
|
||||
"-mb-px": "margin-bottom:-1px",
|
||||
"mb-1": "margin-bottom:.25rem",
|
||||
"mb-2": "margin-bottom:.5rem",
|
||||
"mb-3": "margin-bottom:.75rem",
|
||||
"mb-4": "margin-bottom:1rem",
|
||||
"mb-6": "margin-bottom:1.5rem",
|
||||
"mb-8": "margin-bottom:2rem",
|
||||
"mb-12": "margin-bottom:3rem",
|
||||
"mb-[8px]": "margin-bottom:8px",
|
||||
"mb-[24px]": "margin-bottom:24px",
|
||||
"ml-1": "margin-left:.25rem",
|
||||
"ml-2": "margin-left:.5rem",
|
||||
"ml-4": "margin-left:1rem",
|
||||
"ml-auto": "margin-left:auto",
|
||||
"mr-1": "margin-right:.25rem",
|
||||
"mr-2": "margin-right:.5rem",
|
||||
"mr-3": "margin-right:.75rem",
|
||||
"mt-0.5": "margin-top:.125rem",
|
||||
"mt-1": "margin-top:.25rem",
|
||||
"mt-2": "margin-top:.5rem",
|
||||
"mt-3": "margin-top:.75rem",
|
||||
"mt-4": "margin-top:1rem",
|
||||
"mt-5": "margin-top:1.25rem",
|
||||
"mt-6": "margin-top:1.5rem",
|
||||
"mt-8": "margin-top:2rem",
|
||||
"mt-[8px]": "margin-top:8px",
|
||||
"mt-[16px]": "margin-top:16px",
|
||||
"mt-[32px]": "margin-top:32px",
|
||||
|
||||
# ── Padding ──────────────────────────────────────────────────────────
|
||||
"p-0": "padding:0",
|
||||
"p-1": "padding:.25rem",
|
||||
"p-1.5": "padding:.375rem",
|
||||
"p-2": "padding:.5rem",
|
||||
"p-3": "padding:.75rem",
|
||||
"p-4": "padding:1rem",
|
||||
"p-5": "padding:1.25rem",
|
||||
"p-6": "padding:1.5rem",
|
||||
"p-8": "padding:2rem",
|
||||
"px-1": "padding-left:.25rem;padding-right:.25rem",
|
||||
"px-1.5": "padding-left:.375rem;padding-right:.375rem",
|
||||
"px-2": "padding-left:.5rem;padding-right:.5rem",
|
||||
"px-2.5": "padding-left:.625rem;padding-right:.625rem",
|
||||
"px-3": "padding-left:.75rem;padding-right:.75rem",
|
||||
"px-4": "padding-left:1rem;padding-right:1rem",
|
||||
"px-6": "padding-left:1.5rem;padding-right:1.5rem",
|
||||
"px-[8px]": "padding-left:8px;padding-right:8px",
|
||||
"px-[12px]": "padding-left:12px;padding-right:12px",
|
||||
"px-[16px]": "padding-left:16px;padding-right:16px",
|
||||
"px-[20px]": "padding-left:20px;padding-right:20px",
|
||||
"py-0.5": "padding-top:.125rem;padding-bottom:.125rem",
|
||||
"py-1": "padding-top:.25rem;padding-bottom:.25rem",
|
||||
"py-1.5": "padding-top:.375rem;padding-bottom:.375rem",
|
||||
"py-2": "padding-top:.5rem;padding-bottom:.5rem",
|
||||
"py-3": "padding-top:.75rem;padding-bottom:.75rem",
|
||||
"py-4": "padding-top:1rem;padding-bottom:1rem",
|
||||
"py-6": "padding-top:1.5rem;padding-bottom:1.5rem",
|
||||
"py-8": "padding-top:2rem;padding-bottom:2rem",
|
||||
"py-12": "padding-top:3rem;padding-bottom:3rem",
|
||||
"py-16": "padding-top:4rem;padding-bottom:4rem",
|
||||
"py-[6px]": "padding-top:6px;padding-bottom:6px",
|
||||
"py-[12px]": "padding-top:12px;padding-bottom:12px",
|
||||
"pb-1": "padding-bottom:.25rem",
|
||||
"pb-2": "padding-bottom:.5rem",
|
||||
"pb-3": "padding-bottom:.75rem",
|
||||
"pb-4": "padding-bottom:1rem",
|
||||
"pb-6": "padding-bottom:1.5rem",
|
||||
"pb-8": "padding-bottom:2rem",
|
||||
"pb-[48px]": "padding-bottom:48px",
|
||||
"pl-2": "padding-left:.5rem",
|
||||
"pl-3": "padding-left:.75rem",
|
||||
"pl-5": "padding-left:1.25rem",
|
||||
"pl-6": "padding-left:1.5rem",
|
||||
"pr-1": "padding-right:.25rem",
|
||||
"pr-2": "padding-right:.5rem",
|
||||
"pr-4": "padding-right:1rem",
|
||||
"pt-2": "padding-top:.5rem",
|
||||
"pt-3": "padding-top:.75rem",
|
||||
"pt-4": "padding-top:1rem",
|
||||
"pt-[16px]": "padding-top:16px",
|
||||
|
||||
# ── Width ────────────────────────────────────────────────────────────
|
||||
"w-1": "width:.25rem",
|
||||
"w-2": "width:.5rem",
|
||||
"w-4": "width:1rem",
|
||||
"w-5": "width:1.25rem",
|
||||
"w-6": "width:1.5rem",
|
||||
"w-8": "width:2rem",
|
||||
"w-10": "width:2.5rem",
|
||||
"w-11": "width:2.75rem",
|
||||
"w-12": "width:3rem",
|
||||
"w-14": "width:3.5rem",
|
||||
"w-16": "width:4rem",
|
||||
"w-20": "width:5rem",
|
||||
"w-24": "width:6rem",
|
||||
"w-28": "width:7rem",
|
||||
"w-32": "width:8rem",
|
||||
"w-40": "width:10rem",
|
||||
"w-48": "width:12rem",
|
||||
"w-56": "width:14rem",
|
||||
"w-1/2": "width:50%",
|
||||
"w-1/3": "width:33.333333%",
|
||||
"w-1/4": "width:25%",
|
||||
"w-1/6": "width:16.666667%",
|
||||
"w-2/6": "width:33.333333%",
|
||||
"w-3/4": "width:75%",
|
||||
"w-full": "width:100%",
|
||||
"w-auto": "width:auto",
|
||||
"w-[1em]": "width:1em",
|
||||
"w-[32px]": "width:32px",
|
||||
|
||||
# ── Height ───────────────────────────────────────────────────────────
|
||||
"h-2": "height:.5rem",
|
||||
"h-4": "height:1rem",
|
||||
"h-5": "height:1.25rem",
|
||||
"h-6": "height:1.5rem",
|
||||
"h-8": "height:2rem",
|
||||
"h-10": "height:2.5rem",
|
||||
"h-12": "height:3rem",
|
||||
"h-14": "height:3.5rem",
|
||||
"h-14": "height:3.5rem",
|
||||
"h-16": "height:4rem",
|
||||
"h-24": "height:6rem",
|
||||
"h-28": "height:7rem",
|
||||
"h-48": "height:12rem",
|
||||
"h-64": "height:16rem",
|
||||
"h-full": "height:100%",
|
||||
"h-[1em]": "height:1em",
|
||||
"h-[30vh]": "height:30vh",
|
||||
"h-[32px]": "height:32px",
|
||||
"h-[60vh]": "height:60vh",
|
||||
|
||||
# ── Min/Max Dimensions ───────────────────────────────────────────────
|
||||
"min-w-0": "min-width:0",
|
||||
"min-w-full": "min-width:100%",
|
||||
"min-w-[1.25rem]": "min-width:1.25rem",
|
||||
"min-w-[180px]": "min-width:180px",
|
||||
"min-h-0": "min-height:0",
|
||||
"min-h-20": "min-height:5rem",
|
||||
"min-h-[3rem]": "min-height:3rem",
|
||||
"min-h-[50vh]": "min-height:50vh",
|
||||
"max-w-xs": "max-width:20rem",
|
||||
"max-w-md": "max-width:28rem",
|
||||
"max-w-lg": "max-width:32rem",
|
||||
"max-w-2xl": "max-width:42rem",
|
||||
"max-w-3xl": "max-width:48rem",
|
||||
"max-w-4xl": "max-width:56rem",
|
||||
"max-w-full": "max-width:100%",
|
||||
"max-w-0": "max-width:0",
|
||||
"max-w-none": "max-width:none",
|
||||
"max-w-screen-2xl": "max-width:1536px",
|
||||
"max-w-[360px]": "max-width:360px",
|
||||
"max-w-[768px]": "max-width:768px",
|
||||
"max-w-[640px]": "max-width:640px",
|
||||
"max-h-32": "max-height:8rem",
|
||||
"max-h-64": "max-height:16rem",
|
||||
"max-h-72": "max-height:18rem",
|
||||
"max-h-96": "max-height:24rem",
|
||||
"max-h-none": "max-height:none",
|
||||
"max-h-[448px]": "max-height:448px",
|
||||
"max-h-[50vh]": "max-height:50vh",
|
||||
|
||||
# ── Typography ───────────────────────────────────────────────────────
|
||||
"text-xs": "font-size:.75rem;line-height:1rem",
|
||||
"text-sm": "font-size:.875rem;line-height:1.25rem",
|
||||
"text-base": "font-size:1rem;line-height:1.5rem",
|
||||
"text-md": "font-size:1rem;line-height:1.5rem", # alias for text-base
|
||||
"text-lg": "font-size:1.125rem;line-height:1.75rem",
|
||||
"text-xl": "font-size:1.25rem;line-height:1.75rem",
|
||||
"text-2xl": "font-size:1.5rem;line-height:2rem",
|
||||
"text-3xl": "font-size:1.875rem;line-height:2.25rem",
|
||||
"text-4xl": "font-size:2.25rem;line-height:2.5rem",
|
||||
"text-5xl": "font-size:3rem;line-height:1",
|
||||
"text-6xl": "font-size:3.75rem;line-height:1",
|
||||
"text-8xl": "font-size:6rem;line-height:1",
|
||||
"text-[8px]": "font-size:8px",
|
||||
"text-[9px]": "font-size:9px",
|
||||
"text-[10px]": "font-size:10px",
|
||||
"text-[11px]": "font-size:11px",
|
||||
"text-[13px]": "font-size:13px",
|
||||
"text-[14px]": "font-size:14px",
|
||||
"text-[16px]": "font-size:16px",
|
||||
"text-[18px]": "font-size:18px",
|
||||
"text-[36px]": "font-size:36px",
|
||||
"text-[40px]": "font-size:40px",
|
||||
"text-[0.6rem]": "font-size:.6rem",
|
||||
"text-[0.65rem]": "font-size:.65rem",
|
||||
"text-[0.7rem]": "font-size:.7rem",
|
||||
"font-normal": "font-weight:400",
|
||||
"font-medium": "font-weight:500",
|
||||
"font-semibold": "font-weight:600",
|
||||
"font-bold": "font-weight:700",
|
||||
"font-mono": "font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace",
|
||||
"italic": "font-style:italic",
|
||||
"uppercase": "text-transform:uppercase",
|
||||
"capitalize": "text-transform:capitalize",
|
||||
"tabular-nums": "font-variant-numeric:tabular-nums",
|
||||
"leading-none": "line-height:1",
|
||||
"leading-tight": "line-height:1.25",
|
||||
"leading-snug": "line-height:1.375",
|
||||
"leading-relaxed": "line-height:1.625",
|
||||
"tracking-tight": "letter-spacing:-.025em",
|
||||
"tracking-wide": "letter-spacing:.025em",
|
||||
"tracking-widest": "letter-spacing:.1em",
|
||||
"text-left": "text-align:left",
|
||||
"text-center": "text-align:center",
|
||||
"text-right": "text-align:right",
|
||||
"align-top": "vertical-align:top",
|
||||
|
||||
# ── Text Colors ──────────────────────────────────────────────────────
|
||||
"text-white": "color:rgb(255 255 255)",
|
||||
"text-white/80": "color:rgba(255,255,255,.8)",
|
||||
"text-black": "color:rgb(0 0 0)",
|
||||
"text-stone-300": "color:rgb(214 211 209)",
|
||||
"text-stone-400": "color:rgb(168 162 158)",
|
||||
"text-stone-500": "color:rgb(120 113 108)",
|
||||
"text-stone-600": "color:rgb(87 83 78)",
|
||||
"text-stone-700": "color:rgb(68 64 60)",
|
||||
"text-stone-800": "color:rgb(41 37 36)",
|
||||
"text-stone-900": "color:rgb(28 25 23)",
|
||||
"text-slate-400": "color:rgb(148 163 184)",
|
||||
"text-gray-500": "color:rgb(107 114 128)",
|
||||
"text-gray-600": "color:rgb(75 85 99)",
|
||||
"text-red-500": "color:rgb(239 68 68)",
|
||||
"text-red-600": "color:rgb(220 38 38)",
|
||||
"text-red-700": "color:rgb(185 28 28)",
|
||||
"text-red-800": "color:rgb(153 27 27)",
|
||||
"text-rose-500": "color:rgb(244 63 94)",
|
||||
"text-rose-600": "color:rgb(225 29 72)",
|
||||
"text-rose-700": "color:rgb(190 18 60)",
|
||||
"text-rose-800": "color:rgb(159 18 57)",
|
||||
"text-rose-800/80": "color:rgba(159,18,57,.8)",
|
||||
"text-rose-900": "color:rgb(136 19 55)",
|
||||
"text-orange-600": "color:rgb(234 88 12)",
|
||||
"text-amber-500": "color:rgb(245 158 11)",
|
||||
"text-amber-600": "color:rgb(217 119 6)",
|
||||
"text-amber-700": "color:rgb(180 83 9)",
|
||||
"text-amber-800": "color:rgb(146 64 14)",
|
||||
"text-yellow-700": "color:rgb(161 98 7)",
|
||||
"text-green-600": "color:rgb(22 163 74)",
|
||||
"text-green-800": "color:rgb(22 101 52)",
|
||||
"text-green-900": "color:rgb(20 83 45)",
|
||||
"text-neutral-400": "color:rgb(163 163 163)",
|
||||
"text-neutral-500": "color:rgb(115 115 115)",
|
||||
"text-neutral-600": "color:rgb(82 82 82)",
|
||||
"text-emerald-500": "color:rgb(16 185 129)",
|
||||
"text-emerald-600": "color:rgb(5 150 105)",
|
||||
"text-emerald-700": "color:rgb(4 120 87)",
|
||||
"text-emerald-800": "color:rgb(6 95 70)",
|
||||
"text-emerald-900": "color:rgb(6 78 59)",
|
||||
"text-sky-600": "color:rgb(2 132 199)",
|
||||
"text-sky-700": "color:rgb(3 105 161)",
|
||||
"text-sky-800": "color:rgb(7 89 133)",
|
||||
"text-blue-500": "color:rgb(59 130 246)",
|
||||
"text-blue-600": "color:rgb(37 99 235)",
|
||||
"text-blue-700": "color:rgb(29 78 216)",
|
||||
"text-blue-800": "color:rgb(30 64 175)",
|
||||
"text-purple-600": "color:rgb(147 51 234)",
|
||||
"text-violet-600": "color:rgb(124 58 237)",
|
||||
"text-violet-700": "color:rgb(109 40 217)",
|
||||
"text-violet-800": "color:rgb(91 33 182)",
|
||||
"text-violet-900": "color:rgb(76 29 149)",
|
||||
|
||||
# ── Background Colors ────────────────────────────────────────────────
|
||||
"bg-transparent": "background-color:transparent",
|
||||
"bg-white": "background-color:rgb(255 255 255)",
|
||||
"bg-white/60": "background-color:rgba(255,255,255,.6)",
|
||||
"bg-white/70": "background-color:rgba(255,255,255,.7)",
|
||||
"bg-white/80": "background-color:rgba(255,255,255,.8)",
|
||||
"bg-white/90": "background-color:rgba(255,255,255,.9)",
|
||||
"bg-black": "background-color:rgb(0 0 0)",
|
||||
"bg-black/50": "background-color:rgba(0,0,0,.5)",
|
||||
"bg-stone-50": "background-color:rgb(250 250 249)",
|
||||
"bg-stone-100": "background-color:rgb(245 245 244)",
|
||||
"bg-stone-200": "background-color:rgb(231 229 228)",
|
||||
"bg-stone-300": "background-color:rgb(214 211 209)",
|
||||
"bg-stone-400": "background-color:rgb(168 162 158)",
|
||||
"bg-stone-500": "background-color:rgb(120 113 108)",
|
||||
"bg-stone-600": "background-color:rgb(87 83 78)",
|
||||
"bg-stone-700": "background-color:rgb(68 64 60)",
|
||||
"bg-stone-800": "background-color:rgb(41 37 36)",
|
||||
"bg-stone-900": "background-color:rgb(28 25 23)",
|
||||
"bg-slate-100": "background-color:rgb(241 245 249)",
|
||||
"bg-slate-200": "background-color:rgb(226 232 240)",
|
||||
"bg-gray-100": "background-color:rgb(243 244 246)",
|
||||
"bg-red-50": "background-color:rgb(254 242 242)",
|
||||
"bg-red-100": "background-color:rgb(254 226 226)",
|
||||
"bg-red-200": "background-color:rgb(254 202 202)",
|
||||
"bg-red-500": "background-color:rgb(239 68 68)",
|
||||
"bg-red-600": "background-color:rgb(220 38 38)",
|
||||
"bg-rose-50": "background-color:rgb(255 241 242)",
|
||||
"bg-rose-50/80": "background-color:rgba(255,241,242,.8)",
|
||||
"bg-orange-100": "background-color:rgb(255 237 213)",
|
||||
"bg-amber-50": "background-color:rgb(255 251 235)",
|
||||
"bg-amber-50/60": "background-color:rgba(255,251,235,.6)",
|
||||
"bg-amber-100": "background-color:rgb(254 243 199)",
|
||||
"bg-amber-500": "background-color:rgb(245 158 11)",
|
||||
"bg-amber-600": "background-color:rgb(217 119 6)",
|
||||
"bg-yellow-50": "background-color:rgb(254 252 232)",
|
||||
"bg-yellow-100": "background-color:rgb(254 249 195)",
|
||||
"bg-yellow-200": "background-color:rgb(254 240 138)",
|
||||
"bg-yellow-300": "background-color:rgb(253 224 71)",
|
||||
"bg-green-50": "background-color:rgb(240 253 244)",
|
||||
"bg-green-100": "background-color:rgb(220 252 231)",
|
||||
"bg-green-200": "background-color:rgb(187 247 208)",
|
||||
"bg-neutral-50/70": "background-color:rgba(250,250,250,.7)",
|
||||
"bg-black/70": "background-color:rgba(0,0,0,.7)",
|
||||
"bg-emerald-50": "background-color:rgb(236 253 245)",
|
||||
"bg-emerald-50/80": "background-color:rgba(236,253,245,.8)",
|
||||
"bg-emerald-100": "background-color:rgb(209 250 229)",
|
||||
"bg-emerald-200": "background-color:rgb(167 243 208)",
|
||||
"bg-emerald-500": "background-color:rgb(16 185 129)",
|
||||
"bg-emerald-600": "background-color:rgb(5 150 105)",
|
||||
"bg-sky-100": "background-color:rgb(224 242 254)",
|
||||
"bg-sky-200": "background-color:rgb(186 230 253)",
|
||||
"bg-sky-300": "background-color:rgb(125 211 252)",
|
||||
"bg-sky-400": "background-color:rgb(56 189 248)",
|
||||
"bg-sky-500": "background-color:rgb(14 165 233)",
|
||||
"bg-blue-50": "background-color:rgb(239 246 255)",
|
||||
"bg-blue-100": "background-color:rgb(219 234 254)",
|
||||
"bg-blue-600": "background-color:rgb(37 99 235)",
|
||||
"bg-purple-600": "background-color:rgb(147 51 234)",
|
||||
"bg-violet-50": "background-color:rgb(245 243 255)",
|
||||
"bg-violet-100": "background-color:rgb(237 233 254)",
|
||||
"bg-violet-200": "background-color:rgb(221 214 254)",
|
||||
"bg-violet-300": "background-color:rgb(196 181 253)",
|
||||
"bg-violet-400": "background-color:rgb(167 139 250)",
|
||||
"bg-violet-500": "background-color:rgb(139 92 246)",
|
||||
"bg-violet-600": "background-color:rgb(124 58 237)",
|
||||
"bg-violet-700": "background-color:rgb(109 40 217)",
|
||||
"bg-amber-200": "background-color:rgb(253 230 138)",
|
||||
"bg-blue-700": "background-color:rgb(29 78 216)",
|
||||
"bg-emerald-700": "background-color:rgb(4 120 87)",
|
||||
"bg-purple-700": "background-color:rgb(126 34 206)",
|
||||
"bg-stone-50/60": "background-color:rgba(250,250,249,.6)",
|
||||
|
||||
# ── Border ───────────────────────────────────────────────────────────
|
||||
"border": "border-width:1px",
|
||||
"border-2": "border-width:2px",
|
||||
"border-4": "border-width:4px",
|
||||
"border-t": "border-top-width:1px",
|
||||
"border-t-0": "border-top-width:0",
|
||||
"border-b": "border-bottom-width:1px",
|
||||
"border-b-2": "border-bottom-width:2px",
|
||||
"border-r": "border-right-width:1px",
|
||||
"border-l": "border-left-width:1px",
|
||||
"border-l-4": "border-left-width:4px",
|
||||
"border-dashed": "border-style:dashed",
|
||||
"border-none": "border-style:none",
|
||||
"border-transparent": "border-color:transparent",
|
||||
"border-white": "border-color:rgb(255 255 255)",
|
||||
"border-white/30": "border-color:rgba(255,255,255,.3)",
|
||||
"border-stone-100": "border-color:rgb(245 245 244)",
|
||||
"border-stone-200": "border-color:rgb(231 229 228)",
|
||||
"border-stone-300": "border-color:rgb(214 211 209)",
|
||||
"border-stone-700": "border-color:rgb(68 64 60)",
|
||||
"border-red-200": "border-color:rgb(254 202 202)",
|
||||
"border-red-300": "border-color:rgb(252 165 165)",
|
||||
"border-rose-200": "border-color:rgb(254 205 211)",
|
||||
"border-rose-300": "border-color:rgb(253 164 175)",
|
||||
"border-amber-200": "border-color:rgb(253 230 138)",
|
||||
"border-amber-300": "border-color:rgb(252 211 77)",
|
||||
"border-yellow-200": "border-color:rgb(254 240 138)",
|
||||
"border-green-300": "border-color:rgb(134 239 172)",
|
||||
"border-emerald-100": "border-color:rgb(209 250 229)",
|
||||
"border-emerald-200": "border-color:rgb(167 243 208)",
|
||||
"border-emerald-300": "border-color:rgb(110 231 183)",
|
||||
"border-emerald-600": "border-color:rgb(5 150 105)",
|
||||
"border-blue-200": "border-color:rgb(191 219 254)",
|
||||
"border-blue-300": "border-color:rgb(147 197 253)",
|
||||
"border-violet-200": "border-color:rgb(221 214 254)",
|
||||
"border-violet-300": "border-color:rgb(196 181 253)",
|
||||
"border-violet-400": "border-color:rgb(167 139 250)",
|
||||
"border-neutral-200": "border-color:rgb(229 229 229)",
|
||||
"border-red-400": "border-color:rgb(248 113 113)",
|
||||
"border-stone-400": "border-color:rgb(168 162 158)",
|
||||
"border-t-white": "border-top-color:rgb(255 255 255)",
|
||||
"border-t-stone-600": "border-top-color:rgb(87 83 78)",
|
||||
"border-l-stone-400": "border-left-color:rgb(168 162 158)",
|
||||
|
||||
# ── Border Radius ────────────────────────────────────────────────────
|
||||
"rounded": "border-radius:.25rem",
|
||||
"rounded-md": "border-radius:.375rem",
|
||||
"rounded-lg": "border-radius:.5rem",
|
||||
"rounded-xl": "border-radius:.75rem",
|
||||
"rounded-2xl": "border-radius:1rem",
|
||||
"rounded-full": "border-radius:9999px",
|
||||
"rounded-t": "border-top-left-radius:.25rem;border-top-right-radius:.25rem",
|
||||
"rounded-b": "border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem",
|
||||
"rounded-[4px]": "border-radius:4px",
|
||||
"rounded-[8px]": "border-radius:8px",
|
||||
|
||||
# ── Shadow ───────────────────────────────────────────────────────────
|
||||
"shadow-sm": "box-shadow:0 1px 2px 0 rgba(0,0,0,.05)",
|
||||
"shadow": "box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1)",
|
||||
"shadow-md": "box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1)",
|
||||
"shadow-lg": "box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1)",
|
||||
"shadow-xl": "box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1)",
|
||||
|
||||
# ── Opacity ──────────────────────────────────────────────────────────
|
||||
"opacity-0": "opacity:0",
|
||||
"opacity-40": "opacity:.4",
|
||||
"opacity-50": "opacity:.5",
|
||||
"opacity-90": "opacity:.9",
|
||||
"opacity-100": "opacity:1",
|
||||
|
||||
# ── Ring / Outline ───────────────────────────────────────────────────
|
||||
"outline-none": "outline:2px solid transparent;outline-offset:2px",
|
||||
"ring-2": "box-shadow:0 0 0 2px var(--tw-ring-color,rgb(59 130 246))",
|
||||
"ring-offset-2": "box-shadow:0 0 0 2px rgb(255 255 255),0 0 0 4px var(--tw-ring-color,rgb(59 130 246))",
|
||||
"ring-stone-300": "--tw-ring-color:rgb(214 211 209)",
|
||||
"ring-stone-500": "--tw-ring-color:rgb(120 113 108)",
|
||||
"ring-violet-500": "--tw-ring-color:rgb(139 92 246)",
|
||||
"ring-blue-500": "--tw-ring-color:rgb(59 130 246)",
|
||||
"ring-green-500": "--tw-ring-color:rgb(22 163 74)",
|
||||
"ring-purple-500": "--tw-ring-color:rgb(147 51 234)",
|
||||
|
||||
# ── Overflow ─────────────────────────────────────────────────────────
|
||||
"overflow-hidden": "overflow:hidden",
|
||||
"overflow-x-auto": "overflow-x:auto",
|
||||
"overflow-y-auto": "overflow-y:auto",
|
||||
"overflow-visible": "overflow:visible",
|
||||
"overflow-y-visible": "overflow-y:visible",
|
||||
"overscroll-contain": "overscroll-behavior:contain",
|
||||
|
||||
# ── Text Decoration ──────────────────────────────────────────────────
|
||||
"underline": "text-decoration-line:underline",
|
||||
"line-through": "text-decoration-line:line-through",
|
||||
"no-underline": "text-decoration-line:none",
|
||||
|
||||
# ── Text Overflow ────────────────────────────────────────────────────
|
||||
"truncate": "overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
|
||||
"line-clamp-2": "display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden",
|
||||
"line-clamp-3": "display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden",
|
||||
|
||||
# ── Whitespace / Word Break ──────────────────────────────────────────
|
||||
"whitespace-normal": "white-space:normal",
|
||||
"whitespace-nowrap": "white-space:nowrap",
|
||||
"whitespace-pre-line": "white-space:pre-line",
|
||||
"whitespace-pre-wrap": "white-space:pre-wrap",
|
||||
"break-words": "overflow-wrap:break-word",
|
||||
"break-all": "word-break:break-all",
|
||||
|
||||
# ── Transform ────────────────────────────────────────────────────────
|
||||
"rotate-180": "transform:rotate(180deg)",
|
||||
"-translate-x-1/2": "transform:translateX(-50%)",
|
||||
"-translate-y-1/2": "transform:translateY(-50%)",
|
||||
|
||||
# ── Transition ───────────────────────────────────────────────────────
|
||||
"transition": "transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"transition-all": "transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"transition-colors": "transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"transition-opacity": "transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"transition-transform": "transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
|
||||
"duration-75": "transition-duration:75ms",
|
||||
"duration-100": "transition-duration:100ms",
|
||||
"duration-150": "transition-duration:150ms",
|
||||
"duration-200": "transition-duration:200ms",
|
||||
"duration-300": "transition-duration:300ms",
|
||||
"duration-500": "transition-duration:500ms",
|
||||
"duration-700": "transition-duration:700ms",
|
||||
|
||||
# ── Animation ────────────────────────────────────────────────────────
|
||||
"animate-spin": "animation:spin 1s linear infinite",
|
||||
"animate-ping": "animation:ping 1s cubic-bezier(0,0,0.2,1) infinite",
|
||||
"animate-pulse": "animation:pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
|
||||
"animate-bounce": "animation:bounce 1s infinite",
|
||||
"animate-none": "animation:none",
|
||||
|
||||
# ── Aspect Ratio ─────────────────────────────────────────────────────
|
||||
"aspect-square": "aspect-ratio:1/1",
|
||||
"aspect-video": "aspect-ratio:16/9",
|
||||
|
||||
# ── Object Fit / Position ────────────────────────────────────────────
|
||||
"object-contain": "object-fit:contain",
|
||||
"object-cover": "object-fit:cover",
|
||||
"object-center": "object-position:center",
|
||||
"object-top": "object-position:top",
|
||||
|
||||
# ── Cursor ───────────────────────────────────────────────────────────
|
||||
"cursor-pointer": "cursor:pointer",
|
||||
"cursor-move": "cursor:move",
|
||||
|
||||
# ── User Select ──────────────────────────────────────────────────────
|
||||
"select-none": "user-select:none",
|
||||
"select-all": "user-select:all",
|
||||
|
||||
# ── Pointer Events ───────────────────────────────────────────────────
|
||||
"pointer-events-none": "pointer-events:none",
|
||||
|
||||
# ── Resize ───────────────────────────────────────────────────────────
|
||||
"resize": "resize:both",
|
||||
"resize-none": "resize:none",
|
||||
|
||||
# ── Scroll Snap ──────────────────────────────────────────────────────
|
||||
"snap-y": "scroll-snap-type:y mandatory",
|
||||
"snap-start": "scroll-snap-align:start",
|
||||
"snap-mandatory": "scroll-snap-type:y mandatory",
|
||||
|
||||
# ── List Style ───────────────────────────────────────────────────────
|
||||
"list-disc": "list-style-type:disc",
|
||||
"list-decimal": "list-style-type:decimal",
|
||||
"list-inside": "list-style-position:inside",
|
||||
|
||||
# ── Table ────────────────────────────────────────────────────────────
|
||||
"table-fixed": "table-layout:fixed",
|
||||
|
||||
# ── Backdrop ─────────────────────────────────────────────────────────
|
||||
"backdrop-blur": "backdrop-filter:blur(8px)",
|
||||
"backdrop-blur-sm": "backdrop-filter:blur(4px)",
|
||||
"backdrop-blur-md": "backdrop-filter:blur(12px)",
|
||||
|
||||
# ── Filter ───────────────────────────────────────────────────────────
|
||||
"saturate-0": "filter:saturate(0)",
|
||||
|
||||
# ── Space Between (child selector atoms) ─────────────────────────────
|
||||
# These generate `.atom > :not(:first-child)` rules
|
||||
"space-y-0": "margin-top:0",
|
||||
"space-y-0.5": "margin-top:.125rem",
|
||||
"space-y-1": "margin-top:.25rem",
|
||||
"space-y-2": "margin-top:.5rem",
|
||||
"space-y-3": "margin-top:.75rem",
|
||||
"space-y-4": "margin-top:1rem",
|
||||
"space-y-6": "margin-top:1.5rem",
|
||||
"space-y-8": "margin-top:2rem",
|
||||
"space-y-10": "margin-top:2.5rem",
|
||||
"space-x-1": "margin-left:.25rem",
|
||||
"space-x-2": "margin-left:.5rem",
|
||||
|
||||
# ── Divide (child selector atoms) ────────────────────────────────────
|
||||
# These generate `.atom > :not(:first-child)` rules
|
||||
"divide-y": "border-top-width:1px",
|
||||
"divide-stone-100": "border-color:rgb(245 245 244)",
|
||||
"divide-stone-200": "border-color:rgb(231 229 228)",
|
||||
|
||||
# ── Important modifiers ──────────────────────────────────────────────
|
||||
"!bg-stone-500": "background-color:rgb(120 113 108)!important",
|
||||
"!text-white": "color:rgb(255 255 255)!important",
|
||||
}
|
||||
|
||||
# Atoms that need a child selector: `.atom > :not(:first-child)` instead of `.atom`
|
||||
CHILD_SELECTOR_ATOMS: frozenset[str] = frozenset({
|
||||
k for k in STYLE_ATOMS
|
||||
if k.startswith(("space-x-", "space-y-", "divide-y", "divide-x"))
|
||||
and not k.startswith("divide-stone")
|
||||
})
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Pseudo-class / pseudo-element variants
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PSEUDO_VARIANTS: dict[str, str] = {
|
||||
"hover": ":hover",
|
||||
"focus": ":focus",
|
||||
"focus-within": ":focus-within",
|
||||
"focus-visible": ":focus-visible",
|
||||
"active": ":active",
|
||||
"disabled": ":disabled",
|
||||
"first": ":first-child",
|
||||
"last": ":last-child",
|
||||
"odd": ":nth-child(odd)",
|
||||
"even": ":nth-child(even)",
|
||||
"empty": ":empty",
|
||||
"open": "[open]",
|
||||
"placeholder": "::placeholder",
|
||||
"file": "::file-selector-button",
|
||||
"aria-selected": "[aria-selected=true]",
|
||||
"invalid": ":invalid",
|
||||
"placeholder-shown": ":placeholder-shown",
|
||||
"group-hover": ":is(.group:hover) &",
|
||||
"group-open": ":is(.group[open]) &",
|
||||
"group-open/cat": ":is(.group\\/cat[open]) &",
|
||||
"group-open/filter": ":is(.group\\/filter[open]) &",
|
||||
"group-open/root": ":is(.group\\/root[open]) &",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Responsive breakpoints
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
RESPONSIVE_BREAKPOINTS: dict[str, str] = {
|
||||
"sm": "(min-width:640px)",
|
||||
"md": "(min-width:768px)",
|
||||
"lg": "(min-width:1024px)",
|
||||
"xl": "(min-width:1280px)",
|
||||
"2xl": "(min-width:1536px)",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Keyframes — built-in animation definitions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
KEYFRAMES: dict[str, str] = {
|
||||
"spin": "@keyframes spin{to{transform:rotate(360deg)}}",
|
||||
"ping": "@keyframes ping{75%,100%{transform:scale(2);opacity:0}}",
|
||||
"pulse": "@keyframes pulse{50%{opacity:.5}}",
|
||||
"bounce": "@keyframes bounce{0%,100%{transform:translateY(-25%);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,0.2,1)}}",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Arbitrary value patterns — fallback when atom not in STYLE_ATOMS
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# Each tuple is (regex_pattern, css_template).
|
||||
# The regex captures value groups; the template uses {0}, {1}, etc.
|
||||
|
||||
ARBITRARY_PATTERNS: list[tuple[str, str]] = [
|
||||
# Width / Height
|
||||
(r"w-\[(.+)\]", "width:{0}"),
|
||||
(r"h-\[(.+)\]", "height:{0}"),
|
||||
(r"min-w-\[(.+)\]", "min-width:{0}"),
|
||||
(r"min-h-\[(.+)\]", "min-height:{0}"),
|
||||
(r"max-w-\[(.+)\]", "max-width:{0}"),
|
||||
(r"max-h-\[(.+)\]", "max-height:{0}"),
|
||||
# Spacing
|
||||
(r"p-\[(.+)\]", "padding:{0}"),
|
||||
(r"px-\[(.+)\]", "padding-left:{0};padding-right:{0}"),
|
||||
(r"py-\[(.+)\]", "padding-top:{0};padding-bottom:{0}"),
|
||||
(r"pt-\[(.+)\]", "padding-top:{0}"),
|
||||
(r"pb-\[(.+)\]", "padding-bottom:{0}"),
|
||||
(r"pl-\[(.+)\]", "padding-left:{0}"),
|
||||
(r"pr-\[(.+)\]", "padding-right:{0}"),
|
||||
(r"m-\[(.+)\]", "margin:{0}"),
|
||||
(r"mx-\[(.+)\]", "margin-left:{0};margin-right:{0}"),
|
||||
(r"my-\[(.+)\]", "margin-top:{0};margin-bottom:{0}"),
|
||||
(r"mt-\[(.+)\]", "margin-top:{0}"),
|
||||
(r"mb-\[(.+)\]", "margin-bottom:{0}"),
|
||||
(r"ml-\[(.+)\]", "margin-left:{0}"),
|
||||
(r"mr-\[(.+)\]", "margin-right:{0}"),
|
||||
# Gap
|
||||
(r"gap-\[(.+)\]", "gap:{0}"),
|
||||
(r"gap-x-\[(.+)\]", "column-gap:{0}"),
|
||||
(r"gap-y-\[(.+)\]", "row-gap:{0}"),
|
||||
# Position
|
||||
(r"top-\[(.+)\]", "top:{0}"),
|
||||
(r"right-\[(.+)\]", "right:{0}"),
|
||||
(r"bottom-\[(.+)\]", "bottom:{0}"),
|
||||
(r"left-\[(.+)\]", "left:{0}"),
|
||||
# Border radius
|
||||
(r"rounded-\[(.+)\]", "border-radius:{0}"),
|
||||
# Background / Text color
|
||||
(r"bg-\[(.+)\]", "background-color:{0}"),
|
||||
(r"text-\[(.+)\]", "font-size:{0}"),
|
||||
# Grid
|
||||
(r"grid-cols-\[(.+)\]", "grid-template-columns:{0}"),
|
||||
(r"col-span-(\d+)", "grid-column:span {0}/span {0}"),
|
||||
]
|
||||
@@ -1,254 +0,0 @@
|
||||
"""
|
||||
Style resolver — ``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue.
|
||||
|
||||
Resolves a tuple of atom strings into a ``StyleValue`` with:
|
||||
- A content-addressed class name (``sx-{hash[:6]}``)
|
||||
- Base CSS declarations
|
||||
- Pseudo-class rules (hover, focus, etc.)
|
||||
- Media-query rules (responsive breakpoints)
|
||||
- Referenced @keyframes definitions
|
||||
|
||||
Resolution order per atom:
|
||||
1. Dictionary lookup in ``STYLE_ATOMS``
|
||||
2. Arbitrary value pattern match (``w-[347px]`` → ``width:347px``)
|
||||
3. Ignored (unknown atoms are silently skipped)
|
||||
|
||||
Results are memoized by input tuple for zero-cost repeat calls.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import Sequence
|
||||
|
||||
from .style_dict import (
|
||||
ARBITRARY_PATTERNS,
|
||||
CHILD_SELECTOR_ATOMS,
|
||||
KEYFRAMES,
|
||||
PSEUDO_VARIANTS,
|
||||
RESPONSIVE_BREAKPOINTS,
|
||||
STYLE_ATOMS,
|
||||
)
|
||||
from .types import StyleValue
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compiled arbitrary-value patterns
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_COMPILED_PATTERNS: list[tuple[re.Pattern, str]] = [
|
||||
(re.compile(f"^{pat}$"), tmpl)
|
||||
for pat, tmpl in ARBITRARY_PATTERNS
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def resolve_style(atoms: tuple[str, ...]) -> StyleValue:
|
||||
"""Resolve a tuple of keyword atoms into a StyleValue.
|
||||
|
||||
Each atom is a Tailwind-compatible keyword (``flex``, ``gap-4``,
|
||||
``hover:bg-sky-200``, ``sm:flex-row``, etc.). Both keywords
|
||||
(without leading colon) and runtime strings are accepted.
|
||||
"""
|
||||
return _resolve_cached(atoms)
|
||||
|
||||
|
||||
def merge_styles(styles: Sequence[StyleValue]) -> StyleValue:
|
||||
"""Merge multiple StyleValues into one.
|
||||
|
||||
Later declarations win for the same CSS property. Class name is
|
||||
recomputed from the merged declarations.
|
||||
"""
|
||||
if len(styles) == 1:
|
||||
return styles[0]
|
||||
|
||||
all_decls: list[str] = []
|
||||
all_media: list[tuple[str, str]] = []
|
||||
all_pseudo: list[tuple[str, str]] = []
|
||||
all_kf: list[tuple[str, str]] = []
|
||||
|
||||
for sv in styles:
|
||||
if sv.declarations:
|
||||
all_decls.append(sv.declarations)
|
||||
all_media.extend(sv.media_rules)
|
||||
all_pseudo.extend(sv.pseudo_rules)
|
||||
all_kf.extend(sv.keyframes)
|
||||
|
||||
merged_decls = ";".join(all_decls)
|
||||
return _build_style_value(
|
||||
merged_decls,
|
||||
tuple(all_media),
|
||||
tuple(all_pseudo),
|
||||
tuple(dict(all_kf).items()), # dedupe keyframes by name
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def _resolve_cached(atoms: tuple[str, ...]) -> StyleValue:
|
||||
"""Memoized resolver."""
|
||||
base_decls: list[str] = []
|
||||
media_rules: list[tuple[str, str]] = [] # (query, decls)
|
||||
pseudo_rules: list[tuple[str, str]] = [] # (selector_suffix, decls)
|
||||
keyframes_needed: list[tuple[str, str]] = []
|
||||
|
||||
for atom in atoms:
|
||||
if not atom:
|
||||
continue
|
||||
# Strip leading colon if keyword form (":flex" → "flex")
|
||||
a = atom.lstrip(":")
|
||||
|
||||
# Split variant prefix(es): "hover:bg-sky-200" → ["hover", "bg-sky-200"]
|
||||
# "sm:hover:bg-sky-200" → ["sm", "hover", "bg-sky-200"]
|
||||
variant, base = _split_variant(a)
|
||||
|
||||
# Resolve the base atom to CSS declarations
|
||||
decls = _resolve_atom(base)
|
||||
if not decls:
|
||||
continue
|
||||
|
||||
# Check if this atom references a keyframe
|
||||
_check_keyframes(base, keyframes_needed)
|
||||
|
||||
# Route to the appropriate bucket
|
||||
if variant is None:
|
||||
base_decls.append(decls)
|
||||
elif variant in RESPONSIVE_BREAKPOINTS:
|
||||
query = RESPONSIVE_BREAKPOINTS[variant]
|
||||
media_rules.append((query, decls))
|
||||
elif variant in PSEUDO_VARIANTS:
|
||||
pseudo_sel = PSEUDO_VARIANTS[variant]
|
||||
pseudo_rules.append((pseudo_sel, decls))
|
||||
else:
|
||||
# Compound variant: "sm:hover:..." → media + pseudo
|
||||
parts = variant.split(":")
|
||||
media_part = None
|
||||
pseudo_part = None
|
||||
for p in parts:
|
||||
if p in RESPONSIVE_BREAKPOINTS:
|
||||
media_part = RESPONSIVE_BREAKPOINTS[p]
|
||||
elif p in PSEUDO_VARIANTS:
|
||||
pseudo_part = PSEUDO_VARIANTS[p]
|
||||
if media_part and pseudo_part:
|
||||
# Both media and pseudo — store as pseudo within media
|
||||
# For now, put in pseudo_rules with media annotation
|
||||
pseudo_rules.append((pseudo_part, decls))
|
||||
media_rules.append((media_part, decls))
|
||||
elif media_part:
|
||||
media_rules.append((media_part, decls))
|
||||
elif pseudo_part:
|
||||
pseudo_rules.append((pseudo_part, decls))
|
||||
else:
|
||||
# Unknown variant — treat as base
|
||||
base_decls.append(decls)
|
||||
|
||||
return _build_style_value(
|
||||
";".join(base_decls),
|
||||
tuple(media_rules),
|
||||
tuple(pseudo_rules),
|
||||
tuple(keyframes_needed),
|
||||
)
|
||||
|
||||
|
||||
def _split_variant(atom: str) -> tuple[str | None, str]:
|
||||
"""Split a potentially variant-prefixed atom.
|
||||
|
||||
Returns (variant, base) where variant is None for non-prefixed atoms.
|
||||
Examples:
|
||||
"flex" → (None, "flex")
|
||||
"hover:bg-sky-200" → ("hover", "bg-sky-200")
|
||||
"sm:flex-row" → ("sm", "flex-row")
|
||||
"sm:hover:bg-sky-200" → ("sm:hover", "bg-sky-200")
|
||||
"""
|
||||
# Check for responsive prefix first (always outermost)
|
||||
for bp in RESPONSIVE_BREAKPOINTS:
|
||||
prefix = bp + ":"
|
||||
if atom.startswith(prefix):
|
||||
rest = atom[len(prefix):]
|
||||
# Check for nested pseudo variant
|
||||
for pv in PSEUDO_VARIANTS:
|
||||
inner_prefix = pv + ":"
|
||||
if rest.startswith(inner_prefix):
|
||||
return (bp + ":" + pv, rest[len(inner_prefix):])
|
||||
return (bp, rest)
|
||||
|
||||
# Check for pseudo variant
|
||||
for pv in PSEUDO_VARIANTS:
|
||||
prefix = pv + ":"
|
||||
if atom.startswith(prefix):
|
||||
return (pv, atom[len(prefix):])
|
||||
|
||||
return (None, atom)
|
||||
|
||||
|
||||
def _resolve_atom(atom: str) -> str | None:
|
||||
"""Look up CSS declarations for a single base atom.
|
||||
|
||||
Returns None if the atom is unknown.
|
||||
"""
|
||||
# 1. Dictionary lookup
|
||||
decls = STYLE_ATOMS.get(atom)
|
||||
if decls is not None:
|
||||
return decls
|
||||
|
||||
# 2. Dynamic keyframes: animate-{name} → animation-name:{name}
|
||||
if atom.startswith("animate-"):
|
||||
name = atom[len("animate-"):]
|
||||
if name in KEYFRAMES:
|
||||
return f"animation-name:{name}"
|
||||
|
||||
# 3. Arbitrary value pattern match
|
||||
for pattern, template in _COMPILED_PATTERNS:
|
||||
m = pattern.match(atom)
|
||||
if m:
|
||||
groups = m.groups()
|
||||
result = template
|
||||
for i, g in enumerate(groups):
|
||||
result = result.replace(f"{{{i}}}", g)
|
||||
return result
|
||||
|
||||
# 4. Unknown atom — silently skip
|
||||
return None
|
||||
|
||||
|
||||
def _check_keyframes(atom: str, kf_list: list[tuple[str, str]]) -> None:
|
||||
"""If the atom references a built-in animation, add its @keyframes."""
|
||||
if atom.startswith("animate-"):
|
||||
name = atom[len("animate-"):]
|
||||
if name in KEYFRAMES:
|
||||
kf_list.append((name, KEYFRAMES[name]))
|
||||
|
||||
|
||||
def _build_style_value(
|
||||
declarations: str,
|
||||
media_rules: tuple,
|
||||
pseudo_rules: tuple,
|
||||
keyframes: tuple,
|
||||
) -> StyleValue:
|
||||
"""Build a StyleValue with a content-addressed class name."""
|
||||
# Build hash from all rules for deterministic class name
|
||||
hash_input = declarations
|
||||
for query, decls in media_rules:
|
||||
hash_input += f"@{query}{{{decls}}}"
|
||||
for sel, decls in pseudo_rules:
|
||||
hash_input += f"{sel}{{{decls}}}"
|
||||
for name, rule in keyframes:
|
||||
hash_input += rule
|
||||
|
||||
h = hashlib.sha256(hash_input.encode()).hexdigest()[:6]
|
||||
class_name = f"sx-{h}"
|
||||
|
||||
return StyleValue(
|
||||
class_name=class_name,
|
||||
declarations=declarations,
|
||||
media_rules=media_rules,
|
||||
pseudo_rules=pseudo_rules,
|
||||
keyframes=keyframes,
|
||||
)
|
||||
@@ -184,6 +184,8 @@ env = {
|
||||
"dict-get": "_deferred",
|
||||
"append!": "_deferred",
|
||||
"inc": lambda n: n + 1,
|
||||
# Component accessor for affinity (Phase 7)
|
||||
"component-affinity": lambda c: getattr(c, 'affinity', 'auto'),
|
||||
}
|
||||
|
||||
|
||||
@@ -286,6 +288,8 @@ def _load_deps_from_bootstrap(env):
|
||||
transitive_io_refs,
|
||||
compute_all_io_refs,
|
||||
component_pure_p,
|
||||
render_target,
|
||||
page_render_plan,
|
||||
)
|
||||
env["scan-refs"] = scan_refs
|
||||
env["scan-components-from-source"] = scan_components_from_source
|
||||
@@ -298,6 +302,8 @@ def _load_deps_from_bootstrap(env):
|
||||
env["transitive-io-refs"] = transitive_io_refs
|
||||
env["compute-all-io-refs"] = compute_all_io_refs
|
||||
env["component-pure?"] = component_pure_p
|
||||
env["render-target"] = render_target
|
||||
env["page-render-plan"] = page_render_plan
|
||||
env["test-env"] = lambda: env
|
||||
except ImportError:
|
||||
eval_file("deps.sx", env)
|
||||
|
||||
313
shared/sx/tests/test_isomorphic.py
Normal file
313
shared/sx/tests/test_isomorphic.py
Normal file
@@ -0,0 +1,313 @@
|
||||
"""Isomorphic tests — same SX expressions evaluated on Python and JS.
|
||||
|
||||
Phase 7e: verifies that the bootstrapped evaluator produces identical
|
||||
output across both hosts. Runs each test case through:
|
||||
1. Python sx_ref.py (bootstrapped from spec)
|
||||
2. Node.js sx-browser.js (bootstrapped from spec)
|
||||
|
||||
Compares rendered HTML output to catch any host-specific divergence.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from shared.sx.parser import parse
|
||||
from shared.sx.ref import sx_ref
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_SX_BROWSER = os.path.abspath(os.path.join(_HERE, "..", "..", "static", "scripts", "sx-browser.js"))
|
||||
|
||||
def _node_harness(sx_path: str) -> str:
|
||||
return f"""
|
||||
const Sx = require({json.dumps(sx_path)});
|
||||
let input = "";
|
||||
process.stdin.on("data", d => input += d);
|
||||
process.stdin.on("end", () => {{
|
||||
const cases = JSON.parse(input);
|
||||
const results = [];
|
||||
for (const c of cases) {{
|
||||
try {{
|
||||
const env = {{}};
|
||||
if (c.defs) {{
|
||||
for (const d of c.defs) {{
|
||||
Sx.eval(Sx.parse(d)[0], env);
|
||||
}}
|
||||
}}
|
||||
const parsed = Sx.parse(c.expr)[0];
|
||||
let result;
|
||||
if (c.mode === "render") {{
|
||||
result = Sx.renderToHtml(parsed, env);
|
||||
}} else {{
|
||||
const val = Sx.eval(parsed, env);
|
||||
result = typeof val === "object" && val !== null && val._nil ? "nil" : String(val);
|
||||
}}
|
||||
results.push({{ ok: true, result: result }});
|
||||
}} catch (e) {{
|
||||
results.push({{ ok: false, error: e.message || String(e) }});
|
||||
}}
|
||||
}}
|
||||
process.stdout.write(JSON.stringify(results));
|
||||
}});
|
||||
"""
|
||||
|
||||
|
||||
def _run_js(cases: list[dict]) -> list[dict]:
|
||||
"""Run test cases through Node.js sx-browser.js."""
|
||||
harness = _node_harness(_SX_BROWSER)
|
||||
result = subprocess.run(
|
||||
["node", "-e", harness],
|
||||
input=json.dumps(cases),
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Node.js failed: {result.stderr}")
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def _normalize_val(val: str) -> str:
|
||||
"""Normalize value representations across hosts."""
|
||||
# Python True/False → lowercase
|
||||
if val == "True":
|
||||
return "true"
|
||||
if val == "False":
|
||||
return "false"
|
||||
# Python float 5.0 → 5 (match JS integer division)
|
||||
try:
|
||||
f = float(val)
|
||||
if f == int(f):
|
||||
return str(int(f))
|
||||
except (ValueError, OverflowError):
|
||||
pass
|
||||
# Python list repr [1, 2, 3] → 1,2,3 (match JS Array.toString)
|
||||
if val.startswith("[") and val.endswith("]"):
|
||||
inner = val[1:-1]
|
||||
return inner.replace(", ", ",")
|
||||
return val
|
||||
|
||||
|
||||
def _run_py(cases: list[dict]) -> list[dict]:
|
||||
"""Run test cases through Python sx_ref.py."""
|
||||
results = []
|
||||
for c in cases:
|
||||
try:
|
||||
env = {}
|
||||
if c.get("defs"):
|
||||
for d in c["defs"]:
|
||||
sx_ref.evaluate(parse(d), env)
|
||||
parsed = parse(c["expr"])
|
||||
if c.get("mode") == "render":
|
||||
result = sx_ref.render(parsed, env)
|
||||
else:
|
||||
val = sx_ref.evaluate(parsed, env)
|
||||
from shared.sx.types import NIL as SX_NIL
|
||||
result = "nil" if val is SX_NIL or val is None else _normalize_val(str(val))
|
||||
results.append({"ok": True, "result": result})
|
||||
except Exception as e:
|
||||
results.append({"ok": False, "error": str(e)})
|
||||
return results
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test cases
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
EVAL_CASES = [
|
||||
# Arithmetic
|
||||
{"expr": "(+ 1 2)", "expected": "3"},
|
||||
{"expr": "(* 3 4)", "expected": "12"},
|
||||
{"expr": "(- 10 3)", "expected": "7"},
|
||||
{"expr": "(/ 10 2)", "expected": "5"},
|
||||
|
||||
# Comparison
|
||||
{"expr": "(> 3 2)", "expected": "true"},
|
||||
{"expr": "(< 1 2)", "expected": "true"},
|
||||
{"expr": "(= 5 5)", "expected": "true"},
|
||||
{"expr": '(= "a" "a")', "expected": "true"},
|
||||
|
||||
# String ops
|
||||
{"expr": '(str "hello" " " "world")', "expected": "hello world"},
|
||||
{"expr": '(upper "hello")', "expected": "HELLO"},
|
||||
{"expr": '(lower "WORLD")', "expected": "world"},
|
||||
{"expr": '(join "-" (list "a" "b" "c"))', "expected": "a-b-c"},
|
||||
{"expr": '(len "hello")', "expected": "5"},
|
||||
{"expr": '(trim " hi ")', "expected": "hi"},
|
||||
|
||||
# Collections
|
||||
{"expr": "(len (list 1 2 3))", "expected": "3"},
|
||||
{"expr": "(first (list 10 20 30))", "expected": "10"},
|
||||
{"expr": "(last (list 10 20 30))", "expected": "30"},
|
||||
{"expr": "(nth (list 10 20 30) 1)", "expected": "20"},
|
||||
{"expr": "(contains? (list 1 2 3) 2)", "expected": "true"},
|
||||
{"expr": "(empty? (list))", "expected": "true"},
|
||||
|
||||
# Logic
|
||||
{"expr": "(if true 1 2)", "expected": "1"},
|
||||
{"expr": "(if false 1 2)", "expected": "2"},
|
||||
{"expr": "(and true true)", "expected": "true"},
|
||||
{"expr": "(or false true)", "expected": "true"},
|
||||
{"expr": "(not false)", "expected": "true"},
|
||||
|
||||
# Let / lambda
|
||||
{"expr": "(let ((x 10) (y 20)) (+ x y))", "expected": "30"},
|
||||
{"expr": "((fn (x) (* x x)) 5)", "expected": "25"},
|
||||
|
||||
# Higher-order
|
||||
{"expr": "(map (fn (x) (* x x)) (list 1 2 3))", "expected": "1,4,9"},
|
||||
{"expr": "(filter (fn (x) (> x 2)) (list 1 2 3 4))", "expected": "3,4"},
|
||||
{"expr": "(reduce (fn (a x) (+ a x)) 0 (list 1 2 3))", "expected": "6"},
|
||||
{"expr": "(some (fn (x) (> x 3)) (list 1 2 5))", "expected": "true"},
|
||||
{"expr": "(every? (fn (x) (> x 0)) (list 1 2 3))", "expected": "true"},
|
||||
|
||||
# Dict
|
||||
{"expr": '(get {:a 1 :b 2} :a)', "expected": "1"},
|
||||
{"expr": '(len {:x 10 :y 20})', "expected": "2"},
|
||||
|
||||
# Keywords
|
||||
{"expr": ":hello", "expected": "hello"},
|
||||
|
||||
# Cond
|
||||
{"expr": '(cond (= 1 2) "a" (= 1 1) "b" :else "c")', "expected": "b"},
|
||||
|
||||
# Case
|
||||
{"expr": '(let ((x 2)) (case x 1 "one" 2 "two" "other"))', "expected": "two"},
|
||||
]
|
||||
|
||||
RENDER_CASES = [
|
||||
# Basic elements
|
||||
{"expr": '(div "hello")', "mode": "render", "expected": "<div>hello</div>"},
|
||||
{"expr": '(span "text")', "mode": "render", "expected": "<span>text</span>"},
|
||||
{"expr": '(p "paragraph")', "mode": "render", "expected": "<p>paragraph</p>"},
|
||||
|
||||
# Attributes
|
||||
{"expr": '(div :class "box" "content")', "mode": "render",
|
||||
"expected": '<div class="box">content</div>'},
|
||||
{"expr": '(a :href "/link" "click")', "mode": "render",
|
||||
"expected": '<a href="/link">click</a>'},
|
||||
{"expr": '(input :type "text" :name "q")', "mode": "render",
|
||||
"expected": '<input type="text" name="q" />'},
|
||||
|
||||
# Nested elements
|
||||
{"expr": '(div (span "a") (span "b"))', "mode": "render",
|
||||
"expected": "<div><span>a</span><span>b</span></div>"},
|
||||
{"expr": '(ul (li "one") (li "two"))', "mode": "render",
|
||||
"expected": "<ul><li>one</li><li>two</li></ul>"},
|
||||
|
||||
# Void elements (self-closing per spec)
|
||||
{"expr": '(br)', "mode": "render", "expected": "<br />"},
|
||||
{"expr": '(hr)', "mode": "render", "expected": "<hr />"},
|
||||
{"expr": '(img :src "a.png")', "mode": "render", "expected": '<img src="a.png" />'},
|
||||
|
||||
# Boolean attributes
|
||||
{"expr": '(input :disabled true)', "mode": "render", "expected": "<input disabled />"},
|
||||
{"expr": '(input :disabled false)', "mode": "render", "expected": "<input />"},
|
||||
|
||||
# Conditional rendering
|
||||
{"expr": '(div (if true (span "yes") (span "no")))', "mode": "render",
|
||||
"expected": "<div><span>yes</span></div>"},
|
||||
{"expr": '(div (when true (span "shown")))', "mode": "render",
|
||||
"expected": "<div><span>shown</span></div>"},
|
||||
{"expr": '(div (when false (span "hidden")))', "mode": "render",
|
||||
"expected": "<div></div>"},
|
||||
|
||||
# Map in render
|
||||
{"expr": '(ul (map (fn (x) (li x)) (list "a" "b")))', "mode": "render",
|
||||
"expected": "<ul><li>a</li><li>b</li></ul>"},
|
||||
|
||||
# Component rendering
|
||||
{"defs": ['(defcomp ~box (&key title) (div :class "box" title))'],
|
||||
"expr": '(~box :title "hi")', "mode": "render",
|
||||
"expected": '<div class="box">hi</div>'},
|
||||
|
||||
{"defs": ['(defcomp ~wrap (&rest children) (section children))'],
|
||||
"expr": '(~wrap (p "a") (p "b"))', "mode": "render",
|
||||
"expected": "<section><p>a</p><p>b</p></section>"},
|
||||
|
||||
{"defs": [
|
||||
'(defcomp ~inner (&key x) (em x))',
|
||||
'(defcomp ~outer (&key label) (div (~inner :x label)))'],
|
||||
"expr": '(~outer :label "nested")', "mode": "render",
|
||||
"expected": "<div><em>nested</em></div>"},
|
||||
|
||||
# Component with affinity (should render identically regardless)
|
||||
{"defs": ['(defcomp ~client-comp (&key x) :affinity :client (strong x))'],
|
||||
"expr": '(~client-comp :x "bold")', "mode": "render",
|
||||
"expected": "<strong>bold</strong>"},
|
||||
|
||||
{"defs": ['(defcomp ~server-comp (&key x) :affinity :server (em x))'],
|
||||
"expr": '(~server-comp :x "italic")', "mode": "render",
|
||||
"expected": "<em>italic</em>"},
|
||||
|
||||
# HTML escaping
|
||||
{"expr": '(div "<script>alert(1)</script>")', "mode": "render",
|
||||
"expected": "<div><script>alert(1)</script></div>"},
|
||||
{"expr": '(div :title "a&b" "text")', "mode": "render",
|
||||
"expected": '<div title="a&b">text</div>'},
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestIsomorphicEval:
|
||||
"""Eval cases: same expression → same string result on both hosts."""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def js_results(self):
|
||||
return _run_js(EVAL_CASES)
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def py_results(self):
|
||||
return _run_py(EVAL_CASES)
|
||||
|
||||
@pytest.mark.parametrize("idx", range(len(EVAL_CASES)))
|
||||
def test_eval(self, idx, js_results, py_results):
|
||||
case = EVAL_CASES[idx]
|
||||
py = py_results[idx]
|
||||
js = js_results[idx]
|
||||
assert py["ok"], f"Python failed: {py.get('error')}"
|
||||
assert js["ok"], f"JS failed: {js.get('error')}"
|
||||
assert py["result"] == js["result"], (
|
||||
f"Divergence in {case['expr']!r}: "
|
||||
f"Python={py['result']!r}, JS={js['result']!r}"
|
||||
)
|
||||
if "expected" in case:
|
||||
assert py["result"] == case["expected"], (
|
||||
f"Wrong result for {case['expr']!r}: got {py['result']!r}"
|
||||
)
|
||||
|
||||
|
||||
class TestIsomorphicRender:
|
||||
"""Render cases: same expression → same HTML on both hosts."""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def js_results(self):
|
||||
return _run_js(RENDER_CASES)
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def py_results(self):
|
||||
return _run_py(RENDER_CASES)
|
||||
|
||||
@pytest.mark.parametrize("idx", range(len(RENDER_CASES)))
|
||||
def test_render(self, idx, js_results, py_results):
|
||||
case = RENDER_CASES[idx]
|
||||
py = py_results[idx]
|
||||
js = js_results[idx]
|
||||
assert py["ok"], f"Python failed: {py.get('error')}"
|
||||
assert js["ok"], f"JS failed: {js.get('error')}"
|
||||
assert py["result"] == js["result"], (
|
||||
f"Divergence in {case['expr']!r}: "
|
||||
f"Python={py['result']!r}, JS={js['result']!r}"
|
||||
)
|
||||
if "expected" in case:
|
||||
assert py["result"] == case["expected"], (
|
||||
f"Wrong result for {case['expr']!r}: "
|
||||
f"expected {case['expected']!r}, got {py['result']!r}"
|
||||
)
|
||||
@@ -169,12 +169,22 @@ class Component:
|
||||
css_classes: set[str] = field(default_factory=set) # pre-scanned :class values
|
||||
deps: set[str] = field(default_factory=set) # transitive component deps (~names)
|
||||
io_refs: set[str] = field(default_factory=set) # transitive IO primitive refs
|
||||
affinity: str = "auto" # "auto" | "client" | "server"
|
||||
|
||||
@property
|
||||
def is_pure(self) -> bool:
|
||||
"""True if this component has no transitive IO dependencies."""
|
||||
return not self.io_refs
|
||||
|
||||
@property
|
||||
def render_target(self) -> str:
|
||||
"""Where this component should render: 'server' or 'client'."""
|
||||
if self.affinity == "server":
|
||||
return "server"
|
||||
if self.affinity == "client":
|
||||
return "client"
|
||||
return "server" if self.io_refs else "client"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Component ~{self.name}({', '.join(self.params)})>"
|
||||
|
||||
@@ -245,6 +255,7 @@ class PageDef:
|
||||
fallback_expr: Any = None # fallback content while streaming
|
||||
shell_expr: Any = None # immediate shell content (wraps suspense)
|
||||
closure: dict[str, Any] = field(default_factory=dict)
|
||||
render_plan: dict[str, Any] | None = field(default=None, repr=False)
|
||||
|
||||
_FIELD_MAP = {
|
||||
"name": "name", "path": "path", "auth": "auth",
|
||||
@@ -303,30 +314,6 @@ class ActionDef:
|
||||
return f"<action:{self.name}({', '.join(self.params)})>"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StyleValue
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StyleValue:
|
||||
"""A resolved CSS style produced by ``(css :flex :gap-4 :hover:bg-sky-200)``.
|
||||
|
||||
Generated by the style resolver. The renderer emits ``class_name`` as a
|
||||
CSS class and registers the CSS rule for on-demand delivery.
|
||||
"""
|
||||
class_name: str # "sx-a3f2c1"
|
||||
declarations: str # "display:flex;gap:1rem"
|
||||
media_rules: tuple = () # ((query, decls), ...)
|
||||
pseudo_rules: tuple = () # ((selector, decls), ...)
|
||||
keyframes: tuple = () # (("spin", "@keyframes spin{...}"), ...)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<StyleValue {self.class_name}>"
|
||||
|
||||
def __str__(self):
|
||||
return self.class_name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Continuation
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -368,4 +355,4 @@ class _ShiftSignal(BaseException):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# An s-expression value after evaluation
|
||||
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | StyleValue | list | dict | _Nil | None
|
||||
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | list | dict | _Nil | None
|
||||
|
||||
206
sx/sx/affinity-demo.sx
Normal file
206
sx/sx/affinity-demo.sx
Normal file
@@ -0,0 +1,206 @@
|
||||
;; Affinity demo — Phase 7a render boundary annotations.
|
||||
;;
|
||||
;; Demonstrates :affinity annotations on defcomp and how they influence
|
||||
;; the server/client render boundary decision. Components declare where
|
||||
;; they prefer to render; the runtime combines this with IO analysis.
|
||||
|
||||
;; --- Demo components with different affinities ---
|
||||
|
||||
(defcomp ~aff-demo-auto (&key label)
|
||||
(div :class "rounded border border-stone-200 bg-white p-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
(span :class "inline-block w-2 h-2 rounded-full bg-stone-400")
|
||||
(span :class "text-sm font-mono text-stone-500" ":affinity :auto"))
|
||||
(p :class "text-stone-800" (or label "Pure component — no IO calls. Auto-detected as client-renderable."))))
|
||||
|
||||
(defcomp ~aff-demo-client (&key label)
|
||||
:affinity :client
|
||||
(div :class "rounded border border-blue-200 bg-blue-50 p-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
(span :class "inline-block w-2 h-2 rounded-full bg-blue-400")
|
||||
(span :class "text-sm font-mono text-blue-600" ":affinity :client"))
|
||||
(p :class "text-blue-800" (or label "Explicitly client-rendered — even IO calls would be proxied."))))
|
||||
|
||||
(defcomp ~aff-demo-server (&key label)
|
||||
:affinity :server
|
||||
(div :class "rounded border border-amber-200 bg-amber-50 p-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
(span :class "inline-block w-2 h-2 rounded-full bg-amber-400")
|
||||
(span :class "text-sm font-mono text-amber-600" ":affinity :server"))
|
||||
(p :class "text-amber-800" (or label "Always server-rendered — auth-sensitive or secret-dependent."))))
|
||||
|
||||
(defcomp ~aff-demo-io-auto ()
|
||||
(div :class "rounded border border-red-200 bg-red-50 p-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
(span :class "inline-block w-2 h-2 rounded-full bg-red-400")
|
||||
(span :class "text-sm font-mono text-red-600" ":affinity :auto + IO"))
|
||||
(p :class "text-red-800 mb-3" "Auto affinity with IO dependency — auto-detected as server-rendered.")
|
||||
(~doc-code :code (highlight "(render-target name env io-names)" "lisp"))))
|
||||
|
||||
(defcomp ~aff-demo-io-client ()
|
||||
:affinity :client
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
(span :class "inline-block w-2 h-2 rounded-full bg-violet-400")
|
||||
(span :class "text-sm font-mono text-violet-600" ":affinity :client + IO"))
|
||||
(p :class "text-violet-800 mb-3" "Client affinity overrides IO — calls proxied to server via /sx/io/.")
|
||||
(~doc-code :code (highlight "(component-affinity comp)" "lisp"))))
|
||||
|
||||
|
||||
;; --- Main page component ---
|
||||
|
||||
(defcomp ~affinity-demo-content (&key components page-plans)
|
||||
(div :class "space-y-8"
|
||||
(div :class "border-b border-stone-200 pb-6"
|
||||
(h1 :class "text-2xl font-bold text-stone-900" "Affinity Annotations")
|
||||
(p :class "mt-2 text-stone-600"
|
||||
"Phase 7a: components declare where they prefer to render. The "
|
||||
(code :class "bg-stone-100 px-1 rounded text-violet-700" "render-target")
|
||||
" function in deps.sx combines the annotation with IO analysis to produce a per-component boundary decision."))
|
||||
|
||||
;; Syntax
|
||||
(~doc-section :title "Syntax" :id "syntax"
|
||||
(p "Add " (code ":affinity") " between the params list and the body:")
|
||||
(~doc-code :code (highlight "(defcomp ~my-component (&key title)\n :affinity :client ;; or :server, or omit for :auto\n (div title))" "lisp"))
|
||||
(p "Three values:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (code ":auto") " (default) — runtime decides from IO dependency analysis")
|
||||
(li (code ":client") " — always render client-side; IO calls proxied to server")
|
||||
(li (code ":server") " — always render server-side; never sent to client as SX")))
|
||||
|
||||
;; Live components
|
||||
(~doc-section :title "Live Components" :id "live"
|
||||
(p "These components are defined with different affinities. The server analyzed them at registration time:")
|
||||
|
||||
(div :class "space-y-4 mt-4"
|
||||
(~aff-demo-auto)
|
||||
(~aff-demo-client)
|
||||
(~aff-demo-server)
|
||||
(~aff-demo-io-auto)
|
||||
(~aff-demo-io-client)))
|
||||
|
||||
;; Analysis table from server
|
||||
(~doc-section :title "Server Analysis" :id "analysis"
|
||||
(p "The server computed these render targets at component registration time:")
|
||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Component")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Affinity")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "IO Deps")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Render Target")))
|
||||
(tbody
|
||||
(map (fn (c)
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" (get c "name"))
|
||||
(td :class "px-3 py-2"
|
||||
(span :class (str "inline-block px-2 py-0.5 rounded text-xs font-bold uppercase "
|
||||
(case (get c "affinity")
|
||||
"client" "bg-blue-100 text-blue-700"
|
||||
"server" "bg-amber-100 text-amber-700"
|
||||
"bg-stone-100 text-stone-600"))
|
||||
(get c "affinity")))
|
||||
(td :class "px-3 py-2 text-stone-600"
|
||||
(if (> (len (get c "io-refs")) 0)
|
||||
(span :class "text-red-600 font-medium"
|
||||
(join ", " (get c "io-refs")))
|
||||
(span :class "text-green-600" "none")))
|
||||
(td :class "px-3 py-2"
|
||||
(span :class (str "inline-block px-2 py-0.5 rounded text-xs font-bold uppercase "
|
||||
(if (= (get c "render-target") "client")
|
||||
"bg-green-100 text-green-700"
|
||||
"bg-orange-100 text-orange-700"))
|
||||
(get c "render-target")))))
|
||||
components)))))
|
||||
|
||||
;; Decision matrix
|
||||
(~doc-section :title "Decision Matrix" :id "matrix"
|
||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Affinity")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Has IO?")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Render Target")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Why")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono" ":auto")
|
||||
(td :class "px-3 py-2" "No")
|
||||
(td :class "px-3 py-2 font-bold text-green-700" "client")
|
||||
(td :class "px-3 py-2 text-stone-600" "Pure — can render anywhere"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono" ":auto")
|
||||
(td :class "px-3 py-2" "Yes")
|
||||
(td :class "px-3 py-2 font-bold text-orange-700" "server")
|
||||
(td :class "px-3 py-2 text-stone-600" "IO must resolve server-side"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono" ":client")
|
||||
(td :class "px-3 py-2" "No")
|
||||
(td :class "px-3 py-2 font-bold text-green-700" "client")
|
||||
(td :class "px-3 py-2 text-stone-600" "Explicit + pure"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono" ":client")
|
||||
(td :class "px-3 py-2" "Yes")
|
||||
(td :class "px-3 py-2 font-bold text-green-700" "client")
|
||||
(td :class "px-3 py-2 text-stone-600" "Override — IO proxied via /sx/io/"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono" ":server")
|
||||
(td :class "px-3 py-2" "No")
|
||||
(td :class "px-3 py-2 font-bold text-orange-700" "server")
|
||||
(td :class "px-3 py-2 text-stone-600" "Override — auth-sensitive"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono" ":server")
|
||||
(td :class "px-3 py-2" "Yes")
|
||||
(td :class "px-3 py-2 font-bold text-orange-700" "server")
|
||||
(td :class "px-3 py-2 text-stone-600" "Both affinity and IO say server"))))))
|
||||
|
||||
;; Per-page render plans
|
||||
(~doc-section :title "Page Render Plans" :id "plans"
|
||||
(p "Phase 7b: render plans are pre-computed at registration time for each page. The plan maps every component needed by the page to its render target.")
|
||||
|
||||
(when (> (len page-plans) 0)
|
||||
(div :class "space-y-4 mt-4"
|
||||
(map (fn (plan)
|
||||
(div :class "rounded border border-stone-200 p-4"
|
||||
(div :class "flex items-center justify-between mb-3"
|
||||
(div
|
||||
(span :class "font-mono font-medium text-stone-800" (get plan "name"))
|
||||
(span :class "text-stone-400 ml-2 text-sm" (get plan "path")))
|
||||
(div :class "flex gap-2"
|
||||
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-orange-100 text-orange-700"
|
||||
(str (get plan "server-count") " server"))
|
||||
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-100 text-green-700"
|
||||
(str (get plan "client-count") " client"))))
|
||||
(when (> (get plan "server-count") 0)
|
||||
(div :class "mb-2"
|
||||
(span :class "text-xs font-medium text-stone-500 uppercase" "Server-expanded: ")
|
||||
(span :class "text-sm font-mono text-orange-700"
|
||||
(join " " (get plan "server")))))
|
||||
(when (> (get plan "client-count") 0)
|
||||
(div
|
||||
(span :class "text-xs font-medium text-stone-500 uppercase" "Client-rendered: ")
|
||||
(span :class "text-sm font-mono text-green-700"
|
||||
(join " " (get plan "client")))))))
|
||||
page-plans))))
|
||||
|
||||
;; How it integrates
|
||||
(~doc-section :title "How It Works" :id "how"
|
||||
(ol :class "list-decimal list-inside text-stone-700 space-y-2"
|
||||
(li (code "defcomp") " parses " (code ":affinity") " annotation between params and body")
|
||||
(li "Component object stores " (code "affinity") " field (\"auto\", \"client\", or \"server\")")
|
||||
(li (code "compute-all-io-refs") " scans transitive IO deps at registration time")
|
||||
(li (code "render-target") " in deps.sx combines affinity + IO analysis → \"server\" or \"client\"")
|
||||
(li "Server partial evaluator (" (code "_aser") ") checks " (code "render_target") ":")
|
||||
(ul :class "list-disc pl-8 text-stone-600"
|
||||
(li "\"server\" → expand component, embed rendered HTML")
|
||||
(li "\"client\" → serialize as SX, let client render"))
|
||||
(li "Client routing uses same info: IO deps in page registry → proxy registration")))
|
||||
|
||||
;; Verification
|
||||
(div :class "rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm space-y-2"
|
||||
(p :class "font-semibold text-amber-800" "How to verify")
|
||||
(ol :class "list-decimal list-inside text-amber-700 space-y-1"
|
||||
(li "View page source — components with render-target \"server\" are expanded to HTML")
|
||||
(li "Components with render-target \"client\" appear as " (code "(~name ...)") " in the SX wire format")
|
||||
(li "Navigate away and back — client-routable pure components render instantly")
|
||||
(li "Check the analysis table above — it shows live data from the server's component registry")))))
|
||||
@@ -74,3 +74,8 @@
|
||||
:params ()
|
||||
:returns "async-generator<dict>"
|
||||
:service "sx")
|
||||
|
||||
(define-page-helper "affinity-demo-data"
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
421
sx/sx/cssx.sx
Normal file
421
sx/sx/cssx.sx
Normal file
@@ -0,0 +1,421 @@
|
||||
;; CSSX — Styling as Components
|
||||
;; Documentation for the CSSX approach: no parallel style infrastructure,
|
||||
;; just defcomp components that decide how to style their children.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Overview
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-overview-content ()
|
||||
(~doc-page :title "CSSX Components"
|
||||
|
||||
(~doc-section :title "The Idea" :id "idea"
|
||||
(p (strong "Styling is just components.") " A CSSX component is a regular "
|
||||
(code "defcomp") " that decides how to style its children. It might apply "
|
||||
"Tailwind classes, or hand-written CSS classes, or inline styles, or generate "
|
||||
"rules at runtime. The implementation is the component's private business. "
|
||||
"The consumer just calls " (code "(~btn :variant \"primary\" \"Submit\")") " and doesn't care.")
|
||||
(p "Because it's " (code "defcomp") ", you get everything for free: caching, bundling, "
|
||||
"dependency scanning, server/client rendering, composition. No parallel infrastructure."))
|
||||
|
||||
(~doc-section :title "Why Not a Style Dictionary?" :id "why"
|
||||
(p "SX previously had a parallel CSS system: a style dictionary (JSON blob of "
|
||||
"atom-to-declaration mappings), a " (code "StyleValue") " type threaded through "
|
||||
"the evaluator and renderer, content-addressed hash class names (" (code "sx-a3f2b1")
|
||||
"), runtime CSS injection, and a separate caching pipeline (cookies, localStorage).")
|
||||
(p "This was ~3,000 lines of code across the spec, bootstrappers, and host implementations. "
|
||||
"It was never adopted. The codebase voted with its feet: " (code ":class") " strings "
|
||||
"with " (code "defcomp") " already covered every real use case.")
|
||||
(p "The result of that system: elements in the DOM got opaque class names like "
|
||||
(code "class=\"sx-a3f2b1\"") ". DevTools became useless. You couldn't inspect an "
|
||||
"element and understand its styling. " (strong "That was a deal breaker.")))
|
||||
|
||||
(~doc-section :title "Key Advantages" :id "advantages"
|
||||
(ul :class "list-disc pl-5 space-y-2 text-stone-700"
|
||||
(li (strong "Readable DOM: ") "Elements have real class names, not content-addressed "
|
||||
"hashes. DevTools works.")
|
||||
(li (strong "Data-driven styling: ") "Components receive data and decide styling. "
|
||||
(code "(~metric :value 150)") " renders red because " (code "value > 100")
|
||||
" — logic lives in the component, not a CSS preprocessor.")
|
||||
(li (strong "One system: ") "No separate " (code "StyleValue") " type, no style "
|
||||
"dictionary JSON, no injection pipeline. Components ARE the styling abstraction.")
|
||||
(li (strong "One cache: ") "Component hash/localStorage handles everything. No "
|
||||
"separate style dict caching.")
|
||||
(li (strong "Composable: ") (code "(~card :elevated true (~metric :value v))")
|
||||
" — styling composes like any other component.")
|
||||
(li (strong "Strategy-agnostic: ") "A component can apply Tailwind classes, emit "
|
||||
(code "<style>") " blocks, use inline styles, generate CSS custom properties, or "
|
||||
"any combination. Swap strategies without touching call sites.")))
|
||||
|
||||
(~doc-section :title "What Changed" :id "changes"
|
||||
(~doc-subsection :title "Removed (~3,000 lines)"
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600 text-sm"
|
||||
(li (code "StyleValue") " type and all plumbing (type checks in eval, render, serialize)")
|
||||
(li (code "cssx.sx") " spec module (resolve-style, resolve-atom, split-variant, hash, injection)")
|
||||
(li "Style dictionary JSON format, loading, caching (" (code "<script type=\"text/sx-styles\">") ", localStorage)")
|
||||
(li (code "style_dict.py") " (782 lines) and " (code "style_resolver.py") " (254 lines)")
|
||||
(li (code "css") " and " (code "merge-styles") " primitives")
|
||||
(li "Platform interface: " (code "fnv1a-hash") ", " (code "compile-regex") ", " (code "make-style-value") ", " (code "inject-style-value"))
|
||||
(li (code "defkeyframes") " special form")
|
||||
(li "Style dict cookies and localStorage keys")))
|
||||
|
||||
(~doc-subsection :title "Kept"
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600 text-sm"
|
||||
(li (code "defstyle") " — simplified to bind any value (string, function, etc.)")
|
||||
(li (code "tw.css") " — the compiled Tailwind stylesheet, delivered via CSS class tracking")
|
||||
(li (code ":class") " attribute — just takes strings, no special-casing")
|
||||
(li "CSS class delivery (" (code "SX-Css") " headers, " (code "<style id=\"sx-css\">") ")")
|
||||
(li "All component infrastructure (defcomp, caching, bundling, deps)")))
|
||||
|
||||
(~doc-subsection :title "Added"
|
||||
(p "Nothing. CSSX components are just " (code "defcomp") ". The only new thing is "
|
||||
"a convention: components whose primary purpose is styling.")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Patterns
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-patterns-content ()
|
||||
(~doc-page :title "Patterns"
|
||||
|
||||
(~doc-section :title "Class Mapping" :id "class-mapping"
|
||||
(p "The simplest pattern: a component that maps semantic keywords to class strings.")
|
||||
(highlight
|
||||
"(defcomp ~btn (&key variant disabled &rest children)\n (button\n :class (str \"px-4 py-2 rounded font-medium transition \"\n (case variant\n \"primary\" \"bg-blue-600 text-white hover:bg-blue-700\"\n \"danger\" \"bg-red-600 text-white hover:bg-red-700\"\n \"ghost\" \"bg-transparent hover:bg-stone-100\"\n \"bg-stone-200 hover:bg-stone-300\")\n (when disabled \" opacity-50 cursor-not-allowed\"))\n :disabled disabled\n children))"
|
||||
"lisp")
|
||||
(p "Consumers call " (code "(~btn :variant \"primary\" \"Submit\")") ". The Tailwind "
|
||||
"classes are readable in DevTools but never repeated across call sites."))
|
||||
|
||||
(~doc-section :title "Data-Driven Styling" :id "data-driven"
|
||||
(p "Styling that responds to data values — impossible with static CSS:")
|
||||
(highlight
|
||||
"(defcomp ~metric (&key value label threshold)\n (let ((t (or threshold 10)))\n (div :class (str \"p-3 rounded font-bold \"\n (cond\n ((> value (* t 10)) \"bg-red-500 text-white\")\n ((> value t) \"bg-amber-200 text-amber-900\")\n (:else \"bg-green-100 text-green-800\")))\n (span :class \"text-sm\" label) \": \" (span (str value)))))"
|
||||
"lisp")
|
||||
(p "The component makes a " (em "decision") " about styling based on data. "
|
||||
"No CSS preprocessor or class name convention can express \"red when value > 100\"."))
|
||||
|
||||
(~doc-section :title "Style Functions" :id "style-functions"
|
||||
(p "Reusable style logic that returns class strings — no wrapping element needed:")
|
||||
(highlight
|
||||
"(define card-classes\n (fn (&key elevated bordered)\n (str \"rounded-lg p-4 \"\n (if elevated \"shadow-lg\" \"shadow-sm\")\n (when bordered \" border border-stone-200\"))))\n\n;; Usage:\n(div :class (card-classes :elevated true) ...)\n(article :class (card-classes :bordered true) ...)"
|
||||
"lisp")
|
||||
(p "Or with " (code "defstyle") " for named bindings:")
|
||||
(highlight
|
||||
"(defstyle card-base \"rounded-lg p-4 shadow-sm\")\n(defstyle card-elevated \"rounded-lg p-4 shadow-lg\")\n\n(div :class card-base ...)"
|
||||
"lisp"))
|
||||
|
||||
(~doc-section :title "Responsive Layouts" :id "responsive"
|
||||
(p "Components that encode responsive breakpoints:")
|
||||
(highlight
|
||||
"(defcomp ~responsive-grid (&key cols &rest children)\n (div :class (str \"grid gap-4 \"\n (case (or cols 3)\n 1 \"grid-cols-1\"\n 2 \"grid-cols-1 md:grid-cols-2\"\n 3 \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n 4 \"grid-cols-2 md:grid-cols-3 lg:grid-cols-4\"))\n children))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-section :title "Emitting CSS Directly" :id "emitting-css"
|
||||
(p "Components are not limited to referencing existing classes. They can generate "
|
||||
"CSS — " (code "<style>") " tags, keyframes, custom properties — as part of their output:")
|
||||
(highlight
|
||||
"(defcomp ~pulse (&key color duration &rest children)\n (<>\n (style (str \"@keyframes sx-pulse {\"\n \"0%,100% { opacity:1 } 50% { opacity:.5 } }\"))\n (div :style (str \"animation: sx-pulse \" (or duration \"2s\") \" infinite;\"\n \"color:\" (or color \"inherit\"))\n children)))"
|
||||
"lisp")
|
||||
(highlight
|
||||
"(defcomp ~theme (&key primary surface &rest children)\n (<>\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\"))\n children))"
|
||||
"lisp")
|
||||
(p "The CSS strategy is the component's private implementation detail. Consumers call "
|
||||
(code "(~pulse :color \"red\" \"Loading...\")") " or "
|
||||
(code "(~theme :primary \"#2563eb\" ...)") " without knowing or caring whether the "
|
||||
"component uses classes, inline styles, generated rules, or all three."))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Async CSS
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-async-content ()
|
||||
(~doc-page :title "Async CSS"
|
||||
|
||||
(~doc-section :title "The Pattern" :id "pattern"
|
||||
(p "A CSSX component that needs CSS it doesn't have yet can "
|
||||
(strong "fetch and cache it before rendering") ". This is just "
|
||||
(code "~suspense") " combined with a style component — no new infrastructure:")
|
||||
(highlight
|
||||
"(defcomp ~styled (&key css-url css-hash fallback &rest children)\n (if (css-cached? css-hash)\n ;; Already have it — render immediately\n children\n ;; Don't have it — suspense while we fetch\n (~suspense :id (str \"css-\" css-hash)\n :fallback (or fallback (span \"\"))\n (do\n (fetch-css css-url css-hash)\n children))))"
|
||||
"lisp")
|
||||
(p "The consumer never knows:")
|
||||
(highlight
|
||||
"(~styled :css-url \"/css/charts.css\" :css-hash \"abc123\"\n (~bar-chart :data metrics))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-section :title "Use Cases" :id "use-cases"
|
||||
(~doc-subsection :title "Federated Components"
|
||||
(p "A " (code "~btn") " from another site arrives via IPFS with a CID pointing "
|
||||
"to its CSS. The component fetches and caches it before rendering. "
|
||||
"No coordination needed between sites.")
|
||||
(highlight
|
||||
"(defcomp ~federated-widget (&key cid &rest children)\n (let ((css-cid (str cid \"/style.css\"))\n (cached (css-cached? css-cid)))\n (if cached\n children\n (~suspense :id (str \"fed-\" cid)\n :fallback (div :class \"animate-pulse bg-stone-100 rounded h-20\")\n (do (fetch-css (str \"https://ipfs.io/ipfs/\" css-cid) css-cid)\n children)))))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-subsection :title "Heavy UI Libraries"
|
||||
(p "Code editors, chart libraries, rich text editors — their CSS only loads "
|
||||
"when the component actually appears on screen:")
|
||||
(highlight
|
||||
"(defcomp ~code-editor (&key language value on-change)\n (~styled :css-url \"/css/codemirror.css\" :css-hash (asset-hash \"codemirror\")\n :fallback (pre :class \"p-4 bg-stone-900 text-stone-300 rounded\" value)\n (div :class \"cm-editor\"\n :data-language language\n :data-value value)))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-subsection :title "Lazy Themes"
|
||||
(p "Theme CSS loads on first use, then is instant on subsequent visits:")
|
||||
(highlight
|
||||
"(defcomp ~lazy-theme (&key name &rest children)\n (let ((css-url (str \"/css/themes/\" name \".css\"))\n (hash (str \"theme-\" name)))\n (~styled :css-url css-url :css-hash hash\n :fallback children ;; render unstyled immediately\n children)))"
|
||||
"lisp")))
|
||||
|
||||
(~doc-section :title "How It Composes" :id "composition"
|
||||
(p "Async CSS composes with everything already in SX:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (code "~suspense") " handles the async gap with fallback content")
|
||||
(li "localStorage handles caching across sessions")
|
||||
(li (code "<style id=\"sx-css\">") " is the injection target (same as CSS class delivery)")
|
||||
(li "Component content-hashing tracks what the client has")
|
||||
(li "No new types, no new delivery protocol, no new spec code")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Live Styles
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-live-content ()
|
||||
(~doc-page :title "Live Styles"
|
||||
|
||||
(~doc-section :title "Styles That Respond to Events" :id "concept"
|
||||
(p "Combine " (code "~live") " (SSE) or " (code "~ws") " (WebSocket) with style "
|
||||
"components, and you get styles that change in real-time in response to server "
|
||||
"events. No new infrastructure — just components receiving data through a "
|
||||
"persistent transport."))
|
||||
|
||||
(~doc-section :title "SSE: Live Theme Updates" :id "sse-theme"
|
||||
(p "A " (code "~live") " component declares a persistent connection to an SSE "
|
||||
"endpoint. When the server pushes a new event, " (code "resolveSuspense")
|
||||
" replaces the content:")
|
||||
(highlight
|
||||
"(~live :src \"/api/stream/brand\"\n (~suspense :id \"theme\"\n (~theme :primary \"#7c3aed\" :surface \"#fafaf9\")))"
|
||||
"lisp")
|
||||
(p "Server pushes a new theme:")
|
||||
(highlight
|
||||
"event: sx-resolve\ndata: {\"id\": \"theme\", \"sx\": \"(~theme :primary \\\"#2563eb\\\" :surface \\\"#1e1e2e\\\")\"}"
|
||||
"text")
|
||||
(p "The " (code "~theme") " component emits CSS custom properties. Everything "
|
||||
"using " (code "var(--color-primary)") " repaints instantly:")
|
||||
(highlight
|
||||
"(defcomp ~theme (&key primary surface)\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\")))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-section :title "SSE: Live Dashboard Metrics" :id "sse-metrics"
|
||||
(p "Style changes driven by live data — the component decides the visual treatment:")
|
||||
(highlight
|
||||
"(~live :src \"/api/stream/dashboard\"\n (~suspense :id \"cpu\"\n (~metric :value 0 :label \"CPU\" :threshold 80))\n (~suspense :id \"memory\"\n (~metric :value 0 :label \"Memory\" :threshold 90))\n (~suspense :id \"requests\"\n (~metric :value 0 :label \"RPS\" :threshold 1000)))"
|
||||
"lisp")
|
||||
(p "Server pushes updated values. " (code "~metric") " turns red when "
|
||||
(code "value > threshold") " — the styling logic lives in the component, "
|
||||
"not in CSS selectors or JavaScript event handlers."))
|
||||
|
||||
(~doc-section :title "WebSocket: Collaborative Design" :id "ws-design"
|
||||
(p "Bidirectional channel for real-time collaboration. A designer adjusts a color, "
|
||||
"all connected clients see the change:")
|
||||
(highlight
|
||||
"(~ws :src \"/ws/design-studio\"\n (~suspense :id \"canvas-theme\"\n (~theme :primary \"#7c3aed\")))"
|
||||
"lisp")
|
||||
(p "Client sends a color change:")
|
||||
(highlight
|
||||
";; Designer picks a new primary color\n(sx-send ws-conn '(theme-update :primary \"#dc2626\"))"
|
||||
"lisp")
|
||||
(p "Server broadcasts to all connected clients via " (code "sx-resolve") " — "
|
||||
"every client's " (code "~theme") " component re-renders with the new color."))
|
||||
|
||||
(~doc-section :title "Why This Works" :id "why"
|
||||
(p "Every one of these patterns is just a " (code "defcomp") " receiving data "
|
||||
"through a persistent transport. The styling strategy — CSS custom properties, "
|
||||
"class swaps, inline styles, " (code "<style>") " blocks — is the component's "
|
||||
"private business. The transport doesn't know or care.")
|
||||
(p "A parallel style system would have needed its own streaming, its own caching, "
|
||||
"its own delta protocol for each of these use cases — duplicating what components "
|
||||
"already do.")
|
||||
(p :class "mt-4 text-stone-500 italic"
|
||||
"Note: ~live and ~ws are planned (see Live Streaming). The patterns shown here "
|
||||
"will work as described once the streaming transport is implemented. The component "
|
||||
"and suspense infrastructure they depend on already exists."))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Comparison with CSS Technologies
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-comparison-content ()
|
||||
(~doc-page :title "Comparisons"
|
||||
|
||||
(~doc-section :title "styled-components / Emotion" :id "styled-components"
|
||||
(p (a :href "https://styled-components.com" :class "text-violet-600 hover:underline" "styled-components")
|
||||
" pioneered the idea that styling belongs in components. But it generates CSS "
|
||||
"at runtime, injects " (code "<style>") " tags, and produces opaque hashed class "
|
||||
"names (" (code "class=\"sc-bdfBwQ fNMpVx\"") "). Open DevTools and you see gibberish. "
|
||||
"It also carries significant runtime cost — parsing CSS template literals, hashing, "
|
||||
"deduplicating — and needs a separate SSR extraction step (" (code "ServerStyleSheet") ").")
|
||||
(p "CSSX components share the core insight (" (em "styling is a component concern")
|
||||
") but without the runtime machinery. When a component applies Tailwind classes, "
|
||||
"there's zero CSS generation overhead. When it does emit " (code "<style>")
|
||||
" blocks, it's explicit — not hidden behind a tagged template literal. "
|
||||
"And the DOM is always readable."))
|
||||
|
||||
(~doc-section :title "CSS Modules" :id "css-modules"
|
||||
(p (a :href "https://github.com/css-modules/css-modules" :class "text-violet-600 hover:underline" "CSS Modules")
|
||||
" scope class names to avoid collisions by rewriting them at build time: "
|
||||
(code ".button") " becomes " (code ".button_abc123")
|
||||
". This solves the global namespace problem but creates the same opacity issue — "
|
||||
"hashed names in the DOM that you can't grep for or reason about.")
|
||||
(p "CSSX components don't need scoping because component boundaries already provide "
|
||||
"isolation. A " (code "~btn") " owns its markup. There's nothing to collide with."))
|
||||
|
||||
(~doc-section :title "Tailwind CSS" :id "tailwind"
|
||||
(p "Tailwind is " (em "complementary") ", not competitive. CSSX components are the "
|
||||
"semantic layer on top. Raw Tailwind in markup — "
|
||||
(code ":class \"px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700\"")
|
||||
" — is powerful but verbose and duplicated across call sites.")
|
||||
(p "A CSSX component wraps that string once: " (code "(~btn :variant \"primary\" \"Submit\")")
|
||||
". The Tailwind classes are still there, readable in DevTools, but consumers don't "
|
||||
"repeat them. This is the same pattern Tailwind's own docs recommend ("
|
||||
(em "\"extracting components\"") ") — CSSX components are just SX's native way of doing it."))
|
||||
|
||||
(~doc-section :title "Vanilla Extract" :id "vanilla-extract"
|
||||
(p (a :href "https://vanilla-extract.style" :class "text-violet-600 hover:underline" "Vanilla Extract")
|
||||
" is zero-runtime CSS-in-JS: styles are written in TypeScript, compiled to static "
|
||||
"CSS at build time, and referenced by generated class names. It avoids the runtime "
|
||||
"cost of styled-components but still requires a build step, a bundler plugin, and "
|
||||
"TypeScript. The generated class names are again opaque.")
|
||||
(p "CSSX components need no build step for styling — they're evaluated at render time "
|
||||
"like any other component. And since the component chooses its own strategy, it can "
|
||||
"reference pre-built classes (zero runtime) " (em "or") " generate CSS on the fly — "
|
||||
"same API either way."))
|
||||
|
||||
(~doc-section :title "Design Tokens / Style Dictionary" :id "design-tokens"
|
||||
(p "The " (a :href "https://amzn.github.io/style-dictionary/" :class "text-violet-600 hover:underline" "Style Dictionary")
|
||||
" pattern — a JSON/YAML file mapping token names to values, compiled to "
|
||||
"platform-specific output — is essentially what the old CSSX was. It's the "
|
||||
"industry standard for design systems.")
|
||||
(p "The problem is that it's a parallel system: separate file format, separate build "
|
||||
"pipeline, separate caching, separate tooling. CSSX components eliminate all of "
|
||||
"that by expressing tokens as component parameters: "
|
||||
(code "(~theme :primary \"#7c3aed\")") " instead of "
|
||||
(code "{\"color\": {\"primary\": {\"value\": \"#7c3aed\"}}}")
|
||||
". Same result, no parallel infrastructure."))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Philosophy
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-philosophy-content ()
|
||||
(~doc-page :title "Philosophy"
|
||||
|
||||
(~doc-section :title "The Collapse" :id "collapse"
|
||||
(p "The web has spent two decades building increasingly complex CSS tooling: "
|
||||
"preprocessors, CSS-in-JS, atomic CSS, utility frameworks, design tokens, style "
|
||||
"dictionaries. Each solves a real problem but adds a new system with its own "
|
||||
"caching, bundling, and mental model.")
|
||||
(p "CSSX components collapse all of this back to the simplest possible thing: "
|
||||
(strong "a function that takes data and returns markup with classes.")
|
||||
" That's what a component already is. There is no separate styling system because "
|
||||
"there doesn't need to be."))
|
||||
|
||||
(~doc-section :title "Proof by Deletion" :id "proof"
|
||||
(p "The strongest validation: we built the full parallel system — style dictionary, "
|
||||
"StyleValue type, content-addressed hashing, runtime injection, localStorage "
|
||||
"caching — and then deleted it because nobody used it. The codebase already had "
|
||||
"the answer: " (code "defcomp") " with " (code ":class") " strings.")
|
||||
(p "3,000 lines of infrastructure removed. Zero lines added. Every use case still works."))
|
||||
|
||||
(~doc-section :title "The Right Abstraction Level" :id "abstraction"
|
||||
(p "CSS-in-JS puts styling " (em "below") " components — you style elements, then compose "
|
||||
"them. Utility CSS puts styling " (em "beside") " components — classes in markup, logic "
|
||||
"elsewhere. Both create a seam between what something does and how it looks.")
|
||||
(p "CSSX components put styling " (em "inside") " components — at the same level as "
|
||||
"structure and behavior. A " (code "~metric") " component knows its own thresholds, "
|
||||
"its own color scheme, its own responsive behavior. Styling is just another "
|
||||
"decision the component makes, not a separate concern."))
|
||||
|
||||
(~doc-section :title "Relationship to Other Plans" :id "relationships"
|
||||
(ul :class "list-disc pl-5 space-y-2 text-stone-700"
|
||||
(li (strong "Content-Addressed Components: ") "CSSX components get CIDs like any "
|
||||
"other component. A " (code "~btn") " from one site can be shared to another via "
|
||||
"IPFS. No " (code ":css-atoms") " manifest field needed — the component carries "
|
||||
"its own styling logic.")
|
||||
(li (strong "Isomorphic Rendering: ") "Components render the same on server and client. "
|
||||
"No style injection timing issues, no FOUC from late CSS loading.")
|
||||
(li (strong "Component Bundling: ") "deps.sx already handles transitive component deps. "
|
||||
"Style components are just more components in the bundle — no separate style bundling.")
|
||||
(li (strong "Live Streaming: ") "SSE and WebSocket transports push data to components. "
|
||||
"Style components react to that data like any other component — no separate style "
|
||||
"streaming protocol.")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CSS Delivery
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx-delivery-content ()
|
||||
(~doc-page :title "CSS Delivery"
|
||||
|
||||
(~doc-section :title "Multiple Strategies" :id "strategies"
|
||||
(p "A CSSX component chooses its own styling strategy — and each strategy has its "
|
||||
"own delivery path:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (strong "Tailwind classes: ") "Delivered via the on-demand protocol below. "
|
||||
"The server ships only the rules the page actually uses.")
|
||||
(li (strong "Inline styles: ") "No delivery needed — " (code ":style") " attributes "
|
||||
"are part of the markup.")
|
||||
(li (strong "Emitted " (code "<style>") " blocks: ") "Components can emit CSS rules "
|
||||
"directly. They arrive as part of the rendered HTML — keyframes, custom properties, "
|
||||
"scoped rules, anything.")
|
||||
(li (strong "External stylesheets: ") "Components can reference pre-loaded CSS files "
|
||||
"or lazy-load them via " (code "~suspense") " (see " (a :href "/cssx/async" "Async CSS") ").")
|
||||
(li (strong "Custom properties: ") "A " (code "~theme") " component sets "
|
||||
(code "--color-primary") " etc. via a " (code "<style>") " block. Everything "
|
||||
"using " (code "var()") " repaints automatically."))
|
||||
(p "The protocol below handles the Tailwind utility class case — the most common "
|
||||
"strategy — but it's not the only game in town."))
|
||||
|
||||
(~doc-section :title "On-Demand Tailwind Delivery" :id "on-demand"
|
||||
(p "When components use Tailwind utility classes, SX ships " (em "only the CSS rules "
|
||||
"actually used on the page") ", computed at render time. No build step, no purging, "
|
||||
"no unused CSS.")
|
||||
(p "The server pre-parses the full Tailwind CSS file into an in-memory registry at "
|
||||
"startup. When a response is rendered, SX scans all " (code ":class") " values in "
|
||||
"the output, looks up only those classes, and embeds the matching rules."))
|
||||
|
||||
(~doc-section :title "The Protocol" :id "protocol"
|
||||
(p "First page load gets the full set of used rules. Subsequent navigations send a "
|
||||
"hash of what the client already has, and the server ships only the delta:")
|
||||
(highlight
|
||||
"# First page load:\nGET / HTTP/1.1\n\nHTTP/1.1 200 OK\nContent-Type: text/html\n# Full CSS in <style id=\"sx-css\"> + hash in <meta name=\"sx-css-classes\">\n\n# Subsequent navigation:\nGET /about HTTP/1.1\nSX-Css: a1b2c3d4\n\nHTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: e5f6g7h8\nSX-Css-Add: bg-blue-500,text-white,rounded-lg\n# Only new rules in <style data-sx-css>"
|
||||
"bash")
|
||||
(p "The client merges new rules into the existing " (code "<style id=\"sx-css\">")
|
||||
" block and updates its hash. No flicker, no duplicate rules."))
|
||||
|
||||
(~doc-section :title "Component-Aware Scanning" :id "scanning"
|
||||
(p "CSS scanning happens at two levels:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (strong "Registration time: ") "When a component is defined via " (code "defcomp")
|
||||
", its body is scanned for class strings. The component records which CSS classes "
|
||||
"it uses.")
|
||||
(li (strong "Render time: ") "The rendered output is scanned for any classes the "
|
||||
"static scan missed — dynamically constructed class strings, conditional classes, etc."))
|
||||
(p "This means the CSS registry knows roughly what a page needs before rendering, "
|
||||
"and catches any stragglers after."))
|
||||
|
||||
(~doc-section :title "Trade-offs" :id "tradeoffs"
|
||||
(ul :class "list-disc pl-5 space-y-2 text-stone-700"
|
||||
(li (strong "Full Tailwind in memory: ") "The parsed CSS registry is ~4MB. This is "
|
||||
"a one-time startup cost per app instance.")
|
||||
(li (strong "Regex scanning: ") "Class detection uses regex, so dynamically "
|
||||
"constructed class names (e.g. " (code "(str \"bg-\" color \"-500\")") ") can be "
|
||||
"missed. Use complete class strings in " (code "case") "/" (code "cond") " branches.")
|
||||
(li (strong "No @apply: ") "Tailwind's " (code "@apply") " is a build-time feature. "
|
||||
"On-demand delivery works with utility classes directly.")
|
||||
(li (strong "Tailwind-shaped: ") "The registry parser understands Tailwind's naming "
|
||||
"conventions. Non-Tailwind CSS works via " (code "<style>") " blocks in components.")))))
|
||||
@@ -85,28 +85,6 @@
|
||||
"Forms marked with a tail position enable " (a :href "/essays/tco" :class "text-violet-600 hover:underline" "tail-call optimization") " — recursive calls in tail position use constant stack space.")
|
||||
(div :class "space-y-10" forms))))
|
||||
|
||||
(defcomp ~docs-css-content ()
|
||||
(~doc-page :title "On-Demand CSS"
|
||||
(~doc-section :title "How it works" :id "how"
|
||||
(p :class "text-stone-600"
|
||||
"sx scans every response for CSS class names used in :class attributes. It looks up only those classes in a pre-parsed Tailwind CSS registry and ships just the rules that are needed. No build step. No purging. No unused CSS.")
|
||||
(p :class "text-stone-600"
|
||||
"On the first page load, the full set of used classes is embedded in a <style> block. A hash of the class set is stored. On subsequent navigations, the client sends the hash in the SX-Css header. The server computes the diff and sends only new rules via SX-Css-Add and a <style data-sx-css> block."))
|
||||
(~doc-section :title "The protocol" :id "protocol"
|
||||
(~doc-code :code (highlight "# First page load:\nGET / HTTP/1.1\n\nHTTP/1.1 200 OK\nContent-Type: text/html\n# Full CSS in <style id=\"sx-css\"> + hash in <meta name=\"sx-css-classes\">\n\n# Subsequent navigation:\nGET /about HTTP/1.1\nSX-Css: a1b2c3d4\n\nHTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: e5f6g7h8\nSX-Css-Add: bg-blue-500,text-white,rounded-lg\n# Only new rules in <style data-sx-css>" "bash")))
|
||||
(~doc-section :title "Advantages" :id "advantages"
|
||||
(ul :class "space-y-2 text-stone-600"
|
||||
(li "Zero build step — no Tailwind CLI, no PostCSS, no purging")
|
||||
(li "Exact CSS — never ships a rule that isn't used on the page")
|
||||
(li "Incremental — subsequent navigations only ship new rules")
|
||||
(li "Component-aware — pre-scans component definitions at registration time")))
|
||||
(~doc-section :title "Disadvantages" :id "disadvantages"
|
||||
(ul :class "space-y-2 text-stone-600"
|
||||
(li "Requires the full Tailwind CSS file loaded in memory at startup (~4MB parsed)")
|
||||
(li "Regex-based class scanning — can miss dynamically constructed class names")
|
||||
(li "No @apply support — classes must be used directly")
|
||||
(li "Tied to Tailwind's utility class naming conventions")))))
|
||||
|
||||
(defcomp ~docs-server-rendering-content ()
|
||||
(~doc-page :title "Server Rendering"
|
||||
(~doc-section :title "Python API" :id "python"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(let* ((sc "aria-selected:bg-violet-200 aria-selected:text-violet-900")
|
||||
(items (list
|
||||
(dict :label "Docs" :href "/docs/introduction")
|
||||
(dict :label "CSSX" :href "/cssx/")
|
||||
(dict :label "Reference" :href "/reference/")
|
||||
(dict :label "Protocols" :href "/protocols/wire-format")
|
||||
(dict :label "Examples" :href "/examples/click-to-load")
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
(dict :label "Evaluator" :href "/docs/evaluator")
|
||||
(dict :label "Primitives" :href "/docs/primitives")
|
||||
(dict :label "Special Forms" :href "/docs/special-forms")
|
||||
(dict :label "CSS" :href "/docs/css")
|
||||
(dict :label "Server Rendering" :href "/docs/server-rendering")))
|
||||
|
||||
(define reference-nav-items (list
|
||||
@@ -55,13 +54,22 @@
|
||||
(dict :label "Request Abort" :href "/examples/sync-replace")
|
||||
(dict :label "Retry" :href "/examples/retry")))
|
||||
|
||||
(define cssx-nav-items (list
|
||||
(dict :label "Overview" :href "/cssx/")
|
||||
(dict :label "Patterns" :href "/cssx/patterns")
|
||||
(dict :label "Delivery" :href "/cssx/delivery")
|
||||
(dict :label "Async CSS" :href "/cssx/async")
|
||||
(dict :label "Live Styles" :href "/cssx/live")
|
||||
(dict :label "Comparisons" :href "/cssx/comparisons")
|
||||
(dict :label "Philosophy" :href "/cssx/philosophy")))
|
||||
|
||||
(define essays-nav-items (list
|
||||
(dict :label "Why S-Expressions" :href "/essays/why-sexps"
|
||||
:summary "Why SX uses s-expressions instead of HTML templates, JSX, or any other syntax.")
|
||||
(dict :label "The htmx/React Hybrid" :href "/essays/htmx-react-hybrid"
|
||||
:summary "How SX combines the server-driven simplicity of htmx with the component model of React.")
|
||||
(dict :label "On-Demand CSS" :href "/essays/on-demand-css"
|
||||
:summary "The CSSX system: keyword atoms resolved to class names, CSS rules injected on first use.")
|
||||
:summary "How SX delivers only the CSS each page needs — server scans rendered classes, sends the delta.")
|
||||
(dict :label "Client Reactivity" :href "/essays/client-reactivity"
|
||||
:summary "Reactive UI updates without a virtual DOM, diffing library, or build step.")
|
||||
(dict :label "SX Native" :href "/essays/sx-native"
|
||||
@@ -103,7 +111,6 @@
|
||||
(dict :label "SxEngine" :href "/specs/engine")
|
||||
(dict :label "Orchestration" :href "/specs/orchestration")
|
||||
(dict :label "Boot" :href "/specs/boot")
|
||||
(dict :label "CSSX" :href "/specs/cssx")
|
||||
(dict :label "Continuations" :href "/specs/continuations")
|
||||
(dict :label "call/cc" :href "/specs/callcc")
|
||||
(dict :label "Deps" :href "/specs/deps")
|
||||
@@ -125,7 +132,8 @@
|
||||
(dict :label "Routing Analyzer" :href "/isomorphism/routing-analyzer")
|
||||
(dict :label "Data Test" :href "/isomorphism/data-test")
|
||||
(dict :label "Async IO" :href "/isomorphism/async-io")
|
||||
(dict :label "Streaming" :href "/isomorphism/streaming")))
|
||||
(dict :label "Streaming" :href "/isomorphism/streaming")
|
||||
(dict :label "Affinity" :href "/isomorphism/affinity")))
|
||||
|
||||
(define plans-nav-items (list
|
||||
(dict :label "Status" :href "/plans/status"
|
||||
@@ -146,8 +154,6 @@
|
||||
:summary "OAuth-based sharing to Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon.")
|
||||
(dict :label "SX CI Pipeline" :href "/plans/sx-ci"
|
||||
:summary "Build, test, and deploy in s-expressions — CI pipelines as SX components.")
|
||||
(dict :label "CSSX Components" :href "/plans/cssx-components"
|
||||
:summary "Styling as components — replace the style dictionary with regular defcomps that apply classes, respond to data, and compose naturally.")
|
||||
(dict :label "Live Streaming" :href "/plans/live-streaming"
|
||||
:summary "SSE and WebSocket transports for re-resolving suspense slots after initial page load — live data, real-time collaboration.")))
|
||||
|
||||
@@ -176,7 +182,7 @@
|
||||
:prose "Special forms are the syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface. The registry covers control flow (if, when, cond, case, and, or), binding (let, letrec, define, set!), functions (lambda, defcomp, defmacro), sequencing (begin, do, thread-first), quoting (quote, quasiquote), continuations (reset, shift), guards (dynamic-wind), higher-order forms (map, filter, reduce), and domain-specific definitions (defstyle, defhandler, defpage, defquery, defaction).")
|
||||
(dict :slug "renderer" :filename "render.sx" :title "Renderer"
|
||||
:desc "Shared rendering registries and utilities used by all adapters."
|
||||
:prose "The renderer defines what is renderable and how arguments are parsed, but not the output format. It maintains registries of known HTML tags, SVG tags, void elements, and boolean attributes. It specifies how keyword arguments on elements become HTML attributes, how children are collected, and how special attributes (class, style, data-*) are handled. All three adapters (DOM, HTML, SX wire) share these definitions so they agree on what constitutes valid markup. The renderer also defines the StyleValue type used by the CSSX on-demand CSS system.")))
|
||||
:prose "The renderer defines what is renderable and how arguments are parsed, but not the output format. It maintains registries of known HTML tags, SVG tags, void elements, and boolean attributes. It specifies how keyword arguments on elements become HTML attributes, how children are collected, and how special attributes (class, style, data-*) are handled. All three adapters (DOM, HTML, SX wire) share these definitions so they agree on what constitutes valid markup.")))
|
||||
|
||||
(define adapter-spec-items (list
|
||||
(dict :slug "adapter-dom" :filename "adapter-dom.sx" :title "DOM Adapter"
|
||||
@@ -198,10 +204,7 @@
|
||||
(define browser-spec-items (list
|
||||
(dict :slug "boot" :filename "boot.sx" :title "Boot"
|
||||
:desc "Browser startup lifecycle: mount, hydrate, script processing."
|
||||
:prose "Boot handles the browser startup sequence and provides the public API for mounting SX content. On page load it: (1) initializes CSS tracking, (2) loads the style dictionary from inline JSON, (3) processes <script type=\"text/sx\"> tags (component definitions and mount directives), (4) hydrates [data-sx] elements, and (5) activates the engine on all elements. It also provides the public mount/hydrate/update/render-component API, and the head element hoisting logic that moves <meta>, <title>, and <link> tags from rendered content into <head>.")
|
||||
(dict :slug "cssx" :filename "cssx.sx" :title "CSSX"
|
||||
:desc "On-demand CSS: style dictionary, keyword resolution, rule injection."
|
||||
:prose "CSSX is the on-demand CSS system. It resolves keyword atoms (:flex, :gap-4, :hover:bg-sky-200) into StyleValue objects with content-addressed class names, injecting CSS rules into the document on first use. The style dictionary is a JSON blob containing: atoms (keyword to CSS declarations), pseudo-variants (hover:, focus:, etc.), responsive breakpoints (md:, lg:, etc.), keyframe animations, arbitrary value patterns, and child selector prefixes (space-x-, space-y-). Classes are only emitted when used, keeping the CSS payload minimal. The dictionary is typically served inline in a <script type=\"text/sx-styles\"> tag.")))
|
||||
:prose "Boot handles the browser startup sequence and provides the public API for mounting SX content. On page load it: (1) initializes CSS tracking, (2) processes <script type=\"text/sx\"> tags (component definitions and mount directives), (3) hydrates [data-sx] elements, and (4) activates the engine on all elements. It also provides the public mount/hydrate/update/render-component API, and the head element hoisting logic that moves <meta>, <title>, and <link> tags from rendered content into <head>.")))
|
||||
|
||||
(define extension-spec-items (list
|
||||
(dict :slug "continuations" :filename "continuations.sx" :title "Continuations"
|
||||
|
||||
246
sx/sx/plans.sx
246
sx/sx/plans.sx
@@ -1970,37 +1970,111 @@
|
||||
|
||||
(~doc-section :title "Phase 7: Full Isomorphism" :id "phase-7"
|
||||
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
||||
(p :class "text-violet-900 font-medium" "What it enables")
|
||||
(p :class "text-violet-800" "Same SX code runs on either side. Runtime chooses optimal split. Offline-first with cached data + client eval."))
|
||||
(div :class "rounded border border-green-200 bg-green-50 p-4 mb-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
|
||||
(p :class "text-green-900 font-medium" "What it enables")
|
||||
(p :class "text-green-800" "Same SX code runs on either side. Runtime chooses optimal split via affinity annotations and render plans. Cross-host isomorphism verified by 61 automated tests."))
|
||||
|
||||
(~doc-subsection :title "Approach"
|
||||
(~doc-subsection :title "7a. Affinity Annotations & Render Target"
|
||||
|
||||
(div :class "space-y-4"
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "1. Runtime boundary optimizer")
|
||||
(p "Given component tree + IO dependency graph, decide per-component: server-expand, client-render, or stream. Planning step cached at registration, recomputed on component change."))
|
||||
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
|
||||
(div :class "flex items-center gap-2 mb-1"
|
||||
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
|
||||
(p :class "text-green-800 text-sm" "Components declare where they prefer to render. The spec combines affinity with IO analysis to produce a per-component render target decision."))
|
||||
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "2. Affinity annotations")
|
||||
(~doc-code :code (highlight "(defcomp ~product-grid (&key products)\n :affinity :client ;; interactive, prefer client\n ...)\n\n(defcomp ~auth-menu (&key user)\n :affinity :server ;; auth-sensitive, always server\n ...)" "lisp"))
|
||||
(p "Default: auto (runtime decides from IO analysis)."))
|
||||
(p "Affinity annotations let component authors express rendering preferences:")
|
||||
(~doc-code :code (highlight "(defcomp ~product-grid (&key products)\n :affinity :client ;; interactive, prefer client rendering\n (div ...))\n\n(defcomp ~auth-menu (&key user)\n :affinity :server ;; auth-sensitive, always server\n (div ...))\n\n(defcomp ~card (&key title)\n ;; no annotation = :affinity :auto (default)\n ;; runtime decides from IO analysis\n (div ...))" "lisp"))
|
||||
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "3. Optimistic data updates")
|
||||
(p "Extend existing apply-optimistic/revert-optimistic in engine.sx from DOM-level to data-level. Client updates cached data optimistically, sends mutation to server, reverts on rejection."))
|
||||
(p "The " (code "render-target") " function in deps.sx combines affinity with IO analysis:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (code ":affinity :server") " → always " (code "\"server\"") " (auth-sensitive, secrets, heavy IO)")
|
||||
(li (code ":affinity :client") " → always " (code "\"client\"") " (interactive, IO proxied)")
|
||||
(li (code ":affinity :auto") " (default) → " (code "\"server\"") " if IO-dependent, " (code "\"client\"") " if pure"))
|
||||
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "4. Offline data layer")
|
||||
(p "Service Worker intercepts /internal/data/ requests, serves from IndexedDB when offline, syncs when back online."))
|
||||
(p "The server's partial evaluator (" (code "_aser") ") uses " (code "render_target") " instead of the previous " (code "is_pure") " check. Components with " (code ":affinity :client") " are serialized for client rendering even if they call IO primitives — the IO proxy (Phase 5) handles the calls.")
|
||||
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "5. Isomorphic testing")
|
||||
(p "Evaluate same component on Python and JS, compare output. Extends existing test_sx_ref.py cross-evaluator comparison."))
|
||||
(~doc-subsection :title "Files"
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
|
||||
(li "shared/sx/ref/eval.sx — defcomp annotation parsing, defcomp-kwarg helper")
|
||||
(li "shared/sx/ref/deps.sx — render-target function, platform interface")
|
||||
(li "shared/sx/types.py — Component.affinity field, render_target property")
|
||||
(li "shared/sx/evaluator.py — _sf_defcomp annotation extraction")
|
||||
(li "shared/sx/async_eval.py — _aser uses render_target")
|
||||
(li "shared/sx/ref/bootstrap_js.py — Component.affinity, componentAffinity()")
|
||||
(li "shared/sx/ref/bootstrap_py.py — component_affinity(), make_component()")
|
||||
(li "shared/sx/ref/test-eval.sx — 4 new defcomp affinity tests")
|
||||
(li "shared/sx/ref/test-deps.sx — 6 new render-target tests")))
|
||||
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "6. Universal page descriptor")
|
||||
(p "defpage is portable: server executes via execute_page(), client executes via route match → fetch data → eval content → render DOM. Same descriptor, different execution environment."))))
|
||||
(~doc-subsection :title "Verification"
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li "269 spec tests pass (10 new: 4 eval + 6 deps)")
|
||||
(li "79 Python unit tests pass")
|
||||
(li "Bootstrapped to both hosts (sx_ref.py + sx-browser.js)")
|
||||
(li "Backward compatible: existing defcomp without :affinity defaults to \"auto\""))))
|
||||
|
||||
(~doc-subsection :title "7b. Runtime Boundary Optimizer"
|
||||
|
||||
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
|
||||
(div :class "flex items-center gap-2 mb-1"
|
||||
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
|
||||
(p :class "text-green-800 text-sm" "Per-page render plans computed at registration time. Each page knows exactly which components render server-side vs client-side, cached on PageDef."))
|
||||
|
||||
(p "Given component tree + IO dependency graph + affinity annotations, decide per-component: server-expand, client-render, or stream. Planning step cached at registration, recomputed on component change.")
|
||||
|
||||
(p (code "page-render-plan") " in deps.sx computes per-page boundary decisions:")
|
||||
(~doc-code :code (highlight "(page-render-plan page-source env io-names)\n;; Returns:\n;; {:components {~name \"server\"|\"client\" ...}\n;; :server (list of server-expanded names)\n;; :client (list of client-rendered names)\n;; :io-deps (IO primitives needed by server components)}" "lisp"))
|
||||
|
||||
(~doc-subsection :title "Integration Points"
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (code "shared/sx/ref/deps.sx") " — " (code "page-render-plan") " spec function")
|
||||
(li (code "shared/sx/deps.py") " — Python wrapper, dispatches to bootstrapped code")
|
||||
(li (code "shared/sx/pages.py") " — " (code "compute_page_render_plans()") " called at mount time, caches on PageDef")
|
||||
(li (code "shared/sx/helpers.py") " — " (code "_build_pages_sx()") " includes " (code ":render-plan") " in client page registry")
|
||||
(li (code "shared/sx/types.py") " — " (code "PageDef.render_plan") " field")))
|
||||
|
||||
(~doc-subsection :title "Verification"
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li "5 new spec tests (page-render-plan suite)")
|
||||
(li "Render plans visible on " (a :href "/isomorphism/affinity" "affinity demo page"))
|
||||
(li "Client page registry includes :render-plan for each page"))))
|
||||
|
||||
(~doc-subsection :title "7c. Optimistic Data Updates"
|
||||
(p "Extend existing apply-optimistic/revert-optimistic in engine.sx from DOM-level to data-level. Client updates cached data optimistically, sends mutation to server, reverts on rejection."))
|
||||
|
||||
(~doc-subsection :title "7d. Offline Data Layer"
|
||||
(p "Service Worker intercepts /internal/data/ requests, serves from IndexedDB when offline, syncs when back online."))
|
||||
|
||||
(~doc-subsection :title "7e. Isomorphic Testing"
|
||||
|
||||
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
|
||||
(div :class "flex items-center gap-2 mb-1"
|
||||
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
|
||||
(p :class "text-green-800 text-sm" "Cross-host test suite: same SX expressions evaluated on Python (sx_ref.py) and JS (sx-browser.js via Node.js), HTML output compared."))
|
||||
|
||||
(p "61 isomorphic tests verify that Python and JS produce identical results:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li "37 eval tests: arithmetic, comparison, strings, collections, logic, let/lambda, higher-order, dict, keywords, cond/case")
|
||||
(li "24 render tests: elements, attributes, nesting, void elements, boolean attrs, conditionals, map, components, affinity, HTML escaping"))
|
||||
|
||||
(~doc-subsection :title "Files"
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
|
||||
(li "shared/sx/tests/test_isomorphic.py — cross-host test suite")
|
||||
(li "Run: " (code "python3 -m pytest shared/sx/tests/test_isomorphic.py -q")))))
|
||||
|
||||
(~doc-subsection :title "7f. Universal Page Descriptor"
|
||||
|
||||
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
|
||||
(div :class "flex items-center gap-2 mb-1"
|
||||
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
|
||||
(p :class "text-green-800 text-sm" "defpage is portable: same descriptor executes on server (execute_page) and client (tryClientRoute)."))
|
||||
|
||||
(p "The defpage descriptor is universal — the same definition works on both hosts:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (strong "Server: ") (code "execute_page()") " evaluates :data and :content slots, expands server components via " (code "_aser") ", returns SX wire format")
|
||||
(li (strong "Client: ") (code "try-client-route") " matches route, evaluates content SX, renders to DOM. Data pages fetch via " (code "/sx/data/") ", IO proxied via " (code "/sx/io/"))
|
||||
(li (strong "Render plan: ") "each page's " (code ":render-plan") " is included in the client page registry, showing which components render where")
|
||||
(li (strong "Console visibility: ") "client logs " (code "sx:route plan pagename — N server, M client") " on each navigation")))
|
||||
|
||||
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
|
||||
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "All previous phases.")))
|
||||
@@ -2221,130 +2295,6 @@
|
||||
(td :class "px-3 py-2 text-stone-700" "Add CI primitive declarations"))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CSSX Components
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plan-cssx-components-content ()
|
||||
(~doc-page :title "CSSX Components"
|
||||
|
||||
(~doc-section :title "Context" :id "context"
|
||||
(p "SX currently has a parallel CSS system: a style dictionary (JSON blob of atom-to-declaration mappings), a " (code "StyleValue") " type threaded through the evaluator and renderer, content-addressed hash class names (" (code "sx-a3f2b1") "), runtime CSS injection into " (code "<style id=\"sx-css\">") ", and a separate caching pipeline (" (code "<script type=\"text/sx-styles\">") ", localStorage, cookies).")
|
||||
(p "This is ~300 lines of spec code (cssx.sx) plus platform interface (hash, regex, injection), plus server-side infrastructure (css_registry.py, tw.css parsing). All to solve one problem: " (em "resolving keyword atoms like ") (code ":flex :gap-4 :hover:bg-sky-200") (em " into CSS at render time."))
|
||||
(p "The result: elements in the DOM get opaque class names like " (code "class=\"sx-a3f2b1\"") ". DevTools becomes useless. You can't inspect an element and understand its styling. " (strong "This is a deal breaker.")))
|
||||
|
||||
(~doc-section :title "The Idea" :id "idea"
|
||||
(p (strong "Styling is just components.") " A CSSX component is a regular " (code "defcomp") " that decides how to style its children. It might apply Tailwind classes, or hand-written CSS classes, or inline styles, or generate rules at runtime. The implementation is the component's private business. The consumer just calls " (code "(~btn :variant \"primary\" \"Submit\")") " and doesn't care.")
|
||||
(p "Because it's " (code "defcomp") ", you get everything for free: caching, bundling, dependency scanning, server/client rendering, composition. No parallel infrastructure.")
|
||||
(p "Key advantages:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (strong "Readable DOM: ") "Elements have real class names, not content-addressed hashes. DevTools works.")
|
||||
(li (strong "Data-driven styling: ") "Components receive data and decide styling. " (code "(~metric :value 150)") " renders red because " (code "value > 100") " — logic lives in the component, not a CSS preprocessor.")
|
||||
(li (strong "One system: ") "No separate " (code "StyleValue") " type, no style dictionary JSON, no " (code "<script type=\"text/sx-styles\">") ", no " (code "sx-css") " injection. Components ARE the styling abstraction.")
|
||||
(li (strong "One cache: ") "Component hash/localStorage handles everything. No separate style dict caching.")
|
||||
(li (strong "Composable: ") (code "(~card :elevated true (~metric :value v))") " — styling composes like any other component.")
|
||||
(li (strong "Strategy-agnostic: ") "A component can apply Tailwind classes, emit " (code "<style>") " blocks, use inline styles, generate CSS custom properties, or any combination. The consumer never knows or cares. Swap strategies without touching call sites.")))
|
||||
|
||||
(~doc-section :title "Examples" :id "examples"
|
||||
(~doc-subsection :title "Simple class mapping"
|
||||
(p "A button component that maps variant keywords to class strings:")
|
||||
(highlight
|
||||
"(defcomp ~btn (&key variant disabled &rest children)\n (button\n :class (str \"px-4 py-2 rounded font-medium transition \"\n (case variant\n \"primary\" \"bg-blue-600 text-white hover:bg-blue-700\"\n \"danger\" \"bg-red-600 text-white hover:bg-red-700\"\n \"ghost\" \"bg-transparent hover:bg-stone-100\"\n \"bg-stone-200 hover:bg-stone-300\")\n (when disabled \" opacity-50 cursor-not-allowed\"))\n :disabled disabled\n children))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-subsection :title "Data-driven styling"
|
||||
(p "Styling that responds to data values — impossible with static CSS:")
|
||||
(highlight
|
||||
"(defcomp ~metric (&key value label threshold)\n (let ((t (or threshold 10)))\n (div :class (str \"p-3 rounded font-bold \"\n (cond\n ((> value (* t 10)) \"bg-red-500 text-white\")\n ((> value t) \"bg-amber-200 text-amber-900\")\n (:else \"bg-green-100 text-green-800\")))\n (span :class \"text-sm\" label) \": \" (span (str value)))))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-subsection :title "Style functions"
|
||||
(p "Reusable style logic without wrapping — returns class strings:")
|
||||
(highlight
|
||||
"(define card-classes\n (fn (&key elevated bordered)\n (str \"rounded-lg p-4 \"\n (if elevated \"shadow-lg\" \"shadow-sm\")\n (when bordered \" border border-stone-200\"))))\n\n;; Usage: (div :class (card-classes :elevated true) ...)"
|
||||
"lisp"))
|
||||
|
||||
(~doc-subsection :title "Responsive and interactive"
|
||||
(p "Components can encode responsive breakpoints and interactive states as class strings — the same way you'd write Tailwind, but wrapped in a semantic component:")
|
||||
(highlight
|
||||
"(defcomp ~responsive-grid (&key cols &rest children)\n (div :class (str \"grid gap-4 \"\n (case (or cols 3)\n 1 \"grid-cols-1\"\n 2 \"grid-cols-1 md:grid-cols-2\"\n 3 \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n 4 \"grid-cols-2 md:grid-cols-3 lg:grid-cols-4\"))\n children))"
|
||||
"lisp"))
|
||||
|
||||
(~doc-subsection :title "Emitting CSS directly"
|
||||
(p "Components are not limited to referencing existing classes. They can generate CSS — " (code "<style>") " tags, keyframes, custom properties — as part of their output:")
|
||||
(highlight
|
||||
"(defcomp ~pulse (&key color duration &rest children)\n (<>\n (style (str \"@keyframes sx-pulse {\"\n \"0%,100% { opacity:1 } 50% { opacity:.5 } }\"))\n (div :style (str \"animation: sx-pulse \" (or duration \"2s\") \" infinite;\"\n \"color:\" (or color \"inherit\"))\n children)))\n\n(defcomp ~theme (&key primary surface &rest children)\n (<>\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\"))\n children))"
|
||||
"lisp")
|
||||
(p "The CSS strategy is the component's private implementation detail. Consumers call " (code "(~pulse :color \"red\" \"Loading...\")") " or " (code "(~theme :primary \"#2563eb\" ...)") " without knowing or caring whether the component uses classes, inline styles, generated rules, or all three.")))
|
||||
|
||||
(~doc-section :title "What Changes" :id "changes"
|
||||
|
||||
(~doc-subsection :title "Remove"
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (code "StyleValue") " type and all plumbing (type checks in eval, render, serialize)")
|
||||
(li (code "cssx.sx") " spec module (~300 lines: resolve-style, resolve-atom, split-variant, hash, injection)")
|
||||
(li "Style dictionary JSON format, loading, caching (" (code "<script type=\"text/sx-styles\">") ", " (code "initStyleDict") ", " (code "parseAndLoadStyleDict") ")")
|
||||
(li (code "<style id=\"sx-css\">") " runtime CSS injection system")
|
||||
(li (code "css_registry.py") " server-side (builds style dictionary from tw.css)")
|
||||
(li "Style dict cookies (" (code "sx-styles-hash") "), localStorage keys (" (code "sx-styles-src") ")")
|
||||
(li "Platform interface: " (code "fnv1a-hash") ", " (code "compile-regex") ", " (code "regex-match") ", " (code "regex-replace-groups") ", " (code "make-style-value") ", " (code "inject-style-value"))))
|
||||
|
||||
(~doc-subsection :title "Keep"
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (code "defstyle") " — already just " (code "(defstyle name expr)") " which binds name to a value. Stays as sugar for defining reusable style values/functions. No " (code "StyleValue") " type needed — the value can be a string, a function, anything.")
|
||||
(li (code "defkeyframes") " — could stay if we want declarative keyframe definitions. Or could become a component/function too.")
|
||||
(li (code "tw.css") " — the compiled Tailwind stylesheet. Components reference its classes directly. No runtime resolution needed.")
|
||||
(li (code ":class") " attribute — just takes strings now, no " (code "StyleValue") " special-casing.")))
|
||||
|
||||
(~doc-subsection :title "Add"
|
||||
(p "Nothing new to the spec. CSSX components are just " (code "defcomp") ". The only new thing is a convention: components whose primary purpose is styling. They live in the same component files, cache the same way, bundle the same way.")))
|
||||
|
||||
(~doc-section :title "Migration" :id "migration"
|
||||
(p "The existing codebase uses " (code ":class") " with plain Tailwind strings everywhere already. The CSSX style dictionary was an alternative path that was never widely adopted. Migration is mostly deletion:")
|
||||
(ol :class "list-decimal pl-5 space-y-2 text-stone-700"
|
||||
(li "Remove " (code "StyleValue") " type from " (code "types.py") ", " (code "render.sx") ", " (code "eval.sx") ", bootstrappers")
|
||||
(li "Remove " (code "cssx.sx") " from spec modules and bootstrapper")
|
||||
(li "Remove " (code "css_registry.py") " and style dict generation pipeline")
|
||||
(li "Remove style dict loading from " (code "boot.sx") " (" (code "initStyleDict") ", " (code "queryStyleScripts") ")")
|
||||
(li "Remove style-related cookies and localStorage from " (code "boot.sx") " platform interface")
|
||||
(li "Remove " (code "StyleValue") " special-casing from " (code "render-attrs") " in " (code "render.sx") " and DOM adapter")
|
||||
(li "Simplify " (code ":class") " / " (code ":style") " attribute handling — just strings")
|
||||
(li "Convert any existing " (code "defstyle") " uses to return plain class strings instead of " (code "StyleValue") " objects"))
|
||||
(p :class "mt-4 text-stone-600 italic" "Net effect: hundreds of lines of spec and infrastructure removed, zero new lines added. The component system already does everything CSSX was trying to do."))
|
||||
|
||||
(~doc-section :title "Relationship to Other Plans" :id "relationships"
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-700"
|
||||
(li (strong "Content-Addressed Components: ") "CSSX components get CIDs like any other component. A " (code "~btn") " from one site can be shared to another via IPFS. No " (code ":css-atoms") " manifest field needed — the component carries its own styling logic.")
|
||||
(li (strong "Isomorphic Rendering: ") "Components render the same on server and client. No style injection timing issues, no FOUC from late CSS loading.")
|
||||
(li (strong "Component Bundling: ") "deps.sx already handles transitive component deps. Style components are just more components in the bundle — no separate style bundling.")))
|
||||
|
||||
(~doc-section :title "Comparison with CSS Technologies" :id "comparison"
|
||||
(p "CSSX components share DNA with several existing approaches but avoid the problems that make each one painful at scale.")
|
||||
|
||||
(~doc-subsection :title "styled-components / Emotion"
|
||||
(p (a :href "https://styled-components.com" :class "text-violet-600 hover:underline" "styled-components") " pioneered the idea that styling belongs in components. But it generates CSS at runtime, injects " (code "<style>") " tags, and produces opaque hashed class names (" (code "class=\"sc-bdfBwQ fNMpVx\"") "). Open DevTools and you see gibberish. It also carries significant runtime cost — parsing CSS template literals, hashing, deduplicating — and needs a separate SSR extraction step (" (code "ServerStyleSheet") ").")
|
||||
(p "CSSX components share the core insight (" (em "styling is a component concern") ") but without the runtime machinery. When a component applies Tailwind classes, there's zero CSS generation overhead. When it does emit " (code "<style>") " blocks, it's explicit — not hidden behind a tagged template literal. And the DOM is always readable."))
|
||||
|
||||
(~doc-subsection :title "CSS Modules"
|
||||
(p (a :href "https://github.com/css-modules/css-modules" :class "text-violet-600 hover:underline" "CSS Modules") " scope class names to avoid collisions by rewriting them at build time: " (code ".button") " becomes " (code ".button_abc123") ". This solves the global namespace problem but creates the same opacity issue — hashed names in the DOM that you can't grep for or reason about.")
|
||||
(p "CSSX components don't need scoping because component boundaries already provide isolation. A " (code "~btn") " owns its markup. There's nothing to collide with."))
|
||||
|
||||
(~doc-subsection :title "Tailwind CSS"
|
||||
(p "Tailwind is " (em "complementary") ", not competitive. CSSX components are the semantic layer on top. Raw Tailwind in markup — " (code ":class \"px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700\"") " — is powerful but verbose and duplicated across call sites.")
|
||||
(p "A CSSX component wraps that string once: " (code "(~btn :variant \"primary\" \"Submit\")") ". The Tailwind classes are still there, readable in DevTools, but consumers don't repeat them. This is the same pattern Tailwind's own docs recommend (" (em "\"extracting components\"") ") — CSSX components are just SX's native way of doing it."))
|
||||
|
||||
(~doc-subsection :title "Vanilla Extract"
|
||||
(p (a :href "https://vanilla-extract.style" :class "text-violet-600 hover:underline" "Vanilla Extract") " is zero-runtime CSS-in-JS: styles are written in TypeScript, compiled to static CSS at build time, and referenced by generated class names. It avoids the runtime cost of styled-components but still requires a build step, a bundler plugin, and TypeScript. The generated class names are again opaque.")
|
||||
(p "CSSX components need no build step for styling — they're evaluated at render time like any other component. And since the component chooses its own strategy, it can reference pre-built classes (zero runtime) " (em "or") " generate CSS on the fly — same API either way."))
|
||||
|
||||
(~doc-subsection :title "Design Tokens / Style Dictionary"
|
||||
(p "The " (a :href "https://amzn.github.io/style-dictionary/" :class "text-violet-600 hover:underline" "Style Dictionary") " pattern — a JSON/YAML file mapping token names to values, compiled to platform-specific output — is essentially what the old CSSX was. It's the industry standard for design systems.")
|
||||
(p "The problem is that it's a parallel system: separate file format, separate build pipeline, separate caching, separate tooling. CSSX components eliminate all of that by expressing tokens as component parameters: " (code "(~theme :primary \"#7c3aed\")") " instead of " (code "{\"color\": {\"primary\": {\"value\": \"#7c3aed\"}}}") ". Same result, no parallel infrastructure.")))
|
||||
|
||||
(~doc-section :title "Philosophy" :id "philosophy"
|
||||
(p "The web has spent two decades building increasingly complex CSS tooling: preprocessors, CSS-in-JS, atomic CSS, utility frameworks, design tokens, style dictionaries. Each solves a real problem but adds a new system with its own caching, bundling, and mental model.")
|
||||
(p "CSSX components collapse all of this back to the simplest possible thing: " (strong "a function that takes data and returns markup with classes.") " That's what a component already is. There is no separate styling system because there doesn't need to be."))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Live Streaming — SSE & WebSocket
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
:prims (~doc-primitives-tables :primitives (primitives-data)))
|
||||
"special-forms" (~docs-special-forms-content
|
||||
:forms (~doc-special-forms-tables :forms (special-forms-data)))
|
||||
"css" (~docs-css-content)
|
||||
"server-rendering" (~docs-server-rendering-content)
|
||||
:else (~docs-introduction-content)))
|
||||
|
||||
@@ -286,6 +285,40 @@
|
||||
"no-alternative" (~essay-no-alternative)
|
||||
:else (~essays-index-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CSSX section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage cssx-index
|
||||
:path "/cssx/"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "CSSX"
|
||||
:sub-label "CSSX"
|
||||
:sub-href "/cssx/"
|
||||
:sub-nav (~section-nav :items cssx-nav-items :current "Overview")
|
||||
:selected "Overview")
|
||||
:content (~cssx-overview-content))
|
||||
|
||||
(defpage cssx-page
|
||||
:path "/cssx/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "CSSX"
|
||||
:sub-label "CSSX"
|
||||
:sub-href "/cssx/"
|
||||
:sub-nav (~section-nav :items cssx-nav-items
|
||||
:current (find-current cssx-nav-items slug))
|
||||
:selected (or (find-current cssx-nav-items slug) ""))
|
||||
:content (case slug
|
||||
"patterns" (~cssx-patterns-content)
|
||||
"delivery" (~cssx-delivery-content)
|
||||
"async" (~cssx-async-content)
|
||||
"live" (~cssx-live-content)
|
||||
"comparisons" (~cssx-comparison-content)
|
||||
"philosophy" (~cssx-philosophy-content)
|
||||
:else (~cssx-overview-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Specs section
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -479,6 +512,18 @@
|
||||
:stream-message stream-message
|
||||
:stream-time stream-time))
|
||||
|
||||
(defpage affinity-demo
|
||||
:path "/isomorphism/affinity"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Isomorphism"
|
||||
:sub-label "Isomorphism"
|
||||
:sub-href "/isomorphism/"
|
||||
:sub-nav (~section-nav :items isomorphism-nav-items :current "Affinity")
|
||||
:selected "Affinity")
|
||||
:data (affinity-demo-data)
|
||||
:content (~affinity-demo-content :components components :page-plans page-plans))
|
||||
|
||||
;; Wildcard must come AFTER specific routes (first-match routing)
|
||||
(defpage isomorphism-page
|
||||
:path "/isomorphism/<slug>"
|
||||
@@ -534,7 +579,6 @@
|
||||
"glue-decoupling" (~plan-glue-decoupling-content)
|
||||
"social-sharing" (~plan-social-sharing-content)
|
||||
"sx-ci" (~plan-sx-ci-content)
|
||||
"cssx-components" (~plan-cssx-components-content)
|
||||
"live-streaming" (~plan-live-streaming-content)
|
||||
:else (~plans-index-content)))
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ def _register_sx_helpers() -> None:
|
||||
"run-spec-tests": _run_spec_tests,
|
||||
"run-modular-tests": _run_modular_tests,
|
||||
"streaming-demo-data": _streaming_demo_data,
|
||||
"affinity-demo-data": _affinity_demo_data,
|
||||
})
|
||||
|
||||
|
||||
@@ -75,7 +76,7 @@ def _special_forms_data() -> dict:
|
||||
"filter": "Higher-Order Forms", "reduce": "Higher-Order Forms",
|
||||
"some": "Higher-Order Forms", "every?": "Higher-Order Forms",
|
||||
"for-each": "Higher-Order Forms",
|
||||
"defstyle": "Domain Definitions", "defkeyframes": "Domain Definitions",
|
||||
"defstyle": "Domain Definitions",
|
||||
"defhandler": "Domain Definitions", "defpage": "Domain Definitions",
|
||||
"defquery": "Domain Definitions", "defaction": "Domain Definitions",
|
||||
}
|
||||
@@ -318,6 +319,8 @@ def _bundle_analyzer_data() -> dict:
|
||||
comp_details.append({
|
||||
"name": comp_name,
|
||||
"is-pure": is_pure,
|
||||
"affinity": val.affinity,
|
||||
"render-target": val.render_target,
|
||||
"io-refs": sorted(val.io_refs),
|
||||
"deps": sorted(val.deps),
|
||||
"source": source,
|
||||
@@ -875,3 +878,47 @@ async def _streaming_demo_data():
|
||||
"stream-message": "Model inference completed in ~5 seconds",
|
||||
"stream-time": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
}
|
||||
|
||||
|
||||
def _affinity_demo_data() -> dict:
|
||||
"""Return affinity analysis for the demo components + page render plans."""
|
||||
from shared.sx.jinja_bridge import get_component_env
|
||||
from shared.sx.types import Component
|
||||
from shared.sx.pages import get_all_pages
|
||||
|
||||
env = get_component_env()
|
||||
demo_names = [
|
||||
"~aff-demo-auto",
|
||||
"~aff-demo-client",
|
||||
"~aff-demo-server",
|
||||
"~aff-demo-io-auto",
|
||||
"~aff-demo-io-client",
|
||||
]
|
||||
components = []
|
||||
for name in demo_names:
|
||||
val = env.get(name)
|
||||
if isinstance(val, Component):
|
||||
components.append({
|
||||
"name": name,
|
||||
"affinity": val.affinity,
|
||||
"render-target": val.render_target,
|
||||
"io-refs": sorted(val.io_refs),
|
||||
"is-pure": val.is_pure,
|
||||
})
|
||||
|
||||
# Collect render plans from all sx service pages
|
||||
page_plans = []
|
||||
for page_def in get_all_pages("sx").values():
|
||||
plan = page_def.render_plan
|
||||
if plan:
|
||||
page_plans.append({
|
||||
"name": page_def.name,
|
||||
"path": page_def.path,
|
||||
"server-count": len(plan.get("server", [])),
|
||||
"client-count": len(plan.get("client", [])),
|
||||
"server": plan.get("server", []),
|
||||
"client": plan.get("client", []),
|
||||
"io-deps": plan.get("io-deps", []),
|
||||
})
|
||||
|
||||
return {"components": components, "page-plans": page_plans}
|
||||
|
||||
Reference in New Issue
Block a user