Add macros, declarative handlers (defhandler), and convert all fragment routes to sx
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
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:
837
shared/sx/async_eval.py
Normal file
837
shared/sx/async_eval.py
Normal file
@@ -0,0 +1,837 @@
|
||||
"""
|
||||
Async s-expression evaluator and HTML renderer.
|
||||
|
||||
Mirrors the sync evaluator (evaluator.py) and HTML renderer (html.py) but
|
||||
every step is ``async`` so I/O primitives can be ``await``ed inline.
|
||||
|
||||
This is the execution engine for ``defhandler`` — handlers contain I/O
|
||||
calls (``query``, ``service``, ``request-arg``, etc.) interleaved with
|
||||
control flow (``if``, ``let``, ``map``, ``when``). The sync
|
||||
collect-then-substitute resolver can't handle data dependencies between
|
||||
I/O results and control flow, so handlers need inline async evaluation.
|
||||
|
||||
Usage::
|
||||
|
||||
from shared.sx.async_eval import async_render
|
||||
|
||||
html = await async_render(handler_def.body, env, ctx)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from .evaluator import _expand_macro, EvalError
|
||||
from .primitives import _PRIMITIVES
|
||||
from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
|
||||
from .html import (
|
||||
HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
|
||||
escape_text, escape_attr, _RawHTML, css_class_collector,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Async evaluate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def async_eval(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
|
||||
"""Evaluate *expr* in *env*, awaiting I/O primitives inline."""
|
||||
# --- literals ---
|
||||
if isinstance(expr, (int, float, str, bool)):
|
||||
return expr
|
||||
if expr is None or expr is NIL:
|
||||
return NIL
|
||||
|
||||
# --- symbol lookup ---
|
||||
if isinstance(expr, Symbol):
|
||||
name = expr.name
|
||||
if name in env:
|
||||
return env[name]
|
||||
if name in _PRIMITIVES:
|
||||
return _PRIMITIVES[name]
|
||||
if name == "true":
|
||||
return True
|
||||
if name == "false":
|
||||
return False
|
||||
if name == "nil":
|
||||
return NIL
|
||||
raise EvalError(f"Undefined symbol: {name}")
|
||||
|
||||
# --- keyword ---
|
||||
if isinstance(expr, Keyword):
|
||||
return expr.name
|
||||
|
||||
# --- dict literal ---
|
||||
if isinstance(expr, dict):
|
||||
return {k: await async_eval(v, env, ctx) for k, v in expr.items()}
|
||||
|
||||
# --- list ---
|
||||
if not isinstance(expr, list):
|
||||
return expr
|
||||
if not expr:
|
||||
return []
|
||||
|
||||
head = expr[0]
|
||||
|
||||
if not isinstance(head, (Symbol, Lambda, list)):
|
||||
return [await async_eval(x, env, ctx) for x in expr]
|
||||
|
||||
if isinstance(head, Symbol):
|
||||
name = head.name
|
||||
|
||||
# I/O primitives — await inline
|
||||
if name in IO_PRIMITIVES:
|
||||
args, kwargs = await _parse_io_args(expr[1:], env, ctx)
|
||||
return await execute_io(name, args, kwargs, ctx)
|
||||
|
||||
# Special forms
|
||||
sf = _ASYNC_SPECIAL_FORMS.get(name)
|
||||
if sf is not None:
|
||||
return await sf(expr, env, ctx)
|
||||
|
||||
ho = _ASYNC_HO_FORMS.get(name)
|
||||
if ho is not None:
|
||||
return await ho(expr, env, ctx)
|
||||
|
||||
# Macro expansion
|
||||
if name in env:
|
||||
val = env[name]
|
||||
if isinstance(val, Macro):
|
||||
expanded = _expand_macro(val, expr[1:], env)
|
||||
return await async_eval(expanded, env, ctx)
|
||||
|
||||
# Render forms in eval position — delegate to renderer and return
|
||||
# the HTML string. Allows (let ((x (<> ...))) ...) etc.
|
||||
if name in ("<>", "raw!") or name in HTML_TAGS:
|
||||
return await _arender(expr, env, ctx)
|
||||
|
||||
# --- function / lambda call ---
|
||||
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)):
|
||||
return fn(*args)
|
||||
if isinstance(fn, Lambda):
|
||||
return await _async_call_lambda(fn, args, env, ctx)
|
||||
if isinstance(fn, Component):
|
||||
return await _async_call_component(fn, expr[1:], env, ctx)
|
||||
raise EvalError(f"Not callable: {fn!r}")
|
||||
|
||||
|
||||
async def _parse_io_args(
|
||||
exprs: list[Any], env: dict[str, Any], ctx: RequestContext,
|
||||
) -> tuple[list[Any], dict[str, Any]]:
|
||||
"""Parse and evaluate I/O node args."""
|
||||
args: list[Any] = []
|
||||
kwargs: dict[str, Any] = {}
|
||||
i = 0
|
||||
while i < len(exprs):
|
||||
item = exprs[i]
|
||||
if isinstance(item, Keyword) and i + 1 < len(exprs):
|
||||
kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx)
|
||||
i += 2
|
||||
else:
|
||||
args.append(await async_eval(item, env, ctx))
|
||||
i += 1
|
||||
return args, kwargs
|
||||
|
||||
|
||||
async def _async_call_lambda(
|
||||
fn: Lambda, args: list[Any], caller_env: dict[str, Any], ctx: RequestContext,
|
||||
) -> Any:
|
||||
if len(args) != len(fn.params):
|
||||
raise EvalError(f"{fn!r} expects {len(fn.params)} args, got {len(args)}")
|
||||
local = dict(fn.closure)
|
||||
local.update(caller_env)
|
||||
for p, v in zip(fn.params, args):
|
||||
local[p] = v
|
||||
return await async_eval(fn.body, local, ctx)
|
||||
|
||||
|
||||
async def _async_call_component(
|
||||
comp: Component, raw_args: list[Any], env: dict[str, Any], ctx: RequestContext,
|
||||
) -> Any:
|
||||
kwargs: dict[str, Any] = {}
|
||||
children: list[Any] = []
|
||||
i = 0
|
||||
while i < len(raw_args):
|
||||
arg = raw_args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(raw_args):
|
||||
kwargs[arg.name] = await async_eval(raw_args[i + 1], env, ctx)
|
||||
i += 2
|
||||
else:
|
||||
children.append(await async_eval(arg, env, ctx))
|
||||
i += 1
|
||||
local = dict(comp.closure)
|
||||
local.update(env)
|
||||
for p in comp.params:
|
||||
local[p] = kwargs.get(p, NIL)
|
||||
if comp.has_children:
|
||||
local["children"] = children
|
||||
return await async_eval(comp.body, local, ctx)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Async special forms
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _asf_if(expr, env, ctx):
|
||||
cond = await async_eval(expr[1], env, ctx)
|
||||
if cond and cond is not NIL:
|
||||
return await async_eval(expr[2], env, ctx)
|
||||
if len(expr) > 3:
|
||||
return await async_eval(expr[3], env, ctx)
|
||||
return NIL
|
||||
|
||||
|
||||
async def _asf_when(expr, env, ctx):
|
||||
cond = await async_eval(expr[1], env, ctx)
|
||||
if cond and cond is not NIL:
|
||||
result = NIL
|
||||
for body_expr in expr[2:]:
|
||||
result = await async_eval(body_expr, env, ctx)
|
||||
return result
|
||||
return NIL
|
||||
|
||||
|
||||
async def _asf_and(expr, env, ctx):
|
||||
result: Any = True
|
||||
for arg in expr[1:]:
|
||||
result = await async_eval(arg, env, ctx)
|
||||
if not result:
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
async def _asf_or(expr, env, ctx):
|
||||
result: Any = False
|
||||
for arg in expr[1:]:
|
||||
result = await async_eval(arg, env, ctx)
|
||||
if result:
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
async def _asf_let(expr, env, ctx):
|
||||
bindings = expr[1]
|
||||
local = dict(env)
|
||||
if isinstance(bindings, list):
|
||||
if bindings and isinstance(bindings[0], list):
|
||||
for binding in bindings:
|
||||
var = binding[0]
|
||||
vname = var.name if isinstance(var, Symbol) else var
|
||||
local[vname] = await async_eval(binding[1], local, ctx)
|
||||
elif len(bindings) % 2 == 0:
|
||||
for i in range(0, len(bindings), 2):
|
||||
var = bindings[i]
|
||||
vname = var.name if isinstance(var, Symbol) else var
|
||||
local[vname] = await async_eval(bindings[i + 1], local, ctx)
|
||||
result: Any = NIL
|
||||
for body_expr in expr[2:]:
|
||||
result = await async_eval(body_expr, local, ctx)
|
||||
return result
|
||||
|
||||
|
||||
async def _asf_lambda(expr, env, ctx):
|
||||
params_expr = expr[1]
|
||||
param_names = []
|
||||
for p in params_expr:
|
||||
if isinstance(p, Symbol):
|
||||
param_names.append(p.name)
|
||||
elif isinstance(p, str):
|
||||
param_names.append(p)
|
||||
return Lambda(param_names, expr[2], dict(env))
|
||||
|
||||
|
||||
async def _asf_define(expr, env, ctx):
|
||||
name_sym = expr[1]
|
||||
value = await async_eval(expr[2], env, ctx)
|
||||
if isinstance(value, Lambda) and value.name is None:
|
||||
value.name = name_sym.name
|
||||
env[name_sym.name] = value
|
||||
return value
|
||||
|
||||
|
||||
async def _asf_defcomp(expr, env, ctx):
|
||||
from .evaluator import _sf_defcomp
|
||||
return _sf_defcomp(expr, env)
|
||||
|
||||
|
||||
async def _asf_defmacro(expr, env, ctx):
|
||||
from .evaluator import _sf_defmacro
|
||||
return _sf_defmacro(expr, env)
|
||||
|
||||
|
||||
async def _asf_defhandler(expr, env, ctx):
|
||||
from .evaluator import _sf_defhandler
|
||||
return _sf_defhandler(expr, env)
|
||||
|
||||
|
||||
async def _asf_begin(expr, env, ctx):
|
||||
result: Any = NIL
|
||||
for sub in expr[1:]:
|
||||
result = await async_eval(sub, env, ctx)
|
||||
return result
|
||||
|
||||
|
||||
async def _asf_quote(expr, env, ctx):
|
||||
return expr[1] if len(expr) > 1 else NIL
|
||||
|
||||
|
||||
async def _asf_quasiquote(expr, env, ctx):
|
||||
return await _async_qq_expand(expr[1], env, ctx)
|
||||
|
||||
|
||||
async def _async_qq_expand(template, env, ctx):
|
||||
if not isinstance(template, list):
|
||||
return template
|
||||
if not template:
|
||||
return []
|
||||
head = template[0]
|
||||
if isinstance(head, Symbol):
|
||||
if head.name == "unquote":
|
||||
return await async_eval(template[1], env, ctx)
|
||||
if head.name == "splice-unquote":
|
||||
raise EvalError("splice-unquote not inside a list")
|
||||
result: list[Any] = []
|
||||
for item in template:
|
||||
if (isinstance(item, list) and len(item) == 2
|
||||
and isinstance(item[0], Symbol) and item[0].name == "splice-unquote"):
|
||||
spliced = await async_eval(item[1], env, ctx)
|
||||
if isinstance(spliced, list):
|
||||
result.extend(spliced)
|
||||
elif spliced is not None and spliced is not NIL:
|
||||
result.append(spliced)
|
||||
else:
|
||||
result.append(await _async_qq_expand(item, env, ctx))
|
||||
return result
|
||||
|
||||
|
||||
async def _asf_cond(expr, env, ctx):
|
||||
clauses = expr[1:]
|
||||
if not clauses:
|
||||
return NIL
|
||||
if (isinstance(clauses[0], list) and len(clauses[0]) == 2
|
||||
and not (isinstance(clauses[0][0], Symbol) and clauses[0][0].name in (
|
||||
"=", "<", ">", "<=", ">=", "!=", "and", "or"))):
|
||||
for clause in clauses:
|
||||
test = clause[0]
|
||||
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
||||
return await async_eval(clause[1], env, ctx)
|
||||
if isinstance(test, Keyword) and test.name == "else":
|
||||
return await async_eval(clause[1], env, ctx)
|
||||
if await async_eval(test, env, ctx):
|
||||
return await async_eval(clause[1], env, ctx)
|
||||
else:
|
||||
i = 0
|
||||
while i < len(clauses) - 1:
|
||||
test = clauses[i]
|
||||
result = clauses[i + 1]
|
||||
if isinstance(test, Keyword) and test.name == "else":
|
||||
return await async_eval(result, env, ctx)
|
||||
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
||||
return await async_eval(result, env, ctx)
|
||||
if await async_eval(test, env, ctx):
|
||||
return await async_eval(result, env, ctx)
|
||||
i += 2
|
||||
return NIL
|
||||
|
||||
|
||||
async def _asf_case(expr, env, ctx):
|
||||
match_val = await async_eval(expr[1], env, ctx)
|
||||
clauses = expr[2:]
|
||||
i = 0
|
||||
while i < len(clauses) - 1:
|
||||
test = clauses[i]
|
||||
result = clauses[i + 1]
|
||||
if isinstance(test, Keyword) and test.name == "else":
|
||||
return await async_eval(result, env, ctx)
|
||||
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
||||
return await async_eval(result, env, ctx)
|
||||
if match_val == await async_eval(test, env, ctx):
|
||||
return await async_eval(result, env, ctx)
|
||||
i += 2
|
||||
return NIL
|
||||
|
||||
|
||||
async def _asf_thread_first(expr, env, ctx):
|
||||
result = await async_eval(expr[1], env, ctx)
|
||||
for form in expr[2:]:
|
||||
if isinstance(form, list):
|
||||
fn = await async_eval(form[0], env, ctx)
|
||||
args = [result] + [await async_eval(a, env, ctx) for a in form[1:]]
|
||||
else:
|
||||
fn = await async_eval(form, env, ctx)
|
||||
args = [result]
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||
result = fn(*args)
|
||||
elif isinstance(fn, Lambda):
|
||||
result = await _async_call_lambda(fn, args, env, ctx)
|
||||
else:
|
||||
raise EvalError(f"-> form not callable: {fn!r}")
|
||||
return result
|
||||
|
||||
|
||||
async def _asf_set_bang(expr, env, ctx):
|
||||
value = await async_eval(expr[2], env, ctx)
|
||||
env[expr[1].name] = value
|
||||
return value
|
||||
|
||||
|
||||
_ASYNC_SPECIAL_FORMS: dict[str, Any] = {
|
||||
"if": _asf_if,
|
||||
"when": _asf_when,
|
||||
"cond": _asf_cond,
|
||||
"case": _asf_case,
|
||||
"and": _asf_and,
|
||||
"or": _asf_or,
|
||||
"let": _asf_let,
|
||||
"let*": _asf_let,
|
||||
"lambda": _asf_lambda,
|
||||
"fn": _asf_lambda,
|
||||
"define": _asf_define,
|
||||
"defcomp": _asf_defcomp,
|
||||
"defmacro": _asf_defmacro,
|
||||
"defhandler": _asf_defhandler,
|
||||
"begin": _asf_begin,
|
||||
"do": _asf_begin,
|
||||
"quote": _asf_quote,
|
||||
"quasiquote": _asf_quasiquote,
|
||||
"->": _asf_thread_first,
|
||||
"set!": _asf_set_bang,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Async higher-order forms
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _aho_map(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
coll = await async_eval(expr[2], env, ctx)
|
||||
results = []
|
||||
for item in coll:
|
||||
if isinstance(fn, Lambda):
|
||||
results.append(await _async_call_lambda(fn, [item], env, ctx))
|
||||
elif callable(fn):
|
||||
results.append(fn(item))
|
||||
else:
|
||||
raise EvalError(f"map requires callable, got {type(fn).__name__}")
|
||||
return results
|
||||
|
||||
|
||||
async def _aho_map_indexed(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
coll = await async_eval(expr[2], env, ctx)
|
||||
results = []
|
||||
for i, item in enumerate(coll):
|
||||
if isinstance(fn, Lambda):
|
||||
results.append(await _async_call_lambda(fn, [i, item], env, ctx))
|
||||
elif callable(fn):
|
||||
results.append(fn(i, item))
|
||||
else:
|
||||
raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
|
||||
return results
|
||||
|
||||
|
||||
async def _aho_filter(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
coll = await async_eval(expr[2], env, ctx)
|
||||
results = []
|
||||
for item in coll:
|
||||
if isinstance(fn, Lambda):
|
||||
val = await _async_call_lambda(fn, [item], env, ctx)
|
||||
elif callable(fn):
|
||||
val = fn(item)
|
||||
else:
|
||||
raise EvalError(f"filter requires callable, got {type(fn).__name__}")
|
||||
if val:
|
||||
results.append(item)
|
||||
return results
|
||||
|
||||
|
||||
async def _aho_reduce(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
acc = await async_eval(expr[2], env, ctx)
|
||||
coll = await async_eval(expr[3], env, ctx)
|
||||
for item in coll:
|
||||
acc = await _async_call_lambda(fn, [acc, item], env, ctx) if isinstance(fn, Lambda) else fn(acc, item)
|
||||
return acc
|
||||
|
||||
|
||||
async def _aho_some(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
coll = await async_eval(expr[2], env, ctx)
|
||||
for item in coll:
|
||||
result = await _async_call_lambda(fn, [item], env, ctx) if isinstance(fn, Lambda) else fn(item)
|
||||
if result:
|
||||
return result
|
||||
return NIL
|
||||
|
||||
|
||||
async def _aho_every(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
coll = await async_eval(expr[2], env, ctx)
|
||||
for item in coll:
|
||||
if not (await _async_call_lambda(fn, [item], env, ctx) if isinstance(fn, Lambda) else fn(item)):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def _aho_for_each(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
coll = await async_eval(expr[2], env, ctx)
|
||||
for item in coll:
|
||||
if isinstance(fn, Lambda):
|
||||
await _async_call_lambda(fn, [item], env, ctx)
|
||||
elif callable(fn):
|
||||
fn(item)
|
||||
return NIL
|
||||
|
||||
|
||||
_ASYNC_HO_FORMS: dict[str, Any] = {
|
||||
"map": _aho_map,
|
||||
"map-indexed": _aho_map_indexed,
|
||||
"filter": _aho_filter,
|
||||
"reduce": _aho_reduce,
|
||||
"some": _aho_some,
|
||||
"every?": _aho_every,
|
||||
"for-each": _aho_for_each,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Async HTML renderer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def async_render(
|
||||
expr: Any,
|
||||
env: dict[str, Any],
|
||||
ctx: RequestContext | None = None,
|
||||
) -> str:
|
||||
"""Render an s-expression to HTML, awaiting I/O primitives inline."""
|
||||
if ctx is None:
|
||||
ctx = RequestContext()
|
||||
return await _arender(expr, env, ctx)
|
||||
|
||||
|
||||
async def _arender(expr: Any, env: dict[str, Any], ctx: RequestContext) -> str:
|
||||
if expr is None or expr is NIL or expr is False or expr is True:
|
||||
return ""
|
||||
if isinstance(expr, _RawHTML):
|
||||
return expr.html
|
||||
if isinstance(expr, str):
|
||||
return escape_text(expr)
|
||||
if isinstance(expr, (int, float)):
|
||||
return escape_text(str(expr))
|
||||
if isinstance(expr, Symbol):
|
||||
val = await async_eval(expr, env, ctx)
|
||||
return await _arender(val, env, ctx)
|
||||
if isinstance(expr, Keyword):
|
||||
return escape_text(expr.name)
|
||||
if isinstance(expr, list):
|
||||
if not expr:
|
||||
return ""
|
||||
return await _arender_list(expr, env, ctx)
|
||||
if isinstance(expr, dict):
|
||||
return ""
|
||||
return escape_text(str(expr))
|
||||
|
||||
|
||||
async def _arender_list(expr: list, env: dict[str, Any], ctx: RequestContext) -> str:
|
||||
head = expr[0]
|
||||
|
||||
if isinstance(head, Symbol):
|
||||
name = head.name
|
||||
|
||||
# I/O primitive — await, then render result
|
||||
if name in IO_PRIMITIVES:
|
||||
result = await async_eval(expr, env, ctx)
|
||||
return await _arender(result, env, ctx)
|
||||
|
||||
# raw!
|
||||
if name == "raw!":
|
||||
parts = []
|
||||
for arg in expr[1:]:
|
||||
val = await async_eval(arg, env, ctx)
|
||||
if isinstance(val, _RawHTML):
|
||||
parts.append(val.html)
|
||||
elif isinstance(val, str):
|
||||
parts.append(val)
|
||||
elif val is not None and val is not NIL:
|
||||
parts.append(str(val))
|
||||
return "".join(parts)
|
||||
|
||||
# <>
|
||||
if name == "<>":
|
||||
parts = []
|
||||
for child in expr[1:]:
|
||||
parts.append(await _arender(child, env, ctx))
|
||||
return "".join(parts)
|
||||
|
||||
# Render-aware special forms
|
||||
arsf = _ASYNC_RENDER_FORMS.get(name)
|
||||
if arsf is not None:
|
||||
return await arsf(expr, env, ctx)
|
||||
|
||||
# Macro expansion
|
||||
if name in env:
|
||||
val = env[name]
|
||||
if isinstance(val, Macro):
|
||||
expanded = _expand_macro(val, expr[1:], env)
|
||||
return await _arender(expanded, env, ctx)
|
||||
|
||||
# HTML tag
|
||||
if name in HTML_TAGS:
|
||||
return await _arender_element(name, expr[1:], env, ctx)
|
||||
|
||||
# Component
|
||||
if name.startswith("~"):
|
||||
val = env.get(name)
|
||||
if isinstance(val, Component):
|
||||
return await _arender_component(val, expr[1:], env, ctx)
|
||||
|
||||
# Fallback — evaluate then render
|
||||
result = await async_eval(expr, env, ctx)
|
||||
return await _arender(result, env, ctx)
|
||||
|
||||
if isinstance(head, (Lambda, list)):
|
||||
result = await async_eval(expr, env, ctx)
|
||||
return await _arender(result, env, ctx)
|
||||
|
||||
# Data list
|
||||
parts = []
|
||||
for item in expr:
|
||||
parts.append(await _arender(item, env, ctx))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
async def _arender_element(
|
||||
tag: str, args: list, env: dict[str, Any], ctx: RequestContext,
|
||||
) -> str:
|
||||
attrs: dict[str, Any] = {}
|
||||
children: list[Any] = []
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||
attr_val = await async_eval(args[i + 1], env, ctx)
|
||||
attrs[arg.name] = attr_val
|
||||
i += 2
|
||||
else:
|
||||
children.append(arg)
|
||||
i += 1
|
||||
|
||||
class_val = attrs.get("class")
|
||||
if class_val is not None and class_val is not NIL and class_val is not False:
|
||||
collector = css_class_collector.get(None)
|
||||
if collector is not None:
|
||||
collector.update(str(class_val).split())
|
||||
|
||||
parts = [f"<{tag}"]
|
||||
for attr_name, attr_val in attrs.items():
|
||||
if attr_val is None or attr_val is NIL or attr_val is False:
|
||||
continue
|
||||
if attr_name in BOOLEAN_ATTRS:
|
||||
if attr_val:
|
||||
parts.append(f" {attr_name}")
|
||||
elif attr_val is True:
|
||||
parts.append(f" {attr_name}")
|
||||
else:
|
||||
parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
|
||||
parts.append(">")
|
||||
opening = "".join(parts)
|
||||
|
||||
if tag in VOID_ELEMENTS:
|
||||
return opening
|
||||
|
||||
child_parts = []
|
||||
for child in children:
|
||||
child_parts.append(await _arender(child, env, ctx))
|
||||
return f"{opening}{''.join(child_parts)}</{tag}>"
|
||||
|
||||
|
||||
async def _arender_component(
|
||||
comp: Component, args: list, env: dict[str, Any], ctx: RequestContext,
|
||||
) -> str:
|
||||
kwargs: dict[str, Any] = {}
|
||||
children: list[Any] = []
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||
kwargs[arg.name] = await async_eval(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_html = []
|
||||
for c in children:
|
||||
child_html.append(await _arender(c, env, ctx))
|
||||
local["children"] = _RawHTML("".join(child_html))
|
||||
return await _arender(comp.body, local, ctx)
|
||||
|
||||
|
||||
async def _arender_lambda(
|
||||
fn: Lambda, args: tuple, env: dict[str, Any], ctx: RequestContext,
|
||||
) -> str:
|
||||
local = dict(fn.closure)
|
||||
local.update(env)
|
||||
for p, v in zip(fn.params, args):
|
||||
local[p] = v
|
||||
return await _arender(fn.body, local, ctx)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Async render-aware special forms
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _arsf_if(expr, env, ctx):
|
||||
cond = await async_eval(expr[1], env, ctx)
|
||||
if cond and cond is not NIL:
|
||||
return await _arender(expr[2], env, ctx)
|
||||
if len(expr) > 3:
|
||||
return await _arender(expr[3], env, ctx)
|
||||
return ""
|
||||
|
||||
|
||||
async def _arsf_when(expr, env, ctx):
|
||||
cond = await async_eval(expr[1], env, ctx)
|
||||
if cond and cond is not NIL:
|
||||
parts = []
|
||||
for body_expr in expr[2:]:
|
||||
parts.append(await _arender(body_expr, env, ctx))
|
||||
return "".join(parts)
|
||||
return ""
|
||||
|
||||
|
||||
async def _arsf_cond(expr, env, ctx):
|
||||
clauses = expr[1:]
|
||||
if not clauses:
|
||||
return ""
|
||||
if isinstance(clauses[0], list) and len(clauses[0]) == 2:
|
||||
for clause in clauses:
|
||||
test = clause[0]
|
||||
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
||||
return await _arender(clause[1], env, ctx)
|
||||
if isinstance(test, Keyword) and test.name == "else":
|
||||
return await _arender(clause[1], env, ctx)
|
||||
if await async_eval(test, env, ctx):
|
||||
return await _arender(clause[1], env, ctx)
|
||||
else:
|
||||
i = 0
|
||||
while i < len(clauses) - 1:
|
||||
test = clauses[i]
|
||||
result = clauses[i + 1]
|
||||
if isinstance(test, Keyword) and test.name == "else":
|
||||
return await _arender(result, env, ctx)
|
||||
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
||||
return await _arender(result, env, ctx)
|
||||
if await async_eval(test, env, ctx):
|
||||
return await _arender(result, env, ctx)
|
||||
i += 2
|
||||
return ""
|
||||
|
||||
|
||||
async def _arsf_let(expr, env, ctx):
|
||||
bindings = expr[1]
|
||||
local = dict(env)
|
||||
if isinstance(bindings, list):
|
||||
if bindings and isinstance(bindings[0], list):
|
||||
for binding in bindings:
|
||||
var = binding[0]
|
||||
vname = var.name if isinstance(var, Symbol) else var
|
||||
local[vname] = await async_eval(binding[1], local, ctx)
|
||||
elif len(bindings) % 2 == 0:
|
||||
for i in range(0, len(bindings), 2):
|
||||
var = bindings[i]
|
||||
vname = var.name if isinstance(var, Symbol) else var
|
||||
local[vname] = await async_eval(bindings[i + 1], local, ctx)
|
||||
parts = []
|
||||
for body_expr in expr[2:]:
|
||||
parts.append(await _arender(body_expr, local, ctx))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
async def _arsf_begin(expr, env, ctx):
|
||||
parts = []
|
||||
for sub in expr[1:]:
|
||||
parts.append(await _arender(sub, env, ctx))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
async def _arsf_define(expr, env, ctx):
|
||||
await async_eval(expr, env, ctx)
|
||||
return ""
|
||||
|
||||
|
||||
async def _arsf_map(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
coll = await async_eval(expr[2], env, ctx)
|
||||
parts = []
|
||||
for item in coll:
|
||||
if isinstance(fn, Lambda):
|
||||
parts.append(await _arender_lambda(fn, (item,), env, ctx))
|
||||
elif callable(fn):
|
||||
parts.append(await _arender(fn(item), env, ctx))
|
||||
else:
|
||||
parts.append(await _arender(item, env, ctx))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
async def _arsf_map_indexed(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
coll = await async_eval(expr[2], env, ctx)
|
||||
parts = []
|
||||
for i, item in enumerate(coll):
|
||||
if isinstance(fn, Lambda):
|
||||
parts.append(await _arender_lambda(fn, (i, item), env, ctx))
|
||||
elif callable(fn):
|
||||
parts.append(await _arender(fn(i, item), env, ctx))
|
||||
else:
|
||||
parts.append(await _arender(item, env, ctx))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
async def _arsf_filter(expr, env, ctx):
|
||||
result = await async_eval(expr, env, ctx)
|
||||
return await _arender(result, env, ctx)
|
||||
|
||||
|
||||
async def _arsf_for_each(expr, env, ctx):
|
||||
fn = await async_eval(expr[1], env, ctx)
|
||||
coll = await async_eval(expr[2], env, ctx)
|
||||
parts = []
|
||||
for item in coll:
|
||||
if isinstance(fn, Lambda):
|
||||
parts.append(await _arender_lambda(fn, (item,), env, ctx))
|
||||
elif callable(fn):
|
||||
parts.append(await _arender(fn(item), env, ctx))
|
||||
else:
|
||||
parts.append(await _arender(item, env, ctx))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
_ASYNC_RENDER_FORMS: dict[str, Any] = {
|
||||
"if": _arsf_if,
|
||||
"when": _arsf_when,
|
||||
"cond": _arsf_cond,
|
||||
"let": _arsf_let,
|
||||
"let*": _arsf_let,
|
||||
"begin": _arsf_begin,
|
||||
"do": _arsf_begin,
|
||||
"define": _arsf_define,
|
||||
"defcomp": _arsf_define,
|
||||
"defmacro": _arsf_define,
|
||||
"defhandler": _arsf_define,
|
||||
"map": _arsf_map,
|
||||
"map-indexed": _arsf_map_indexed,
|
||||
"filter": _arsf_filter,
|
||||
"for-each": _arsf_for_each,
|
||||
}
|
||||
Reference in New Issue
Block a user