All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m6s
Python no longer generates s-expression strings. All SX rendering now goes through render_to_sx() which builds AST from native Python values and evaluates via async_eval_to_sx() — no SX string literals in Python. - Add render_to_sx()/render_to_html() infrastructure in shared/sx/helpers.py - Add (abort status msg) IO primitive in shared/sx/primitives_io.py - Convert all 9 services: ~650 sx_call() invocations replaced - Convert shared helpers (root_header_sx, full_page_sx, etc.) to async - Fix likes service import bug (likes.models → models) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
174 lines
5.8 KiB
Python
174 lines
5.8 KiB
Python
"""
|
|
Named layout presets for defpage.
|
|
|
|
Each layout generates header rows for full-page and OOB rendering.
|
|
Layouts wrap existing helper functions from ``shared.sx.helpers`` so
|
|
defpage can reference them by name (e.g. ``:layout :root``).
|
|
|
|
Layouts are registered in ``_LAYOUT_REGISTRY`` and looked up by
|
|
``get_layout()`` at request time.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
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,
|
|
mobile_menu_sx, mobile_root_nav_sx,
|
|
post_mobile_nav_sx, post_admin_mobile_nav_sx,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Layout protocol
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class Layout:
|
|
"""A named layout that generates header rows for full and OOB rendering."""
|
|
|
|
__slots__ = ("name", "_full_fn", "_oob_fn", "_mobile_fn")
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
full_fn: Callable[..., str | Awaitable[str]],
|
|
oob_fn: Callable[..., str | Awaitable[str]],
|
|
mobile_fn: Callable[..., str | Awaitable[str]] | None = None,
|
|
):
|
|
self.name = name
|
|
self._full_fn = full_fn
|
|
self._oob_fn = oob_fn
|
|
self._mobile_fn = mobile_fn
|
|
|
|
async def full_headers(self, ctx: dict, **kwargs: Any) -> str:
|
|
result = self._full_fn(ctx, **kwargs)
|
|
if hasattr(result, "__await__"):
|
|
result = await result
|
|
return result
|
|
|
|
async def oob_headers(self, ctx: dict, **kwargs: Any) -> str:
|
|
result = self._oob_fn(ctx, **kwargs)
|
|
if hasattr(result, "__await__"):
|
|
result = await result
|
|
return result
|
|
|
|
async def mobile_menu(self, ctx: dict, **kwargs: Any) -> str:
|
|
if self._mobile_fn is None:
|
|
return ""
|
|
result = self._mobile_fn(ctx, **kwargs)
|
|
if hasattr(result, "__await__"):
|
|
result = await result
|
|
return result
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Layout:{self.name}>"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_LAYOUT_REGISTRY: dict[str, Layout] = {}
|
|
|
|
|
|
def register_layout(layout: Layout) -> None:
|
|
"""Register a layout preset."""
|
|
_LAYOUT_REGISTRY[layout.name] = layout
|
|
|
|
|
|
def get_layout(name: str) -> Layout | None:
|
|
"""Look up a layout by name."""
|
|
return _LAYOUT_REGISTRY.get(name)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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)
|
|
return "(<> " + root_hdr + " " + post_hdr + ")"
|
|
|
|
|
|
async def _post_oob(ctx: dict, **kw: Any) -> str:
|
|
post_hdr = await post_header_sx(ctx, oob=True)
|
|
# Also replace #post-header-child (empty — clears any nested admin rows)
|
|
child_oob = await oob_header_sx("post-header-child", "", "")
|
|
return "(<> " + post_hdr + " " + child_oob + ")"
|
|
|
|
|
|
async def _post_admin_full(ctx: dict, **kw: Any) -> str:
|
|
slug = ctx.get("post", {}).get("slug", "")
|
|
selected = kw.get("selected", "")
|
|
root_hdr = await root_header_sx(ctx)
|
|
admin_hdr = await post_admin_header_sx(ctx, slug, selected=selected)
|
|
post_hdr = await post_header_sx(ctx, child=admin_hdr)
|
|
return "(<> " + root_hdr + " " + post_hdr + ")"
|
|
|
|
|
|
async def _post_admin_oob(ctx: dict, **kw: Any) -> str:
|
|
slug = ctx.get("post", {}).get("slug", "")
|
|
selected = kw.get("selected", "")
|
|
post_hdr = await post_header_sx(ctx, oob=True)
|
|
admin_hdr = await post_admin_header_sx(ctx, slug, selected=selected)
|
|
admin_oob = await oob_header_sx("post-header-child", "post-admin-header-child", admin_hdr)
|
|
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))
|
|
|
|
|
|
async def _post_admin_mobile(ctx: dict, **kw: Any) -> str:
|
|
slug = ctx.get("post", {}).get("slug", "")
|
|
selected = kw.get("selected", "")
|
|
return mobile_menu_sx(
|
|
await post_admin_mobile_nav_sx(ctx, slug, selected),
|
|
await post_mobile_nav_sx(ctx),
|
|
await mobile_root_nav_sx(ctx),
|
|
)
|
|
|
|
|
|
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))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Callable layout — services register custom Python layout functions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_CUSTOM_LAYOUTS: dict[str, tuple] = {} # name → (full_fn, oob_fn)
|
|
|
|
|
|
def register_custom_layout(name: str,
|
|
full_fn: Callable[..., str | Awaitable[str]],
|
|
oob_fn: Callable[..., str | Awaitable[str]],
|
|
mobile_fn: Callable[..., str | Awaitable[str]] | None = None) -> None:
|
|
"""Register a custom layout function.
|
|
|
|
Used by services with non-standard header patterns::
|
|
|
|
register_custom_layout("sx-section",
|
|
full_fn=my_full_headers,
|
|
oob_fn=my_oob_headers,
|
|
mobile_fn=my_mobile_menu)
|
|
"""
|
|
register_layout(Layout(name, full_fn, oob_fn, mobile_fn))
|