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:
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user