Delete account/sx/sx_components.py — all rendering now in .sx
Phase 1 of zero-Python rendering: account service. - Auth pages (login, device, check-email) use _render_auth_page() helper calling render_to_sx() + full_page_sx() directly in routes - Newsletter toggle POST renders inline via render_to_sx() - Newsletter page helper returns data dict; defpage :data slot fetches, :content slot renders via ~account-newsletters-content defcomp - Fragment page uses (frag ...) IO primitive directly in .sx - Defpage _eval_slot now uses async_eval_slot_to_sx which expands component bodies server-side (executing IO) but serializes tags as SX - Fix pre-existing OOB ParseError: _eval_slot was producing HTML instead of s-expressions for component content slots - Fix market url_for endpoint: defpage_market_home (app-level, not blueprint) - Fix events calendar nav: wrap multiple SX parts in fragment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -910,6 +910,42 @@ async def async_eval_to_sx(
|
||||
return serialize(result)
|
||||
|
||||
|
||||
async def async_eval_slot_to_sx(
|
||||
expr: Any,
|
||||
env: dict[str, Any],
|
||||
ctx: RequestContext | None = None,
|
||||
) -> str:
|
||||
"""Like async_eval_to_sx but expands component calls.
|
||||
|
||||
Used by defpage slot evaluation where the content expression is
|
||||
typically a component call like ``(~dashboard-content)``. Normal
|
||||
``async_eval_to_sx`` serializes component calls without expanding;
|
||||
this variant expands one level so IO primitives in the body execute,
|
||||
then serializes the result as SX wire format.
|
||||
"""
|
||||
if ctx is None:
|
||||
ctx = RequestContext()
|
||||
# If expr is a component call, expand it through _aser
|
||||
if isinstance(expr, list) and expr:
|
||||
head = expr[0]
|
||||
if isinstance(head, Symbol) and head.name.startswith("~"):
|
||||
comp = env.get(head.name)
|
||||
if isinstance(comp, Component):
|
||||
result = await _aser_component(comp, expr[1:], env, ctx)
|
||||
if isinstance(result, SxExpr):
|
||||
return result.source
|
||||
if result is None or result is NIL:
|
||||
return ""
|
||||
return serialize(result)
|
||||
# Fall back to normal async_eval_to_sx
|
||||
result = await _aser(expr, env, ctx)
|
||||
if isinstance(result, SxExpr):
|
||||
return result.source
|
||||
if result is None or result is NIL:
|
||||
return ""
|
||||
return serialize(result)
|
||||
|
||||
|
||||
async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
|
||||
"""Evaluate *expr*, producing SxExpr for rendering forms, raw values
|
||||
for everything else."""
|
||||
@@ -1022,6 +1058,33 @@ async def _aser_fragment(children: list, env: dict, ctx: RequestContext) -> SxEx
|
||||
return SxExpr("(<> " + " ".join(parts) + ")")
|
||||
|
||||
|
||||
async def _aser_component(
|
||||
comp: Component, args: list, env: dict, ctx: RequestContext,
|
||||
) -> Any:
|
||||
"""Expand a component body through _aser — produces SX, not HTML."""
|
||||
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_parts = []
|
||||
for c in children:
|
||||
child_parts.append(serialize(await _aser(c, env, ctx)))
|
||||
local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
|
||||
return await _aser(comp.body, local, ctx)
|
||||
|
||||
|
||||
async def _aser_call(
|
||||
name: str, args: list, env: dict, ctx: RequestContext,
|
||||
) -> SxExpr:
|
||||
|
||||
@@ -132,31 +132,14 @@ def load_page_dir(directory: str, service_name: str) -> list[PageDef]:
|
||||
# Page execution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _eval_slot(expr: Any, env: dict, ctx: Any,
|
||||
async_eval_fn: Any, async_eval_to_sx_fn: Any) -> str:
|
||||
async def _eval_slot(expr: Any, env: dict, ctx: Any) -> str:
|
||||
"""Evaluate a page slot expression and return an sx source string.
|
||||
|
||||
If the expression evaluates to a plain string (e.g. from a Python content
|
||||
builder), use it directly as sx source. If it evaluates to an AST/list,
|
||||
serialize it to sx wire format via async_eval_to_sx.
|
||||
Expands component calls (so IO in the body executes) but serializes
|
||||
the result as SX wire format, not HTML.
|
||||
"""
|
||||
from .html import _RawHTML
|
||||
from .parser import SxExpr
|
||||
# First try async_eval to get the raw value
|
||||
result = await async_eval_fn(expr, env, ctx)
|
||||
# If it's already an sx source string, use as-is
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
if isinstance(result, _RawHTML):
|
||||
return result.html
|
||||
if isinstance(result, SxExpr):
|
||||
return result.source
|
||||
if result is None:
|
||||
return ""
|
||||
# For other types (lists, components rendered to HTML via _RawHTML, etc.),
|
||||
# serialize to sx wire format
|
||||
from .parser import serialize
|
||||
return serialize(result)
|
||||
from .async_eval import async_eval_slot_to_sx
|
||||
return await async_eval_slot_to_sx(expr, env, ctx)
|
||||
|
||||
|
||||
async def execute_page(
|
||||
@@ -174,7 +157,7 @@ async def execute_page(
|
||||
6. Branch: full_page_sx() vs oob_page_sx() based on is_htmx_request()
|
||||
"""
|
||||
from .jinja_bridge import get_component_env, _get_request_context
|
||||
from .async_eval import async_eval, async_eval_to_sx
|
||||
from .async_eval import async_eval
|
||||
from .page import get_template_context
|
||||
from .helpers import full_page_sx, oob_page_sx, sx_response
|
||||
from .layouts import get_layout
|
||||
@@ -204,20 +187,20 @@ async def execute_page(
|
||||
env.update(data_result)
|
||||
|
||||
# Render content slot (required)
|
||||
content_sx = await _eval_slot(page_def.content_expr, env, ctx, async_eval, async_eval_to_sx)
|
||||
content_sx = await _eval_slot(page_def.content_expr, env, ctx)
|
||||
|
||||
# Render optional slots
|
||||
filter_sx = ""
|
||||
if page_def.filter_expr is not None:
|
||||
filter_sx = await _eval_slot(page_def.filter_expr, env, ctx, async_eval, async_eval_to_sx)
|
||||
filter_sx = await _eval_slot(page_def.filter_expr, env, ctx)
|
||||
|
||||
aside_sx = ""
|
||||
if page_def.aside_expr is not None:
|
||||
aside_sx = await _eval_slot(page_def.aside_expr, env, ctx, async_eval, async_eval_to_sx)
|
||||
aside_sx = await _eval_slot(page_def.aside_expr, env, ctx)
|
||||
|
||||
menu_sx = ""
|
||||
if page_def.menu_expr is not None:
|
||||
menu_sx = await _eval_slot(page_def.menu_expr, env, ctx, async_eval, async_eval_to_sx)
|
||||
menu_sx = await _eval_slot(page_def.menu_expr, env, ctx)
|
||||
|
||||
# Resolve layout → header rows + mobile menu fallback
|
||||
tctx = await get_template_context()
|
||||
|
||||
Reference in New Issue
Block a user