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