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:
2026-03-04 01:16:01 +00:00
parent 44503a7d9b
commit 400667b15a
10 changed files with 173 additions and 228 deletions

View File

@@ -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()