Add macros, declarative handlers (defhandler), and convert all fragment routes to sx

Phase 1 — Macros: defmacro + quasiquote syntax (`, ,, ,@) in parser,
evaluator, HTML renderer, and JS mirror. Macro type, expansion, and
round-trip serialization.

Phase 2 — Expanded primitives: app-url, url-for, asset-url, config,
format-date, parse-int (pure); service, request-arg, request-path,
nav-tree, get-children (I/O); jinja-global, relations-from (pure).
Updated _io_service to accept (service "registry-name" "method" :kwargs)
with auto kebab→snake conversion. DTO-to-dict now expands datetime fields
into year/month/day convenience keys. Tuple returns converted to lists.

Phase 3 — Declarative handlers: HandlerDef type, defhandler special form,
handler registry (service → name → HandlerDef), async evaluator+renderer
(async_eval.py) that awaits I/O primitives inline within control flow.
Handler loading from .sx files, execute_handler, blueprint factory.

Phase 4 — Convert all fragment routes: 13 Python fragment handlers across
8 services replaced with declarative .sx handler files. All routes.py
simplified to uniform sx dispatch pattern. Two Jinja HTML handlers
(events/container-cards, events/account-page) kept as Python.

New files: shared/sx/async_eval.py, shared/sx/handlers.py,
shared/sx/tests/test_handlers.py, plus 13 handler .sx files under
{service}/sx/handlers/. MarketService.product_by_slug() added.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 00:22:18 +00:00
parent 13bcf755f6
commit ab75e505a8
48 changed files with 2538 additions and 638 deletions

View File

@@ -50,6 +50,15 @@
}
Component.prototype._component = true;
function Macro(params, restParam, body, closure, name) {
this.params = params;
this.restParam = restParam;
this.body = body;
this.closure = closure || {};
this.name = name || null;
}
Macro.prototype._macro = true;
/** Marker for pre-rendered HTML that bypasses escaping. */
function RawHTML(html) { this.html = html; }
RawHTML.prototype._raw = true;
@@ -58,6 +67,7 @@
function isKw(x) { return x && x._kw === true; }
function isLambda(x) { return x && x._lambda === true; }
function isComponent(x) { return x && x._component === true; }
function isMacro(x) { return x && x._macro === true; }
function isRaw(x) { return x && x._raw === true; }
// =========================================================================
@@ -181,6 +191,16 @@
if (raw === "(") { tok.next(); return parseList(tok, ")"); }
if (raw === "[") { tok.next(); return parseList(tok, "]"); }
if (raw === "{") { tok.next(); return parseMap(tok); }
// Quasiquote syntax
if (raw === "`") { tok._advance(1); return [new Symbol("quasiquote"), parseExpr(tok)]; }
if (raw === ",") {
tok._advance(1);
if (tok.pos < tok.text.length && tok.text[tok.pos] === "@") {
tok._advance(1);
return [new Symbol("splice-unquote"), parseExpr(tok)];
}
return [new Symbol("unquote"), parseExpr(tok)];
}
return tok.next();
}
@@ -372,6 +392,15 @@
if (sf) return sf(expr, env);
var ho = HO_FORMS[head.name];
if (ho) return ho(expr, env);
// Macro expansion
if (head.name in env) {
var macroVal = env[head.name];
if (isMacro(macroVal)) {
var expanded = expandMacro(macroVal, expr.slice(1), env);
return sxEval(expanded, env);
}
}
}
// Function call
@@ -576,6 +605,64 @@
return result;
};
SPECIAL_FORMS["defmacro"] = function (expr, env) {
var nameSym = expr[1];
var paramsExpr = expr[2];
var params = [], restParam = null;
for (var i = 0; i < paramsExpr.length; i++) {
var p = paramsExpr[i];
if (isSym(p) && p.name === "&rest") {
if (i + 1 < paramsExpr.length) {
var rp = paramsExpr[i + 1];
restParam = isSym(rp) ? rp.name : String(rp);
}
break;
}
if (isSym(p)) params.push(p.name);
else if (typeof p === "string") params.push(p);
}
var macro = new Macro(params, restParam, expr[3], merge({}, env), nameSym.name);
env[nameSym.name] = macro;
return macro;
};
SPECIAL_FORMS["quasiquote"] = function (expr, env) {
return qqExpand(expr[1], env);
};
function qqExpand(template, env) {
if (!Array.isArray(template)) return template;
if (!template.length) return [];
var head = template[0];
if (isSym(head)) {
if (head.name === "unquote") return sxEval(template[1], env);
if (head.name === "splice-unquote") throw new Error("splice-unquote not inside a list");
}
var result = [];
for (var i = 0; i < template.length; i++) {
var item = template[i];
if (Array.isArray(item) && item.length === 2 && isSym(item[0]) && item[0].name === "splice-unquote") {
var spliced = sxEval(item[1], env);
if (Array.isArray(spliced)) { for (var j = 0; j < spliced.length; j++) result.push(spliced[j]); }
else if (!isNil(spliced)) result.push(spliced);
} else {
result.push(qqExpand(item, env));
}
}
return result;
}
function expandMacro(macro, rawArgs, env) {
var local = merge({}, macro.closure, env);
for (var i = 0; i < macro.params.length; i++) {
local[macro.params[i]] = i < rawArgs.length ? rawArgs[i] : NIL;
}
if (macro.restParam !== null) {
local[macro.restParam] = rawArgs.slice(macro.params.length);
}
return sxEval(macro.body, local);
}
// --- Higher-order forms --------------------------------------------------
var HO_FORMS = {};
@@ -772,6 +859,8 @@
RENDER_FORMS["define"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
RENDER_FORMS["defcomp"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
RENDER_FORMS["defmacro"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
RENDER_FORMS["defhandler"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); };
RENDER_FORMS["map"] = function (expr, env) {
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
@@ -913,6 +1002,12 @@
// Render-aware special forms
if (RENDER_FORMS[name]) return RENDER_FORMS[name](expr, env);
// Macro expansion
if (name in env && isMacro(env[name])) {
var mExpanded = expandMacro(env[name], expr.slice(1), env);
return renderDOM(mExpanded, env);
}
// HTML tag
if (HTML_TAGS[name]) return renderElement(name, expr.slice(1), env);
@@ -1051,7 +1146,13 @@
for (var bi = 1; bi < expr.length; bi++) bs.push(renderStr(expr[bi], env));
return bs.join("");
}
if (name === "define" || name === "defcomp") { sxEval(expr, env); return ""; }
if (name === "define" || name === "defcomp" || name === "defmacro" || name === "defhandler") { sxEval(expr, env); return ""; }
// Macro expansion in string renderer
if (name in env && isMacro(env[name])) {
var smExp = expandMacro(env[name], expr.slice(1), env);
return renderStr(smExp, env);
}
// Higher-order forms — render-aware (lambda bodies may contain HTML/components)
if (name === "map") {