Merge branch 'worktree-macros-essays' into macros
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m22s

This commit is contained in:
2026-03-04 14:39:57 +00:00
9 changed files with 244 additions and 229 deletions

View File

@@ -385,6 +385,59 @@ def _build_component_ast(__name: str, **kwargs: Any) -> list:
return ast
def _ctx_to_env(ctx: dict, *, oob: bool = False) -> dict:
"""Convert template context dict → SX evaluation env dict.
Applies ``_as_sx()`` to HTML fragments, ``call_url()`` to URL helpers,
extracts rights/admin flags. Returns kebab-case keys matching SX
symbol conventions so .sx defcomps can read them as free variables.
"""
rights = ctx.get("rights") or {}
is_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
env = {
# Root header values (match ~header-row-sx &key params)
"cart-mini": _as_sx(ctx.get("cart_mini")),
"blog-url": call_url(ctx, "blog_url", ""),
"site-title": ctx.get("base_title", ""),
"app-label": ctx.get("app_label", ""),
"nav-tree": _as_sx(ctx.get("nav_tree")),
"auth-menu": _as_sx(ctx.get("auth_menu")),
"nav-panel": _as_sx(ctx.get("nav_panel")),
"settings-url": call_url(ctx, "blog_url", "/settings/") if is_admin else "",
"is-admin": is_admin,
"oob": oob,
# URL helpers (pre-resolved to strings)
"account-url": call_url(ctx, "account_url", ""),
"events-url": call_url(ctx, "events_url", ""),
"market-url": call_url(ctx, "market_url", ""),
"cart-url": call_url(ctx, "cart_url", ""),
# Common values
"select-colours": ctx.get("select_colours", ""),
"rights": rights,
# Fragments (used by various services)
"container-nav": _as_sx(ctx.get("container_nav")),
"account-nav": _as_sx(ctx.get("account_nav")),
# Post context
"post": ctx.get("post") or {},
}
return env
async def render_to_sx_with_env(__name: str, extra_env: dict, **kwargs: Any) -> str:
"""Like ``render_to_sx`` but merges *extra_env* into the evaluation
environment before eval. Used by ``register_sx_layout`` so .sx
defcomps can read ctx values as free variables.
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_eval_to_sx
ast = _build_component_ast(__name, **kwargs)
env = dict(get_component_env())
env.update(extra_env)
ctx = _get_request_context()
return await async_eval_to_sx(ast, env, ctx)
async def render_to_sx(__name: str, **kwargs: Any) -> str:
"""Call a defcomp and get SX wire format back. No SX string literals.

View File

@@ -15,7 +15,7 @@ from typing import Any, Callable, Awaitable
from .helpers import (
root_header_sx, post_header_sx, post_admin_header_sx,
oob_header_sx, header_child_sx,
oob_header_sx,
mobile_menu_sx, mobile_root_nav_sx,
post_mobile_nav_sx, post_admin_mobile_nav_sx,
)
@@ -87,15 +87,6 @@ def get_layout(name: str) -> Layout | None:
# Built-in layouts
# ---------------------------------------------------------------------------
async def _root_full(ctx: dict, **kw: Any) -> str:
return await root_header_sx(ctx)
async def _root_oob(ctx: dict, **kw: Any) -> str:
root_hdr = await root_header_sx(ctx)
return await oob_header_sx("root-header-child", "root-header-child", root_hdr)
async def _post_full(ctx: dict, **kw: Any) -> str:
root_hdr = await root_header_sx(ctx)
post_hdr = await post_header_sx(ctx)
@@ -127,10 +118,6 @@ async def _post_admin_oob(ctx: dict, **kw: Any) -> str:
return "(<> " + post_hdr + " " + admin_oob + ")"
async def _root_mobile(ctx: dict, **kw: Any) -> str:
return await mobile_root_nav_sx(ctx)
async def _post_mobile(ctx: dict, **kw: Any) -> str:
return mobile_menu_sx(await post_mobile_nav_sx(ctx), await mobile_root_nav_sx(ctx))
@@ -145,11 +132,53 @@ async def _post_admin_mobile(ctx: dict, **kw: Any) -> str:
)
register_layout(Layout("root", _root_full, _root_oob, _root_mobile))
register_layout(Layout("post", _post_full, _post_oob, _post_mobile))
register_layout(Layout("post-admin", _post_admin_full, _post_admin_oob, _post_admin_mobile))
# ---------------------------------------------------------------------------
# register_sx_layout — declarative layout from .sx defcomp names
# ---------------------------------------------------------------------------
# (defined below, used immediately after for built-in "root" layout)
# ---------------------------------------------------------------------------
def register_sx_layout(name: str, full_defcomp: str, oob_defcomp: str,
mobile_defcomp: str | None = None) -> None:
"""Register a layout that delegates entirely to .sx defcomps.
The defcomps read ctx values as free variables from the evaluation
environment (populated by ``_ctx_to_env``). Python layouts become
one-liners::
register_sx_layout("account", "account-layout-full",
"account-layout-oob", "account-layout-mobile")
"""
from .helpers import render_to_sx_with_env, _ctx_to_env
async def full_fn(ctx: dict, **kw: Any) -> str:
env = _ctx_to_env(ctx)
env.update({k.replace("_", "-"): v for k, v in kw.items()})
return await render_to_sx_with_env(full_defcomp, env)
async def oob_fn(ctx: dict, **kw: Any) -> str:
env = _ctx_to_env(ctx, oob=True)
env.update({k.replace("_", "-"): v for k, v in kw.items()})
return await render_to_sx_with_env(oob_defcomp, env)
mobile_fn = None
if mobile_defcomp:
async def mobile_fn(ctx: dict, **kw: Any) -> str:
env = _ctx_to_env(ctx)
env.update({k.replace("_", "-"): v for k, v in kw.items()})
return await render_to_sx_with_env(mobile_defcomp, env)
register_layout(Layout(name, full_fn, oob_fn, mobile_fn))
# Register built-in "root" layout via .sx defcomps
register_sx_layout("root", "layout-root-full", "layout-root-oob", "layout-root-mobile")
# ---------------------------------------------------------------------------
# Callable layout — services register custom Python layout functions
# ---------------------------------------------------------------------------

View File

@@ -146,6 +146,33 @@
(when auth-menu
(div :class "p-3 border-t border-stone-200" auth-menu))))
;; ---------------------------------------------------------------------------
;; Root header/mobile wrappers — read ctx values from env free variables
;; Used by register_sx_layout so .sx defcomps compose without Python
;; ---------------------------------------------------------------------------
(defcomp ~root-header (&key oob)
(~header-row-sx :cart-mini cart-mini :blog-url blog-url :site-title site-title
:app-label app-label :nav-tree nav-tree :auth-menu auth-menu
:nav-panel nav-panel :settings-url settings-url :is-admin is-admin
:oob oob))
(defcomp ~root-mobile ()
(~mobile-root-nav :nav-tree nav-tree :auth-menu auth-menu))
;; ---------------------------------------------------------------------------
;; Built-in layout defcomps — used by register_sx_layout("root", ...)
;; ---------------------------------------------------------------------------
(defcomp ~layout-root-full ()
(~root-header))
(defcomp ~layout-root-oob ()
(~oob-header-sx :parent-id "root-header-child" :row (~root-header)))
(defcomp ~layout-root-mobile ()
(~root-mobile))
(defcomp ~error-content (&key errnum message image)
(div :class "text-center p-8 max-w-lg mx-auto"
(div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4" errnum)