Page helpers demo: defisland, map-in-children fix, _eval_slot ref evaluator
- Add page-helpers-demo page with defisland ~demo-client-runner (pure SX, zero JS files) showing spec functions running on both server and client - Fix _aser_component children serialization: flatten list results from map instead of serialize(list) which wraps in parens creating ((div ...) ...) that re-parses as invalid function call. Fixed in adapter-async.sx spec and async_eval_ref.py - Switch _eval_slot to use async_eval_ref.py when SX_USE_REF=1 (was hardcoded to async_eval.py) - Add Island type support to async_eval_ref.py: import, SSR rendering, aser dispatch, thread-first, defisland in _ASER_FORMS - Add server affinity check: components with :affinity :server expand even when _expand_components is False - Add diagnostic _aser_stack context to EvalError messages - New spec files: adapter-async.sx, page-helpers.sx, platform_js.py - Bootstrappers: page-helpers module support, performance.now() timing - 0-arity lambda event handler fix in adapter-dom.sx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-11T04:41:27Z";
|
||||
var SX_VERSION = "2026-03-11T13:57:48Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -974,8 +974,8 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var head = first(template);
|
||||
return (isSxTruthy((isSxTruthy((typeOf(head) == "symbol")) && (symbolName(head) == "unquote"))) ? trampoline(evalExpr(nth(template, 1), env)) : reduce(function(result, item) { return (isSxTruthy((isSxTruthy((typeOf(item) == "list")) && isSxTruthy((len(item) == 2)) && isSxTruthy((typeOf(first(item)) == "symbol")) && (symbolName(first(item)) == "splice-unquote"))) ? (function() {
|
||||
var spliced = trampoline(evalExpr(nth(item, 1), env));
|
||||
return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : append(result, spliced)));
|
||||
})() : append(result, qqExpand(item, env))); }, [], template));
|
||||
return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : concat(result, [spliced])));
|
||||
})() : concat(result, [qqExpand(item, env)])); }, [], template));
|
||||
})())); };
|
||||
|
||||
// sf-thread-first
|
||||
@@ -1658,7 +1658,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
var attrExpr = nth(args, (get(state, "i") + 1));
|
||||
(isSxTruthy(startsWith(attrName, "on-")) ? (function() {
|
||||
var attrVal = trampoline(evalExpr(attrExpr, env));
|
||||
return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), attrVal) : NIL);
|
||||
return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), (isSxTruthy((isSxTruthy(isLambda(attrVal)) && (len(lambdaParams(attrVal)) == 0))) ? function(e) { return callLambda(attrVal, [], lambdaClosure(attrVal)); } : attrVal)) : NIL);
|
||||
})() : (isSxTruthy((attrName == "bind")) ? (function() {
|
||||
var attrVal = trampoline(evalExpr(attrExpr, env));
|
||||
return (isSxTruthy(isSignal(attrVal)) ? bindInput(el, attrVal) : NIL);
|
||||
@@ -3433,6 +3433,125 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
})(); }, keys(env)); };
|
||||
|
||||
|
||||
// === Transpiled from page-helpers (pure data transformation helpers) ===
|
||||
|
||||
// special-form-category-map
|
||||
var specialFormCategoryMap = {"if": "Control Flow", "when": "Control Flow", "cond": "Control Flow", "case": "Control Flow", "and": "Control Flow", "or": "Control Flow", "let": "Binding", "let*": "Binding", "letrec": "Binding", "define": "Binding", "set!": "Binding", "lambda": "Functions & Components", "fn": "Functions & Components", "defcomp": "Functions & Components", "defmacro": "Functions & Components", "begin": "Sequencing & Threading", "do": "Sequencing & Threading", "->": "Sequencing & Threading", "quote": "Quoting", "quasiquote": "Quoting", "reset": "Continuations", "shift": "Continuations", "dynamic-wind": "Guards", "map": "Higher-Order Forms", "map-indexed": "Higher-Order Forms", "filter": "Higher-Order Forms", "reduce": "Higher-Order Forms", "some": "Higher-Order Forms", "every?": "Higher-Order Forms", "for-each": "Higher-Order Forms", "defstyle": "Domain Definitions", "defhandler": "Domain Definitions", "defpage": "Domain Definitions", "defquery": "Domain Definitions", "defaction": "Domain Definitions"};
|
||||
|
||||
// extract-define-kwargs
|
||||
var extractDefineKwargs = function(expr) { return (function() {
|
||||
var result = {};
|
||||
var items = slice(expr, 2);
|
||||
var n = len(items);
|
||||
{ var _c = range(0, n); for (var _i = 0; _i < _c.length; _i++) { var idx = _c[_i]; if (isSxTruthy((isSxTruthy(((idx + 1) < n)) && (typeOf(nth(items, idx)) == "keyword")))) {
|
||||
(function() {
|
||||
var key = keywordName(nth(items, idx));
|
||||
var val = nth(items, (idx + 1));
|
||||
return dictSet(result, key, (isSxTruthy((typeOf(val) == "list")) ? (String("(") + String(join(" ", map(serialize, val))) + String(")")) : (String(val))));
|
||||
})();
|
||||
} } }
|
||||
return result;
|
||||
})(); };
|
||||
|
||||
// categorize-special-forms
|
||||
var categorizeSpecialForms = function(parsedExprs) { return (function() {
|
||||
var categories = {};
|
||||
{ var _c = parsedExprs; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(expr) == "list")) && isSxTruthy((len(expr) >= 2)) && isSxTruthy((typeOf(first(expr)) == "symbol")) && (symbolName(first(expr)) == "define-special-form")))) {
|
||||
(function() {
|
||||
var name = nth(expr, 1);
|
||||
var kwargs = extractDefineKwargs(expr);
|
||||
var category = sxOr(get(specialFormCategoryMap, name), "Other");
|
||||
if (isSxTruthy(!isSxTruthy(dictHas(categories, category)))) {
|
||||
categories[category] = [];
|
||||
}
|
||||
return append_b(get(categories, category), {"name": name, "syntax": sxOr(get(kwargs, "syntax"), ""), "doc": sxOr(get(kwargs, "doc"), ""), "tail-position": sxOr(get(kwargs, "tail-position"), ""), "example": sxOr(get(kwargs, "example"), "")});
|
||||
})();
|
||||
} } }
|
||||
return categories;
|
||||
})(); };
|
||||
|
||||
// build-ref-items-with-href
|
||||
var buildRefItemsWithHref = function(items, basePath, detailKeys, nFields) { return map(function(item) { return (isSxTruthy((nFields == 3)) ? (function() {
|
||||
var name = nth(item, 0);
|
||||
var field2 = nth(item, 1);
|
||||
var field3 = nth(item, 2);
|
||||
return {"name": name, "desc": field2, "exists": field3, "href": (isSxTruthy((isSxTruthy(field3) && some(function(k) { return (k == name); }, detailKeys))) ? (String(basePath) + String(name)) : NIL)};
|
||||
})() : (function() {
|
||||
var name = nth(item, 0);
|
||||
var desc = nth(item, 1);
|
||||
return {"name": name, "desc": desc, "href": (isSxTruthy(some(function(k) { return (k == name); }, detailKeys)) ? (String(basePath) + String(name)) : NIL)};
|
||||
})()); }, items); };
|
||||
|
||||
// build-reference-data
|
||||
var buildReferenceData = function(slug, rawData, detailKeys) { return (function() { var _m = slug; if (_m == "attributes") return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3)}; if (_m == "headers") return {"req-headers": buildRefItemsWithHref(get(rawData, "req-headers"), "/hypermedia/reference/headers/", detailKeys, 3), "resp-headers": buildRefItemsWithHref(get(rawData, "resp-headers"), "/hypermedia/reference/headers/", detailKeys, 3)}; if (_m == "events") return {"events-list": buildRefItemsWithHref(get(rawData, "events-list"), "/hypermedia/reference/events/", detailKeys, 2)}; if (_m == "js-api") return {"js-api-list": map(function(item) { return {"name": nth(item, 0), "desc": nth(item, 1)}; }, get(rawData, "js-api-list"))}; return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3)}; })(); };
|
||||
|
||||
// build-attr-detail
|
||||
var buildAttrDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"attr-not-found": true} : {"attr-not-found": NIL, "attr-title": slug, "attr-description": get(detail, "description"), "attr-example": get(detail, "example"), "attr-handler": get(detail, "handler"), "attr-demo": get(detail, "demo"), "attr-wire-id": (isSxTruthy(dictHas(detail, "handler")) ? (String("ref-wire-") + String(replace_(replace_(slug, ":", "-"), "*", "star"))) : NIL)}); };
|
||||
|
||||
// build-header-detail
|
||||
var buildHeaderDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"header-not-found": true} : {"header-not-found": NIL, "header-title": slug, "header-direction": get(detail, "direction"), "header-description": get(detail, "description"), "header-example": get(detail, "example"), "header-demo": get(detail, "demo")}); };
|
||||
|
||||
// build-event-detail
|
||||
var buildEventDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"event-not-found": true} : {"event-not-found": NIL, "event-title": slug, "event-description": get(detail, "description"), "event-example": get(detail, "example"), "event-demo": get(detail, "demo")}); };
|
||||
|
||||
// build-component-source
|
||||
var buildComponentSource = function(compData) { return (function() {
|
||||
var compType = get(compData, "type");
|
||||
var name = get(compData, "name");
|
||||
var params = get(compData, "params");
|
||||
var hasChildren = get(compData, "has-children");
|
||||
var bodySx = get(compData, "body-sx");
|
||||
var affinity = get(compData, "affinity");
|
||||
return (isSxTruthy((compType == "not-found")) ? (String(";; component ") + String(name) + String(" not found")) : (function() {
|
||||
var paramStrs = (isSxTruthy(isEmpty(params)) ? (isSxTruthy(hasChildren) ? ["&rest", "children"] : []) : (isSxTruthy(hasChildren) ? append(cons("&key", params), ["&rest", "children"]) : cons("&key", params)));
|
||||
var paramsSx = (String("(") + String(join(" ", paramStrs)) + String(")"));
|
||||
var formName = (isSxTruthy((compType == "island")) ? "defisland" : "defcomp");
|
||||
var affinityStr = (isSxTruthy((isSxTruthy((compType == "component")) && isSxTruthy(!isSxTruthy(isNil(affinity))) && !isSxTruthy((affinity == "auto")))) ? (String(" :affinity ") + String(affinity)) : "");
|
||||
return (String("(") + String(formName) + String(" ") + String(name) + String(" ") + String(paramsSx) + String(affinityStr) + String("\n ") + String(bodySx) + String(")"));
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// build-bundle-analysis
|
||||
var buildBundleAnalysis = function(pagesRaw, componentsRaw, totalComponents, totalMacros, pureCount, ioCount) { return (function() {
|
||||
var pagesData = [];
|
||||
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
|
||||
var neededNames = get(page, "needed-names");
|
||||
var n = len(neededNames);
|
||||
var pct = (isSxTruthy((totalComponents > 0)) ? round(((n / totalComponents) * 100)) : 0);
|
||||
var savings = (100 - pct);
|
||||
var pureInPage = 0;
|
||||
var ioInPage = 0;
|
||||
var pageIoRefs = [];
|
||||
var compDetails = [];
|
||||
{ var _c = neededNames; for (var _i = 0; _i < _c.length; _i++) { var compName = _c[_i]; (function() {
|
||||
var info = get(componentsRaw, compName);
|
||||
return (isSxTruthy(!isSxTruthy(isNil(info))) ? ((isSxTruthy(get(info, "is-pure")) ? (pureInPage = (pureInPage + 1)) : ((ioInPage = (ioInPage + 1)), forEach(function(ref) { return (isSxTruthy(!isSxTruthy(some(function(r) { return (r == ref); }, pageIoRefs))) ? append_b(pageIoRefs, ref) : NIL); }, sxOr(get(info, "io-refs"), [])))), append_b(compDetails, {"name": compName, "is-pure": get(info, "is-pure"), "affinity": get(info, "affinity"), "render-target": get(info, "render-target"), "io-refs": sxOr(get(info, "io-refs"), []), "deps": sxOr(get(info, "deps"), []), "source": get(info, "source")})) : NIL);
|
||||
})(); } }
|
||||
return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "direct": get(page, "direct"), "needed": n, "pct": pct, "savings": savings, "io-refs": len(pageIoRefs), "pure-in-page": pureInPage, "io-in-page": ioInPage, "components": compDetails});
|
||||
})(); } }
|
||||
return {"pages": pagesData, "total-components": totalComponents, "total-macros": totalMacros, "pure-count": pureCount, "io-count": ioCount};
|
||||
})(); };
|
||||
|
||||
// build-routing-analysis
|
||||
var buildRoutingAnalysis = function(pagesRaw) { return (function() {
|
||||
var pagesData = [];
|
||||
var clientCount = 0;
|
||||
var serverCount = 0;
|
||||
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
|
||||
var hasData = get(page, "has-data");
|
||||
var contentSrc = sxOr(get(page, "content-src"), "");
|
||||
var mode = NIL;
|
||||
var reason = "";
|
||||
(isSxTruthy(hasData) ? ((mode = "server"), (reason = "Has :data expression — needs server IO"), (serverCount = (serverCount + 1))) : (isSxTruthy(isEmpty(contentSrc)) ? ((mode = "server"), (reason = "No content expression"), (serverCount = (serverCount + 1))) : ((mode = "client"), (clientCount = (clientCount + 1)))));
|
||||
return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "mode": mode, "has-data": hasData, "content-expr": (isSxTruthy((len(contentSrc) > 80)) ? (String(slice(contentSrc, 0, 80)) + String("...")) : contentSrc), "reason": reason});
|
||||
})(); } }
|
||||
return {"pages": pagesData, "total-pages": (clientCount + serverCount), "client-count": clientCount, "server-count": serverCount};
|
||||
})(); };
|
||||
|
||||
// build-affinity-analysis
|
||||
var buildAffinityAnalysis = function(demoComponents, pagePlans) { return {"components": demoComponents, "page-plans": pagePlans}; };
|
||||
|
||||
|
||||
// === Transpiled from router (client-side route matching) ===
|
||||
|
||||
// split-path-segments
|
||||
@@ -3947,7 +4066,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
function nowMs() { return (typeof performance !== "undefined") ? performance.now() : Date.now(); }
|
||||
|
||||
function parseHeaderValue(s) {
|
||||
if (!s) return null;
|
||||
@@ -5060,6 +5179,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue;
|
||||
if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml;
|
||||
if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml;
|
||||
if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent;
|
||||
if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse;
|
||||
if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs;
|
||||
PRIMITIVES["sx-parse"] = sxParse;
|
||||
|
||||
// Expose deps module functions as primitives so runtime-evaluated SX code
|
||||
// (e.g. test-deps.sx in browser) can call them
|
||||
@@ -5090,6 +5213,19 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
PRIMITIVES["render-target"] = renderTarget;
|
||||
PRIMITIVES["page-render-plan"] = pageRenderPlan;
|
||||
|
||||
// Expose page-helper functions as primitives
|
||||
PRIMITIVES["categorize-special-forms"] = categorizeSpecialForms;
|
||||
PRIMITIVES["extract-define-kwargs"] = extractDefineKwargs;
|
||||
PRIMITIVES["build-reference-data"] = buildReferenceData;
|
||||
PRIMITIVES["build-ref-items-with-href"] = buildRefItemsWithHref;
|
||||
PRIMITIVES["build-attr-detail"] = buildAttrDetail;
|
||||
PRIMITIVES["build-header-detail"] = buildHeaderDetail;
|
||||
PRIMITIVES["build-event-detail"] = buildEventDetail;
|
||||
PRIMITIVES["build-component-source"] = buildComponentSource;
|
||||
PRIMITIVES["build-bundle-analysis"] = buildBundleAnalysis;
|
||||
PRIMITIVES["build-routing-analysis"] = buildRoutingAnalysis;
|
||||
PRIMITIVES["build-affinity-analysis"] = buildAffinityAnalysis;
|
||||
|
||||
// =========================================================================
|
||||
// Async IO: Promise-aware rendering for client-side IO primitives
|
||||
// =========================================================================
|
||||
@@ -5823,6 +5959,15 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
transitiveIoRefs: transitiveIoRefs,
|
||||
computeAllIoRefs: computeAllIoRefs,
|
||||
componentPure_p: componentPure_p,
|
||||
categorizeSpecialForms: categorizeSpecialForms,
|
||||
buildReferenceData: buildReferenceData,
|
||||
buildAttrDetail: buildAttrDetail,
|
||||
buildHeaderDetail: buildHeaderDetail,
|
||||
buildEventDetail: buildEventDetail,
|
||||
buildComponentSource: buildComponentSource,
|
||||
buildBundleAnalysis: buildBundleAnalysis,
|
||||
buildRoutingAnalysis: buildRoutingAnalysis,
|
||||
buildAffinityAnalysis: buildAffinityAnalysis,
|
||||
splitPathSegments: splitPathSegments,
|
||||
parseRoutePattern: parseRoutePattern,
|
||||
matchRoute: matchRoute,
|
||||
|
||||
@@ -170,7 +170,11 @@ async def _eval_slot(expr: Any, env: dict, ctx: Any) -> str:
|
||||
Expands component calls (so IO in the body executes) but serializes
|
||||
the result as SX wire format, not HTML.
|
||||
"""
|
||||
from .async_eval import async_eval_slot_to_sx
|
||||
import os
|
||||
if os.environ.get("SX_USE_REF") == "1":
|
||||
from .ref.async_eval_ref import async_eval_slot_to_sx
|
||||
else:
|
||||
from .async_eval import async_eval_slot_to_sx
|
||||
return await async_eval_slot_to_sx(expr, env, ctx)
|
||||
|
||||
|
||||
|
||||
1198
shared/sx/ref/adapter-async.sx
Normal file
1198
shared/sx/ref/adapter-async.sx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -186,10 +186,15 @@
|
||||
(attr-expr (nth args (inc (get state "i")))))
|
||||
(cond
|
||||
;; Event handler: evaluate eagerly, bind listener
|
||||
;; If handler is a 0-arity lambda, wrap to ignore the event arg
|
||||
(starts-with? attr-name "on-")
|
||||
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
||||
(when (callable? attr-val)
|
||||
(dom-listen el (slice attr-name 3) attr-val)))
|
||||
(dom-listen el (slice attr-name 3)
|
||||
(if (and (lambda? attr-val)
|
||||
(= (len (lambda-params attr-val)) 0))
|
||||
(fn (e) (call-lambda attr-val (list) (lambda-closure attr-val)))
|
||||
attr-val))))
|
||||
;; Two-way input binding: :bind signal
|
||||
(= attr-name "bind")
|
||||
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
||||
|
||||
@@ -26,7 +26,7 @@ import contextvars
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from ..types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from ..types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from ..parser import SxExpr, serialize
|
||||
from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io
|
||||
from ..html import (
|
||||
@@ -210,9 +210,11 @@ async def _arender_list(expr, env, ctx):
|
||||
if name in HTML_TAGS:
|
||||
return await _arender_element(name, expr[1:], env, ctx)
|
||||
|
||||
# Component
|
||||
# Component / Island
|
||||
if name.startswith("~"):
|
||||
val = env.get(name)
|
||||
if isinstance(val, Island):
|
||||
return sx_ref.render_html_island(val, expr[1:], env)
|
||||
if isinstance(val, Component):
|
||||
return await _arender_component(val, expr[1:], env, ctx)
|
||||
|
||||
@@ -455,6 +457,7 @@ _ASYNC_RENDER_FORMS = {
|
||||
"defcomp": _arsf_define,
|
||||
"defmacro": _arsf_define,
|
||||
"defhandler": _arsf_define,
|
||||
"defisland": _arsf_define,
|
||||
"map": _arsf_map,
|
||||
"map-indexed": _arsf_map_indexed,
|
||||
"filter": _arsf_filter,
|
||||
@@ -505,6 +508,8 @@ async def _eval_slot_inner(expr, env, ctx):
|
||||
if isinstance(result, str):
|
||||
return SxExpr(result)
|
||||
return SxExpr(serialize(result))
|
||||
elif isinstance(comp, Island):
|
||||
pass # Islands serialize as SX for client hydration
|
||||
result = await _aser(expr, env, ctx)
|
||||
result = await _maybe_expand_component_result(result, env, ctx)
|
||||
if isinstance(result, SxExpr):
|
||||
@@ -530,6 +535,9 @@ async def _maybe_expand_component_result(result, env, ctx):
|
||||
return result
|
||||
|
||||
|
||||
_aser_stack: list[str] = [] # diagnostic: track expression context
|
||||
|
||||
|
||||
async def _aser(expr, env, ctx):
|
||||
"""Evaluate for SX wire format — serialize rendering forms, evaluate control flow."""
|
||||
if isinstance(expr, (int, float, bool)):
|
||||
@@ -553,7 +561,8 @@ async def _aser(expr, env, ctx):
|
||||
return False
|
||||
if name == "nil":
|
||||
return NIL
|
||||
raise EvalError(f"Undefined symbol: {name}")
|
||||
ctx_info = " → ".join(_aser_stack[-5:]) if _aser_stack else "(top)"
|
||||
raise EvalError(f"Undefined symbol: {name} [aser context: {ctx_info}]")
|
||||
|
||||
if isinstance(expr, Keyword):
|
||||
return expr.name
|
||||
@@ -590,7 +599,7 @@ async def _aser(expr, env, ctx):
|
||||
if name.startswith("html:"):
|
||||
return await _aser_call(name[5:], expr[1:], env, ctx)
|
||||
|
||||
# Component call
|
||||
# Component / Island call
|
||||
if name.startswith("~"):
|
||||
val = env.get(name)
|
||||
if isinstance(val, Macro):
|
||||
@@ -598,7 +607,10 @@ async def _aser(expr, env, ctx):
|
||||
sx_ref.expand_macro(val, expr[1:], env)
|
||||
)
|
||||
return await _aser(expanded, env, ctx)
|
||||
if isinstance(val, Component) and _expand_components.get():
|
||||
if isinstance(val, Component) and (
|
||||
_expand_components.get()
|
||||
or getattr(val, "render_target", None) == "server"
|
||||
):
|
||||
return await _aser_component(val, expr[1:], env, ctx)
|
||||
return await _aser_call(name, expr[1:], env, ctx)
|
||||
|
||||
@@ -633,11 +645,11 @@ async def _aser(expr, env, ctx):
|
||||
if _svg_context.get(False):
|
||||
return await _aser_call(name, expr[1:], env, ctx)
|
||||
|
||||
# Function/lambda call
|
||||
# Function/lambda call — fallback: evaluate head as callable
|
||||
fn = await async_eval(head, env, ctx)
|
||||
args = [await async_eval(a, env, ctx) for a in expr[1:]]
|
||||
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
|
||||
result = fn(*args)
|
||||
if inspect.iscoroutine(result):
|
||||
return await result
|
||||
@@ -650,7 +662,9 @@ async def _aser(expr, env, ctx):
|
||||
return await _aser(fn.body, local, ctx)
|
||||
if isinstance(fn, Component):
|
||||
return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
|
||||
raise EvalError(f"Not callable: {fn!r}")
|
||||
if isinstance(fn, Island):
|
||||
return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
|
||||
raise EvalError(f"Not callable in aser: {fn!r} (expr head: {head!r})")
|
||||
|
||||
|
||||
async def _aser_fragment(children, env, ctx):
|
||||
@@ -669,28 +683,41 @@ async def _aser_fragment(children, env, ctx):
|
||||
|
||||
|
||||
async def _aser_component(comp, args, env, ctx):
|
||||
kwargs = {}
|
||||
children = []
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||
kwargs[arg.name] = await _aser(args[i + 1], env, ctx)
|
||||
i += 2
|
||||
else:
|
||||
children.append(arg)
|
||||
i += 1
|
||||
local = dict(comp.closure)
|
||||
local.update(env)
|
||||
for p in comp.params:
|
||||
local[p] = kwargs.get(p, NIL)
|
||||
if comp.has_children:
|
||||
child_parts = [serialize(await _aser(c, env, ctx)) for c in children]
|
||||
local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
|
||||
return await _aser(comp.body, local, ctx)
|
||||
_aser_stack.append(f"~{comp.name}")
|
||||
try:
|
||||
kwargs = {}
|
||||
children = []
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||
kwargs[arg.name] = await _aser(args[i + 1], env, ctx)
|
||||
i += 2
|
||||
else:
|
||||
children.append(arg)
|
||||
i += 1
|
||||
local = dict(comp.closure)
|
||||
local.update(env)
|
||||
for p in comp.params:
|
||||
local[p] = kwargs.get(p, NIL)
|
||||
if comp.has_children:
|
||||
child_parts = []
|
||||
for c in children:
|
||||
result = await _aser(c, env, ctx)
|
||||
if isinstance(result, list):
|
||||
for item in result:
|
||||
if item is not NIL and item is not None:
|
||||
child_parts.append(serialize(item))
|
||||
elif result is not NIL and result is not None:
|
||||
child_parts.append(serialize(result))
|
||||
local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
|
||||
return await _aser(comp.body, local, ctx)
|
||||
finally:
|
||||
_aser_stack.pop()
|
||||
|
||||
|
||||
async def _aser_call(name, args, env, ctx):
|
||||
_aser_stack.append(name)
|
||||
token = None
|
||||
if name in ("svg", "math"):
|
||||
token = _svg_context.set(True)
|
||||
@@ -730,6 +757,7 @@ async def _aser_call(name, args, env, ctx):
|
||||
_merge_class_into_parts(parts, extra_class)
|
||||
return SxExpr("(" + " ".join(parts) + ")")
|
||||
finally:
|
||||
_aser_stack.pop()
|
||||
if token is not None:
|
||||
_svg_context.reset(token)
|
||||
|
||||
@@ -885,7 +913,7 @@ async def _assf_thread_first(expr, env, ctx):
|
||||
else:
|
||||
fn = await async_eval(form, env, ctx)
|
||||
fn_args = [result]
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
|
||||
result = fn(*fn_args)
|
||||
if inspect.iscoroutine(result):
|
||||
result = await result
|
||||
@@ -981,6 +1009,7 @@ _ASER_FORMS = {
|
||||
"defcomp": _assf_define,
|
||||
"defmacro": _assf_define,
|
||||
"defhandler": _assf_define,
|
||||
"defisland": _assf_define,
|
||||
"begin": _assf_begin,
|
||||
"do": _assf_begin,
|
||||
"quote": _assf_quote,
|
||||
|
||||
@@ -1196,6 +1196,8 @@ def compile_ref_to_py(
|
||||
spec_mod_set.add("deps")
|
||||
if "signals" in SPEC_MODULES:
|
||||
spec_mod_set.add("signals")
|
||||
if "page-helpers" in SPEC_MODULES:
|
||||
spec_mod_set.add("page-helpers")
|
||||
has_deps = "deps" in spec_mod_set
|
||||
|
||||
# Core files always included, then selected adapters, then spec modules
|
||||
|
||||
368
shared/sx/ref/page-helpers.sx
Normal file
368
shared/sx/ref/page-helpers.sx
Normal file
@@ -0,0 +1,368 @@
|
||||
;; ==========================================================================
|
||||
;; page-helpers.sx — Pure data-transformation page helpers
|
||||
;;
|
||||
;; These functions take raw data (from Python I/O edge) and return
|
||||
;; structured dicts for page rendering. No I/O — pure transformations
|
||||
;; only. Bootstrapped to every host.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; categorize-special-forms
|
||||
;;
|
||||
;; Parses define-special-form declarations from special-forms.sx AST,
|
||||
;; categorizes each form by name lookup, returns dict of category → forms.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define special-form-category-map
|
||||
{"if" "Control Flow" "when" "Control Flow" "cond" "Control Flow"
|
||||
"case" "Control Flow" "and" "Control Flow" "or" "Control Flow"
|
||||
"let" "Binding" "let*" "Binding" "letrec" "Binding"
|
||||
"define" "Binding" "set!" "Binding"
|
||||
"lambda" "Functions & Components" "fn" "Functions & Components"
|
||||
"defcomp" "Functions & Components" "defmacro" "Functions & Components"
|
||||
"begin" "Sequencing & Threading" "do" "Sequencing & Threading"
|
||||
"->" "Sequencing & Threading"
|
||||
"quote" "Quoting" "quasiquote" "Quoting"
|
||||
"reset" "Continuations" "shift" "Continuations"
|
||||
"dynamic-wind" "Guards"
|
||||
"map" "Higher-Order Forms" "map-indexed" "Higher-Order Forms"
|
||||
"filter" "Higher-Order Forms" "reduce" "Higher-Order Forms"
|
||||
"some" "Higher-Order Forms" "every?" "Higher-Order Forms"
|
||||
"for-each" "Higher-Order Forms"
|
||||
"defstyle" "Domain Definitions"
|
||||
"defhandler" "Domain Definitions" "defpage" "Domain Definitions"
|
||||
"defquery" "Domain Definitions" "defaction" "Domain Definitions"})
|
||||
|
||||
|
||||
(define extract-define-kwargs
|
||||
(fn (expr)
|
||||
;; Extract keyword args from a define-special-form expression.
|
||||
;; Returns dict of keyword-name → string value.
|
||||
;; Walks items pairwise: when item[i] is a keyword, item[i+1] is its value.
|
||||
(let ((result {})
|
||||
(items (slice expr 2))
|
||||
(n (len items)))
|
||||
(for-each
|
||||
(fn (idx)
|
||||
(when (and (< (+ idx 1) n)
|
||||
(= (type-of (nth items idx)) "keyword"))
|
||||
(let ((key (keyword-name (nth items idx)))
|
||||
(val (nth items (+ idx 1))))
|
||||
(dict-set! result key
|
||||
(if (= (type-of val) "list")
|
||||
(str "(" (join " " (map serialize val)) ")")
|
||||
(str val))))))
|
||||
(range 0 n))
|
||||
result)))
|
||||
|
||||
|
||||
(define categorize-special-forms
|
||||
(fn (parsed-exprs)
|
||||
;; parsed-exprs: result of parse-all on special-forms.sx
|
||||
;; Returns dict of category-name → list of form dicts.
|
||||
(let ((categories {}))
|
||||
(for-each
|
||||
(fn (expr)
|
||||
(when (and (= (type-of expr) "list")
|
||||
(>= (len expr) 2)
|
||||
(= (type-of (first expr)) "symbol")
|
||||
(= (symbol-name (first expr)) "define-special-form"))
|
||||
(let ((name (nth expr 1))
|
||||
(kwargs (extract-define-kwargs expr))
|
||||
(category (or (get special-form-category-map name) "Other")))
|
||||
(when (not (has-key? categories category))
|
||||
(dict-set! categories category (list)))
|
||||
(append! (get categories category)
|
||||
{"name" name
|
||||
"syntax" (or (get kwargs "syntax") "")
|
||||
"doc" (or (get kwargs "doc") "")
|
||||
"tail-position" (or (get kwargs "tail-position") "")
|
||||
"example" (or (get kwargs "example") "")}))))
|
||||
parsed-exprs)
|
||||
categories)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-reference-data
|
||||
;;
|
||||
;; Takes a slug and raw reference data, returns structured dict for rendering.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-ref-items-with-href
|
||||
(fn (items base-path detail-keys n-fields)
|
||||
;; items: list of lists (tuples), each with n-fields elements
|
||||
;; base-path: e.g. "/hypermedia/reference/attributes/"
|
||||
;; detail-keys: list of strings (keys that have detail pages)
|
||||
;; n-fields: 2 or 3 (number of fields per tuple)
|
||||
(map
|
||||
(fn (item)
|
||||
(if (= n-fields 3)
|
||||
;; [name, desc/value, exists/desc]
|
||||
(let ((name (nth item 0))
|
||||
(field2 (nth item 1))
|
||||
(field3 (nth item 2)))
|
||||
{"name" name
|
||||
"desc" field2
|
||||
"exists" field3
|
||||
"href" (if (and field3 (some (fn (k) (= k name)) detail-keys))
|
||||
(str base-path name)
|
||||
nil)})
|
||||
;; [name, desc]
|
||||
(let ((name (nth item 0))
|
||||
(desc (nth item 1)))
|
||||
{"name" name
|
||||
"desc" desc
|
||||
"href" (if (some (fn (k) (= k name)) detail-keys)
|
||||
(str base-path name)
|
||||
nil)})))
|
||||
items)))
|
||||
|
||||
|
||||
(define build-reference-data
|
||||
(fn (slug raw-data detail-keys)
|
||||
;; slug: "attributes", "headers", "events", "js-api"
|
||||
;; raw-data: dict with the raw data lists for this slug
|
||||
;; detail-keys: list of names that have detail pages
|
||||
(case slug
|
||||
"attributes"
|
||||
{"req-attrs" (build-ref-items-with-href
|
||||
(get raw-data "req-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)
|
||||
"beh-attrs" (build-ref-items-with-href
|
||||
(get raw-data "beh-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)
|
||||
"uniq-attrs" (build-ref-items-with-href
|
||||
(get raw-data "uniq-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)}
|
||||
|
||||
"headers"
|
||||
{"req-headers" (build-ref-items-with-href
|
||||
(get raw-data "req-headers")
|
||||
"/hypermedia/reference/headers/" detail-keys 3)
|
||||
"resp-headers" (build-ref-items-with-href
|
||||
(get raw-data "resp-headers")
|
||||
"/hypermedia/reference/headers/" detail-keys 3)}
|
||||
|
||||
"events"
|
||||
{"events-list" (build-ref-items-with-href
|
||||
(get raw-data "events-list")
|
||||
"/hypermedia/reference/events/" detail-keys 2)}
|
||||
|
||||
"js-api"
|
||||
{"js-api-list" (map (fn (item) {"name" (nth item 0) "desc" (nth item 1)})
|
||||
(get raw-data "js-api-list"))}
|
||||
|
||||
;; default: attributes
|
||||
:else
|
||||
{"req-attrs" (build-ref-items-with-href
|
||||
(get raw-data "req-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)
|
||||
"beh-attrs" (build-ref-items-with-href
|
||||
(get raw-data "beh-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)
|
||||
"uniq-attrs" (build-ref-items-with-href
|
||||
(get raw-data "uniq-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-attr-detail / build-header-detail / build-event-detail
|
||||
;;
|
||||
;; Lookup a slug in a detail dict, reshape for page rendering.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-attr-detail
|
||||
(fn (slug detail)
|
||||
;; detail: dict with "description", "example", "handler", "demo" keys or nil
|
||||
(if (nil? detail)
|
||||
{"attr-not-found" true}
|
||||
{"attr-not-found" nil
|
||||
"attr-title" slug
|
||||
"attr-description" (get detail "description")
|
||||
"attr-example" (get detail "example")
|
||||
"attr-handler" (get detail "handler")
|
||||
"attr-demo" (get detail "demo")
|
||||
"attr-wire-id" (if (has-key? detail "handler")
|
||||
(str "ref-wire-"
|
||||
(replace (replace slug ":" "-") "*" "star"))
|
||||
nil)})))
|
||||
|
||||
|
||||
(define build-header-detail
|
||||
(fn (slug detail)
|
||||
(if (nil? detail)
|
||||
{"header-not-found" true}
|
||||
{"header-not-found" nil
|
||||
"header-title" slug
|
||||
"header-direction" (get detail "direction")
|
||||
"header-description" (get detail "description")
|
||||
"header-example" (get detail "example")
|
||||
"header-demo" (get detail "demo")})))
|
||||
|
||||
|
||||
(define build-event-detail
|
||||
(fn (slug detail)
|
||||
(if (nil? detail)
|
||||
{"event-not-found" true}
|
||||
{"event-not-found" nil
|
||||
"event-title" slug
|
||||
"event-description" (get detail "description")
|
||||
"event-example" (get detail "example")
|
||||
"event-demo" (get detail "demo")})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-component-source
|
||||
;;
|
||||
;; Reconstruct defcomp/defisland source from component metadata.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-component-source
|
||||
(fn (comp-data)
|
||||
;; comp-data: dict with "type", "name", "params", "has-children", "body-sx", "affinity"
|
||||
(let ((comp-type (get comp-data "type"))
|
||||
(name (get comp-data "name"))
|
||||
(params (get comp-data "params"))
|
||||
(has-children (get comp-data "has-children"))
|
||||
(body-sx (get comp-data "body-sx"))
|
||||
(affinity (get comp-data "affinity")))
|
||||
(if (= comp-type "not-found")
|
||||
(str ";; component " name " not found")
|
||||
(let ((param-strs (if (empty? params)
|
||||
(if has-children
|
||||
(list "&rest" "children")
|
||||
(list))
|
||||
(if has-children
|
||||
(append (cons "&key" params) (list "&rest" "children"))
|
||||
(cons "&key" params))))
|
||||
(params-sx (str "(" (join " " param-strs) ")"))
|
||||
(form-name (if (= comp-type "island") "defisland" "defcomp"))
|
||||
(affinity-str (if (and (= comp-type "component")
|
||||
(not (nil? affinity))
|
||||
(not (= affinity "auto")))
|
||||
(str " :affinity " affinity)
|
||||
"")))
|
||||
(str "(" form-name " " name " " params-sx affinity-str "\n " body-sx ")"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-bundle-analysis
|
||||
;;
|
||||
;; Compute per-page bundle stats from pre-extracted component data.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-bundle-analysis
|
||||
(fn (pages-raw components-raw total-components total-macros pure-count io-count)
|
||||
;; pages-raw: list of {:name :path :direct :needed-names}
|
||||
;; components-raw: dict of name → {:is-pure :affinity :render-target :io-refs :deps :source}
|
||||
(let ((pages-data (list)))
|
||||
(for-each
|
||||
(fn (page)
|
||||
(let ((needed-names (get page "needed-names"))
|
||||
(n (len needed-names))
|
||||
(pct (if (> total-components 0)
|
||||
(round (* (/ n total-components) 100))
|
||||
0))
|
||||
(savings (- 100 pct))
|
||||
(pure-in-page 0)
|
||||
(io-in-page 0)
|
||||
(page-io-refs (list))
|
||||
(comp-details (list)))
|
||||
;; Walk needed components
|
||||
(for-each
|
||||
(fn (comp-name)
|
||||
(let ((info (get components-raw comp-name)))
|
||||
(when (not (nil? info))
|
||||
(if (get info "is-pure")
|
||||
(set! pure-in-page (+ pure-in-page 1))
|
||||
(do
|
||||
(set! io-in-page (+ io-in-page 1))
|
||||
(for-each
|
||||
(fn (ref) (when (not (some (fn (r) (= r ref)) page-io-refs))
|
||||
(append! page-io-refs ref)))
|
||||
(or (get info "io-refs") (list)))))
|
||||
(append! comp-details
|
||||
{"name" comp-name
|
||||
"is-pure" (get info "is-pure")
|
||||
"affinity" (get info "affinity")
|
||||
"render-target" (get info "render-target")
|
||||
"io-refs" (or (get info "io-refs") (list))
|
||||
"deps" (or (get info "deps") (list))
|
||||
"source" (get info "source")}))))
|
||||
needed-names)
|
||||
(append! pages-data
|
||||
{"name" (get page "name")
|
||||
"path" (get page "path")
|
||||
"direct" (get page "direct")
|
||||
"needed" n
|
||||
"pct" pct
|
||||
"savings" savings
|
||||
"io-refs" (len page-io-refs)
|
||||
"pure-in-page" pure-in-page
|
||||
"io-in-page" io-in-page
|
||||
"components" comp-details})))
|
||||
pages-raw)
|
||||
{"pages" pages-data
|
||||
"total-components" total-components
|
||||
"total-macros" total-macros
|
||||
"pure-count" pure-count
|
||||
"io-count" io-count})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-routing-analysis
|
||||
;;
|
||||
;; Classify pages by routing mode (client vs server).
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-routing-analysis
|
||||
(fn (pages-raw)
|
||||
;; pages-raw: list of {:name :path :has-data :content-src}
|
||||
(let ((pages-data (list))
|
||||
(client-count 0)
|
||||
(server-count 0))
|
||||
(for-each
|
||||
(fn (page)
|
||||
(let ((has-data (get page "has-data"))
|
||||
(content-src (or (get page "content-src") ""))
|
||||
(mode nil)
|
||||
(reason ""))
|
||||
(cond
|
||||
has-data
|
||||
(do (set! mode "server")
|
||||
(set! reason "Has :data expression — needs server IO")
|
||||
(set! server-count (+ server-count 1)))
|
||||
(empty? content-src)
|
||||
(do (set! mode "server")
|
||||
(set! reason "No content expression")
|
||||
(set! server-count (+ server-count 1)))
|
||||
:else
|
||||
(do (set! mode "client")
|
||||
(set! client-count (+ client-count 1))))
|
||||
(append! pages-data
|
||||
{"name" (get page "name")
|
||||
"path" (get page "path")
|
||||
"mode" mode
|
||||
"has-data" has-data
|
||||
"content-expr" (if (> (len content-src) 80)
|
||||
(str (slice content-src 0 80) "...")
|
||||
content-src)
|
||||
"reason" reason})))
|
||||
pages-raw)
|
||||
{"pages" pages-data
|
||||
"total-pages" (+ client-count server-count)
|
||||
"client-count" client-count
|
||||
"server-count" server-count})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-affinity-analysis
|
||||
;;
|
||||
;; Package component affinity info + page render plans for display.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-affinity-analysis
|
||||
(fn (demo-components page-plans)
|
||||
{"components" demo-components
|
||||
"page-plans" page-plans}))
|
||||
3163
shared/sx/ref/platform_js.py
Normal file
3163
shared/sx/ref/platform_js.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1428,6 +1428,7 @@ SPEC_MODULES = {
|
||||
"router": ("router.sx", "router (client-side route matching)"),
|
||||
"engine": ("engine.sx", "engine (fetch/swap/trigger pure logic)"),
|
||||
"signals": ("signals.sx", "signals (reactive signal runtime)"),
|
||||
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
|
||||
}
|
||||
|
||||
EXTENSION_NAMES = {"continuations"}
|
||||
|
||||
@@ -103,8 +103,11 @@ def compile_ref_to_js(
|
||||
if "boot" in adapter_set:
|
||||
spec_mod_set.add("router")
|
||||
spec_mod_set.add("deps")
|
||||
if "page-helpers" in SPEC_MODULES:
|
||||
spec_mod_set.add("page-helpers")
|
||||
has_deps = "deps" in spec_mod_set
|
||||
has_router = "router" in spec_mod_set
|
||||
has_page_helpers = "page-helpers" in spec_mod_set
|
||||
|
||||
# Resolve extensions
|
||||
ext_set = set()
|
||||
@@ -198,12 +201,12 @@ def compile_ref_to_js(
|
||||
if name in adapter_set and name in adapter_platform:
|
||||
parts.append(adapter_platform[name])
|
||||
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps))
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers))
|
||||
if has_continuations:
|
||||
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_boot, has_parser, adapter_label, has_deps, has_router, has_signals))
|
||||
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, has_signals, has_page_helpers))
|
||||
parts.append(EPILOGUE)
|
||||
|
||||
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
@@ -2342,452 +2342,147 @@ def env_components(env):
|
||||
return filter(lambda k: (lambda v: (is_component(v) if sx_truthy(is_component(v)) else is_macro(v)))(env_get(env, k)), keys(env))
|
||||
|
||||
|
||||
# === Transpiled from engine (fetch/swap/trigger pure logic) ===
|
||||
# === Transpiled from page-helpers (pure data transformation helpers) ===
|
||||
|
||||
# ENGINE_VERBS
|
||||
ENGINE_VERBS = ['get', 'post', 'put', 'delete', 'patch']
|
||||
# special-form-category-map
|
||||
special_form_category_map = {'if': 'Control Flow', 'when': 'Control Flow', 'cond': 'Control Flow', 'case': 'Control Flow', 'and': 'Control Flow', 'or': 'Control Flow', 'let': 'Binding', 'let*': 'Binding', 'letrec': 'Binding', 'define': 'Binding', 'set!': 'Binding', 'lambda': 'Functions & Components', 'fn': 'Functions & Components', 'defcomp': 'Functions & Components', 'defmacro': 'Functions & Components', 'begin': 'Sequencing & Threading', 'do': 'Sequencing & Threading', '->': 'Sequencing & Threading', 'quote': 'Quoting', 'quasiquote': 'Quoting', 'reset': 'Continuations', 'shift': 'Continuations', 'dynamic-wind': 'Guards', 'map': 'Higher-Order Forms', 'map-indexed': 'Higher-Order Forms', 'filter': 'Higher-Order Forms', 'reduce': 'Higher-Order Forms', 'some': 'Higher-Order Forms', 'every?': 'Higher-Order Forms', 'for-each': 'Higher-Order Forms', 'defstyle': 'Domain Definitions', 'defhandler': 'Domain Definitions', 'defpage': 'Domain Definitions', 'defquery': 'Domain Definitions', 'defaction': 'Domain Definitions'}
|
||||
|
||||
# DEFAULT_SWAP
|
||||
DEFAULT_SWAP = 'outerHTML'
|
||||
# extract-define-kwargs
|
||||
def extract_define_kwargs(expr):
|
||||
result = {}
|
||||
items = slice(expr, 2)
|
||||
n = len(items)
|
||||
for idx in range(0, n):
|
||||
if sx_truthy((((idx + 1) < n) if not sx_truthy(((idx + 1) < n)) else (type_of(nth(items, idx)) == 'keyword'))):
|
||||
key = keyword_name(nth(items, idx))
|
||||
val = nth(items, (idx + 1))
|
||||
result[key] = (sx_str('(', join(' ', map(serialize, val)), ')') if sx_truthy((type_of(val) == 'list')) else sx_str(val))
|
||||
return result
|
||||
|
||||
# parse-time
|
||||
def parse_time(s):
|
||||
if sx_truthy(is_nil(s)):
|
||||
return 0
|
||||
elif sx_truthy(ends_with_p(s, 'ms')):
|
||||
return parse_int(s, 0)
|
||||
elif sx_truthy(ends_with_p(s, 's')):
|
||||
return (parse_int(replace(s, 's', ''), 0) * 1000)
|
||||
# categorize-special-forms
|
||||
def categorize_special_forms(parsed_exprs):
|
||||
categories = {}
|
||||
for expr in parsed_exprs:
|
||||
if sx_truthy(((type_of(expr) == 'list') if not sx_truthy((type_of(expr) == 'list')) else ((len(expr) >= 2) if not sx_truthy((len(expr) >= 2)) else ((type_of(first(expr)) == 'symbol') if not sx_truthy((type_of(first(expr)) == 'symbol')) else (symbol_name(first(expr)) == 'define-special-form'))))):
|
||||
name = nth(expr, 1)
|
||||
kwargs = extract_define_kwargs(expr)
|
||||
category = (get(special_form_category_map, name) if sx_truthy(get(special_form_category_map, name)) else 'Other')
|
||||
if sx_truthy((not sx_truthy(has_key_p(categories, category)))):
|
||||
categories[category] = []
|
||||
get(categories, category).append({'name': name, 'syntax': (get(kwargs, 'syntax') if sx_truthy(get(kwargs, 'syntax')) else ''), 'doc': (get(kwargs, 'doc') if sx_truthy(get(kwargs, 'doc')) else ''), 'tail-position': (get(kwargs, 'tail-position') if sx_truthy(get(kwargs, 'tail-position')) else ''), 'example': (get(kwargs, 'example') if sx_truthy(get(kwargs, 'example')) else '')})
|
||||
return categories
|
||||
|
||||
# build-ref-items-with-href
|
||||
def build_ref_items_with_href(items, base_path, detail_keys, n_fields):
|
||||
return map(lambda item: ((lambda name: (lambda field2: (lambda field3: {'name': name, 'desc': field2, 'exists': field3, 'href': (sx_str(base_path, name) if sx_truthy((field3 if not sx_truthy(field3) else some(lambda k: (k == name), detail_keys))) else NIL)})(nth(item, 2)))(nth(item, 1)))(nth(item, 0)) if sx_truthy((n_fields == 3)) else (lambda name: (lambda desc: {'name': name, 'desc': desc, 'href': (sx_str(base_path, name) if sx_truthy(some(lambda k: (k == name), detail_keys)) else NIL)})(nth(item, 1)))(nth(item, 0))), items)
|
||||
|
||||
# build-reference-data
|
||||
def build_reference_data(slug, raw_data, detail_keys):
|
||||
_match = slug
|
||||
if _match == 'attributes':
|
||||
return {'req-attrs': build_ref_items_with_href(get(raw_data, 'req-attrs'), '/hypermedia/reference/attributes/', detail_keys, 3), 'beh-attrs': build_ref_items_with_href(get(raw_data, 'beh-attrs'), '/hypermedia/reference/attributes/', detail_keys, 3), 'uniq-attrs': build_ref_items_with_href(get(raw_data, 'uniq-attrs'), '/hypermedia/reference/attributes/', detail_keys, 3)}
|
||||
elif _match == 'headers':
|
||||
return {'req-headers': build_ref_items_with_href(get(raw_data, 'req-headers'), '/hypermedia/reference/headers/', detail_keys, 3), 'resp-headers': build_ref_items_with_href(get(raw_data, 'resp-headers'), '/hypermedia/reference/headers/', detail_keys, 3)}
|
||||
elif _match == 'events':
|
||||
return {'events-list': build_ref_items_with_href(get(raw_data, 'events-list'), '/hypermedia/reference/events/', detail_keys, 2)}
|
||||
elif _match == 'js-api':
|
||||
return {'js-api-list': map(lambda item: {'name': nth(item, 0), 'desc': nth(item, 1)}, get(raw_data, 'js-api-list'))}
|
||||
else:
|
||||
return parse_int(s, 0)
|
||||
return {'req-attrs': build_ref_items_with_href(get(raw_data, 'req-attrs'), '/hypermedia/reference/attributes/', detail_keys, 3), 'beh-attrs': build_ref_items_with_href(get(raw_data, 'beh-attrs'), '/hypermedia/reference/attributes/', detail_keys, 3), 'uniq-attrs': build_ref_items_with_href(get(raw_data, 'uniq-attrs'), '/hypermedia/reference/attributes/', detail_keys, 3)}
|
||||
|
||||
# parse-trigger-spec
|
||||
def parse_trigger_spec(spec):
|
||||
if sx_truthy(is_nil(spec)):
|
||||
return NIL
|
||||
# build-attr-detail
|
||||
def build_attr_detail(slug, detail):
|
||||
if sx_truthy(is_nil(detail)):
|
||||
return {'attr-not-found': True}
|
||||
else:
|
||||
raw_parts = split(spec, ',')
|
||||
return 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))
|
||||
return {'attr-not-found': NIL, 'attr-title': slug, 'attr-description': get(detail, 'description'), 'attr-example': get(detail, 'example'), 'attr-handler': get(detail, 'handler'), 'attr-demo': get(detail, 'demo'), 'attr-wire-id': (sx_str('ref-wire-', replace(replace(slug, ':', '-'), '*', 'star')) if sx_truthy(has_key_p(detail, 'handler')) else NIL)}
|
||||
|
||||
# default-trigger
|
||||
def default_trigger(tag_name):
|
||||
if sx_truthy((tag_name == 'FORM')):
|
||||
return [{'event': 'submit', 'modifiers': {}}]
|
||||
elif 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')))):
|
||||
return [{'event': 'change', 'modifiers': {}}]
|
||||
# build-header-detail
|
||||
def build_header_detail(slug, detail):
|
||||
if sx_truthy(is_nil(detail)):
|
||||
return {'header-not-found': True}
|
||||
else:
|
||||
return [{'event': 'click', 'modifiers': {}}]
|
||||
return {'header-not-found': NIL, 'header-title': slug, 'header-direction': get(detail, 'direction'), 'header-description': get(detail, 'description'), 'header-example': get(detail, 'example'), 'header-demo': get(detail, 'demo')}
|
||||
|
||||
# get-verb-info
|
||||
def get_verb_info(el):
|
||||
return 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-event-detail
|
||||
def build_event_detail(slug, detail):
|
||||
if sx_truthy(is_nil(detail)):
|
||||
return {'event-not-found': True}
|
||||
else:
|
||||
return {'event-not-found': NIL, 'event-title': slug, 'event-description': get(detail, 'description'), 'event-example': get(detail, 'example'), 'event-demo': get(detail, 'demo')}
|
||||
|
||||
# build-request-headers
|
||||
def build_request_headers(el, loaded_components, css_hash):
|
||||
headers = {'SX-Request': 'true', 'SX-Current-URL': browser_location_href()}
|
||||
target_sel = dom_get_attr(el, 'sx-target')
|
||||
if sx_truthy(target_sel):
|
||||
headers['SX-Target'] = target_sel
|
||||
if sx_truthy((not sx_truthy(empty_p(loaded_components)))):
|
||||
headers['SX-Components'] = join(',', loaded_components)
|
||||
if sx_truthy(css_hash):
|
||||
headers['SX-Css'] = css_hash
|
||||
extra_h = dom_get_attr(el, 'sx-headers')
|
||||
if sx_truthy(extra_h):
|
||||
parsed = parse_header_value(extra_h)
|
||||
if sx_truthy(parsed):
|
||||
for key in keys(parsed):
|
||||
headers[key] = sx_str(get(parsed, key))
|
||||
return headers
|
||||
# build-component-source
|
||||
def build_component_source(comp_data):
|
||||
comp_type = get(comp_data, 'type')
|
||||
name = get(comp_data, 'name')
|
||||
params = get(comp_data, 'params')
|
||||
has_children = get(comp_data, 'has-children')
|
||||
body_sx = get(comp_data, 'body-sx')
|
||||
affinity = get(comp_data, 'affinity')
|
||||
if sx_truthy((comp_type == 'not-found')):
|
||||
return sx_str(';; component ', name, ' not found')
|
||||
else:
|
||||
param_strs = ((['&rest', 'children'] if sx_truthy(has_children) else []) if sx_truthy(empty_p(params)) else (append(cons('&key', params), ['&rest', 'children']) if sx_truthy(has_children) else cons('&key', params)))
|
||||
params_sx = sx_str('(', join(' ', param_strs), ')')
|
||||
form_name = ('defisland' if sx_truthy((comp_type == 'island')) else 'defcomp')
|
||||
affinity_str = (sx_str(' :affinity ', affinity) if sx_truthy(((comp_type == 'component') if not sx_truthy((comp_type == 'component')) else ((not sx_truthy(is_nil(affinity))) if not sx_truthy((not sx_truthy(is_nil(affinity)))) else (not sx_truthy((affinity == 'auto')))))) else '')
|
||||
return sx_str('(', form_name, ' ', name, ' ', params_sx, affinity_str, '\n ', body_sx, ')')
|
||||
|
||||
# process-response-headers
|
||||
def process_response_headers(get_header):
|
||||
return {'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'), 'cache-invalidate': get_header('SX-Cache-Invalidate'), 'cache-update': get_header('SX-Cache-Update')}
|
||||
|
||||
# parse-swap-spec
|
||||
def parse_swap_spec(raw_swap, global_transitions_p):
|
||||
# build-bundle-analysis
|
||||
def build_bundle_analysis(pages_raw, components_raw, total_components, total_macros, pure_count, io_count):
|
||||
_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']}
|
||||
pages_data = []
|
||||
for page in pages_raw:
|
||||
needed_names = get(page, 'needed-names')
|
||||
n = len(needed_names)
|
||||
pct = (round(((n / total_components) * 100)) if sx_truthy((total_components > 0)) else 0)
|
||||
savings = (100 - pct)
|
||||
_cells['pure_in_page'] = 0
|
||||
_cells['io_in_page'] = 0
|
||||
page_io_refs = []
|
||||
comp_details = []
|
||||
for comp_name in needed_names:
|
||||
info = get(components_raw, comp_name)
|
||||
if sx_truthy((not sx_truthy(is_nil(info)))):
|
||||
if sx_truthy(get(info, 'is-pure')):
|
||||
_cells['pure_in_page'] = (_cells['pure_in_page'] + 1)
|
||||
else:
|
||||
_cells['io_in_page'] = (_cells['io_in_page'] + 1)
|
||||
for ref in (get(info, 'io-refs') if sx_truthy(get(info, 'io-refs')) else []):
|
||||
if sx_truthy((not sx_truthy(some(lambda r: (r == ref), page_io_refs)))):
|
||||
page_io_refs.append(ref)
|
||||
comp_details.append({'name': comp_name, 'is-pure': get(info, 'is-pure'), 'affinity': get(info, 'affinity'), 'render-target': get(info, 'render-target'), 'io-refs': (get(info, 'io-refs') if sx_truthy(get(info, 'io-refs')) else []), 'deps': (get(info, 'deps') if sx_truthy(get(info, 'deps')) else []), 'source': get(info, 'source')})
|
||||
pages_data.append({'name': get(page, 'name'), 'path': get(page, 'path'), 'direct': get(page, 'direct'), 'needed': n, 'pct': pct, 'savings': savings, 'io-refs': len(page_io_refs), 'pure-in-page': _cells['pure_in_page'], 'io-in-page': _cells['io_in_page'], 'components': comp_details})
|
||||
return {'pages': pages_data, 'total-components': total_components, 'total-macros': total_macros, 'pure-count': pure_count, 'io-count': io_count}
|
||||
|
||||
# parse-retry-spec
|
||||
def parse_retry_spec(retry_attr):
|
||||
if sx_truthy(is_nil(retry_attr)):
|
||||
return NIL
|
||||
else:
|
||||
parts = split(retry_attr, ':')
|
||||
return {'strategy': first(parts), 'start-ms': parse_int(nth(parts, 1), 1000), 'cap-ms': parse_int(nth(parts, 2), 30000)}
|
||||
|
||||
# next-retry-ms
|
||||
def next_retry_ms(current_ms, cap_ms):
|
||||
return min((current_ms * 2), cap_ms)
|
||||
|
||||
# filter-params
|
||||
def filter_params(params_spec, all_params):
|
||||
if sx_truthy(is_nil(params_spec)):
|
||||
return all_params
|
||||
elif sx_truthy((params_spec == 'none')):
|
||||
return []
|
||||
elif sx_truthy((params_spec == '*')):
|
||||
return all_params
|
||||
elif sx_truthy(starts_with_p(params_spec, 'not ')):
|
||||
excluded = map(trim, split(slice(params_spec, 4), ','))
|
||||
return filter(lambda p: (not sx_truthy(contains_p(excluded, first(p)))), all_params)
|
||||
else:
|
||||
allowed = map(trim, split(params_spec, ','))
|
||||
return filter(lambda p: contains_p(allowed, first(p)), all_params)
|
||||
|
||||
# resolve-target
|
||||
def resolve_target(el):
|
||||
sel = dom_get_attr(el, 'sx-target')
|
||||
if sx_truthy((is_nil(sel) if sx_truthy(is_nil(sel)) else (sel == 'this'))):
|
||||
return el
|
||||
elif sx_truthy((sel == 'closest')):
|
||||
return dom_parent(el)
|
||||
else:
|
||||
return dom_query(sel)
|
||||
|
||||
# apply-optimistic
|
||||
def apply_optimistic(el):
|
||||
directive = dom_get_attr(el, 'sx-optimistic')
|
||||
if sx_truthy(is_nil(directive)):
|
||||
return NIL
|
||||
else:
|
||||
target = (resolve_target(el) if sx_truthy(resolve_target(el)) else el)
|
||||
state = {'target': target, 'directive': directive}
|
||||
(_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)))
|
||||
return state
|
||||
|
||||
# revert-optimistic
|
||||
def revert_optimistic(state):
|
||||
if sx_truthy(state):
|
||||
target = get(state, 'target')
|
||||
directive = get(state, 'directive')
|
||||
if sx_truthy((directive == 'remove')):
|
||||
dom_set_style(target, 'opacity', (get(state, 'opacity') if sx_truthy(get(state, 'opacity')) else ''))
|
||||
return dom_set_style(target, 'pointer-events', '')
|
||||
elif sx_truthy((directive == 'disable')):
|
||||
return dom_set_prop(target, 'disabled', (get(state, 'disabled') if sx_truthy(get(state, 'disabled')) else False))
|
||||
elif sx_truthy(get(state, 'add-class')):
|
||||
return dom_remove_class(target, get(state, 'add-class'))
|
||||
return NIL
|
||||
return NIL
|
||||
|
||||
# find-oob-swaps
|
||||
def find_oob_swaps(container):
|
||||
results = []
|
||||
for attr in ['sx-swap-oob', 'hx-swap-oob']:
|
||||
oob_els = dom_query_all(container, sx_str('[', attr, ']'))
|
||||
for oob in oob_els:
|
||||
swap_type = (dom_get_attr(oob, attr) if sx_truthy(dom_get_attr(oob, attr)) else 'outerHTML')
|
||||
target_id = dom_id(oob)
|
||||
dom_remove_attr(oob, attr)
|
||||
if sx_truthy(target_id):
|
||||
results.append({'element': oob, 'swap-type': swap_type, 'target-id': target_id})
|
||||
return results
|
||||
|
||||
# morph-node
|
||||
def morph_node(old_node, new_node):
|
||||
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'))):
|
||||
return NIL
|
||||
elif sx_truthy((dom_has_attr_p(old_node, 'data-sx-island') if not sx_truthy(dom_has_attr_p(old_node, 'data-sx-island')) else (is_processed_p(old_node, 'island-hydrated') if not sx_truthy(is_processed_p(old_node, 'island-hydrated')) else (dom_has_attr_p(new_node, 'data-sx-island') if not sx_truthy(dom_has_attr_p(new_node, 'data-sx-island')) else (dom_get_attr(old_node, 'data-sx-island') == dom_get_attr(new_node, 'data-sx-island')))))):
|
||||
return morph_island_children(old_node, new_node)
|
||||
elif 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)))))):
|
||||
return dom_replace_child(dom_parent(old_node), dom_clone(new_node), old_node)
|
||||
elif sx_truthy(((dom_node_type(old_node) == 3) if sx_truthy((dom_node_type(old_node) == 3)) else (dom_node_type(old_node) == 8))):
|
||||
if sx_truthy((not sx_truthy((dom_text_content(old_node) == dom_text_content(new_node))))):
|
||||
return dom_set_text_content(old_node, dom_text_content(new_node))
|
||||
return NIL
|
||||
elif sx_truthy((dom_node_type(old_node) == 1)):
|
||||
sync_attrs(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))))):
|
||||
return morph_children(old_node, new_node)
|
||||
return NIL
|
||||
return NIL
|
||||
|
||||
# sync-attrs
|
||||
def sync_attrs(old_el, new_el):
|
||||
ra_str = (dom_get_attr(old_el, 'data-sx-reactive-attrs') if sx_truthy(dom_get_attr(old_el, 'data-sx-reactive-attrs')) else '')
|
||||
reactive_attrs = ([] if sx_truthy(empty_p(ra_str)) else split(ra_str, ','))
|
||||
for attr in dom_attr_list(new_el):
|
||||
name = first(attr)
|
||||
val = nth(attr, 1)
|
||||
if sx_truthy(((not sx_truthy((dom_get_attr(old_el, name) == val))) if not sx_truthy((not sx_truthy((dom_get_attr(old_el, name) == val)))) else (not sx_truthy(contains_p(reactive_attrs, name))))):
|
||||
dom_set_attr(old_el, name, val)
|
||||
return for_each(lambda attr: (lambda aname: (dom_remove_attr(old_el, aname) if sx_truthy(((not sx_truthy(dom_has_attr_p(new_el, aname))) if not sx_truthy((not sx_truthy(dom_has_attr_p(new_el, aname)))) else ((not sx_truthy(contains_p(reactive_attrs, aname))) if not sx_truthy((not sx_truthy(contains_p(reactive_attrs, aname)))) else (not sx_truthy((aname == 'data-sx-reactive-attrs')))))) else NIL))(first(attr)), dom_attr_list(old_el))
|
||||
|
||||
# morph-children
|
||||
def morph_children(old_parent, new_parent):
|
||||
# build-routing-analysis
|
||||
def build_routing_analysis(pages_raw):
|
||||
_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)
|
||||
pages_data = []
|
||||
_cells['client_count'] = 0
|
||||
_cells['server_count'] = 0
|
||||
for page in pages_raw:
|
||||
has_data = get(page, 'has-data')
|
||||
content_src = (get(page, 'content-src') if sx_truthy(get(page, 'content-src')) else '')
|
||||
_cells['mode'] = NIL
|
||||
_cells['reason'] = ''
|
||||
if sx_truthy(has_data):
|
||||
_cells['mode'] = 'server'
|
||||
_cells['reason'] = 'Has :data expression — needs server IO'
|
||||
_cells['server_count'] = (_cells['server_count'] + 1)
|
||||
elif sx_truthy(empty_p(content_src)):
|
||||
_cells['mode'] = 'server'
|
||||
_cells['reason'] = 'No content expression'
|
||||
_cells['server_count'] = (_cells['server_count'] + 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)))
|
||||
_cells['mode'] = 'client'
|
||||
_cells['client_count'] = (_cells['client_count'] + 1)
|
||||
pages_data.append({'name': get(page, 'name'), 'path': get(page, 'path'), 'mode': _cells['mode'], 'has-data': has_data, 'content-expr': (sx_str(slice(content_src, 0, 80), '...') if sx_truthy((len(content_src) > 80)) else content_src), 'reason': _cells['reason']})
|
||||
return {'pages': pages_data, 'total-pages': (_cells['client_count'] + _cells['server_count']), 'client-count': _cells['client_count'], 'server-count': _cells['server_count']}
|
||||
|
||||
# morph-island-children
|
||||
def morph_island_children(old_island, new_island):
|
||||
old_lakes = dom_query_all(old_island, '[data-sx-lake]')
|
||||
new_lakes = dom_query_all(new_island, '[data-sx-lake]')
|
||||
old_marshes = dom_query_all(old_island, '[data-sx-marsh]')
|
||||
new_marshes = dom_query_all(new_island, '[data-sx-marsh]')
|
||||
new_lake_map = {}
|
||||
new_marsh_map = {}
|
||||
for lake in new_lakes:
|
||||
id_ = dom_get_attr(lake, 'data-sx-lake')
|
||||
if sx_truthy(id_):
|
||||
new_lake_map[id_] = lake
|
||||
for marsh in new_marshes:
|
||||
id_ = dom_get_attr(marsh, 'data-sx-marsh')
|
||||
if sx_truthy(id_):
|
||||
new_marsh_map[id_] = marsh
|
||||
for old_lake in old_lakes:
|
||||
id_ = dom_get_attr(old_lake, 'data-sx-lake')
|
||||
new_lake = dict_get(new_lake_map, id_)
|
||||
if sx_truthy(new_lake):
|
||||
sync_attrs(old_lake, new_lake)
|
||||
morph_children(old_lake, new_lake)
|
||||
for old_marsh in old_marshes:
|
||||
id_ = dom_get_attr(old_marsh, 'data-sx-marsh')
|
||||
new_marsh = dict_get(new_marsh_map, id_)
|
||||
if sx_truthy(new_marsh):
|
||||
morph_marsh(old_marsh, new_marsh, old_island)
|
||||
return process_signal_updates(new_island)
|
||||
|
||||
# morph-marsh
|
||||
def morph_marsh(old_marsh, new_marsh, island_el):
|
||||
transform = dom_get_data(old_marsh, 'sx-marsh-transform')
|
||||
env = dom_get_data(old_marsh, 'sx-marsh-env')
|
||||
new_html = dom_inner_html(new_marsh)
|
||||
if sx_truthy((env if not sx_truthy(env) else (new_html if not sx_truthy(new_html) else (not sx_truthy(empty_p(new_html)))))):
|
||||
parsed = parse(new_html)
|
||||
sx_content = (invoke(transform, parsed) if sx_truthy(transform) else parsed)
|
||||
dispose_marsh_scope(old_marsh)
|
||||
return with_marsh_scope(old_marsh, lambda : (lambda new_dom: _sx_begin(dom_remove_children_after(old_marsh, NIL), dom_append(old_marsh, new_dom)))(render_to_dom(sx_content, env, NIL)))
|
||||
else:
|
||||
sync_attrs(old_marsh, new_marsh)
|
||||
return morph_children(old_marsh, new_marsh)
|
||||
|
||||
# process-signal-updates
|
||||
def process_signal_updates(root):
|
||||
signal_els = dom_query_all(root, '[data-sx-signal]')
|
||||
return for_each(lambda el: (lambda spec: ((lambda colon_idx: ((lambda store_name: (lambda raw_value: _sx_begin((lambda parsed: reset_b(use_store(store_name), parsed))(json_parse(raw_value)), dom_remove_attr(el, 'data-sx-signal')))(slice(spec, (colon_idx + 1))))(slice(spec, 0, colon_idx)) if sx_truthy((colon_idx > 0)) else NIL))(index_of(spec, ':')) if sx_truthy(spec) else NIL))(dom_get_attr(el, 'data-sx-signal')), signal_els)
|
||||
|
||||
# swap-dom-nodes
|
||||
def swap_dom_nodes(target, new_nodes, strategy):
|
||||
_match = strategy
|
||||
if _match == 'innerHTML':
|
||||
if sx_truthy(dom_is_fragment_p(new_nodes)):
|
||||
return morph_children(target, new_nodes)
|
||||
else:
|
||||
wrapper = dom_create_element('div', NIL)
|
||||
dom_append(wrapper, new_nodes)
|
||||
return morph_children(target, wrapper)
|
||||
elif _match == 'outerHTML':
|
||||
parent = dom_parent(target)
|
||||
((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))
|
||||
return parent
|
||||
elif _match == 'afterend':
|
||||
return dom_insert_after(target, new_nodes)
|
||||
elif _match == 'beforeend':
|
||||
return dom_append(target, new_nodes)
|
||||
elif _match == 'afterbegin':
|
||||
return dom_prepend(target, new_nodes)
|
||||
elif _match == 'beforebegin':
|
||||
return dom_insert_before(dom_parent(target), new_nodes, target)
|
||||
elif _match == 'delete':
|
||||
return dom_remove_child(dom_parent(target), target)
|
||||
elif _match == 'none':
|
||||
return NIL
|
||||
else:
|
||||
if sx_truthy(dom_is_fragment_p(new_nodes)):
|
||||
return morph_children(target, new_nodes)
|
||||
else:
|
||||
wrapper = dom_create_element('div', NIL)
|
||||
dom_append(wrapper, new_nodes)
|
||||
return morph_children(target, wrapper)
|
||||
|
||||
# insert-remaining-siblings
|
||||
def insert_remaining_siblings(parent, ref_node, sib):
|
||||
if sx_truthy(sib):
|
||||
next = dom_next_sibling(sib)
|
||||
dom_insert_after(ref_node, sib)
|
||||
return insert_remaining_siblings(parent, sib, next)
|
||||
return NIL
|
||||
|
||||
# swap-html-string
|
||||
def swap_html_string(target, html, strategy):
|
||||
_match = strategy
|
||||
if _match == 'innerHTML':
|
||||
return dom_set_inner_html(target, html)
|
||||
elif _match == 'outerHTML':
|
||||
parent = dom_parent(target)
|
||||
dom_insert_adjacent_html(target, 'afterend', html)
|
||||
dom_remove_child(parent, target)
|
||||
return parent
|
||||
elif _match == 'afterend':
|
||||
return dom_insert_adjacent_html(target, 'afterend', html)
|
||||
elif _match == 'beforeend':
|
||||
return dom_insert_adjacent_html(target, 'beforeend', html)
|
||||
elif _match == 'afterbegin':
|
||||
return dom_insert_adjacent_html(target, 'afterbegin', html)
|
||||
elif _match == 'beforebegin':
|
||||
return dom_insert_adjacent_html(target, 'beforebegin', html)
|
||||
elif _match == 'delete':
|
||||
return dom_remove_child(dom_parent(target), target)
|
||||
elif _match == 'none':
|
||||
return NIL
|
||||
else:
|
||||
return dom_set_inner_html(target, html)
|
||||
|
||||
# handle-history
|
||||
def handle_history(el, url, resp_headers):
|
||||
push_url = dom_get_attr(el, 'sx-push-url')
|
||||
replace_url = dom_get_attr(el, 'sx-replace-url')
|
||||
hdr_replace = get(resp_headers, 'replace-url')
|
||||
if sx_truthy(hdr_replace):
|
||||
return browser_replace_state(hdr_replace)
|
||||
elif sx_truthy((push_url if not sx_truthy(push_url) else (not sx_truthy((push_url == 'false'))))):
|
||||
return browser_push_state((url if sx_truthy((push_url == 'true')) else push_url))
|
||||
elif sx_truthy((replace_url if not sx_truthy(replace_url) else (not sx_truthy((replace_url == 'false'))))):
|
||||
return browser_replace_state((url if sx_truthy((replace_url == 'true')) else replace_url))
|
||||
return NIL
|
||||
|
||||
# PRELOAD_TTL
|
||||
PRELOAD_TTL = 30000
|
||||
|
||||
# preload-cache-get
|
||||
def preload_cache_get(cache, url):
|
||||
entry = dict_get(cache, url)
|
||||
if sx_truthy(is_nil(entry)):
|
||||
return NIL
|
||||
else:
|
||||
if sx_truthy(((now_ms() - get(entry, 'timestamp')) > PRELOAD_TTL)):
|
||||
dict_delete(cache, url)
|
||||
return NIL
|
||||
else:
|
||||
dict_delete(cache, url)
|
||||
return entry
|
||||
|
||||
# preload-cache-set
|
||||
def preload_cache_set(cache, url, text, content_type):
|
||||
return _sx_dict_set(cache, url, {'text': text, 'content-type': content_type, 'timestamp': now_ms()})
|
||||
|
||||
# classify-trigger
|
||||
def classify_trigger(trigger):
|
||||
event = get(trigger, 'event')
|
||||
if sx_truthy((event == 'every')):
|
||||
return 'poll'
|
||||
elif sx_truthy((event == 'intersect')):
|
||||
return 'intersect'
|
||||
elif sx_truthy((event == 'load')):
|
||||
return 'load'
|
||||
elif sx_truthy((event == 'revealed')):
|
||||
return 'revealed'
|
||||
else:
|
||||
return 'event'
|
||||
|
||||
# should-boost-link?
|
||||
def should_boost_link_p(link):
|
||||
href = dom_get_attr(link, 'href')
|
||||
return (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'))))))))))
|
||||
|
||||
# should-boost-form?
|
||||
def should_boost_form_p(form):
|
||||
return ((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
|
||||
def parse_sse_swap(el):
|
||||
return (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
|
||||
def split_path_segments(path):
|
||||
trimmed = (slice(path, 1) if sx_truthy(starts_with_p(path, '/')) else path)
|
||||
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)
|
||||
if sx_truthy(empty_p(trimmed2)):
|
||||
return []
|
||||
else:
|
||||
return split(trimmed2, '/')
|
||||
|
||||
# make-route-segment
|
||||
def make_route_segment(seg):
|
||||
if sx_truthy((starts_with_p(seg, '<') if not sx_truthy(starts_with_p(seg, '<')) else ends_with_p(seg, '>'))):
|
||||
param_name = slice(seg, 1, (len(seg) - 1))
|
||||
d = {}
|
||||
d['type'] = 'param'
|
||||
d['value'] = param_name
|
||||
return d
|
||||
else:
|
||||
d = {}
|
||||
d['type'] = 'literal'
|
||||
d['value'] = seg
|
||||
return d
|
||||
|
||||
# parse-route-pattern
|
||||
def parse_route_pattern(pattern):
|
||||
segments = split_path_segments(pattern)
|
||||
return map(make_route_segment, segments)
|
||||
|
||||
# match-route-segments
|
||||
def match_route_segments(path_segs, parsed_segs):
|
||||
_cells = {}
|
||||
if sx_truthy((not sx_truthy((len(path_segs) == len(parsed_segs))))):
|
||||
return NIL
|
||||
else:
|
||||
params = {}
|
||||
_cells['matched'] = True
|
||||
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)
|
||||
if sx_truthy(_cells['matched']):
|
||||
return params
|
||||
else:
|
||||
return NIL
|
||||
|
||||
# match-route
|
||||
def match_route(path, pattern):
|
||||
path_segs = split_path_segments(path)
|
||||
parsed_segs = parse_route_pattern(pattern)
|
||||
return match_route_segments(path_segs, parsed_segs)
|
||||
|
||||
# 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']
|
||||
# build-affinity-analysis
|
||||
def build_affinity_analysis(demo_components, page_plans):
|
||||
return {'components': demo_components, 'page-plans': page_plans}
|
||||
|
||||
|
||||
# === Transpiled from signals (reactive signal runtime) ===
|
||||
@@ -3058,4 +2753,4 @@ def render(expr, env=None):
|
||||
|
||||
def make_env(**kwargs):
|
||||
"""Create an environment with initial bindings."""
|
||||
return _Env(dict(kwargs))
|
||||
return _Env(dict(kwargs))
|
||||
Reference in New Issue
Block a user