Add register_sx_layout infrastructure, convert account/federation/orders

Phase 0: Add _ctx_to_env() and render_to_sx_with_env() to shared/sx/helpers.py,
register_sx_layout() to shared/sx/layouts.py, and ~root-header/~root-mobile
wrapper defcomps to layout.sx. Convert built-in "root" layout to .sx.

Phases 1-3: Convert account (65→19 lines), federation (105→97 lines),
and orders (88→21 lines) to use register_sx_layout with .sx defcomps
that read ctx values as free variables from the evaluation environment.
No more Python building SX strings via SxExpr(await root_header_sx(ctx)).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 14:39:53 +00:00
parent 28388540d5
commit 45c5e4a0db
9 changed files with 244 additions and 229 deletions

17
federation/sx/layouts.sx Normal file
View File

@@ -0,0 +1,17 @@
;; Federation layout defcomps — read ctx values from env free variables.
;; `actor` is injected into env by the layout registration in __init__.py.
;; Full page: root header + social header in header-child
(defcomp ~social-layout-full ()
(<> (~root-header)
(~header-child-sx
:inner (~federation-social-header
:nav (~federation-social-nav :actor actor)))))
;; OOB (HTMX): social header oob + root header oob
(defcomp ~social-layout-oob ()
(<> (~oob-header-sx
:parent-id "root-header-child"
:row (~federation-social-header
:nav (~federation-social-nav :actor actor)))
(~root-header :oob true)))

View File

@@ -17,7 +17,7 @@ def _load_federation_page_files() -> None:
# ---------------------------------------------------------------------------
# Layouts
# Layouts — .sx defcomps read free variables from env
# ---------------------------------------------------------------------------
def _register_federation_layouts() -> None:
@@ -25,36 +25,30 @@ def _register_federation_layouts() -> None:
register_custom_layout("social", _social_full, _social_oob)
async def _social_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx, render_to_sx
from shared.sx.parser import SxExpr
def _actor_data(ctx: dict) -> dict | None:
actor = ctx.get("actor")
actor_data = _serialize_actor(actor) if actor else None
nav = await render_to_sx("federation-social-nav", actor=actor_data)
social_hdr = await render_to_sx("federation-social-header", nav=SxExpr(nav))
root_hdr = await root_header_sx(ctx)
child = await header_child_sx(social_hdr)
return "(<> " + root_hdr + " " + child + ")"
if not actor:
return None
from services.federation_page import _serialize_actor
return _serialize_actor(actor)
async def _social_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
env = _ctx_to_env(ctx)
env["actor"] = kw.get("actor") or _actor_data(ctx)
return await render_to_sx_with_env("social-layout-full", env)
async def _social_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, render_to_sx
from shared.sx.parser import SxExpr
actor = ctx.get("actor")
actor_data = _serialize_actor(actor) if actor else None
nav = await render_to_sx("federation-social-nav", actor=actor_data)
social_hdr = await render_to_sx("federation-social-header", nav=SxExpr(nav))
child_oob = await render_to_sx("oob-header-sx",
parent_id="root-header-child",
row=SxExpr(social_hdr))
root_hdr_oob = await root_header_sx(ctx, oob=True)
return "(<> " + child_oob + " " + root_hdr_oob + ")"
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env
env = _ctx_to_env(ctx, oob=True)
env["actor"] = kw.get("actor") or _actor_data(ctx)
return await render_to_sx_with_env("social-layout-oob", env)
# ---------------------------------------------------------------------------
# Serializers and helpers still used by layouts and route handlers
# Helpers still used by route handlers
# ---------------------------------------------------------------------------
def _serialize_actor(actor) -> dict | None:
@@ -78,16 +72,12 @@ def _serialize_remote_actor(a) -> dict:
async def _social_page(ctx: dict, actor, *, content: str,
title: str = "Rose Ash", meta_html: str = "") -> str:
"""Build a full social page with social header."""
from shared.sx.helpers import render_to_sx, root_header_sx, header_child_sx, full_page_sx
from shared.sx.parser import SxExpr
from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, full_page_sx
from markupsafe import escape
actor_data = _serialize_actor(actor)
nav = await render_to_sx("federation-social-nav", actor=actor_data)
social_hdr = await render_to_sx("federation-social-header", nav=SxExpr(nav))
hdr = await root_header_sx(ctx)
child = await header_child_sx(social_hdr)
header_rows = "(<> " + hdr + " " + child + ")"
env = _ctx_to_env(ctx)
env["actor"] = _serialize_actor(actor) if actor else None
header_rows = await render_to_sx_with_env("social-layout-full", env)
return await full_page_sx(ctx, header_rows=header_rows, content=content,
meta_html=meta_html or f'<title>{escape(title)}</title>')