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:
2026-03-11 14:30:12 +00:00
parent 29c90a625b
commit c95e19dcf2
16 changed files with 5584 additions and 781 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View 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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -104,3 +104,8 @@
:params ()
:returns "dict"
:service "sx")
(define-page-helper "page-helpers-demo-data"
:params ()
:returns "dict"
:service "sx")

View File

@@ -227,7 +227,8 @@
(dict :label "JavaScript" :href "/bootstrappers/javascript")
(dict :label "Python" :href "/bootstrappers/python")
(dict :label "Self-Hosting (py.sx)" :href "/bootstrappers/self-hosting")
(dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js")))
(dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js")
(dict :label "Page Helpers" :href "/bootstrappers/page-helpers")))
;; Spec file registry — canonical metadata for spec viewer pages.
;; Python only handles file I/O (read-spec-file); all metadata lives here.

265
sx/sx/page-helpers-demo.sx Normal file
View File

@@ -0,0 +1,265 @@
;; page-helpers-demo.sx — Demo: same SX spec functions on server and client
;;
;; Shows page-helpers.sx functions running on Python (server-side, via sx_ref.py)
;; and JavaScript (client-side, via sx-browser.js) with identical results.
;; Server renders with render-to-html. Client runs as a defisland — pure SX,
;; no JavaScript file. The button click triggers spec functions via signals.
;; ---------------------------------------------------------------------------
;; Shared card component — used by both server and client results
;; ---------------------------------------------------------------------------
(defcomp ~demo-result-card (&key title ms desc theme &rest children)
(let ((border (if (= theme "blue") "border-blue-200 bg-blue-50/30" "border-stone-200"))
(title-c (if (= theme "blue") "text-blue-700" "text-stone-700"))
(badge-c (if (= theme "blue") "text-blue-400" "text-stone-400"))
(desc-c (if (= theme "blue") "text-blue-500" "text-stone-500"))
(body-c (if (= theme "blue") "text-blue-600" "text-stone-600")))
(div :class (str "rounded-lg border p-4 " border)
(h4 :class (str "font-semibold text-sm mb-1 " title-c)
title " "
(span :class (str "text-xs " badge-c) (str ms "ms")))
(p :class (str "text-xs mb-2 " desc-c) desc)
(div :class (str "text-xs space-y-0.5 " body-c)
children))))
;; ---------------------------------------------------------------------------
;; Client-side island — runs spec functions in the browser on button click
;; ---------------------------------------------------------------------------
(defisland ~demo-client-runner (&key sf-source attr-detail req-attrs attr-keys)
(let ((results (signal nil))
(running (signal false))
(run-demo (fn (e)
(reset! running true)
(let* ((t0 (now-ms))
;; 1. categorize-special-forms
(t1 (now-ms))
(sf-exprs (sx-parse sf-source))
(sf-result (categorize-special-forms sf-exprs))
(sf-ms (- (now-ms) t1))
(sf-cats {})
(sf-total 0)
;; 2. build-reference-data
(t2 (now-ms))
(ref-result (build-reference-data "attributes"
{"req-attrs" req-attrs "beh-attrs" (list) "uniq-attrs" (list)}
attr-keys))
(ref-ms (- (now-ms) t2))
(ref-sample (slice (or (get ref-result "req-attrs") (list)) 0 3))
;; 3. build-attr-detail
(t3 (now-ms))
(attr-result (build-attr-detail "sx-get" attr-detail))
(attr-ms (- (now-ms) t3))
;; 4. build-component-source
(t4 (now-ms))
(comp-result (build-component-source
{"type" "component" "name" "~demo-card"
"params" (list "title" "subtitle")
"has-children" true
"body-sx" "(div :class \"card\"\n (h2 title)\n (when subtitle (p subtitle))\n children)"
"affinity" "auto"}))
(comp-ms (- (now-ms) t4))
;; 5. build-routing-analysis
(t5 (now-ms))
(routing-result (build-routing-analysis (list
{"name" "home" "path" "/" "has-data" false "content-src" "(~home-content)"}
{"name" "dashboard" "path" "/dash" "has-data" true "content-src" "(~dashboard)"}
{"name" "about" "path" "/about" "has-data" false "content-src" "(~about-content)"}
{"name" "settings" "path" "/settings" "has-data" true "content-src" "(~settings)"})))
(routing-ms (- (now-ms) t5))
(total-ms (- (now-ms) t0)))
;; Post-process sf-result: count forms per category
(for-each (fn (k)
(let ((count (len (get sf-result k))))
(set! sf-cats (assoc sf-cats k count))
(set! sf-total (+ sf-total count))))
(keys sf-result))
(reset! results
{"sf-cats" sf-cats "sf-total" sf-total "sf-ms" sf-ms
"ref-sample" ref-sample "ref-ms" ref-ms
"attr-result" attr-result "attr-ms" attr-ms
"comp-result" comp-result "comp-ms" comp-ms
"routing-result" routing-result "routing-ms" routing-ms
"total-ms" total-ms})))))
(<>
(button
:class (if (deref running)
"px-4 py-2 rounded-md bg-blue-600 text-white font-medium text-sm cursor-default mb-4"
"px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 transition-colors mb-4")
:on-click run-demo
(if (deref running)
(str "Done (" (get (deref results) "total-ms") "ms total)")
"Run in Browser"))
(when (deref results)
(let ((r (deref results)))
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(~demo-result-card
:title "categorize-special-forms"
:ms (get r "sf-ms") :theme "blue"
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
(p :class "text-sm mb-1"
(str (get r "sf-total") " forms in "
(len (keys (get r "sf-cats"))) " categories"))
(map (fn (k)
(div (str k ": " (get (get r "sf-cats") k))))
(keys (get r "sf-cats"))))
(~demo-result-card
:title "build-reference-data"
:ms (get r "ref-ms") :theme "blue"
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
(p :class "text-sm mb-1"
(str (len (get r "ref-sample")) " attributes with detail page links"))
(map (fn (item)
(div (str (get item "name") " → "
(or (get item "href") "no detail page"))))
(get r "ref-sample")))
(~demo-result-card
:title "build-attr-detail"
:ms (get r "attr-ms") :theme "blue"
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
(div (str "title: " (get (get r "attr-result") "attr-title")))
(div (str "wire-id: " (or (get (get r "attr-result") "attr-wire-id") "none")))
(div (str "has handler: " (if (get (get r "attr-result") "attr-handler") "yes" "no"))))
(~demo-result-card
:title "build-component-source"
:ms (get r "comp-ms") :theme "blue"
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
(pre :class "bg-blue-50 p-2 rounded overflow-x-auto"
(get r "comp-result")))
(div :class "rounded-lg border border-blue-200 bg-blue-50/30 p-4 md:col-span-2"
(h4 :class "font-semibold text-blue-700 text-sm mb-1"
"build-routing-analysis "
(span :class "text-xs text-blue-400" (str (get r "routing-ms") "ms")))
(p :class "text-xs text-blue-500 mb-2"
"Classifies pages as client-routable or server-only based on whether they have data dependencies.")
(div :class "text-xs text-blue-600"
(p :class "text-sm mb-1"
(str (get (get r "routing-result") "total-pages") " pages: "
(get (get r "routing-result") "client-count") " client-routable, "
(get (get r "routing-result") "server-count") " server-only"))
(div :class "space-y-0.5"
(map (fn (pg)
(div (str (get pg "name") " → " (get pg "mode")
(when (not (empty? (get pg "reason")))
(str " (" (get pg "reason") ")")))))
(get (get r "routing-result") "pages")))))))))))
;; ---------------------------------------------------------------------------
;; Main page component — server-rendered content + client island
;; ---------------------------------------------------------------------------
(defcomp ~page-helpers-demo-content (&key
sf-categories sf-total sf-ms
ref-sample ref-ms
attr-result attr-ms
comp-source comp-ms
routing-result routing-ms
server-total-ms
sf-source
attr-detail req-attrs attr-keys)
(div :class "max-w-3xl mx-auto px-4"
(div :class "mb-8"
(h2 :class "text-2xl font-bold text-stone-800 mb-2" "Bootstrapped Page Helpers")
(p :class "text-stone-600 mb-4"
"These functions are defined once in "
(code :class "text-violet-700" "page-helpers.sx")
" and bootstrapped to both Python ("
(code :class "text-violet-700" "sx_ref.py")
") and JavaScript ("
(code :class "text-violet-700" "sx-browser.js")
"). The server ran them in Python during this page load. Click the button below to run the identical functions client-side in the browser — same spec, same inputs, same results."))
;; Server results
(div :class "mb-8"
(h3 :class "text-lg font-semibold text-stone-700 mb-3"
"Server Results "
(span :class "text-sm font-normal text-stone-500"
(str "(Python, " server-total-ms "ms total)")))
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(~demo-result-card
:title "categorize-special-forms"
:ms sf-ms :theme "stone"
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
(p :class "text-sm mb-1"
(str sf-total " forms in "
(len (keys sf-categories)) " categories"))
(map (fn (k)
(div (str k ": " (get sf-categories k))))
(keys sf-categories)))
(~demo-result-card
:title "build-reference-data"
:ms ref-ms :theme "stone"
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
(p :class "text-sm mb-1"
(str (len ref-sample) " attributes with detail page links"))
(map (fn (item)
(div (str (get item "name") " → "
(or (get item "href") "no detail page"))))
ref-sample))
(~demo-result-card
:title "build-attr-detail"
:ms attr-ms :theme "stone"
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
(div (str "title: " (get attr-result "attr-title")))
(div (str "wire-id: " (or (get attr-result "attr-wire-id") "none")))
(div (str "has handler: " (if (get attr-result "attr-handler") "yes" "no"))))
(~demo-result-card
:title "build-component-source"
:ms comp-ms :theme "stone"
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
(pre :class "bg-stone-50 p-2 rounded overflow-x-auto"
comp-source))
(div :class "rounded-lg border border-stone-200 p-4 md:col-span-2"
(h4 :class "font-semibold text-stone-700 text-sm mb-1"
"build-routing-analysis "
(span :class "text-xs text-stone-400" (str routing-ms "ms")))
(p :class "text-xs text-stone-500 mb-2"
"Classifies pages as client-routable or server-only based on whether they have data dependencies.")
(div :class "text-xs text-stone-600"
(p :class "text-sm mb-1"
(str (get routing-result "total-pages") " pages: "
(get routing-result "client-count") " client-routable, "
(get routing-result "server-count") " server-only"))
(div :class "space-y-0.5"
(map (fn (pg)
(div (str (get pg "name") " → " (get pg "mode")
(when (not (empty? (get pg "reason")))
(str " (" (get pg "reason") ")")))))
(get routing-result "pages")))))))
;; Client execution area — pure SX island, no JavaScript file
(div :class "mb-8"
(h3 :class "text-lg font-semibold text-stone-700 mb-3"
"Client Results "
(span :class "text-sm font-normal text-stone-500" "(JavaScript, sx-browser.js)"))
(~demo-client-runner
:sf-source sf-source
:attr-detail attr-detail
:req-attrs req-attrs
:attr-keys attr-keys))))

View File

@@ -553,6 +553,28 @@
"phase2" (~reactive-islands-phase2-content)
:else (~reactive-islands-index-content))))
;; ---------------------------------------------------------------------------
;; Bootstrapped page helpers demo
;; ---------------------------------------------------------------------------
(defpage page-helpers-demo
:path "/bootstrappers/page-helpers"
:auth :public
:layout :sx-docs
:data (page-helpers-demo-data)
:content (~sx-doc :path "/bootstrappers/page-helpers"
(~page-helpers-demo-content
:sf-categories sf-categories :sf-total sf-total :sf-ms sf-ms
:ref-sample ref-sample :ref-ms ref-ms
:attr-result attr-result :attr-ms attr-ms
:comp-source comp-source :comp-ms comp-ms
:routing-result routing-result :routing-ms routing-ms
:server-total-ms server-total-ms
:sf-source sf-source
:attr-detail attr-detail
:req-attrs req-attrs
:attr-keys attr-keys)))
;; ---------------------------------------------------------------------------
;; Testing section
;; ---------------------------------------------------------------------------

View File

@@ -33,6 +33,7 @@ def _register_sx_helpers() -> None:
"action:add-demo-item": _add_demo_item,
"offline-demo-data": _offline_demo_data,
"prove-data": _prove_data,
"page-helpers-demo-data": _page_helpers_demo_data,
})
@@ -41,26 +42,29 @@ def _component_source(name: str) -> str:
from shared.sx.jinja_bridge import get_component_env
from shared.sx.parser import serialize
from shared.sx.types import Component, Island
from shared.sx.ref.sx_ref import build_component_source
comp = get_component_env().get(name)
if isinstance(comp, Island):
param_strs = (["&key"] + list(comp.params)) if comp.params else []
if comp.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(comp.body, pretty=True)
return f"(defisland {name} {params_sx}\n {body_sx})"
return build_component_source({
"type": "island", "name": name,
"params": list(comp.params) if comp.params else [],
"has-children": comp.has_children,
"body-sx": serialize(comp.body, pretty=True),
"affinity": None,
})
if not isinstance(comp, Component):
return f";; component {name} not found"
param_strs = ["&key"] + list(comp.params)
if comp.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(comp.body, pretty=True)
affinity = ""
if comp.render_target == "server":
affinity = " :affinity :server"
return f"(defcomp {name} {params_sx}{affinity}\n {body_sx})"
return build_component_source({
"type": "not-found", "name": name,
"params": [], "has-children": False, "body-sx": "", "affinity": None,
})
return build_component_source({
"type": "component", "name": name,
"params": list(comp.params),
"has-children": comp.has_children,
"body-sx": serialize(comp.body, pretty=True),
"affinity": comp.affinity,
})
def _primitives_data() -> dict:
@@ -70,168 +74,57 @@ def _primitives_data() -> dict:
def _special_forms_data() -> dict:
"""Parse special-forms.sx and return categorized form data.
Returns a dict of category → list of form dicts, each with:
name, syntax, doc, tail_position, example
"""
"""Parse special-forms.sx and return categorized form data."""
import os
from shared.sx.parser import parse_all, serialize
from shared.sx.types import Symbol, Keyword
from shared.sx.parser import parse_all
from shared.sx.ref.sx_ref import categorize_special_forms
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
if not os.path.isdir(ref_dir):
ref_dir = "/app/shared/sx/ref"
ref_dir = _ref_dir()
spec_path = os.path.join(ref_dir, "special-forms.sx")
with open(spec_path) as f:
exprs = parse_all(f.read())
# Categories inferred from comment sections in the file.
# We assign forms to categories based on their order in the spec.
categories: dict[str, list[dict]] = {}
current_category = "Other"
# Map form names to categories
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",
}
for expr in exprs:
if not isinstance(expr, list) or len(expr) < 2:
continue
head = expr[0]
if not isinstance(head, Symbol) or head.name != "define-special-form":
continue
name = expr[1]
# Extract keyword args
kwargs: dict[str, str] = {}
i = 2
while i < len(expr) - 1:
if isinstance(expr[i], Keyword):
key = expr[i].name
val = expr[i + 1]
if isinstance(val, list):
# For :syntax, avoid quote sugar (quasiquote → `x)
items = [serialize(item) for item in val]
kwargs[key] = "(" + " ".join(items) + ")"
else:
kwargs[key] = str(val)
i += 2
else:
i += 1
category = category_map.get(name, "Other")
if category not in categories:
categories[category] = []
categories[category].append({
"name": name,
"syntax": kwargs.get("syntax", ""),
"doc": kwargs.get("doc", ""),
"tail-position": kwargs.get("tail-position", ""),
"example": kwargs.get("example", ""),
})
return categories
return categorize_special_forms(exprs)
def _reference_data(slug: str) -> dict:
"""Return reference table data for a given slug.
Returns a dict whose keys become SX env bindings:
- attributes: req-attrs, beh-attrs, uniq-attrs
- headers: req-headers, resp-headers
- events: events-list
- js-api: js-api-list
"""
"""Return reference table data for a given slug."""
from content.pages import (
REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS,
REQUEST_HEADERS, RESPONSE_HEADERS,
EVENTS, JS_API, ATTR_DETAILS, HEADER_DETAILS,
)
from shared.sx.ref.sx_ref import build_reference_data
# Build raw data dict and detail keys based on slug
if slug == "attributes":
return {
"req-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in REQUEST_ATTRS
],
"beh-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in BEHAVIOR_ATTRS
],
"uniq-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in SX_UNIQUE_ATTRS
],
raw = {
"req-attrs": [list(t) for t in REQUEST_ATTRS],
"beh-attrs": [list(t) for t in BEHAVIOR_ATTRS],
"uniq-attrs": [list(t) for t in SX_UNIQUE_ATTRS],
}
detail_keys = list(ATTR_DETAILS.keys())
elif slug == "headers":
return {
"req-headers": [
{"name": n, "value": v, "desc": d,
"href": f"/hypermedia/reference/headers/{n}" if n in HEADER_DETAILS else None}
for n, v, d in REQUEST_HEADERS
],
"resp-headers": [
{"name": n, "value": v, "desc": d,
"href": f"/hypermedia/reference/headers/{n}" if n in HEADER_DETAILS else None}
for n, v, d in RESPONSE_HEADERS
],
raw = {
"req-headers": [list(t) for t in REQUEST_HEADERS],
"resp-headers": [list(t) for t in RESPONSE_HEADERS],
}
detail_keys = list(HEADER_DETAILS.keys())
elif slug == "events":
from content.pages import EVENT_DETAILS
return {
"events-list": [
{"name": n, "desc": d,
"href": f"/hypermedia/reference/events/{n}" if n in EVENT_DETAILS else None}
for n, d in EVENTS
],
}
raw = {"events-list": [list(t) for t in EVENTS]}
detail_keys = list(EVENT_DETAILS.keys())
elif slug == "js-api":
return {
"js-api-list": [
{"name": n, "desc": d}
for n, d in JS_API
],
raw = {"js-api-list": [list(t) for t in JS_API]}
detail_keys = []
else:
raw = {
"req-attrs": [list(t) for t in REQUEST_ATTRS],
"beh-attrs": [list(t) for t in BEHAVIOR_ATTRS],
"uniq-attrs": [list(t) for t in SX_UNIQUE_ATTRS],
}
# Default — return attrs data for fallback
return {
"req-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in REQUEST_ATTRS
],
"beh-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in BEHAVIOR_ATTRS
],
"uniq-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in SX_UNIQUE_ATTRS
],
}
detail_keys = list(ATTR_DETAILS.keys())
return build_reference_data(slug, raw, detail_keys)
def _read_spec_file(filename: str) -> str:
@@ -425,6 +318,7 @@ def _js_self_hosting_data(ref_dir: str) -> dict:
return {
"bootstrapper-not-found": None,
"js-sx-source": js_sx_source,
"defines-matched": str(total),
"defines-total": str(total),
"js-sx-lines": str(len(js_sx_source.splitlines())),
"verification-status": status,
@@ -438,6 +332,7 @@ def _bundle_analyzer_data() -> dict:
from shared.sx.deps import components_needed, scan_components_from_sx
from shared.sx.parser import serialize
from shared.sx.types import Component, Macro
from shared.sx.ref.sx_ref import build_bundle_analysis
env = get_component_env()
total_components = sum(1 for v in env.values() if isinstance(v, Component))
@@ -445,68 +340,47 @@ def _bundle_analyzer_data() -> dict:
pure_count = sum(1 for v in env.values() if isinstance(v, Component) and v.is_pure)
io_count = total_components - pure_count
pages_data = []
# Extract raw data at I/O edge — Python accesses Component objects, serializes bodies
pages_raw = []
components_raw: dict[str, dict] = {}
for name, page_def in sorted(get_all_pages("sx").items()):
content_sx = serialize(page_def.content_expr)
direct = scan_components_from_sx(content_sx)
needed = components_needed(content_sx, env)
n = len(needed)
pct = round(n / total_components * 100) if total_components else 0
savings = 100 - pct
needed = sorted(components_needed(content_sx, env))
# IO classification + component details for this page
pure_in_page = 0
io_in_page = 0
page_io_refs: set[str] = set()
comp_details = []
for comp_name in sorted(needed):
val = env.get(comp_name)
if isinstance(val, Component):
is_pure = val.is_pure
if is_pure:
pure_in_page += 1
else:
io_in_page += 1
page_io_refs.update(val.io_refs)
# Reconstruct defcomp source
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body, pretty=True)
source = f"(defcomp ~{val.name} {params_sx}\n {body_sx})"
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,
})
for comp_name in needed:
if comp_name not in components_raw:
val = env.get(comp_name)
if isinstance(val, Component):
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body, pretty=True)
components_raw[comp_name] = {
"is-pure": val.is_pure,
"affinity": val.affinity,
"render-target": val.render_target,
"io-refs": sorted(val.io_refs),
"deps": sorted(val.deps),
"source": f"(defcomp ~{val.name} {params_sx}\n {body_sx})",
}
pages_data.append({
pages_raw.append({
"name": name,
"path": page_def.path,
"direct": len(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,
"needed-names": needed,
})
pages_data.sort(key=lambda p: p["needed"], reverse=True)
return {
"pages": pages_data,
"total-components": total_components,
"total-macros": total_macros,
"pure-count": pure_count,
"io-count": io_count,
}
# Pure data transformation in SX spec
result = build_bundle_analysis(
pages_raw, components_raw,
total_components, total_macros, pure_count, io_count,
)
# Sort pages by needed count (descending) — SX has no sort primitive
result["pages"] = sorted(result["pages"], key=lambda p: p["needed"], reverse=True)
return result
def _routing_analyzer_data() -> dict:
@@ -514,12 +388,11 @@ def _routing_analyzer_data() -> dict:
from shared.sx.pages import get_all_pages
from shared.sx.parser import serialize as sx_serialize
from shared.sx.helpers import _sx_literal
from shared.sx.ref.sx_ref import build_routing_analysis
pages_data = []
full_content: list[tuple[str, str, bool]] = [] # (name, full_content, has_data)
client_count = 0
server_count = 0
# I/O edge: extract page data from page registry
pages_raw = []
full_content: list[tuple[str, str, bool]] = []
for name, page_def in sorted(get_all_pages("sx").items()):
has_data = page_def.data_expr is not None
content_src = ""
@@ -528,37 +401,21 @@ def _routing_analyzer_data() -> dict:
content_src = sx_serialize(page_def.content_expr)
except Exception:
pass
pages_raw.append({
"name": name, "path": page_def.path,
"has-data": has_data, "content-src": content_src,
})
full_content.append((name, content_src, has_data))
# Determine routing mode and reason
if has_data:
mode = "server"
reason = "Has :data expression — needs server IO"
server_count += 1
elif not content_src:
mode = "server"
reason = "No content expression"
server_count += 1
else:
mode = "client"
reason = ""
client_count += 1
# Pure classification in SX spec
result = build_routing_analysis(pages_raw)
# Sort: client pages first, then server (SX has no sort primitive)
result["pages"] = sorted(
result["pages"],
key=lambda p: (0 if p["mode"] == "client" else 1, p["name"]),
)
pages_data.append({
"name": name,
"path": page_def.path,
"mode": mode,
"has-data": has_data,
"content-expr": content_src[:80] + ("..." if len(content_src) > 80 else ""),
"reason": reason,
})
# Sort: client pages first, then server
pages_data.sort(key=lambda p: (0 if p["mode"] == "client" else 1, p["name"]))
# Build a sample of the SX page registry format (use full content, first 3)
total = client_count + server_count
# Build registry sample (uses _sx_literal which is Python string escaping)
sample_entries = []
sorted_full = sorted(full_content, key=lambda x: x[0])
for name, csrc, hd in sorted_full[:3]:
@@ -574,86 +431,50 @@ def _routing_analyzer_data() -> dict:
+ "\n :closure {}}"
)
sample_entries.append(entry)
registry_sample = "\n\n".join(sample_entries)
result["registry-sample"] = "\n\n".join(sample_entries)
return {
"pages": pages_data,
"total-pages": total,
"client-count": client_count,
"server-count": server_count,
"registry-sample": registry_sample,
}
return result
def _attr_detail_data(slug: str) -> dict:
"""Return attribute detail data for a specific attribute slug.
Returns a dict whose keys become SX env bindings:
- attr-title, attr-description, attr-example, attr-handler
- attr-demo (component call or None)
- attr-wire-id (wire placeholder id or None)
- attr-not-found (truthy if not found)
"""
"""Return attribute detail data for a specific attribute slug."""
from content.pages import ATTR_DETAILS
from shared.sx.helpers import sx_call
from shared.sx.ref.sx_ref import build_attr_detail
detail = ATTR_DETAILS.get(slug)
if not detail:
return {"attr-not-found": True}
demo_name = detail.get("demo")
wire_id = None
if "handler" in detail:
wire_id = f"ref-wire-{slug.replace(':', '-').replace('*', 'star')}"
return {
"attr-not-found": None,
"attr-title": slug,
"attr-description": detail["description"],
"attr-example": detail["example"],
"attr-handler": detail.get("handler"),
"attr-demo": sx_call(demo_name) if demo_name else None,
"attr-wire-id": wire_id,
}
result = build_attr_detail(slug, detail)
# Convert demo name to sx_call if present
demo_name = result.get("attr-demo")
if demo_name:
result["attr-demo"] = sx_call(demo_name)
return result
def _header_detail_data(slug: str) -> dict:
"""Return header detail data for a specific header slug."""
from content.pages import HEADER_DETAILS
from shared.sx.helpers import sx_call
from shared.sx.ref.sx_ref import build_header_detail
detail = HEADER_DETAILS.get(slug)
if not detail:
return {"header-not-found": True}
demo_name = detail.get("demo")
return {
"header-not-found": None,
"header-title": slug,
"header-direction": detail["direction"],
"header-description": detail["description"],
"header-example": detail.get("example"),
"header-demo": sx_call(demo_name) if demo_name else None,
}
result = build_header_detail(slug, HEADER_DETAILS.get(slug))
demo_name = result.get("header-demo")
if demo_name:
result["header-demo"] = sx_call(demo_name)
return result
def _event_detail_data(slug: str) -> dict:
"""Return event detail data for a specific event slug."""
from content.pages import EVENT_DETAILS
from shared.sx.helpers import sx_call
from shared.sx.ref.sx_ref import build_event_detail
detail = EVENT_DETAILS.get(slug)
if not detail:
return {"event-not-found": True}
demo_name = detail.get("demo")
return {
"event-not-found": None,
"event-title": slug,
"event-description": detail["description"],
"event-example": detail.get("example"),
"event-demo": sx_call(demo_name) if demo_name else None,
}
result = build_event_detail(slug, EVENT_DETAILS.get(slug))
demo_name = result.get("event-demo")
if demo_name:
result["event-demo"] = sx_call(demo_name)
return result
def _run_spec_tests() -> dict:
@@ -1089,35 +910,30 @@ def _affinity_demo_data() -> dict:
from shared.sx.jinja_bridge import get_component_env
from shared.sx.types import Component
from shared.sx.pages import get_all_pages
from shared.sx.ref.sx_ref import build_affinity_analysis
# I/O edge: extract component data and page render plans
env = get_component_env()
demo_names = [
"~aff-demo-auto",
"~aff-demo-client",
"~aff-demo-server",
"~aff-demo-io-auto",
"~aff-demo-io-client",
"~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,
"name": name, "affinity": val.affinity,
"render-target": val.render_target,
"io-refs": sorted(val.io_refs),
"is-pure": val.is_pure,
"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,
"name": page_def.name, "path": page_def.path,
"server-count": len(plan.get("server", [])),
"client-count": len(plan.get("client", [])),
"server": plan.get("server", []),
@@ -1125,7 +941,7 @@ def _affinity_demo_data() -> dict:
"io-deps": plan.get("io-deps", []),
})
return {"components": components, "page-plans": page_plans}
return build_affinity_analysis(components, page_plans)
def _optimistic_demo_data() -> dict:
@@ -1271,3 +1087,84 @@ def _offline_demo_data() -> dict:
],
"server-time": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
def _page_helpers_demo_data() -> dict:
"""Run page-helpers.sx functions server-side, return results for comparison with client."""
import os
import time
from shared.sx.parser import parse_all
from shared.sx.ref.sx_ref import (
categorize_special_forms, build_reference_data,
build_attr_detail, build_component_source,
build_routing_analysis,
)
ref_dir = _ref_dir()
results = {}
# 1. categorize-special-forms
t0 = time.monotonic()
with open(os.path.join(ref_dir, "special-forms.sx")) as f:
sf_exprs = parse_all(f.read())
sf_result = categorize_special_forms(sf_exprs)
sf_ms = round((time.monotonic() - t0) * 1000, 1)
sf_summary = {cat: len(forms) for cat, forms in sf_result.items()}
results["sf-categories"] = sf_summary
results["sf-total"] = sum(sf_summary.values())
results["sf-ms"] = sf_ms
# 2. build-reference-data
from content.pages import REQUEST_ATTRS, ATTR_DETAILS
t1 = time.monotonic()
ref_result = build_reference_data("attributes", {
"req-attrs": [list(t) for t in REQUEST_ATTRS[:5]],
"beh-attrs": [], "uniq-attrs": [],
}, list(ATTR_DETAILS.keys()))
ref_ms = round((time.monotonic() - t1) * 1000, 1)
results["ref-sample"] = ref_result.get("req-attrs", [])[:3]
results["ref-ms"] = ref_ms
# 3. build-attr-detail
t2 = time.monotonic()
detail = ATTR_DETAILS.get("sx-get")
attr_result = build_attr_detail("sx-get", detail)
attr_ms = round((time.monotonic() - t2) * 1000, 1)
results["attr-result"] = attr_result
results["attr-ms"] = attr_ms
# 4. build-component-source
t3 = time.monotonic()
comp_result = build_component_source({
"type": "component", "name": "~demo-card",
"params": ["title", "subtitle"],
"has-children": True,
"body-sx": "(div :class \"card\"\n (h2 title)\n (when subtitle (p subtitle))\n children)",
"affinity": "auto",
})
comp_ms = round((time.monotonic() - t3) * 1000, 1)
results["comp-source"] = comp_result
results["comp-ms"] = comp_ms
# 5. build-routing-analysis
t4 = time.monotonic()
routing_result = build_routing_analysis([
{"name": "home", "path": "/", "has-data": False, "content-src": "(~home-content)"},
{"name": "dashboard", "path": "/dash", "has-data": True, "content-src": "(~dashboard)"},
{"name": "about", "path": "/about", "has-data": False, "content-src": "(~about-content)"},
{"name": "settings", "path": "/settings", "has-data": True, "content-src": "(~settings)"},
])
routing_ms = round((time.monotonic() - t4) * 1000, 1)
results["routing-result"] = routing_result
results["routing-ms"] = routing_ms
# Total
results["server-total-ms"] = round(sf_ms + ref_ms + attr_ms + comp_ms + routing_ms, 1)
# Pass raw inputs for client-side island (serialized as data-sx-state)
results["sf-source"] = open(os.path.join(ref_dir, "special-forms.sx")).read()
results["attr-detail"] = detail
results["req-attrs"] = [list(t) for t in REQUEST_ATTRS[:5]]
results["attr-keys"] = list(ATTR_DETAILS.keys())
return results