diff --git a/.claude/plans/glittery-discovering-kahn.md b/.claude/plans/glittery-discovering-kahn.md new file mode 100644 index 0000000..e341caa --- /dev/null +++ b/.claude/plans/glittery-discovering-kahn.md @@ -0,0 +1,177 @@ +# Sexp Fragment Protocol: Component Defs Between Services + +## Context + +Fragment endpoints return raw sexp source (e.g., `(~blog-nav-wrapper :items ...)`). The consuming service embeds this in its page sexp, which the client evaluates. But blog-specific components like `~blog-nav-wrapper` are only in blog's `_COMPONENT_ENV` — not in market's. So market's `client_components_tag()` never sends them to the client, causing "Unknown component" errors. + +The fix: transfer component definitions alongside fragments. Services tell the provider what they already have; the provider sends only what's missing. The consuming service registers received defs into its `_COMPONENT_ENV` so they're included in `client_components_tag()` output for the client. + +## Approach: Structured Sexp Request/Response + +Replace the current GET + `X-Fragment-Request` header protocol with POST + sexp body. This aligns with the vision in `docs/sexpr-internal-protocol-first.md`. + +### Request format (POST body) +```scheme +(fragment-request + :type "nav-tree" + :params (:app-name "market" :path "/") + :components (~blog-nav-wrapper ~blog-nav-item-link ~header-row-sx ...)) +``` + +`:components` lists component names already in the consumer's `_COMPONENT_ENV`. Provider skips these. + +### Response format +```scheme +(fragment-response + :defs ((defcomp ~blog-nav-wrapper (&key ...) ...) (defcomp ~blog-nav-item-link ...)) + :content (~blog-nav-wrapper :items ...)) +``` + +`:defs` contains only components the consumer doesn't have. `:content` is the fragment sexp (same as current response body). + +## Changes + +### 1. `shared/infrastructure/fragments.py` — Client side + +**`fetch_fragment()`**: Switch from GET to POST with sexp body. + +- Build request body using `sexp_call`: + ```python + from shared.sexp.helpers import sexp_call, SexpExpr + from shared.sexp.jinja_bridge import _COMPONENT_ENV + + comp_names = [k for k in _COMPONENT_ENV if k.startswith("~")] + body = sexp_call("fragment-request", + type=fragment_type, + params=params or {}, + components=SexpExpr("(" + " ".join(comp_names) + ")")) + ``` +- POST to same URL, body as `text/sexp`, keep `X-Fragment-Request` header for backward compat +- Parse response: extract `:defs` and `:content` from the sexp response +- Register defs into `_COMPONENT_ENV` via `register_components()` +- Return `:content` wrapped as `SexpExpr` + +**New helper `_parse_fragment_response(text)`**: +- `parse()` the response sexp +- Extract keyword args (reuse the keyword-extraction pattern from `evaluator.py`) +- Return `(defs_source, content_source)` tuple + +### 2. `shared/sexp/helpers.py` — Response builder + +**New `fragment_response(content, request_text)`**: + +```python +def fragment_response(content: str, request_text: str) -> str: + """Build a structured fragment response with missing component defs.""" + from .parser import parse, serialize + from .types import Keyword, Component + from .jinja_bridge import _COMPONENT_ENV + + # Parse request to get :components list + req = parse(request_text) + loaded = set() + # extract :components keyword value + ... + + # Diff against _COMPONENT_ENV, serialize missing defs + defs_parts = [] + for key, val in _COMPONENT_ENV.items(): + if not isinstance(val, Component): + continue + if key in loaded or f"~{val.name}" in loaded: + continue + defs_parts.append(_serialize_defcomp(val)) + + defs_sexp = "(" + " ".join(defs_parts) + ")" if defs_parts else "nil" + return sexp_call("fragment-response", + defs=SexpExpr(defs_sexp), + content=SexpExpr(content)) +``` + +### 3. Fragment endpoints — All services + +**Generic change in each `bp/fragments/routes.py`**: Update the route handler to accept POST, read sexp body, use `fragment_response()` for the response. + +The `get_fragment` handler becomes: +```python +@bp.route("/", methods=["GET", "POST"]) +async def get_fragment(fragment_type: str): + handler = _handlers.get(fragment_type) + if handler is None: + return Response("", status=200, content_type="text/sexp") + content = await handler() + + # Structured sexp protocol (POST with sexp body) + request_body = await request.get_data(as_text=True) + if request_body and request.content_type == "text/sexp": + from shared.sexp.helpers import fragment_response + body = fragment_response(content, request_body) + return Response(body, status=200, content_type="text/sexp") + + # Legacy GET fallback + return Response(content, status=200, content_type="text/sexp") +``` + +Since all fragment endpoints follow the identical `_handlers` + `get_fragment` pattern, we can extract this into a shared helper in `fragments.py` or a new `shared/infrastructure/fragment_endpoint.py`. + +### 4. Extract shared fragment endpoint helper + +To avoid touching every service's fragment routes, create a shared blueprint factory: + +**`shared/infrastructure/fragment_endpoint.py`**: +```python +def create_fragment_blueprint(handlers: dict) -> Blueprint: + """Create a fragment endpoint blueprint with sexp protocol support.""" + bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") + + @bp.before_request + async def _require_fragment_header(): + if not request.headers.get(FRAGMENT_HEADER): + return Response("", status=403) + + @bp.route("/", methods=["GET", "POST"]) + async def get_fragment(fragment_type: str): + handler = handlers.get(fragment_type) + if handler is None: + return Response("", status=200, content_type="text/sexp") + content = await handler() + + # Sexp protocol: POST with structured request/response + if request.method == "POST" and request.content_type == "text/sexp": + request_body = await request.get_data(as_text=True) + from shared.sexp.helpers import fragment_response + body = fragment_response(content, request_body) + return Response(body, status=200, content_type="text/sexp") + + return Response(content, status=200, content_type="text/sexp") + + return bp +``` + +Then each service's `register()` just returns `create_fragment_blueprint(_handlers)`. This is a small refactor since they all duplicate the same boilerplate today. + +## Files to modify + +| File | Change | +|------|--------| +| `shared/infrastructure/fragments.py` | POST sexp body, parse response, register defs | +| `shared/sexp/helpers.py` | `fragment_response()` builder, `_serialize_defcomp()` | +| `shared/infrastructure/fragment_endpoint.py` | **New** — shared blueprint factory | +| `blog/bp/fragments/routes.py` | Use `create_fragment_blueprint` | +| `market/bp/fragments/routes.py` | Use `create_fragment_blueprint` | +| `events/bp/fragments/routes.py` | Use `create_fragment_blueprint` | +| `cart/bp/fragments/routes.py` | Use `create_fragment_blueprint` | +| `account/bp/fragments/routes.py` | Use `create_fragment_blueprint` | +| `orders/bp/fragments/routes.py` | Use `create_fragment_blueprint` | +| `federation/bp/fragments/routes.py` | Use `create_fragment_blueprint` | +| `relations/bp/fragments/routes.py` | Use `create_fragment_blueprint` | + +## Verification + +1. Start blog + market services: `./dev.sh blog market` +2. Load market page — should fetch nav-tree from blog with sexp protocol +3. Check market logs: no "Unknown component" errors +4. Inspect page source: `client_components_tag()` output includes `~blog-nav-wrapper` etc. +5. Cross-domain sx-get navigation (blog → market) works without reload +6. Run sexp tests: `python3 -m pytest shared/sexp/tests/ -x -q` +7. Second page load: `:components` list in request includes blog nav components, response `:defs` is empty diff --git a/account/app.py b/account/app.py index 1a46aae..5c7296e 100644 --- a/account/app.py +++ b/account/app.py @@ -1,6 +1,6 @@ from __future__ import annotations import path_setup # noqa: F401 # adds shared/ to sys.path -import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file +import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file from pathlib import Path from quart import g, request diff --git a/account/bp/account/routes.py b/account/bp/account/routes.py index de94bf0..3aa4209 100644 --- a/account/bp/account/routes.py +++ b/account/bp/account/routes.py @@ -18,7 +18,7 @@ from shared.models import UserNewsletter from shared.models.ghost_membership_entities import GhostNewsletter from shared.infrastructure.urls import login_url from shared.infrastructure.fragments import fetch_fragment, fetch_fragments -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response oob = { "oob_extends": "oob_elements.html", @@ -47,8 +47,8 @@ def register(url_prefix="/"): @account_bp.get("/") async def account(): from shared.browser.app.utils.htmx import is_htmx_request - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_account_page, render_account_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_account_page, render_account_oob if not g.get("user"): return redirect(login_url("/")) @@ -58,8 +58,8 @@ def register(url_prefix="/"): html = await render_account_page(ctx) return await make_response(html) else: - sexp_src = await render_account_oob(ctx) - return sexp_response(sexp_src) + sx_src = await render_account_oob(ctx) + return sx_response(sx_src) @account_bp.get("/newsletters/") async def newsletters(): @@ -89,16 +89,16 @@ def register(url_prefix="/"): "subscribed": un.subscribed if un else False, }) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_newsletters_page, render_newsletters_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_newsletters_page, render_newsletters_oob ctx = await get_template_context() if not is_htmx_request(): html = await render_newsletters_page(ctx, newsletter_list) return await make_response(html) else: - sexp_src = await render_newsletters_oob(ctx, newsletter_list) - return sexp_response(sexp_src) + sx_src = await render_newsletters_oob(ctx, newsletter_list) + return sx_response(sx_src) @account_bp.post("/newsletter//toggle/") async def toggle_newsletter(newsletter_id: int): @@ -125,8 +125,8 @@ def register(url_prefix="/"): await g.s.flush() - from sexp.sexp_components import render_newsletter_toggle - return sexp_response(render_newsletter_toggle(un)) + from sx.sx_components import render_newsletter_toggle + return sx_response(render_newsletter_toggle(un)) # Catch-all for fragment-provided pages — must be last @account_bp.get("//") @@ -144,15 +144,15 @@ def register(url_prefix="/"): if not fragment_html: abort(404) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_fragment_page, render_fragment_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_fragment_page, render_fragment_oob ctx = await get_template_context() if not is_htmx_request(): html = await render_fragment_page(ctx, fragment_html) return await make_response(html) else: - sexp_src = await render_fragment_oob(ctx, fragment_html) - return sexp_response(sexp_src) + sx_src = await render_fragment_oob(ctx, fragment_html) + return sx_response(sx_src) return account_bp diff --git a/account/bp/auth/routes.py b/account/bp/auth/routes.py index a922b49..a49d96a 100644 --- a/account/bp/auth/routes.py +++ b/account/bp/auth/routes.py @@ -275,8 +275,8 @@ def register(url_prefix="/auth"): redirect_url = pop_login_redirect_target() return redirect(redirect_url) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_login_page + from shared.sx.page import get_template_context + from sx.sx_components import render_login_page ctx = await get_template_context() return await render_login_page(ctx) @@ -291,8 +291,8 @@ def register(url_prefix="/auth"): is_valid, email = validate_email(email_input) if not is_valid: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_login_page + from shared.sx.page import get_template_context + from sx.sx_components import render_login_page ctx = await get_template_context(error="Please enter a valid email address.", email=email_input) return await render_login_page(ctx), 400 @@ -301,8 +301,8 @@ def register(url_prefix="/auth"): try: allowed, _ = await _check_rate_limit(f"magic_email:{email}", 5, 900) if not allowed: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_check_email_page + from shared.sx.page import get_template_context + from sx.sx_components import render_check_email_page ctx = await get_template_context(email=email, email_error=None) return await render_check_email_page(ctx), 200 except Exception: @@ -324,8 +324,8 @@ def register(url_prefix="/auth"): "Please try again in a moment." ) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_check_email_page + from shared.sx.page import get_template_context + from sx.sx_components import render_check_email_page ctx = await get_template_context(email=email, email_error=email_error) return await render_check_email_page(ctx) @@ -340,15 +340,15 @@ def register(url_prefix="/auth"): user, error = await validate_magic_link(s, token) if error: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_login_page + from shared.sx.page import get_template_context + from sx.sx_components import render_login_page ctx = await get_template_context(error=error) return await render_login_page(ctx), 400 user_id = user.id except Exception: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_login_page + from shared.sx.page import get_template_context + from sx.sx_components import render_login_page ctx = await get_template_context(error="Could not sign you in right now. Please try again.") return await render_login_page(ctx), 502 @@ -679,8 +679,8 @@ def register(url_prefix="/auth"): @auth_bp.get("/device/") async def device_form(): """Browser form where user enters the code displayed in terminal.""" - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_device_page + from shared.sx.page import get_template_context + from sx.sx_components import render_device_page code = request.args.get("code", "") ctx = await get_template_context(code=code) return await render_device_page(ctx) @@ -693,8 +693,8 @@ def register(url_prefix="/auth"): user_code = (form.get("code") or "").strip().replace("-", "").upper() if not user_code or len(user_code) != 8: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_device_page + from shared.sx.page import get_template_context + from sx.sx_components import render_device_page ctx = await get_template_context(error="Please enter a valid 8-character code.", code=form.get("code", "")) return await render_device_page(ctx), 400 @@ -703,8 +703,8 @@ def register(url_prefix="/auth"): r = await get_auth_redis() device_code = await r.get(f"devflow_uc:{user_code}") if not device_code: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_device_page + from shared.sx.page import get_template_context + from sx.sx_components import render_device_page ctx = await get_template_context(error="Code not found or expired. Please try again.", code=form.get("code", "")) return await render_device_page(ctx), 400 @@ -720,13 +720,13 @@ def register(url_prefix="/auth"): # Logged in — approve immediately ok = await _approve_device(device_code, g.user) if not ok: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_device_page + from shared.sx.page import get_template_context + from sx.sx_components import render_device_page ctx = await get_template_context(error="Code expired or already used.") return await render_device_page(ctx), 400 - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_device_approved_page + from shared.sx.page import get_template_context + from sx.sx_components import render_device_approved_page ctx = await get_template_context() return await render_device_approved_page(ctx) @@ -734,8 +734,8 @@ def register(url_prefix="/auth"): @auth_bp.get("/device/complete/") async def device_complete(): """Post-login redirect — completes approval after magic link auth.""" - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_device_page, render_device_approved_page + from shared.sx.page import get_template_context + from sx.sx_components import render_device_page, render_device_approved_page device_code = request.args.get("code", "") diff --git a/account/bp/fragments/routes.py b/account/bp/fragments/routes.py index 1298c54..0512696 100644 --- a/account/bp/fragments/routes.py +++ b/account/bp/fragments/routes.py @@ -1,6 +1,6 @@ """Account app fragment endpoints. -Exposes sexp fragments at ``/internal/fragments/`` for consumption +Exposes sx fragments at ``/internal/fragments/`` for consumption by other coop apps via the fragment client. Fragments: @@ -18,15 +18,15 @@ def register(): bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") # --------------------------------------------------------------- - # Fragment handlers — return sexp source text + # Fragment handlers — return sx source text # --------------------------------------------------------------- async def _auth_menu(): from shared.infrastructure.urls import account_url - from shared.sexp.helpers import sexp_call + from shared.sx.helpers import sx_call user_email = request.args.get("email", "") - return sexp_call("auth-menu", + return sx_call("auth-menu", user_email=user_email or None, account_url=account_url("")) @@ -47,8 +47,8 @@ def register(): async def get_fragment(fragment_type: str): handler = _handlers.get(fragment_type) if handler is None: - return Response("", status=200, content_type="text/sexp") + return Response("", status=200, content_type="text/sx") src = await handler() - return Response(src, status=200, content_type="text/sexp") + return Response(src, status=200, content_type="text/sx") return bp diff --git a/account/sexp/__init__.py b/account/sx/__init__.py similarity index 100% rename from account/sexp/__init__.py rename to account/sx/__init__.py diff --git a/account/sexp/auth.sexpr b/account/sx/auth.sx similarity index 100% rename from account/sexp/auth.sexpr rename to account/sx/auth.sx diff --git a/account/sexp/dashboard.sexpr b/account/sx/dashboard.sx similarity index 100% rename from account/sexp/dashboard.sexpr rename to account/sx/dashboard.sx diff --git a/account/sexp/newsletters.sexpr b/account/sx/newsletters.sx similarity index 100% rename from account/sexp/newsletters.sexpr rename to account/sx/newsletters.sx diff --git a/account/sexp/sexp_components.py b/account/sx/sx_components.py similarity index 65% rename from account/sexp/sexp_components.py rename to account/sx/sx_components.py index b8abd4c..ff2ca40 100644 --- a/account/sexp/sexp_components.py +++ b/account/sx/sx_components.py @@ -9,13 +9,13 @@ from __future__ import annotations import os from typing import Any -from shared.sexp.jinja_bridge import load_service_components -from shared.sexp.helpers import ( - call_url, sexp_call, SexpExpr, - root_header_sexp, full_page_sexp, header_child_sexp, oob_page_sexp, +from shared.sx.jinja_bridge import load_service_components +from shared.sx.helpers import ( + call_url, sx_call, SxExpr, + root_header_sx, full_page_sx, header_child_sx, oob_page_sx, ) -# Load account-specific .sexpr components at import time +# Load account-specific .sx components at import time load_service_components(os.path.dirname(os.path.dirname(__file__))) @@ -23,10 +23,10 @@ load_service_components(os.path.dirname(os.path.dirname(__file__))) # Header helpers # --------------------------------------------------------------------------- -def _auth_nav_sexp(ctx: dict) -> str: +def _auth_nav_sx(ctx: dict) -> str: """Auth section desktop nav items.""" parts = [ - sexp_call("nav-link", + sx_call("nav-link", href=call_url(ctx, "account_url", "/newsletters/"), label="newsletters", select_colours=ctx.get("select_colours", ""), @@ -38,22 +38,22 @@ def _auth_nav_sexp(ctx: dict) -> str: return "(<> " + " ".join(parts) + ")" -def _auth_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the account section header row.""" - return sexp_call( + return sx_call( "menu-row-sx", id="auth-row", level=1, colour="sky", link_href=call_url(ctx, "account_url", "/"), link_label="account", icon="fa-solid fa-user", - nav=SexpExpr(_auth_nav_sexp(ctx)), + nav=SxExpr(_auth_nav_sx(ctx)), child_id="auth-header-child", oob=oob, ) -def _auth_nav_mobile_sexp(ctx: dict) -> str: +def _auth_nav_mobile_sx(ctx: dict) -> str: """Mobile nav menu for auth section.""" parts = [ - sexp_call("nav-link", + sx_call("nav-link", href=call_url(ctx, "account_url", "/newsletters/"), label="newsletters", select_colours=ctx.get("select_colours", ""), @@ -69,7 +69,7 @@ def _auth_nav_mobile_sexp(ctx: dict) -> str: # Account dashboard (GET /) # --------------------------------------------------------------------------- -def _account_main_panel_sexp(ctx: dict) -> str: +def _account_main_panel_sx(ctx: dict) -> str: """Account info panel with user details and logout.""" from quart import g from shared.browser.app.csrf import generate_csrf_token @@ -77,33 +77,33 @@ def _account_main_panel_sexp(ctx: dict) -> str: user = getattr(g, "user", None) error = ctx.get("error", "") - error_sexp = sexp_call("account-error-banner", error=error) if error else "" + error_sx = sx_call("account-error-banner", error=error) if error else "" - user_email_sexp = "" - user_name_sexp = "" + user_email_sx = "" + user_name_sx = "" if user: - user_email_sexp = sexp_call("account-user-email", email=user.email) + user_email_sx = sx_call("account-user-email", email=user.email) if user.name: - user_name_sexp = sexp_call("account-user-name", name=user.name) + user_name_sx = sx_call("account-user-name", name=user.name) - logout_sexp = sexp_call("account-logout-form", csrf_token=generate_csrf_token()) + logout_sx = sx_call("account-logout-form", csrf_token=generate_csrf_token()) - labels_sexp = "" + labels_sx = "" if user and hasattr(user, "labels") and user.labels: label_items = " ".join( - sexp_call("account-label-item", name=label.name) + sx_call("account-label-item", name=label.name) for label in user.labels ) - labels_sexp = sexp_call("account-labels-section", - items=SexpExpr("(<> " + label_items + ")")) + labels_sx = sx_call("account-labels-section", + items=SxExpr("(<> " + label_items + ")")) - return sexp_call( + return sx_call( "account-main-panel", - error=SexpExpr(error_sexp) if error_sexp else None, - email=SexpExpr(user_email_sexp) if user_email_sexp else None, - name=SexpExpr(user_name_sexp) if user_name_sexp else None, - logout=SexpExpr(logout_sexp), - labels=SexpExpr(labels_sexp) if labels_sexp else None, + error=SxExpr(error_sx) if error_sx else None, + email=SxExpr(user_email_sx) if user_email_sx else None, + name=SxExpr(user_name_sx) if user_name_sx else None, + logout=SxExpr(logout_sx), + labels=SxExpr(labels_sx) if labels_sx else None, ) @@ -111,7 +111,7 @@ def _account_main_panel_sexp(ctx: dict) -> str: # Newsletters (GET /newsletters/) # --------------------------------------------------------------------------- -def _newsletter_toggle_sexp(un: Any, account_url_fn: Any, csrf_token: str) -> str: +def _newsletter_toggle_sx(un: Any, account_url_fn: Any, csrf_token: str) -> str: """Render a single newsletter toggle switch.""" nid = un.newsletter_id toggle_url = account_url_fn(f"/newsletter/{nid}/toggle/") @@ -123,7 +123,7 @@ def _newsletter_toggle_sexp(un: Any, account_url_fn: Any, csrf_token: str) -> st bg = "bg-stone-300" translate = "translate-x-1" checked = "false" - return sexp_call( + return sx_call( "account-newsletter-toggle", id=f"nl-{nid}", url=toggle_url, hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}', @@ -134,9 +134,9 @@ def _newsletter_toggle_sexp(un: Any, account_url_fn: Any, csrf_token: str) -> st ) -def _newsletter_toggle_off_sexp(nid: int, toggle_url: str, csrf_token: str) -> str: +def _newsletter_toggle_off_sx(nid: int, toggle_url: str, csrf_token: str) -> str: """Render an unsubscribed newsletter toggle (no subscription record yet).""" - return sexp_call( + return sx_call( "account-newsletter-toggle-off", id=f"nl-{nid}", url=toggle_url, hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}', @@ -144,7 +144,7 @@ def _newsletter_toggle_off_sexp(nid: int, toggle_url: str, csrf_token: str) -> s ) -def _newsletters_panel_sexp(ctx: dict, newsletter_list: list) -> str: +def _newsletters_panel_sx(ctx: dict, newsletter_list: list) -> str: """Newsletters management panel.""" from shared.browser.app.csrf import generate_csrf_token @@ -157,30 +157,30 @@ def _newsletters_panel_sexp(ctx: dict, newsletter_list: list) -> str: nl = item["newsletter"] un = item.get("un") - desc_sexp = sexp_call( + desc_sx = sx_call( "account-newsletter-desc", description=nl.description ) if nl.description else "" if un: - toggle = _newsletter_toggle_sexp(un, account_url_fn, csrf) + toggle = _newsletter_toggle_sx(un, account_url_fn, csrf) else: toggle_url = account_url_fn(f"/newsletter/{nl.id}/toggle/") - toggle = _newsletter_toggle_off_sexp(nl.id, toggle_url, csrf) + toggle = _newsletter_toggle_off_sx(nl.id, toggle_url, csrf) - items.append(sexp_call( + items.append(sx_call( "account-newsletter-item", name=nl.name, - desc=SexpExpr(desc_sexp) if desc_sexp else None, - toggle=SexpExpr(toggle), + desc=SxExpr(desc_sx) if desc_sx else None, + toggle=SxExpr(toggle), )) - list_sexp = sexp_call( + list_sx = sx_call( "account-newsletter-list", - items=SexpExpr("(<> " + " ".join(items) + ")"), + items=SxExpr("(<> " + " ".join(items) + ")"), ) else: - list_sexp = sexp_call("account-newsletter-empty") + list_sx = sx_call("account-newsletter-empty") - return sexp_call("account-newsletters-panel", list=SexpExpr(list_sexp)) + return sx_call("account-newsletters-panel", list=SxExpr(list_sx)) # --------------------------------------------------------------------------- @@ -196,11 +196,11 @@ def _login_page_content(ctx: dict) -> str: email = ctx.get("email", "") action = url_for("auth.start_login") - error_sexp = sexp_call("account-login-error", error=error) if error else "" + error_sx = sx_call("account-login-error", error=error) if error else "" - return sexp_call( + return sx_call( "account-login-form", - error=SexpExpr(error_sexp) if error_sexp else None, + error=SxExpr(error_sx) if error_sx else None, action=action, csrf_token=generate_csrf_token(), email=email, ) @@ -215,11 +215,11 @@ def _device_page_content(ctx: dict) -> str: code = ctx.get("code", "") action = url_for("auth.device_submit") - error_sexp = sexp_call("account-device-error", error=error) if error else "" + error_sx = sx_call("account-device-error", error=error) if error else "" - return sexp_call( + return sx_call( "account-device-form", - error=SexpExpr(error_sexp) if error_sexp else None, + error=SxExpr(error_sx) if error_sx else None, action=action, csrf_token=generate_csrf_token(), code=code, ) @@ -227,7 +227,7 @@ def _device_page_content(ctx: dict) -> str: def _device_approved_content() -> str: """Device approved success content.""" - return sexp_call("account-device-approved") + return sx_call("account-device-approved") # --------------------------------------------------------------------------- @@ -236,26 +236,26 @@ def _device_approved_content() -> str: async def render_account_page(ctx: dict) -> str: """Full page: account dashboard.""" - main = _account_main_panel_sexp(ctx) + main = _account_main_panel_sx(ctx) - hdr = root_header_sexp(ctx) - hdr_child = header_child_sexp(_auth_header_sexp(ctx)) + hdr = root_header_sx(ctx) + hdr_child = header_child_sx(_auth_header_sx(ctx)) header_rows = "(<> " + hdr + " " + hdr_child + ")" - return full_page_sexp(ctx, header_rows=header_rows, + return full_page_sx(ctx, header_rows=header_rows, content=main, - menu=_auth_nav_mobile_sexp(ctx)) + menu=_auth_nav_mobile_sx(ctx)) async def render_account_oob(ctx: dict) -> str: """OOB response for account dashboard.""" - main = _account_main_panel_sexp(ctx) + main = _account_main_panel_sx(ctx) - oobs = "(<> " + _auth_header_sexp(ctx, oob=True) + " " + root_header_sexp(ctx, oob=True) + ")" + oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")" - return oob_page_sexp(oobs=oobs, + return oob_page_sx(oobs=oobs, content=main, - menu=_auth_nav_mobile_sexp(ctx)) + menu=_auth_nav_mobile_sx(ctx)) # --------------------------------------------------------------------------- @@ -264,26 +264,26 @@ async def render_account_oob(ctx: dict) -> str: async def render_newsletters_page(ctx: dict, newsletter_list: list) -> str: """Full page: newsletters.""" - main = _newsletters_panel_sexp(ctx, newsletter_list) + main = _newsletters_panel_sx(ctx, newsletter_list) - hdr = root_header_sexp(ctx) - hdr_child = header_child_sexp(_auth_header_sexp(ctx)) + hdr = root_header_sx(ctx) + hdr_child = header_child_sx(_auth_header_sx(ctx)) header_rows = "(<> " + hdr + " " + hdr_child + ")" - return full_page_sexp(ctx, header_rows=header_rows, + return full_page_sx(ctx, header_rows=header_rows, content=main, - menu=_auth_nav_mobile_sexp(ctx)) + menu=_auth_nav_mobile_sx(ctx)) async def render_newsletters_oob(ctx: dict, newsletter_list: list) -> str: """OOB response for newsletters.""" - main = _newsletters_panel_sexp(ctx, newsletter_list) + main = _newsletters_panel_sx(ctx, newsletter_list) - oobs = "(<> " + _auth_header_sexp(ctx, oob=True) + " " + root_header_sexp(ctx, oob=True) + ")" + oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")" - return oob_page_sexp(oobs=oobs, + return oob_page_sx(oobs=oobs, content=main, - menu=_auth_nav_mobile_sexp(ctx)) + menu=_auth_nav_mobile_sx(ctx)) # --------------------------------------------------------------------------- @@ -292,22 +292,22 @@ async def render_newsletters_oob(ctx: dict, newsletter_list: list) -> str: async def render_fragment_page(ctx: dict, page_fragment_html: str) -> str: """Full page: fragment-provided content.""" - hdr = root_header_sexp(ctx) - hdr_child = header_child_sexp(_auth_header_sexp(ctx)) + hdr = root_header_sx(ctx) + hdr_child = header_child_sx(_auth_header_sx(ctx)) header_rows = "(<> " + hdr + " " + hdr_child + ")" - return full_page_sexp(ctx, header_rows=header_rows, - content=f'(~rich-text :html "{_sexp_escape(page_fragment_html)}")', - menu=_auth_nav_mobile_sexp(ctx)) + return full_page_sx(ctx, header_rows=header_rows, + content=f'(~rich-text :html "{_sx_escape(page_fragment_html)}")', + menu=_auth_nav_mobile_sx(ctx)) async def render_fragment_oob(ctx: dict, page_fragment_html: str) -> str: """OOB response for fragment pages.""" - oobs = "(<> " + _auth_header_sexp(ctx, oob=True) + " " + root_header_sexp(ctx, oob=True) + ")" + oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")" - return oob_page_sexp(oobs=oobs, - content=f'(~rich-text :html "{_sexp_escape(page_fragment_html)}")', - menu=_auth_nav_mobile_sexp(ctx)) + return oob_page_sx(oobs=oobs, + content=f'(~rich-text :html "{_sx_escape(page_fragment_html)}")', + menu=_auth_nav_mobile_sx(ctx)) # --------------------------------------------------------------------------- @@ -316,24 +316,24 @@ async def render_fragment_oob(ctx: dict, page_fragment_html: str) -> str: async def render_login_page(ctx: dict) -> str: """Full page: login form.""" - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=_login_page_content(ctx), meta_html='Login \u2014 Rose Ash') async def render_device_page(ctx: dict) -> str: """Full page: device authorization form.""" - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=_device_page_content(ctx), meta_html='Authorize Device \u2014 Rose Ash') async def render_device_approved_page(ctx: dict) -> str: """Full page: device approved.""" - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=_device_approved_content(), meta_html='Device Authorized \u2014 Rose Ash') @@ -346,14 +346,14 @@ def _check_email_content(email: str, email_error: str | None = None) -> str: """Check email confirmation content.""" from markupsafe import escape - error_sexp = sexp_call( + error_sx = sx_call( "account-check-email-error", error=str(escape(email_error)) ) if email_error else "" - return sexp_call( + return sx_call( "account-check-email", email=str(escape(email)), - error=SexpExpr(error_sexp) if error_sexp else None, + error=SxExpr(error_sx) if error_sx else None, ) @@ -361,8 +361,8 @@ async def render_check_email_page(ctx: dict) -> str: """Full page: check email after magic link sent.""" email = ctx.get("email", "") email_error = ctx.get("email_error") - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=_check_email_content(email, email_error), meta_html='Check your email \u2014 Rose Ash') @@ -380,13 +380,13 @@ def render_newsletter_toggle(un) -> str: if account_url_fn is None: from shared.infrastructure.urls import account_url account_url_fn = account_url - return _newsletter_toggle_sexp(un, account_url_fn, generate_csrf_token()) + return _newsletter_toggle_sx(un, account_url_fn, generate_csrf_token()) # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- -def _sexp_escape(s: str) -> str: - """Escape a string for embedding in sexp string literals.""" +def _sx_escape(s: str) -> str: + """Escape a string for embedding in sx string literals.""" return s.replace("\\", "\\\\").replace('"', '\\"') diff --git a/blog/app.py b/blog/app.py index 78df1b8..38fb151 100644 --- a/blog/app.py +++ b/blog/app.py @@ -1,6 +1,6 @@ from __future__ import annotations import path_setup # noqa: F401 # adds shared/ to sys.path -import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file +import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file from pathlib import Path from quart import g, request diff --git a/blog/bp/admin/routes.py b/blog/bp/admin/routes.py index e8e9cb3..b2b560c 100644 --- a/blog/bp/admin/routes.py +++ b/blog/bp/admin/routes.py @@ -14,7 +14,7 @@ from quart import ( from shared.browser.app.redis_cacher import clear_all_cache from shared.browser.app.authz import require_admin from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from shared.config import config from datetime import datetime @@ -30,30 +30,30 @@ def register(url_prefix): @bp.get("/") @require_admin async def home(): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_settings_page, render_settings_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_settings_page, render_settings_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_settings_page(tctx) return await make_response(html) else: - sexp_src = await render_settings_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_settings_oob(tctx) + return sx_response(sx_src) @bp.get("/cache/") @require_admin async def cache(): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_cache_page, render_cache_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_cache_page, render_cache_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_cache_page(tctx) return await make_response(html) else: - sexp_src = await render_cache_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_cache_oob(tctx) + return sx_response(sx_src) @bp.post("/cache_clear/") @require_admin @@ -61,9 +61,9 @@ def register(url_prefix): await clear_all_cache() if is_htmx_request(): now = datetime.now() - from shared.sexp.jinja_bridge import render as render_comp + from shared.sx.jinja_bridge import render as render_comp html = render_comp("cache-cleared", time_str=now.strftime("%H:%M:%S")) - return sexp_response(html) + return sx_response(html) return redirect(url_for("settings.cache")) return bp diff --git a/blog/bp/blog/admin/routes.py b/blog/bp/blog/admin/routes.py index c2f9aba..26bac5c 100644 --- a/blog/bp/blog/admin/routes.py +++ b/blog/bp/blog/admin/routes.py @@ -15,7 +15,7 @@ from sqlalchemy import select, delete from shared.browser.app.authz import require_admin from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.redis_cacher import invalidate_tag_cache -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from models.tag_group import TagGroup, TagGroupTag from models.ghost_content import Tag @@ -58,15 +58,15 @@ def register(): ctx = {"groups": groups, "unassigned_tags": unassigned} - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_tag_groups_page, render_tag_groups_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_tag_groups_page, render_tag_groups_oob tctx = await get_template_context() tctx.update(ctx) if not is_htmx_request(): return await make_response(await render_tag_groups_page(tctx)) else: - return sexp_response(await render_tag_groups_oob(tctx)) + return sx_response(await render_tag_groups_oob(tctx)) @bp.post("/") @require_admin @@ -123,15 +123,15 @@ def register(): "assigned_tag_ids": assigned_tag_ids, } - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_tag_group_edit_page, render_tag_group_edit_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_tag_group_edit_page, render_tag_group_edit_oob tctx = await get_template_context() tctx.update(ctx) if not is_htmx_request(): return await make_response(await render_tag_group_edit_page(tctx)) else: - return sexp_response(await render_tag_group_edit_oob(tctx)) + return sx_response(await render_tag_group_edit_oob(tctx)) @bp.post("//") @require_admin diff --git a/blog/bp/blog/routes.py b/blog/bp/blog/routes.py index 9f3b6f0..e860450 100644 --- a/blog/bp/blog/routes.py +++ b/blog/bp/blog/routes.py @@ -22,7 +22,7 @@ from .services.pages_data import pages_data from shared.browser.app.redis_cacher import cache_page, invalidate_tag_cache from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.authz import require_admin -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from shared.utils import host_url def register(url_prefix, title): @@ -143,8 +143,8 @@ def register(url_prefix, title): ctx["page_cart_count"] = page_summary.count + page_summary.calendar_count + page_summary.ticket_count ctx["page_cart_total"] = float(page_summary.total + page_summary.calendar_total + page_summary.ticket_total) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_home_page, render_home_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_home_page, render_home_oob tctx = await get_template_context() tctx.update(ctx) @@ -152,8 +152,8 @@ def register(url_prefix, title): html = await render_home_page(tctx) return await make_response(html) else: - sexp_src = await render_home_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_home_oob(tctx) + return sx_response(sx_src) @blogs_bp.get("/index") @blogs_bp.get("/index/") @@ -181,8 +181,8 @@ def register(url_prefix, title): "tag_groups": [], "posts": data.get("pages", []), } - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_blog_page, render_blog_oob, render_blog_page_cards + from shared.sx.page import get_template_context + from sx.sx_components import render_blog_page, render_blog_oob, render_blog_page_cards tctx = await get_template_context() tctx.update(context) @@ -190,11 +190,11 @@ def register(url_prefix, title): html = await render_blog_page(tctx) return await make_response(html) elif q.page > 1: - sexp_src = await render_blog_page_cards(tctx) - return sexp_response(sexp_src) + sx_src = await render_blog_page_cards(tctx) + return sx_response(sx_src) else: - sexp_src = await render_blog_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_blog_oob(tctx) + return sx_response(sx_src) # Default: posts listing # Drafts filter requires login; ignore if not logged in @@ -224,8 +224,8 @@ def register(url_prefix, title): "drafts": q.drafts if show_drafts else None, } - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_blog_page, render_blog_oob, render_blog_cards + from shared.sx.page import get_template_context + from sx.sx_components import render_blog_page, render_blog_oob, render_blog_cards tctx = await get_template_context() tctx.update(context) @@ -233,18 +233,18 @@ def register(url_prefix, title): html = await render_blog_page(tctx) return await make_response(html) elif q.page > 1: - # Sexp wire format — client renders blog cards - sexp_src = await render_blog_cards(tctx) - return sexp_response(sexp_src) + # Sx wire format — client renders blog cards + sx_src = await render_blog_cards(tctx) + return sx_response(sx_src) else: - sexp_src = await render_blog_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_blog_oob(tctx) + return sx_response(sx_src) @blogs_bp.get("/new/") @require_admin async def new_post(): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_new_post_page, render_new_post_oob, render_editor_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_new_post_page, render_new_post_oob, render_editor_panel tctx = await get_template_context() tctx["editor_html"] = render_editor_panel() @@ -252,8 +252,8 @@ def register(url_prefix, title): html = await render_new_post_page(tctx) return await make_response(html) else: - sexp_src = await render_new_post_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_new_post_oob(tctx) + return sx_response(sx_src) @blogs_bp.post("/new/") @require_admin @@ -274,8 +274,8 @@ def register(url_prefix, title): try: lexical_doc = json.loads(lexical_raw) except (json.JSONDecodeError, TypeError): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_new_post_page, render_editor_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_new_post_page, render_editor_panel tctx = await get_template_context() tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.") html = await render_new_post_page(tctx) @@ -283,8 +283,8 @@ def register(url_prefix, title): ok, reason = validate_lexical(lexical_doc) if not ok: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_new_post_page, render_editor_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_new_post_page, render_editor_panel tctx = await get_template_context() tctx["editor_html"] = render_editor_panel(save_error=reason) html = await render_new_post_page(tctx) @@ -324,8 +324,8 @@ def register(url_prefix, title): @blogs_bp.get("/new-page/") @require_admin async def new_page(): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_new_post_page, render_new_post_oob, render_editor_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_new_post_page, render_new_post_oob, render_editor_panel tctx = await get_template_context() tctx["editor_html"] = render_editor_panel(is_page=True) @@ -334,8 +334,8 @@ def register(url_prefix, title): html = await render_new_post_page(tctx) return await make_response(html) else: - sexp_src = await render_new_post_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_new_post_oob(tctx) + return sx_response(sx_src) @blogs_bp.post("/new-page/") @require_admin @@ -356,8 +356,8 @@ def register(url_prefix, title): try: lexical_doc = json.loads(lexical_raw) except (json.JSONDecodeError, TypeError): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_new_post_page, render_editor_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_new_post_page, render_editor_panel tctx = await get_template_context() tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.", is_page=True) tctx["is_page"] = True @@ -366,8 +366,8 @@ def register(url_prefix, title): ok, reason = validate_lexical(lexical_doc) if not ok: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_new_post_page, render_editor_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_new_post_page, render_editor_panel tctx = await get_template_context() tctx["editor_html"] = render_editor_panel(save_error=reason, is_page=True) tctx["is_page"] = True diff --git a/blog/bp/fragments/routes.py b/blog/bp/fragments/routes.py index 44269dd..7e16a44 100644 --- a/blog/bp/fragments/routes.py +++ b/blog/bp/fragments/routes.py @@ -1,6 +1,6 @@ """Blog app fragment endpoints. -Exposes sexp fragments at ``/internal/fragments/`` for consumption +Exposes sx fragments at ``/internal/fragments/`` for consumption by other coop apps via the fragment client. """ @@ -26,13 +26,13 @@ def register(): async def get_fragment(fragment_type: str): handler = _handlers.get(fragment_type) if handler is None: - return Response("", status=200, content_type="text/sexp") + return Response("", status=200, content_type="text/sx") result = await handler() - return Response(result, status=200, content_type="text/sexp") + return Response(result, status=200, content_type="text/sx") - # --- nav-tree fragment — returns sexp source --- + # --- nav-tree fragment — returns sx source --- async def _nav_tree_handler(): - from shared.sexp.helpers import sexp_call, SexpExpr + from shared.sx.helpers import sx_call, SxExpr from shared.infrastructure.urls import ( blog_url, cart_url, market_url, events_url, federation_url, account_url, artdag_url, @@ -54,36 +54,36 @@ def register(): nav_cls = "whitespace-nowrap flex items-center gap-2 rounded p-2 text-sm" - item_sexps = [] + item_sxs = [] for item in menu_items: href = app_slugs.get(item.slug, blog_url(f"/{item.slug}/")) selected = "true" if (item.slug == first_seg or item.slug == app_name) else "false" - img = sexp_call("blog-nav-item-image", + img = sx_call("blog-nav-item-image", src=getattr(item, "feature_image", None), label=getattr(item, "label", item.slug)) - item_sexps.append(sexp_call( + item_sxs.append(sx_call( "blog-nav-item-link", href=href, hx_get=href, selected=selected, nav_cls=nav_cls, - img=SexpExpr(img), label=getattr(item, "label", item.slug), + img=SxExpr(img), label=getattr(item, "label", item.slug), )) # artdag link href = artdag_url("/") selected = "true" if ("artdag" == first_seg or "artdag" == app_name) else "false" - img = sexp_call("blog-nav-item-image", src=None, label="art-dag") - item_sexps.append(sexp_call( + img = sx_call("blog-nav-item-image", src=None, label="art-dag") + item_sxs.append(sx_call( "blog-nav-item-link", href=href, hx_get=href, selected=selected, nav_cls=nav_cls, - img=SexpExpr(img), label="art-dag", + img=SxExpr(img), label="art-dag", )) - if not item_sexps: - return sexp_call("blog-nav-empty", + if not item_sxs: + return sx_call("blog-nav-empty", wrapper_id="menu-items-nav-wrapper") - items_frag = "(<> " + " ".join(item_sexps) + ")" + items_frag = "(<> " + " ".join(item_sxs) + ")" arrow_cls = "scrolling-menu-arrow-menu-items-container" container_id = "menu-items-container" @@ -101,21 +101,21 @@ def register(): right_hs = ("on click set #" + container_id + ".scrollLeft to #" + container_id + ".scrollLeft + 200") - return sexp_call("blog-nav-wrapper", + return sx_call("blog-nav-wrapper", arrow_cls=arrow_cls, container_id=container_id, left_hs=left_hs, scroll_hs=scroll_hs, right_hs=right_hs, - items=SexpExpr(items_frag)) + items=SxExpr(items_frag)) _handlers["nav-tree"] = _nav_tree_handler - # --- link-card fragment — returns sexp source --- - def _blog_link_card_sexp(post, link: str) -> str: - from shared.sexp.helpers import sexp_call + # --- link-card fragment — returns sx source --- + def _blog_link_card_sx(post, link: str) -> str: + from shared.sx.helpers import sx_call published = post.published_at.strftime("%d %b %Y") if post.published_at else None - return sexp_call("link-card", + return sx_call("link-card", link=link, title=post.title, image=post.feature_image, @@ -139,7 +139,7 @@ def register(): parts.append(f"") post = await services.blog.get_post_by_slug(g.s, s) if post: - parts.append(_blog_link_card_sexp(post, blog_url(f"/{post.slug}"))) + parts.append(_blog_link_card_sx(post, blog_url(f"/{post.slug}"))) return "\n".join(parts) # Single mode @@ -148,7 +148,7 @@ def register(): post = await services.blog.get_post_by_slug(g.s, slug) if not post: return "" - return _blog_link_card_sexp(post, blog_url(f"/{post.slug}")) + return _blog_link_card_sx(post, blog_url(f"/{post.slug}")) _handlers["link-card"] = _link_card_handler diff --git a/blog/bp/menu_items/routes.py b/blog/bp/menu_items/routes.py index 4022b0c..ccf4248 100644 --- a/blog/bp/menu_items/routes.py +++ b/blog/bp/menu_items/routes.py @@ -13,14 +13,14 @@ from .services.menu_items import ( MenuItemError, ) from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): bp = Blueprint("menu_items", __name__, url_prefix='/settings/menu_items') def get_menu_items_nav_oob_sync(menu_items): """Helper to generate OOB update for root nav menu items""" - from sexp.sexp_components import render_menu_items_nav_oob + from sx.sx_components import render_menu_items_nav_oob return render_menu_items_nav_oob(menu_items) @bp.get("/") @@ -30,8 +30,8 @@ def register(): menu_items = await get_all_menu_items(g.s) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_menu_items_page, render_menu_items_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_menu_items_page, render_menu_items_oob tctx = await get_template_context() tctx["menu_items"] = menu_items @@ -39,8 +39,8 @@ def register(): html = await render_menu_items_page(tctx) return await make_response(html) else: - sexp_src = await render_menu_items_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_menu_items_oob(tctx) + return sx_response(sx_src) @bp.get("/new/") @require_admin @@ -73,10 +73,10 @@ def register(): # Get updated list and nav OOB menu_items = await get_all_menu_items(g.s) - from sexp.sexp_components import render_menu_items_list + from sx.sx_components import render_menu_items_list html = render_menu_items_list(menu_items) nav_oob = get_menu_items_nav_oob_sync(menu_items) - return sexp_response(html + nav_oob) + return sx_response(html + nav_oob) except MenuItemError as e: return jsonify({"message": str(e), "errors": {}}), 400 @@ -116,10 +116,10 @@ def register(): # Get updated list and nav OOB menu_items = await get_all_menu_items(g.s) - from sexp.sexp_components import render_menu_items_list + from sx.sx_components import render_menu_items_list html = render_menu_items_list(menu_items) nav_oob = get_menu_items_nav_oob_sync(menu_items) - return sexp_response(html + nav_oob) + return sx_response(html + nav_oob) except MenuItemError as e: return jsonify({"message": str(e), "errors": {}}), 400 @@ -137,10 +137,10 @@ def register(): # Get updated list and nav OOB menu_items = await get_all_menu_items(g.s) - from sexp.sexp_components import render_menu_items_list + from sx.sx_components import render_menu_items_list html = render_menu_items_list(menu_items) nav_oob = get_menu_items_nav_oob_sync(menu_items) - return sexp_response(html + nav_oob) + return sx_response(html + nav_oob) @bp.get("/pages/search/") @require_admin @@ -184,9 +184,9 @@ def register(): # Get updated list and nav OOB menu_items = await get_all_menu_items(g.s) - from sexp.sexp_components import render_menu_items_list + from sx.sx_components import render_menu_items_list html = render_menu_items_list(menu_items) nav_oob = get_menu_items_nav_oob_sync(menu_items) - return sexp_response(html + nav_oob) + return sx_response(html + nav_oob) return bp diff --git a/blog/bp/post/admin/routes.py b/blog/bp/post/admin/routes.py index 066c8ff..0ff50fe 100644 --- a/blog/bp/post/admin/routes.py +++ b/blog/bp/post/admin/routes.py @@ -12,7 +12,7 @@ from quart import ( ) from shared.browser.app.authz import require_admin, require_post_author from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from shared.utils import host_url def register(): @@ -52,8 +52,8 @@ def register(): "sumup_checkout_prefix": sumup_checkout_prefix, } - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_post_admin_page, render_post_admin_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_post_admin_page, render_post_admin_oob tctx = await get_template_context() tctx.update(ctx) @@ -61,8 +61,8 @@ def register(): html = await render_post_admin_page(tctx) return await make_response(html) else: - sexp_src = await render_post_admin_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_post_admin_oob(tctx) + return sx_response(sx_src) @bp.put("/features/") @require_admin @@ -99,14 +99,14 @@ def register(): features = result.get("features", {}) - from sexp.sexp_components import render_features_panel + from sx.sx_components import render_features_panel html = render_features_panel( features, post, sumup_configured=result.get("sumup_configured", False), sumup_merchant_code=result.get("sumup_merchant_code") or "", sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "", ) - return sexp_response(html) + return sx_response(html) @bp.put("/admin/sumup/") @require_admin @@ -138,20 +138,20 @@ def register(): result = await call_action("blog", "update-page-config", payload=payload) features = result.get("features", {}) - from sexp.sexp_components import render_features_panel + from sx.sx_components import render_features_panel html = render_features_panel( features, post, sumup_configured=result.get("sumup_configured", False), sumup_merchant_code=result.get("sumup_merchant_code") or "", sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "", ) - return sexp_response(html) + return sx_response(html) @bp.get("/data/") @require_admin async def data(slug: str): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_post_data_page, render_post_data_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_post_data_page, render_post_data_oob data_html = await render_template("_types/post_data/_main_panel.html") tctx = await get_template_context() @@ -160,8 +160,8 @@ def register(): html = await render_post_data_page(tctx) return await make_response(html) else: - sexp_src = await render_post_data_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_post_data_oob(tctx) + return sx_response(sx_src) @bp.get("/entries/calendar//") @require_admin @@ -269,8 +269,8 @@ def register(): # Load entries and post for each calendar for calendar in all_calendars: await g.s.refresh(calendar, ["entries", "post"]) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_post_entries_page, render_post_entries_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_post_entries_page, render_post_entries_oob entries_html = await render_template( "_types/post_entries/_main_panel.html", @@ -283,8 +283,8 @@ def register(): html = await render_post_entries_page(tctx) return await make_response(html) else: - sexp_src = await render_post_entries_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_post_entries_oob(tctx) + return sx_response(sx_src) @bp.post("/entries//toggle/") @require_admin @@ -330,13 +330,13 @@ def register(): ).scalars().all() # Return the associated entries admin list + OOB update for nav entries - from sexp.sexp_components import render_associated_entries, render_nav_entries_oob + from sx.sx_components import render_associated_entries, render_nav_entries_oob post = g.post_data["post"] admin_list = render_associated_entries(all_calendars, associated_entry_ids, post["slug"]) nav_entries_html = render_nav_entries_oob(associated_entries, calendars, post) - return sexp_response(admin_list + nav_entries_html) + return sx_response(admin_list + nav_entries_html) @bp.get("/settings/") @require_post_author @@ -348,8 +348,8 @@ def register(): ghost_post = await get_post_for_edit(ghost_id, is_page=is_page) save_success = request.args.get("saved") == "1" - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_post_settings_page, render_post_settings_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_post_settings_page, render_post_settings_oob settings_html = await render_template( "_types/post_settings/_main_panel.html", @@ -362,8 +362,8 @@ def register(): html = await render_post_settings_page(tctx) return await make_response(html) else: - sexp_src = await render_post_settings_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_post_settings_oob(tctx) + return sx_response(sx_src) @bp.post("/settings/") @require_post_author @@ -452,8 +452,8 @@ def register(): from types import SimpleNamespace newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters] - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_post_edit_page, render_post_edit_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_post_edit_page, render_post_edit_oob edit_html = await render_template( "_types/post_edit/_main_panel.html", @@ -468,8 +468,8 @@ def register(): html = await render_post_edit_page(tctx) return await make_response(html) else: - sexp_src = await render_post_edit_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_post_edit_oob(tctx) + return sx_response(sx_src) @bp.post("/edit/") @require_post_author @@ -598,8 +598,8 @@ def register(): page_markets = await _fetch_page_markets(post_id) - from sexp.sexp_components import render_markets_panel - return sexp_response(render_markets_panel(page_markets, post)) + from sx.sx_components import render_markets_panel + return sx_response(render_markets_panel(page_markets, post)) @bp.post("/markets/new/") @require_admin @@ -624,8 +624,8 @@ def register(): # Return updated markets list page_markets = await _fetch_page_markets(post_id) - from sexp.sexp_components import render_markets_panel - return sexp_response(render_markets_panel(page_markets, post)) + from sx.sx_components import render_markets_panel + return sx_response(render_markets_panel(page_markets, post)) @bp.delete("/markets//") @require_admin @@ -644,7 +644,7 @@ def register(): # Return updated markets list page_markets = await _fetch_page_markets(post_id) - from sexp.sexp_components import render_markets_panel - return sexp_response(render_markets_panel(page_markets, post)) + from sx.sx_components import render_markets_panel + return sx_response(render_markets_panel(page_markets, post)) return bp diff --git a/blog/bp/post/routes.py b/blog/bp/post/routes.py index 73b67f4..8951937 100644 --- a/blog/bp/post/routes.py +++ b/blog/bp/post/routes.py @@ -21,7 +21,7 @@ from shared.browser.app.redis_cacher import cache_page, clear_cache from .admin.routes import register as register_admin from shared.config import config from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): bp = Blueprint("post", __name__, url_prefix='/') @@ -104,28 +104,28 @@ def register(): @bp.get("/") @cache_page(tag="post.post_detail") async def post_detail(slug: str): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_post_page, render_post_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_post_page, render_post_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_post_page(tctx) return await make_response(html) else: - sexp_src = await render_post_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_post_oob(tctx) + return sx_response(sx_src) @bp.post("/like/toggle/") @clear_cache(tag="post.post_detail", tag_scope="user") async def like_toggle(slug: str): from shared.utils import host_url - from sexp.sexp_components import render_like_toggle_button + from sx.sx_components import render_like_toggle_button like_url = host_url(url_for('blog.post.like_toggle', slug=slug)) # Get post_id from g.post_data if not g.user: - return sexp_response(render_like_toggle_button(slug, False, like_url), status=403) + return sx_response(render_like_toggle_button(slug, False, like_url), status=403) post_id = g.post_data["post"]["id"] user_id = g.user.id @@ -135,7 +135,7 @@ def register(): }) liked = result["liked"] - return sexp_response(render_like_toggle_button(slug, liked, like_url)) + return sx_response(render_like_toggle_button(slug, liked, like_url)) @bp.get("/w//") async def widget_paginate(slug: str, widget_domain: str): diff --git a/blog/bp/snippets/routes.py b/blog/bp/snippets/routes.py index 4d17408..d6828ee 100644 --- a/blog/bp/snippets/routes.py +++ b/blog/bp/snippets/routes.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import selectinload from shared.browser.app.authz import require_login from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from models import Snippet @@ -39,8 +39,8 @@ def register(): snippets = await _visible_snippets(g.s) is_admin = g.rights.get("admin") - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_snippets_page, render_snippets_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_snippets_page, render_snippets_oob tctx = await get_template_context() tctx["snippets"] = snippets @@ -49,8 +49,8 @@ def register(): html = await render_snippets_page(tctx) return await make_response(html) else: - sexp_src = await render_snippets_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_snippets_oob(tctx) + return sx_response(sx_src) @bp.delete("//") @require_login @@ -68,8 +68,8 @@ def register(): await g.s.flush() snippets = await _visible_snippets(g.s) - from sexp.sexp_components import render_snippets_list - return sexp_response(render_snippets_list(snippets, is_admin)) + from sx.sx_components import render_snippets_list + return sx_response(render_snippets_list(snippets, is_admin)) @bp.patch("//visibility/") @require_login @@ -92,7 +92,7 @@ def register(): await g.s.flush() snippets = await _visible_snippets(g.s) - from sexp.sexp_components import render_snippets_list - return sexp_response(render_snippets_list(snippets, True)) + from sx.sx_components import render_snippets_list + return sx_response(render_snippets_list(snippets, True)) return bp diff --git a/blog/sexp/__init__.py b/blog/sx/__init__.py similarity index 100% rename from blog/sexp/__init__.py rename to blog/sx/__init__.py diff --git a/blog/sexp/admin.sexpr b/blog/sx/admin.sx similarity index 100% rename from blog/sexp/admin.sexpr rename to blog/sx/admin.sx diff --git a/blog/sexp/cards.sexpr b/blog/sx/cards.sx similarity index 100% rename from blog/sexp/cards.sexpr rename to blog/sx/cards.sx diff --git a/blog/sexp/detail.sexpr b/blog/sx/detail.sx similarity index 100% rename from blog/sexp/detail.sexpr rename to blog/sx/detail.sx diff --git a/blog/sexp/editor.sexpr b/blog/sx/editor.sx similarity index 100% rename from blog/sexp/editor.sexpr rename to blog/sx/editor.sx diff --git a/blog/sexp/filters.sexpr b/blog/sx/filters.sx similarity index 100% rename from blog/sexp/filters.sexpr rename to blog/sx/filters.sx diff --git a/blog/sexp/header.sexpr b/blog/sx/header.sx similarity index 100% rename from blog/sexp/header.sexpr rename to blog/sx/header.sx diff --git a/blog/sexp/index.sexpr b/blog/sx/index.sx similarity index 100% rename from blog/sexp/index.sexpr rename to blog/sx/index.sx diff --git a/blog/sexp/nav.sexpr b/blog/sx/nav.sx similarity index 100% rename from blog/sexp/nav.sexpr rename to blog/sx/nav.sx diff --git a/blog/sexp/settings.sexpr b/blog/sx/settings.sx similarity index 100% rename from blog/sexp/settings.sexpr rename to blog/sx/settings.sx diff --git a/blog/sexp/sexp_components.py b/blog/sx/sx_components.py similarity index 71% rename from blog/sexp/sexp_components.py rename to blog/sx/sx_components.py index cc3d9e3..0678649 100644 --- a/blog/sexp/sexp_components.py +++ b/blog/sx/sx_components.py @@ -12,22 +12,22 @@ import os from typing import Any from markupsafe import escape -from shared.sexp.jinja_bridge import load_service_components -from shared.sexp.helpers import ( - SexpExpr, sexp_call, +from shared.sx.jinja_bridge import load_service_components +from shared.sx.helpers import ( + SxExpr, sx_call, call_url, get_asset_url, - root_header_sexp, - post_header_sexp, - post_admin_header_sexp, - header_child_sexp, - oob_header_sexp, - oob_page_sexp, - search_mobile_sexp, - search_desktop_sexp, - full_page_sexp, + root_header_sx, + post_header_sx, + post_admin_header_sx, + header_child_sx, + oob_header_sx, + oob_page_sx, + search_mobile_sx, + search_desktop_sx, + full_page_sx, ) -# Load blog service .sexpr component definitions +# Load blog service .sx component definitions load_service_components(os.path.dirname(os.path.dirname(__file__))) @@ -35,39 +35,39 @@ load_service_components(os.path.dirname(os.path.dirname(__file__))) # OOB header helper — delegates to shared # --------------------------------------------------------------------------- -_oob_header_sexp = oob_header_sexp +_oob_header_sx = oob_header_sx # --------------------------------------------------------------------------- # Blog header (root-header-child -> blog-header-child) # --------------------------------------------------------------------------- -def _blog_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _blog_header_sx(ctx: dict, *, oob: bool = False) -> str: """Blog header row — empty child of root.""" - label_sexp = sexp_call("blog-header-label") - return sexp_call("menu-row-sx", + label_sx = sx_call("blog-header-label") + return sx_call("menu-row-sx", id="blog-row", level=1, - link_label_content=SexpExpr(label_sexp), + link_label_content=SxExpr(label_sx), child_id="blog-header-child", oob=oob, ) # --------------------------------------------------------------------------- -# Post header helpers — thin wrapper over shared post_header_sexp +# Post header helpers — thin wrapper over shared post_header_sx # --------------------------------------------------------------------------- -def _post_header_sexp(ctx: dict, *, oob: bool = False) -> str: - """Build the post-level header row as sexp — delegates to shared helper.""" - return post_header_sexp(ctx, oob=oob) +def _post_header_sx(ctx: dict, *, oob: bool = False) -> str: + """Build the post-level header row as sx — delegates to shared helper.""" + return post_header_sx(ctx, oob=oob) # --------------------------------------------------------------------------- # Post admin header # --------------------------------------------------------------------------- -def _post_admin_header_sexp(ctx: dict, *, oob: bool = False, selected: str = "") -> str: - """Post admin header row as sexp — delegates to shared helper.""" +def _post_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str: + """Post admin header row as sx — delegates to shared helper.""" slug = (ctx.get("post") or {}).get("slug", "") - return post_admin_header_sexp(ctx, slug, oob=oob, selected=selected) + return post_admin_header_sx(ctx, slug, oob=oob, selected=selected) @@ -75,26 +75,26 @@ def _post_admin_header_sexp(ctx: dict, *, oob: bool = False, selected: str = "") # Settings header (root-header-child -> root-settings-header-child) # --------------------------------------------------------------------------- -def _settings_header_sexp(ctx: dict, *, oob: bool = False) -> str: - """Settings header row with admin icon and nav links (sexp).""" +def _settings_header_sx(ctx: dict, *, oob: bool = False) -> str: + """Settings header row with admin icon and nav links (sx).""" from quart import url_for as qurl settings_href = qurl("settings.home") - label_sexp = sexp_call("blog-admin-label") - nav_sexp = _settings_nav_sexp(ctx) + label_sx = sx_call("blog-admin-label") + nav_sx = _settings_nav_sx(ctx) - return sexp_call("menu-row-sx", + return sx_call("menu-row-sx", id="root-settings-row", level=1, link_href=settings_href, - link_label_content=SexpExpr(label_sexp), - nav=SexpExpr(nav_sexp) if nav_sexp else None, + link_label_content=SxExpr(label_sx), + nav=SxExpr(nav_sx) if nav_sx else None, child_id="root-settings-header-child", oob=oob, ) -def _settings_nav_sexp(ctx: dict) -> str: - """Settings desktop nav as sexp.""" +def _settings_nav_sx(ctx: dict) -> str: + """Settings desktop nav as sx.""" from quart import url_for as qurl select_colours = ctx.get("select_colours", "") @@ -107,7 +107,7 @@ def _settings_nav_sexp(ctx: dict) -> str: ("settings.cache", "refresh", "Cache"), ]: href = qurl(endpoint) - parts.append(sexp_call("nav-link", + parts.append(sx_call("nav-link", href=href, icon=f"fa fa-{icon}", label=label, select_colours=select_colours, )) @@ -120,19 +120,19 @@ def _settings_nav_sexp(ctx: dict) -> str: # Sub-settings headers (root-settings-header-child -> X-header-child) # --------------------------------------------------------------------------- -def _sub_settings_header_sexp(row_id: str, child_id: str, href: str, +def _sub_settings_header_sx(row_id: str, child_id: str, href: str, icon: str, label: str, ctx: dict, - *, oob: bool = False, nav_sexp: str = "") -> str: - """Generic sub-settings header row as sexp.""" - label_sexp = sexp_call("blog-sub-settings-label", + *, oob: bool = False, nav_sx: str = "") -> str: + """Generic sub-settings header row as sx.""" + label_sx = sx_call("blog-sub-settings-label", icon=f"fa fa-{icon}", label=label, ) - return sexp_call("menu-row-sx", + return sx_call("menu-row-sx", id=row_id, level=2, link_href=href, - link_label_content=SexpExpr(label_sexp), - nav=SexpExpr(nav_sexp) if nav_sexp else None, + link_label_content=SxExpr(label_sx), + nav=SxExpr(nav_sx) if nav_sx else None, child_id=child_id, oob=oob, ) @@ -144,16 +144,16 @@ def _sub_settings_header_sexp(row_id: str, child_id: str, href: str, -def _blog_sentinel_sexp(ctx: dict) -> str: - """Infinite scroll sentinels as sexp calls (for wire format).""" - from shared.sexp.helpers import sexp_call +def _blog_sentinel_sx(ctx: dict) -> str: + """Infinite scroll sentinels as sx calls (for wire format).""" + from shared.sx.helpers import sx_call page = ctx.get("page", 1) total_pages = ctx.get("total_pages", 1) if isinstance(total_pages, str): total_pages = int(total_pages) if page >= total_pages: - return sexp_call("blog-end-of-results") + return sx_call("blog-end-of-results") current_local_href = ctx.get("current_local_href", "/index") next_url = f"{current_local_href}?page={page + 1}" @@ -183,23 +183,23 @@ def _blog_sentinel_sexp(ctx: dict) -> str: ) return ( - sexp_call("blog-sentinel-mobile", id=f"sentinel-{page}-m", next_url=next_url, hyperscript=mobile_hs) + sx_call("blog-sentinel-mobile", id=f"sentinel-{page}-m", next_url=next_url, hyperscript=mobile_hs) + " " - + sexp_call("blog-sentinel-desktop", id=f"sentinel-{page}-d", next_url=next_url, hyperscript=desktop_hs) + + sx_call("blog-sentinel-desktop", id=f"sentinel-{page}-d", next_url=next_url, hyperscript=desktop_hs) ) -def _blog_cards_sexp(ctx: dict) -> str: +def _blog_cards_sx(ctx: dict) -> str: """S-expression wire format for blog cards (client renders).""" posts = ctx.get("posts") or [] view = ctx.get("view") parts = [] for p in posts: if view == "tile": - parts.append(_blog_card_tile_sexp(p, ctx)) + parts.append(_blog_card_tile_sx(p, ctx)) else: - parts.append(_blog_card_sexp(p, ctx)) - parts.append(_blog_sentinel_sexp(ctx)) + parts.append(_blog_card_sx(p, ctx)) + parts.append(_blog_sentinel_sx(ctx)) return "(<> " + " ".join(parts) + ")" @@ -230,8 +230,8 @@ def _author_data(authors: list) -> list[dict]: return result -def _blog_card_sexp(post: dict, ctx: dict) -> str: - """Single blog card as sexp call (wire format) — pure data, no HTML.""" +def _blog_card_sx(post: dict, ctx: dict) -> str: + """Single blog card as sx call (wire format) — pure data, no HTML.""" from quart import g slug = post.get("slug", "") @@ -275,13 +275,13 @@ def _blog_card_sexp(post: dict, ctx: dict) -> str: if authors: kwargs["authors"] = authors if widget: - kwargs["widget"] = SexpExpr(widget) if widget else None + kwargs["widget"] = SxExpr(widget) if widget else None - return sexp_call("blog-card", **kwargs) + return sx_call("blog-card", **kwargs) -def _blog_card_tile_sexp(post: dict, ctx: dict) -> str: - """Single blog card tile as sexp call (wire format) — pure data.""" +def _blog_card_tile_sx(post: dict, ctx: dict) -> str: + """Single blog card tile as sx call (wire format) — pure data.""" slug = post.get("slug", "") href = call_url(ctx, "blog_url", f"/{slug}/") hx_select = ctx.get("hx_select_search", "#main-panel") @@ -313,11 +313,11 @@ def _blog_card_tile_sexp(post: dict, ctx: dict) -> str: if authors: kwargs["authors"] = authors - return sexp_call("blog-card-tile", **kwargs) + return sx_call("blog-card-tile", **kwargs) -def _at_bar_sexp(post: dict, ctx: dict) -> str: - """Tags + authors bar below a card as sexp.""" +def _at_bar_sx(post: dict, ctx: dict) -> str: + """Tags + authors bar below a card as sx.""" tags = post.get("tags") or [] authors = post.get("authors") or [] if not tags and not authors: @@ -336,12 +336,12 @@ def _at_bar_sexp(post: dict, ctx: dict) -> str: for a in authors ] if authors else [] - return sexp_call("blog-at-bar", tags=tag_data, authors=author_data) + return sx_call("blog-at-bar", tags=tag_data, authors=author_data) -def _page_cards_sexp(ctx: dict) -> str: - """Render page cards with sentinel (sexp).""" +def _page_cards_sx(ctx: dict) -> str: + """Render page cards with sentinel (sx).""" pages = ctx.get("pages") or ctx.get("posts") or [] page_num = ctx.get("page", 1) total_pages = ctx.get("total_pages", 1) @@ -351,24 +351,24 @@ def _page_cards_sexp(ctx: dict) -> str: parts = [] for pg in pages: - parts.append(_page_card_sexp(pg, ctx)) + parts.append(_page_card_sx(pg, ctx)) if page_num < total_pages: current_local_href = ctx.get("current_local_href", "/index?type=pages") next_url = f"{current_local_href}&page={page_num + 1}" if "?" in current_local_href else f"{current_local_href}?page={page_num + 1}" - parts.append(sexp_call("blog-page-sentinel", + parts.append(sx_call("blog-page-sentinel", id=f"sentinel-{page_num}-d", next_url=next_url, )) elif pages: - parts.append(sexp_call("blog-end-of-results")) + parts.append(sx_call("blog-end-of-results")) else: - parts.append(sexp_call("blog-no-pages")) + parts.append(sx_call("blog-no-pages")) return "".join(parts) -def _page_card_sexp(page: dict, ctx: dict) -> str: - """Single page card as sexp.""" +def _page_card_sx(page: dict, ctx: dict) -> str: + """Single page card as sx.""" slug = page.get("slug", "") href = call_url(ctx, "blog_url", f"/{slug}/") hx_select = ctx.get("hx_select_search", "#main-panel") @@ -379,7 +379,7 @@ def _page_card_sexp(page: dict, ctx: dict) -> str: fi = page.get("feature_image") excerpt = page.get("custom_excerpt") or page.get("excerpt", "") - return sexp_call("blog-page-card", + return sx_call("blog-page-card", href=href, hx_select=hx_select, title=page.get("title", ""), has_calendar=features.get("calendar", False), has_market=features.get("market", False), @@ -388,7 +388,7 @@ def _page_card_sexp(page: dict, ctx: dict) -> str: ) -def _view_toggle_sexp(ctx: dict) -> str: +def _view_toggle_sx(ctx: dict) -> str: """View toggle bar (list/tile) for desktop.""" view = ctx.get("view") current_local_href = ctx.get("current_local_href", "/index") @@ -400,17 +400,17 @@ def _view_toggle_sexp(ctx: dict) -> str: list_href = f"{current_local_href}" tile_href = f"{current_local_href}{'&' if '?' in current_local_href else '?'}view=tile" - list_svg_sexp = sexp_call("blog-list-svg") - tile_svg_sexp = sexp_call("blog-tile-svg") + list_svg_sx = sx_call("blog-list-svg") + tile_svg_sx = sx_call("blog-tile-svg") - return sexp_call("blog-view-toggle", + return sx_call("blog-view-toggle", list_href=list_href, tile_href=tile_href, hx_select=hx_select, list_cls=list_cls, tile_cls=tile_cls, - list_svg=SexpExpr(list_svg_sexp), tile_svg=SexpExpr(tile_svg_sexp), + list_svg=SxExpr(list_svg_sx), tile_svg=SxExpr(tile_svg_sx), ) -def _content_type_tabs_sexp(ctx: dict) -> str: +def _content_type_tabs_sx(ctx: dict) -> str: """Posts/Pages tabs.""" content_type = ctx.get("content_type", "posts") hx_select = ctx.get("hx_select_search", "#main-panel") @@ -421,31 +421,31 @@ def _content_type_tabs_sexp(ctx: dict) -> str: posts_cls = "bg-stone-700 text-white" if content_type != "pages" else "bg-stone-100 text-stone-600 hover:bg-stone-200" pages_cls = "bg-stone-700 text-white" if content_type == "pages" else "bg-stone-100 text-stone-600 hover:bg-stone-200" - return sexp_call("blog-content-type-tabs", + return sx_call("blog-content-type-tabs", posts_href=posts_href, pages_href=pages_href, hx_select=hx_select, posts_cls=posts_cls, pages_cls=pages_cls, ) -def _blog_main_panel_sexp(ctx: dict) -> str: +def _blog_main_panel_sx(ctx: dict) -> str: """Blog index main panel with tabs, toggle, and cards.""" content_type = ctx.get("content_type", "posts") view = ctx.get("view") - tabs = _content_type_tabs_sexp(ctx) + tabs = _content_type_tabs_sx(ctx) if content_type == "pages": - cards = _page_cards_sexp(ctx) - return sexp_call("blog-main-panel-pages", - tabs=SexpExpr(tabs), cards=SexpExpr(cards), + cards = _page_cards_sx(ctx) + return sx_call("blog-main-panel-pages", + tabs=SxExpr(tabs), cards=SxExpr(cards), ) else: - toggle = _view_toggle_sexp(ctx) + toggle = _view_toggle_sx(ctx) grid_cls = "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" if view == "tile" else "max-w-full px-3 py-3 space-y-3" - cards = _blog_cards_sexp(ctx) - return sexp_call("blog-main-panel-posts", - tabs=SexpExpr(tabs), toggle=SexpExpr(toggle), grid_cls=grid_cls, - cards=SexpExpr(cards), + cards = _blog_cards_sx(ctx) + return sx_call("blog-main-panel-posts", + tabs=SxExpr(tabs), toggle=SxExpr(toggle), grid_cls=grid_cls, + cards=SxExpr(cards), ) @@ -453,49 +453,49 @@ def _blog_main_panel_sexp(ctx: dict) -> str: # Desktop aside (filter sidebar) # --------------------------------------------------------------------------- -def _blog_aside_sexp(ctx: dict) -> str: +def _blog_aside_sx(ctx: dict) -> str: """Desktop aside with search, action buttons, and filters.""" - sd = search_desktop_sexp(ctx) - ab = _action_buttons_sexp(ctx) - tgf = _tag_groups_filter_sexp(ctx) - af = _authors_filter_sexp(ctx) - return sexp_call("blog-aside", - search=SexpExpr(sd), action_buttons=SexpExpr(ab), - tag_groups_filter=SexpExpr(tgf), authors_filter=SexpExpr(af), + sd = search_desktop_sx(ctx) + ab = _action_buttons_sx(ctx) + tgf = _tag_groups_filter_sx(ctx) + af = _authors_filter_sx(ctx) + return sx_call("blog-aside", + search=SxExpr(sd), action_buttons=SxExpr(ab), + tag_groups_filter=SxExpr(tgf), authors_filter=SxExpr(af), ) -def _blog_filter_sexp(ctx: dict) -> str: +def _blog_filter_sx(ctx: dict) -> str: """Mobile filter (details/summary).""" # Mobile filter summary tags summary_parts = [] - tg_summary = _tag_groups_filter_summary_sexp(ctx) - au_summary = _authors_filter_summary_sexp(ctx) + tg_summary = _tag_groups_filter_summary_sx(ctx) + au_summary = _authors_filter_summary_sx(ctx) if tg_summary: summary_parts.append(tg_summary) if au_summary: summary_parts.append(au_summary) - search_sexp = search_mobile_sexp(ctx) + search_sx = search_mobile_sx(ctx) if summary_parts: - filter_content = "(<> " + search_sexp + " " + " ".join(summary_parts) + ")" + filter_content = "(<> " + search_sx + " " + " ".join(summary_parts) + ")" else: - filter_content = search_sexp + filter_content = search_sx - action_buttons = _action_buttons_sexp(ctx) - tgf = _tag_groups_filter_sexp(ctx) - af = _authors_filter_sexp(ctx) + action_buttons = _action_buttons_sx(ctx) + tgf = _tag_groups_filter_sx(ctx) + af = _authors_filter_sx(ctx) filter_details = "(<> " + tgf + " " + af + ")" - return sexp_call("mobile-filter", - filter_summary=SexpExpr(filter_content), - action_buttons=SexpExpr(action_buttons), - filter_details=SexpExpr(filter_details), + return sx_call("mobile-filter", + filter_summary=SxExpr(filter_content), + action_buttons=SxExpr(action_buttons), + filter_details=SxExpr(filter_details), ) -def _action_buttons_sexp(ctx: dict) -> str: - """New Post/Page + Drafts toggle buttons (sexp).""" +def _action_buttons_sx(ctx: dict) -> str: + """New Post/Page + Drafts toggle buttons (sx).""" from quart import g rights = ctx.get("rights") or {} @@ -510,13 +510,13 @@ def _action_buttons_sexp(ctx: dict) -> str: if has_admin: new_href = call_url(ctx, "blog_url", "/new/") - parts.append(sexp_call("blog-action-button", + parts.append(sx_call("blog-action-button", href=new_href, hx_select=hx_select, btn_class="px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors", title="New Post", icon_class="fa fa-plus mr-1", label=" New Post", )) new_page_href = call_url(ctx, "blog_url", "/new-page/") - parts.append(sexp_call("blog-action-button", + parts.append(sx_call("blog-action-button", href=new_page_href, hx_select=hx_select, btn_class="px-3 py-1 rounded bg-blue-600 text-white text-sm hover:bg-blue-700 transition-colors", title="New Page", icon_class="fa fa-plus mr-1", label=" New Page", @@ -525,27 +525,27 @@ def _action_buttons_sexp(ctx: dict) -> str: if user and (draft_count or drafts): if drafts: off_href = f"{current_local_href}" - parts.append(sexp_call("blog-drafts-button", + parts.append(sx_call("blog-drafts-button", href=off_href, hx_select=hx_select, btn_class="px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors", title="Hide Drafts", label=" Drafts ", draft_count=str(draft_count), )) else: on_href = f"{current_local_href}{'&' if '?' in current_local_href else '?'}drafts=1" - parts.append(sexp_call("blog-drafts-button-amber", + parts.append(sx_call("blog-drafts-button-amber", href=on_href, hx_select=hx_select, btn_class="px-3 py-1 rounded bg-amber-600 text-white text-sm hover:bg-amber-700 transition-colors", title="Show Drafts", label=" Drafts ", draft_count=str(draft_count), )) inner = "(<> " + " ".join(parts) + ")" if parts else "" - return sexp_call("blog-action-buttons-wrapper", - inner=SexpExpr(inner) if inner else None, + return sx_call("blog-action-buttons-wrapper", + inner=SxExpr(inner) if inner else None, ) -def _tag_groups_filter_sexp(ctx: dict) -> str: - """Tag group filter bar as sexp.""" +def _tag_groups_filter_sx(ctx: dict) -> str: + """Tag group filter bar as sx.""" tag_groups = ctx.get("tag_groups") or [] selected_groups = ctx.get("selected_groups") or () selected_tags = ctx.get("selected_tags") or () @@ -554,7 +554,7 @@ def _tag_groups_filter_sexp(ctx: dict) -> str: is_any = len(selected_groups) == 0 and len(selected_tags) == 0 any_cls = "bg-stone-900 text-white border-stone-900" if is_any else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50" - li_parts = [sexp_call("blog-filter-any-topic", cls=any_cls, hx_select=hx_select)] + li_parts = [sx_call("blog-filter-any-topic", cls=any_cls, hx_select=hx_select)] for group in tag_groups: g_slug = getattr(group, "slug", "") if hasattr(group, "slug") else group.get("slug", "") @@ -570,22 +570,22 @@ def _tag_groups_filter_sexp(ctx: dict) -> str: cls = "bg-stone-900 text-white border-stone-900" if is_on else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50" if g_fi: - icon = sexp_call("blog-filter-group-icon-image", src=g_fi, name=g_name) + icon = sx_call("blog-filter-group-icon-image", src=g_fi, name=g_name) else: style = f"background-color: {g_colour}; color: white;" if g_colour else "background-color: #e7e5e4; color: #57534e;" - icon = sexp_call("blog-filter-group-icon-color", style=style, initial=g_name[:1]) + icon = sx_call("blog-filter-group-icon-color", style=style, initial=g_name[:1]) - li_parts.append(sexp_call("blog-filter-group-li", + li_parts.append(sx_call("blog-filter-group-li", cls=cls, hx_get=f"?group={g_slug}&page=1", hx_select=hx_select, - icon=SexpExpr(icon), name=g_name, count=str(g_count), + icon=SxExpr(icon), name=g_name, count=str(g_count), )) items = "(<> " + " ".join(li_parts) + ")" - return sexp_call("blog-filter-nav", items=SexpExpr(items)) + return sx_call("blog-filter-nav", items=SxExpr(items)) -def _authors_filter_sexp(ctx: dict) -> str: - """Author filter bar as sexp.""" +def _authors_filter_sx(ctx: dict) -> str: + """Author filter bar as sx.""" authors = ctx.get("authors") or [] selected_authors = ctx.get("selected_authors") or () hx_select = ctx.get("hx_select_search", "#main-panel") @@ -593,7 +593,7 @@ def _authors_filter_sexp(ctx: dict) -> str: is_any = len(selected_authors) == 0 any_cls = "bg-stone-900 text-white border-stone-900" if is_any else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50" - li_parts = [sexp_call("blog-filter-any-author", cls=any_cls, hx_select=hx_select)] + li_parts = [sx_call("blog-filter-any-author", cls=any_cls, hx_select=hx_select)] for author in authors: a_slug = getattr(author, "slug", "") if hasattr(author, "slug") else author.get("slug", "") @@ -604,21 +604,21 @@ def _authors_filter_sexp(ctx: dict) -> str: is_on = a_slug in selected_authors cls = "bg-stone-900 text-white border-stone-900" if is_on else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50" - icon_sexp = None + icon_sx = None if a_img: - icon_sexp = sexp_call("blog-filter-author-icon", src=a_img, name=a_name) + icon_sx = sx_call("blog-filter-author-icon", src=a_img, name=a_name) - li_parts.append(sexp_call("blog-filter-author-li", + li_parts.append(sx_call("blog-filter-author-li", cls=cls, hx_get=f"?author={a_slug}&page=1", hx_select=hx_select, - icon=SexpExpr(icon_sexp) if icon_sexp else None, name=a_name, count=str(a_count), + icon=SxExpr(icon_sx) if icon_sx else None, name=a_name, count=str(a_count), )) items = "(<> " + " ".join(li_parts) + ")" - return sexp_call("blog-filter-nav", items=SexpExpr(items)) + return sx_call("blog-filter-nav", items=SxExpr(items)) -def _tag_groups_filter_summary_sexp(ctx: dict) -> str: - """Mobile filter summary for tag groups (sexp).""" +def _tag_groups_filter_summary_sx(ctx: dict) -> str: + """Mobile filter summary for tag groups (sx).""" selected_groups = ctx.get("selected_groups") or () tag_groups = ctx.get("tag_groups") or [] if not selected_groups: @@ -631,12 +631,12 @@ def _tag_groups_filter_summary_sexp(ctx: dict) -> str: names.append(g_name) if not names: return "" - return sexp_call("blog-filter-summary", text=", ".join(names)) + return sx_call("blog-filter-summary", text=", ".join(names)) -def _authors_filter_summary_sexp(ctx: dict) -> str: - """Mobile filter summary for authors (sexp).""" +def _authors_filter_summary_sx(ctx: dict) -> str: + """Mobile filter summary for authors (sx).""" selected_authors = ctx.get("selected_authors") or () authors = ctx.get("authors") or [] if not selected_authors: @@ -649,7 +649,7 @@ def _authors_filter_summary_sexp(ctx: dict) -> str: names.append(a_name) if not names: return "" - return sexp_call("blog-filter-summary", text=", ".join(names)) + return sx_call("blog-filter-summary", text=", ".join(names)) @@ -657,7 +657,7 @@ def _authors_filter_summary_sexp(ctx: dict) -> str: # Post detail main panel # --------------------------------------------------------------------------- -def _post_main_panel_sexp(ctx: dict) -> str: +def _post_main_panel_sx(ctx: dict) -> str: """Post/page article content.""" from quart import g, url_for as qurl @@ -669,57 +669,57 @@ def _post_main_panel_sexp(ctx: dict) -> str: hx_select = ctx.get("hx_select_search", "#main-panel") # Draft indicator - draft_sexp = "" + draft_sx = "" if post.get("status") == "draft": - edit_sexp = "" + edit_sx = "" if is_admin or (user and post.get("user_id") == getattr(user, "id", None)): edit_href = qurl("blog.post.admin.edit", slug=slug) - edit_sexp = sexp_call("blog-detail-edit-link", + edit_sx = sx_call("blog-detail-edit-link", href=edit_href, hx_select=hx_select, ) - draft_sexp = sexp_call("blog-detail-draft", + draft_sx = sx_call("blog-detail-draft", publish_requested=post.get("publish_requested"), - edit=SexpExpr(edit_sexp) if edit_sexp else None, + edit=SxExpr(edit_sx) if edit_sx else None, ) # Blog post chrome (not for pages) - chrome_sexp = "" + chrome_sx = "" if not post.get("is_page"): - like_sexp = "" + like_sx = "" if user: liked = post.get("is_liked", False) like_url = call_url(ctx, "blog_url", f"/{slug}/like/toggle/") - like_sexp = sexp_call("blog-detail-like", + like_sx = sx_call("blog-detail-like", like_url=like_url, hx_headers=f'{{"X-CSRFToken": "{ctx.get("csrf_token", "")}"}}', heart="\u2764\ufe0f" if liked else "\U0001f90d", ) - excerpt_sexp = "" + excerpt_sx = "" if post.get("custom_excerpt"): - excerpt_sexp = sexp_call("blog-detail-excerpt", + excerpt_sx = sx_call("blog-detail-excerpt", excerpt=post["custom_excerpt"], ) - at_bar = _at_bar_sexp(post, ctx) - chrome_sexp = sexp_call("blog-detail-chrome", - like=SexpExpr(like_sexp) if like_sexp else None, - excerpt=SexpExpr(excerpt_sexp) if excerpt_sexp else None, - at_bar=SexpExpr(at_bar) if at_bar else None, + at_bar = _at_bar_sx(post, ctx) + chrome_sx = sx_call("blog-detail-chrome", + like=SxExpr(like_sx) if like_sx else None, + excerpt=SxExpr(excerpt_sx) if excerpt_sx else None, + at_bar=SxExpr(at_bar) if at_bar else None, ) fi = post.get("feature_image") html_content = post.get("html", "") - return sexp_call("blog-detail-main", - draft=SexpExpr(draft_sexp) if draft_sexp else None, - chrome=SexpExpr(chrome_sexp) if chrome_sexp else None, + return sx_call("blog-detail-main", + draft=SxExpr(draft_sx) if draft_sx else None, + chrome=SxExpr(chrome_sx) if chrome_sx else None, feature_image=fi, html_content=html_content, ) -def _post_meta_sexp(ctx: dict) -> str: - """Post SEO meta tags as sexp (auto-hoisted to by sexp.js).""" +def _post_meta_sx(ctx: dict) -> str: + """Post SEO meta tags as sx (auto-hoisted to by sx.js).""" post = ctx.get("post") or {} base_title = ctx.get("base_title", "") @@ -750,7 +750,7 @@ def _post_meta_sexp(ctx: dict) -> str: tw_title = post.get("twitter_title") or page_title is_article = not post.get("is_page") - return sexp_call("blog-meta", + return sx_call("blog-meta", robots=robots, page_title=page_title, desc=desc, canonical=canonical, og_type="article" if is_article else "website", og_title=og_title, image=image, @@ -763,47 +763,47 @@ def _post_meta_sexp(ctx: dict) -> str: # Home page (Ghost "home" page) # --------------------------------------------------------------------------- -def _home_main_panel_sexp(ctx: dict) -> str: +def _home_main_panel_sx(ctx: dict) -> str: """Home page content — renders the Ghost page HTML.""" post = ctx.get("post") or {} html = post.get("html", "") - return sexp_call("blog-home-main", html_content=html) + return sx_call("blog-home-main", html_content=html) # --------------------------------------------------------------------------- # Post admin - empty main panel # --------------------------------------------------------------------------- -def _post_admin_main_panel_sexp(ctx: dict) -> str: - return sexp_call("blog-admin-empty") +def _post_admin_main_panel_sx(ctx: dict) -> str: + return sx_call("blog-admin-empty") # --------------------------------------------------------------------------- # Settings main panels # --------------------------------------------------------------------------- -def _settings_main_panel_sexp(ctx: dict) -> str: - return sexp_call("blog-settings-empty") +def _settings_main_panel_sx(ctx: dict) -> str: + return sx_call("blog-settings-empty") -def _cache_main_panel_sexp(ctx: dict) -> str: +def _cache_main_panel_sx(ctx: dict) -> str: from quart import url_for as qurl csrf = ctx.get("csrf_token", "") clear_url = qurl("settings.cache_clear") - return sexp_call("blog-cache-panel", clear_url=clear_url, csrf=csrf) + return sx_call("blog-cache-panel", clear_url=clear_url, csrf=csrf) # --------------------------------------------------------------------------- # Snippets main panel # --------------------------------------------------------------------------- -def _snippets_main_panel_sexp(ctx: dict) -> str: - sl = _snippets_list_sexp(ctx) - return sexp_call("blog-snippets-panel", list=SexpExpr(sl)) +def _snippets_main_panel_sx(ctx: dict) -> str: + sl = _snippets_list_sx(ctx) + return sx_call("blog-snippets-panel", list=SxExpr(sl)) -def _snippets_list_sexp(ctx: dict) -> str: +def _snippets_list_sx(ctx: dict) -> str: """Snippets list with visibility badges and delete buttons.""" from quart import url_for as qurl, g @@ -814,7 +814,7 @@ def _snippets_list_sexp(ctx: dict) -> str: user_id = getattr(user, "id", None) if not snippets: - return sexp_call("blog-snippets-empty") + return sx_call("blog-snippets-empty") badge_colours = { "private": "bg-stone-200 text-stone-700", @@ -837,53 +837,53 @@ def _snippets_list_sexp(ctx: dict) -> str: patch_url = qurl("snippets.patch_visibility", snippet_id=s_id) opts = "" for v in ["private", "shared", "admin"]: - opts += sexp_call("blog-snippet-option", + opts += sx_call("blog-snippet-option", value=v, selected=(s_vis == v), label=v, ) - extra += sexp_call("blog-snippet-visibility-select", + extra += sx_call("blog-snippet-visibility-select", patch_url=patch_url, hx_headers=f'{{"X-CSRFToken": "{csrf}"}}', - options=SexpExpr("(<> " + opts + ")") if opts else None, + options=SxExpr("(<> " + opts + ")") if opts else None, cls="text-sm border border-stone-300 rounded px-2 py-1", ) if s_uid == user_id or is_admin: del_url = qurl("snippets.delete_snippet", snippet_id=s_id) - extra += sexp_call("blog-snippet-delete-button", + extra += sx_call("blog-snippet-delete-button", confirm_text=f'Delete \u201c{s_name}\u201d?', delete_url=del_url, hx_headers=f'{{"X-CSRFToken": "{csrf}"}}', ) - row_parts.append(sexp_call("blog-snippet-row", + row_parts.append(sx_call("blog-snippet-row", name=s_name, owner=owner, badge_cls=badge_cls, - visibility=s_vis, extra=SexpExpr("(<> " + extra + ")") if extra else None, + visibility=s_vis, extra=SxExpr("(<> " + extra + ")") if extra else None, )) rows = "(<> " + " ".join(row_parts) + ")" - return sexp_call("blog-snippets-list", rows=SexpExpr(rows)) + return sx_call("blog-snippets-list", rows=SxExpr(rows)) # --------------------------------------------------------------------------- # Menu items main panel # --------------------------------------------------------------------------- -def _menu_items_main_panel_sexp(ctx: dict) -> str: +def _menu_items_main_panel_sx(ctx: dict) -> str: from quart import url_for as qurl new_url = qurl("menu_items.new_menu_item") - ml = _menu_items_list_sexp(ctx) - return sexp_call("blog-menu-items-panel", new_url=new_url, list=SexpExpr(ml)) + ml = _menu_items_list_sx(ctx) + return sx_call("blog-menu-items-panel", new_url=new_url, list=SxExpr(ml)) -def _menu_items_list_sexp(ctx: dict) -> str: +def _menu_items_list_sx(ctx: dict) -> str: from quart import url_for as qurl menu_items = ctx.get("menu_items") or [] csrf = ctx.get("csrf_token", "") if not menu_items: - return sexp_call("blog-menu-items-empty") + return sx_call("blog-menu-items-empty") row_parts = [] for item in menu_items: @@ -896,24 +896,24 @@ def _menu_items_list_sexp(ctx: dict) -> str: edit_url = qurl("menu_items.edit_menu_item", item_id=i_id) del_url = qurl("menu_items.delete_menu_item_route", item_id=i_id) - img_sexp = sexp_call("blog-menu-item-image", src=fi, label=label) + img_sx = sx_call("blog-menu-item-image", src=fi, label=label) - row_parts.append(sexp_call("blog-menu-item-row", - img=SexpExpr(img_sexp), label=label, slug=slug, + row_parts.append(sx_call("blog-menu-item-row", + img=SxExpr(img_sx), label=label, slug=slug, sort_order=str(sort), edit_url=edit_url, delete_url=del_url, confirm_text=f"Remove {label} from the menu?", hx_headers=f'{{"X-CSRFToken": "{csrf}"}}', )) rows = "(<> " + " ".join(row_parts) + ")" - return sexp_call("blog-menu-items-list", rows=SexpExpr(rows)) + return sx_call("blog-menu-items-list", rows=SxExpr(rows)) # --------------------------------------------------------------------------- # Tag groups main panel # --------------------------------------------------------------------------- -def _tag_groups_main_panel_sexp(ctx: dict) -> str: +def _tag_groups_main_panel_sx(ctx: dict) -> str: from quart import url_for as qurl groups = ctx.get("groups") or [] @@ -921,7 +921,7 @@ def _tag_groups_main_panel_sexp(ctx: dict) -> str: csrf = ctx.get("csrf_token", "") create_url = qurl("blog.tag_groups_admin.create") - form_sexp = sexp_call("blog-tag-groups-create-form", + form_sx = sx_call("blog-tag-groups-create-form", create_url=create_url, csrf=csrf, ) @@ -940,39 +940,39 @@ def _tag_groups_main_panel_sexp(ctx: dict) -> str: edit_href = qurl("blog.tag_groups_admin.edit", id=g_id) if g_fi: - icon = sexp_call("blog-tag-group-icon-image", src=g_fi, name=g_name) + icon = sx_call("blog-tag-group-icon-image", src=g_fi, name=g_name) else: style = f"background-color: {g_colour}; color: white;" if g_colour else "background-color: #e7e5e4; color: #57534e;" - icon = sexp_call("blog-tag-group-icon-color", style=style, initial=g_name[:1]) + icon = sx_call("blog-tag-group-icon-color", style=style, initial=g_name[:1]) - li_parts.append(sexp_call("blog-tag-group-li", - icon=SexpExpr(icon), edit_href=edit_href, name=g_name, + li_parts.append(sx_call("blog-tag-group-li", + icon=SxExpr(icon), edit_href=edit_href, name=g_name, slug=g_slug, sort_order=str(g_sort), )) - groups_sexp = sexp_call("blog-tag-groups-list", items=SexpExpr("(<> " + " ".join(li_parts) + ")")) + groups_sx = sx_call("blog-tag-groups-list", items=SxExpr("(<> " + " ".join(li_parts) + ")")) else: - groups_sexp = sexp_call("blog-tag-groups-empty") + groups_sx = sx_call("blog-tag-groups-empty") # Unassigned tags - unassigned_sexp = "" + unassigned_sx = "" if unassigned_tags: tag_spans = [] for tag in unassigned_tags: t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "") - tag_spans.append(sexp_call("blog-unassigned-tag", name=t_name)) - unassigned_sexp = sexp_call("blog-unassigned-tags", + tag_spans.append(sx_call("blog-unassigned-tag", name=t_name)) + unassigned_sx = sx_call("blog-unassigned-tags", heading=f"Unassigned Tags ({len(unassigned_tags)})", - spans=SexpExpr("(<> " + " ".join(tag_spans) + ")"), + spans=SxExpr("(<> " + " ".join(tag_spans) + ")"), ) - return sexp_call("blog-tag-groups-main", - form=SexpExpr(form_sexp), - groups=SexpExpr(groups_sexp), - unassigned=SexpExpr(unassigned_sexp) if unassigned_sexp else None, + return sx_call("blog-tag-groups-main", + form=SxExpr(form_sx), + groups=SxExpr(groups_sx), + unassigned=SxExpr(unassigned_sx) if unassigned_sx else None, ) -def _tag_groups_edit_main_panel_sexp(ctx: dict) -> str: +def _tag_groups_edit_main_panel_sx(ctx: dict) -> str: from quart import url_for as qurl group = ctx.get("group") @@ -996,25 +996,25 @@ def _tag_groups_edit_main_panel_sexp(ctx: dict) -> str: t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "") t_fi = getattr(tag, "feature_image", None) if hasattr(tag, "feature_image") else tag.get("feature_image") checked = t_id in assigned_tag_ids - img = sexp_call("blog-tag-checkbox-image", src=t_fi) if t_fi else "" - tag_items.append(sexp_call("blog-tag-checkbox", + img = sx_call("blog-tag-checkbox-image", src=t_fi) if t_fi else "" + tag_items.append(sx_call("blog-tag-checkbox", tag_id=str(t_id), checked=checked, - img=SexpExpr(img) if img else None, name=t_name, + img=SxExpr(img) if img else None, name=t_name, )) - edit_form = sexp_call("blog-tag-group-edit-form", + edit_form = sx_call("blog-tag-group-edit-form", save_url=save_url, csrf=csrf, name=g_name, colour=g_colour or "", sort_order=str(g_sort), feature_image=g_fi or "", - tags=SexpExpr("(<> " + " ".join(tag_items) + ")"), + tags=SxExpr("(<> " + " ".join(tag_items) + ")"), ) - del_form = sexp_call("blog-tag-group-delete-form", + del_form = sx_call("blog-tag-group-delete-form", delete_url=del_url, csrf=csrf, ) - return sexp_call("blog-tag-group-edit-main", - edit_form=SexpExpr(edit_form), delete_form=SexpExpr(del_form), + return sx_call("blog-tag-group-edit-main", + edit_form=SxExpr(edit_form), delete_form=SxExpr(del_form), ) @@ -1034,59 +1034,59 @@ def _tag_groups_edit_main_panel_sexp(ctx: dict) -> str: # ---- Home page ---- async def render_home_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + post_hdr + ")" - content = _home_main_panel_sexp(ctx) - meta = _post_meta_sexp(ctx) - menu = ctx.get("nav_sexp", "") or "" - return full_page_sexp(ctx, header_rows=header_rows, content=content, + content = _home_main_panel_sx(ctx) + meta = _post_meta_sx(ctx) + menu = ctx.get("nav_sx", "") or "" + return full_page_sx(ctx, header_rows=header_rows, content=content, meta=meta, menu=menu) async def render_home_oob(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx, oob=True) - post_oob = _oob_header_sexp("root-header-child", "post-header-child", - _post_header_sexp(ctx)) - content = _home_main_panel_sexp(ctx) + root_hdr = root_header_sx(ctx, oob=True) + post_oob = _oob_header_sx("root-header-child", "post-header-child", + _post_header_sx(ctx)) + content = _home_main_panel_sx(ctx) oobs = "(<> " + root_hdr + " " + post_oob + ")" - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # ---- Blog index ---- async def render_blog_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - blog_hdr = _blog_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + blog_hdr = _blog_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + blog_hdr + ")" - content = _blog_main_panel_sexp(ctx) - aside = _blog_aside_sexp(ctx) - filter_sexp = _blog_filter_sexp(ctx) - return full_page_sexp(ctx, header_rows=header_rows, content=content, - aside=aside, filter=filter_sexp) + content = _blog_main_panel_sx(ctx) + aside = _blog_aside_sx(ctx) + filter_sx = _blog_filter_sx(ctx) + return full_page_sx(ctx, header_rows=header_rows, content=content, + aside=aside, filter=filter_sx) async def render_blog_oob(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx, oob=True) - blog_oob = _oob_header_sexp("root-header-child", "blog-header-child", - _blog_header_sexp(ctx)) - content = _blog_main_panel_sexp(ctx) - aside = _blog_aside_sexp(ctx) - filter_sexp = _blog_filter_sexp(ctx) - nav = ctx.get("nav_sexp", "") or "" + root_hdr = root_header_sx(ctx, oob=True) + blog_oob = _oob_header_sx("root-header-child", "blog-header-child", + _blog_header_sx(ctx)) + content = _blog_main_panel_sx(ctx) + aside = _blog_aside_sx(ctx) + filter_sx = _blog_filter_sx(ctx) + nav = ctx.get("nav_sx", "") or "" oobs = "(<> " + root_hdr + " " + blog_oob + ")" - return oob_page_sexp(oobs=oobs, content=content, aside=aside, - filter=filter_sexp, menu=nav) + return oob_page_sx(oobs=oobs, content=content, aside=aside, + filter=filter_sx, menu=nav) async def render_blog_cards(ctx: dict) -> str: - """Pagination-only response (page > 1) — sexp wire format.""" - return _blog_cards_sexp(ctx) + """Pagination-only response (page > 1) — sx wire format.""" + return _blog_cards_sx(ctx) async def render_blog_page_cards(ctx: dict) -> str: """Page cards pagination response.""" - return _page_cards_sexp(ctx) + return _page_cards_sx(ctx) # ---- New post/page editor panel ---- @@ -1121,17 +1121,17 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) -> # Error banner if save_error: - parts.append(sexp_call("blog-editor-error", error=str(save_error))) + parts.append(sx_call("blog-editor-error", error=str(save_error))) # Form structure - form_html = sexp_call("blog-editor-form", + form_html = sx_call("blog-editor-form", csrf=csrf, title_placeholder=title_placeholder, create_label=create_label, ) parts.append(form_html) # Editor CSS + inline styles - parts.append(sexp_call("blog-editor-styles", css_href=editor_css)) + parts.append(sx_call("blog-editor-styles", css_href=editor_css)) # Editor JS + init script init_js = ( @@ -1265,7 +1265,7 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) -> " }\n" "})();\n" ) - parts.append(sexp_call("blog-editor-scripts", js_src=editor_js, init_js=init_js)) + parts.append(sx_call("blog-editor-scripts", js_src=editor_js, init_js=init_js)) return "".join(parts) @@ -1273,303 +1273,303 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) -> # ---- New post/page ---- async def render_new_post_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - blog_hdr = _blog_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + blog_hdr = _blog_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + blog_hdr + ")" content = ctx.get("editor_html", "") - return full_page_sexp(ctx, header_rows=header_rows, content=content) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_new_post_oob(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx, oob=True) - blog_oob = _blog_header_sexp(ctx, oob=True) + root_hdr = root_header_sx(ctx, oob=True) + blog_oob = _blog_header_sx(ctx, oob=True) content = ctx.get("editor_html", "") oobs = "(<> " + root_hdr + " " + blog_oob + ")" - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # ---- Post detail ---- async def render_post_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + post_hdr + ")" - content = _post_main_panel_sexp(ctx) - meta = _post_meta_sexp(ctx) - menu = ctx.get("nav_sexp", "") or "" - return full_page_sexp(ctx, header_rows=header_rows, content=content, + content = _post_main_panel_sx(ctx) + meta = _post_meta_sx(ctx) + menu = ctx.get("nav_sx", "") or "" + return full_page_sx(ctx, header_rows=header_rows, content=content, meta=meta, menu=menu) async def render_post_oob(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx, oob=True) - post_oob = _oob_header_sexp("root-header-child", "post-header-child", - _post_header_sexp(ctx)) - content = _post_main_panel_sexp(ctx) - menu = ctx.get("nav_sexp", "") or "" + root_hdr = root_header_sx(ctx, oob=True) + post_oob = _oob_header_sx("root-header-child", "post-header-child", + _post_header_sx(ctx)) + content = _post_main_panel_sx(ctx) + menu = ctx.get("nav_sx", "") or "" oobs = "(<> " + root_hdr + " " + post_oob + ")" - return oob_page_sexp(oobs=oobs, content=content, menu=menu) + return oob_page_sx(oobs=oobs, content=content, menu=menu) # ---- Post admin ---- async def render_post_admin_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = _post_admin_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = _post_admin_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr - content = _post_admin_main_panel_sexp(ctx) - menu = ctx.get("nav_sexp", "") or "" - return full_page_sexp(ctx, header_rows=header_rows, content=content, + content = _post_admin_main_panel_sx(ctx) + menu = ctx.get("nav_sx", "") or "" + return full_page_sx(ctx, header_rows=header_rows, content=content, menu=menu) async def render_post_admin_oob(ctx: dict) -> str: - post_hdr_oob = _post_header_sexp(ctx, oob=True) - admin_oob = _oob_header_sexp("post-header-child", "post-admin-header-child", - _post_admin_header_sexp(ctx)) - content = _post_admin_main_panel_sexp(ctx) - menu = ctx.get("nav_sexp", "") or "" + post_hdr_oob = _post_header_sx(ctx, oob=True) + admin_oob = _oob_header_sx("post-header-child", "post-admin-header-child", + _post_admin_header_sx(ctx)) + content = _post_admin_main_panel_sx(ctx) + menu = ctx.get("nav_sx", "") or "" oobs = "(<> " + post_hdr_oob + " " + admin_oob + ")" - return oob_page_sexp(oobs=oobs, content=content, menu=menu) + return oob_page_sx(oobs=oobs, content=content, menu=menu) # ---- Post data ---- async def render_post_data_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = _post_admin_header_sexp(ctx, selected="data") + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = _post_admin_header_sx(ctx, selected="data") header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr content = ctx.get("data_html", "") - return full_page_sexp(ctx, header_rows=header_rows, content=content) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_post_data_oob(ctx: dict) -> str: - admin_hdr_oob = _post_admin_header_sexp(ctx, oob=True, selected="data") + admin_hdr_oob = _post_admin_header_sx(ctx, oob=True, selected="data") content = ctx.get("data_html", "") - return oob_page_sexp(oobs=admin_hdr_oob, content=content) + return oob_page_sx(oobs=admin_hdr_oob, content=content) # ---- Post entries ---- async def render_post_entries_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = _post_admin_header_sexp(ctx, selected="entries") + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = _post_admin_header_sx(ctx, selected="entries") header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr content = ctx.get("entries_html", "") - return full_page_sexp(ctx, header_rows=header_rows, content=content) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_post_entries_oob(ctx: dict) -> str: - admin_hdr_oob = _post_admin_header_sexp(ctx, oob=True, selected="entries") + admin_hdr_oob = _post_admin_header_sx(ctx, oob=True, selected="entries") content = ctx.get("entries_html", "") - return oob_page_sexp(oobs=admin_hdr_oob, content=content) + return oob_page_sx(oobs=admin_hdr_oob, content=content) # ---- Post edit ---- async def render_post_edit_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = _post_admin_header_sexp(ctx, selected="edit") + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = _post_admin_header_sx(ctx, selected="edit") header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr content = ctx.get("edit_html", "") body_end = ctx.get("body_end_html", "") - return full_page_sexp(ctx, header_rows=header_rows, content=content) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_post_edit_oob(ctx: dict) -> str: - admin_hdr_oob = _post_admin_header_sexp(ctx, oob=True, selected="edit") + admin_hdr_oob = _post_admin_header_sx(ctx, oob=True, selected="edit") content = ctx.get("edit_html", "") - return oob_page_sexp(oobs=admin_hdr_oob, content=content) + return oob_page_sx(oobs=admin_hdr_oob, content=content) # ---- Post settings ---- async def render_post_settings_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = _post_admin_header_sexp(ctx, selected="settings") + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = _post_admin_header_sx(ctx, selected="settings") header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr content = ctx.get("settings_html", "") - return full_page_sexp(ctx, header_rows=header_rows, content=content) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_post_settings_oob(ctx: dict) -> str: - admin_hdr_oob = _post_admin_header_sexp(ctx, oob=True, selected="settings") + admin_hdr_oob = _post_admin_header_sx(ctx, oob=True, selected="settings") content = ctx.get("settings_html", "") - return oob_page_sexp(oobs=admin_hdr_oob, content=content) + return oob_page_sx(oobs=admin_hdr_oob, content=content) # ---- Settings home ---- async def render_settings_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - settings_hdr = _settings_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + settings_hdr = _settings_header_sx(ctx) header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" - content = _settings_main_panel_sexp(ctx) - menu = _settings_nav_sexp(ctx) - return full_page_sexp(ctx, header_rows=header_rows, content=content, + content = _settings_main_panel_sx(ctx) + menu = _settings_nav_sx(ctx) + return full_page_sx(ctx, header_rows=header_rows, content=content, menu=menu) async def render_settings_oob(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx, oob=True) - settings_oob = _oob_header_sexp("root-header-child", "root-settings-header-child", - _settings_header_sexp(ctx)) - content = _settings_main_panel_sexp(ctx) - menu = _settings_nav_sexp(ctx) + root_hdr = root_header_sx(ctx, oob=True) + settings_oob = _oob_header_sx("root-header-child", "root-settings-header-child", + _settings_header_sx(ctx)) + content = _settings_main_panel_sx(ctx) + menu = _settings_nav_sx(ctx) oobs = "(<> " + root_hdr + " " + settings_oob + ")" - return oob_page_sexp(oobs=oobs, content=content, menu=menu) + return oob_page_sx(oobs=oobs, content=content, menu=menu) # ---- Cache ---- async def render_cache_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - settings_hdr = _settings_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + settings_hdr = _settings_header_sx(ctx) from quart import url_for as qurl - cache_hdr = _sub_settings_header_sexp( + cache_hdr = _sub_settings_header_sx( "cache-row", "cache-header-child", qurl("settings.cache"), "refresh", "Cache", ctx, ) header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + cache_hdr - content = _cache_main_panel_sexp(ctx) - return full_page_sexp(ctx, header_rows=header_rows, content=content) + content = _cache_main_panel_sx(ctx) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_cache_oob(ctx: dict) -> str: - settings_hdr_oob = _settings_header_sexp(ctx, oob=True) + settings_hdr_oob = _settings_header_sx(ctx, oob=True) from quart import url_for as qurl - cache_hdr = _sub_settings_header_sexp( + cache_hdr = _sub_settings_header_sx( "cache-row", "cache-header-child", qurl("settings.cache"), "refresh", "Cache", ctx, ) - cache_oob = _oob_header_sexp("root-settings-header-child", "cache-header-child", + cache_oob = _oob_header_sx("root-settings-header-child", "cache-header-child", cache_hdr) - content = _cache_main_panel_sexp(ctx) + content = _cache_main_panel_sx(ctx) oobs = "(<> " + settings_hdr_oob + " " + cache_oob + ")" - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # ---- Snippets ---- async def render_snippets_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - settings_hdr = _settings_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + settings_hdr = _settings_header_sx(ctx) from quart import url_for as qurl - snippets_hdr = _sub_settings_header_sexp( + snippets_hdr = _sub_settings_header_sx( "snippets-row", "snippets-header-child", qurl("snippets.list_snippets"), "puzzle-piece", "Snippets", ctx, ) header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + snippets_hdr - content = _snippets_main_panel_sexp(ctx) - return full_page_sexp(ctx, header_rows=header_rows, content=content) + content = _snippets_main_panel_sx(ctx) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_snippets_oob(ctx: dict) -> str: - settings_hdr_oob = _settings_header_sexp(ctx, oob=True) + settings_hdr_oob = _settings_header_sx(ctx, oob=True) from quart import url_for as qurl - snippets_hdr = _sub_settings_header_sexp( + snippets_hdr = _sub_settings_header_sx( "snippets-row", "snippets-header-child", qurl("snippets.list_snippets"), "puzzle-piece", "Snippets", ctx, ) - snippets_oob = _oob_header_sexp("root-settings-header-child", "snippets-header-child", + snippets_oob = _oob_header_sx("root-settings-header-child", "snippets-header-child", snippets_hdr) - content = _snippets_main_panel_sexp(ctx) + content = _snippets_main_panel_sx(ctx) oobs = "(<> " + settings_hdr_oob + " " + snippets_oob + ")" - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # ---- Menu items ---- async def render_menu_items_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - settings_hdr = _settings_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + settings_hdr = _settings_header_sx(ctx) from quart import url_for as qurl - mi_hdr = _sub_settings_header_sexp( + mi_hdr = _sub_settings_header_sx( "menu_items-row", "menu_items-header-child", qurl("menu_items.list_menu_items"), "bars", "Menu Items", ctx, ) header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + mi_hdr - content = _menu_items_main_panel_sexp(ctx) - return full_page_sexp(ctx, header_rows=header_rows, content=content) + content = _menu_items_main_panel_sx(ctx) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_menu_items_oob(ctx: dict) -> str: - settings_hdr_oob = _settings_header_sexp(ctx, oob=True) + settings_hdr_oob = _settings_header_sx(ctx, oob=True) from quart import url_for as qurl - mi_hdr = _sub_settings_header_sexp( + mi_hdr = _sub_settings_header_sx( "menu_items-row", "menu_items-header-child", qurl("menu_items.list_menu_items"), "bars", "Menu Items", ctx, ) - mi_oob = _oob_header_sexp("root-settings-header-child", "menu_items-header-child", + mi_oob = _oob_header_sx("root-settings-header-child", "menu_items-header-child", mi_hdr) - content = _menu_items_main_panel_sexp(ctx) + content = _menu_items_main_panel_sx(ctx) oobs = "(<> " + settings_hdr_oob + " " + mi_oob + ")" - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # ---- Tag groups ---- async def render_tag_groups_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - settings_hdr = _settings_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + settings_hdr = _settings_header_sx(ctx) from quart import url_for as qurl - tg_hdr = _sub_settings_header_sexp( + tg_hdr = _sub_settings_header_sx( "tag-groups-row", "tag-groups-header-child", qurl("blog.tag_groups_admin.index"), "tags", "Tag Groups", ctx, ) header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + tg_hdr - content = _tag_groups_main_panel_sexp(ctx) - return full_page_sexp(ctx, header_rows=header_rows, content=content) + content = _tag_groups_main_panel_sx(ctx) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_tag_groups_oob(ctx: dict) -> str: - settings_hdr_oob = _settings_header_sexp(ctx, oob=True) + settings_hdr_oob = _settings_header_sx(ctx, oob=True) from quart import url_for as qurl - tg_hdr = _sub_settings_header_sexp( + tg_hdr = _sub_settings_header_sx( "tag-groups-row", "tag-groups-header-child", qurl("blog.tag_groups_admin.index"), "tags", "Tag Groups", ctx, ) - tg_oob = _oob_header_sexp("root-settings-header-child", "tag-groups-header-child", + tg_oob = _oob_header_sx("root-settings-header-child", "tag-groups-header-child", tg_hdr) - content = _tag_groups_main_panel_sexp(ctx) + content = _tag_groups_main_panel_sx(ctx) oobs = "(<> " + settings_hdr_oob + " " + tg_oob + ")" - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # ---- Tag group edit ---- async def render_tag_group_edit_page(ctx: dict) -> str: - root_hdr = root_header_sexp(ctx) - settings_hdr = _settings_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + settings_hdr = _settings_header_sx(ctx) from quart import url_for as qurl g_id = (ctx.get("group") or {}).get("id") or getattr(ctx.get("group"), "id", None) - tg_hdr = _sub_settings_header_sexp( + tg_hdr = _sub_settings_header_sx( "tag-groups-row", "tag-groups-header-child", qurl("blog.tag_groups_admin.edit", id=g_id), "tags", "Tag Groups", ctx, ) header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + tg_hdr - content = _tag_groups_edit_main_panel_sexp(ctx) - return full_page_sexp(ctx, header_rows=header_rows, content=content) + content = _tag_groups_edit_main_panel_sx(ctx) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_tag_group_edit_oob(ctx: dict) -> str: - settings_hdr_oob = _settings_header_sexp(ctx, oob=True) + settings_hdr_oob = _settings_header_sx(ctx, oob=True) from quart import url_for as qurl g_id = (ctx.get("group") or {}).get("id") or getattr(ctx.get("group"), "id", None) - tg_hdr = _sub_settings_header_sexp( + tg_hdr = _sub_settings_header_sx( "tag-groups-row", "tag-groups-header-child", qurl("blog.tag_groups_admin.edit", id=g_id), "tags", "Tag Groups", ctx, ) - tg_oob = _oob_header_sexp("root-settings-header-child", "tag-groups-header-child", + tg_oob = _oob_header_sx("root-settings-header-child", "tag-groups-header-child", tg_hdr) - content = _tag_groups_edit_main_panel_sexp(ctx) + content = _tag_groups_edit_main_panel_sx(ctx) oobs = "(<> " + settings_hdr_oob + " " + tg_oob + ")" - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # =========================================================================== @@ -1580,7 +1580,7 @@ async def render_tag_group_edit_oob(ctx: dict) -> str: def render_like_toggle_button(slug: str, liked: bool, like_url: str) -> str: """Render a like toggle button for HTMX POST response.""" - from market.sexp.sexp_components import render_like_toggle_button as _market_like + from market.sx.sx_components import render_like_toggle_button as _market_like return _market_like(slug, liked, like_url=like_url, item_type="post") @@ -1596,7 +1596,7 @@ def render_snippets_list(snippets, is_admin: bool) -> str: "is_admin": is_admin, "csrf_token": generate_csrf_token(), } - return _snippets_list_sexp(ctx) + return _snippets_list_sx(ctx) # ---- Menu items list + nav OOB ---- @@ -1609,7 +1609,7 @@ def render_menu_items_list(menu_items) -> str: "menu_items": menu_items, "csrf_token": generate_csrf_token(), } - return _menu_items_list_sexp(ctx) + return _menu_items_list_sx(ctx) def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str: @@ -1622,7 +1622,7 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str: from quart import request as qrequest if not menu_items: - return sexp_call("blog-nav-empty", wrapper_id="menu-items-nav-wrapper") + return sx_call("blog-nav-empty", wrapper_id="menu-items-nav-wrapper") # Resolve URL helpers from context or fall back to template globals if ctx is None: @@ -1670,27 +1670,27 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str: selected = "true" if (item_slug == first_seg or item_slug == app_name) else "false" - img_sexp = sexp_call("blog-nav-item-image", src=fi, label=label) + img_sx = sx_call("blog-nav-item-image", src=fi, label=label) if item_slug != "cart": - item_parts.append(sexp_call("blog-nav-item-link", + item_parts.append(sx_call("blog-nav-item-link", href=href, hx_get=f"/{item_slug}/", selected=selected, - nav_cls=nav_button_cls, img=SexpExpr(img_sexp), label=label, + nav_cls=nav_button_cls, img=SxExpr(img_sx), label=label, )) else: - item_parts.append(sexp_call("blog-nav-item-plain", + item_parts.append(sx_call("blog-nav-item-plain", href=href, selected=selected, nav_cls=nav_button_cls, - img=SexpExpr(img_sexp), label=label, + img=SxExpr(img_sx), label=label, )) - items_sexp = "(<> " + " ".join(item_parts) + ")" if item_parts else "" + items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "" - return sexp_call("blog-nav-wrapper", + return sx_call("blog-nav-wrapper", arrow_cls=arrow_cls, container_id=container_id, left_hs=f"on click set #{container_id}.scrollLeft to #{container_id}.scrollLeft - 200", scroll_hs=scroll_hs, right_hs=f"on click set #{container_id}.scrollLeft to #{container_id}.scrollLeft + 200", - items=SexpExpr(items_sexp) if items_sexp else None, + items=SxExpr(items_sx) if items_sx else None, ) @@ -1710,30 +1710,30 @@ def render_features_panel(features: dict, post: dict, hs_trigger = "on change trigger submit on closest
" - form_sexp = sexp_call("blog-features-form", + form_sx = sx_call("blog-features-form", features_url=features_url, calendar_checked=bool(features.get("calendar")), market_checked=bool(features.get("market")), hs_trigger=hs_trigger, ) - sumup_sexp = "" + sumup_sx = "" if features.get("calendar") or features.get("market"): placeholder = "\u2022" * 8 if sumup_configured else "sup_sk_..." - connected = sexp_call("blog-sumup-connected") if sumup_configured else "" - key_hint = sexp_call("blog-sumup-key-hint") if sumup_configured else "" + connected = sx_call("blog-sumup-connected") if sumup_configured else "" + key_hint = sx_call("blog-sumup-key-hint") if sumup_configured else "" - sumup_sexp = sexp_call("blog-sumup-form", + sumup_sx = sx_call("blog-sumup-form", sumup_url=sumup_url, merchant_code=sumup_merchant_code, placeholder=placeholder, - key_hint=SexpExpr(key_hint) if key_hint else None, + key_hint=SxExpr(key_hint) if key_hint else None, checkout_prefix=sumup_checkout_prefix, - connected=SexpExpr(connected) if connected else None, + connected=SxExpr(connected) if connected else None, ) - return sexp_call("blog-features-panel", - form=SexpExpr(form_sexp), - sumup=SexpExpr(sumup_sexp) if sumup_sexp else None, + return sx_call("blog-features-panel", + form=SxExpr(form_sx), + sumup=SxExpr(sumup_sx) if sumup_sx else None, ) @@ -1747,23 +1747,23 @@ def render_markets_panel(markets, post: dict) -> str: slug = post.get("slug", "") create_url = host_url(qurl("blog.post.admin.create_market", slug=slug)) - list_sexp = "" + list_sx = "" if markets: li_parts = [] for m in markets: m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "") m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "") del_url = host_url(qurl("blog.post.admin.delete_market", slug=slug, market_slug=m_slug)) - li_parts.append(sexp_call("blog-market-item", + li_parts.append(sx_call("blog-market-item", name=m_name, slug=m_slug, delete_url=del_url, confirm_text=f"Delete market '{m_name}'?", )) - list_sexp = sexp_call("blog-markets-list", items=SexpExpr("(<> " + " ".join(li_parts) + ")")) + list_sx = sx_call("blog-markets-list", items=SxExpr("(<> " + " ".join(li_parts) + ")")) else: - list_sexp = sexp_call("blog-markets-empty") + list_sx = sx_call("blog-markets-empty") - return sexp_call("blog-markets-panel", - list=SexpExpr(list_sexp), create_url=create_url, + return sx_call("blog-markets-panel", + list=SxExpr(list_sx), create_url=create_url, ) @@ -1799,28 +1799,28 @@ def render_associated_entries(all_calendars, associated_entry_ids, post_slug: st toggle_url = host_url(qurl("blog.post.admin.toggle_entry", slug=post_slug, entry_id=e_id)) - img_sexp = sexp_call("blog-entry-image", src=cal_fi, title=cal_title) + img_sx = sx_call("blog-entry-image", src=cal_fi, title=cal_title) date_str = e_start.strftime("%A, %B %d, %Y at %H:%M") if e_start else "" if e_end: date_str += f" \u2013 {e_end.strftime('%H:%M')}" - entry_items.append(sexp_call("blog-associated-entry", + entry_items.append(sx_call("blog-associated-entry", confirm_text=f"This will remove {e_name} from this post", toggle_url=toggle_url, hx_headers=f'{{"X-CSRFToken": "{csrf}"}}', - img=SexpExpr(img_sexp), name=e_name, + img=SxExpr(img_sx), name=e_name, date_str=f"{cal_name} \u2022 {date_str}", )) if has_entries: - content_sexp = sexp_call("blog-associated-entries-content", - items=SexpExpr("(<> " + " ".join(entry_items) + ")"), + content_sx = sx_call("blog-associated-entries-content", + items=SxExpr("(<> " + " ".join(entry_items) + ")"), ) else: - content_sexp = sexp_call("blog-associated-entries-empty") + content_sx = sx_call("blog-associated-entries-empty") - return sexp_call("blog-associated-entries-panel", content=SexpExpr(content_sexp)) + return sx_call("blog-associated-entries-panel", content=SxExpr(content_sx)) # ---- Nav entries OOB ---- @@ -1841,7 +1841,7 @@ def render_nav_entries_oob(associated_entries, calendars, post: dict, ctx: dict has_items = bool(entries_list or calendars) if not has_items: - return sexp_call("blog-nav-entries-empty") + return sx_call("blog-nav-entries-empty") events_url_fn = ctx.get("events_url") @@ -1889,7 +1889,7 @@ def render_nav_entries_oob(associated_entries, calendars, post: dict, ctx: dict href = events_url_fn(entry_path) if events_url_fn else entry_path - item_parts.append(sexp_call("blog-nav-entry-item", + item_parts.append(sx_call("blog-nav-entry-item", href=href, nav_cls=nav_cls, name=e_name, date_str=date_str, )) @@ -1900,12 +1900,12 @@ def render_nav_entries_oob(associated_entries, calendars, post: dict, ctx: dict cal_path = f"/{post_slug}/{cal_slug}/" href = events_url_fn(cal_path) if events_url_fn else cal_path - item_parts.append(sexp_call("blog-nav-calendar-item", + item_parts.append(sx_call("blog-nav-calendar-item", href=href, nav_cls=nav_cls, name=cal_name, )) - items_sexp = "(<> " + " ".join(item_parts) + ")" if item_parts else "" + items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "" - return sexp_call("blog-nav-entries-wrapper", - scroll_hs=scroll_hs, items=SexpExpr(items_sexp) if items_sexp else None, + return sx_call("blog-nav-entries-wrapper", + scroll_hs=scroll_hs, items=SxExpr(items_sx) if items_sx else None, ) diff --git a/cart/app.py b/cart/app.py index 79cdc0d..8c45fb0 100644 --- a/cart/app.py +++ b/cart/app.py @@ -1,6 +1,6 @@ from __future__ import annotations import path_setup # noqa: F401 # adds shared/ to sys.path -import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file +import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file from decimal import Decimal from pathlib import Path diff --git a/cart/bp/cart/global_routes.py b/cart/bp/cart/global_routes.py index 9ec0d7b..a1590ce 100644 --- a/cart/bp/cart/global_routes.py +++ b/cart/bp/cart/global_routes.py @@ -150,8 +150,8 @@ def register(url_prefix: str) -> Blueprint: try: page_config = await resolve_page_config(g.s, cart, calendar_entries, tickets) except ValueError as e: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_checkout_error_page + from shared.sx.page import get_template_context + from sx.sx_components import render_checkout_error_page tctx = await get_template_context() html = await render_checkout_error_page(tctx, error=str(e)) return await make_response(html, 400) @@ -207,8 +207,8 @@ def register(url_prefix: str) -> Blueprint: hosted_url = result.get("sumup_hosted_url") if not hosted_url: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_checkout_error_page + from shared.sx.page import get_template_context + from sx.sx_components import render_checkout_error_page tctx = await get_template_context() html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp.") return await make_response(html, 500) diff --git a/cart/bp/cart/overview_routes.py b/cart/bp/cart/overview_routes.py index ef74e8b..59e6977 100644 --- a/cart/bp/cart/overview_routes.py +++ b/cart/bp/cart/overview_routes.py @@ -5,7 +5,7 @@ from __future__ import annotations from quart import Blueprint, render_template, make_response from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from .services import get_cart_grouped_by_page @@ -15,8 +15,8 @@ def register(url_prefix: str) -> Blueprint: @bp.get("/") async def overview(): from quart import g - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_overview_page, render_overview_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_overview_page, render_overview_oob page_groups = await get_cart_grouped_by_page(g.s) ctx = await get_template_context() @@ -25,7 +25,7 @@ def register(url_prefix: str) -> Blueprint: html = await render_overview_page(ctx, page_groups) return await make_response(html) else: - sexp_src = await render_overview_oob(ctx, page_groups) - return sexp_response(sexp_src) + sx_src = await render_overview_oob(ctx, page_groups) + return sx_response(sx_src) return bp diff --git a/cart/bp/cart/page_routes.py b/cart/bp/cart/page_routes.py index f3b3e28..210c8f0 100644 --- a/cart/bp/cart/page_routes.py +++ b/cart/bp/cart/page_routes.py @@ -5,7 +5,7 @@ from __future__ import annotations from quart import Blueprint, g, redirect, make_response, url_for from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from shared.infrastructure.actions import call_action from .services import ( total, @@ -41,8 +41,8 @@ def register(url_prefix: str) -> Blueprint: ticket_total=ticket_total, ) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_page_cart_page, render_page_cart_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_page_cart_page, render_page_cart_oob ctx = await get_template_context() if not is_htmx_request(): @@ -52,11 +52,11 @@ def register(url_prefix: str) -> Blueprint: ) return await make_response(html) else: - sexp_src = await render_page_cart_oob( + sx_src = await render_page_cart_oob( ctx, post, cart, cal_entries, page_tickets, ticket_groups, total, calendar_total, ticket_total, ) - return sexp_response(sexp_src) + return sx_response(sx_src) @bp.post("/checkout/") async def page_checkout(): @@ -111,8 +111,8 @@ def register(url_prefix: str) -> Blueprint: hosted_url = result.get("sumup_hosted_url") if not hosted_url: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_checkout_error_page + from shared.sx.page import get_template_context + from sx.sx_components import render_checkout_error_page tctx = await get_template_context() html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp.") return await make_response(html, 500) diff --git a/cart/bp/fragments/routes.py b/cart/bp/fragments/routes.py index 4269998..b84a85c 100644 --- a/cart/bp/fragments/routes.py +++ b/cart/bp/fragments/routes.py @@ -1,6 +1,6 @@ """Cart app fragment endpoints. -Exposes sexp fragments at ``/internal/fragments/`` for consumption +Exposes sx fragments at ``/internal/fragments/`` for consumption by other coop apps via the fragment client. Fragments: @@ -19,13 +19,13 @@ def register(): bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") # --------------------------------------------------------------- - # Fragment handlers — return sexp source text + # Fragment handlers — return sx source text # --------------------------------------------------------------- async def _cart_mini(): from shared.services.registry import services from shared.infrastructure.urls import blog_url, cart_url - from shared.sexp.helpers import sexp_call + from shared.sx.helpers import sx_call user_id = request.args.get("user_id", type=int) session_id = request.args.get("session_id") @@ -35,7 +35,7 @@ def register(): ) count = summary.count + summary.calendar_count + summary.ticket_count oob = request.args.get("oob", "") - return sexp_call("cart-mini", + return sx_call("cart-mini", cart_count=count, blog_url=blog_url(""), cart_url=cart_url(""), @@ -43,9 +43,9 @@ def register(): async def _account_nav_item(): from shared.infrastructure.urls import cart_url - from shared.sexp.helpers import sexp_call + from shared.sx.helpers import sx_call - return sexp_call("account-nav-item", + return sx_call("account-nav-item", href=cart_url("/orders/"), label="orders") @@ -67,8 +67,8 @@ def register(): async def get_fragment(fragment_type: str): handler = _handlers.get(fragment_type) if handler is None: - return Response("", status=200, content_type="text/sexp") + return Response("", status=200, content_type="text/sx") src = await handler() - return Response(src, status=200, content_type="text/sexp") + return Response(src, status=200, content_type="text/sx") return bp diff --git a/cart/bp/order/routes.py b/cart/bp/order/routes.py index 52d9723..c65d5a4 100644 --- a/cart/bp/order/routes.py +++ b/cart/bp/order/routes.py @@ -13,7 +13,7 @@ from shared.infrastructure.http_utils import vary as _vary, current_url_without_ from shared.infrastructure.cart_identity import current_cart_identity from bp.cart.services import check_sumup_status from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from .filters.qs import makeqs_factory, decode @@ -56,8 +56,8 @@ def register() -> Blueprint: order = result.scalar_one_or_none() if not order: return await make_response("Order not found", 404) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_order_page, render_order_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_order_page, render_order_oob ctx = await get_template_context() calendar_entries = ctx.get("calendar_entries") @@ -66,8 +66,8 @@ def register() -> Blueprint: html = await render_order_page(ctx, order, calendar_entries, url_for) return await make_response(html) else: - sexp_src = await render_order_oob(ctx, order, calendar_entries, url_for) - return sexp_response(sexp_src) + sx_src = await render_order_oob(ctx, order, calendar_entries, url_for) + return sx_response(sx_src) @bp.get("/pay/") async def order_pay(order_id: int): @@ -121,8 +121,8 @@ def register() -> Blueprint: await g.s.flush() if not hosted_url: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_checkout_error_page + from shared.sx.page import get_template_context + from sx.sx_components import render_checkout_error_page tctx = await get_template_context() html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp when trying to reopen payment.", order=order) return await make_response(html, 500) diff --git a/cart/bp/orders/routes.py b/cart/bp/orders/routes.py index a185e8b..41d989b 100644 --- a/cart/bp/orders/routes.py +++ b/cart/bp/orders/routes.py @@ -13,7 +13,7 @@ from shared.infrastructure.http_utils import vary as _vary, current_url_without_ from shared.infrastructure.cart_identity import current_cart_identity from bp.cart.services import check_sumup_status from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from bp import register_order from .filters.qs import makeqs_factory, decode @@ -137,8 +137,8 @@ def register(url_prefix: str) -> Blueprint: result = await g.s.execute(stmt) orders = result.scalars().all() - from shared.sexp.page import get_template_context - from sexp.sexp_components import ( + from shared.sx.page import get_template_context + from sx.sx_components import ( render_orders_page, render_orders_rows, render_orders_oob, @@ -154,16 +154,16 @@ def register(url_prefix: str) -> Blueprint: ) resp = await make_response(html) elif page > 1: - sexp_src = await render_orders_rows( + sx_src = await render_orders_rows( ctx, orders, page, total_pages, url_for, qs_fn, ) - resp = sexp_response(sexp_src) + resp = sx_response(sx_src) else: - sexp_src = await render_orders_oob( + sx_src = await render_orders_oob( ctx, orders, page, total_pages, search, total_count, url_for, qs_fn, ) - resp = sexp_response(sexp_src) + resp = sx_response(sx_src) resp.headers["Hx-Push-Url"] = _current_url_without_page() return _vary(resp) diff --git a/cart/bp/page_admin/routes.py b/cart/bp/page_admin/routes.py index 2f41219..c7eca84 100644 --- a/cart/bp/page_admin/routes.py +++ b/cart/bp/page_admin/routes.py @@ -8,7 +8,7 @@ from shared.infrastructure.actions import call_action from shared.infrastructure.data_client import fetch_data from shared.browser.app.authz import require_admin from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -17,8 +17,8 @@ def register(): @bp.get("/") @require_admin async def admin(**kwargs): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_cart_admin_page, render_cart_admin_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_cart_admin_page, render_cart_admin_oob ctx = await get_template_context() page_post = getattr(g, "page_post", None) @@ -26,14 +26,14 @@ def register(): html = await render_cart_admin_page(ctx, page_post) return await make_response(html) else: - sexp_src = await render_cart_admin_oob(ctx, page_post) - return sexp_response(sexp_src) + sx_src = await render_cart_admin_oob(ctx, page_post) + return sx_response(sx_src) @bp.get("/payments/") @require_admin async def payments(**kwargs): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_cart_payments_page, render_cart_payments_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_cart_payments_page, render_cart_payments_oob ctx = await get_template_context() page_post = getattr(g, "page_post", None) @@ -41,8 +41,8 @@ def register(): html = await render_cart_payments_page(ctx, page_post) return await make_response(html) else: - sexp_src = await render_cart_payments_oob(ctx, page_post) - return sexp_response(sexp_src) + sx_src = await render_cart_payments_oob(ctx, page_post) + return sx_response(sx_src) @bp.put("/payments/") @require_admin @@ -77,10 +77,10 @@ def register(): ) g.page_config = SimpleNamespace(**raw_pc) if raw_pc else None - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_cart_payments_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_cart_payments_panel ctx = await get_template_context() html = render_cart_payments_panel(ctx) - return sexp_response(html) + return sx_response(html) return bp diff --git a/cart/sexp/__init__.py b/cart/sx/__init__.py similarity index 100% rename from cart/sexp/__init__.py rename to cart/sx/__init__.py diff --git a/cart/sexp/calendar.sexpr b/cart/sx/calendar.sx similarity index 100% rename from cart/sexp/calendar.sexpr rename to cart/sx/calendar.sx diff --git a/cart/sexp/checkout.sexpr b/cart/sx/checkout.sx similarity index 100% rename from cart/sexp/checkout.sexpr rename to cart/sx/checkout.sx diff --git a/cart/sexp/header.sexpr b/cart/sx/header.sx similarity index 100% rename from cart/sexp/header.sexpr rename to cart/sx/header.sx diff --git a/cart/sexp/items.sexpr b/cart/sx/items.sx similarity index 100% rename from cart/sexp/items.sexpr rename to cart/sx/items.sx diff --git a/cart/sexp/order_detail.sexpr b/cart/sx/order_detail.sx similarity index 100% rename from cart/sexp/order_detail.sexpr rename to cart/sx/order_detail.sx diff --git a/cart/sexp/orders.sexpr b/cart/sx/orders.sx similarity index 100% rename from cart/sexp/orders.sexpr rename to cart/sx/orders.sx diff --git a/cart/sexp/overview.sexpr b/cart/sx/overview.sx similarity index 100% rename from cart/sexp/overview.sexpr rename to cart/sx/overview.sx diff --git a/cart/sexp/payments.sexpr b/cart/sx/payments.sx similarity index 100% rename from cart/sexp/payments.sexpr rename to cart/sx/payments.sx diff --git a/cart/sexp/summary.sexpr b/cart/sx/summary.sx similarity index 100% rename from cart/sexp/summary.sexpr rename to cart/sx/summary.sx diff --git a/cart/sexp/sexp_components.py b/cart/sx/sx_components.py similarity index 66% rename from cart/sexp/sexp_components.py rename to cart/sx/sx_components.py index a8e825c..d5a62af 100644 --- a/cart/sexp/sexp_components.py +++ b/cart/sx/sx_components.py @@ -10,17 +10,17 @@ import os from typing import Any from markupsafe import escape -from shared.sexp.jinja_bridge import load_service_components -from shared.sexp.helpers import ( - call_url, root_header_sexp, post_admin_header_sexp, - post_header_sexp as _shared_post_header_sexp, - search_desktop_sexp, search_mobile_sexp, - full_page_sexp, oob_page_sexp, header_child_sexp, - sexp_call, SexpExpr, +from shared.sx.jinja_bridge import load_service_components +from shared.sx.helpers import ( + call_url, root_header_sx, post_admin_header_sx, + post_header_sx as _shared_post_header_sx, + search_desktop_sx, search_mobile_sx, + full_page_sx, oob_page_sx, header_child_sx, + sx_call, SxExpr, ) from shared.infrastructure.urls import market_product_url, cart_url -# Load cart-specific .sexpr components at import time +# Load cart-specific .sx components at import time load_service_components(os.path.dirname(os.path.dirname(__file__))) @@ -29,7 +29,7 @@ load_service_components(os.path.dirname(os.path.dirname(__file__))) # --------------------------------------------------------------------------- def _ensure_post_ctx(ctx: dict, page_post: Any) -> dict: - """Ensure ctx has a 'post' dict from page_post DTO (for shared post_header_sexp).""" + """Ensure ctx has a 'post' dict from page_post DTO (for shared post_header_sx).""" if ctx.get("post") or not page_post: return ctx ctx = {**ctx, "post": { @@ -63,16 +63,16 @@ async def _ensure_container_nav(ctx: dict) -> dict: return {**ctx, "container_nav": events_nav + market_nav} -async def _post_header_sexp(ctx: dict, page_post: Any, *, oob: bool = False) -> str: +async def _post_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str: """Build post-level header row from page_post DTO, using shared helper.""" ctx = _ensure_post_ctx(ctx, page_post) ctx = await _ensure_container_nav(ctx) - return _shared_post_header_sexp(ctx, oob=oob) + return _shared_post_header_sx(ctx, oob=oob) -def _cart_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _cart_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the cart section header row.""" - return sexp_call( + return sx_call( "menu-row-sx", id="cart-row", level=1, colour="sky", link_href=call_url(ctx, "cart_url", "/"), @@ -81,28 +81,28 @@ def _cart_header_sexp(ctx: dict, *, oob: bool = False) -> str: ) -def _page_cart_header_sexp(ctx: dict, page_post: Any, *, oob: bool = False) -> str: +def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str: """Build the per-page cart header row.""" slug = page_post.slug if page_post else "" title = ((page_post.title if page_post else None) or "")[:160] label_parts = [] if page_post and page_post.feature_image: - label_parts.append(sexp_call("cart-page-label-img", src=page_post.feature_image)) + label_parts.append(sx_call("cart-page-label-img", src=page_post.feature_image)) label_parts.append(f'(span "{escape(title)}")') - label_sexp = "(<> " + " ".join(label_parts) + ")" - nav_sexp = sexp_call("cart-all-carts-link", href=call_url(ctx, "cart_url", "/")) - return sexp_call( + label_sx = "(<> " + " ".join(label_parts) + ")" + nav_sx = sx_call("cart-all-carts-link", href=call_url(ctx, "cart_url", "/")) + return sx_call( "menu-row-sx", id="page-cart-row", level=2, colour="sky", link_href=call_url(ctx, "cart_url", f"/{slug}/"), - link_label_content=SexpExpr(label_sexp), - nav=SexpExpr(nav_sexp), oob=oob, + link_label_content=SxExpr(label_sx), + nav=SxExpr(nav_sx), oob=oob, ) -def _auth_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the account section header row (for orders).""" - return sexp_call( + return sx_call( "menu-row-sx", id="auth-row", level=1, colour="sky", link_href=call_url(ctx, "account_url", "/"), @@ -111,9 +111,9 @@ def _auth_header_sexp(ctx: dict, *, oob: bool = False) -> str: ) -def _orders_header_sexp(ctx: dict, list_url: str) -> str: +def _orders_header_sx(ctx: dict, list_url: str) -> str: """Build the orders section header row.""" - return sexp_call( + return sx_call( "menu-row-sx", id="orders-row", level=2, colour="sky", link_href=list_url, link_label="Orders", icon="fa fa-gbp", @@ -125,13 +125,13 @@ def _orders_header_sexp(ctx: dict, list_url: str) -> str: # Cart overview # --------------------------------------------------------------------------- -def _badge_sexp(icon: str, count: int, label: str) -> str: +def _badge_sx(icon: str, count: int, label: str) -> str: """Render a count badge.""" s = "s" if count != 1 else "" - return sexp_call("cart-badge", icon=icon, text=f"{count} {label}{s}") + return sx_call("cart-badge", icon=icon, text=f"{count} {label}{s}") -def _page_group_card_sexp(grp: Any, ctx: dict) -> str: +def _page_group_card_sx(grp: Any, ctx: dict) -> str: """Render a single page group card for cart overview.""" post = grp.get("post") if isinstance(grp, dict) else getattr(grp, "post", None) cart_items = grp.get("cart_items", []) if isinstance(grp, dict) else getattr(grp, "cart_items", []) @@ -149,13 +149,13 @@ def _page_group_card_sexp(grp: Any, ctx: dict) -> str: # Count badges badge_parts = [] if product_count > 0: - badge_parts.append(_badge_sexp("fa fa-box-open", product_count, "item")) + badge_parts.append(_badge_sx("fa fa-box-open", product_count, "item")) if calendar_count > 0: - badge_parts.append(_badge_sexp("fa fa-calendar", calendar_count, "booking")) + badge_parts.append(_badge_sx("fa fa-calendar", calendar_count, "booking")) if ticket_count > 0: - badge_parts.append(_badge_sexp("fa fa-ticket", ticket_count, "ticket")) - badges_sexp = "(<> " + " ".join(badge_parts) + ")" if badge_parts else '""' - badges_wrap = sexp_call("cart-badges-wrap", badges=SexpExpr(badges_sexp)) + badge_parts.append(_badge_sx("fa fa-ticket", ticket_count, "ticket")) + badges_sx = "(<> " + " ".join(badge_parts) + ")" if badge_parts else '""' + badges_wrap = sx_call("cart-badges-wrap", badges=SxExpr(badges_sx)) if post: slug = post.slug if hasattr(post, "slug") else post.get("slug", "") @@ -164,58 +164,58 @@ def _page_group_card_sexp(grp: Any, ctx: dict) -> str: cart_href = call_url(ctx, "cart_url", f"/{slug}/") if feature_image: - img = sexp_call("cart-group-card-img", src=feature_image, alt=title) + img = sx_call("cart-group-card-img", src=feature_image, alt=title) else: - img = sexp_call("cart-group-card-placeholder") + img = sx_call("cart-group-card-placeholder") mp_sub = "" if market_place: mp_name = market_place.name if hasattr(market_place, "name") else market_place.get("name", "") - mp_sub = sexp_call("cart-mp-subtitle", title=title) + mp_sub = sx_call("cart-mp-subtitle", title=title) else: mp_name = "" display_title = mp_name or title - return sexp_call( + return sx_call( "cart-group-card", - href=cart_href, img=SexpExpr(img), display_title=display_title, - subtitle=SexpExpr(mp_sub) if mp_sub else None, - badges=SexpExpr(badges_wrap), + href=cart_href, img=SxExpr(img), display_title=display_title, + subtitle=SxExpr(mp_sub) if mp_sub else None, + badges=SxExpr(badges_wrap), total=f"\u00a3{total:.2f}", ) else: # Orphan items - return sexp_call( + return sx_call( "cart-orphan-card", - badges=SexpExpr(badges_wrap), + badges=SxExpr(badges_wrap), total=f"\u00a3{total:.2f}", ) -def _empty_cart_sexp() -> str: +def _empty_cart_sx() -> str: """Empty cart state.""" - return sexp_call("cart-empty") + return sx_call("cart-empty") -def _overview_main_panel_sexp(page_groups: list, ctx: dict) -> str: +def _overview_main_panel_sx(page_groups: list, ctx: dict) -> str: """Cart overview main panel.""" if not page_groups: - return _empty_cart_sexp() + return _empty_cart_sx() - cards = [_page_group_card_sexp(grp, ctx) for grp in page_groups] + cards = [_page_group_card_sx(grp, ctx) for grp in page_groups] has_items = any(c for c in cards) if not has_items: - return _empty_cart_sexp() + return _empty_cart_sx() - cards_sexp = "(<> " + " ".join(c for c in cards if c) + ")" - return sexp_call("cart-overview-panel", cards=SexpExpr(cards_sexp)) + cards_sx = "(<> " + " ".join(c for c in cards if c) + ")" + return sx_call("cart-overview-panel", cards=SxExpr(cards_sx)) # --------------------------------------------------------------------------- # Page cart # --------------------------------------------------------------------------- -def _cart_item_sexp(item: Any, ctx: dict) -> str: +def _cart_item_sx(item: Any, ctx: dict) -> str: """Render a single product cart item.""" from shared.browser.app.csrf import generate_csrf_token from quart import url_for @@ -230,41 +230,41 @@ def _cart_item_sexp(item: Any, ctx: dict) -> str: prod_url = market_product_url(slug) if p.image: - img = sexp_call("cart-item-img", src=p.image, alt=p.title) + img = sx_call("cart-item-img", src=p.image, alt=p.title) else: - img = sexp_call("cart-item-no-img") + img = sx_call("cart-item-no-img") price_parts = [] if unit_price: - price_parts.append(sexp_call("cart-item-price", text=f"{symbol}{unit_price:.2f}")) + price_parts.append(sx_call("cart-item-price", text=f"{symbol}{unit_price:.2f}")) if p.special_price and p.special_price != p.regular_price: - price_parts.append(sexp_call("cart-item-price-was", text=f"{symbol}{p.regular_price:.2f}")) + price_parts.append(sx_call("cart-item-price-was", text=f"{symbol}{p.regular_price:.2f}")) else: - price_parts.append(sexp_call("cart-item-no-price")) - price_sexp = "(<> " + " ".join(price_parts) + ")" if len(price_parts) > 1 else price_parts[0] + price_parts.append(sx_call("cart-item-no-price")) + price_sx = "(<> " + " ".join(price_parts) + ")" if len(price_parts) > 1 else price_parts[0] - deleted_sexp = sexp_call("cart-item-deleted") if getattr(item, "is_deleted", False) else None + deleted_sx = sx_call("cart-item-deleted") if getattr(item, "is_deleted", False) else None - brand_sexp = sexp_call("cart-item-brand", brand=p.brand) if getattr(p, "brand", None) else None + brand_sx = sx_call("cart-item-brand", brand=p.brand) if getattr(p, "brand", None) else None - line_total_sexp = None + line_total_sx = None if unit_price: lt = unit_price * item.quantity - line_total_sexp = sexp_call("cart-item-line-total", text=f"Line total: {symbol}{lt:.2f}") + line_total_sx = sx_call("cart-item-line-total", text=f"Line total: {symbol}{lt:.2f}") - return sexp_call( + return sx_call( "cart-item", - id=f"cart-item-{slug}", img=SexpExpr(img), prod_url=prod_url, title=p.title, - brand=SexpExpr(brand_sexp) if brand_sexp else None, - deleted=SexpExpr(deleted_sexp) if deleted_sexp else None, - price=SexpExpr(price_sexp), + id=f"cart-item-{slug}", img=SxExpr(img), prod_url=prod_url, title=p.title, + brand=SxExpr(brand_sx) if brand_sx else None, + deleted=SxExpr(deleted_sx) if deleted_sx else None, + price=SxExpr(price_sx), qty_url=qty_url, csrf=csrf, minus=str(item.quantity - 1), qty=str(item.quantity), plus=str(item.quantity + 1), - line_total=SexpExpr(line_total_sexp) if line_total_sexp else None, + line_total=SxExpr(line_total_sx) if line_total_sx else None, ) -def _calendar_entries_sexp(entries: list) -> str: +def _calendar_entries_sx(entries: list) -> str: """Render calendar booking entries in cart.""" if not entries: return "" @@ -275,15 +275,15 @@ def _calendar_entries_sexp(entries: list) -> str: end = getattr(e, "end_at", None) cost = getattr(e, "cost", 0) or 0 end_str = f" \u2013 {end}" if end else "" - parts.append(sexp_call( + parts.append(sx_call( "cart-cal-entry", name=name, date_str=f"{start}{end_str}", cost=f"\u00a3{cost:.2f}", )) - items_sexp = "(<> " + " ".join(parts) + ")" - return sexp_call("cart-cal-section", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(parts) + ")" + return sx_call("cart-cal-section", items=SxExpr(items_sx)) -def _ticket_groups_sexp(ticket_groups: list, ctx: dict) -> str: +def _ticket_groups_sx(ticket_groups: list, ctx: dict) -> str: """Render ticket groups in cart.""" if not ticket_groups: return "" @@ -309,26 +309,26 @@ def _ticket_groups_sexp(ticket_groups: list, ctx: dict) -> str: if end_at: date_str += f" \u2013 {end_at.strftime('%-d %b %Y, %H:%M')}" - tt_name_sexp = sexp_call("cart-ticket-type-name", name=tt_name) if tt_name else None - tt_hidden_sexp = sexp_call("cart-ticket-type-hidden", value=str(tt_id)) if tt_id else None + tt_name_sx = sx_call("cart-ticket-type-name", name=tt_name) if tt_name else None + tt_hidden_sx = sx_call("cart-ticket-type-hidden", value=str(tt_id)) if tt_id else None - parts.append(sexp_call( + parts.append(sx_call( "cart-ticket-article", name=name, - type_name=SexpExpr(tt_name_sexp) if tt_name_sexp else None, + type_name=SxExpr(tt_name_sx) if tt_name_sx else None, date_str=date_str, price=f"\u00a3{price or 0:.2f}", qty_url=qty_url, csrf=csrf, entry_id=str(entry_id), - type_hidden=SexpExpr(tt_hidden_sexp) if tt_hidden_sexp else None, + type_hidden=SxExpr(tt_hidden_sx) if tt_hidden_sx else None, minus=str(max(quantity - 1, 0)), qty=str(quantity), plus=str(quantity + 1), line_total=f"Line total: \u00a3{line_total:.2f}", )) - items_sexp = "(<> " + " ".join(parts) + ")" - return sexp_call("cart-tickets-section", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(parts) + ")" + return sx_call("cart-tickets-section", items=SxExpr(items_sx)) -def _cart_summary_sexp(ctx: dict, cart: list, cal_entries: list, tickets: list, +def _cart_summary_sx(ctx: dict, cart: list, cal_entries: list, tickets: list, total_fn: Any, cal_total_fn: Any, ticket_total_fn: Any) -> str: """Render the order summary sidebar.""" from shared.browser.app.csrf import generate_csrf_token @@ -360,41 +360,41 @@ def _cart_summary_sexp(ctx: dict, cart: list, cal_entries: list, tickets: list, action = url_for("cart_global.checkout") from shared.utils import route_prefix action = route_prefix() + action - checkout_sexp = sexp_call( + checkout_sx = sx_call( "cart-checkout-form", action=action, csrf=csrf, label=f" Checkout as {user.email}", ) else: href = login_url(request.url) - checkout_sexp = sexp_call("cart-checkout-signin", href=href) + checkout_sx = sx_call("cart-checkout-signin", href=href) - return sexp_call( + return sx_call( "cart-summary-panel", item_count=str(item_count), subtotal=f"{symbol}{grand:.2f}", - checkout=SexpExpr(checkout_sexp), + checkout=SxExpr(checkout_sx), ) -def _page_cart_main_panel_sexp(ctx: dict, cart: list, cal_entries: list, +def _page_cart_main_panel_sx(ctx: dict, cart: list, cal_entries: list, tickets: list, ticket_groups: list, total_fn: Any, cal_total_fn: Any, ticket_total_fn: Any) -> str: """Page cart main panel.""" if not cart and not cal_entries and not tickets: - return sexp_call("cart-page-empty") + return sx_call("cart-page-empty") - item_parts = [_cart_item_sexp(item, ctx) for item in cart] - items_sexp = "(<> " + " ".join(item_parts) + ")" if item_parts else '""' - cal_sexp = _calendar_entries_sexp(cal_entries) - tickets_sexp = _ticket_groups_sexp(ticket_groups, ctx) - summary_sexp = _cart_summary_sexp(ctx, cart, cal_entries, tickets, total_fn, cal_total_fn, ticket_total_fn) + item_parts = [_cart_item_sx(item, ctx) for item in cart] + items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else '""' + cal_sx = _calendar_entries_sx(cal_entries) + tickets_sx = _ticket_groups_sx(ticket_groups, ctx) + summary_sx = _cart_summary_sx(ctx, cart, cal_entries, tickets, total_fn, cal_total_fn, ticket_total_fn) - return sexp_call( + return sx_call( "cart-page-panel", - items=SexpExpr(items_sexp), - cal=SexpExpr(cal_sexp) if cal_sexp else None, - tickets=SexpExpr(tickets_sexp) if tickets_sexp else None, - summary=SexpExpr(summary_sexp), + items=SxExpr(items_sx), + cal=SxExpr(cal_sx) if cal_sx else None, + tickets=SxExpr(tickets_sx) if tickets_sx else None, + summary=SxExpr(summary_sx), ) @@ -402,7 +402,7 @@ def _page_cart_main_panel_sexp(ctx: dict, cart: list, cal_entries: list, # Orders list (same pattern as orders service) # --------------------------------------------------------------------------- -def _order_row_sexp(order: Any, detail_url: str) -> str: +def _order_row_sx(order: Any, detail_url: str) -> str: """Render a single order as desktop table row + mobile card.""" status = order.status or "pending" sl = status.lower() @@ -415,14 +415,14 @@ def _order_row_sexp(order: Any, detail_url: str) -> str: created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014" total = f"{order.currency or 'GBP'} {order.total_amount or 0:.2f}" - desktop = sexp_call( + desktop = sx_call( "cart-order-row-desktop", order_id=f"#{order.id}", created=created, desc=order.description or "", total=total, pill=pill_cls, status=status, detail_url=detail_url, ) mobile_pill = f"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] {pill}" - mobile = sexp_call( + mobile = sx_call( "cart-order-row-mobile", order_id=f"#{order.id}", pill=mobile_pill, status=status, created=created, total=total, detail_url=detail_url, @@ -431,47 +431,47 @@ def _order_row_sexp(order: Any, detail_url: str) -> str: return "(<> " + desktop + " " + mobile + ")" -def _orders_rows_sexp(orders: list, page: int, total_pages: int, +def _orders_rows_sx(orders: list, page: int, total_pages: int, url_for_fn: Any, qs_fn: Any) -> str: """Render order rows + infinite scroll sentinel.""" from shared.utils import route_prefix pfx = route_prefix() parts = [ - _order_row_sexp(o, pfx + url_for_fn("orders.order.order_detail", order_id=o.id)) + _order_row_sx(o, pfx + url_for_fn("orders.order.order_detail", order_id=o.id)) for o in orders ] if page < total_pages: next_url = pfx + url_for_fn("orders.list_orders") + qs_fn(page=page + 1) - parts.append(sexp_call( + parts.append(sx_call( "infinite-scroll", url=next_url, page=page, total_pages=total_pages, id_prefix="orders", colspan=5, )) else: - parts.append(sexp_call("cart-orders-end")) + parts.append(sx_call("cart-orders-end")) return "(<> " + " ".join(parts) + ")" -def _orders_main_panel_sexp(orders: list, rows_sexp: str) -> str: +def _orders_main_panel_sx(orders: list, rows_sx: str) -> str: """Main panel for orders list.""" if not orders: - return sexp_call("cart-orders-empty") - return sexp_call("cart-orders-table", rows=SexpExpr(rows_sexp)) + return sx_call("cart-orders-empty") + return sx_call("cart-orders-table", rows=SxExpr(rows_sx)) -def _orders_summary_sexp(ctx: dict) -> str: +def _orders_summary_sx(ctx: dict) -> str: """Filter section for orders list.""" - return sexp_call("cart-orders-filter", search_mobile=SexpExpr(search_mobile_sexp(ctx))) + return sx_call("cart-orders-filter", search_mobile=SxExpr(search_mobile_sx(ctx))) # --------------------------------------------------------------------------- # Single order detail # --------------------------------------------------------------------------- -def _order_items_sexp(order: Any) -> str: +def _order_items_sx(order: Any) -> str: """Render order items list.""" if not order or not order.items: return "" @@ -479,27 +479,27 @@ def _order_items_sexp(order: Any) -> str: for item in order.items: prod_url = market_product_url(item.product_slug) if item.product_image: - img = sexp_call( + img = sx_call( "cart-order-item-img", src=item.product_image, alt=item.product_title or "Product image", ) else: - img = sexp_call("cart-order-item-no-img") - parts.append(sexp_call( + img = sx_call("cart-order-item-no-img") + parts.append(sx_call( "cart-order-item", - prod_url=prod_url, img=SexpExpr(img), + prod_url=prod_url, img=SxExpr(img), title=item.product_title or "Unknown product", product_id=f"Product ID: {item.product_id}", qty=f"Qty: {item.quantity}", price=f"{item.currency or order.currency or 'GBP'} {item.unit_price or 0:.2f}", )) - items_sexp = "(<> " + " ".join(parts) + ")" - return sexp_call("cart-order-items-panel", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(parts) + ")" + return sx_call("cart-order-items-panel", items=SxExpr(items_sx)) -def _order_summary_sexp(order: Any) -> str: +def _order_summary_sx(order: Any) -> str: """Order summary card.""" - return sexp_call( + return sx_call( "order-summary-card", order_id=order.id, created_at=order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else None, @@ -508,7 +508,7 @@ def _order_summary_sexp(order: Any) -> str: ) -def _order_calendar_items_sexp(calendar_entries: list | None) -> str: +def _order_calendar_items_sx(calendar_entries: list | None) -> str: """Render calendar bookings for an order.""" if not calendar_entries: return "" @@ -525,43 +525,43 @@ def _order_calendar_items_sexp(calendar_entries: list | None) -> str: ds = e.start_at.strftime("%-d %b %Y, %H:%M") if e.start_at else "" if e.end_at: ds += f" \u2013 {e.end_at.strftime('%-d %b %Y, %H:%M')}" - parts.append(sexp_call( + parts.append(sx_call( "cart-order-cal-entry", name=e.name, pill=pill_cls, status=st.capitalize(), date_str=ds, cost=f"\u00a3{e.cost or 0:.2f}", )) - items_sexp = "(<> " + " ".join(parts) + ")" - return sexp_call("cart-order-cal-section", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(parts) + ")" + return sx_call("cart-order-cal-section", items=SxExpr(items_sx)) -def _order_main_sexp(order: Any, calendar_entries: list | None) -> str: +def _order_main_sx(order: Any, calendar_entries: list | None) -> str: """Main panel for single order detail.""" - summary = _order_summary_sexp(order) - items = _order_items_sexp(order) - cal = _order_calendar_items_sexp(calendar_entries) - return sexp_call( + summary = _order_summary_sx(order) + items = _order_items_sx(order) + cal = _order_calendar_items_sx(calendar_entries) + return sx_call( "cart-order-main", - summary=SexpExpr(summary), - items=SexpExpr(items) if items else None, - cal=SexpExpr(cal) if cal else None, + summary=SxExpr(summary), + items=SxExpr(items) if items else None, + cal=SxExpr(cal) if cal else None, ) -def _order_filter_sexp(order: Any, list_url: str, recheck_url: str, +def _order_filter_sx(order: Any, list_url: str, recheck_url: str, pay_url: str, csrf_token: str) -> str: """Filter section for single order detail.""" created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014" status = order.status or "pending" - pay_sexp = None + pay_sx = None if status != "paid": - pay_sexp = sexp_call("cart-order-pay-btn", url=pay_url) + pay_sx = sx_call("cart-order-pay-btn", url=pay_url) - return sexp_call( + return sx_call( "cart-order-filter", info=f"Placed {created} \u00b7 Status: {status}", list_url=list_url, recheck_url=recheck_url, csrf=csrf_token, - pay=SexpExpr(pay_sexp) if pay_sexp else None, + pay=SxExpr(pay_sx) if pay_sx else None, ) @@ -571,16 +571,16 @@ def _order_filter_sexp(order: Any, list_url: str, recheck_url: str, async def render_overview_page(ctx: dict, page_groups: list) -> str: """Full page: cart overview.""" - main = _overview_main_panel_sexp(page_groups, ctx) - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, content=main) + main = _overview_main_panel_sx(page_groups, ctx) + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=main) async def render_overview_oob(ctx: dict, page_groups: list) -> str: """OOB response for cart overview.""" - main = _overview_main_panel_sexp(page_groups, ctx) - oobs = root_header_sexp(ctx, oob=True) - return oob_page_sexp(oobs=oobs, content=main) + main = _overview_main_panel_sx(page_groups, ctx) + oobs = root_header_sx(ctx, oob=True) + return oob_page_sx(oobs=oobs, content=main) # --------------------------------------------------------------------------- @@ -592,17 +592,17 @@ async def render_page_cart_page(ctx: dict, page_post: Any, ticket_groups: list, total_fn: Any, cal_total_fn: Any, ticket_total_fn: Any) -> str: """Full page: page-specific cart.""" - main = _page_cart_main_panel_sexp(ctx, cart, cal_entries, tickets, ticket_groups, + main = _page_cart_main_panel_sx(ctx, cart, cal_entries, tickets, ticket_groups, total_fn, cal_total_fn, ticket_total_fn) - hdr = root_header_sexp(ctx) - child = _cart_header_sexp(ctx) - page_hdr = _page_cart_header_sexp(ctx, page_post) - nested = sexp_call( + hdr = root_header_sx(ctx) + child = _cart_header_sx(ctx) + page_hdr = _page_cart_header_sx(ctx, page_post) + nested = sx_call( "cart-header-child-nested", - outer=SexpExpr(child), inner=SexpExpr(page_hdr), + outer=SxExpr(child), inner=SxExpr(page_hdr), ) header_rows = "(<> " + hdr + " " + nested + ")" - return full_page_sexp(ctx, header_rows=header_rows, content=main) + return full_page_sx(ctx, header_rows=header_rows, content=main) async def render_page_cart_oob(ctx: dict, page_post: Any, @@ -610,14 +610,14 @@ async def render_page_cart_oob(ctx: dict, page_post: Any, ticket_groups: list, total_fn: Any, cal_total_fn: Any, ticket_total_fn: Any) -> str: """OOB response for page cart.""" - main = _page_cart_main_panel_sexp(ctx, cart, cal_entries, tickets, ticket_groups, + main = _page_cart_main_panel_sx(ctx, cart, cal_entries, tickets, ticket_groups, total_fn, cal_total_fn, ticket_total_fn) - child_oob = sexp_call("cart-header-child-oob", - inner=SexpExpr(_page_cart_header_sexp(ctx, page_post))) - cart_hdr_oob = _cart_header_sexp(ctx, oob=True) - root_hdr_oob = root_header_sexp(ctx, oob=True) + child_oob = sx_call("cart-header-child-oob", + inner=SxExpr(_page_cart_header_sx(ctx, page_post))) + cart_hdr_oob = _cart_header_sx(ctx, oob=True) + root_hdr_oob = root_header_sx(ctx, oob=True) oobs = "(<> " + child_oob + " " + cart_hdr_oob + " " + root_hdr_oob + ")" - return oob_page_sexp(oobs=oobs, content=main) + return oob_page_sx(oobs=oobs, content=main) # --------------------------------------------------------------------------- @@ -635,21 +635,21 @@ async def render_orders_page(ctx: dict, orders: list, page: int, ctx["search_count"] = search_count list_url = route_prefix() + url_for_fn("orders.list_orders") - rows = _orders_rows_sexp(orders, page, total_pages, url_for_fn, qs_fn) - main = _orders_main_panel_sexp(orders, rows) + rows = _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn) + main = _orders_main_panel_sx(orders, rows) - hdr = root_header_sexp(ctx) - auth = _auth_header_sexp(ctx) - orders_hdr = _orders_header_sexp(ctx, list_url) - auth_child = sexp_call( + hdr = root_header_sx(ctx) + auth = _auth_header_sx(ctx) + orders_hdr = _orders_header_sx(ctx, list_url) + auth_child = sx_call( "cart-auth-header-child", - auth=SexpExpr(auth), orders=SexpExpr(orders_hdr), + auth=SxExpr(auth), orders=SxExpr(orders_hdr), ) header_rows = "(<> " + hdr + " " + auth_child + ")" - return full_page_sexp(ctx, header_rows=header_rows, - filter=_orders_summary_sexp(ctx), - aside=search_desktop_sexp(ctx), + return full_page_sx(ctx, header_rows=header_rows, + filter=_orders_summary_sx(ctx), + aside=search_desktop_sx(ctx), content=main) @@ -657,7 +657,7 @@ async def render_orders_rows(ctx: dict, orders: list, page: int, total_pages: int, url_for_fn: Any, qs_fn: Any) -> str: """Pagination: just the table rows.""" - return _orders_rows_sexp(orders, page, total_pages, url_for_fn, qs_fn) + return _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn) async def render_orders_oob(ctx: dict, orders: list, page: int, @@ -671,20 +671,20 @@ async def render_orders_oob(ctx: dict, orders: list, page: int, ctx["search_count"] = search_count list_url = route_prefix() + url_for_fn("orders.list_orders") - rows = _orders_rows_sexp(orders, page, total_pages, url_for_fn, qs_fn) - main = _orders_main_panel_sexp(orders, rows) + rows = _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn) + main = _orders_main_panel_sx(orders, rows) - auth_oob = _auth_header_sexp(ctx, oob=True) - auth_child_oob = sexp_call( + auth_oob = _auth_header_sx(ctx, oob=True) + auth_child_oob = sx_call( "cart-auth-header-child-oob", - inner=SexpExpr(_orders_header_sexp(ctx, list_url)), + inner=SxExpr(_orders_header_sx(ctx, list_url)), ) - root_oob = root_header_sexp(ctx, oob=True) + root_oob = root_header_sx(ctx, oob=True) oobs = "(<> " + auth_oob + " " + auth_child_oob + " " + root_oob + ")" - return oob_page_sexp(oobs=oobs, - filter=_orders_summary_sexp(ctx), - aside=search_desktop_sexp(ctx), + return oob_page_sx(oobs=oobs, + filter=_orders_summary_sx(ctx), + aside=search_desktop_sx(ctx), content=main) @@ -705,24 +705,24 @@ async def render_order_page(ctx: dict, order: Any, recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id) pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id) - main = _order_main_sexp(order, calendar_entries) - filt = _order_filter_sexp(order, list_url, recheck_url, pay_url, generate_csrf_token()) + main = _order_main_sx(order, calendar_entries) + filt = _order_filter_sx(order, list_url, recheck_url, pay_url, generate_csrf_token()) - hdr = root_header_sexp(ctx) - order_row = sexp_call( + hdr = root_header_sx(ctx) + order_row = sx_call( "menu-row-sx", id="order-row", level=3, colour="sky", link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp", ) - order_child = sexp_call( + order_child = sx_call( "cart-order-header-child", - auth=SexpExpr(_auth_header_sexp(ctx)), - orders=SexpExpr(_orders_header_sexp(ctx, list_url)), - order=SexpExpr(order_row), + auth=SxExpr(_auth_header_sx(ctx)), + orders=SxExpr(_orders_header_sx(ctx, list_url)), + order=SxExpr(order_row), ) header_rows = "(<> " + hdr + " " + order_child + ")" - return full_page_sexp(ctx, header_rows=header_rows, filter=filt, content=main) + return full_page_sx(ctx, header_rows=header_rows, filter=filt, content=main) async def render_order_oob(ctx: dict, order: Any, @@ -738,66 +738,66 @@ async def render_order_oob(ctx: dict, order: Any, recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id) pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id) - main = _order_main_sexp(order, calendar_entries) - filt = _order_filter_sexp(order, list_url, recheck_url, pay_url, generate_csrf_token()) + main = _order_main_sx(order, calendar_entries) + filt = _order_filter_sx(order, list_url, recheck_url, pay_url, generate_csrf_token()) - order_row_oob = sexp_call( + order_row_oob = sx_call( "menu-row-sx", id="order-row", level=3, colour="sky", link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp", oob=True, ) - orders_child_oob = sexp_call("cart-orders-header-child-oob", - inner=SexpExpr(order_row_oob)) - root_oob = root_header_sexp(ctx, oob=True) + orders_child_oob = sx_call("cart-orders-header-child-oob", + inner=SxExpr(order_row_oob)) + root_oob = root_header_sx(ctx, oob=True) oobs = "(<> " + orders_child_oob + " " + root_oob + ")" - return oob_page_sexp(oobs=oobs, filter=filt, content=main) + return oob_page_sx(oobs=oobs, filter=filt, content=main) # --------------------------------------------------------------------------- # Public API: Checkout error # --------------------------------------------------------------------------- -def _checkout_error_filter_sexp() -> str: - return sexp_call("cart-checkout-error-filter") +def _checkout_error_filter_sx() -> str: + return sx_call("cart-checkout-error-filter") -def _checkout_error_content_sexp(error: str | None, order: Any | None) -> str: +def _checkout_error_content_sx(error: str | None, order: Any | None) -> str: err_msg = error or "Unexpected error while creating the hosted checkout session." - order_sexp = None + order_sx = None if order: - order_sexp = sexp_call("cart-checkout-error-order-id", order_id=f"#{order.id}") + order_sx = sx_call("cart-checkout-error-order-id", order_id=f"#{order.id}") back_url = cart_url("/") - return sexp_call( + return sx_call( "cart-checkout-error-content", error_msg=err_msg, - order=SexpExpr(order_sexp) if order_sexp else None, + order=SxExpr(order_sx) if order_sx else None, back_url=back_url, ) async def render_checkout_error_page(ctx: dict, error: str | None = None, order: Any | None = None) -> str: """Full page: checkout error.""" - hdr = root_header_sexp(ctx) - filt = _checkout_error_filter_sexp() - content = _checkout_error_content_sexp(error, order) - return full_page_sexp(ctx, header_rows=hdr, filter=filt, content=content) + hdr = root_header_sx(ctx) + filt = _checkout_error_filter_sx() + content = _checkout_error_content_sx(error, order) + return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content) # --------------------------------------------------------------------------- # Page admin (//admin/) # --------------------------------------------------------------------------- -def _cart_page_admin_header_sexp(ctx: dict, page_post: Any, *, oob: bool = False, +def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = False, selected: str = "") -> str: """Build the page-level admin header row -- delegates to shared helper.""" slug = page_post.slug if page_post else "" ctx = _ensure_post_ctx(ctx, page_post) - return post_admin_header_sexp(ctx, slug, oob=oob, selected=selected) + return post_admin_header_sx(ctx, slug, oob=oob, selected=selected) -def _cart_admin_main_panel_sexp(ctx: dict) -> str: +def _cart_admin_main_panel_sx(ctx: dict) -> str: """Admin overview panel -- links to sub-admin pages.""" from quart import url_for payments_href = url_for("page_admin.payments") @@ -809,7 +809,7 @@ def _cart_admin_main_panel_sexp(ctx: dict) -> str: ) -def _cart_payments_main_panel_sexp(ctx: dict) -> str: +def _cart_payments_main_panel_sx(ctx: dict) -> str: """Render SumUp payment config form.""" from quart import url_for csrf_token = ctx.get("csrf_token") @@ -823,7 +823,7 @@ def _cart_payments_main_panel_sexp(ctx: dict) -> str: placeholder = "--------" if sumup_configured else "sup_sk_..." input_cls = "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500" - return sexp_call("cart-payments-panel", + return sx_call("cart-payments-panel", update_url=update_url, csrf=csrf, merchant_code=merchant_code, placeholder=placeholder, input_cls=input_cls, sumup_configured=sumup_configured, @@ -836,19 +836,19 @@ def _cart_payments_main_panel_sexp(ctx: dict) -> str: async def render_cart_admin_page(ctx: dict, page_post: Any) -> str: """Full page: cart page admin overview.""" - content = _cart_admin_main_panel_sexp(ctx) - root_hdr = root_header_sexp(ctx) - post_hdr = await _post_header_sexp(ctx, page_post) - admin_hdr = _cart_page_admin_header_sexp(ctx, page_post) + content = _cart_admin_main_panel_sx(ctx) + root_hdr = root_header_sx(ctx) + post_hdr = await _post_header_sx(ctx, page_post) + admin_hdr = _cart_page_admin_header_sx(ctx, page_post) header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")" - return full_page_sexp(ctx, header_rows=header_rows, content=content) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_cart_admin_oob(ctx: dict, page_post: Any) -> str: """OOB response: cart page admin overview.""" - content = _cart_admin_main_panel_sexp(ctx) - oobs = _cart_page_admin_header_sexp(ctx, page_post, oob=True) - return oob_page_sexp(oobs=oobs, content=content) + content = _cart_admin_main_panel_sx(ctx) + oobs = _cart_page_admin_header_sx(ctx, page_post, oob=True) + return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- @@ -857,21 +857,21 @@ async def render_cart_admin_oob(ctx: dict, page_post: Any) -> str: async def render_cart_payments_page(ctx: dict, page_post: Any) -> str: """Full page: payments config.""" - content = _cart_payments_main_panel_sexp(ctx) - root_hdr = root_header_sexp(ctx) - post_hdr = await _post_header_sexp(ctx, page_post) - admin_hdr = _cart_page_admin_header_sexp(ctx, page_post, selected="payments") + content = _cart_payments_main_panel_sx(ctx) + root_hdr = root_header_sx(ctx) + post_hdr = await _post_header_sx(ctx, page_post) + admin_hdr = _cart_page_admin_header_sx(ctx, page_post, selected="payments") header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")" - return full_page_sexp(ctx, header_rows=header_rows, content=content) + return full_page_sx(ctx, header_rows=header_rows, content=content) async def render_cart_payments_oob(ctx: dict, page_post: Any) -> str: """OOB response: payments config.""" - content = _cart_payments_main_panel_sexp(ctx) - oobs = _cart_page_admin_header_sexp(ctx, page_post, oob=True, selected="payments") - return oob_page_sexp(oobs=oobs, content=content) + content = _cart_payments_main_panel_sx(ctx) + oobs = _cart_page_admin_header_sx(ctx, page_post, oob=True, selected="payments") + return oob_page_sx(oobs=oobs, content=content) def render_cart_payments_panel(ctx: dict) -> str: """Render the payments config panel for PUT response.""" - return _cart_payments_main_panel_sexp(ctx) + return _cart_payments_main_panel_sx(ctx) diff --git a/cart/sexp/tickets.sexpr b/cart/sx/tickets.sx similarity index 100% rename from cart/sexp/tickets.sexpr rename to cart/sx/tickets.sx diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6ec9c1a..935f918 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -45,7 +45,7 @@ services: - ./blog/alembic.ini:/app/blog/alembic.ini:ro - ./blog/alembic:/app/blog/alembic:ro - ./blog/app.py:/app/app.py - - ./blog/sexp:/app/sexp + - ./blog/sx:/app/sx - ./blog/bp:/app/bp - ./blog/services:/app/services - ./blog/templates:/app/templates @@ -83,7 +83,7 @@ services: - ./market/alembic.ini:/app/market/alembic.ini:ro - ./market/alembic:/app/market/alembic:ro - ./market/app.py:/app/app.py - - ./market/sexp:/app/sexp + - ./market/sx:/app/sx - ./market/bp:/app/bp - ./market/services:/app/services - ./market/templates:/app/templates @@ -120,7 +120,7 @@ services: - ./cart/alembic.ini:/app/cart/alembic.ini:ro - ./cart/alembic:/app/cart/alembic:ro - ./cart/app.py:/app/app.py - - ./cart/sexp:/app/sexp + - ./cart/sx:/app/sx - ./cart/bp:/app/bp - ./cart/services:/app/services - ./cart/templates:/app/templates @@ -157,7 +157,7 @@ services: - ./events/alembic.ini:/app/events/alembic.ini:ro - ./events/alembic:/app/events/alembic:ro - ./events/app.py:/app/app.py - - ./events/sexp:/app/sexp + - ./events/sx:/app/sx - ./events/bp:/app/bp - ./events/services:/app/services - ./events/templates:/app/templates @@ -194,7 +194,7 @@ services: - ./federation/alembic.ini:/app/federation/alembic.ini:ro - ./federation/alembic:/app/federation/alembic:ro - ./federation/app.py:/app/app.py - - ./federation/sexp:/app/sexp + - ./federation/sx:/app/sx - ./federation/bp:/app/bp - ./federation/services:/app/services - ./federation/templates:/app/templates @@ -231,7 +231,7 @@ services: - ./account/alembic.ini:/app/account/alembic.ini:ro - ./account/alembic:/app/account/alembic:ro - ./account/app.py:/app/app.py - - ./account/sexp:/app/sexp + - ./account/sx:/app/sx - ./account/bp:/app/bp - ./account/services:/app/services - ./account/templates:/app/templates @@ -330,7 +330,7 @@ services: - ./orders/alembic.ini:/app/orders/alembic.ini:ro - ./orders/alembic:/app/orders/alembic:ro - ./orders/app.py:/app/app.py - - ./orders/sexp:/app/sexp + - ./orders/sx:/app/sx - ./orders/bp:/app/bp - ./orders/services:/app/services - ./orders/templates:/app/templates @@ -361,7 +361,7 @@ services: - /root/rose-ash/_config/app-config.yaml:/app/config/app-config.yaml:ro - ./shared:/app/shared - ./test/app.py:/app/app.py - - ./test/sexp:/app/sexp + - ./test/sx:/app/sx - ./test/bp:/app/bp - ./test/services:/app/services - ./test/runner.py:/app/runner.py diff --git a/events/app.py b/events/app.py index 086926d..dd9c194 100644 --- a/events/app.py +++ b/events/app.py @@ -1,7 +1,7 @@ from __future__ import annotations import path_setup # noqa: F401 # adds shared/ to sys.path -import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file +import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file from pathlib import Path from quart import g, abort, request diff --git a/events/bp/all_events/routes.py b/events/bp/all_events/routes.py index adfbc5b..1f091ac 100644 --- a/events/bp/all_events/routes.py +++ b/events/bp/all_events/routes.py @@ -14,7 +14,7 @@ from __future__ import annotations from quart import Blueprint, g, request, render_template, make_response from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.data_client import fetch_data from shared.contracts.dtos import PostDTO, dto_from_dict @@ -66,13 +66,13 @@ def register() -> Blueprint: entries, has_more, pending_tickets, page_info = await _load_entries(page) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_all_events_page, render_all_events_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_all_events_page, render_all_events_oob ctx = await get_template_context() if is_htmx_request(): - sexp_src = await render_all_events_oob(ctx, entries, has_more, pending_tickets, page_info, page, view) - return sexp_response(sexp_src) + sx_src = await render_all_events_oob(ctx, entries, has_more, pending_tickets, page_info, page, view) + return sx_response(sx_src) else: html = await render_all_events_page(ctx, entries, has_more, pending_tickets, page_info, page, view) return await make_response(html, 200) @@ -84,9 +84,9 @@ def register() -> Blueprint: entries, has_more, pending_tickets, page_info = await _load_entries(page) - from sexp.sexp_components import render_all_events_cards - sexp_src = await render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view) - return sexp_response(sexp_src) + from sx.sx_components import render_all_events_cards + sx_src = await render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view) + return sx_response(sx_src) @bp.post("/all-tickets/adjust") async def adjust_ticket(): @@ -125,9 +125,9 @@ def register() -> Blueprint: if ident["session_id"] is not None: frag_params["session_id"] = ident["session_id"] - from sexp.sexp_components import render_ticket_widget + from sx.sx_components import render_ticket_widget widget_html = render_ticket_widget(entry, qty, "/all-tickets/adjust") mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False) - return sexp_response(widget_html + (mini_html or "")) + return sx_response(widget_html + (mini_html or "")) return bp diff --git a/events/bp/calendar/admin/routes.py b/events/bp/calendar/admin/routes.py index 0d5bb42..16b2e94 100644 --- a/events/bp/calendar/admin/routes.py +++ b/events/bp/calendar/admin/routes.py @@ -7,7 +7,7 @@ from quart import ( from shared.browser.app.authz import require_admin from shared.browser.app.redis_cacher import clear_cache -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response @@ -20,24 +20,24 @@ def register(): async def admin(calendar_slug: str, **kwargs): from shared.browser.app.utils.htmx import is_htmx_request - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_calendar_admin_page, render_calendar_admin_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_calendar_admin_page, render_calendar_admin_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_calendar_admin_page(tctx) return await make_response(html) else: - sexp_src = await render_calendar_admin_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_calendar_admin_oob(tctx) + return sx_response(sx_src) @bp.get("/description/") @require_admin async def calendar_description_edit(calendar_slug: str, **kwargs): - from sexp.sexp_components import render_calendar_description_edit + from sx.sx_components import render_calendar_description_edit html = render_calendar_description_edit(g.calendar) - return sexp_response(html) + return sx_response(html) @bp.post("/description/") @@ -51,16 +51,16 @@ def register(): g.calendar.description = description await g.s.flush() - from sexp.sexp_components import render_calendar_description + from sx.sx_components import render_calendar_description html = render_calendar_description(g.calendar, oob=True) - return sexp_response(html) + return sx_response(html) @bp.get("/description/view/") @require_admin async def calendar_description_view(calendar_slug: str, **kwargs): - from sexp.sexp_components import render_calendar_description + from sx.sx_components import render_calendar_description html = render_calendar_description(g.calendar) - return sexp_response(html) + return sx_response(html) return bp diff --git a/events/bp/calendar/routes.py b/events/bp/calendar/routes.py index b5eda3f..a6fc8ed 100644 --- a/events/bp/calendar/routes.py +++ b/events/bp/calendar/routes.py @@ -24,7 +24,7 @@ from .services.calendar_view import ( update_calendar_description, ) from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from ..slots.routes import register as register_slots @@ -157,8 +157,8 @@ def register(): user_entries = visible.user_entries confirmed_entries = visible.confirmed_entries - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_calendar_page, render_calendar_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_calendar_page, render_calendar_oob tctx = await get_template_context() tctx.update(dict( @@ -175,8 +175,8 @@ def register(): html = await render_calendar_page(tctx) return await make_response(html) else: - sexp_src = await render_calendar_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_calendar_oob(tctx) + return sx_response(sx_src) @bp.put("/") @@ -198,11 +198,11 @@ def register(): description = (form.get("description") or "").strip() await update_calendar_description(g.calendar, description) - from shared.sexp.page import get_template_context - from sexp.sexp_components import _calendar_admin_main_panel_html + from shared.sx.page import get_template_context + from sx.sx_components import _calendar_admin_main_panel_html ctx = await get_template_context() html = _calendar_admin_main_panel_html(ctx) - return sexp_response(html) + return sx_response(html) @bp.delete("/") @@ -217,14 +217,14 @@ def register(): # If we have post context (blog-embedded mode), update nav post_data = getattr(g, "post_data", None) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_calendars_list_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_calendars_list_panel ctx = await get_template_context() html = render_calendars_list_panel(ctx) if post_data: from shared.services.entry_associations import get_associated_entries - from sexp.sexp_components import render_post_nav_entries_oob + from sx.sx_components import render_post_nav_entries_oob post_id = (post_data.get("post") or {}).get("id") cals = ( @@ -239,7 +239,7 @@ def register(): nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"]) html = html + nav_oob - return sexp_response(html) + return sx_response(html) return bp diff --git a/events/bp/calendar_entries/routes.py b/events/bp/calendar_entries/routes.py index 718112c..544935d 100644 --- a/events/bp/calendar_entries/routes.py +++ b/events/bp/calendar_entries/routes.py @@ -16,7 +16,7 @@ from .services.entries import ( ) from shared.browser.app.authz import require_admin from shared.browser.app.redis_cacher import clear_cache -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from bp.calendar_entry.routes import register as register_calendar_entry @@ -217,7 +217,7 @@ def register(): if ident["session_id"] is not None: frag_params["session_id"] = ident["session_id"] - # Re-query day entries for the sexp component + # Re-query day entries for the sx component from datetime import date as date_cls, timedelta from bp.calendar.services import get_visible_entries_for_period from quart import session as qsession @@ -258,10 +258,10 @@ def register(): "styles": styles, } - from sexp.sexp_components import render_day_main_panel + from sx.sx_components import render_day_main_panel html = render_day_main_panel(ctx) mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False) - return sexp_response(html + (mini_html or "")) + return sx_response(html + (mini_html or "")) @bp.get("/add/") async def add_form(day: int, month: int, year: int, **kwargs): diff --git a/events/bp/calendar_entry/routes.py b/events/bp/calendar_entry/routes.py index 04b9336..e3cd542 100644 --- a/events/bp/calendar_entry/routes.py +++ b/events/bp/calendar_entry/routes.py @@ -28,7 +28,7 @@ import math import logging from shared.infrastructure.fragments import fetch_fragment -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from ..ticket_types.routes import register as register_ticket_types @@ -111,7 +111,7 @@ def register(): ) # Render OOB nav - from sexp.sexp_components import render_day_entries_nav_oob + from sx.sx_components import render_day_entries_nav_oob return render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date) async def get_post_nav_oob(entry_id: int): @@ -148,7 +148,7 @@ def register(): ).scalars().all() # Render OOB nav for this post - from sexp.sexp_components import render_post_nav_entries_oob + from sx.sx_components import render_post_nav_entries_oob nav_oob = render_post_nav_entries_oob(associated_entries, calendars, post) nav_oobs.append(nav_oob) @@ -242,16 +242,16 @@ def register(): @require_admin async def get(entry_id: int, **rest): from shared.browser.app.utils.htmx import is_htmx_request - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_entry_page, render_entry_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_entry_page, render_entry_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_entry_page(tctx) return await make_response(html, 200) else: - sexp_src = await render_entry_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_entry_oob(tctx) + return sx_response(sx_src) @bp.get("/edit/") @require_admin @@ -419,12 +419,12 @@ def register(): # Get nav OOB update nav_oob = await get_day_nav_oob(year, month, day) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_entry_page + from shared.sx.page import get_template_context + from sx.sx_components import render_entry_page tctx = await get_template_context() html = await render_entry_page(tctx) - return sexp_response(html + nav_oob) + return sx_response(html + nav_oob) @bp.post("/confirm/") @@ -448,9 +448,9 @@ def register(): # Re-read entry to get updated state await g.s.refresh(g.entry) - from sexp.sexp_components import render_entry_optioned + from sx.sx_components import render_entry_optioned html = render_entry_optioned(g.entry, g.calendar, day, month, year) - return sexp_response(html + day_nav_oob + post_nav_oob) + return sx_response(html + day_nav_oob + post_nav_oob) @bp.post("/decline/") @require_admin @@ -473,9 +473,9 @@ def register(): # Re-read entry to get updated state await g.s.refresh(g.entry) - from sexp.sexp_components import render_entry_optioned + from sx.sx_components import render_entry_optioned html = render_entry_optioned(g.entry, g.calendar, day, month, year) - return sexp_response(html + day_nav_oob + post_nav_oob) + return sx_response(html + day_nav_oob + post_nav_oob) @bp.post("/provisional/") @require_admin @@ -498,9 +498,9 @@ def register(): # Re-read entry to get updated state await g.s.refresh(g.entry) - from sexp.sexp_components import render_entry_optioned + from sx.sx_components import render_entry_optioned html = render_entry_optioned(g.entry, g.calendar, day, month, year) - return sexp_response(html + day_nav_oob + post_nav_oob) + return sx_response(html + day_nav_oob + post_nav_oob) @bp.post("/tickets/") @require_admin @@ -542,9 +542,9 @@ def register(): # Return just the tickets fragment (targeted by hx-target="#entry-tickets-...") await g.s.refresh(g.entry) - from sexp.sexp_components import render_entry_tickets_config + from sx.sx_components import render_entry_tickets_config html = render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year")) - return sexp_response(html) + return sx_response(html) @bp.get("/posts/search/") @require_admin @@ -593,11 +593,11 @@ def register(): entry_posts = await get_entry_posts(g.s, entry_id) # Return updated posts list + OOB nav update - from sexp.sexp_components import render_entry_posts_panel, render_entry_posts_nav_oob + from sx.sx_components import render_entry_posts_panel, render_entry_posts_nav_oob va = request.view_args or {} html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year")) nav_oob = render_entry_posts_nav_oob(entry_posts) - return sexp_response(html + nav_oob) + return sx_response(html + nav_oob) @bp.delete("/posts//") @require_admin @@ -615,10 +615,10 @@ def register(): entry_posts = await get_entry_posts(g.s, entry_id) # Return updated posts list + OOB nav update - from sexp.sexp_components import render_entry_posts_panel, render_entry_posts_nav_oob + from sx.sx_components import render_entry_posts_panel, render_entry_posts_nav_oob va = request.view_args or {} html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year")) nav_oob = render_entry_posts_nav_oob(entry_posts) - return sexp_response(html + nav_oob) + return sx_response(html + nav_oob) return bp diff --git a/events/bp/calendars/routes.py b/events/bp/calendars/routes.py index b10e94a..98eb03b 100644 --- a/events/bp/calendars/routes.py +++ b/events/bp/calendars/routes.py @@ -16,7 +16,7 @@ from shared.browser.app.redis_cacher import cache_page, clear_cache from shared.browser.app.authz import require_admin from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -31,16 +31,16 @@ def register(): @bp.get("/") @cache_page(tag="calendars") async def home(**kwargs): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_calendars_page, render_calendars_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_calendars_page, render_calendars_oob ctx = await get_template_context() if not is_htmx_request(): html = await render_calendars_page(ctx) return await make_response(html) else: - sexp_src = await render_calendars_oob(ctx) - return sexp_response(sexp_src) + sx_src = await render_calendars_oob(ctx) + return sx_response(sx_src) @bp.post("/new/") @@ -63,18 +63,18 @@ def register(): try: await svc_create_calendar(g.s, post_id, name) except Exception as e: - from shared.sexp.jinja_bridge import render as render_comp + from shared.sx.jinja_bridge import render as render_comp return await make_response(render_comp("error-inline", message=str(e)), 422) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_calendars_list_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_calendars_list_panel ctx = await get_template_context() html = render_calendars_list_panel(ctx) # Blog-embedded mode: also update post nav if post_data: from shared.services.entry_associations import get_associated_entries - from sexp.sexp_components import render_post_nav_entries_oob + from sx.sx_components import render_post_nav_entries_oob cals = ( await g.s.execute( @@ -88,5 +88,5 @@ def register(): nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"]) html = html + nav_oob - return sexp_response(html) + return sx_response(html) return bp diff --git a/events/bp/day/admin/routes.py b/events/bp/day/admin/routes.py index 805a7f4..6585a5a 100644 --- a/events/bp/day/admin/routes.py +++ b/events/bp/day/admin/routes.py @@ -6,7 +6,7 @@ from quart import ( from shared.browser.app.authz import require_admin -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -18,14 +18,14 @@ def register(): async def admin(year: int, month: int, day: int, **kwargs): from shared.browser.app.utils.htmx import is_htmx_request - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_day_admin_page, render_day_admin_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_day_admin_page, render_day_admin_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_day_admin_page(tctx) return await make_response(html) else: - sexp_src = await render_day_admin_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_day_admin_oob(tctx) + return sx_response(sx_src) return bp diff --git a/events/bp/day/routes.py b/events/bp/day/routes.py index e25e619..ad78eb4 100644 --- a/events/bp/day/routes.py +++ b/events/bp/day/routes.py @@ -18,7 +18,7 @@ from models.calendars import CalendarSlot # add this import from sqlalchemy import select from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -122,16 +122,16 @@ def register(): - all confirmed + provisional + ordered entries for that day (all users) - pending only for current user/session """ - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_day_page, render_day_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_day_page, render_day_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_day_page(tctx) return await make_response(html) else: - sexp_src = await render_day_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_day_oob(tctx) + return sx_response(sx_src) @bp.get("/w//") async def widget_paginate(widget_domain: str, **kwargs): diff --git a/events/bp/fragments/routes.py b/events/bp/fragments/routes.py index 008c131..6043553 100644 --- a/events/bp/fragments/routes.py +++ b/events/bp/fragments/routes.py @@ -1,6 +1,6 @@ """Events app fragment endpoints. -Exposes sexp fragments at ``/internal/fragments/`` for consumption +Exposes sx fragments at ``/internal/fragments/`` for consumption by other coop apps via the fragment client. """ @@ -31,9 +31,9 @@ def register(): async def get_fragment(fragment_type: str): handler = _handlers.get(fragment_type) if handler is None: - return Response("", status=200, content_type="text/sexp") + return Response("", status=200, content_type="text/sx") result = await handler() - ct = "text/html" if fragment_type in _html_types else "text/sexp" + ct = "text/html" if fragment_type in _html_types else "text/sx" return Response(result, status=200, content_type=ct) # --- container-nav fragment: calendar entries + calendar links ----------- @@ -41,7 +41,7 @@ def register(): async def _container_nav_handler(): from quart import current_app from shared.infrastructure.urls import events_url - from shared.sexp.helpers import sexp_call + from shared.sx.helpers import sx_call container_type = request.args.get("container_type", "page") container_id = int(request.args.get("container_id", 0)) @@ -69,11 +69,11 @@ def register(): date_str = entry.start_at.strftime("%b %d, %Y at %H:%M") if entry.end_at: date_str += f" – {entry.end_at.strftime('%H:%M')}" - parts.append(sexp_call("calendar-entry-nav", + parts.append(sx_call("calendar-entry-nav", href=events_url(entry_path), name=entry.name, date_str=date_str, nav_class=nav_class)) if has_more and paginate_url_base: - parts.append(sexp_call("htmx-sentinel", + parts.append(sx_call("htmx-sentinel", id=f"entries-load-sentinel-{page}", hx_get=f"{paginate_url_base}?page={page + 1}", hx_trigger="intersect once", @@ -87,7 +87,7 @@ def register(): ) for cal in calendars: href = events_url(f"/{post_slug}/{cal.slug}/") - parts.append(sexp_call("calendar-link-nav", + parts.append(sx_call("calendar-link-nav", href=href, name=cal.name, nav_class=nav_class)) if not parts: @@ -123,7 +123,7 @@ def register(): async def _account_nav_item_handler(): from quart import current_app from shared.infrastructure.urls import account_url - from shared.sexp.helpers import sexp_call + from shared.sx.helpers import sx_call styles = current_app.jinja_env.globals.get("styles", {}) nav_class = styles.get("nav_button", "") @@ -135,7 +135,7 @@ def register(): bookings_url = account_url("/bookings/") parts = [] for href, label in [(tickets_url, "tickets"), (bookings_url, "bookings")]: - parts.append(sexp_call("nav-group-link", + parts.append(sx_call("nav-group-link", href=href, hx_select=hx_select, nav_class=nav_class, label=label)) return "(<> " + " ".join(parts) + ")" @@ -169,13 +169,13 @@ def register(): async def _link_card_handler(): from shared.infrastructure.urls import events_url - from shared.sexp.helpers import sexp_call + from shared.sx.helpers import sx_call slug = request.args.get("slug", "") keys_raw = request.args.get("keys", "") - def _event_link_card_sexp(post, cal_names: str) -> str: - return sexp_call("link-card", + def _event_link_card_sx(post, cal_names: str) -> str: + return sx_call("link-card", title=post.title, image=post.feature_image, subtitle=cal_names, link=events_url(f"/{post.slug}")) @@ -193,7 +193,7 @@ def register(): g.s, "page", post.id, ) cal_names = ", ".join(c.name for c in calendars) if calendars else "" - parts.append(_event_link_card_sexp(post, cal_names)) + parts.append(_event_link_card_sx(post, cal_names)) return "\n".join(parts) # Single mode @@ -207,7 +207,7 @@ def register(): g.s, "page", post.id, ) cal_names = ", ".join(c.name for c in calendars) if calendars else "" - return _event_link_card_sexp(post, cal_names) + return _event_link_card_sx(post, cal_names) _handlers["link-card"] = _link_card_handler diff --git a/events/bp/markets/routes.py b/events/bp/markets/routes.py index 07a6962..34b4407 100644 --- a/events/bp/markets/routes.py +++ b/events/bp/markets/routes.py @@ -12,7 +12,7 @@ from .services.markets import ( from shared.browser.app.redis_cacher import cache_page, clear_cache from shared.browser.app.authz import require_admin from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -24,16 +24,16 @@ def register(): @bp.get("/") async def home(**kwargs): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_markets_page, render_markets_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_markets_page, render_markets_oob ctx = await get_template_context() if not is_htmx_request(): html = await render_markets_page(ctx) return await make_response(html) else: - sexp_src = await render_markets_oob(ctx) - return sexp_response(sexp_src) + sx_src = await render_markets_oob(ctx) + return sx_response(sx_src) @bp.post("/new/") @require_admin @@ -52,13 +52,13 @@ def register(): try: await svc_create_market(g.s, post_id, name) except Exception as e: - from shared.sexp.jinja_bridge import render as render_comp + from shared.sx.jinja_bridge import render as render_comp return await make_response(render_comp("error-inline", message=str(e)), 422) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_markets_list_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_markets_list_panel ctx = await get_template_context() - return sexp_response(render_markets_list_panel(ctx)) + return sx_response(render_markets_list_panel(ctx)) @bp.delete("//") @require_admin @@ -68,9 +68,9 @@ def register(): if not deleted: return await make_response("Market not found", 404) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_markets_list_panel + from shared.sx.page import get_template_context + from sx.sx_components import render_markets_list_panel ctx = await get_template_context() - return sexp_response(render_markets_list_panel(ctx)) + return sx_response(render_markets_list_panel(ctx)) return bp diff --git a/events/bp/page/routes.py b/events/bp/page/routes.py index af7c161..8ee7571 100644 --- a/events/bp/page/routes.py +++ b/events/bp/page/routes.py @@ -11,7 +11,7 @@ from __future__ import annotations from quart import Blueprint, g, request, render_template, make_response from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from shared.infrastructure.cart_identity import current_cart_identity from shared.services.registry import services @@ -46,13 +46,13 @@ def register() -> Blueprint: entries, has_more, pending_tickets = await _load_entries(post["id"], page) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_page_summary_page, render_page_summary_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_page_summary_page, render_page_summary_oob ctx = await get_template_context() if is_htmx_request(): - sexp_src = await render_page_summary_oob(ctx, entries, has_more, pending_tickets, {}, page, view) - return sexp_response(sexp_src) + sx_src = await render_page_summary_oob(ctx, entries, has_more, pending_tickets, {}, page, view) + return sx_response(sx_src) else: html = await render_page_summary_page(ctx, entries, has_more, pending_tickets, {}, page, view) return await make_response(html, 200) @@ -65,9 +65,9 @@ def register() -> Blueprint: entries, has_more, pending_tickets = await _load_entries(post["id"], page) - from sexp.sexp_components import render_page_summary_cards - sexp_src = await render_page_summary_cards(entries, has_more, pending_tickets, {}, page, view, post) - return sexp_response(sexp_src) + from sx.sx_components import render_page_summary_cards + sx_src = await render_page_summary_cards(entries, has_more, pending_tickets, {}, page, view, post) + return sx_response(sx_src) @bp.post("/tickets/adjust") async def adjust_ticket(): @@ -106,9 +106,9 @@ def register() -> Blueprint: if ident["session_id"] is not None: frag_params["session_id"] = ident["session_id"] - from sexp.sexp_components import render_ticket_widget + from sx.sx_components import render_ticket_widget widget_html = render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust") mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False) - return sexp_response(widget_html + (mini_html or "")) + return sx_response(widget_html + (mini_html or "")) return bp diff --git a/events/bp/slot/routes.py b/events/bp/slot/routes.py index 66202a4..3ef7779 100644 --- a/events/bp/slot/routes.py +++ b/events/bp/slot/routes.py @@ -24,7 +24,7 @@ from shared.browser.app.utils import ( parse_cost ) from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -75,8 +75,8 @@ def register(): slot = await svc_get_slot(g.s, slot_id) if not slot: return await make_response("Not found", 404) - from sexp.sexp_components import render_slot_main_panel - return sexp_response(render_slot_main_panel(slot, g.calendar)) + from sx.sx_components import render_slot_main_panel + return sx_response(render_slot_main_panel(slot, g.calendar)) @bp.delete("/") @require_admin @@ -84,8 +84,8 @@ def register(): async def slot_delete(slot_id: int, **kwargs): await svc_delete_slot(g.s, slot_id) slots = await svc_list_slots(g.s, g.calendar.id) - from sexp.sexp_components import render_slots_table - return sexp_response(render_slots_table(slots, g.calendar)) + from sx.sx_components import render_slots_table + return sx_response(render_slots_table(slots, g.calendar)) @bp.put("/") @require_admin @@ -166,8 +166,8 @@ def register(): } ), 422 - from sexp.sexp_components import render_slot_main_panel - return sexp_response(render_slot_main_panel(slot, g.calendar, oob=True)) + from sx.sx_components import render_slot_main_panel + return sx_response(render_slot_main_panel(slot, g.calendar, oob=True)) diff --git a/events/bp/slots/routes.py b/events/bp/slots/routes.py index 62fab7d..3e37271 100644 --- a/events/bp/slots/routes.py +++ b/events/bp/slots/routes.py @@ -20,7 +20,7 @@ from shared.browser.app.utils import ( parse_cost ) from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -45,16 +45,16 @@ def register(): @bp.get("/") async def get(**kwargs): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_slots_page, render_slots_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_slots_page, render_slots_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_slots_page(tctx) return await make_response(html) else: - sexp_src = await render_slots_oob(tctx) - return sexp_response(sexp_src) + sx_src = await render_slots_oob(tctx) + return sx_response(sx_src) @bp.post("/") @@ -129,8 +129,8 @@ def register(): # Success → re-render the slots table slots = await svc_list_slots(g.s, g.calendar.id) - from sexp.sexp_components import render_slots_table - return sexp_response(render_slots_table(slots, g.calendar)) + from sx.sx_components import render_slots_table + return sx_response(render_slots_table(slots, g.calendar)) @bp.get("/add") diff --git a/events/bp/ticket_admin/routes.py b/events/bp/ticket_admin/routes.py index 772b1f4..80cfd7e 100644 --- a/events/bp/ticket_admin/routes.py +++ b/events/bp/ticket_admin/routes.py @@ -20,7 +20,7 @@ from sqlalchemy.orm import selectinload from models.calendars import CalendarEntry, Ticket, TicketType from shared.browser.app.authz import require_admin from shared.browser.app.redis_cacher import clear_cache -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from ..tickets.services.tickets import ( get_ticket_by_code, @@ -71,16 +71,16 @@ def register() -> Blueprint: "reserved": reserved or 0, } - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_ticket_admin_page, render_ticket_admin_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_ticket_admin_page, render_ticket_admin_oob ctx = await get_template_context() if not is_htmx_request(): html = await render_ticket_admin_page(ctx, tickets, stats) return await make_response(html, 200) else: - sexp_src = await render_ticket_admin_oob(ctx, tickets, stats) - return sexp_response(sexp_src) + sx_src = await render_ticket_admin_oob(ctx, tickets, stats) + return sx_response(sx_src) @bp.get("/entry//") @require_admin @@ -101,9 +101,9 @@ def register() -> Blueprint: tickets = await get_tickets_for_entry(g.s, entry_id) - from sexp.sexp_components import render_entry_tickets_admin + from sx.sx_components import render_entry_tickets_admin html = render_entry_tickets_admin(entry, tickets) - return sexp_response(html) + return sx_response(html) @bp.get("/lookup/") @require_admin @@ -117,11 +117,11 @@ def register() -> Blueprint: ) ticket = await get_ticket_by_code(g.s, code) - from sexp.sexp_components import render_lookup_result + from sx.sx_components import render_lookup_result if not ticket: - return sexp_response(render_lookup_result(None, "Ticket not found")) + return sx_response(render_lookup_result(None, "Ticket not found")) - return sexp_response(render_lookup_result(ticket, None)) + return sx_response(render_lookup_result(ticket, None)) @bp.post("//checkin/") @require_admin @@ -130,11 +130,11 @@ def register() -> Blueprint: """Check in a ticket by its code.""" success, error = await checkin_ticket(g.s, code) - from sexp.sexp_components import render_checkin_result + from sx.sx_components import render_checkin_result if not success: - return sexp_response(render_checkin_result(False, error, None)) + return sx_response(render_checkin_result(False, error, None)) ticket = await get_ticket_by_code(g.s, code) - return sexp_response(render_checkin_result(True, None, ticket)) + return sx_response(render_checkin_result(True, None, ticket)) return bp diff --git a/events/bp/ticket_type/routes.py b/events/bp/ticket_type/routes.py index 5e1832c..93c1421 100644 --- a/events/bp/ticket_type/routes.py +++ b/events/bp/ticket_type/routes.py @@ -17,7 +17,7 @@ from ..ticket_types.services.tickets import ( list_ticket_types as svc_list_ticket_types, ) from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -67,9 +67,9 @@ def register(): if not ticket_type: return await make_response("Not found", 404) - from sexp.sexp_components import render_ticket_type_main_panel + from sx.sx_components import render_ticket_type_main_panel va = request.view_args or {} - return sexp_response(render_ticket_type_main_panel( + return sx_response(render_ticket_type_main_panel( ticket_type, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), )) @@ -134,9 +134,9 @@ def register(): return await make_response("Not found", 404) # Return updated view with OOB flag - from sexp.sexp_components import render_ticket_type_main_panel + from sx.sx_components import render_ticket_type_main_panel va = request.view_args or {} - return sexp_response(render_ticket_type_main_panel( + return sx_response(render_ticket_type_main_panel( ticket_type, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), oob=True, @@ -153,9 +153,9 @@ def register(): # Re-render the ticket types list ticket_types = await svc_list_ticket_types(g.s, g.entry.id) - from sexp.sexp_components import render_ticket_types_table + from sx.sx_components import render_ticket_types_table va = request.view_args or {} - return sexp_response(render_ticket_types_table( + return sx_response(render_ticket_types_table( ticket_types, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), )) diff --git a/events/bp/ticket_types/routes.py b/events/bp/ticket_types/routes.py index 1d88bca..a5c3380 100644 --- a/events/bp/ticket_types/routes.py +++ b/events/bp/ticket_types/routes.py @@ -15,7 +15,7 @@ from .services.tickets import ( from ..ticket_type.routes import register as register_ticket_type from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -110,9 +110,9 @@ def register(): # Success → re-render the ticket types table ticket_types = await svc_list_ticket_types(g.s, g.entry.id) - from sexp.sexp_components import render_ticket_types_table + from sx.sx_components import render_ticket_types_table va = request.view_args or {} - return sexp_response(render_ticket_types_table( + return sx_response(render_ticket_types_table( ticket_types, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"), )) diff --git a/events/bp/tickets/routes.py b/events/bp/tickets/routes.py index 3a06f9d..43f56d3 100644 --- a/events/bp/tickets/routes.py +++ b/events/bp/tickets/routes.py @@ -20,7 +20,7 @@ from sqlalchemy.orm import selectinload from models.calendars import CalendarEntry from shared.infrastructure.cart_identity import current_cart_identity from shared.browser.app.redis_cacher import clear_cache -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from .services.tickets import ( create_ticket, @@ -51,16 +51,16 @@ def register() -> Blueprint: session_id=ident["session_id"], ) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_tickets_page, render_tickets_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_tickets_page, render_tickets_oob ctx = await get_template_context() if not is_htmx_request(): html = await render_tickets_page(ctx, tickets) return await make_response(html, 200) else: - sexp_src = await render_tickets_oob(ctx, tickets) - return sexp_response(sexp_src) + sx_src = await render_tickets_oob(ctx, tickets) + return sx_response(sx_src) @bp.get("//") async def ticket_detail(code: str): @@ -82,16 +82,16 @@ def register() -> Blueprint: else: return await make_response("Ticket not found", 404) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_ticket_detail_page, render_ticket_detail_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_ticket_detail_page, render_ticket_detail_oob ctx = await get_template_context() if not is_htmx_request(): html = await render_ticket_detail_page(ctx, ticket) return await make_response(html, 200) else: - sexp_src = await render_ticket_detail_oob(ctx, ticket) - return sexp_response(sexp_src) + sx_src = await render_ticket_detail_oob(ctx, ticket) + return sx_response(sx_src) @bp.post("/buy/") @clear_cache(tag="calendars", tag_scope="all") @@ -182,8 +182,8 @@ def register() -> Blueprint: summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO() cart_count = summary.count + summary.calendar_count + summary.ticket_count - from sexp.sexp_components import render_buy_result - return sexp_response(render_buy_result(entry, created, remaining, cart_count)) + from sx.sx_components import render_buy_result + return sx_response(render_buy_result(entry, created, remaining, cart_count)) @bp.post("/adjust/") @clear_cache(tag="calendars", tag_scope="all") @@ -305,8 +305,8 @@ def register() -> Blueprint: summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO() cart_count = summary.count + summary.calendar_count + summary.ticket_count - from sexp.sexp_components import render_adjust_response - return sexp_response(render_adjust_response( + from sx.sx_components import render_adjust_response + return sx_response(render_adjust_response( entry, ticket_remaining, ticket_sold_count, user_ticket_count, user_ticket_counts_by_type, cart_count, )) diff --git a/events/sexp/__init__.py b/events/sx/__init__.py similarity index 100% rename from events/sexp/__init__.py rename to events/sx/__init__.py diff --git a/events/sexp/admin.sexpr b/events/sx/admin.sx similarity index 100% rename from events/sexp/admin.sexpr rename to events/sx/admin.sx diff --git a/events/sexp/calendar.sexpr b/events/sx/calendar.sx similarity index 100% rename from events/sexp/calendar.sexpr rename to events/sx/calendar.sx diff --git a/events/sexp/day.sexpr b/events/sx/day.sx similarity index 100% rename from events/sexp/day.sexpr rename to events/sx/day.sx diff --git a/events/sexp/entries.sexpr b/events/sx/entries.sx similarity index 100% rename from events/sexp/entries.sexpr rename to events/sx/entries.sx diff --git a/events/sexp/forms.sexpr b/events/sx/forms.sx similarity index 100% rename from events/sexp/forms.sexpr rename to events/sx/forms.sx diff --git a/events/sexp/fragments.sexpr b/events/sx/fragments.sx similarity index 100% rename from events/sexp/fragments.sexpr rename to events/sx/fragments.sx diff --git a/events/sexp/header.sexpr b/events/sx/header.sx similarity index 100% rename from events/sexp/header.sexpr rename to events/sx/header.sx diff --git a/events/sexp/markets.sexpr b/events/sx/markets.sx similarity index 100% rename from events/sexp/markets.sexpr rename to events/sx/markets.sx diff --git a/events/sexp/page.sexpr b/events/sx/page.sx similarity index 100% rename from events/sexp/page.sexpr rename to events/sx/page.sx diff --git a/events/sexp/payments.sexpr b/events/sx/payments.sx similarity index 100% rename from events/sexp/payments.sexpr rename to events/sx/payments.sx diff --git a/events/sexp/sexp_components.py b/events/sx/sx_components.py similarity index 80% rename from events/sexp/sexp_components.py rename to events/sx/sx_components.py index d18f5dd..1bb6931 100644 --- a/events/sexp/sexp_components.py +++ b/events/sx/sx_components.py @@ -11,17 +11,17 @@ import os from typing import Any from markupsafe import escape -from shared.sexp.jinja_bridge import load_service_components -from shared.sexp.helpers import ( - call_url, get_asset_url, sexp_call, - root_header_sexp, post_header_sexp, post_admin_header_sexp, - oob_header_sexp, header_child_sexp, - full_page_sexp, oob_page_sexp, - search_mobile_sexp, search_desktop_sexp, +from shared.sx.jinja_bridge import load_service_components +from shared.sx.helpers import ( + call_url, get_asset_url, sx_call, + root_header_sx, post_header_sx, post_admin_header_sx, + oob_header_sx, header_child_sx, + full_page_sx, oob_page_sx, + search_mobile_sx, search_desktop_sx, ) -from shared.sexp.parser import SexpExpr +from shared.sx.parser import SxExpr -# Load events-specific .sexpr components at import time +# Load events-specific .sx components at import time load_service_components(os.path.dirname(os.path.dirname(__file__))) @@ -32,7 +32,7 @@ load_service_components(os.path.dirname(os.path.dirname(__file__))) # --------------------------------------------------------------------------- -# Post header helpers — thin wrapper over shared post_header_sexp +# Post header helpers — thin wrapper over shared post_header_sx # --------------------------------------------------------------------------- def _clear_oob(*ids: str) -> str: @@ -81,12 +81,12 @@ async def _ensure_container_nav(ctx: dict) -> dict: return {**ctx, "container_nav": events_nav + market_nav} -def _post_header_sexp(ctx: dict, *, oob: bool = False) -> str: - """Build the post-level header row — delegates to shared sexp helper.""" - return post_header_sexp(ctx, oob=oob) +def _post_header_sx(ctx: dict, *, oob: bool = False) -> str: + """Build the post-level header row — delegates to shared sx helper.""" + return post_header_sx(ctx, oob=oob) -def _post_nav_sexp(ctx: dict) -> str: +def _post_nav_sx(ctx: dict) -> str: """Post desktop nav: calendar links + container nav (markets, etc.).""" from quart import url_for, g @@ -100,7 +100,7 @@ def _post_nav_sexp(ctx: dict) -> str: cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "") href = url_for("calendar.get", calendar_slug=cal_slug) is_sel = (cal_slug == current_cal_slug) - parts.append(sexp_call("nav-link", href=href, icon="fa fa-calendar", + parts.append(sx_call("nav-link", href=href, icon="fa fa-calendar", label=cal_name, select_colours=select_colours, is_selected=is_sel)) # Container nav fragments (markets, etc.) @@ -139,13 +139,13 @@ def _post_nav_sexp(ctx: dict) -> str: # Calendars header # --------------------------------------------------------------------------- -def _calendars_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the calendars section header row.""" from quart import url_for link_href = url_for("calendars.home") - return sexp_call("menu-row-sx", id="calendars-row", level=3, + return sx_call("menu-row-sx", id="calendars-row", level=3, link_href=link_href, - link_label_content=SexpExpr(sexp_call("events-calendars-label")), + link_label_content=SxExpr(sx_call("events-calendars-label")), child_id="calendars-header-child", oob=oob) @@ -153,7 +153,7 @@ def _calendars_header_sexp(ctx: dict, *, oob: bool = False) -> str: # Calendar header # --------------------------------------------------------------------------- -def _calendar_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build a single calendar's header row.""" from quart import url_for calendar = ctx.get("calendar") @@ -164,18 +164,18 @@ def _calendar_header_sexp(ctx: dict, *, oob: bool = False) -> str: cal_desc = getattr(calendar, "description", "") or "" link_href = url_for("calendar.get", calendar_slug=cal_slug) - label_html = sexp_call("events-calendar-label", + label_html = sx_call("events-calendar-label", name=cal_name, description=cal_desc) # Desktop nav: slots + admin - nav_html = _calendar_nav_sexp(ctx) + nav_html = _calendar_nav_sx(ctx) - return sexp_call("menu-row-sx", id="calendar-row", level=3, - link_href=link_href, link_label_content=SexpExpr(label_html), - nav=SexpExpr(nav_html) if nav_html else None, child_id="calendar-header-child", oob=oob) + return sx_call("menu-row-sx", id="calendar-row", level=3, + link_href=link_href, link_label_content=SxExpr(label_html), + nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-header-child", oob=oob) -def _calendar_nav_sexp(ctx: dict) -> str: +def _calendar_nav_sx(ctx: dict) -> str: """Calendar desktop nav: Slots + admin link.""" from quart import url_for calendar = ctx.get("calendar") @@ -188,11 +188,11 @@ def _calendar_nav_sexp(ctx: dict) -> str: parts = [] slots_href = url_for("calendar.slots.get", calendar_slug=cal_slug) - parts.append(sexp_call("nav-link", href=slots_href, icon="fa fa-clock", + parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock", label="Slots", select_colours=select_colours)) if is_admin: admin_href = url_for("calendar.admin.admin", calendar_slug=cal_slug) - parts.append(sexp_call("nav-link", href=admin_href, icon="fa fa-cog", + parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog", select_colours=select_colours)) return "".join(parts) @@ -201,7 +201,7 @@ def _calendar_nav_sexp(ctx: dict) -> str: # Day header # --------------------------------------------------------------------------- -def _day_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _day_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build day detail header row.""" from quart import url_for calendar = ctx.get("calendar") @@ -219,17 +219,17 @@ def _day_header_sexp(ctx: dict, *, oob: bool = False) -> str: month=day_date.month, day=day_date.day, ) - label_html = sexp_call("events-day-label", + label_html = sx_call("events-day-label", date_str=day_date.strftime("%A %d %B %Y")) - nav_html = _day_nav_sexp(ctx) + nav_html = _day_nav_sx(ctx) - return sexp_call("menu-row-sx", id="day-row", level=4, - link_href=link_href, link_label_content=SexpExpr(label_html), - nav=SexpExpr(nav_html) if nav_html else None, child_id="day-header-child", oob=oob) + return sx_call("menu-row-sx", id="day-row", level=4, + link_href=link_href, link_label_content=SxExpr(label_html), + nav=SxExpr(nav_html) if nav_html else None, child_id="day-header-child", oob=oob) -def _day_nav_sexp(ctx: dict) -> str: +def _day_nav_sx(ctx: dict) -> str: """Day desktop nav: confirmed entries scrolling menu + admin link.""" from quart import url_for calendar = ctx.get("calendar") @@ -256,11 +256,11 @@ def _day_nav_sexp(ctx: dict) -> str: ) start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - entry_links.append(sexp_call("events-day-entry-link", + entry_links.append(sx_call("events-day-entry-link", href=href, name=entry.name, time_str=f"{start}{end}")) inner = "".join(entry_links) - parts.append(sexp_call("events-day-entries-nav", inner=SexpExpr(inner))) + parts.append(sx_call("events-day-entries-nav", inner=SxExpr(inner))) if is_admin and day_date: admin_href = url_for( @@ -270,7 +270,7 @@ def _day_nav_sexp(ctx: dict) -> str: month=day_date.month, day=day_date.day, ) - parts.append(sexp_call("nav-link", href=admin_href, icon="fa fa-cog")) + parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog")) return "".join(parts) @@ -278,7 +278,7 @@ def _day_nav_sexp(ctx: dict) -> str: # Day admin header # --------------------------------------------------------------------------- -def _day_admin_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build day admin header row.""" from quart import url_for calendar = ctx.get("calendar") @@ -296,7 +296,7 @@ def _day_admin_header_sexp(ctx: dict, *, oob: bool = False) -> str: month=day_date.month, day=day_date.day, ) - return sexp_call("menu-row-sx", id="day-admin-row", level=5, + return sx_call("menu-row-sx", id="day-admin-row", level=5, link_href=link_href, link_label="admin", icon="fa fa-cog", child_id="day-admin-header-child", oob=oob) @@ -305,7 +305,7 @@ def _day_admin_header_sexp(ctx: dict, *, oob: bool = False) -> str: # Calendar admin header # --------------------------------------------------------------------------- -def _calendar_admin_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build calendar admin header row with nav links.""" from quart import url_for calendar = ctx.get("calendar") @@ -319,26 +319,26 @@ def _calendar_admin_header_sexp(ctx: dict, *, oob: bool = False) -> str: ("calendar.admin.calendar_description_edit", "description"), ]: href = url_for(endpoint, calendar_slug=cal_slug) - nav_parts.append(sexp_call("nav-link", href=href, label=label, + nav_parts.append(sx_call("nav-link", href=href, label=label, select_colours=select_colours)) nav_html = "".join(nav_parts) - return sexp_call("menu-row-sx", id="calendar-admin-row", level=4, + return sx_call("menu-row-sx", id="calendar-admin-row", level=4, link_label="admin", icon="fa fa-cog", - nav=SexpExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob) + nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob) # --------------------------------------------------------------------------- # Markets header # --------------------------------------------------------------------------- -def _markets_header_sexp(ctx: dict, *, oob: bool = False) -> str: +def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the markets section header row.""" from quart import url_for link_href = url_for("markets.home") - return sexp_call("menu-row-sx", id="markets-row", level=3, + return sx_call("menu-row-sx", id="markets-row", level=3, link_href=link_href, - link_label_content=SexpExpr(sexp_call("events-markets-label")), + link_label_content=SxExpr(sx_call("events-markets-label")), child_id="markets-header-child", oob=oob) @@ -346,7 +346,7 @@ def _markets_header_sexp(ctx: dict, *, oob: bool = False) -> str: # Calendars main panel # --------------------------------------------------------------------------- -def _calendars_main_panel_sexp(ctx: dict) -> str: +def _calendars_main_panel_sx(ctx: dict) -> str: """Render the calendars list + create form panel.""" from quart import url_for rights = ctx.get("rights") or {} @@ -361,15 +361,15 @@ def _calendars_main_panel_sexp(ctx: dict) -> str: form_html = "" if can_create: create_url = url_for("calendars.create_calendar") - form_html = sexp_call("events-calendars-create-form", + form_html = sx_call("events-calendars-create-form", create_url=create_url, csrf=csrf) - list_html = _calendars_list_sexp(ctx, calendars) - return sexp_call("events-calendars-panel", - form=SexpExpr(form_html), list=SexpExpr(list_html)) + list_html = _calendars_list_sx(ctx, calendars) + return sx_call("events-calendars-panel", + form=SxExpr(form_html), list=SxExpr(list_html)) -def _calendars_list_sexp(ctx: dict, calendars: list) -> str: +def _calendars_list_sx(ctx: dict, calendars: list) -> str: """Render the calendars list items.""" from quart import url_for from shared.utils import route_prefix @@ -378,7 +378,7 @@ def _calendars_list_sexp(ctx: dict, calendars: list) -> str: prefix = route_prefix() if not calendars: - return sexp_call("events-calendars-empty") + return sx_call("events-calendars-empty") parts = [] for cal in calendars: @@ -387,7 +387,7 @@ def _calendars_list_sexp(ctx: dict, calendars: list) -> str: href = prefix + url_for("calendar.get", calendar_slug=cal_slug) del_url = url_for("calendar.delete", calendar_slug=cal_slug) csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' - parts.append(sexp_call("events-calendars-item", + parts.append(sx_call("events-calendars-item", href=href, cal_name=cal_name, cal_slug=cal_slug, del_url=del_url, csrf_hdr=csrf_hdr)) return "".join(parts) @@ -435,10 +435,10 @@ def _calendar_main_panel_html(ctx: dict) -> str: ("\u2039", prev_month_year, prev_month), ]: href = nav_link(yr, mn) - nav_arrows.append(sexp_call("events-calendar-nav-arrow", + nav_arrows.append(sx_call("events-calendar-nav-arrow", pill_cls=pill_cls, href=href, label=label)) - nav_arrows.append(sexp_call("events-calendar-month-label", + nav_arrows.append(sx_call("events-calendar-month-label", month_name=month_name, year=str(year))) for label, yr, mn in [ @@ -446,11 +446,11 @@ def _calendar_main_panel_html(ctx: dict) -> str: ("\u00bb", next_year, month), ]: href = nav_link(yr, mn) - nav_arrows.append(sexp_call("events-calendar-nav-arrow", + nav_arrows.append(sx_call("events-calendar-nav-arrow", pill_cls=pill_cls, href=href, label=label)) # Weekday headers - wd_html = "".join(sexp_call("events-calendar-weekday", name=wd) for wd in weekday_names) + wd_html = "".join(sx_call("events-calendar-weekday", name=wd) for wd in weekday_names) # Day cells cells = [] @@ -480,9 +480,9 @@ def _calendar_main_panel_html(ctx: dict) -> str: calendar_slug=cal_slug, year=day_date.year, month=day_date.month, day=day_date.day, ) - day_short_html = sexp_call("events-calendar-day-short", + day_short_html = sx_call("events-calendar-day-short", day_str=day_date.strftime("%a")) - day_num_html = sexp_call("events-calendar-day-num", + day_num_html = sx_call("events-calendar-day-num", pill_cls=pill_cls, href=day_href, num=str(day_date.day)) @@ -500,20 +500,20 @@ def _calendar_main_panel_html(ctx: dict) -> str: else: bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700" state_label = (e.state or "pending").replace("_", " ") - entry_badges.append(sexp_call("events-calendar-entry-badge", + entry_badges.append(sx_call("events-calendar-entry-badge", bg_cls=bg_cls, name=e.name, state_label=state_label)) badges_html = "".join(entry_badges) - cells.append(sexp_call("events-calendar-cell", - cell_cls=cell_cls, day_short=SexpExpr(day_short_html), - day_num=SexpExpr(day_num_html), badges=SexpExpr(badges_html))) + cells.append(sx_call("events-calendar-cell", + cell_cls=cell_cls, day_short=SxExpr(day_short_html), + day_num=SxExpr(day_num_html), badges=SxExpr(badges_html))) cells_html = "".join(cells) arrows_html = "".join(nav_arrows) - return sexp_call("events-calendar-grid", - arrows=SexpExpr(arrows_html), weekdays=SexpExpr(wd_html), - cells=SexpExpr(cells_html)) + return sx_call("events-calendar-grid", + arrows=SxExpr(arrows_html), weekdays=SxExpr(wd_html), + cells=SxExpr(cells_html)) # --------------------------------------------------------------------------- @@ -543,7 +543,7 @@ def _day_main_panel_html(ctx: dict) -> str: if day_entries: rows_html = "".join(_day_row_html(ctx, entry) for entry in day_entries) else: - rows_html = sexp_call("events-day-empty-row") + rows_html = sx_call("events-day-empty-row") add_url = url_for( "calendar.day.calendar_entries.add_form", @@ -551,8 +551,8 @@ def _day_main_panel_html(ctx: dict) -> str: day=day, month=month, year=year, ) - return sexp_call("events-day-table", - list_container=list_container, rows=SexpExpr(rows_html), + return sx_call("events-day-table", + list_container=list_container, rows=SxExpr(rows_html), pre_action=pre_action, add_url=add_url) @@ -575,7 +575,7 @@ def _day_row_html(ctx: dict, entry) -> str: ) # Name - name_html = sexp_call("events-day-row-name", + name_html = sx_call("events-day-row-name", href=entry_href, pill_cls=pill_cls, name=entry.name) # Slot/Time @@ -584,41 +584,41 @@ def _day_row_html(ctx: dict, entry) -> str: slot_href = url_for("calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=slot.id) time_start = slot.time_start.strftime("%H:%M") if slot.time_start else "" time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else "" - slot_html = sexp_call("events-day-row-slot", + slot_html = sx_call("events-day-row-slot", href=slot_href, pill_cls=pill_cls, slot_name=slot.name, time_str=f"({time_start}{time_end})") else: start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - slot_html = sexp_call("events-day-row-time", start=start, end=end) + slot_html = sx_call("events-day-row-time", start=start, end=end) # State state = getattr(entry, "state", "pending") or "pending" state_badge = _entry_state_badge_html(state) - state_td = sexp_call("events-day-row-state", - state_id=f"entry-state-{entry.id}", badge=SexpExpr(state_badge)) + state_td = sx_call("events-day-row-state", + state_id=f"entry-state-{entry.id}", badge=SxExpr(state_badge)) # Cost cost = getattr(entry, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" - cost_td = sexp_call("events-day-row-cost", cost_str=cost_str) + cost_td = sx_call("events-day-row-cost", cost_str=cost_str) # Tickets tp = getattr(entry, "ticket_price", None) if tp is not None: tc = getattr(entry, "ticket_count", None) tc_str = f"{tc} tickets" if tc is not None else "Unlimited" - tickets_td = sexp_call("events-day-row-tickets", + tickets_td = sx_call("events-day-row-tickets", price_str=f"\u00a3{tp:.2f}", count_str=tc_str) else: - tickets_td = sexp_call("events-day-row-no-tickets") + tickets_td = sx_call("events-day-row-no-tickets") - actions_td = sexp_call("events-day-row-actions") + actions_td = sx_call("events-day-row-actions") - return sexp_call("events-day-row", - tr_cls=tr_cls, name=SexpExpr(name_html), slot=SexpExpr(slot_html), - state=SexpExpr(state_td), cost=SexpExpr(cost_td), - tickets=SexpExpr(tickets_td), actions=SexpExpr(actions_td)) + return sx_call("events-day-row", + tr_cls=tr_cls, name=SxExpr(name_html), slot=SxExpr(slot_html), + state=SxExpr(state_td), cost=SxExpr(cost_td), + tickets=SxExpr(tickets_td), actions=SxExpr(actions_td)) def _entry_state_badge_html(state: str) -> str: @@ -632,7 +632,7 @@ def _entry_state_badge_html(state: str) -> str: } cls = state_classes.get(state, "bg-stone-100 text-stone-700") label = state.replace("_", " ").capitalize() - return sexp_call("events-state-badge", cls=cls, label=label) + return sx_call("events-state-badge", cls=cls, label=label) # --------------------------------------------------------------------------- @@ -641,7 +641,7 @@ def _entry_state_badge_html(state: str) -> str: def _day_admin_main_panel_html(ctx: dict) -> str: """Render day admin panel (placeholder nav).""" - return sexp_call("events-day-admin-panel") + return sx_call("events-day-admin-panel") # --------------------------------------------------------------------------- @@ -663,15 +663,15 @@ def _calendar_admin_main_panel_html(ctx: dict) -> str: desc_edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug) description_html = _calendar_description_display_html(calendar, desc_edit_url) - return sexp_call("events-calendar-admin-panel", - description_content=SexpExpr(description_html), csrf=csrf, + return sx_call("events-calendar-admin-panel", + description_content=SxExpr(description_html), csrf=csrf, description=desc) def _calendar_description_display_html(calendar, edit_url: str) -> str: """Render calendar description display with edit button.""" desc = getattr(calendar, "description", "") or "" - return sexp_call("events-calendar-description-display", + return sx_call("events-calendar-description-display", description=desc, edit_url=edit_url) @@ -693,12 +693,12 @@ def _markets_main_panel_html(ctx: dict) -> str: form_html = "" if can_create: create_url = url_for("markets.create_market") - form_html = sexp_call("events-markets-create-form", + form_html = sx_call("events-markets-create-form", create_url=create_url, csrf=csrf) list_html = _markets_list_html(ctx, markets) - return sexp_call("events-markets-panel", - form=SexpExpr(form_html), list=SexpExpr(list_html)) + return sx_call("events-markets-panel", + form=SxExpr(form_html), list=SxExpr(list_html)) def _markets_list_html(ctx: dict, markets: list) -> str: @@ -710,7 +710,7 @@ def _markets_list_html(ctx: dict, markets: list) -> str: slug = post.get("slug", "") if not markets: - return sexp_call("events-markets-empty") + return sx_call("events-markets-empty") parts = [] for m in markets: @@ -719,7 +719,7 @@ def _markets_list_html(ctx: dict, markets: list) -> str: market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/") del_url = url_for("markets.delete_market", market_slug=m_slug) csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}' - parts.append(sexp_call("events-markets-item", + parts.append(sx_call("events-markets-item", href=market_href, market_name=m_name, market_slug=m_slug, del_url=del_url, csrf_hdr=csrf_hdr)) @@ -740,7 +740,7 @@ def _ticket_state_badge_html(state: str) -> str: } cls = cls_map.get(state, "bg-stone-100 text-stone-700") label = (state or "").replace("_", " ").capitalize() - return sexp_call("events-state-badge", cls=cls, label=label) + return sx_call("events-state-badge", cls=cls, label=label) # --------------------------------------------------------------------------- @@ -767,7 +767,7 @@ def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: if entry.end_at: time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}" - ticket_cards.append(sexp_call("events-ticket-card", + ticket_cards.append(sx_call("events-ticket-card", href=href, entry_name=entry_name, type_name=tt.name if tt else None, time_str=time_str or None, @@ -776,9 +776,9 @@ def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: code_prefix=ticket.code[:8])) cards_html = "".join(ticket_cards) - return sexp_call("events-tickets-panel", + return sx_call("events-tickets-panel", list_container=_list_container(ctx), - has_tickets=bool(tickets), cards=SexpExpr(cards_html)) + has_tickets=bool(tickets), cards=SxExpr(cards_html)) # --------------------------------------------------------------------------- @@ -821,10 +821,10 @@ def _ticket_detail_panel_html(ctx: dict, ticket) -> str: "}})()" ) - return sexp_call("events-ticket-detail", + return sx_call("events-ticket-detail", list_container=_list_container(ctx), back_href=back_href, header_bg=header_bg, entry_name=entry_name, - badge=SexpExpr(badge), type_name=tt.name if tt else None, + badge=SxExpr(badge), type_name=tt.name if tt else None, code=code, time_date=time_date, time_range=time_range, cal_name=cal.name if cal else None, type_desc=tt_desc, checkin_str=checkin_str, @@ -852,7 +852,7 @@ def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: ]: val = stats.get(key, 0) lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500" - stats_html += sexp_call("events-ticket-admin-stat", + stats_html += sx_call("events-ticket-admin-stat", border=border, bg=bg, text_cls=text_cls, label_cls=lbl_cls, value=str(val), label=label) @@ -866,32 +866,32 @@ def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: date_html = "" if entry and entry.start_at: - date_html = sexp_call("events-ticket-admin-date", + date_html = sx_call("events-ticket-admin-date", date_str=entry.start_at.strftime("%d %b %Y, %H:%M")) action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) - action_html = sexp_call("events-ticket-admin-checkin-form", + action_html = sx_call("events-ticket-admin-checkin-form", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": checked_in_at = getattr(ticket, "checked_in_at", None) t_str = checked_in_at.strftime("%H:%M") if checked_in_at else "" - action_html = sexp_call("events-ticket-admin-checked-in", + action_html = sx_call("events-ticket-admin-checked-in", time_str=t_str) - rows_html += sexp_call("events-ticket-admin-row", + rows_html += sx_call("events-ticket-admin-row", code=code, code_short=code[:12] + "...", entry_name=entry.name if entry else "\u2014", - date=SexpExpr(date_html), + date=SxExpr(date_html), type_name=tt.name if tt else "\u2014", badge=_ticket_state_badge_html(state), - action=SexpExpr(action_html)) + action=SxExpr(action_html)) - return sexp_call("events-ticket-admin-panel", - list_container=_list_container(ctx), stats=SexpExpr(stats_html), + return sx_call("events-ticket-admin-panel", + list_container=_list_container(ctx), stats=SxExpr(stats_html), lookup_url=lookup_url, has_tickets=bool(tickets), - rows=SexpExpr(rows_html)) + rows=SxExpr(rows_html)) # --------------------------------------------------------------------------- @@ -916,36 +916,36 @@ def _entry_card_html(entry, page_info: dict, pending_tickets: dict, # Title (linked or plain) if entry_href: - title_html = sexp_call("events-entry-title-linked", + title_html = sx_call("events-entry-title-linked", href=entry_href, name=entry.name) else: - title_html = sexp_call("events-entry-title-plain", name=entry.name) + title_html = sx_call("events-entry-title-plain", name=entry.name) # Badges badges_html = "" if page_title and (not is_page_scoped or page_title != (post or {}).get("title")): page_href = events_url_fn(f"/{page_slug}/") - badges_html += sexp_call("events-entry-page-badge", + badges_html += sx_call("events-entry-page-badge", href=page_href, title=page_title) cal_name = getattr(entry, "calendar_name", "") if cal_name: - badges_html += sexp_call("events-entry-cal-badge", name=cal_name) + badges_html += sx_call("events-entry-cal-badge", name=cal_name) # Time line time_parts = "" if day_href and not is_page_scoped: - time_parts += sexp_call("events-entry-time-linked", + time_parts += sx_call("events-entry-time-linked", href=day_href, date_str=entry.start_at.strftime("%a %-d %b")) elif not is_page_scoped: - time_parts += sexp_call("events-entry-time-plain", + time_parts += sx_call("events-entry-time-plain", date_str=entry.start_at.strftime("%a %-d %b")) time_parts += entry.start_at.strftime("%H:%M") if entry.end_at: time_parts += f' \u2013 {entry.end_at.strftime("%H:%M")}' cost = getattr(entry, "cost", None) - cost_html = sexp_call("events-entry-cost", + cost_html = sx_call("events-entry-cost", cost=f"£{cost:.2f}") if cost else "" # Ticket widget @@ -953,13 +953,13 @@ def _entry_card_html(entry, page_info: dict, pending_tickets: dict, widget_html = "" if tp is not None: qty = pending_tickets.get(entry.id, 0) - widget_html = sexp_call("events-entry-widget-wrapper", + widget_html = sx_call("events-entry-widget-wrapper", widget=_ticket_widget_html(entry, qty, ticket_url, ctx={})) - return sexp_call("events-entry-card", - title=SexpExpr(title_html), badges=SexpExpr(badges_html), - time_parts=SexpExpr(time_parts), cost=SexpExpr(cost_html), - widget=SexpExpr(widget_html)) + return sx_call("events-entry-card", + title=SxExpr(title_html), badges=SxExpr(badges_html), + time_parts=SxExpr(time_parts), cost=SxExpr(cost_html), + widget=SxExpr(widget_html)) def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, @@ -980,25 +980,25 @@ def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, # Title if entry_href: - title_html = sexp_call("events-entry-title-tile-linked", + title_html = sx_call("events-entry-title-tile-linked", href=entry_href, name=entry.name) else: - title_html = sexp_call("events-entry-title-tile-plain", name=entry.name) + title_html = sx_call("events-entry-title-tile-plain", name=entry.name) # Badges badges_html = "" if page_title and (not is_page_scoped or page_title != (post or {}).get("title")): page_href = events_url_fn(f"/{page_slug}/") - badges_html += sexp_call("events-entry-page-badge", + badges_html += sx_call("events-entry-page-badge", href=page_href, title=page_title) cal_name = getattr(entry, "calendar_name", "") if cal_name: - badges_html += sexp_call("events-entry-cal-badge", name=cal_name) + badges_html += sx_call("events-entry-cal-badge", name=cal_name) # Time time_html = "" if day_href: - time_html += sexp_call("events-entry-time-linked", + time_html += sx_call("events-entry-time-linked", href=day_href, date_str=entry.start_at.strftime("%a %-d %b")).replace(" · ", "") else: @@ -1008,7 +1008,7 @@ def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, time_html += f' \u2013 {entry.end_at.strftime("%H:%M")}' cost = getattr(entry, "cost", None) - cost_html = sexp_call("events-entry-cost", + cost_html = sx_call("events-entry-cost", cost=f"£{cost:.2f}") if cost else "" # Ticket widget @@ -1016,13 +1016,13 @@ def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, widget_html = "" if tp is not None: qty = pending_tickets.get(entry.id, 0) - widget_html = sexp_call("events-entry-tile-widget-wrapper", + widget_html = sx_call("events-entry-tile-widget-wrapper", widget=_ticket_widget_html(entry, qty, ticket_url, ctx={})) - return sexp_call("events-entry-card-tile", - title=SexpExpr(title_html), badges=SexpExpr(badges_html), - time=SexpExpr(time_html), cost=SexpExpr(cost_html), - widget=SexpExpr(widget_html)) + return sx_call("events-entry-card-tile", + title=SxExpr(title_html), badges=SxExpr(badges_html), + time=SxExpr(time_html), cost=SxExpr(cost_html), + widget=SxExpr(widget_html)) def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str: @@ -1043,22 +1043,22 @@ def _ticket_widget_html(entry, qty: int, ticket_url: str, *, ctx: dict) -> str: tgt = f"#page-ticket-{eid}" def _tw_form(count_val, btn_html): - return sexp_call("events-tw-form", + return sx_call("events-tw-form", ticket_url=ticket_url, target=tgt, csrf=csrf_token_val, entry_id=str(eid), - count_val=str(count_val), btn=SexpExpr(btn_html)) + count_val=str(count_val), btn=SxExpr(btn_html)) if qty == 0: - inner = _tw_form(1, sexp_call("events-tw-cart-plus")) + inner = _tw_form(1, sx_call("events-tw-cart-plus")) else: - minus = _tw_form(qty - 1, sexp_call("events-tw-minus")) - cart_icon = sexp_call("events-tw-cart-icon", qty=str(qty)) - plus = _tw_form(qty + 1, sexp_call("events-tw-plus")) + minus = _tw_form(qty - 1, sx_call("events-tw-minus")) + cart_icon = sx_call("events-tw-cart-icon", qty=str(qty)) + plus = _tw_form(qty + 1, sx_call("events-tw-plus")) inner = minus + cart_icon + plus - return sexp_call("events-tw-widget", + return sx_call("events-tw-widget", entry_id=str(eid), price=f"£{tp:.2f}", - inner=SexpExpr(inner)) + inner=SxExpr(inner)) def _entry_cards_html(entries, page_info, pending_tickets, ticket_url, @@ -1076,7 +1076,7 @@ def _entry_cards_html(entries, page_info, pending_tickets, ticket_url, else: entry_date = entry.start_at.strftime("%A %-d %B %Y") if entry.start_at else "" if entry_date != last_date: - parts.append(sexp_call("events-date-separator", + parts.append(sx_call("events-date-separator", date_str=entry_date)) last_date = entry_date parts.append(_entry_card_html( @@ -1085,7 +1085,7 @@ def _entry_cards_html(entries, page_info, pending_tickets, ticket_url, )) if has_more: - parts.append(sexp_call("events-sentinel", + parts.append(sx_call("events-sentinel", page=str(page), next_url=next_url)) return "".join(parts) @@ -1101,14 +1101,14 @@ _TILE_SVG = None def _get_list_svg(): global _LIST_SVG if _LIST_SVG is None: - _LIST_SVG = sexp_call("events-list-svg") + _LIST_SVG = sx_call("events-list-svg") return _LIST_SVG def _get_tile_svg(): global _TILE_SVG if _TILE_SVG is None: - _TILE_SVG = sexp_call("events-tile-svg") + _TILE_SVG = sx_call("events-tile-svg") return _TILE_SVG @@ -1131,7 +1131,7 @@ def _view_toggle_html(ctx: dict, view: str) -> str: list_active = 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600' tile_active = 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600' - return sexp_call("events-view-toggle", + return sx_call("events-view-toggle", list_href=list_href, tile_href=tile_href, hx_select=hx_select, list_active=list_active, tile_active=tile_active, list_svg=_get_list_svg(), @@ -1152,12 +1152,12 @@ def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_ ) grid_cls = ("max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" if view == "tile" else "max-w-full px-3 py-3 space-y-3") - body = sexp_call("events-grid", grid_cls=grid_cls, cards=SexpExpr(cards)) + body = sx_call("events-grid", grid_cls=grid_cls, cards=SxExpr(cards)) else: - body = sexp_call("events-empty") + body = sx_call("events-empty") - return sexp_call("events-main-panel-body", - toggle=SexpExpr(toggle), body=SexpExpr(body)) + return sx_call("events-main-panel-body", + toggle=SxExpr(toggle), body=SxExpr(body)) # --------------------------------------------------------------------------- @@ -1194,8 +1194,8 @@ async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets, ctx, entries, has_more, pending_tickets, page_info, page, view, ticket_url, next_url, events_url, ) - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets, @@ -1213,7 +1213,7 @@ async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets, ctx, entries, has_more, pending_tickets, page_info, page, view, ticket_url, next_url, events_url, ) - return oob_page_sexp(content=content) + return oob_page_sx(content=content) async def render_all_events_cards(entries, has_more, pending_tickets, @@ -1255,9 +1255,9 @@ async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets is_page_scoped=True, post=post, ) - hdr = root_header_sexp(ctx) - hdr += header_child_sexp(_post_header_sexp(ctx)) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + hdr += header_child_sx(_post_header_sx(ctx)) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets, @@ -1278,9 +1278,9 @@ async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets, is_page_scoped=True, post=post, ) - oobs = _post_header_sexp(ctx, oob=True) + oobs = _post_header_sx(ctx, oob=True) oobs += _clear_deeper_oob("post-row", "post-header-child") - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) async def render_page_summary_cards(entries, has_more, pending_tickets, @@ -1307,24 +1307,24 @@ async def render_page_summary_cards(entries, has_more, pending_tickets, async def render_calendars_page(ctx: dict) -> str: """Full page: calendars listing.""" - content = _calendars_main_panel_sexp(ctx) + content = _calendars_main_panel_sx(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = post_admin_header_sexp(ctx, slug, selected="calendars") - return full_page_sexp(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content) + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") + return full_page_sx(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content) async def render_calendars_oob(ctx: dict) -> str: """OOB response: calendars listing.""" - content = _calendars_main_panel_sexp(ctx) + content = _calendars_main_panel_sx(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = post_admin_header_sexp(ctx, slug, oob=True, selected="calendars") + oobs = post_admin_header_sx(ctx, slug, oob=True, selected="calendars") oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child") - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- @@ -1334,21 +1334,21 @@ async def render_calendars_oob(ctx: dict) -> str: async def render_calendar_page(ctx: dict) -> str: """Full page: calendar month view.""" content = _calendar_main_panel_html(ctx) - hdr = root_header_sexp(ctx) - child = _post_header_sexp(ctx) + _calendar_header_sexp(ctx) - hdr += header_child_sexp(child) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + child = _post_header_sx(ctx) + _calendar_header_sx(ctx) + hdr += header_child_sx(child) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_calendar_oob(ctx: dict) -> str: """OOB response: calendar month view.""" content = _calendar_main_panel_html(ctx) - oobs = _post_header_sexp(ctx, oob=True) - oobs += oob_header_sexp("post-header-child", "calendar-header-child", - _calendar_header_sexp(ctx)) + oobs = _post_header_sx(ctx, oob=True) + oobs += oob_header_sx("post-header-child", "calendar-header-child", + _calendar_header_sx(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "calendar-row", "calendar-header-child") - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- @@ -1358,23 +1358,23 @@ async def render_calendar_oob(ctx: dict) -> str: async def render_day_page(ctx: dict) -> str: """Full page: day detail.""" content = _day_main_panel_html(ctx) - hdr = root_header_sexp(ctx) - child = (_post_header_sexp(ctx) - + _calendar_header_sexp(ctx) + _day_header_sexp(ctx)) - hdr += header_child_sexp(child) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + child = (_post_header_sx(ctx) + + _calendar_header_sx(ctx) + _day_header_sx(ctx)) + hdr += header_child_sx(child) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_day_oob(ctx: dict) -> str: """OOB response: day detail.""" content = _day_main_panel_html(ctx) - oobs = _calendar_header_sexp(ctx, oob=True) - oobs += oob_header_sexp("calendar-header-child", "day-header-child", - _day_header_sexp(ctx)) + oobs = _calendar_header_sx(ctx, oob=True) + oobs += oob_header_sx("calendar-header-child", "day-header-child", + _day_header_sx(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "calendar-row", "calendar-header-child", "day-row", "day-header-child") - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- @@ -1386,13 +1386,13 @@ async def render_day_admin_page(ctx: dict) -> str: content = _day_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = post_admin_header_sexp(ctx, slug, selected="calendars") - child = (admin_hdr + _calendar_header_sexp(ctx) + _day_header_sexp(ctx) - + _day_admin_header_sexp(ctx)) - hdr = root_hdr + post_hdr + header_child_sexp(child) - return full_page_sexp(ctx, header_rows=hdr, content=content) + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") + child = (admin_hdr + _calendar_header_sx(ctx) + _day_header_sx(ctx) + + _day_admin_header_sx(ctx)) + hdr = root_hdr + post_hdr + header_child_sx(child) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_day_admin_oob(ctx: dict) -> str: @@ -1400,27 +1400,27 @@ async def render_day_admin_oob(ctx: dict) -> str: content = _day_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (post_admin_header_sexp(ctx, slug, oob=True, selected="calendars") - + _calendar_header_sexp(ctx, oob=True)) - oobs += oob_header_sexp("day-header-child", "day-admin-header-child", - _day_admin_header_sexp(ctx)) + oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars") + + _calendar_header_sx(ctx, oob=True)) + oobs += oob_header_sx("day-header-child", "day-admin-header-child", + _day_admin_header_sx(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child", "calendar-row", "calendar-header-child", "day-row", "day-header-child", "day-admin-row", "day-admin-header-child") - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- # Calendar admin # --------------------------------------------------------------------------- -def _events_post_admin_header_sexp(ctx: dict, *, oob: bool = False, +def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str: """Post-level admin row for events — delegates to shared helper.""" slug = (ctx.get("post") or {}).get("slug", "") - return post_admin_header_sexp(ctx, slug, oob=oob, selected=selected) + return post_admin_header_sx(ctx, slug, oob=oob, selected=selected) async def render_calendar_admin_page(ctx: dict) -> str: @@ -1428,12 +1428,12 @@ async def render_calendar_admin_page(ctx: dict) -> str: content = _calendar_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = post_admin_header_sexp(ctx, slug, selected="calendars") - child = admin_hdr + _calendar_header_sexp(ctx) + _calendar_admin_header_sexp(ctx) - hdr = root_hdr + post_hdr + header_child_sexp(child) - return full_page_sexp(ctx, header_rows=hdr, content=content) + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") + child = admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx) + hdr = root_hdr + post_hdr + header_child_sx(child) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_calendar_admin_oob(ctx: dict) -> str: @@ -1441,15 +1441,15 @@ async def render_calendar_admin_oob(ctx: dict) -> str: content = _calendar_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (post_admin_header_sexp(ctx, slug, oob=True, selected="calendars") - + _calendar_header_sexp(ctx, oob=True)) - oobs += oob_header_sexp("calendar-header-child", "calendar-admin-header-child", - _calendar_admin_header_sexp(ctx)) + oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars") + + _calendar_header_sx(ctx, oob=True)) + oobs += oob_header_sx("calendar-header-child", "calendar-admin-header-child", + _calendar_admin_header_sx(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child", "calendar-row", "calendar-header-child", "calendar-admin-row", "calendar-admin-header-child") - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- @@ -1464,12 +1464,12 @@ async def render_slots_page(ctx: dict) -> str: content = render_slots_table(slots, calendar) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = post_admin_header_sexp(ctx, slug, selected="calendars") - child = admin_hdr + _calendar_header_sexp(ctx) + _calendar_admin_header_sexp(ctx) - hdr = root_hdr + post_hdr + header_child_sexp(child) - return full_page_sexp(ctx, header_rows=hdr, content=content) + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") + child = admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx) + hdr = root_hdr + post_hdr + header_child_sx(child) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_slots_oob(ctx: dict) -> str: @@ -1479,13 +1479,13 @@ async def render_slots_oob(ctx: dict) -> str: content = render_slots_table(slots, calendar) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (post_admin_header_sexp(ctx, slug, oob=True, selected="calendars") - + _calendar_admin_header_sexp(ctx, oob=True)) + oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars") + + _calendar_admin_header_sx(ctx, oob=True)) oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child", "calendar-row", "calendar-header-child", "calendar-admin-row", "calendar-admin-header-child") - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- @@ -1495,27 +1495,27 @@ async def render_slots_oob(ctx: dict) -> str: async def render_tickets_page(ctx: dict, tickets: list) -> str: """Full page: my tickets.""" content = _tickets_main_panel_html(ctx, tickets) - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_tickets_oob(ctx: dict, tickets: list) -> str: """OOB response: my tickets.""" content = _tickets_main_panel_html(ctx, tickets) - return oob_page_sexp(content=content) + return oob_page_sx(content=content) async def render_ticket_detail_page(ctx: dict, ticket) -> str: """Full page: ticket detail with QR.""" content = _ticket_detail_panel_html(ctx, ticket) - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_ticket_detail_oob(ctx: dict, ticket) -> str: """OOB response: ticket detail.""" content = _ticket_detail_panel_html(ctx, ticket) - return oob_page_sexp(content=content) + return oob_page_sx(content=content) # --------------------------------------------------------------------------- @@ -1525,14 +1525,14 @@ async def render_ticket_detail_oob(ctx: dict, ticket) -> str: async def render_ticket_admin_page(ctx: dict, tickets: list, stats: dict) -> str: """Full page: ticket admin dashboard.""" content = _ticket_admin_main_panel_html(ctx, tickets, stats) - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_ticket_admin_oob(ctx: dict, tickets: list, stats: dict) -> str: """OOB response: ticket admin dashboard.""" content = _ticket_admin_main_panel_html(ctx, tickets, stats) - return oob_page_sexp(content=content) + return oob_page_sx(content=content) # --------------------------------------------------------------------------- @@ -1542,19 +1542,19 @@ async def render_ticket_admin_oob(ctx: dict, tickets: list, stats: dict) -> str: async def render_markets_page(ctx: dict) -> str: """Full page: markets listing.""" content = _markets_main_panel_html(ctx) - hdr = root_header_sexp(ctx) - child = _post_header_sexp(ctx) + _markets_header_sexp(ctx) - hdr += header_child_sexp(child) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + child = _post_header_sx(ctx) + _markets_header_sx(ctx) + hdr += header_child_sx(child) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_markets_oob(ctx: dict) -> str: """OOB response: markets listing.""" content = _markets_main_panel_html(ctx) - oobs = _post_header_sexp(ctx, oob=True) - oobs += oob_header_sexp("post-header-child", "markets-header-child", - _markets_header_sexp(ctx)) - return oob_page_sexp(oobs=oobs, content=content) + oobs = _post_header_sx(ctx, oob=True) + oobs += oob_header_sx("post-header-child", "markets-header-child", + _markets_header_sx(ctx)) + return oob_page_sx(oobs=oobs, content=content) # =========================================================================== @@ -1578,7 +1578,7 @@ def render_ticket_widget(entry, qty: int, ticket_url: str) -> str: def render_checkin_result(success: bool, error: str | None, ticket) -> str: """Render checkin result: table row on success, error div on failure.""" if not success: - return sexp_call("events-checkin-error", + return sx_call("events-checkin-error", message=error or "Check-in failed") if not ticket: return "" @@ -1590,13 +1590,13 @@ def render_checkin_result(success: bool, error: str | None, ticket) -> str: date_html = "" if entry and entry.start_at: - date_html = sexp_call("events-ticket-admin-date", + date_html = sx_call("events-ticket-admin-date", date_str=entry.start_at.strftime("%d %b %Y, %H:%M")) - return sexp_call("events-checkin-success-row", + return sx_call("events-checkin-success-row", code=code, code_short=code[:12] + "...", entry_name=entry.name if entry else "\u2014", - date=SexpExpr(date_html), + date=SxExpr(date_html), type_name=tt.name if tt else "\u2014", badge=_ticket_state_badge_html("checked_in"), time_str=time_str) @@ -1612,7 +1612,7 @@ def render_lookup_result(ticket, error: str | None) -> str: from shared.browser.app.csrf import generate_csrf_token if error: - return sexp_call("events-lookup-error", message=error) + return sx_call("events-lookup-error", message=error) if not ticket: return "" @@ -1624,35 +1624,35 @@ def render_lookup_result(ticket, error: str | None) -> str: csrf = generate_csrf_token() # Info section - info_html = sexp_call("events-lookup-info", + info_html = sx_call("events-lookup-info", entry_name=entry.name if entry else "Unknown event") if tt: - info_html += sexp_call("events-lookup-type", type_name=tt.name) + info_html += sx_call("events-lookup-type", type_name=tt.name) if entry and entry.start_at: - info_html += sexp_call("events-lookup-date", + info_html += sx_call("events-lookup-date", date_str=entry.start_at.strftime("%A, %B %d, %Y at %H:%M")) cal = getattr(entry, "calendar", None) if entry else None if cal: - info_html += sexp_call("events-lookup-cal", cal_name=cal.name) - info_html += sexp_call("events-lookup-status", + info_html += sx_call("events-lookup-cal", cal_name=cal.name) + info_html += sx_call("events-lookup-status", badge=_ticket_state_badge_html(state), code=code) if checked_in_at: - info_html += sexp_call("events-lookup-checkin-time", + info_html += sx_call("events-lookup-checkin-time", date_str=checked_in_at.strftime("%B %d, %Y at %H:%M")) # Action area action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) - action_html = sexp_call("events-lookup-checkin-btn", + action_html = sx_call("events-lookup-checkin-btn", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": - action_html = sexp_call("events-lookup-checked-in") + action_html = sx_call("events-lookup-checked-in") elif state == "cancelled": - action_html = sexp_call("events-lookup-cancelled") + action_html = sx_call("events-lookup-cancelled") - return sexp_call("events-lookup-card", - info=SexpExpr(info_html), code=code, action=SexpExpr(action_html)) + return sx_call("events-lookup-card", + info=SxExpr(info_html), code=code, action=SxExpr(action_html)) # --------------------------------------------------------------------------- @@ -1678,29 +1678,29 @@ def render_entry_tickets_admin(entry, tickets: list) -> str: action_html = "" if state in ("confirmed", "reserved"): checkin_url = url_for("ticket_admin.do_checkin", code=code) - action_html = sexp_call("events-entry-tickets-admin-checkin", + action_html = sx_call("events-entry-tickets-admin-checkin", checkin_url=checkin_url, code=code, csrf=csrf) elif state == "checked_in": t_str = checked_in_at.strftime("%H:%M") if checked_in_at else "" - action_html = sexp_call("events-ticket-admin-checked-in", + action_html = sx_call("events-ticket-admin-checked-in", time_str=t_str) - rows_html += sexp_call("events-entry-tickets-admin-row", + rows_html += sx_call("events-entry-tickets-admin-row", code=code, code_short=code[:12] + "...", type_name=tt.name if tt else "\u2014", badge=_ticket_state_badge_html(state), - action=SexpExpr(action_html)) + action=SxExpr(action_html)) if tickets: - body_html = sexp_call("events-entry-tickets-admin-table", - rows=SexpExpr(rows_html)) + body_html = sx_call("events-entry-tickets-admin-table", + rows=SxExpr(rows_html)) else: - body_html = sexp_call("events-entry-tickets-admin-empty") + body_html = sx_call("events-entry-tickets-admin-empty") - return sexp_call("events-entry-tickets-admin-panel", + return sx_call("events-entry-tickets-admin-panel", entry_name=entry.name, count_label=f"{count} ticket{suffix}", - body=SexpExpr(body_html)) + body=SxExpr(body_html)) # --------------------------------------------------------------------------- @@ -1738,40 +1738,40 @@ def _entry_main_panel_html(ctx: dict) -> str: state = getattr(entry, "state", "pending") or "pending" def _field(label, content_html): - return sexp_call("events-entry-field", label=label, content=SexpExpr(content_html)) + return sx_call("events-entry-field", label=label, content=SxExpr(content_html)) # Name - name_html = _field("Name", sexp_call("events-entry-name-field", name=entry.name)) + name_html = _field("Name", sx_call("events-entry-name-field", name=entry.name)) # Slot slot = getattr(entry, "slot", None) if slot: flex_label = "(flexible)" if getattr(slot, "flexible", False) else "(fixed)" - slot_inner = sexp_call("events-entry-slot-assigned", + slot_inner = sx_call("events-entry-slot-assigned", slot_name=slot.name, flex_label=flex_label) else: - slot_inner = sexp_call("events-entry-slot-none") + slot_inner = sx_call("events-entry-slot-none") slot_html = _field("Slot", slot_inner) # Time Period start_str = entry.start_at.strftime("%H:%M") if entry.start_at else "" end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else " \u2013 open-ended" - time_html = _field("Time Period", sexp_call("events-entry-time-field", + time_html = _field("Time Period", sx_call("events-entry-time-field", time_str=start_str + end_str)) # State - state_html = _field("State", sexp_call("events-entry-state-field", + state_html = _field("State", sx_call("events-entry-state-field", entry_id=str(eid), badge=_entry_state_badge_html(state))) # Cost cost = getattr(entry, "cost", None) cost_str = f"{cost:.2f}" if cost is not None else "0.00" - cost_html = _field("Cost", sexp_call("events-entry-cost-field", + cost_html = _field("Cost", sx_call("events-entry-cost-field", cost=f"£{cost_str}")) # Ticket Configuration (admin) - tickets_html = _field("Tickets", sexp_call("events-entry-tickets-field", + tickets_html = _field("Tickets", sx_call("events-entry-tickets-field", entry_id=str(eid), tickets_config=render_entry_tickets_config(entry, calendar, day, month, year))) @@ -1787,11 +1787,11 @@ def _entry_main_panel_html(ctx: dict) -> str: # Date date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else "" - date_html = _field("Date", sexp_call("events-entry-date-field", date_str=date_str)) + date_html = _field("Date", sx_call("events-entry-date-field", date_str=date_str)) # Associated Posts entry_posts = ctx.get("entry_posts") or [] - posts_html = _field("Associated Posts", sexp_call("events-entry-posts-field", + posts_html = _field("Associated Posts", sx_call("events-entry-posts-field", entry_id=str(eid), posts_panel=render_entry_posts_panel(entry_posts, entry, calendar, day, month, year))) @@ -1802,13 +1802,13 @@ def _entry_main_panel_html(ctx: dict) -> str: day=day, month=month, year=year, ) - return sexp_call("events-entry-panel", + return sx_call("events-entry-panel", entry_id=str(eid), list_container=list_container, - name=SexpExpr(name_html), slot=SexpExpr(slot_html), - time=SexpExpr(time_html), state=SexpExpr(state_html), - cost=SexpExpr(cost_html), tickets=SexpExpr(tickets_html), - buy=SexpExpr(buy_html), date=SexpExpr(date_html), - posts=SexpExpr(posts_html), + name=SxExpr(name_html), slot=SxExpr(slot_html), + time=SxExpr(time_html), state=SxExpr(state_html), + cost=SxExpr(cost_html), tickets=SxExpr(tickets_html), + buy=SxExpr(buy_html), date=SxExpr(date_html), + posts=SxExpr(posts_html), options=_entry_options_html(entry, calendar, day, month, year), pre_action=pre_action, edit_url=edit_url) @@ -1838,16 +1838,16 @@ def _entry_header_html(ctx: dict, *, oob: bool = False) -> str: year=year, month=month, day=day, entry_id=entry.id, ) - label_html = sexp_call("events-entry-label", + label_html = sx_call("events-entry-label", entry_id=str(entry.id), title=_entry_title_html(entry), times=_entry_times_html(entry)) nav_html = _entry_nav_html(ctx) - return sexp_call("menu-row-sx", id="entry-row", level=5, - link_href=link_href, link_label_content=SexpExpr(label_html), - nav=SexpExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob) + return sx_call("menu-row-sx", id="entry-row", level=5, + link_href=link_href, link_label_content=SxExpr(label_html), + nav=SxExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob) def _entry_times_html(entry) -> str: @@ -1858,7 +1858,7 @@ def _entry_times_html(entry) -> str: return "" start_str = start.strftime("%H:%M") end_str = f" \u2192 {end.strftime('%H:%M')}" if end else "" - return sexp_call("events-entry-times", time_str=start_str + end_str) + return sx_call("events-entry-times", time_str=start_str + end_str) # --------------------------------------------------------------------------- @@ -1896,13 +1896,13 @@ def _entry_nav_html(ctx: dict) -> str: feat = getattr(ep, "feature_image", None) href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/" if feat: - img_html = sexp_call("events-post-img", src=feat, alt=title) + img_html = sx_call("events-post-img", src=feat, alt=title) else: - img_html = sexp_call("events-post-img-placeholder") - post_links += sexp_call("events-entry-nav-post-link", - href=href, img=SexpExpr(img_html), title=title) - parts.append(sexp_call("events-entry-posts-nav-oob", - items=SexpExpr(post_links)).replace(' :hx-swap-oob "true"', '')) + img_html = sx_call("events-post-img-placeholder") + post_links += sx_call("events-entry-nav-post-link", + href=href, img=SxExpr(img_html), title=title) + parts.append(sx_call("events-entry-posts-nav-oob", + items=SxExpr(post_links)).replace(' :hx-swap-oob "true"', '')) # Admin link if is_admin: @@ -1912,7 +1912,7 @@ def _entry_nav_html(ctx: dict) -> str: day=day, month=month, year=year, entry_id=entry.id, ) - parts.append(sexp_call("events-entry-admin-link", href=admin_url)) + parts.append(sx_call("events-entry-admin-link", href=admin_url)) return "".join(parts) @@ -1924,27 +1924,27 @@ def _entry_nav_html(ctx: dict) -> str: async def render_entry_page(ctx: dict) -> str: """Full page: entry detail.""" content = _entry_main_panel_html(ctx) - hdr = root_header_sexp(ctx) - child = (_post_header_sexp(ctx) - + _calendar_header_sexp(ctx) + _day_header_sexp(ctx) + hdr = root_header_sx(ctx) + child = (_post_header_sx(ctx) + + _calendar_header_sx(ctx) + _day_header_sx(ctx) + _entry_header_html(ctx)) - hdr += header_child_sexp(child) + hdr += header_child_sx(child) nav_html = _entry_nav_html(ctx) - return full_page_sexp(ctx, header_rows=hdr, content=content, menu=nav_html) + return full_page_sx(ctx, header_rows=hdr, content=content, menu=nav_html) async def render_entry_oob(ctx: dict) -> str: """OOB response: entry detail.""" content = _entry_main_panel_html(ctx) - oobs = _day_header_sexp(ctx, oob=True) - oobs += oob_header_sexp("day-header-child", "entry-header-child", + oobs = _day_header_sx(ctx, oob=True) + oobs += oob_header_sx("day-header-child", "entry-header-child", _entry_header_html(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "calendar-row", "calendar-header-child", "day-row", "day-header-child", "entry-row", "entry-header-child") nav_html = _entry_nav_html(ctx) - return oob_page_sexp(oobs=oobs, content=content, menu=nav_html) + return oob_page_sx(oobs=oobs, content=content, menu=nav_html) # --------------------------------------------------------------------------- @@ -1957,15 +1957,15 @@ def render_entry_optioned(entry, calendar, day, month, year) -> str: title = _entry_title_html(entry) state = _entry_state_badge_html(getattr(entry, "state", "pending") or "pending") - return options + sexp_call("events-entry-optioned-oob", + return options + sx_call("events-entry-optioned-oob", entry_id=str(entry.id), - title=SexpExpr(title), state=SexpExpr(state)) + title=SxExpr(title), state=SxExpr(state)) def _entry_title_html(entry) -> str: """Render entry title (icon + name + state badge).""" state = getattr(entry, "state", "pending") or "pending" - return sexp_call("events-entry-title", + return sx_call("events-entry-title", name=entry.name, badge=_entry_state_badge_html(state)) @@ -1990,7 +1990,7 @@ def _entry_options_html(entry, calendar, day, month, year) -> str: calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid, ) btn_type = "button" if trigger_type == "button" else "submit" - return sexp_call("events-entry-option-button", + return sx_call("events-entry-option-button", url=url, target=target, csrf=csrf, btn_type=btn_type, action_btn=action_btn, confirm_title=confirm_title, confirm_text=confirm_text, label=label, @@ -2013,8 +2013,8 @@ def _entry_options_html(entry, calendar, day, month, year) -> str: trigger_type="button", ) - return sexp_call("events-entry-options", - entry_id=str(eid), buttons=SexpExpr(buttons_html)) + return sx_call("events-entry-options", + entry_id=str(eid), buttons=SxExpr(buttons_html)) # --------------------------------------------------------------------------- @@ -2038,11 +2038,11 @@ def render_entry_tickets_config(entry, calendar, day, month, year) -> str: if tp is not None: tc_str = f"{tc} tickets" if tc is not None else "Unlimited" - display_html = sexp_call("events-ticket-config-display", + display_html = sx_call("events-ticket-config-display", price_str=f"£{tp:.2f}", count_str=tc_str, show_js=show_js) else: - display_html = sexp_call("events-ticket-config-none", show_js=show_js) + display_html = sx_call("events-ticket-config-none", show_js=show_js) update_url = url_for( "calendar.day.calendar_entries.calendar_entry.update_tickets", @@ -2052,7 +2052,7 @@ def render_entry_tickets_config(entry, calendar, day, month, year) -> str: tp_val = f"{tp:.2f}" if tp is not None else "" tc_val = str(tc) if tc is not None else "" - form_html = sexp_call("events-ticket-config-form", + form_html = sx_call("events-ticket-config-form", entry_id=eid_s, hidden_cls=hidden_cls, update_url=update_url, csrf=csrf, price_val=tp_val, count_val=tc_val, hide_js=hide_js) @@ -2080,29 +2080,29 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> ep_title = getattr(ep, "title", "") ep_id = getattr(ep, "id", 0) feat = getattr(ep, "feature_image", None) - img_html = (sexp_call("events-post-img", src=feat, alt=ep_title) - if feat else sexp_call("events-post-img-placeholder")) + img_html = (sx_call("events-post-img", src=feat, alt=ep_title) + if feat else sx_call("events-post-img-placeholder")) del_url = url_for( "calendar.day.calendar_entries.calendar_entry.remove_post", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid, post_id=ep_id, ) - items += sexp_call("events-entry-post-item", - img=SexpExpr(img_html), title=ep_title, + items += sx_call("events-entry-post-item", + img=SxExpr(img_html), title=ep_title, del_url=del_url, entry_id=eid_s, csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') - posts_html = sexp_call("events-entry-posts-list", items=SexpExpr(items)) + posts_html = sx_call("events-entry-posts-list", items=SxExpr(items)) else: - posts_html = sexp_call("events-entry-posts-none") + posts_html = sx_call("events-entry-posts-none") search_url = url_for( "calendar.day.calendar_entries.calendar_entry.search_posts", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid, ) - return sexp_call("events-entry-posts-panel", - posts=SexpExpr(posts_html), search_url=search_url, + return sx_call("events-entry-posts-panel", + posts=SxExpr(posts_html), search_url=search_url, entry_id=eid_s) @@ -2118,7 +2118,7 @@ def render_entry_posts_nav_oob(entry_posts) -> str: blog_url_fn = getattr(g, "blog_url", None) if not entry_posts: - return sexp_call("events-entry-posts-nav-oob-empty") + return sx_call("events-entry-posts-nav-oob-empty") items = "" for ep in entry_posts: @@ -2126,13 +2126,13 @@ def render_entry_posts_nav_oob(entry_posts) -> str: title = getattr(ep, "title", "") feat = getattr(ep, "feature_image", None) href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/" - img_html = (sexp_call("events-post-img", src=feat, alt=title) - if feat else sexp_call("events-post-img-placeholder")) - items += sexp_call("events-entry-nav-post", + img_html = (sx_call("events-post-img", src=feat, alt=title) + if feat else sx_call("events-post-img-placeholder")) + items += sx_call("events-entry-nav-post", href=href, nav_btn=nav_btn, - img=SexpExpr(img_html), title=title) + img=SxExpr(img_html), title=title) - return sexp_call("events-entry-posts-nav-oob", items=SexpExpr(items)) + return sx_call("events-entry-posts-nav-oob", items=SxExpr(items)) # --------------------------------------------------------------------------- @@ -2148,7 +2148,7 @@ def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str: cal_slug = getattr(calendar, "slug", "") if not confirmed_entries: - return sexp_call("events-day-entries-nav-oob-empty") + return sx_call("events-day-entries-nav-oob-empty") items = "" for entry in confirmed_entries: @@ -2160,11 +2160,11 @@ def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str: ) start = entry.start_at.strftime("%H:%M") if entry.start_at else "" end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - items += sexp_call("events-day-nav-entry", + items += sx_call("events-day-nav-entry", href=href, nav_btn=nav_btn, name=entry.name, time_str=start + end) - return sexp_call("events-day-entries-nav-oob", items=SexpExpr(items)) + return sx_call("events-day-entries-nav-oob", items=SxExpr(items)) # --------------------------------------------------------------------------- @@ -2183,7 +2183,7 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: has_items = has_entries or calendars if not has_items: - return sexp_call("events-post-nav-oob-empty") + return sx_call("events-post-nav-oob-empty") slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "") @@ -2198,7 +2198,7 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: href = events_url(entry_path) time_str = entry.start_at.strftime("%b %d, %Y at %H:%M") end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" - items += sexp_call("events-post-nav-entry", + items += sx_call("events-post-nav-entry", href=href, nav_btn=nav_btn, name=entry.name, time_str=time_str + end_str) @@ -2206,7 +2206,7 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: for cal in calendars: cs = getattr(cal, "slug", "") local_href = events_url(f"/{slug}/{cs}/") - items += sexp_call("events-post-nav-calendar", + items += sx_call("events-post-nav-calendar", href=local_href, nav_btn=nav_btn, name=cal.name) hs = ("on load or scroll " @@ -2214,8 +2214,8 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: "remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow " "else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end") - return sexp_call("events-post-nav-wrapper", - items=SexpExpr(items), hyperscript=hs) + return sx_call("events-post-nav-wrapper", + items=SxExpr(items), hyperscript=hs) # --------------------------------------------------------------------------- @@ -2232,7 +2232,7 @@ def render_calendar_description(calendar, *, oob: bool = False) -> str: if oob: desc = getattr(calendar, "description", "") or "" - html += sexp_call("events-calendar-description-title-oob", + html += sx_call("events-calendar-description-title-oob", description=desc) return html @@ -2248,7 +2248,7 @@ def render_calendar_description_edit(calendar) -> str: save_url = url_for("calendar.admin.calendar_description_save", calendar_slug=cal_slug) cancel_url = url_for("calendar.admin.calendar_description_view", calendar_slug=cal_slug) - return sexp_call("events-calendar-description-edit-form", + return sx_call("events-calendar-description-edit-form", save_url=save_url, cancel_url=cancel_url, csrf=csrf, description=desc) @@ -2259,7 +2259,7 @@ def render_calendar_description_edit(calendar) -> str: def render_calendars_list_panel(ctx: dict) -> str: """Render the calendars main panel HTML for POST/DELETE response.""" - return _calendars_main_panel_sexp(ctx) + return _calendars_main_panel_sx(ctx) # --------------------------------------------------------------------------- @@ -2298,24 +2298,24 @@ def render_slot_main_panel(slot, calendar, *, oob: bool = False) -> str: # Days pills if days and days[0] != "\u2014": days_inner = "".join( - sexp_call("events-slot-day-pill", day=d) for d in days + sx_call("events-slot-day-pill", day=d) for d in days ) - days_html = sexp_call("events-slot-days-pills", days_inner=SexpExpr(days_inner)) + days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner)) else: - days_html = sexp_call("events-slot-no-days") + days_html = sx_call("events-slot-no-days") sid = str(slot.id) - result = sexp_call("events-slot-panel", + result = sx_call("events-slot-panel", slot_id=sid, list_container=list_container, - days=SexpExpr(days_html), + days=SxExpr(days_html), flexible="yes" if flexible else "no", time_str=f"{time_start} \u2014 {time_end}", cost_str=cost_str, pre_action=pre_action, edit_url=edit_url) if oob: - result += sexp_call("events-slot-description-oob", description=desc) + result += sx_call("events-slot-description-oob", description=desc) return result @@ -2350,34 +2350,34 @@ def render_slots_table(slots, calendar) -> str: day_list = days_display.split(", ") if day_list and day_list[0] != "\u2014": days_inner = "".join( - sexp_call("events-slot-day-pill", day=d) for d in day_list + sx_call("events-slot-day-pill", day=d) for d in day_list ) - days_html = sexp_call("events-slot-days-pills", days_inner=SexpExpr(days_inner)) + days_html = sx_call("events-slot-days-pills", days_inner=SxExpr(days_inner)) else: - days_html = sexp_call("events-slot-no-days") + days_html = sx_call("events-slot-no-days") time_start = s.time_start.strftime("%H:%M") if s.time_start else "" time_end = s.time_end.strftime("%H:%M") if s.time_end else "" cost = getattr(s, "cost", None) cost_str = f"{cost:.2f}" if cost is not None else "" - rows_html += sexp_call("events-slots-row", + rows_html += sx_call("events-slots-row", tr_cls=tr_cls, slot_href=slot_href, pill_cls=pill_cls, hx_select=hx_select, slot_name=s.name, description=desc, flexible="yes" if s.flexible else "no", - days=SexpExpr(days_html), + days=SxExpr(days_html), time_str=f"{time_start} - {time_end}", cost_str=cost_str, action_btn=action_btn, del_url=del_url, csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') else: - rows_html = sexp_call("events-slots-empty-row") + rows_html = sx_call("events-slots-empty-row") add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug) - return sexp_call("events-slots-table", - list_container=list_container, rows=SexpExpr(rows_html), + return sx_call("events-slots-table", + list_container=list_container, rows=SxExpr(rows_html), pre_action=pre_action, add_url=add_url) @@ -2406,9 +2406,9 @@ def render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year ) def _col(label, val): - return sexp_call("events-ticket-type-col", label=label, value=val) + return sx_call("events-ticket-type-col", label=label, value=val) - return sexp_call("events-ticket-type-panel", + return sx_call("events-ticket-type-panel", ticket_id=tid, list_container=list_container, c1=_col("Name", ticket_type.name), c2=_col("Cost", cost_str), @@ -2451,7 +2451,7 @@ def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) - cost = getattr(tt, "cost", None) cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00" - rows_html += sexp_call("events-ticket-types-row", + rows_html += sx_call("events-ticket-types-row", tr_cls=tr_cls, tt_href=tt_href, pill_cls=pill_cls, hx_select=hx_select, tt_name=tt.name, cost_str=cost_str, @@ -2459,15 +2459,15 @@ def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) - del_url=del_url, csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}') else: - rows_html = sexp_call("events-ticket-types-empty-row") + rows_html = sx_call("events-ticket-types-empty-row") add_url = url_for( "calendar.day.calendar_entries.calendar_entry.ticket_types.add_form", calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day, ) - return sexp_call("events-ticket-types-table", - list_container=list_container, rows=SexpExpr(rows_html), + return sx_call("events-ticket-types-table", + list_container=list_container, rows=SxExpr(rows_html), action_btn=action_btn, add_url=add_url) @@ -2487,22 +2487,22 @@ def render_buy_result(entry, created_tickets, remaining, cart_count) -> str: tickets_html = "" for ticket in created_tickets: href = url_for("tickets.ticket_detail", code=ticket.code) - tickets_html += sexp_call("events-buy-result-ticket", + tickets_html += sx_call("events-buy-result-ticket", href=href, code_short=ticket.code[:12] + "...") remaining_html = "" if remaining is not None: r_suffix = "s" if remaining != 1 else "" - remaining_html = sexp_call("events-buy-result-remaining", + remaining_html = sx_call("events-buy-result-remaining", text=f"{remaining} ticket{r_suffix} remaining") my_href = url_for("tickets.my_tickets") - return cart_html + sexp_call("events-buy-result", + return cart_html + sx_call("events-buy-result", entry_id=str(entry.id), count_label=f"{count} ticket{suffix} reserved", - tickets=SexpExpr(tickets_html), - remaining=SexpExpr(remaining_html), + tickets=SxExpr(tickets_html), + remaining=SxExpr(remaining_html), my_tickets_href=my_href) @@ -2527,7 +2527,7 @@ def render_buy_form(entry, ticket_remaining, ticket_sold_count, return "" if state != "confirmed": - return sexp_call("events-buy-not-confirmed", entry_id=eid_s) + return sx_call("events-buy-not-confirmed", entry_id=eid_s) adjust_url = url_for("tickets.adjust_quantity") target = f"#ticket-buy-{eid}" @@ -2536,16 +2536,16 @@ def render_buy_form(entry, ticket_remaining, ticket_sold_count, info_html = "" info_items = "" if ticket_sold_count: - info_items += sexp_call("events-buy-info-sold", + info_items += sx_call("events-buy-info-sold", count=str(ticket_sold_count)) if ticket_remaining is not None: - info_items += sexp_call("events-buy-info-remaining", + info_items += sx_call("events-buy-info-remaining", count=str(ticket_remaining)) if user_ticket_count: - info_items += sexp_call("events-buy-info-basket", + info_items += sx_call("events-buy-info-basket", count=str(user_ticket_count)) if info_items: - info_html = sexp_call("events-buy-info-bar", items=SexpExpr(info_items)) + info_html = sx_call("events-buy-info-bar", items=SxExpr(info_items)) active_types = [tt for tt in ticket_types if getattr(tt, "deleted_at", None) is None] @@ -2555,47 +2555,47 @@ def render_buy_form(entry, ticket_remaining, ticket_sold_count, for tt in active_types: type_count = user_ticket_counts_by_type.get(tt.id, 0) if user_ticket_counts_by_type else 0 cost_str = f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00" - type_items += sexp_call("events-buy-type-item", + type_items += sx_call("events-buy-type-item", type_name=tt.name, cost_str=cost_str, adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id)) - body_html = sexp_call("events-buy-types-wrapper", items=SexpExpr(type_items)) + body_html = sx_call("events-buy-types-wrapper", items=SxExpr(type_items)) else: qty = user_ticket_count or 0 - body_html = sexp_call("events-buy-default", + body_html = sx_call("events-buy-default", price_str=f"\u00a3{tp:.2f}", adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, qty)) - return sexp_call("events-buy-panel", - entry_id=eid_s, info=SexpExpr(info_html), body=SexpExpr(body_html)) + return sx_call("events-buy-panel", + entry_id=eid_s, info=SxExpr(info_html), body=SxExpr(body_html)) def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket_type_id=None): """Render +/- ticket controls for buy form.""" from quart import url_for - tt_html = sexp_call("events-adjust-tt-hidden", + tt_html = sx_call("events-adjust-tt-hidden", ticket_type_id=str(ticket_type_id)) if ticket_type_id else "" eid_s = str(entry_id) def _adj_form(count_val, btn_html, *, extra_cls=""): - return sexp_call("events-adjust-form", + return sx_call("events-adjust-form", adjust_url=adjust_url, target=target, extra_cls=extra_cls, csrf=csrf, - entry_id=eid_s, tt=SexpExpr(tt_html) if tt_html else None, - count_val=str(count_val), btn=SexpExpr(btn_html)) + entry_id=eid_s, tt=SxExpr(tt_html) if tt_html else None, + count_val=str(count_val), btn=SxExpr(btn_html)) if count == 0: - return _adj_form(1, sexp_call("events-adjust-cart-plus"), + return _adj_form(1, sx_call("events-adjust-cart-plus"), extra_cls="flex items-center") my_tickets_href = url_for("tickets.my_tickets") - minus = _adj_form(count - 1, sexp_call("events-adjust-minus")) - cart_icon = sexp_call("events-adjust-cart-icon", + minus = _adj_form(count - 1, sx_call("events-adjust-minus")) + cart_icon = sx_call("events-adjust-cart-icon", href=my_tickets_href, count=str(count)) - plus = _adj_form(count + 1, sexp_call("events-adjust-plus")) + plus = _adj_form(count + 1, sx_call("events-adjust-plus")) - return sexp_call("events-adjust-controls", - minus=SexpExpr(minus), cart_icon=SexpExpr(cart_icon), plus=SexpExpr(plus)) + return sx_call("events-adjust-controls", + minus=SxExpr(minus), cart_icon=SxExpr(cart_icon), plus=SxExpr(plus)) # --------------------------------------------------------------------------- @@ -2628,11 +2628,11 @@ def _cart_icon_oob(count: int) -> str: if count == 0: blog_href = blog_url_fn("/") if blog_url_fn else "/" - return sexp_call("events-cart-icon-logo", + return sx_call("events-cart-icon-logo", blog_href=blog_href, logo=logo) cart_href = cart_url_fn("/") if cart_url_fn else "/" - return sexp_call("events-cart-icon-badge", + return sx_call("events-cart-icon-badge", cart_href=cart_href, count=str(count)) @@ -2759,7 +2759,7 @@ def _slot_options_html(day_slots, selected_slot_id=None) -> str: label_parts.append("[flexible]") label = " ".join(label_parts) - parts.append(sexp_call("events-slot-option", + parts.append(sx_call("events-slot-option", value=str(slot.id), data_start=start, data_end=end, data_flexible="1" if flexible else "0", @@ -2790,10 +2790,10 @@ def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str: # Slot picker if day_slots: options_html = _slot_options_html(day_slots, selected_slot_id=getattr(entry, "slot_id", None)) - slot_picker_html = sexp_call("events-slot-picker", - id=f"entry-slot-{eid}", options=SexpExpr(options_html)) + slot_picker_html = sx_call("events-slot-picker", + id=f"entry-slot-{eid}", options=SxExpr(options_html)) else: - slot_picker_html = sexp_call("events-no-slots") + slot_picker_html = sx_call("events-no-slots") # Values start_val = entry.start_at.strftime("%H:%M") if entry.start_at else "" @@ -2805,10 +2805,10 @@ def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str: tp_val = f"{tp:.2f}" if tp is not None else "" tc_val = str(tc) if tc is not None else "" - html = sexp_call("events-entry-edit-form", + html = sx_call("events-entry-edit-form", entry_id=str(eid), list_container=list_container, put_url=put_url, cancel_url=cancel_url, csrf=csrf, - name_val=entry.name or "", slot_picker=SexpExpr(slot_picker_html), + name_val=entry.name or "", slot_picker=SxExpr(slot_picker_html), start_val=start_val, end_val=end_val, cost_display=cost_display, ticket_price_val=tp_val, ticket_count_val=tc_val, action_btn=action_btn, cancel_btn=cancel_btn) @@ -2837,13 +2837,13 @@ def render_post_search_results(search_posts, search_query, page, total_pages, feat = getattr(sp, "feature_image", None) title = getattr(sp, "title", "") if feat: - img_html = sexp_call("events-post-img", src=feat, alt=title) + img_html = sx_call("events-post-img", src=feat, alt=title) else: - img_html = sexp_call("events-post-img-placeholder") + img_html = sx_call("events-post-img-placeholder") - parts.append(sexp_call("events-post-search-item", + parts.append(sx_call("events-post-search-item", post_url=post_url, entry_id=str(eid), csrf=csrf, - post_id=str(sp.id), img=SexpExpr(img_html), title=title)) + post_id=str(sp.id), img=SxExpr(img_html), title=title)) result = "".join(parts) @@ -2851,10 +2851,10 @@ def render_post_search_results(search_posts, search_query, page, total_pages, next_url = url_for("calendar.day.calendar_entries.calendar_entry.search_posts", calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid, q=search_query, page=page + 1) - result += sexp_call("events-post-search-sentinel", + result += sx_call("events-post-search-sentinel", page=str(page), next_url=next_url) elif search_posts: - result += sexp_call("events-post-search-end") + result += sx_call("events-post-search-end") return result @@ -2885,9 +2885,9 @@ def _entry_admin_header_html(ctx: dict, *, oob: bool = False) -> str: # Nav: ticket_types link nav_html = _entry_admin_nav_html(ctx) - return sexp_call("menu-row-sx", id="entry-admin-row", level=6, + return sx_call("menu-row-sx", id="entry-admin-row", level=6, link_href=link_href, link_label="admin", icon="fa fa-cog", - nav=SexpExpr(nav_html) if nav_html else None, child_id="entry-admin-header-child", oob=oob) + nav=SxExpr(nav_html) if nav_html else None, child_id="entry-admin-header-child", oob=oob) def _entry_admin_nav_html(ctx: dict) -> str: @@ -2909,7 +2909,7 @@ def _entry_admin_nav_html(ctx: dict) -> str: href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get", calendar_slug=cal_slug, entry_id=entry.id, year=year, month=month, day=day) - return sexp_call("nav-link", href=href, label="ticket_types", + return sx_call("nav-link", href=href, label="ticket_types", select_colours=select_colours) @@ -2933,7 +2933,7 @@ def _entry_admin_main_panel_html(ctx: dict) -> str: href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get", calendar_slug=cal_slug, entry_id=entry.id, year=year, month=month, day=day) - return sexp_call("nav-link", href=href, label="ticket_types", + return sx_call("nav-link", href=href, label="ticket_types", select_colours=select_colours, aclass=nav_btn, is_selected=False) @@ -2943,14 +2943,14 @@ async def render_entry_admin_page(ctx: dict) -> str: content = _entry_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = post_admin_header_sexp(ctx, slug, selected="calendars") - child = (admin_hdr + _calendar_header_sexp(ctx) + _day_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") + child = (admin_hdr + _calendar_header_sx(ctx) + _day_header_sx(ctx) + _entry_header_html(ctx) + _entry_admin_header_html(ctx)) - hdr = root_hdr + post_hdr + header_child_sexp(child) - nav_html = sexp_call("events-admin-placeholder-nav") - return full_page_sexp(ctx, header_rows=hdr, content=content, menu=nav_html) + hdr = root_hdr + post_hdr + header_child_sx(child) + nav_html = sx_call("events-admin-placeholder-nav") + return full_page_sx(ctx, header_rows=hdr, content=content, menu=nav_html) async def render_entry_admin_oob(ctx: dict) -> str: @@ -2958,9 +2958,9 @@ async def render_entry_admin_oob(ctx: dict) -> str: content = _entry_admin_main_panel_html(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (post_admin_header_sexp(ctx, slug, oob=True, selected="calendars") + oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars") + _entry_header_html(ctx, oob=True)) - oobs += oob_header_sexp("entry-header-child", "entry-admin-header-child", + oobs += oob_header_sx("entry-header-child", "entry-admin-header-child", _entry_admin_header_html(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child", @@ -2968,8 +2968,8 @@ async def render_entry_admin_oob(ctx: dict) -> str: "day-row", "day-header-child", "entry-row", "entry-header-child", "entry-admin-row", "entry-admin-header-child") - nav_html = sexp_call("events-admin-placeholder-nav") - return oob_page_sexp(oobs=oobs, content=content, menu=nav_html) + nav_html = sx_call("events-admin-placeholder-nav") + return oob_page_sx(oobs=oobs, content=content, menu=nav_html) # =========================================================================== @@ -3000,8 +3000,8 @@ def _slot_header_html(ctx: dict, *, oob: bool = False) -> str: f'' ) - return sexp_call("menu-row-sx", id="slot-row", level=5, - link_label_content=SexpExpr(label_inner), + return sx_call("menu-row-sx", id="slot-row", level=5, + link_label_content=SxExpr(label_inner), child_id="slot-header-child", oob=oob) @@ -3014,13 +3014,13 @@ async def render_slot_page(ctx: dict) -> str: content = render_slot_main_panel(slot, calendar) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = root_header_sexp(ctx) - post_hdr = _post_header_sexp(ctx) - admin_hdr = post_admin_header_sexp(ctx, slug, selected="calendars") - child = (admin_hdr + _calendar_header_sexp(ctx) + _calendar_admin_header_sexp(ctx) + root_hdr = root_header_sx(ctx) + post_hdr = _post_header_sx(ctx) + admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars") + child = (admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx) + _slot_header_html(ctx)) - hdr = root_hdr + post_hdr + header_child_sexp(child) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_hdr + post_hdr + header_child_sx(child) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_slot_oob(ctx: dict) -> str: @@ -3032,16 +3032,16 @@ async def render_slot_oob(ctx: dict) -> str: content = render_slot_main_panel(slot, calendar) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (post_admin_header_sexp(ctx, slug, oob=True, selected="calendars") - + _calendar_admin_header_sexp(ctx, oob=True)) - oobs += oob_header_sexp("calendar-admin-header-child", "slot-header-child", + oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars") + + _calendar_admin_header_sx(ctx, oob=True)) + oobs += oob_header_sx("calendar-admin-header-child", "slot-header-child", _slot_header_html(ctx)) oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child", "calendar-row", "calendar-header-child", "calendar-admin-row", "calendar-admin-header-child", "slot-row", "slot-header-child") - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # =========================================================================== @@ -3075,23 +3075,23 @@ def render_slot_edit_form(slot, calendar) -> str: ("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")] all_checked = all(getattr(slot, k, False) for k, _ in day_keys) - days_parts = [sexp_call("events-day-all-checkbox", + days_parts = [sx_call("events-day-all-checkbox", checked="checked" if all_checked else None)] for key, label in day_keys: checked = getattr(slot, key, False) - days_parts.append(sexp_call("events-day-checkbox", + days_parts.append(sx_call("events-day-checkbox", name=key, label=label, checked="checked" if checked else None)) days_html = "".join(days_parts) flexible = getattr(slot, "flexible", False) - return sexp_call("events-slot-edit-form", + return sx_call("events-slot-edit-form", slot_id=str(sid), list_container=list_container, put_url=put_url, cancel_url=cancel_url, csrf=csrf, name_val=slot.name or "", cost_val=cost_val, start_val=start_val, end_val=end_val, - desc_val=desc_val, days=SexpExpr(days_html), + desc_val=desc_val, days=SxExpr(days_html), flexible_checked="checked" if flexible else None, action_btn=action_btn, cancel_btn=cancel_btn) @@ -3118,14 +3118,14 @@ def render_slot_add_form(calendar) -> str: # Days checkboxes (all unchecked for add) day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"), ("fri", "Fri"), ("sat", "Sat"), ("sun", "Sun")] - days_parts = [sexp_call("events-day-all-checkbox", checked=None)] + days_parts = [sx_call("events-day-all-checkbox", checked=None)] for key, label in day_keys: - days_parts.append(sexp_call("events-day-checkbox", name=key, label=label, checked=None)) + days_parts.append(sx_call("events-day-checkbox", name=key, label=label, checked=None)) days_html = "".join(days_parts) - return sexp_call("events-slot-add-form", + return sx_call("events-slot-add-form", post_url=post_url, csrf=csrf_hdr, - days=SexpExpr(days_html), + days=SxExpr(days_html), action_btn=action_btn, cancel_btn=cancel_btn, cancel_url=cancel_url) @@ -3139,7 +3139,7 @@ def render_slot_add_button(calendar) -> str: cal_slug = getattr(calendar, "slug", "") add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug) - return sexp_call("events-slot-add-button", pre_action=pre_action, add_url=add_url) + return sx_call("events-slot-add-button", pre_action=pre_action, add_url=add_url) # =========================================================================== @@ -3165,14 +3165,14 @@ def render_entry_add_form(calendar, day, month, year, day_slots) -> str: # Slot picker if day_slots: options_html = _slot_options_html(day_slots) - slot_picker_html = sexp_call("events-slot-picker", - id="entry-slot-new", options=SexpExpr(options_html)) + slot_picker_html = sx_call("events-slot-picker", + id="entry-slot-new", options=SxExpr(options_html)) else: - slot_picker_html = sexp_call("events-no-slots") + slot_picker_html = sx_call("events-no-slots") - html = sexp_call("events-entry-add-form", + html = sx_call("events-entry-add-form", post_url=post_url, csrf=csrf, - slot_picker=SexpExpr(slot_picker_html), + slot_picker=SxExpr(slot_picker_html), action_btn=action_btn, cancel_btn=cancel_btn, cancel_url=cancel_url) return html + _SLOT_PICKER_JS @@ -3188,7 +3188,7 @@ def render_entry_add_button(calendar, day, month, year) -> str: add_url = url_for("calendar.day.calendar_entries.add_form", calendar_slug=cal_slug, day=day, month=month, year=year) - return sexp_call("events-entry-add-button", pre_action=pre_action, add_url=add_url) + return sx_call("events-entry-add-button", pre_action=pre_action, add_url=add_url) # =========================================================================== @@ -3214,11 +3214,11 @@ def _ticket_types_header_html(ctx: dict, *, oob: bool = False) -> str: label_html = '
ticket types
' - nav_html = sexp_call("events-admin-placeholder-nav") + nav_html = sx_call("events-admin-placeholder-nav") - return sexp_call("menu-row-sx", id="ticket_types-row", level=7, - link_href=link_href, link_label_content=SexpExpr(label_html), - nav=SexpExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child", oob=oob) + return sx_call("menu-row-sx", id="ticket_types-row", level=7, + link_href=link_href, link_label_content=SxExpr(label_html), + nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child", oob=oob) async def render_ticket_types_page(ctx: dict) -> str: @@ -3230,14 +3230,14 @@ async def render_ticket_types_page(ctx: dict) -> str: month = ctx.get("month") year = ctx.get("year") content = render_ticket_types_table(ticket_types, entry, calendar, day, month, year) - hdr = root_header_sexp(ctx) - child = (_post_header_sexp(ctx) - + _calendar_header_sexp(ctx) + _day_header_sexp(ctx) + hdr = root_header_sx(ctx) + child = (_post_header_sx(ctx) + + _calendar_header_sx(ctx) + _day_header_sx(ctx) + _entry_header_html(ctx) + _entry_admin_header_html(ctx) + _ticket_types_header_html(ctx)) - hdr += header_child_sexp(child) - nav_html = sexp_call("events-admin-placeholder-nav") - return full_page_sexp(ctx, header_rows=hdr, content=content, menu=nav_html) + hdr += header_child_sx(child) + nav_html = sx_call("events-admin-placeholder-nav") + return full_page_sx(ctx, header_rows=hdr, content=content, menu=nav_html) async def render_ticket_types_oob(ctx: dict) -> str: @@ -3250,10 +3250,10 @@ async def render_ticket_types_oob(ctx: dict) -> str: year = ctx.get("year") content = render_ticket_types_table(ticket_types, entry, calendar, day, month, year) oobs = _entry_admin_header_html(ctx, oob=True) - oobs += oob_header_sexp("entry-admin-header-child", "ticket_types-header-child", + oobs += oob_header_sx("entry-admin-header-child", "ticket_types-header-child", _ticket_types_header_html(ctx)) - nav_html = sexp_call("events-admin-placeholder-nav") - return oob_page_sexp(oobs=oobs, content=content, menu=nav_html) + nav_html = sx_call("events-admin-placeholder-nav") + return oob_page_sx(oobs=oobs, content=content, menu=nav_html) # =========================================================================== @@ -3288,11 +3288,11 @@ def _ticket_type_header_html(ctx: dict, *, oob: bool = False) -> str: f'' ) - nav_html = sexp_call("events-admin-placeholder-nav") + nav_html = sx_call("events-admin-placeholder-nav") - return sexp_call("menu-row-sx", id="ticket_type-row", level=8, - link_href=link_href, link_label_content=SexpExpr(label_html), - nav=SexpExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child-inner", oob=oob) + return sx_call("menu-row-sx", id="ticket_type-row", level=8, + link_href=link_href, link_label_content=SxExpr(label_html), + nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child-inner", oob=oob) async def render_ticket_type_page(ctx: dict) -> str: @@ -3304,14 +3304,14 @@ async def render_ticket_type_page(ctx: dict) -> str: month = ctx.get("month") year = ctx.get("year") content = render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year) - hdr = root_header_sexp(ctx) - child = (_post_header_sexp(ctx) - + _calendar_header_sexp(ctx) + _day_header_sexp(ctx) + hdr = root_header_sx(ctx) + child = (_post_header_sx(ctx) + + _calendar_header_sx(ctx) + _day_header_sx(ctx) + _entry_header_html(ctx) + _entry_admin_header_html(ctx) + _ticket_types_header_html(ctx) + _ticket_type_header_html(ctx)) - hdr += header_child_sexp(child) - nav_html = sexp_call("events-admin-placeholder-nav") - return full_page_sexp(ctx, header_rows=hdr, content=content, menu=nav_html) + hdr += header_child_sx(child) + nav_html = sx_call("events-admin-placeholder-nav") + return full_page_sx(ctx, header_rows=hdr, content=content, menu=nav_html) async def render_ticket_type_oob(ctx: dict) -> str: @@ -3324,10 +3324,10 @@ async def render_ticket_type_oob(ctx: dict) -> str: year = ctx.get("year") content = render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year) oobs = _ticket_types_header_html(ctx, oob=True) - oobs += oob_header_sexp("ticket_types-header-child", "ticket_type-header-child", + oobs += oob_header_sx("ticket_types-header-child", "ticket_type-header-child", _ticket_type_header_html(ctx)) - nav_html = sexp_call("events-admin-placeholder-nav") - return oob_page_sexp(oobs=oobs, content=content, menu=nav_html) + nav_html = sx_call("events-admin-placeholder-nav") + return oob_page_sx(oobs=oobs, content=content, menu=nav_html) # =========================================================================== @@ -3358,7 +3358,7 @@ def render_ticket_type_edit_form(ticket_type, entry, calendar, day, month, year) cost_val = f"{cost:.2f}" if cost is not None else "" count = getattr(ticket_type, "count", 0) - return sexp_call("events-ticket-type-edit-form", + return sx_call("events-ticket-type-edit-form", ticket_id=str(tid), list_container=list_container, put_url=put_url, cancel_url=cancel_url, csrf=csrf, name_val=ticket_type.name or "", @@ -3389,7 +3389,7 @@ def render_ticket_type_add_form(entry, calendar, day, month, year) -> str: year=year, month=month, day=day) csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}' - return sexp_call("events-ticket-type-add-form", + return sx_call("events-ticket-type-add-form", post_url=post_url, csrf=csrf_hdr, action_btn=action_btn, cancel_btn=cancel_btn, cancel_url=cancel_url) @@ -3407,7 +3407,7 @@ def render_ticket_type_add_button(entry, calendar, day, month, year) -> str: calendar_slug=cal_slug, entry_id=entry.id, year=year, month=month, day=day) - return sexp_call("events-ticket-type-add-button", + return sx_call("events-ticket-type-add-button", action_btn=action_btn, add_url=add_url) @@ -3435,12 +3435,12 @@ def render_fragment_container_cards(batch, post_ids, slug_map) -> str: time_str = entry.start_at.strftime("%H:%M") if entry.end_at: time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}" - cards_html += sexp_call("events-frag-entry-card", + cards_html += sx_call("events-frag-entry-card", href=events_url(_entry_path), name=entry.name, date_str=entry.start_at.strftime("%a, %b %d"), time_str=time_str) - parts.append(sexp_call("events-frag-entries-widget", cards=SexpExpr(cards_html))) + parts.append(sx_call("events-frag-entries-widget", cards=SxExpr(cards_html))) parts.append(f"") return "\n".join(parts) @@ -3465,17 +3465,17 @@ def render_fragment_account_tickets(tickets) -> str: type_name = "" if getattr(ticket, "ticket_type_name", None): type_name = f'· {escape(ticket.ticket_type_name)}' - badge_html = sexp_call("events-frag-ticket-badge", + badge_html = sx_call("events-frag-ticket-badge", state=getattr(ticket, "state", "")) - items_html += sexp_call("events-frag-ticket-item", + items_html += sx_call("events-frag-ticket-item", href=href, entry_name=ticket.entry_name, date_str=date_str, calendar_name=cal_name, type_name=type_name, badge=badge_html) - body = sexp_call("events-frag-tickets-list", items=SexpExpr(items_html)) + body = sx_call("events-frag-tickets-list", items=SxExpr(items_html)) else: - body = sexp_call("events-frag-tickets-empty") + body = sx_call("events-frag-tickets-empty") - return sexp_call("events-frag-tickets-panel", items=SexpExpr(body)) + return sx_call("events-frag-tickets-panel", items=SxExpr(body)) # =========================================================================== @@ -3498,15 +3498,15 @@ def render_fragment_account_bookings(bookings) -> str: cost_str = "" if getattr(booking, "cost", None): cost_str = f'· £{escape(str(booking.cost))}' - badge_html = sexp_call("events-frag-booking-badge", + badge_html = sx_call("events-frag-booking-badge", state=getattr(booking, "state", "")) - items_html += sexp_call("events-frag-booking-item", + items_html += sx_call("events-frag-booking-item", name=booking.name, date_str=date_str + date_str_extra, calendar_name=cal_name, cost_str=cost_str, badge=badge_html) - body = sexp_call("events-frag-bookings-list", items=SexpExpr(items_html)) + body = sx_call("events-frag-bookings-list", items=SxExpr(items_html)) else: - body = sexp_call("events-frag-bookings-empty") + body = sx_call("events-frag-bookings-empty") - return sexp_call("events-frag-bookings-panel", items=SexpExpr(body)) + return sx_call("events-frag-bookings-panel", items=SxExpr(body)) diff --git a/events/sexp/tickets.sexpr b/events/sx/tickets.sx similarity index 100% rename from events/sexp/tickets.sexpr rename to events/sx/tickets.sx diff --git a/federation/app.py b/federation/app.py index c211f94..76180c0 100644 --- a/federation/app.py +++ b/federation/app.py @@ -1,6 +1,6 @@ from __future__ import annotations import path_setup # noqa: F401 # adds shared/ to sys.path -import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file +import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file from pathlib import Path from quart import g, request @@ -95,8 +95,8 @@ def create_app() -> "Quart": @app.get("/") async def home(): from quart import make_response - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_federation_home + from shared.sx.page import get_template_context + from sx.sx_components import render_federation_home ctx = await get_template_context() html = await render_federation_home(ctx) diff --git a/federation/bp/auth/routes.py b/federation/bp/auth/routes.py index 21cc67b..f634d30 100644 --- a/federation/bp/auth/routes.py +++ b/federation/bp/auth/routes.py @@ -99,8 +99,8 @@ def register(url_prefix="/auth"): # If there's a pending redirect (e.g. OAuth authorize), follow it redirect_url = pop_login_redirect_target() return redirect(redirect_url) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_login_page + from shared.sx.page import get_template_context + from sx.sx_components import render_login_page ctx = await get_template_context() return await render_login_page(ctx) @@ -111,8 +111,8 @@ def register(url_prefix="/auth"): is_valid, email = validate_email(email_input) if not is_valid: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_login_page + from shared.sx.page import get_template_context + from sx.sx_components import render_login_page ctx = await get_template_context(error="Please enter a valid email address.", email=email_input) return await render_login_page(ctx), 400 @@ -132,8 +132,8 @@ def register(url_prefix="/auth"): "Please try again in a moment." ) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_check_email_page + from shared.sx.page import get_template_context + from sx.sx_components import render_check_email_page ctx = await get_template_context(email=email, email_error=email_error) return await render_check_email_page(ctx) @@ -148,15 +148,15 @@ def register(url_prefix="/auth"): user, error = await validate_magic_link(s, token) if error: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_login_page + from shared.sx.page import get_template_context + from sx.sx_components import render_login_page ctx = await get_template_context(error=error) return await render_login_page(ctx), 400 user_id = user.id except Exception: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_login_page + from shared.sx.page import get_template_context + from sx.sx_components import render_login_page ctx = await get_template_context(error="Could not sign you in right now. Please try again.") return await render_login_page(ctx), 502 diff --git a/federation/bp/fragments/routes.py b/federation/bp/fragments/routes.py index 2228c36..9114f60 100644 --- a/federation/bp/fragments/routes.py +++ b/federation/bp/fragments/routes.py @@ -1,6 +1,6 @@ """Federation app fragment endpoints. -Exposes sexp fragments at ``/internal/fragments/`` for consumption +Exposes sx fragments at ``/internal/fragments/`` for consumption by other coop apps via the fragment client. """ @@ -25,15 +25,15 @@ def register(): async def get_fragment(fragment_type: str): handler = _handlers.get(fragment_type) if handler is None: - return Response("", status=200, content_type="text/sexp") + return Response("", status=200, content_type="text/sx") src = await handler() - return Response(src, status=200, content_type="text/sexp") + return Response(src, status=200, content_type="text/sx") # --- link-card fragment: actor profile preview card -------------------------- - def _federation_link_card_sexp(actor, link: str) -> str: - from shared.sexp.helpers import sexp_call - return sexp_call("link-card", + def _federation_link_card_sx(actor, link: str) -> str: + from shared.sx.helpers import sx_call + return sx_call("link-card", link=link, title=actor.display_name or actor.preferred_username, image=None, @@ -59,7 +59,7 @@ def register(): parts.append(f"") actor = await services.federation.get_actor_by_username(g.s, u) if actor: - parts.append(_federation_link_card_sexp( + parts.append(_federation_link_card_sx( actor, federation_url(f"/users/{actor.preferred_username}"), )) return "\n".join(parts) @@ -71,7 +71,7 @@ def register(): actor = await services.federation.get_actor_by_username(g.s, lookup) if not actor: return "" - return _federation_link_card_sexp( + return _federation_link_card_sx( actor, federation_url(f"/users/{actor.preferred_username}"), ) diff --git a/federation/bp/identity/routes.py b/federation/bp/identity/routes.py index 56b3550..db8d551 100644 --- a/federation/bp/identity/routes.py +++ b/federation/bp/identity/routes.py @@ -39,8 +39,8 @@ def register(url_prefix="/identity"): if actor: return redirect(url_for("activitypub.actor_profile", username=actor.preferred_username)) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_choose_username_page + from shared.sx.page import get_template_context + from sx.sx_components import render_choose_username_page ctx = await get_template_context() ctx["actor"] = actor return await render_choose_username_page(ctx) @@ -71,8 +71,8 @@ def register(url_prefix="/identity"): error = "This username is already taken." if error: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_choose_username_page + from shared.sx.page import get_template_context + from sx.sx_components import render_choose_username_page ctx = await get_template_context(error=error, username=username) ctx["actor"] = None return await render_choose_username_page(ctx), 400 diff --git a/federation/bp/social/routes.py b/federation/bp/social/routes.py index 75f70ad..04dff3c 100644 --- a/federation/bp/social/routes.py +++ b/federation/bp/social/routes.py @@ -7,7 +7,7 @@ from datetime import datetime from quart import Blueprint, request, g, redirect, url_for, abort, Response from shared.services.registry import services -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response log = logging.getLogger(__name__) @@ -40,8 +40,8 @@ def register(url_prefix="/social"): return redirect(url_for("auth.login_form")) actor = _require_actor() items = await services.federation.get_home_timeline(g.s, actor.id) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_timeline_page + from shared.sx.page import get_template_context + from sx.sx_components import render_timeline_page ctx = await get_template_context() return await render_timeline_page(ctx, items, "home", actor) @@ -58,16 +58,16 @@ def register(url_prefix="/social"): items = await services.federation.get_home_timeline( g.s, actor.id, before=before, ) - from sexp.sexp_components import render_timeline_items - sexp_src = await render_timeline_items(items, "home", actor) - return sexp_response(sexp_src) + from sx.sx_components import render_timeline_items + sx_src = await render_timeline_items(items, "home", actor) + return sx_response(sx_src) @bp.get("/public") async def public_timeline(): items = await services.federation.get_public_timeline(g.s) actor = getattr(g, "_social_actor", None) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_timeline_page + from shared.sx.page import get_template_context + from sx.sx_components import render_timeline_page ctx = await get_template_context() return await render_timeline_page(ctx, items, "public", actor) @@ -82,9 +82,9 @@ def register(url_prefix="/social"): pass items = await services.federation.get_public_timeline(g.s, before=before) actor = getattr(g, "_social_actor", None) - from sexp.sexp_components import render_timeline_items - sexp_src = await render_timeline_items(items, "public", actor) - return sexp_response(sexp_src) + from sx.sx_components import render_timeline_items + sx_src = await render_timeline_items(items, "public", actor) + return sx_response(sx_src) # -- Compose -------------------------------------------------------------- @@ -92,8 +92,8 @@ def register(url_prefix="/social"): async def compose_form(): actor = _require_actor() reply_to = request.args.get("reply_to") - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_compose_page + from shared.sx.page import get_template_context + from sx.sx_components import render_compose_page ctx = await get_template_context() return await render_compose_page(ctx, actor, reply_to) @@ -138,8 +138,8 @@ def register(url_prefix="/social"): g.s, actor.preferred_username, page=1, per_page=1000, ) followed_urls = {a.actor_url for a in following} - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_search_page + from shared.sx.page import get_template_context + from sx.sx_components import render_search_page ctx = await get_template_context() return await render_search_page(ctx, query, actors, total, 1, followed_urls, actor) @@ -160,9 +160,9 @@ def register(url_prefix="/social"): g.s, actor.preferred_username, page=1, per_page=1000, ) followed_urls = {a.actor_url for a in following} - from sexp.sexp_components import render_search_results - sexp_src = await render_search_results(actors, query, page, followed_urls, actor) - return sexp_response(sexp_src) + from sx.sx_components import render_search_results + sx_src = await render_search_results(actors, query, page, followed_urls, actor) + return sx_response(sx_src) @bp.post("/follow") async def follow(): @@ -204,8 +204,8 @@ def register(url_prefix="/social"): list_type = "followers" else: list_type = "following" - from sexp.sexp_components import render_actor_card - return sexp_response(render_actor_card(remote_dto, actor, followed_urls, list_type=list_type)) + from sx.sx_components import render_actor_card + return sx_response(render_actor_card(remote_dto, actor, followed_urls, list_type=list_type)) # -- Interactions --------------------------------------------------------- @@ -293,8 +293,8 @@ def register(url_prefix="/social"): ).limit(1) )).scalar()) - from sexp.sexp_components import render_interaction_buttons - return sexp_response(render_interaction_buttons( + from sx.sx_components import render_interaction_buttons + return sx_response(render_interaction_buttons( object_id=object_id, author_inbox=author_inbox, like_count=like_count, @@ -312,8 +312,8 @@ def register(url_prefix="/social"): actors, total = await services.federation.get_following( g.s, actor.preferred_username, ) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_following_page + from shared.sx.page import get_template_context + from sx.sx_components import render_following_page ctx = await get_template_context() return await render_following_page(ctx, actors, total, actor) @@ -324,9 +324,9 @@ def register(url_prefix="/social"): actors, total = await services.federation.get_following( g.s, actor.preferred_username, page=page, ) - from sexp.sexp_components import render_following_items - sexp_src = await render_following_items(actors, page, actor) - return sexp_response(sexp_src) + from sx.sx_components import render_following_items + sx_src = await render_following_items(actors, page, actor) + return sx_response(sx_src) @bp.get("/followers") async def followers_list(): @@ -339,8 +339,8 @@ def register(url_prefix="/social"): g.s, actor.preferred_username, page=1, per_page=1000, ) followed_urls = {a.actor_url for a in following} - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_followers_page + from shared.sx.page import get_template_context + from sx.sx_components import render_followers_page ctx = await get_template_context() return await render_followers_page(ctx, actors, total, followed_urls, actor) @@ -355,9 +355,9 @@ def register(url_prefix="/social"): g.s, actor.preferred_username, page=1, per_page=1000, ) followed_urls = {a.actor_url for a in following} - from sexp.sexp_components import render_followers_items - sexp_src = await render_followers_items(actors, page, followed_urls, actor) - return sexp_response(sexp_src) + from sx.sx_components import render_followers_items + sx_src = await render_followers_items(actors, page, followed_urls, actor) + return sx_response(sx_src) @bp.get("/actor/") async def actor_timeline(id: int): @@ -388,8 +388,8 @@ def register(url_prefix="/social"): ) ).scalar_one_or_none() is_following = existing is not None - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_actor_timeline_page + from shared.sx.page import get_template_context + from sx.sx_components import render_actor_timeline_page ctx = await get_template_context() return await render_actor_timeline_page(ctx, remote_dto, items, is_following, actor) @@ -406,9 +406,9 @@ def register(url_prefix="/social"): items = await services.federation.get_actor_timeline( g.s, id, before=before, ) - from sexp.sexp_components import render_actor_timeline_items - sexp_src = await render_actor_timeline_items(items, id, actor) - return sexp_response(sexp_src) + from sx.sx_components import render_actor_timeline_items + sx_src = await render_actor_timeline_items(items, id, actor) + return sx_response(sx_src) # -- Notifications -------------------------------------------------------- @@ -417,8 +417,8 @@ def register(url_prefix="/social"): actor = _require_actor() items = await services.federation.get_notifications(g.s, actor.id) await services.federation.mark_notifications_read(g.s, actor.id) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_notifications_page + from shared.sx.page import get_template_context + from sx.sx_components import render_notifications_page ctx = await get_template_context() return await render_notifications_page(ctx, items, actor) @@ -429,7 +429,7 @@ def register(url_prefix="/social"): return Response("0", content_type="text/plain") count = await services.federation.unread_notification_count(g.s, actor.id) if count > 0: - from shared.sexp.jinja_bridge import render as render_comp + from shared.sx.jinja_bridge import render as render_comp return Response( render_comp("notification-badge", count=str(count)), content_type="text/html", diff --git a/federation/sexp/__init__.py b/federation/sx/__init__.py similarity index 100% rename from federation/sexp/__init__.py rename to federation/sx/__init__.py diff --git a/federation/sexp/auth.sexpr b/federation/sx/auth.sx similarity index 100% rename from federation/sexp/auth.sexpr rename to federation/sx/auth.sx diff --git a/federation/sexp/notifications.sexpr b/federation/sx/notifications.sx similarity index 100% rename from federation/sexp/notifications.sexpr rename to federation/sx/notifications.sx diff --git a/federation/sexp/profile.sexpr b/federation/sx/profile.sx similarity index 100% rename from federation/sexp/profile.sexpr rename to federation/sx/profile.sx diff --git a/federation/sexp/search.sexpr b/federation/sx/search.sx similarity index 100% rename from federation/sexp/search.sexpr rename to federation/sx/search.sx diff --git a/federation/sexp/social.sexpr b/federation/sx/social.sx similarity index 100% rename from federation/sexp/social.sexpr rename to federation/sx/social.sx diff --git a/federation/sexp/sexp_components.py b/federation/sx/sx_components.py similarity index 75% rename from federation/sexp/sexp_components.py rename to federation/sx/sx_components.py index a53a978..050157a 100644 --- a/federation/sexp/sexp_components.py +++ b/federation/sx/sx_components.py @@ -10,13 +10,13 @@ import os from typing import Any from markupsafe import escape -from shared.sexp.jinja_bridge import load_service_components -from shared.sexp.helpers import ( - sexp_call, SexpExpr, - root_header_sexp, full_page_sexp, header_child_sexp, +from shared.sx.jinja_bridge import load_service_components +from shared.sx.helpers import ( + sx_call, SxExpr, + root_header_sx, full_page_sx, header_child_sx, ) -# Load federation-specific .sexpr components at import time +# Load federation-specific .sx components at import time load_service_components(os.path.dirname(os.path.dirname(__file__))) @@ -24,13 +24,13 @@ load_service_components(os.path.dirname(os.path.dirname(__file__))) # Social header nav # --------------------------------------------------------------------------- -def _social_nav_sexp(actor: Any) -> str: +def _social_nav_sx(actor: Any) -> str: """Build the social header nav bar content.""" from quart import url_for, request if not actor: choose_url = url_for("identity.choose_username_form") - return sexp_call("federation-nav-choose-username", url=choose_url) + return sx_call("federation-nav-choose-username", url=choose_url) links = [ ("social.home_timeline", "Timeline"), @@ -45,7 +45,7 @@ def _social_nav_sexp(actor: Any) -> str: for endpoint, label in links: href = url_for(endpoint) bold = " font-bold" if request.path == href else "" - parts.append(sexp_call( + parts.append(sx_call( "federation-nav-link", href=href, cls=f"px-2 py-1 rounded hover:bg-stone-200{bold}", @@ -56,7 +56,7 @@ def _social_nav_sexp(actor: Any) -> str: notif_url = url_for("social.notifications") notif_count_url = url_for("social.notification_count") notif_bold = " font-bold" if request.path == notif_url else "" - parts.append(sexp_call( + parts.append(sx_call( "federation-nav-notification-link", href=notif_url, cls=f"px-2 py-1 rounded hover:bg-stone-200 relative{notif_bold}", @@ -65,31 +65,31 @@ def _social_nav_sexp(actor: Any) -> str: # Profile link profile_url = url_for("activitypub.actor_profile", username=actor.preferred_username) - parts.append(sexp_call( + parts.append(sx_call( "federation-nav-link", href=profile_url, cls="px-2 py-1 rounded hover:bg-stone-200", label=f"@{actor.preferred_username}", )) - items_sexp = "(<> " + " ".join(parts) + ")" - return sexp_call("federation-nav-bar", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(parts) + ")" + return sx_call("federation-nav-bar", items=SxExpr(items_sx)) -def _social_header_sexp(actor: Any) -> str: +def _social_header_sx(actor: Any) -> str: """Build the social section header row.""" - nav_sexp = _social_nav_sexp(actor) - return sexp_call("federation-social-header", nav=SexpExpr(nav_sexp)) + nav_sx = _social_nav_sx(actor) + return sx_call("federation-social-header", nav=SxExpr(nav_sx)) def _social_page(ctx: dict, actor: Any, *, content: str, title: str = "Rose Ash", meta_html: str = "") -> str: """Render a social page with header and content.""" - hdr = root_header_sexp(ctx) - social_hdr = _social_header_sexp(actor) - child = header_child_sexp(social_hdr) + hdr = root_header_sx(ctx) + social_hdr = _social_header_sx(actor) + child = header_child_sx(social_hdr) header_rows = "(<> " + hdr + " " + child + ")" - return full_page_sexp(ctx, header_rows=header_rows, content=content, + return full_page_sx(ctx, header_rows=header_rows, content=content, meta_html=meta_html or f'{escape(title)}') @@ -97,7 +97,7 @@ def _social_page(ctx: dict, actor: Any, *, content: str, # Post card # --------------------------------------------------------------------------- -def _interaction_buttons_sexp(item: Any, actor: Any) -> str: +def _interaction_buttons_sx(item: Any, actor: Any) -> str: """Render like/boost/reply buttons for a post.""" from shared.browser.app.csrf import generate_csrf_token from quart import url_for @@ -130,31 +130,31 @@ def _interaction_buttons_sexp(item: Any, actor: Any) -> str: boost_cls = "hover:text-green-600" reply_url = url_for("social.compose_form", reply_to=oid) if oid else "" - reply_sexp = sexp_call("federation-reply-link", url=reply_url) if reply_url else "" + reply_sx = sx_call("federation-reply-link", url=reply_url) if reply_url else "" - like_form = sexp_call( + like_form = sx_call( "federation-like-form", action=like_action, target=target, oid=oid, ainbox=ainbox, csrf=csrf, cls=f"flex items-center gap-1 {like_cls}", icon=like_icon, count=str(lcount), ) - boost_form = sexp_call( + boost_form = sx_call( "federation-boost-form", action=boost_action, target=target, oid=oid, ainbox=ainbox, csrf=csrf, cls=f"flex items-center gap-1 {boost_cls}", count=str(bcount), ) - return sexp_call( + return sx_call( "federation-interaction-buttons", - like=SexpExpr(like_form), - boost=SexpExpr(boost_form), - reply=SexpExpr(reply_sexp) if reply_sexp else None, + like=SxExpr(like_form), + boost=SxExpr(boost_form), + reply=SxExpr(reply_sx) if reply_sx else None, ) -def _post_card_sexp(item: Any, actor: Any) -> str: +def _post_card_sx(item: Any, actor: Any) -> str: """Render a single timeline post card.""" boosted_by = getattr(item, "boosted_by", None) actor_icon = getattr(item, "actor_icon", None) @@ -167,15 +167,15 @@ def _post_card_sexp(item: Any, actor: Any) -> str: url = getattr(item, "url", None) post_type = getattr(item, "post_type", "") - boost_sexp = sexp_call( + boost_sx = sx_call( "federation-boost-label", name=str(escape(boosted_by)), ) if boosted_by else "" if actor_icon: - avatar = sexp_call("federation-avatar-img", src=actor_icon, cls="w-10 h-10 rounded-full") + avatar = sx_call("federation-avatar-img", src=actor_icon, cls="w-10 h-10 rounded-full") else: initial = actor_name[0].upper() if actor_name else "?" - avatar = sexp_call( + avatar = sx_call( "federation-avatar-placeholder", cls="w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm", initial=initial, @@ -185,37 +185,37 @@ def _post_card_sexp(item: Any, actor: Any) -> str: time_str = published.strftime("%b %d, %H:%M") if published else "" if summary: - content_sexp = sexp_call( + content_sx = sx_call( "federation-content-cw", summary=str(escape(summary)), content=content, ) else: - content_sexp = sexp_call("federation-content-plain", content=content) + content_sx = sx_call("federation-content-plain", content=content) - original_sexp = "" + original_sx = "" if url and post_type == "remote": - original_sexp = sexp_call("federation-original-link", url=url) + original_sx = sx_call("federation-original-link", url=url) - interactions_sexp = "" + interactions_sx = "" if actor: oid = getattr(item, "object_id", "") or "" safe_id = oid.replace("/", "_").replace(":", "_") - interactions_sexp = sexp_call( + interactions_sx = sx_call( "federation-interactions-wrap", id=f"interactions-{safe_id}", - buttons=SexpExpr(_interaction_buttons_sexp(item, actor)), + buttons=SxExpr(_interaction_buttons_sx(item, actor)), ) - return sexp_call( + return sx_call( "federation-post-card", - boost=SexpExpr(boost_sexp) if boost_sexp else None, - avatar=SexpExpr(avatar), + boost=SxExpr(boost_sx) if boost_sx else None, + avatar=SxExpr(avatar), actor_name=str(escape(actor_name)), actor_username=str(escape(actor_username)), domain=domain_str, time=time_str, - content=SexpExpr(content_sexp), - original=SexpExpr(original_sexp) if original_sexp else None, - interactions=SexpExpr(interactions_sexp) if interactions_sexp else None, + content=SxExpr(content_sx), + original=SxExpr(original_sx) if original_sx else None, + interactions=SxExpr(interactions_sx) if interactions_sx else None, ) @@ -223,12 +223,12 @@ def _post_card_sexp(item: Any, actor: Any) -> str: # Timeline items (pagination fragment) # --------------------------------------------------------------------------- -def _timeline_items_sexp(items: list, timeline_type: str, actor: Any, +def _timeline_items_sx(items: list, timeline_type: str, actor: Any, actor_id: int | None = None) -> str: """Render timeline items with infinite scroll sentinel.""" from quart import url_for - parts = [_post_card_sexp(item, actor) for item in items] + parts = [_post_card_sx(item, actor) for item in items] if items: last = items[-1] @@ -237,7 +237,7 @@ def _timeline_items_sexp(items: list, timeline_type: str, actor: Any, next_url = url_for("social.actor_timeline_page", id=actor_id, before=before) else: next_url = url_for(f"social.{timeline_type}_timeline_page", before=before) - parts.append(sexp_call("federation-scroll-sentinel", url=next_url)) + parts.append(sx_call("federation-scroll-sentinel", url=next_url)) return "(<> " + " ".join(parts) + ")" if parts else "" @@ -246,7 +246,7 @@ def _timeline_items_sexp(items: list, timeline_type: str, actor: Any, # Search results (pagination fragment) # --------------------------------------------------------------------------- -def _actor_card_sexp(a: Any, actor: Any, followed_urls: set, +def _actor_card_sx(a: Any, actor: Any, followed_urls: set, *, list_type: str = "search") -> str: """Render a single actor card with follow/unfollow button.""" from shared.browser.app.csrf import generate_csrf_token @@ -264,10 +264,10 @@ def _actor_card_sexp(a: Any, actor: Any, followed_urls: set, safe_id = actor_url.replace("/", "_").replace(":", "_") if icon_url: - avatar = sexp_call("federation-actor-avatar-img", src=icon_url, cls="w-12 h-12 rounded-full") + avatar = sx_call("federation-actor-avatar-img", src=icon_url, cls="w-12 h-12 rounded-full") else: initial = (display_name or username)[0].upper() if (display_name or username) else "?" - avatar = sexp_call( + avatar = sx_call( "federation-actor-avatar-placeholder", cls="w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold", initial=initial, @@ -275,69 +275,69 @@ def _actor_card_sexp(a: Any, actor: Any, followed_urls: set, # Name link if (list_type in ("following", "search")) and aid: - name_sexp = sexp_call( + name_sx = sx_call( "federation-actor-name-link", href=url_for("social.actor_timeline", id=aid), name=str(escape(display_name)), ) else: - name_sexp = sexp_call( + name_sx = sx_call( "federation-actor-name-link-external", href=f"https://{domain}/@{username}", name=str(escape(display_name)), ) - summary_sexp = sexp_call("federation-actor-summary", summary=summary) if summary else "" + summary_sx = sx_call("federation-actor-summary", summary=summary) if summary else "" # Follow/unfollow button - button_sexp = "" + button_sx = "" if actor: is_followed = actor_url in (followed_urls or set()) if list_type == "following" or is_followed: - button_sexp = sexp_call( + button_sx = sx_call( "federation-unfollow-button", action=url_for("social.unfollow"), csrf=csrf, actor_url=actor_url, ) else: label = "Follow Back" if list_type == "followers" else "Follow" - button_sexp = sexp_call( + button_sx = sx_call( "federation-follow-button", action=url_for("social.follow"), csrf=csrf, actor_url=actor_url, label=label, ) - return sexp_call( + return sx_call( "federation-actor-card", cls="bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-3 flex items-center gap-4", id=f"actor-{safe_id}", - avatar=SexpExpr(avatar), - name=SexpExpr(name_sexp), + avatar=SxExpr(avatar), + name=SxExpr(name_sx), username=str(escape(username)), domain=str(escape(domain)), - summary=SexpExpr(summary_sexp) if summary_sexp else None, - button=SexpExpr(button_sexp) if button_sexp else None, + summary=SxExpr(summary_sx) if summary_sx else None, + button=SxExpr(button_sx) if button_sx else None, ) -def _search_results_sexp(actors: list, query: str, page: int, +def _search_results_sx(actors: list, query: str, page: int, followed_urls: set, actor: Any) -> str: """Render search results with pagination sentinel.""" from quart import url_for - parts = [_actor_card_sexp(a, actor, followed_urls, list_type="search") for a in actors] + parts = [_actor_card_sx(a, actor, followed_urls, list_type="search") for a in actors] if len(actors) >= 20: next_url = url_for("social.search_page", q=query, page=page + 1) - parts.append(sexp_call("federation-scroll-sentinel", url=next_url)) + parts.append(sx_call("federation-scroll-sentinel", url=next_url)) return "(<> " + " ".join(parts) + ")" if parts else "" -def _actor_list_items_sexp(actors: list, page: int, list_type: str, +def _actor_list_items_sx(actors: list, page: int, list_type: str, followed_urls: set, actor: Any) -> str: """Render actor list items (following/followers) with pagination sentinel.""" from quart import url_for - parts = [_actor_card_sexp(a, actor, followed_urls, list_type=list_type) for a in actors] + parts = [_actor_card_sx(a, actor, followed_urls, list_type=list_type) for a in actors] if len(actors) >= 20: next_url = url_for(f"social.{list_type}_list_page", page=page + 1) - parts.append(sexp_call("federation-scroll-sentinel", url=next_url)) + parts.append(sx_call("federation-scroll-sentinel", url=next_url)) return "(<> " + " ".join(parts) + ")" if parts else "" @@ -345,7 +345,7 @@ def _actor_list_items_sexp(actors: list, page: int, list_type: str, # Notification card # --------------------------------------------------------------------------- -def _notification_sexp(notif: Any) -> str: +def _notification_sx(notif: Any) -> str: """Render a single notification.""" from_name = getattr(notif, "from_actor_name", "?") from_username = getattr(notif, "from_actor_username", "") @@ -360,10 +360,10 @@ def _notification_sexp(notif: Any) -> str: border = " border-l-4 border-l-stone-400" if not read else "" if from_icon: - avatar = sexp_call("federation-avatar-img", src=from_icon, cls="w-8 h-8 rounded-full") + avatar = sx_call("federation-avatar-img", src=from_icon, cls="w-8 h-8 rounded-full") else: initial = from_name[0].upper() if from_name else "?" - avatar = sexp_call( + avatar = sx_call( "federation-avatar-placeholder", cls="w-8 h-8 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xs", initial=initial, @@ -382,19 +382,19 @@ def _notification_sexp(notif: Any) -> str: if ntype == "follow" and app_domain and app_domain != "federation": action += f" on {escape(app_domain)}" - preview_sexp = sexp_call( + preview_sx = sx_call( "federation-notification-preview", preview=str(escape(preview)), ) if preview else "" time_str = created.strftime("%b %d, %H:%M") if created else "" - return sexp_call( + return sx_call( "federation-notification-card", cls=f"bg-white rounded-lg shadow-sm border border-stone-200 p-4{border}", - avatar=SexpExpr(avatar), + avatar=SxExpr(avatar), from_name=str(escape(from_name)), from_username=str(escape(from_username)), from_domain=domain_str, action_text=action, - preview=SexpExpr(preview_sexp) if preview_sexp else None, + preview=SxExpr(preview_sx) if preview_sx else None, time=time_str, ) @@ -405,8 +405,8 @@ def _notification_sexp(notif: Any) -> str: async def render_federation_home(ctx: dict) -> str: """Full page: federation home (minimal).""" - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr) + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr) # --------------------------------------------------------------------------- @@ -423,11 +423,11 @@ async def render_login_page(ctx: dict) -> str: action = url_for("auth.start_login") csrf = generate_csrf_token() - error_sexp = sexp_call("federation-error-banner", error=error) if error else "" + error_sx = sx_call("federation-error-banner", error=error) if error else "" - content = sexp_call( + content = sx_call( "federation-login-form", - error=SexpExpr(error_sexp) if error_sexp else None, + error=SxExpr(error_sx) if error_sx else None, action=action, csrf=csrf, email=str(escape(email)), ) @@ -441,14 +441,14 @@ async def render_check_email_page(ctx: dict) -> str: email = ctx.get("email", "") email_error = ctx.get("email_error") - error_sexp = sexp_call( + error_sx = sx_call( "federation-check-email-error", error=str(escape(email_error)), ) if email_error else "" - content = sexp_call( + content = sx_call( "federation-check-email", email=str(escape(email)), - error=SexpExpr(error_sexp) if error_sexp else None, + error=SxExpr(error_sx) if error_sx else None, ) return _social_page(ctx, None, content=content, @@ -465,18 +465,18 @@ async def render_timeline_page(ctx: dict, items: list, timeline_type: str, from quart import url_for label = "Home" if timeline_type == "home" else "Public" - compose_sexp = "" + compose_sx = "" if actor: compose_url = url_for("social.compose_form") - compose_sexp = sexp_call("federation-compose-button", url=compose_url) + compose_sx = sx_call("federation-compose-button", url=compose_url) - timeline_sexp = _timeline_items_sexp(items, timeline_type, actor) + timeline_sx = _timeline_items_sx(items, timeline_type, actor) - content = sexp_call( + content = sx_call( "federation-timeline-page", label=label, - compose=SexpExpr(compose_sexp) if compose_sexp else None, - timeline=SexpExpr(timeline_sexp) if timeline_sexp else None, + compose=SxExpr(compose_sx) if compose_sx else None, + timeline=SxExpr(timeline_sx) if timeline_sx else None, ) return _social_page(ctx, actor, content=content, @@ -486,7 +486,7 @@ async def render_timeline_page(ctx: dict, items: list, timeline_type: str, async def render_timeline_items(items: list, timeline_type: str, actor: Any, actor_id: int | None = None) -> str: """Pagination fragment: timeline items.""" - return _timeline_items_sexp(items, timeline_type, actor, actor_id) + return _timeline_items_sx(items, timeline_type, actor, actor_id) # --------------------------------------------------------------------------- @@ -501,17 +501,17 @@ async def render_compose_page(ctx: dict, actor: Any, reply_to: str | None) -> st csrf = generate_csrf_token() action = url_for("social.compose_submit") - reply_sexp = "" + reply_sx = "" if reply_to: - reply_sexp = sexp_call( + reply_sx = sx_call( "federation-compose-reply", reply_to=str(escape(reply_to)), ) - content = sexp_call( + content = sx_call( "federation-compose-form", action=action, csrf=csrf, - reply=SexpExpr(reply_sexp) if reply_sexp else None, + reply=SxExpr(reply_sx) if reply_sx else None, ) return _social_page(ctx, actor, content=content, @@ -530,29 +530,29 @@ async def render_search_page(ctx: dict, query: str, actors: list, total: int, search_url = url_for("social.search") search_page_url = url_for("social.search_page") - results_sexp = _search_results_sexp(actors, query, page, followed_urls, actor) + results_sx = _search_results_sx(actors, query, page, followed_urls, actor) - info_sexp = "" + info_sx = "" if query and total: s = "s" if total != 1 else "" - info_sexp = sexp_call( + info_sx = sx_call( "federation-search-info", cls="text-sm text-stone-500 mb-4", text=f"{total} result{s} for {escape(query)}", ) elif query: - info_sexp = sexp_call( + info_sx = sx_call( "federation-search-info", cls="text-stone-500 mb-4", text=f"No results found for {escape(query)}", ) - content = sexp_call( + content = sx_call( "federation-search-page", search_url=search_url, search_page_url=search_page_url, query=str(escape(query)), - info=SexpExpr(info_sexp) if info_sexp else None, - results=SexpExpr(results_sexp) if results_sexp else None, + info=SxExpr(info_sx) if info_sx else None, + results=SxExpr(results_sx) if results_sx else None, ) return _social_page(ctx, actor, content=content, @@ -562,7 +562,7 @@ async def render_search_page(ctx: dict, query: str, actors: list, total: int, async def render_search_results(actors: list, query: str, page: int, followed_urls: set, actor: Any) -> str: """Pagination fragment: search results.""" - return _search_results_sexp(actors, query, page, followed_urls, actor) + return _search_results_sx(actors, query, page, followed_urls, actor) # --------------------------------------------------------------------------- @@ -572,11 +572,11 @@ async def render_search_results(actors: list, query: str, page: int, async def render_following_page(ctx: dict, actors: list, total: int, actor: Any) -> str: """Full page: following list.""" - items_sexp = _actor_list_items_sexp(actors, 1, "following", set(), actor) - content = sexp_call( + items_sx = _actor_list_items_sx(actors, 1, "following", set(), actor) + content = sx_call( "federation-actor-list-page", title="Following", count_str=f"({total})", - items=SexpExpr(items_sexp) if items_sexp else None, + items=SxExpr(items_sx) if items_sx else None, ) return _social_page(ctx, actor, content=content, title="Following \u2014 Rose Ash") @@ -584,17 +584,17 @@ async def render_following_page(ctx: dict, actors: list, total: int, async def render_following_items(actors: list, page: int, actor: Any) -> str: """Pagination fragment: following items.""" - return _actor_list_items_sexp(actors, page, "following", set(), actor) + return _actor_list_items_sx(actors, page, "following", set(), actor) async def render_followers_page(ctx: dict, actors: list, total: int, followed_urls: set, actor: Any) -> str: """Full page: followers list.""" - items_sexp = _actor_list_items_sexp(actors, 1, "followers", followed_urls, actor) - content = sexp_call( + items_sx = _actor_list_items_sx(actors, 1, "followers", followed_urls, actor) + content = sx_call( "federation-actor-list-page", title="Followers", count_str=f"({total})", - items=SexpExpr(items_sexp) if items_sexp else None, + items=SxExpr(items_sx) if items_sx else None, ) return _social_page(ctx, actor, content=content, title="Followers \u2014 Rose Ash") @@ -603,7 +603,7 @@ async def render_followers_page(ctx: dict, actors: list, total: int, async def render_followers_items(actors: list, page: int, followed_urls: set, actor: Any) -> str: """Pagination fragment: followers items.""" - return _actor_list_items_sexp(actors, page, "followers", followed_urls, actor) + return _actor_list_items_sx(actors, page, "followers", followed_urls, actor) # --------------------------------------------------------------------------- @@ -623,50 +623,50 @@ async def render_actor_timeline_page(ctx: dict, remote_actor: Any, items: list, actor_url = getattr(remote_actor, "actor_url", "") if icon_url: - avatar = sexp_call("federation-avatar-img", src=icon_url, cls="w-16 h-16 rounded-full") + avatar = sx_call("federation-avatar-img", src=icon_url, cls="w-16 h-16 rounded-full") else: initial = display_name[0].upper() if display_name else "?" - avatar = sexp_call( + avatar = sx_call( "federation-avatar-placeholder", cls="w-16 h-16 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xl", initial=initial, ) - summary_sexp = sexp_call("federation-profile-summary", summary=summary) if summary else "" + summary_sx = sx_call("federation-profile-summary", summary=summary) if summary else "" - follow_sexp = "" + follow_sx = "" if actor: if is_following: - follow_sexp = sexp_call( + follow_sx = sx_call( "federation-follow-form", action=url_for("social.unfollow"), csrf=csrf, actor_url=actor_url, label="Unfollow", cls="border border-stone-300 rounded px-4 py-2 hover:bg-stone-100", ) else: - follow_sexp = sexp_call( + follow_sx = sx_call( "federation-follow-form", action=url_for("social.follow"), csrf=csrf, actor_url=actor_url, label="Follow", cls="bg-stone-800 text-white rounded px-4 py-2 hover:bg-stone-700", ) - timeline_sexp = _timeline_items_sexp(items, "actor", actor, remote_actor.id) + timeline_sx = _timeline_items_sx(items, "actor", actor, remote_actor.id) - header_sexp = sexp_call( + header_sx = sx_call( "federation-actor-profile-header", - avatar=SexpExpr(avatar), + avatar=SxExpr(avatar), display_name=str(escape(display_name)), username=str(escape(remote_actor.preferred_username)), domain=str(escape(remote_actor.domain)), - summary=SexpExpr(summary_sexp) if summary_sexp else None, - follow=SexpExpr(follow_sexp) if follow_sexp else None, + summary=SxExpr(summary_sx) if summary_sx else None, + follow=SxExpr(follow_sx) if follow_sx else None, ) - content = sexp_call( + content = sx_call( "federation-actor-timeline-layout", - header=SexpExpr(header_sexp), - timeline=SexpExpr(timeline_sexp) if timeline_sexp else None, + header=SxExpr(header_sx), + timeline=SxExpr(timeline_sx) if timeline_sx else None, ) return _social_page(ctx, actor, content=content, @@ -676,7 +676,7 @@ async def render_actor_timeline_page(ctx: dict, remote_actor: Any, items: list, async def render_actor_timeline_items(items: list, actor_id: int, actor: Any) -> str: """Pagination fragment: actor timeline items.""" - return _timeline_items_sexp(items, "actor", actor, actor_id) + return _timeline_items_sx(items, "actor", actor, actor_id) # --------------------------------------------------------------------------- @@ -687,15 +687,15 @@ async def render_notifications_page(ctx: dict, notifications: list, actor: Any) -> str: """Full page: notifications.""" if not notifications: - notif_sexp = sexp_call("federation-notifications-empty") + notif_sx = sx_call("federation-notifications-empty") else: - items_sexp = "(<> " + " ".join(_notification_sexp(n) for n in notifications) + ")" - notif_sexp = sexp_call( + items_sx = "(<> " + " ".join(_notification_sx(n) for n in notifications) + ")" + notif_sx = sx_call( "federation-notifications-list", - items=SexpExpr(items_sexp), + items=SxExpr(items_sx), ) - content = sexp_call("federation-notifications-page", notifs=SexpExpr(notif_sexp)) + content = sx_call("federation-notifications-page", notifs=SxExpr(notif_sx)) return _social_page(ctx, actor, content=content, title="Notifications \u2014 Rose Ash") @@ -717,12 +717,12 @@ async def render_choose_username_page(ctx: dict) -> str: check_url = url_for("identity.check_username") actor = ctx.get("actor") - error_sexp = sexp_call("federation-error-banner", error=error) if error else "" + error_sx = sx_call("federation-error-banner", error=error) if error else "" - content = sexp_call( + content = sx_call( "federation-choose-username", domain=str(escape(ap_domain)), - error=SexpExpr(error_sexp) if error_sexp else None, + error=SxExpr(error_sx) if error_sx else None, csrf=csrf, username=str(escape(username)), check_url=check_url, ) @@ -742,36 +742,36 @@ async def render_profile_page(ctx: dict, actor: Any, activities: list, ap_domain = config().get("ap_domain", "rose-ash.com") display_name = actor.display_name or actor.preferred_username - summary_sexp = sexp_call( + summary_sx = sx_call( "federation-profile-summary-text", text=str(escape(actor.summary)), ) if actor.summary else "" - activities_sexp = "" + activities_sx = "" if activities: parts = [] for a in activities: published = a.published.strftime("%Y-%m-%d %H:%M") if a.published else "" - obj_type_sexp = sexp_call( + obj_type_sx = sx_call( "federation-activity-obj-type", obj_type=a.object_type, ) if a.object_type else "" - parts.append(sexp_call( + parts.append(sx_call( "federation-activity-card", activity_type=a.activity_type, published=published, - obj_type=SexpExpr(obj_type_sexp) if obj_type_sexp else None, + obj_type=SxExpr(obj_type_sx) if obj_type_sx else None, )) - items_sexp = "(<> " + " ".join(parts) + ")" - activities_sexp = sexp_call("federation-activities-list", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(parts) + ")" + activities_sx = sx_call("federation-activities-list", items=SxExpr(items_sx)) else: - activities_sexp = sexp_call("federation-activities-empty") + activities_sx = sx_call("federation-activities-empty") - content = sexp_call( + content = sx_call( "federation-profile-page", display_name=str(escape(display_name)), username=str(escape(actor.preferred_username)), domain=str(escape(ap_domain)), - summary=SexpExpr(summary_sexp) if summary_sexp else None, + summary=SxExpr(summary_sx) if summary_sx else None, activities_heading=f"Activities ({total})", - activities=SexpExpr(activities_sexp), + activities=SxExpr(activities_sx), ) return _social_page(ctx, actor, content=content, @@ -796,10 +796,10 @@ def render_interaction_buttons(object_id: str, author_inbox: str, liked_by_me=liked_by_me, boosted_by_me=boosted_by_me, ) - return _interaction_buttons_sexp(item, actor) + return _interaction_buttons_sx(item, actor) def render_actor_card(actor_dto: Any, actor: Any, followed_urls: set, *, list_type: str = "following") -> str: """Render a single actor card fragment for HTMX POST response.""" - return _actor_card_sexp(actor_dto, actor, followed_urls, list_type=list_type) + return _actor_card_sx(actor_dto, actor, followed_urls, list_type=list_type) diff --git a/market/app.py b/market/app.py index 3b3eecf..e30ac30 100644 --- a/market/app.py +++ b/market/app.py @@ -1,6 +1,6 @@ from __future__ import annotations import path_setup # noqa: F401 # adds shared/ to sys.path -import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file +import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file from pathlib import Path diff --git a/market/bp/all_markets/routes.py b/market/bp/all_markets/routes.py index 203f75e..0c222f1 100644 --- a/market/bp/all_markets/routes.py +++ b/market/bp/all_markets/routes.py @@ -12,7 +12,7 @@ from __future__ import annotations from quart import Blueprint, g, request, render_template, make_response from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from shared.infrastructure.data_client import fetch_data from shared.contracts.dtos import PostDTO, dto_from_dict from shared.services.registry import services @@ -56,13 +56,13 @@ def register() -> Blueprint: page=page, ) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_all_markets_page, render_all_markets_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_all_markets_page, render_all_markets_oob tctx = await get_template_context() if is_htmx_request(): - sexp_src = await render_all_markets_oob(tctx, markets, has_more, page_info, page) - return sexp_response(sexp_src) + sx_src = await render_all_markets_oob(tctx, markets, has_more, page_info, page) + return sx_response(sx_src) else: html = await render_all_markets_page(tctx, markets, has_more, page_info, page) return await make_response(html, 200) @@ -72,8 +72,8 @@ def register() -> Blueprint: page = int(request.args.get("page", 1)) markets, has_more, page_info = await _load_markets(page) - from sexp.sexp_components import render_all_markets_cards - sexp_src = await render_all_markets_cards(markets, has_more, page_info, page) - return sexp_response(sexp_src) + from sx.sx_components import render_all_markets_cards + sx_src = await render_all_markets_cards(markets, has_more, page_info, page) + return sx_response(sx_src) return bp diff --git a/market/bp/browse/routes.py b/market/bp/browse/routes.py index db392e9..c2ad4f0 100644 --- a/market/bp/browse/routes.py +++ b/market/bp/browse/routes.py @@ -23,7 +23,7 @@ from .services import ( from shared.browser.app.redis_cacher import cache_page from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): browse_bp = Blueprint("browse", __name__) @@ -43,8 +43,8 @@ def register(): p_data = getattr(g, "post_data", None) or {} # Determine which template to use based on request type - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_market_home_page, render_market_home_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_market_home_page, render_market_home_oob ctx = await get_template_context() ctx.update(p_data) @@ -52,8 +52,8 @@ def register(): html = await render_market_home_page(ctx) return await make_response(html) else: - sexp_src = await render_market_home_oob(ctx) - return sexp_response(sexp_src) + sx_src = await render_market_home_oob(ctx) + return sx_response(sx_src) @browse_bp.get("/all/") @cache_page(tag="browse") @@ -74,8 +74,8 @@ def register(): product_info = await _productInfo() full_context = {**product_info, **ctx} - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_browse_page, render_browse_oob, render_browse_cards + from shared.sx.page import get_template_context + from sx.sx_components import render_browse_page, render_browse_oob, render_browse_cards tctx = await get_template_context() tctx.update(full_context) @@ -84,11 +84,11 @@ def register(): resp = await make_response(html) elif product_info["page"] > 1: tctx.update(product_info) - sexp_src = await render_browse_cards(tctx) - resp = sexp_response(sexp_src) + sx_src = await render_browse_cards(tctx) + resp = sx_response(sx_src) else: - sexp_src = await render_browse_oob(tctx) - resp = sexp_response(sexp_src) + sx_src = await render_browse_oob(tctx) + resp = sx_response(sx_src) resp.headers["Hx-Push-Url"] = _current_url_without_page() return _vary(resp) @@ -115,8 +115,8 @@ def register(): product_info = await _productInfo(top_slug) full_context = {**product_info, **ctx} - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_browse_page, render_browse_oob, render_browse_cards + from shared.sx.page import get_template_context + from sx.sx_components import render_browse_page, render_browse_oob, render_browse_cards tctx = await get_template_context() tctx.update(full_context) @@ -125,11 +125,11 @@ def register(): resp = await make_response(html) elif product_info["page"] > 1: tctx.update(product_info) - sexp_src = await render_browse_cards(tctx) - resp = sexp_response(sexp_src) + sx_src = await render_browse_cards(tctx) + resp = sx_response(sx_src) else: - sexp_src = await render_browse_oob(tctx) - resp = sexp_response(sexp_src) + sx_src = await render_browse_oob(tctx) + resp = sx_response(sx_src) resp.headers["Hx-Push-Url"] = _current_url_without_page() return _vary(resp) @@ -156,8 +156,8 @@ def register(): product_info = await _productInfo(top_slug, sub_slug) full_context = {**product_info, **ctx} - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_browse_page, render_browse_oob, render_browse_cards + from shared.sx.page import get_template_context + from sx.sx_components import render_browse_page, render_browse_oob, render_browse_cards tctx = await get_template_context() tctx.update(full_context) @@ -166,11 +166,11 @@ def register(): resp = await make_response(html) elif product_info["page"] > 1: tctx.update(product_info) - sexp_src = await render_browse_cards(tctx) - resp = sexp_response(sexp_src) + sx_src = await render_browse_cards(tctx) + resp = sx_response(sx_src) else: - sexp_src = await render_browse_oob(tctx) - resp = sexp_response(sexp_src) + sx_src = await render_browse_oob(tctx) + resp = sx_response(sx_src) resp.headers["Hx-Push-Url"] = _current_url_without_page() return _vary(resp) diff --git a/market/bp/fragments/routes.py b/market/bp/fragments/routes.py index cbfd9ab..9841941 100644 --- a/market/bp/fragments/routes.py +++ b/market/bp/fragments/routes.py @@ -1,6 +1,6 @@ """Market app fragment endpoints. -Exposes sexp fragments at ``/internal/fragments/`` for consumption +Exposes sx fragments at ``/internal/fragments/`` for consumption by other coop apps via the fragment client. """ @@ -26,16 +26,16 @@ def register(): async def get_fragment(fragment_type: str): handler = _handlers.get(fragment_type) if handler is None: - return Response("", status=200, content_type="text/sexp") + return Response("", status=200, content_type="text/sx") src = await handler() - return Response(src, status=200, content_type="text/sexp") + return Response(src, status=200, content_type="text/sx") # --- container-nav fragment: market links -------------------------------- async def _container_nav_handler(): from quart import current_app from shared.infrastructure.urls import market_url - from shared.sexp.helpers import sexp_call + from shared.sx.helpers import sx_call container_type = request.args.get("container_type", "page") container_id = int(request.args.get("container_id", 0)) @@ -51,7 +51,7 @@ def register(): parts = [] for m in markets: href = market_url(f"/{post_slug}/{m.slug}/") - parts.append(sexp_call("market-link-nav", + parts.append(sx_call("market-link-nav", href=href, name=m.name, nav_class=nav_class)) return "(<> " + " ".join(parts) + ")" @@ -59,15 +59,15 @@ def register(): # --- link-card fragment: product preview card -------------------------------- - def _product_link_card_sexp(product, link: str) -> str: - from shared.sexp.helpers import sexp_call + def _product_link_card_sx(product, link: str) -> str: + from shared.sx.helpers import sx_call subtitle = product.brand or "" detail = "" if product.special_price: detail = f"{product.regular_price} → {product.special_price}" elif product.regular_price: detail = str(product.regular_price) - return sexp_call("link-card", + return sx_call("link-card", title=product.title, image=product.image, subtitle=subtitle, detail=detail, link=link) @@ -90,7 +90,7 @@ def register(): await g.s.execute(select(Product).where(Product.slug == s)) ).scalar_one_or_none() if product: - parts.append(_product_link_card_sexp( + parts.append(_product_link_card_sx( product, market_url(f"/product/{product.slug}/"))) return "\n".join(parts) @@ -102,7 +102,7 @@ def register(): ).scalar_one_or_none() if not product: return "" - return _product_link_card_sexp(product, market_url(f"/product/{product.slug}/")) + return _product_link_card_sx(product, market_url(f"/product/{product.slug}/")) _handlers["link-card"] = _link_card_handler diff --git a/market/bp/market/admin/routes.py b/market/bp/market/admin/routes.py index 3c4b853..06a1497 100644 --- a/market/bp/market/admin/routes.py +++ b/market/bp/market/admin/routes.py @@ -17,15 +17,15 @@ def register(): async def admin(): from shared.browser.app.utils.htmx import is_htmx_request - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_market_admin_page, render_market_admin_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_market_admin_page, render_market_admin_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_market_admin_page(tctx) return await make_response(html) else: - from shared.sexp.helpers import sexp_response - sexp_src = await render_market_admin_oob(tctx) - return sexp_response(sexp_src) + from shared.sx.helpers import sx_response + sx_src = await render_market_admin_oob(tctx) + return sx_response(sx_src) return bp diff --git a/market/bp/page_admin/routes.py b/market/bp/page_admin/routes.py index 76584de..3cf2ccf 100644 --- a/market/bp/page_admin/routes.py +++ b/market/bp/page_admin/routes.py @@ -12,16 +12,16 @@ def register(): @bp.get("/") @require_admin async def admin(**kwargs): - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_page_admin_page, render_page_admin_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_page_admin_page, render_page_admin_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_page_admin_page(tctx) return await make_response(html) else: - from shared.sexp.helpers import sexp_response - sexp_src = await render_page_admin_oob(tctx) - return sexp_response(sexp_src) + from shared.sx.helpers import sx_response + sx_src = await render_page_admin_oob(tctx) + return sx_response(sx_src) return bp diff --git a/market/bp/page_markets/routes.py b/market/bp/page_markets/routes.py index be1a2bd..d28318a 100644 --- a/market/bp/page_markets/routes.py +++ b/market/bp/page_markets/routes.py @@ -12,7 +12,7 @@ from __future__ import annotations from quart import Blueprint, g, request, render_template, make_response from shared.browser.app.utils.htmx import is_htmx_request -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response from shared.services.registry import services @@ -40,14 +40,14 @@ def register() -> Blueprint: page=page, ) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_page_markets_page, render_page_markets_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_page_markets_page, render_page_markets_oob tctx = await get_template_context() tctx["post"] = post if is_htmx_request(): - sexp_src = await render_page_markets_oob(tctx, markets, has_more, page) - return sexp_response(sexp_src) + sx_src = await render_page_markets_oob(tctx, markets, has_more, page) + return sx_response(sx_src) else: html = await render_page_markets_page(tctx, markets, has_more, page) return await make_response(html, 200) @@ -59,9 +59,9 @@ def register() -> Blueprint: markets, has_more = await _load_markets(post["id"], page) - from sexp.sexp_components import render_page_markets_cards + from sx.sx_components import render_page_markets_cards post_slug = post.get("slug", "") - sexp_src = await render_page_markets_cards(markets, has_more, page, post_slug) - return sexp_response(sexp_src) + sx_src = await render_page_markets_cards(markets, has_more, page, post_slug) + return sx_response(sx_src) return bp diff --git a/market/bp/product/routes.py b/market/bp/product/routes.py index 8812c57..38bae62 100644 --- a/market/bp/product/routes.py +++ b/market/bp/product/routes.py @@ -19,7 +19,7 @@ from shared.browser.app.redis_cacher import cache_page, clear_cache from ..cart.services import total from shared.infrastructure.actions import call_action from .services.product_operations import massage_full_product -from shared.sexp.helpers import sexp_response +from shared.sx.helpers import sx_response def register(): @@ -107,8 +107,8 @@ def register(): async def product_detail(): from shared.browser.app.utils.htmx import is_htmx_request - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_product_page, render_product_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_product_page, render_product_oob tctx = await get_template_context() item_data = getattr(g, "item_data", {}) @@ -118,18 +118,18 @@ def register(): html = await render_product_page(tctx, d) return html else: - sexp_src = await render_product_oob(tctx, d) - return sexp_response(sexp_src) + sx_src = await render_product_oob(tctx, d) + return sx_response(sx_src) @bp.post("/like/toggle/") @clear_cache(tag="browse", tag_scope="user") async def like_toggle(): product_slug = g.product_slug - from sexp.sexp_components import render_like_toggle_button + from sx.sx_components import render_like_toggle_button if not g.user: - return sexp_response(render_like_toggle_button(product_slug, False), status=403) + return sx_response(render_like_toggle_button(product_slug, False), status=403) user_id = g.user.id @@ -138,7 +138,7 @@ def register(): }) liked = result["liked"] - return sexp_response(render_like_toggle_button(product_slug, liked)) + return sx_response(render_like_toggle_button(product_slug, liked)) @@ -146,8 +146,8 @@ def register(): async def admin(): from shared.browser.app.utils.htmx import is_htmx_request - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_product_admin_page, render_product_admin_oob + from shared.sx.page import get_template_context + from sx.sx_components import render_product_admin_page, render_product_admin_oob tctx = await get_template_context() item_data = getattr(g, "item_data", {}) @@ -157,8 +157,8 @@ def register(): html = await render_product_admin_page(tctx, d) return await make_response(html) else: - sexp_src = await render_product_admin_oob(tctx, d) - return sexp_response(sexp_src) + sx_src = await render_product_admin_oob(tctx, d) + return sx_response(sx_src) from bp.cart.services.identity import current_cart_identity @@ -254,10 +254,10 @@ def register(): # htmx response: OOB-swap mini cart + product buttons if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true": - from sexp.sexp_components import render_cart_added_response + from sx.sx_components import render_cart_added_response item_data = getattr(g, "item_data", {}) d = item_data.get("d", {}) - return sexp_response(render_cart_added_response(g.cart, ci_ns, d)) + return sx_response(render_cart_added_response(g.cart, ci_ns, d)) # normal POST: go to cart page from shared.infrastructure.urls import cart_url diff --git a/market/sexp/__init__.py b/market/sx/__init__.py similarity index 100% rename from market/sexp/__init__.py rename to market/sx/__init__.py diff --git a/market/sexp/cards.sexpr b/market/sx/cards.sx similarity index 100% rename from market/sexp/cards.sexpr rename to market/sx/cards.sx diff --git a/market/sexp/cart.sexpr b/market/sx/cart.sx similarity index 100% rename from market/sexp/cart.sexpr rename to market/sx/cart.sx diff --git a/market/sexp/detail.sexpr b/market/sx/detail.sx similarity index 100% rename from market/sexp/detail.sexpr rename to market/sx/detail.sx diff --git a/market/sexp/filters.sexpr b/market/sx/filters.sx similarity index 100% rename from market/sexp/filters.sexpr rename to market/sx/filters.sx diff --git a/market/sexp/grids.sexpr b/market/sx/grids.sx similarity index 100% rename from market/sexp/grids.sexpr rename to market/sx/grids.sx diff --git a/market/sexp/headers.sexpr b/market/sx/headers.sx similarity index 100% rename from market/sexp/headers.sexpr rename to market/sx/headers.sx diff --git a/market/sexp/meta.sexpr b/market/sx/meta.sx similarity index 100% rename from market/sexp/meta.sexpr rename to market/sx/meta.sx diff --git a/market/sexp/navigation.sexpr b/market/sx/navigation.sx similarity index 100% rename from market/sexp/navigation.sexpr rename to market/sx/navigation.sx diff --git a/market/sexp/prices.sexpr b/market/sx/prices.sx similarity index 100% rename from market/sexp/prices.sexpr rename to market/sx/prices.sx diff --git a/market/sexp/sexp_components.py b/market/sx/sx_components.py similarity index 64% rename from market/sexp/sexp_components.py rename to market/sx/sx_components.py index a36527f..450b96e 100644 --- a/market/sexp/sexp_components.py +++ b/market/sx/sx_components.py @@ -9,19 +9,19 @@ from __future__ import annotations import os from typing import Any -from shared.sexp.jinja_bridge import load_service_components -from shared.sexp.helpers import ( - call_url, get_asset_url, sexp_call, SexpExpr, - root_header_sexp, - post_header_sexp as _post_header_sexp, - post_admin_header_sexp, - oob_header_sexp as _oob_header_sexp, - header_child_sexp, - search_mobile_sexp, search_desktop_sexp, - full_page_sexp, oob_page_sexp, +from shared.sx.jinja_bridge import load_service_components +from shared.sx.helpers import ( + call_url, get_asset_url, sx_call, SxExpr, + root_header_sx, + post_header_sx as _post_header_sx, + post_admin_header_sx, + oob_header_sx as _oob_header_sx, + header_child_sx, + search_mobile_sx, search_desktop_sx, + full_page_sx, oob_page_sx, ) -# Load market-specific .sexpr components at import time +# Load market-specific .sx components at import time load_service_components(os.path.dirname(os.path.dirname(__file__))) @@ -72,29 +72,29 @@ def _set_prices(item: dict) -> dict: rp_val=rp_val, rp_raw=rp_raw, rp_cur=rp_cur) -def _card_price_sexp(p: dict) -> str: - """Build price line for product card as sexp call.""" +def _card_price_sx(p: dict) -> str: + """Build price line for product card as sx call.""" pr = _set_prices(p) sp_str = _price_str(pr["sp_val"], pr["sp_raw"], pr["sp_cur"]) rp_str = _price_str(pr["rp_val"], pr["rp_raw"], pr["rp_cur"]) parts: list[str] = [] if pr["sp_val"]: - parts.append(sexp_call("market-price-special", price=sp_str)) + parts.append(sx_call("market-price-special", price=sp_str)) if pr["rp_val"]: - parts.append(sexp_call("market-price-regular-strike", price=rp_str)) + parts.append(sx_call("market-price-regular-strike", price=rp_str)) elif pr["rp_val"]: - parts.append(sexp_call("market-price-regular", price=rp_str)) + parts.append(sx_call("market-price-regular", price=rp_str)) inner = "(<> " + " ".join(parts) + ")" if parts else None - return sexp_call("market-price-line", inner=SexpExpr(inner) if inner else None) + return sx_call("market-price-line", inner=SxExpr(inner) if inner else None) # --------------------------------------------------------------------------- -# Header helpers — _post_header_sexp and _oob_header_sexp imported from shared +# Header helpers — _post_header_sx and _oob_header_sx imported from shared # --------------------------------------------------------------------------- -def _market_header_sexp(ctx: dict, *, oob: bool = False) -> str: - """Build the market-level header row as sexp call string.""" +def _market_header_sx(ctx: dict, *, oob: bool = False) -> str: + """Build the market-level header row as sx call string.""" from quart import url_for market_title = ctx.get("market_title", "") @@ -102,11 +102,11 @@ def _market_header_sexp(ctx: dict, *, oob: bool = False) -> str: sub_slug = ctx.get("sub_slug", "") hx_select_search = ctx.get("hx_select_search", "#main-panel") - sub_div = sexp_call("market-sub-slug", sub=sub_slug) if sub_slug else "" - label_sexp = sexp_call( + sub_div = sx_call("market-sub-slug", sub=sub_slug) if sub_slug else "" + label_sx = sx_call( "market-shop-label", title=market_title, top_slug=top_slug or "", - sub_div=SexpExpr(sub_div) if sub_div else None, + sub_div=SxExpr(sub_div) if sub_div else None, ) link_href = url_for("market.browse.home") @@ -114,20 +114,20 @@ def _market_header_sexp(ctx: dict, *, oob: bool = False) -> str: # Build desktop nav from categories categories = ctx.get("categories", {}) qs = ctx.get("qs", "") - nav_sexp = _desktop_category_nav_sexp(ctx, categories, qs, hx_select_search) + nav_sx = _desktop_category_nav_sx(ctx, categories, qs, hx_select_search) - return sexp_call( + return sx_call( "menu-row-sx", id="market-row", level=2, - link_href=link_href, link_label_content=SexpExpr(label_sexp), - nav=SexpExpr(nav_sexp) if nav_sexp else None, + link_href=link_href, link_label_content=SxExpr(label_sx), + nav=SxExpr(nav_sx) if nav_sx else None, child_id="market-header-child", oob=oob, ) -def _desktop_category_nav_sexp(ctx: dict, categories: dict, qs: str, +def _desktop_category_nav_sx(ctx: dict, categories: dict, qs: str, hx_select: str) -> str: - """Build desktop category navigation links as sexp.""" + """Build desktop category navigation links as sx.""" from quart import url_for from shared.utils import route_prefix @@ -138,7 +138,7 @@ def _desktop_category_nav_sexp(ctx: dict, categories: dict, qs: str, all_href = prefix + url_for("market.browse.browse_all") + qs all_active = (category_label == "All Products") - link_parts = [sexp_call( + link_parts = [sx_call( "market-category-link", href=all_href, hx_select=hx_select, active=all_active, select_colours=select_colours, label="All", @@ -147,26 +147,26 @@ def _desktop_category_nav_sexp(ctx: dict, categories: dict, qs: str, for cat, data in categories.items(): cat_href = prefix + url_for("market.browse.browse_top", top_slug=data["slug"]) + qs cat_active = (cat == category_label) - link_parts.append(sexp_call( + link_parts.append(sx_call( "market-category-link", href=cat_href, hx_select=hx_select, active=cat_active, select_colours=select_colours, label=cat, )) - links_sexp = "(<> " + " ".join(link_parts) + ")" + links_sx = "(<> " + " ".join(link_parts) + ")" - admin_sexp = "" + admin_sx = "" if rights and rights.get("admin"): admin_href = prefix + url_for("market.admin.admin") - admin_sexp = sexp_call("market-admin-link", href=admin_href, hx_select=hx_select) + admin_sx = sx_call("market-admin-link", href=admin_href, hx_select=hx_select) - return sexp_call("market-desktop-category-nav", - links=SexpExpr(links_sexp), - admin=SexpExpr(admin_sexp) if admin_sexp else None) + return sx_call("market-desktop-category-nav", + links=SxExpr(links_sx), + admin=SxExpr(admin_sx) if admin_sx else None) -def _product_header_sexp(ctx: dict, d: dict, *, oob: bool = False) -> str: - """Build the product-level header row as sexp call string.""" +def _product_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str: + """Build the product-level header row as sx call string.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token @@ -175,30 +175,30 @@ def _product_header_sexp(ctx: dict, d: dict, *, oob: bool = False) -> str: hx_select_search = ctx.get("hx_select_search", "#main-panel") link_href = url_for("market.browse.product.product_detail", product_slug=slug) - label_sexp = sexp_call("market-product-label", title=title) + label_sx = sx_call("market-product-label", title=title) # Prices in nav area pr = _set_prices(d) cart = ctx.get("cart", []) - prices_nav = _prices_header_sexp(d, pr, cart, slug, ctx) + prices_nav = _prices_header_sx(d, pr, cart, slug, ctx) rights = ctx.get("rights", {}) nav_parts = [prices_nav] if rights and rights.get("admin"): admin_href = url_for("market.browse.product.admin", product_slug=slug) - nav_parts.append(sexp_call("market-admin-link", href=admin_href, hx_select=hx_select_search)) - nav_sexp = "(<> " + " ".join(nav_parts) + ")" + nav_parts.append(sx_call("market-admin-link", href=admin_href, hx_select=hx_select_search)) + nav_sx = "(<> " + " ".join(nav_parts) + ")" - return sexp_call( + return sx_call( "menu-row-sx", id="product-row", level=3, - link_href=link_href, link_label_content=SexpExpr(label_sexp), - nav=SexpExpr(nav_sexp), child_id="product-header-child", oob=oob, + link_href=link_href, link_label_content=SxExpr(label_sx), + nav=SxExpr(nav_sx), child_id="product-header-child", oob=oob, ) -def _prices_header_sexp(d: dict, pr: dict, cart: list, slug: str, ctx: dict) -> str: - """Build prices + add-to-cart for product header row as sexp.""" +def _prices_header_sx(d: dict, pr: dict, cart: list, slug: str, ctx: dict) -> str: + """Build prices + add-to-cart for product header row as sx.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token @@ -208,20 +208,20 @@ def _prices_header_sexp(d: dict, pr: dict, cart: list, slug: str, ctx: dict) -> # Add-to-cart button quantity = sum(ci.quantity for ci in cart if ci.product.slug == slug) if cart else 0 - add_sexp = _cart_add_sexp(slug, quantity, cart_action, csrf, cart_url_fn) + add_sx = _cart_add_sx(slug, quantity, cart_action, csrf, cart_url_fn) - parts = [add_sexp] + parts = [add_sx] sp_val, rp_val = pr.get("sp_val"), pr.get("rp_val") if sp_val: - parts.append(sexp_call("market-header-price-special-label")) - parts.append(sexp_call("market-header-price-special", + parts.append(sx_call("market-header-price-special-label")) + parts.append(sx_call("market-header-price-special", price=_price_str(sp_val, pr["sp_raw"], pr["sp_cur"]))) if rp_val: - parts.append(sexp_call("market-header-price-strike", + parts.append(sx_call("market-header-price-strike", price=_price_str(rp_val, pr["rp_raw"], pr["rp_cur"]))) elif rp_val: - parts.append(sexp_call("market-header-price-regular-label")) - parts.append(sexp_call("market-header-price-regular", + parts.append(sx_call("market-header-price-regular-label")) + parts.append(sx_call("market-header-price-regular", price=_price_str(rp_val, pr["rp_raw"], pr["rp_cur"]))) # RRP @@ -230,23 +230,23 @@ def _prices_header_sexp(d: dict, pr: dict, cart: list, slug: str, ctx: dict) -> case_size = d.get("case_size_count") or 1 if rrp_raw and rrp_val: rrp_str = f"{rrp_raw[0]}{rrp_val * case_size:.2f}" - parts.append(sexp_call("market-header-rrp", rrp=rrp_str)) + parts.append(sx_call("market-header-rrp", rrp=rrp_str)) - inner_sexp = "(<> " + " ".join(parts) + ")" - return sexp_call("market-prices-row", inner=SexpExpr(inner_sexp)) + inner_sx = "(<> " + " ".join(parts) + ")" + return sx_call("market-prices-row", inner=SxExpr(inner_sx)) -def _cart_add_sexp(slug: str, quantity: int, action: str, csrf: str, +def _cart_add_sx(slug: str, quantity: int, action: str, csrf: str, cart_url_fn: Any = None) -> str: - """Build add-to-cart button or quantity controls as sexp.""" + """Build add-to-cart button or quantity controls as sx.""" if not quantity: - return sexp_call( + return sx_call( "market-cart-add-empty", cart_id=f"cart-{slug}", action=action, csrf=csrf, ) cart_href = cart_url_fn("/") if callable(cart_url_fn) else "/" - return sexp_call( + return sx_call( "market-cart-add-quantity", cart_id=f"cart-{slug}", action=action, csrf=csrf, minus_val=str(quantity - 1), plus_val=str(quantity + 1), @@ -258,8 +258,8 @@ def _cart_add_sexp(slug: str, quantity: int, action: str, csrf: str, # Mobile nav panel # --------------------------------------------------------------------------- -def _mobile_nav_panel_sexp(ctx: dict) -> str: - """Build mobile nav panel with category accordion as sexp.""" +def _mobile_nav_panel_sx(ctx: dict) -> str: + """Build mobile nav panel with category accordion as sx.""" from quart import url_for from shared.utils import route_prefix @@ -274,7 +274,7 @@ def _mobile_nav_panel_sexp(ctx: dict) -> str: all_href = prefix + url_for("market.browse.browse_all") + qs all_active = (category_label == "All Products") - item_parts = [sexp_call( + item_parts = [sx_call( "market-mobile-all-link", href=all_href, hx_select=hx_select, active=all_active, select_colours=select_colours, @@ -286,19 +286,19 @@ def _mobile_nav_panel_sexp(ctx: dict) -> str: cat_href = prefix + url_for("market.browse.browse_top", top_slug=cat_slug) + qs bg_cls = " bg-stone-900 text-white hover:bg-stone-900" if cat_active else "" - chevron_sexp = sexp_call("market-mobile-chevron") + chevron_sx = sx_call("market-mobile-chevron") cat_count = data.get("count", 0) - summary_sexp = sexp_call( + summary_sx = sx_call( "market-mobile-cat-summary", bg_cls=bg_cls, href=cat_href, hx_select=hx_select, select_colours=select_colours, cat_name=cat, count_label=f"{cat_count} products", count_str=str(cat_count), - chevron=SexpExpr(chevron_sexp), + chevron=SxExpr(chevron_sx), ) subs = data.get("subs", []) - subs_sexp = "" + subs_sx = "" if subs: sub_link_parts = [] for sub in subs: @@ -306,35 +306,35 @@ def _mobile_nav_panel_sexp(ctx: dict) -> str: sub_active = (cat_active and sub_slug == sub.get("slug")) sub_label = sub.get("html_label") or sub.get("name", "") sub_count = sub.get("count", 0) - sub_link_parts.append(sexp_call( + sub_link_parts.append(sx_call( "market-mobile-sub-link", select_colours=select_colours, active=sub_active, href=sub_href, hx_select=hx_select, label=sub_label, count_label=f"{sub_count} products", count_str=str(sub_count), )) - sub_links_sexp = "(<> " + " ".join(sub_link_parts) + ")" - subs_sexp = sexp_call("market-mobile-subs-panel", links=SexpExpr(sub_links_sexp)) + sub_links_sx = "(<> " + " ".join(sub_link_parts) + ")" + subs_sx = sx_call("market-mobile-subs-panel", links=SxExpr(sub_links_sx)) else: view_href = prefix + url_for("market.browse.browse_top", top_slug=cat_slug) + qs - subs_sexp = sexp_call("market-mobile-view-all", href=view_href, hx_select=hx_select) + subs_sx = sx_call("market-mobile-view-all", href=view_href, hx_select=hx_select) - item_parts.append(sexp_call( + item_parts.append(sx_call( "market-mobile-cat-details", open=cat_active or None, - summary=SexpExpr(summary_sexp), - subs=SexpExpr(subs_sexp), + summary=SxExpr(summary_sx), + subs=SxExpr(subs_sx), )) - items_sexp = "(<> " + " ".join(item_parts) + ")" - return sexp_call("market-mobile-nav-wrapper", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(item_parts) + ")" + return sx_call("market-mobile-nav-wrapper", items=SxExpr(items_sx)) # --------------------------------------------------------------------------- # Product card (browse grid item) # --------------------------------------------------------------------------- -def _product_card_sexp(p: dict, ctx: dict) -> str: - """Build a single product card for browse grid as sexp call.""" +def _product_card_sx(p: dict, ctx: dict) -> str: + """Build a single product card for browse grid as sx call.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token from shared.utils import route_prefix @@ -417,10 +417,10 @@ def _product_card_sexp(p: dict, ctx: dict) -> str: kwargs["search_mid"] = search_mid kwargs["search_post"] = search_post - return sexp_call("market-product-card", **kwargs) + return sx_call("market-product-card", **kwargs) -def _product_cards_sexp(ctx: dict) -> str: +def _product_cards_sx(ctx: dict) -> str: """S-expression wire format for product cards (client renders).""" from shared.utils import route_prefix @@ -431,7 +431,7 @@ def _product_cards_sexp(ctx: dict) -> str: current_local_href = ctx.get("current_local_href", "/") qs_fn = ctx.get("qs_filter") - parts = [_product_card_sexp(p, ctx) for p in products] + parts = [_product_card_sx(p, ctx) for p in products] if page < total_pages: if callable(qs_fn): @@ -439,25 +439,25 @@ def _product_cards_sexp(ctx: dict) -> str: else: next_qs = f"?page={page + 1}" next_url = prefix + current_local_href + next_qs - parts.append(sexp_call("market-sentinel-mobile", + parts.append(sx_call("market-sentinel-mobile", id=f"sentinel-{page}-m", next_url=next_url, hyperscript=_MOBILE_SENTINEL_HS)) - parts.append(sexp_call("market-sentinel-desktop", + parts.append(sx_call("market-sentinel-desktop", id=f"sentinel-{page}-d", next_url=next_url, hyperscript=_DESKTOP_SENTINEL_HS)) else: - parts.append(sexp_call("market-sentinel-end")) + parts.append(sx_call("market-sentinel-end")) return "(<> " + " ".join(parts) + ")" -def _like_button_sexp(slug: str, liked: bool, csrf: str, ctx: dict) -> str: - """Build the like/unlike heart button overlay as sexp.""" +def _like_button_sx(slug: str, liked: bool, csrf: str, ctx: dict) -> str: + """Build the like/unlike heart button overlay as sx.""" from quart import url_for action = url_for("market.browse.product.like_toggle", product_slug=slug) icon_cls = "fa-solid fa-heart text-red-500" if liked else "fa-regular fa-heart text-stone-400" - return sexp_call( + return sx_call( "market-like-button", form_id=f"like-{slug}", action=action, slug=slug, csrf=csrf, icon_cls=icon_cls, @@ -538,8 +538,8 @@ _DESKTOP_SENTINEL_HS = ( # Browse filter panels (mobile + desktop) # --------------------------------------------------------------------------- -def _desktop_filter_sexp(ctx: dict) -> str: - """Build the desktop aside filter panel as sexp.""" +def _desktop_filter_sx(ctx: dict) -> str: + """Build the desktop aside filter panel as sx.""" category_label = ctx.get("category_label", "") sort_options = ctx.get("sort_options", []) sort = ctx.get("sort", "") @@ -556,41 +556,41 @@ def _desktop_filter_sexp(ctx: dict) -> str: sub_slug = ctx.get("sub_slug", "") # Search - search_sexp = search_desktop_sexp(ctx) + search_sx = search_desktop_sx(ctx) # Category summary + sort + like + labels + stickers - cat_parts = [sexp_call("market-filter-category-label", label=category_label)] + cat_parts = [sx_call("market-filter-category-label", label=category_label)] if sort_options: - cat_parts.append(_sort_stickers_sexp(sort_options, sort, ctx)) + cat_parts.append(_sort_stickers_sx(sort_options, sort, ctx)) - like_label_parts = [_like_filter_sexp(liked, liked_count, ctx)] + like_label_parts = [_like_filter_sx(liked, liked_count, ctx)] if labels: - like_label_parts.append(_labels_filter_sexp(labels, selected_labels, ctx, prefix="nav-labels")) - like_labels_sexp = "(<> " + " ".join(like_label_parts) + ")" - cat_parts.append(sexp_call("market-filter-like-labels-nav", inner=SexpExpr(like_labels_sexp))) + like_label_parts.append(_labels_filter_sx(labels, selected_labels, ctx, prefix="nav-labels")) + like_labels_sx = "(<> " + " ".join(like_label_parts) + ")" + cat_parts.append(sx_call("market-filter-like-labels-nav", inner=SxExpr(like_labels_sx))) if stickers: - cat_parts.append(_stickers_filter_sexp(stickers, selected_stickers, ctx)) + cat_parts.append(_stickers_filter_sx(stickers, selected_stickers, ctx)) if subs_local and top_local_href: - cat_parts.append(_subcategory_selector_sexp(subs_local, top_local_href, sub_slug, ctx)) + cat_parts.append(_subcategory_selector_sx(subs_local, top_local_href, sub_slug, ctx)) - cat_inner_sexp = "(<> " + " ".join(cat_parts) + ")" - cat_summary = sexp_call("market-desktop-category-summary", inner=SexpExpr(cat_inner_sexp)) + cat_inner_sx = "(<> " + " ".join(cat_parts) + ")" + cat_summary = sx_call("market-desktop-category-summary", inner=SxExpr(cat_inner_sx)) # Brand filter brand_inner = "" if brands: - brand_inner = _brand_filter_sexp(brands, selected_brands, ctx) - brand_summary = sexp_call("market-desktop-brand-summary", - inner=SexpExpr(brand_inner) if brand_inner else None) + brand_inner = _brand_filter_sx(brands, selected_brands, ctx) + brand_summary = sx_call("market-desktop-brand-summary", + inner=SxExpr(brand_inner) if brand_inner else None) - return "(<> " + " ".join([search_sexp, cat_summary, brand_summary]) + ")" + return "(<> " + " ".join([search_sx, cat_summary, brand_summary]) + ")" -def _mobile_filter_summary_sexp(ctx: dict) -> str: - """Build mobile filter summary as sexp.""" +def _mobile_filter_summary_sx(ctx: dict) -> str: + """Build mobile filter summary as sx.""" asset_url_fn = ctx.get("asset_url") sort = ctx.get("sort", "") sort_options = ctx.get("sort_options", []) @@ -604,7 +604,7 @@ def _mobile_filter_summary_sexp(ctx: dict) -> str: brands = ctx.get("brands", []) # Search bar - search_bar = search_mobile_sexp(ctx) + search_bar = search_mobile_sx(ctx) # Summary chips showing active filters chip_parts: list[str] = [] @@ -612,14 +612,14 @@ def _mobile_filter_summary_sexp(ctx: dict) -> str: if sort and sort_options: for k, l, i in sort_options: if k == sort and callable(asset_url_fn): - chip_parts.append(sexp_call("market-mobile-chip-sort", src=asset_url_fn(i), label=l)) + chip_parts.append(sx_call("market-mobile-chip-sort", src=asset_url_fn(i), label=l)) if liked: - liked_parts = [sexp_call("market-mobile-chip-liked-icon")] + liked_parts = [sx_call("market-mobile-chip-liked-icon")] if liked_count is not None: cls = "text-[10px] text-stone-500" if liked_count != 0 else "text-md text-red-500 font-bold" - liked_parts.append(sexp_call("market-mobile-chip-count", cls=cls, count=str(liked_count))) + liked_parts.append(sx_call("market-mobile-chip-count", cls=cls, count=str(liked_count))) liked_inner = "(<> " + " ".join(liked_parts) + ")" - chip_parts.append(sexp_call("market-mobile-chip-liked", inner=SexpExpr(liked_inner))) + chip_parts.append(sx_call("market-mobile-chip-liked", inner=SxExpr(liked_inner))) # Selected labels if selected_labels: @@ -627,18 +627,18 @@ def _mobile_filter_summary_sexp(ctx: dict) -> str: for sl in selected_labels: for lb in labels: if lb.get("name") == sl and callable(asset_url_fn): - li_parts = [sexp_call( + li_parts = [sx_call( "market-mobile-chip-image", src=asset_url_fn("nav-labels/" + sl + ".svg"), name=sl, )] if lb.get("count") is not None: cls = "text-[10px] text-stone-500" if lb["count"] != 0 else "text-md text-red-500 font-bold" - li_parts.append(sexp_call("market-mobile-chip-count", cls=cls, count=str(lb["count"]))) + li_parts.append(sx_call("market-mobile-chip-count", cls=cls, count=str(lb["count"]))) li_inner = "(<> " + " ".join(li_parts) + ")" - label_item_parts.append(sexp_call("market-mobile-chip-item", inner=SexpExpr(li_inner))) + label_item_parts.append(sx_call("market-mobile-chip-item", inner=SxExpr(li_inner))) if label_item_parts: label_items = "(<> " + " ".join(label_item_parts) + ")" - chip_parts.append(sexp_call("market-mobile-chip-list", items=SexpExpr(label_items))) + chip_parts.append(sx_call("market-mobile-chip-list", items=SxExpr(label_items))) # Selected stickers if selected_stickers: @@ -646,18 +646,18 @@ def _mobile_filter_summary_sexp(ctx: dict) -> str: for ss in selected_stickers: for st in stickers: if st.get("name") == ss and callable(asset_url_fn): - si_parts = [sexp_call( + si_parts = [sx_call( "market-mobile-chip-image", src=asset_url_fn("stickers/" + ss + ".svg"), name=ss, )] if st.get("count") is not None: cls = "text-[10px] text-stone-500" if st["count"] != 0 else "text-md text-red-500 font-bold" - si_parts.append(sexp_call("market-mobile-chip-count", cls=cls, count=str(st["count"]))) + si_parts.append(sx_call("market-mobile-chip-count", cls=cls, count=str(st["count"]))) si_inner = "(<> " + " ".join(si_parts) + ")" - sticker_item_parts.append(sexp_call("market-mobile-chip-item", inner=SexpExpr(si_inner))) + sticker_item_parts.append(sx_call("market-mobile-chip-item", inner=SxExpr(si_inner))) if sticker_item_parts: sticker_items = "(<> " + " ".join(sticker_item_parts) + ")" - chip_parts.append(sexp_call("market-mobile-chip-list", items=SexpExpr(sticker_items))) + chip_parts.append(sx_call("market-mobile-chip-list", items=SxExpr(sticker_items))) # Selected brands if selected_brands: @@ -668,30 +668,30 @@ def _mobile_filter_summary_sexp(ctx: dict) -> str: if br.get("name") == b: count = br.get("count", 0) if count: - brand_item_parts.append(sexp_call("market-mobile-chip-brand", name=b, count=str(count))) + brand_item_parts.append(sx_call("market-mobile-chip-brand", name=b, count=str(count))) else: - brand_item_parts.append(sexp_call("market-mobile-chip-brand-zero", name=b)) + brand_item_parts.append(sx_call("market-mobile-chip-brand-zero", name=b)) brand_items = "(<> " + " ".join(brand_item_parts) + ")" - chip_parts.append(sexp_call("market-mobile-chip-brand-list", items=SexpExpr(brand_items))) + chip_parts.append(sx_call("market-mobile-chip-brand-list", items=SxExpr(brand_items))) - chips_sexp = "(<> " + " ".join(chip_parts) + ")" if chip_parts else '(<>)' - chips_row = sexp_call("market-mobile-chips-row", inner=SexpExpr(chips_sexp)) + chips_sx = "(<> " + " ".join(chip_parts) + ")" if chip_parts else '(<>)' + chips_row = sx_call("market-mobile-chips-row", inner=SxExpr(chips_sx)) # Full mobile filter details from shared.utils import route_prefix prefix = route_prefix() - mobile_filter = _mobile_filter_content_sexp(ctx, prefix) + mobile_filter = _mobile_filter_content_sx(ctx, prefix) - return sexp_call( + return sx_call( "market-mobile-filter-summary", - search_bar=SexpExpr(search_bar), - chips=SexpExpr(chips_row), - filter=SexpExpr(mobile_filter), + search_bar=SxExpr(search_bar), + chips=SxExpr(chips_row), + filter=SxExpr(mobile_filter), ) -def _mobile_filter_content_sexp(ctx: dict, prefix: str) -> str: - """Build the expanded mobile filter panel contents as sexp.""" +def _mobile_filter_content_sx(ctx: dict, prefix: str) -> str: + """Build the expanded mobile filter panel contents as sx.""" selected_labels = ctx.get("selected_labels", []) selected_stickers = ctx.get("selected_stickers", []) selected_brands = ctx.get("selected_brands", []) @@ -711,34 +711,34 @@ def _mobile_filter_content_sexp(ctx: dict, prefix: str) -> str: # Sort options if sort_options: - parts.append(_sort_stickers_sexp(sort_options, sort, ctx, mobile=True)) + parts.append(_sort_stickers_sx(sort_options, sort, ctx, mobile=True)) # Clear filters button has_filters = search or selected_labels or selected_stickers or selected_brands if has_filters and callable(qs_fn): clear_url = prefix + current_local_href + qs_fn({"clear_filters": True}) - parts.append(sexp_call("market-mobile-clear-filters", href=clear_url, hx_select=hx_select)) + parts.append(sx_call("market-mobile-clear-filters", href=clear_url, hx_select=hx_select)) # Like + labels row - like_label_parts = [_like_filter_sexp(liked, liked_count, ctx, mobile=True)] + like_label_parts = [_like_filter_sx(liked, liked_count, ctx, mobile=True)] if labels: - like_label_parts.append(_labels_filter_sexp(labels, selected_labels, ctx, prefix="nav-labels", mobile=True)) - like_labels_sexp = "(<> " + " ".join(like_label_parts) + ")" - parts.append(sexp_call("market-mobile-like-labels-row", inner=SexpExpr(like_labels_sexp))) + like_label_parts.append(_labels_filter_sx(labels, selected_labels, ctx, prefix="nav-labels", mobile=True)) + like_labels_sx = "(<> " + " ".join(like_label_parts) + ")" + parts.append(sx_call("market-mobile-like-labels-row", inner=SxExpr(like_labels_sx))) # Stickers if stickers: - parts.append(_stickers_filter_sexp(stickers, selected_stickers, ctx, mobile=True)) + parts.append(_stickers_filter_sx(stickers, selected_stickers, ctx, mobile=True)) # Brands if brands: - parts.append(_brand_filter_sexp(brands, selected_brands, ctx, mobile=True)) + parts.append(_brand_filter_sx(brands, selected_brands, ctx, mobile=True)) return "(<> " + " ".join(parts) + ")" if parts else "(<>)" -def _sort_stickers_sexp(sort_options: list, current_sort: str, ctx: dict, mobile: bool = False) -> str: - """Build sort option stickers as sexp.""" +def _sort_stickers_sx(sort_options: list, current_sort: str, ctx: dict, mobile: bool = False) -> str: + """Build sort option stickers as sx.""" asset_url_fn = ctx.get("asset_url") current_local_href = ctx.get("current_local_href", "/") hx_select = ctx.get("hx_select_search", "#main-panel") @@ -755,16 +755,16 @@ def _sort_stickers_sexp(sort_options: list, current_sort: str, ctx: dict, mobile active = (k == current_sort) ring = " ring-2 ring-emerald-500 rounded" if active else "" src = asset_url_fn(icon) if callable(asset_url_fn) else icon - item_parts.append(sexp_call( + item_parts.append(sx_call( "market-filter-sort-item", href=href, hx_select=hx_select, ring_cls=ring, src=src, label=label, )) - items_sexp = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)" - return sexp_call("market-filter-sort-row", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)" + return sx_call("market-filter-sort-row", items=SxExpr(items_sx)) -def _like_filter_sexp(liked: bool, liked_count: int, ctx: dict, mobile: bool = False) -> str: - """Build the like filter toggle as sexp.""" +def _like_filter_sx(liked: bool, liked_count: int, ctx: dict, mobile: bool = False) -> str: + """Build the like filter toggle as sx.""" current_local_href = ctx.get("current_local_href", "/") hx_select = ctx.get("hx_select_search", "#main-panel") qs_fn = ctx.get("qs_filter") @@ -778,15 +778,15 @@ def _like_filter_sexp(liked: bool, liked_count: int, ctx: dict, mobile: bool = F icon_cls = "fa-solid fa-heart text-red-500" if liked else "fa-regular fa-heart text-stone-400" size = "text-[40px]" if mobile else "text-2xl" - return sexp_call( + return sx_call( "market-filter-like", href=href, hx_select=hx_select, icon_cls=icon_cls, size_cls=size, ) -def _labels_filter_sexp(labels: list, selected: list, ctx: dict, *, +def _labels_filter_sx(labels: list, selected: list, ctx: dict, *, prefix: str = "nav-labels", mobile: bool = False) -> str: - """Build label filter buttons as sexp.""" + """Build label filter buttons as sx.""" asset_url_fn = ctx.get("asset_url") current_local_href = ctx.get("current_local_href", "/") hx_select = ctx.get("hx_select_search", "#main-panel") @@ -805,15 +805,15 @@ def _labels_filter_sexp(labels: list, selected: list, ctx: dict, *, href = "#" ring = " ring-2 ring-emerald-500 rounded" if is_sel else "" src = asset_url_fn(f"{prefix}/{name}.svg") if callable(asset_url_fn) else "" - item_parts.append(sexp_call( + item_parts.append(sx_call( "market-filter-label-item", href=href, hx_select=hx_select, ring_cls=ring, src=src, name=name, )) return "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)" -def _stickers_filter_sexp(stickers: list, selected: list, ctx: dict, mobile: bool = False) -> str: - """Build sticker filter grid as sexp.""" +def _stickers_filter_sx(stickers: list, selected: list, ctx: dict, mobile: bool = False) -> str: + """Build sticker filter grid as sx.""" asset_url_fn = ctx.get("asset_url") current_local_href = ctx.get("current_local_href", "/") hx_select = ctx.get("hx_select_search", "#main-panel") @@ -834,17 +834,17 @@ def _stickers_filter_sexp(stickers: list, selected: list, ctx: dict, mobile: boo ring = " ring-2 ring-emerald-500 rounded" if is_sel else "" src = asset_url_fn(f"stickers/{name}.svg") if callable(asset_url_fn) else "" cls = "text-[10px] text-stone-500" if count != 0 else "text-md text-red-500 font-bold" - item_parts.append(sexp_call( + item_parts.append(sx_call( "market-filter-sticker-item", href=href, hx_select=hx_select, ring_cls=ring, src=src, name=name, count_cls=cls, count=str(count), )) - items_sexp = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)" - return sexp_call("market-filter-stickers-row", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)" + return sx_call("market-filter-stickers-row", items=SxExpr(items_sx)) -def _brand_filter_sexp(brands: list, selected: list, ctx: dict, mobile: bool = False) -> str: - """Build brand filter checkboxes as sexp.""" +def _brand_filter_sx(brands: list, selected: list, ctx: dict, mobile: bool = False) -> str: + """Build brand filter checkboxes as sx.""" current_local_href = ctx.get("current_local_href", "/") hx_select = ctx.get("hx_select_search", "#main-panel") qs_fn = ctx.get("qs_filter") @@ -863,24 +863,24 @@ def _brand_filter_sexp(brands: list, selected: list, ctx: dict, mobile: bool = F href = "#" bg = " bg-yellow-200" if is_sel else "" cls = "text-md" if count else "text-md text-red-500" - item_parts.append(sexp_call( + item_parts.append(sx_call( "market-filter-brand-item", href=href, hx_select=hx_select, bg_cls=bg, name_cls=cls, name=name, count=str(count), )) - items_sexp = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)" - return sexp_call("market-filter-brands-panel", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)" + return sx_call("market-filter-brands-panel", items=SxExpr(items_sx)) -def _subcategory_selector_sexp(subs: list, top_href: str, current_sub: str, ctx: dict) -> str: - """Build subcategory vertical nav as sexp.""" +def _subcategory_selector_sx(subs: list, top_href: str, current_sub: str, ctx: dict) -> str: + """Build subcategory vertical nav as sx.""" hx_select = ctx.get("hx_select_search", "#main-panel") from shared.utils import route_prefix rp = route_prefix() all_cls = " bg-stone-200 font-medium" if not current_sub else "" all_full_href = rp + top_href - item_parts = [sexp_call( + item_parts = [sx_call( "market-filter-subcategory-item", href=all_full_href, hx_select=hx_select, active_cls=all_cls, name="All", )] @@ -891,20 +891,20 @@ def _subcategory_selector_sexp(subs: list, top_href: str, current_sub: str, ctx: active = (slug == current_sub) active_cls = " bg-stone-200 font-medium" if active else "" full_href = rp + href - item_parts.append(sexp_call( + item_parts.append(sx_call( "market-filter-subcategory-item", href=full_href, hx_select=hx_select, active_cls=active_cls, name=name, )) - items_sexp = "(<> " + " ".join(item_parts) + ")" - return sexp_call("market-filter-subcategory-panel", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(item_parts) + ")" + return sx_call("market-filter-subcategory-panel", items=SxExpr(items_sx)) # --------------------------------------------------------------------------- # Product detail page content # --------------------------------------------------------------------------- -def _product_detail_sexp(d: dict, ctx: dict) -> str: - """Build product detail main panel content as sexp.""" +def _product_detail_sx(d: dict, ctx: dict) -> str: + """Build product detail main panel content as sx.""" from quart import url_for from shared.browser.app.csrf import generate_csrf_token @@ -922,69 +922,69 @@ def _product_detail_sexp(d: dict, ctx: dict) -> str: # Gallery if images: # Like button - like_sexp = "" + like_sx = "" if user: - like_sexp = _like_button_sexp(slug, liked_by_current_user, csrf, ctx) + like_sx = _like_button_sx(slug, liked_by_current_user, csrf, ctx) # Main image + labels label_parts: list[str] = [] if callable(asset_url_fn): for l in labels: - label_parts.append(sexp_call( + label_parts.append(sx_call( "market-label-overlay", src=asset_url_fn("labels/" + l + ".svg"), )) - labels_sexp = "(<> " + " ".join(label_parts) + ")" if label_parts else None + labels_sx = "(<> " + " ".join(label_parts) + ")" if label_parts else None - gallery_inner = sexp_call( + gallery_inner = sx_call( "market-detail-gallery-inner", - like=SexpExpr(like_sexp) if like_sexp else None, + like=SxExpr(like_sx) if like_sx else None, image=images[0], alt=d.get("title", ""), - labels=SexpExpr(labels_sexp) if labels_sexp else None, + labels=SxExpr(labels_sx) if labels_sx else None, brand=brand, ) # Prev/next buttons nav_buttons = "" if len(images) > 1: - nav_buttons = sexp_call("market-detail-nav-buttons") + nav_buttons = sx_call("market-detail-nav-buttons") - gallery_sexp = sexp_call( + gallery_sx = sx_call( "market-detail-gallery", - inner=SexpExpr(gallery_inner), - nav=SexpExpr(nav_buttons) if nav_buttons else None, + inner=SxExpr(gallery_inner), + nav=SxExpr(nav_buttons) if nav_buttons else None, ) # Thumbnails - gallery_parts = [gallery_sexp] + gallery_parts = [gallery_sx] if len(images) > 1: thumb_parts = [] for i, u in enumerate(images): - thumb_parts.append(sexp_call( + thumb_parts.append(sx_call( "market-detail-thumb", title=f"Image {i+1}", src=u, alt=f"thumb {i+1}", )) - thumbs_sexp = "(<> " + " ".join(thumb_parts) + ")" - gallery_parts.append(sexp_call("market-detail-thumbs", thumbs=SexpExpr(thumbs_sexp))) + thumbs_sx = "(<> " + " ".join(thumb_parts) + ")" + gallery_parts.append(sx_call("market-detail-thumbs", thumbs=SxExpr(thumbs_sx))) gallery_final = "(<> " + " ".join(gallery_parts) + ")" else: - like_sexp = "" + like_sx = "" if user: - like_sexp = _like_button_sexp(slug, liked_by_current_user, csrf, ctx) - gallery_final = sexp_call("market-detail-no-image", - like=SexpExpr(like_sexp) if like_sexp else None) + like_sx = _like_button_sx(slug, liked_by_current_user, csrf, ctx) + gallery_final = sx_call("market-detail-no-image", + like=SxExpr(like_sx) if like_sx else None) # Stickers below gallery - stickers_sexp = "" + stickers_sx = "" if stickers and callable(asset_url_fn): sticker_parts = [] for s in stickers: - sticker_parts.append(sexp_call( + sticker_parts.append(sx_call( "market-detail-sticker", src=asset_url_fn("stickers/" + s + ".svg"), name=s, )) - sticker_items_sexp = "(<> " + " ".join(sticker_parts) + ")" - stickers_sexp = sexp_call("market-detail-stickers", items=SexpExpr(sticker_items_sexp)) + sticker_items_sx = "(<> " + " ".join(sticker_parts) + ")" + stickers_sx = sx_call("market-detail-stickers", items=SxExpr(sticker_items_sx)) # Right column: prices, description, sections pr = _set_prices(d) @@ -994,15 +994,15 @@ def _product_detail_sexp(d: dict, ctx: dict) -> str: extra_parts: list[str] = [] ppu = d.get("price_per_unit") or d.get("price_per_unit_raw") if ppu: - extra_parts.append(sexp_call( + extra_parts.append(sx_call( "market-detail-unit-price", price=_price_str(d.get("price_per_unit"), d.get("price_per_unit_raw"), d.get("price_per_unit_currency")), )) if d.get("case_size_raw"): - extra_parts.append(sexp_call("market-detail-case-size", size=d["case_size_raw"])) + extra_parts.append(sx_call("market-detail-case-size", size=d["case_size_raw"])) if extra_parts: - extras_sexp = "(<> " + " ".join(extra_parts) + ")" - detail_parts.append(sexp_call("market-detail-extras", inner=SexpExpr(extras_sexp))) + extras_sx = "(<> " + " ".join(extra_parts) + ")" + detail_parts.append(sx_call("market-detail-extras", inner=SxExpr(extras_sx))) # Description desc_short = d.get("description_short") @@ -1010,32 +1010,32 @@ def _product_detail_sexp(d: dict, ctx: dict) -> str: if desc_short or desc_html_val: desc_parts: list[str] = [] if desc_short: - desc_parts.append(sexp_call("market-detail-desc-short", text=desc_short)) + desc_parts.append(sx_call("market-detail-desc-short", text=desc_short)) if desc_html_val: - desc_parts.append(sexp_call("market-detail-desc-html", html=desc_html_val)) + desc_parts.append(sx_call("market-detail-desc-html", html=desc_html_val)) desc_inner = "(<> " + " ".join(desc_parts) + ")" - detail_parts.append(sexp_call("market-detail-desc-wrapper", inner=SexpExpr(desc_inner))) + detail_parts.append(sx_call("market-detail-desc-wrapper", inner=SxExpr(desc_inner))) # Sections (expandable) sections = d.get("sections", []) if sections: sec_parts = [] for sec in sections: - sec_parts.append(sexp_call( + sec_parts.append(sx_call( "market-detail-section", title=sec.get("title", ""), html=sec.get("html", ""), )) - sec_items_sexp = "(<> " + " ".join(sec_parts) + ")" - detail_parts.append(sexp_call("market-detail-sections", items=SexpExpr(sec_items_sexp))) + sec_items_sx = "(<> " + " ".join(sec_parts) + ")" + detail_parts.append(sx_call("market-detail-sections", items=SxExpr(sec_items_sx))) - details_inner_sexp = "(<> " + " ".join(detail_parts) + ")" if detail_parts else "(<>)" - details_sexp = sexp_call("market-detail-right-col", inner=SexpExpr(details_inner_sexp)) + details_inner_sx = "(<> " + " ".join(detail_parts) + ")" if detail_parts else "(<>)" + details_sx = sx_call("market-detail-right-col", inner=SxExpr(details_inner_sx)) - return sexp_call( + return sx_call( "market-detail-layout", - gallery=SexpExpr(gallery_final), - stickers=SexpExpr(stickers_sexp) if stickers_sexp else None, - details=SexpExpr(details_sexp), + gallery=SxExpr(gallery_final), + stickers=SxExpr(stickers_sx) if stickers_sx else None, + details=SxExpr(details_sx), ) @@ -1043,8 +1043,8 @@ def _product_detail_sexp(d: dict, ctx: dict) -> str: # Product meta (OpenGraph, JSON-LD) # --------------------------------------------------------------------------- -def _product_meta_sexp(d: dict, ctx: dict) -> str: - """Build product meta tags as sexp (auto-hoisted to by sexp.js).""" +def _product_meta_sx(d: dict, ctx: dict) -> str: + """Build product meta tags as sx (auto-hoisted to by sx.js).""" import json from quart import request @@ -1061,34 +1061,34 @@ def _product_meta_sexp(d: dict, ctx: dict) -> str: price = d.get("special_price") or d.get("regular_price") or d.get("rrp") price_currency = d.get("special_price_currency") or d.get("regular_price_currency") or d.get("rrp_currency") - parts = [sexp_call("market-meta-title", title=title)] - parts.append(sexp_call("market-meta-description", description=description)) + parts = [sx_call("market-meta-title", title=title)] + parts.append(sx_call("market-meta-description", description=description)) if canonical: - parts.append(sexp_call("market-meta-canonical", href=canonical)) + parts.append(sx_call("market-meta-canonical", href=canonical)) # OpenGraph site_title = ctx.get("base_title", "") - parts.append(sexp_call("market-meta-og", property="og:site_name", content=site_title)) - parts.append(sexp_call("market-meta-og", property="og:type", content="product")) - parts.append(sexp_call("market-meta-og", property="og:title", content=title)) - parts.append(sexp_call("market-meta-og", property="og:description", content=description)) + parts.append(sx_call("market-meta-og", property="og:site_name", content=site_title)) + parts.append(sx_call("market-meta-og", property="og:type", content="product")) + parts.append(sx_call("market-meta-og", property="og:title", content=title)) + parts.append(sx_call("market-meta-og", property="og:description", content=description)) if canonical: - parts.append(sexp_call("market-meta-og", property="og:url", content=canonical)) + parts.append(sx_call("market-meta-og", property="og:url", content=canonical)) if image_url: - parts.append(sexp_call("market-meta-og", property="og:image", content=image_url)) + parts.append(sx_call("market-meta-og", property="og:image", content=image_url)) if price and price_currency: - parts.append(sexp_call("market-meta-og", property="product:price:amount", content=f"{price:.2f}")) - parts.append(sexp_call("market-meta-og", property="product:price:currency", content=price_currency)) + parts.append(sx_call("market-meta-og", property="product:price:amount", content=f"{price:.2f}")) + parts.append(sx_call("market-meta-og", property="product:price:currency", content=price_currency)) if brand: - parts.append(sexp_call("market-meta-og", property="product:brand", content=brand)) + parts.append(sx_call("market-meta-og", property="product:brand", content=brand)) # Twitter card_type = "summary_large_image" if image_url else "summary" - parts.append(sexp_call("market-meta-twitter", name="twitter:card", content=card_type)) - parts.append(sexp_call("market-meta-twitter", name="twitter:title", content=title)) - parts.append(sexp_call("market-meta-twitter", name="twitter:description", content=description)) + parts.append(sx_call("market-meta-twitter", name="twitter:card", content=card_type)) + parts.append(sx_call("market-meta-twitter", name="twitter:title", content=title)) + parts.append(sx_call("market-meta-twitter", name="twitter:description", content=description)) if image_url: - parts.append(sexp_call("market-meta-twitter", name="twitter:image", content=image_url)) + parts.append(sx_call("market-meta-twitter", name="twitter:image", content=image_url)) # JSON-LD jsonld = { @@ -1110,7 +1110,7 @@ def _product_meta_sexp(d: dict, ctx: dict) -> str: "url": canonical, "availability": "https://schema.org/InStock", } - parts.append(sexp_call("market-meta-jsonld", json=json.dumps(jsonld))) + parts.append(sx_call("market-meta-jsonld", json=json.dumps(jsonld))) return "(<> " + " ".join(parts) + ")" @@ -1119,9 +1119,9 @@ def _product_meta_sexp(d: dict, ctx: dict) -> str: # Market cards (all markets / page markets) # --------------------------------------------------------------------------- -def _market_card_sexp(market: Any, page_info: dict, *, show_page_badge: bool = True, +def _market_card_sx(market: Any, page_info: dict, *, show_page_badge: bool = True, post_slug: str = "") -> str: - """Build a single market card as sexp.""" + """Build a single market card as sx.""" from shared.infrastructure.urls import market_url name = getattr(market, "name", "") @@ -1139,37 +1139,37 @@ def _market_card_sexp(market: Any, page_info: dict, *, show_page_badge: bool = T p_title = "" market_href = market_url(f"/{post_slug}/{slug}/") if post_slug else "" - title_sexp = "" + title_sx = "" if market_href: - title_sexp = sexp_call("market-market-card-title-link", href=market_href, name=name) + title_sx = sx_call("market-market-card-title-link", href=market_href, name=name) else: - title_sexp = sexp_call("market-market-card-title", name=name) + title_sx = sx_call("market-market-card-title", name=name) - desc_sexp = "" + desc_sx = "" if description: - desc_sexp = sexp_call("market-market-card-desc", description=description) + desc_sx = sx_call("market-market-card-desc", description=description) - badge_sexp = "" + badge_sx = "" if show_page_badge and p_title: badge_href = market_url(f"/{p_slug}/") - badge_sexp = sexp_call("market-market-card-badge", href=badge_href, title=p_title) + badge_sx = sx_call("market-market-card-badge", href=badge_href, title=p_title) - return sexp_call( + return sx_call( "market-market-card", - title_content=SexpExpr(title_sexp) if title_sexp else None, - desc_content=SexpExpr(desc_sexp) if desc_sexp else None, - badge_content=SexpExpr(badge_sexp) if badge_sexp else None, + title_content=SxExpr(title_sx) if title_sx else None, + desc_content=SxExpr(desc_sx) if desc_sx else None, + badge_content=SxExpr(badge_sx) if badge_sx else None, ) -def _market_cards_sexp(markets: list, page_info: dict, page: int, has_more: bool, +def _market_cards_sx(markets: list, page_info: dict, page: int, has_more: bool, next_url: str, *, show_page_badge: bool = True, post_slug: str = "") -> str: - """Build market cards with infinite scroll sentinel as sexp.""" - parts = [_market_card_sexp(m, page_info, show_page_badge=show_page_badge, + """Build market cards with infinite scroll sentinel as sx.""" + parts = [_market_card_sx(m, page_info, show_page_badge=show_page_badge, post_slug=post_slug) for m in markets] if has_more: - parts.append(sexp_call( + parts.append(sx_call( "market-market-sentinel", id=f"sentinel-{page}", next_url=next_url, )) @@ -1177,7 +1177,7 @@ def _market_cards_sexp(markets: list, page_info: dict, page: int, has_more: bool # --------------------------------------------------------------------------- -# OOB header helpers — _oob_header_sexp imported from shared +# OOB header helpers — _oob_header_sx imported from shared # --------------------------------------------------------------------------- @@ -1190,14 +1190,14 @@ def _market_cards_sexp(markets: list, page_info: dict, page: int, has_more: bool # All markets # --------------------------------------------------------------------------- -def _markets_grid(cards_sexp: str) -> str: - """Wrap market cards in a grid as sexp.""" - return sexp_call("market-markets-grid", cards=SexpExpr(cards_sexp)) +def _markets_grid(cards_sx: str) -> str: + """Wrap market cards in a grid as sx.""" + return sx_call("market-markets-grid", cards=SxExpr(cards_sx)) -def _no_markets_sexp(message: str = "No markets available") -> str: - """Empty state for markets as sexp.""" - return sexp_call("market-no-markets", message=message) +def _no_markets_sx(message: str = "No markets available") -> str: + """Empty state for markets as sx.""" + return sx_call("market-no-markets", message=message) async def render_all_markets_page(ctx: dict, markets: list, has_more: bool, @@ -1210,14 +1210,14 @@ async def render_all_markets_page(ctx: dict, markets: list, has_more: bool, next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1) if markets: - cards = _market_cards_sexp(markets, page_info, page, has_more, next_url) + cards = _market_cards_sx(markets, page_info, page, has_more, next_url) content = _markets_grid(cards) else: - content = _no_markets_sexp() - content = "(<> " + content + " " + sexp_call("market-bottom-spacer") + ")" + content = _no_markets_sx() + content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")" - hdr = root_header_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_all_markets_oob(ctx: dict, markets: list, has_more: bool, @@ -1230,14 +1230,14 @@ async def render_all_markets_oob(ctx: dict, markets: list, has_more: bool, next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1) if markets: - cards = _market_cards_sexp(markets, page_info, page, has_more, next_url) + cards = _market_cards_sx(markets, page_info, page, has_more, next_url) content = _markets_grid(cards) else: - content = _no_markets_sexp() - content = "(<> " + content + " " + sexp_call("market-bottom-spacer") + ")" + content = _no_markets_sx() + content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")" - oobs = root_header_sexp(ctx, oob=True) - return oob_page_sexp(oobs=oobs, content=content) + oobs = root_header_sx(ctx, oob=True) + return oob_page_sx(oobs=oobs, content=content) async def render_all_markets_cards(markets: list, has_more: bool, @@ -1248,7 +1248,7 @@ async def render_all_markets_cards(markets: list, has_more: bool, prefix = route_prefix() next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1) - return _market_cards_sexp(markets, page_info, page, has_more, next_url) + return _market_cards_sx(markets, page_info, page, has_more, next_url) # --------------------------------------------------------------------------- @@ -1267,16 +1267,16 @@ async def render_page_markets_page(ctx: dict, markets: list, has_more: bool, next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1) if markets: - cards = _market_cards_sexp(markets, {}, page, has_more, next_url, + cards = _market_cards_sx(markets, {}, page, has_more, next_url, show_page_badge=False, post_slug=post_slug) content = _markets_grid(cards) else: - content = _no_markets_sexp("No markets for this page") - content = "(<> " + content + " " + sexp_call("market-bottom-spacer") + ")" + content = _no_markets_sx("No markets for this page") + content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")" - hdr = root_header_sexp(ctx) - hdr = "(<> " + hdr + " " + header_child_sexp(_post_header_sexp(ctx)) + ")" - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + hdr = "(<> " + hdr + " " + header_child_sx(_post_header_sx(ctx)) + ")" + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_page_markets_oob(ctx: dict, markets: list, has_more: bool, @@ -1291,16 +1291,16 @@ async def render_page_markets_oob(ctx: dict, markets: list, has_more: bool, next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1) if markets: - cards = _market_cards_sexp(markets, {}, page, has_more, next_url, + cards = _market_cards_sx(markets, {}, page, has_more, next_url, show_page_badge=False, post_slug=post_slug) content = _markets_grid(cards) else: - content = _no_markets_sexp("No markets for this page") - content = "(<> " + content + " " + sexp_call("market-bottom-spacer") + ")" + content = _no_markets_sx("No markets for this page") + content = "(<> " + content + " " + sx_call("market-bottom-spacer") + ")" - oobs = _oob_header_sexp("post-header-child", "market-header-child", "") - oobs = "(<> " + oobs + " " + _post_header_sexp(ctx, oob=True) + ")" - return oob_page_sexp(oobs=oobs, content=content) + oobs = _oob_header_sx("post-header-child", "market-header-child", "") + oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + ")" + return oob_page_sx(oobs=oobs, content=content) async def render_page_markets_cards(markets: list, has_more: bool, @@ -1311,7 +1311,7 @@ async def render_page_markets_cards(markets: list, has_more: bool, prefix = route_prefix() next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1) - return _market_cards_sexp(markets, {}, page, has_more, next_url, + return _market_cards_sx(markets, {}, page, has_more, next_url, show_page_badge=False, post_slug=post_slug) @@ -1322,88 +1322,88 @@ async def render_page_markets_cards(markets: list, has_more: bool, async def render_market_home_page(ctx: dict) -> str: """Full page: market landing page (post content).""" post = ctx.get("post") or {} - content = _market_landing_content_sexp(post) + content = _market_landing_content_sx(post) - hdr = root_header_sexp(ctx) - child = "(<> " + _post_header_sexp(ctx) + " " + _market_header_sexp(ctx) + ")" - hdr = "(<> " + hdr + " " + header_child_sexp(child) + ")" - menu = _mobile_nav_panel_sexp(ctx) - return full_page_sexp(ctx, header_rows=hdr, content=content, menu=menu) + hdr = root_header_sx(ctx) + child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + ")" + hdr = "(<> " + hdr + " " + header_child_sx(child) + ")" + menu = _mobile_nav_panel_sx(ctx) + return full_page_sx(ctx, header_rows=hdr, content=content, menu=menu) async def render_market_home_oob(ctx: dict) -> str: """OOB response: market landing page.""" post = ctx.get("post") or {} - content = _market_landing_content_sexp(post) + content = _market_landing_content_sx(post) - oobs = _oob_header_sexp("post-header-child", "market-header-child", - _market_header_sexp(ctx)) - oobs = "(<> " + oobs + " " + _post_header_sexp(ctx, oob=True) + " " + oobs = _oob_header_sx("post-header-child", "market-header-child", + _market_header_sx(ctx)) + oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + " " oobs += _clear_deeper_oob("post-row", "post-header-child", "market-row", "market-header-child") + ")" - menu = _mobile_nav_panel_sexp(ctx) - return oob_page_sexp(oobs=oobs, content=content, menu=menu) + menu = _mobile_nav_panel_sx(ctx) + return oob_page_sx(oobs=oobs, content=content, menu=menu) -def _market_landing_content_sexp(post: dict) -> str: - """Build market landing page content as sexp.""" +def _market_landing_content_sx(post: dict) -> str: + """Build market landing page content as sx.""" parts: list[str] = [] if post.get("custom_excerpt"): - parts.append(sexp_call("market-landing-excerpt", text=post["custom_excerpt"])) + parts.append(sx_call("market-landing-excerpt", text=post["custom_excerpt"])) if post.get("feature_image"): - parts.append(sexp_call("market-landing-image", src=post["feature_image"])) + parts.append(sx_call("market-landing-image", src=post["feature_image"])) if post.get("html"): - parts.append(sexp_call("market-landing-html", html=post["html"])) + parts.append(sx_call("market-landing-html", html=post["html"])) inner = "(<> " + " ".join(parts) + ")" if parts else "(<>)" - return sexp_call("market-landing-content", inner=SexpExpr(inner)) + return sx_call("market-landing-content", inner=SxExpr(inner)) # --------------------------------------------------------------------------- # Browse page # --------------------------------------------------------------------------- -def _product_grid(cards_sexp: str) -> str: - """Wrap product cards in a grid as sexp.""" - return sexp_call("market-product-grid", cards=SexpExpr(cards_sexp)) +def _product_grid(cards_sx: str) -> str: + """Wrap product cards in a grid as sx.""" + return sx_call("market-product-grid", cards=SxExpr(cards_sx)) async def render_browse_page(ctx: dict) -> str: """Full page: product browse with filters.""" - cards = _product_cards_sexp(ctx) + cards = _product_cards_sx(ctx) content = _product_grid(cards) - hdr = root_header_sexp(ctx) - child = "(<> " + _post_header_sexp(ctx) + " " + _market_header_sexp(ctx) + ")" - hdr = "(<> " + hdr + " " + header_child_sexp(child) + ")" - menu = _mobile_nav_panel_sexp(ctx) - filter_sexp = _mobile_filter_summary_sexp(ctx) - aside_sexp = _desktop_filter_sexp(ctx) + hdr = root_header_sx(ctx) + child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + ")" + hdr = "(<> " + hdr + " " + header_child_sx(child) + ")" + menu = _mobile_nav_panel_sx(ctx) + filter_sx = _mobile_filter_summary_sx(ctx) + aside_sx = _desktop_filter_sx(ctx) - return full_page_sexp(ctx, header_rows=hdr, content=content, - menu=menu, filter=filter_sexp, aside=aside_sexp) + return full_page_sx(ctx, header_rows=hdr, content=content, + menu=menu, filter=filter_sx, aside=aside_sx) async def render_browse_oob(ctx: dict) -> str: """OOB response: product browse.""" - cards = _product_cards_sexp(ctx) + cards = _product_cards_sx(ctx) content = _product_grid(cards) - oobs = _oob_header_sexp("post-header-child", "market-header-child", - _market_header_sexp(ctx)) - oobs = "(<> " + oobs + " " + _post_header_sexp(ctx, oob=True) + " " + oobs = _oob_header_sx("post-header-child", "market-header-child", + _market_header_sx(ctx)) + oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + " " oobs += _clear_deeper_oob("post-row", "post-header-child", "market-row", "market-header-child") + ")" - menu = _mobile_nav_panel_sexp(ctx) - filter_sexp = _mobile_filter_summary_sexp(ctx) - aside_sexp = _desktop_filter_sexp(ctx) + menu = _mobile_nav_panel_sx(ctx) + filter_sx = _mobile_filter_summary_sx(ctx) + aside_sx = _desktop_filter_sx(ctx) - return oob_page_sexp(oobs=oobs, content=content, - menu=menu, filter=filter_sexp, aside=aside_sexp) + return oob_page_sx(oobs=oobs, content=content, + menu=menu, filter=filter_sx, aside=aside_sx) async def render_browse_cards(ctx: dict) -> str: - """Pagination fragment: product cards — sexp wire format.""" - return _product_cards_sexp(ctx) + """Pagination fragment: product cards — sx wire format.""" + return _product_cards_sx(ctx) # --------------------------------------------------------------------------- @@ -1412,27 +1412,27 @@ async def render_browse_cards(ctx: dict) -> str: async def render_product_page(ctx: dict, d: dict) -> str: """Full page: product detail.""" - content = _product_detail_sexp(d, ctx) - meta = _product_meta_sexp(d, ctx) + content = _product_detail_sx(d, ctx) + meta = _product_meta_sx(d, ctx) - hdr = root_header_sexp(ctx) - child = "(<> " + _post_header_sexp(ctx) + " " + _market_header_sexp(ctx) + " " + _product_header_sexp(ctx, d) + ")" - hdr = "(<> " + hdr + " " + header_child_sexp(child) + ")" - return full_page_sexp(ctx, header_rows=hdr, content=content, meta=meta) + hdr = root_header_sx(ctx) + child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + " " + _product_header_sx(ctx, d) + ")" + hdr = "(<> " + hdr + " " + header_child_sx(child) + ")" + return full_page_sx(ctx, header_rows=hdr, content=content, meta=meta) async def render_product_oob(ctx: dict, d: dict) -> str: """OOB response: product detail.""" - content = _product_detail_sexp(d, ctx) + content = _product_detail_sx(d, ctx) - oobs = "(<> " + _market_header_sexp(ctx, oob=True) + " " - oobs += _oob_header_sexp("market-header-child", "product-header-child", - _product_header_sexp(ctx, d)) + " " + oobs = "(<> " + _market_header_sx(ctx, oob=True) + " " + oobs += _oob_header_sx("market-header-child", "product-header-child", + _product_header_sx(ctx, d)) + " " oobs += _clear_deeper_oob("post-row", "post-header-child", "market-row", "market-header-child", "product-row", "product-header-child") + ")" - menu = _mobile_nav_panel_sexp(ctx) - return oob_page_sexp(oobs=oobs, content=content, menu=menu) + menu = _mobile_nav_panel_sx(ctx) + return oob_page_sx(oobs=oobs, content=content, menu=menu) # --------------------------------------------------------------------------- @@ -1441,36 +1441,36 @@ async def render_product_oob(ctx: dict, d: dict) -> str: async def render_product_admin_page(ctx: dict, d: dict) -> str: """Full page: product admin.""" - content = _product_detail_sexp(d, ctx) + content = _product_detail_sx(d, ctx) - hdr = root_header_sexp(ctx) - child = "(<> " + _post_header_sexp(ctx) + " " + _market_header_sexp(ctx) - child += " " + _product_header_sexp(ctx, d) + " " + _product_admin_header_sexp(ctx, d) + ")" - hdr = "(<> " + hdr + " " + header_child_sexp(child) + ")" - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + child += " " + _product_header_sx(ctx, d) + " " + _product_admin_header_sx(ctx, d) + ")" + hdr = "(<> " + hdr + " " + header_child_sx(child) + ")" + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_product_admin_oob(ctx: dict, d: dict) -> str: """OOB response: product admin.""" - content = _product_detail_sexp(d, ctx) + content = _product_detail_sx(d, ctx) - oobs = "(<> " + _product_header_sexp(ctx, d, oob=True) + " " - oobs += _oob_header_sexp("product-header-child", "product-admin-header-child", - _product_admin_header_sexp(ctx, d)) + " " + oobs = "(<> " + _product_header_sx(ctx, d, oob=True) + " " + oobs += _oob_header_sx("product-header-child", "product-admin-header-child", + _product_admin_header_sx(ctx, d)) + " " oobs += _clear_deeper_oob("post-row", "post-header-child", "market-row", "market-header-child", "product-row", "product-header-child", "product-admin-row", "product-admin-header-child") + ")" - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) -def _product_admin_header_sexp(ctx: dict, d: dict, *, oob: bool = False) -> str: - """Build product admin header row as sexp.""" +def _product_admin_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str: + """Build product admin header row as sx.""" from quart import url_for slug = d.get("slug", "") link_href = url_for("market.browse.product.admin", product_slug=slug) - return sexp_call( + return sx_call( "menu-row-sx", id="product-admin-row", level=4, link_href=link_href, link_label="admin!!", icon="fa fa-cog", @@ -1486,30 +1486,30 @@ async def render_market_admin_page(ctx: dict) -> str: """Full page: market admin.""" content = '"market admin"' - hdr = root_header_sexp(ctx) - child = "(<> " + _post_header_sexp(ctx) + " " + _market_header_sexp(ctx) + " " - child += _market_admin_header_sexp(ctx, selected="markets") + ")" - hdr = "(<> " + hdr + " " + header_child_sexp(child) + ")" - return full_page_sexp(ctx, header_rows=hdr, content=content) + hdr = root_header_sx(ctx) + child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + " " + child += _market_admin_header_sx(ctx, selected="markets") + ")" + hdr = "(<> " + hdr + " " + header_child_sx(child) + ")" + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_market_admin_oob(ctx: dict) -> str: """OOB response: market admin.""" content = '"market admin"' - oobs = "(<> " + _market_header_sexp(ctx, oob=True) + " " - oobs += _oob_header_sexp("market-header-child", "market-admin-header-child", - _market_admin_header_sexp(ctx, selected="markets")) + " " + oobs = "(<> " + _market_header_sx(ctx, oob=True) + " " + oobs += _oob_header_sx("market-header-child", "market-admin-header-child", + _market_admin_header_sx(ctx, selected="markets")) + " " oobs += _clear_deeper_oob("post-row", "post-header-child", "market-row", "market-header-child", "market-admin-row", "market-admin-header-child") + ")" - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) -def _market_admin_header_sexp(ctx: dict, *, oob: bool = False, selected: str = "") -> str: +def _market_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str: """Build market admin header row — delegates to shared helper.""" slug = (ctx.get("post") or {}).get("slug", "") - return post_admin_header_sexp(ctx, slug, oob=oob, selected=selected) + return post_admin_header_sx(ctx, slug, oob=oob, selected=selected) # --------------------------------------------------------------------------- @@ -1519,22 +1519,22 @@ def _market_admin_header_sexp(ctx: dict, *, oob: bool = False, selected: str = " async def render_page_admin_page(ctx: dict) -> str: """Full page: page-level market admin.""" slug = (ctx.get("post") or {}).get("slug", "") - admin_hdr = post_admin_header_sexp(ctx, slug, selected="markets") - hdr = root_header_sexp(ctx) - child = "(<> " + _post_header_sexp(ctx) + " " + admin_hdr + ")" - hdr = "(<> " + hdr + " " + header_child_sexp(child) + ")" + admin_hdr = post_admin_header_sx(ctx, slug, selected="markets") + hdr = root_header_sx(ctx) + child = "(<> " + _post_header_sx(ctx) + " " + admin_hdr + ")" + hdr = "(<> " + hdr + " " + header_child_sx(child) + ")" content = '(div :id "main-panel" (div :class "p-4 text-stone-500" "Market admin"))' - return full_page_sexp(ctx, header_rows=hdr, content=content) + return full_page_sx(ctx, header_rows=hdr, content=content) async def render_page_admin_oob(ctx: dict) -> str: """OOB response: page-level market admin.""" slug = (ctx.get("post") or {}).get("slug", "") - oobs = "(<> " + post_admin_header_sexp(ctx, slug, oob=True, selected="markets") + " " + oobs = "(<> " + post_admin_header_sx(ctx, slug, oob=True, selected="markets") + " " oobs += _clear_deeper_oob("post-row", "post-header-child", "post-admin-row", "post-admin-header-child") + ")" content = '(div :id "main-panel" (div :class "p-4 text-stone-500" "Market admin"))' - return oob_page_sexp(oobs=oobs, content=content) + return oob_page_sx(oobs=oobs, content=content) # --------------------------------------------------------------------------- @@ -1565,7 +1565,7 @@ def render_like_toggle_button(slug: str, liked: bool, *, icon = "fa-regular fa-heart" label = f"Like this {item_type}" - return sexp_call( + return sx_call( "market-like-toggle-button", colour=colour, action=like_url, hx_headers=f'{{"X-CSRFToken": "{csrf}"}}', @@ -1589,33 +1589,33 @@ def render_cart_added_response(cart: list, item: Any, d: dict) -> str: # 1. Cart mini icon OOB if count > 0: cart_href = _cart_url("/") - cart_mini = sexp_call("market-cart-mini-count", href=cart_href, count=str(count)) + cart_mini = sx_call("market-cart-mini-count", href=cart_href, count=str(count)) else: from shared.config import config blog_href = config().get("blog_url", "/") logo = config().get("logo", "") - cart_mini = sexp_call("market-cart-mini-empty", href=blog_href, logo=logo) + cart_mini = sx_call("market-cart-mini-empty", href=blog_href, logo=logo) # 2. Add/remove buttons OOB action = url_for("market.browse.product.cart", product_slug=slug) quantity = getattr(item, "quantity", 0) if item else 0 if not quantity: - cart_add = sexp_call( + cart_add = sx_call( "market-cart-add-empty", cart_id=f"cart-{slug}", action=action, csrf=csrf, ) else: cart_href = _cart_url("/") if callable(_cart_url) else "/" - cart_add = sexp_call( + cart_add = sx_call( "market-cart-add-quantity", cart_id=f"cart-{slug}", action=action, csrf=csrf, minus_val=str(quantity - 1), plus_val=str(quantity + 1), quantity=str(quantity), cart_href=cart_href, ) - add_sexp = sexp_call( + add_sx = sx_call( "market-cart-add-oob", id=f"cart-add-{slug}", - inner=SexpExpr(cart_add), + inner=SxExpr(cart_add), ) - return "(<> " + cart_mini + " " + add_sexp + ")" + return "(<> " + cart_mini + " " + add_sx + ")" diff --git a/orders/app.py b/orders/app.py index 1a85ccb..2f70835 100644 --- a/orders/app.py +++ b/orders/app.py @@ -1,6 +1,6 @@ from __future__ import annotations import path_setup # noqa: F401 # adds shared/ to sys.path -import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file +import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file from pathlib import Path from types import SimpleNamespace @@ -71,7 +71,7 @@ def create_app() -> "Quart": ]) # Load orders-specific s-expression components - from sexp.sexp_components import load_orders_components + from sx.sx_components import load_orders_components load_orders_components() app.register_blueprint(register_fragments()) diff --git a/orders/bp/fragments/routes.py b/orders/bp/fragments/routes.py index b499924..8d3cf48 100644 --- a/orders/bp/fragments/routes.py +++ b/orders/bp/fragments/routes.py @@ -14,9 +14,9 @@ def register(): async def _account_nav_item(): from shared.infrastructure.urls import orders_url - from shared.sexp.helpers import sexp_call + from shared.sx.helpers import sx_call - return sexp_call("account-nav-item", + return sx_call("account-nav-item", href=orders_url("/"), label="orders") @@ -33,8 +33,8 @@ def register(): async def get_fragment(fragment_type: str): handler = _handlers.get(fragment_type) if handler is None: - return Response("", status=200, content_type="text/sexp") + return Response("", status=200, content_type="text/sx") src = await handler() - return Response(src, status=200, content_type="text/sexp") + return Response(src, status=200, content_type="text/sx") return bp diff --git a/orders/bp/order/routes.py b/orders/bp/order/routes.py index 3a383d7..6710fd6 100644 --- a/orders/bp/order/routes.py +++ b/orders/bp/order/routes.py @@ -9,7 +9,7 @@ from shared.browser.app.payments.sumup import create_checkout as sumup_create_ch from shared.config import config from shared.infrastructure.cart_identity import current_cart_identity -from shared.sexp.page import get_template_context +from shared.sx.page import get_template_context from services.check_sumup_status import check_sumup_status from shared.browser.app.utils.htmx import is_htmx_request @@ -48,7 +48,7 @@ def register() -> Blueprint: if not order: return await make_response("Order not found", 404) - from sexp.sexp_components import render_order_page, render_order_oob + from sx.sx_components import render_order_page, render_order_oob ctx = await get_template_context() calendar_entries = ctx.get("calendar_entries") @@ -57,9 +57,9 @@ def register() -> Blueprint: html = await render_order_page(ctx, order, calendar_entries, url_for) return await make_response(html) else: - from shared.sexp.helpers import sexp_response - sexp_src = await render_order_oob(ctx, order, calendar_entries, url_for) - return sexp_response(sexp_src) + from shared.sx.helpers import sx_response + sx_src = await render_order_oob(ctx, order, calendar_entries, url_for) + return sx_response(sx_src) @bp.get("/pay/") async def order_pay(order_id: int): @@ -100,8 +100,8 @@ def register() -> Blueprint: await g.s.flush() if not hosted_url: - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_checkout_error_page + from shared.sx.page import get_template_context + from sx.sx_components import render_checkout_error_page tctx = await get_template_context() html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp when trying to reopen payment.", order=order) return await make_response(html, 500) diff --git a/orders/bp/orders/routes.py b/orders/bp/orders/routes.py index cc037a4..c0e890c 100644 --- a/orders/bp/orders/routes.py +++ b/orders/bp/orders/routes.py @@ -116,8 +116,8 @@ def register(url_prefix: str) -> Blueprint: result = await g.s.execute(stmt) orders = result.scalars().all() - from shared.sexp.page import get_template_context - from sexp.sexp_components import ( + from shared.sx.page import get_template_context + from sx.sx_components import ( render_orders_page, render_orders_rows, render_orders_oob, @@ -133,19 +133,19 @@ def register(url_prefix: str) -> Blueprint: ) resp = await make_response(html) elif page > 1: - # Sexp wire format — client renders order rows - from shared.sexp.helpers import sexp_response - sexp_src = await render_orders_rows( + # Sx wire format — client renders order rows + from shared.sx.helpers import sx_response + sx_src = await render_orders_rows( ctx, orders, page, total_pages, url_for, qs_fn, ) - resp = sexp_response(sexp_src) + resp = sx_response(sx_src) else: - from shared.sexp.helpers import sexp_response - sexp_src = await render_orders_oob( + from shared.sx.helpers import sx_response + sx_src = await render_orders_oob( ctx, orders, page, total_pages, search, total_count, url_for, qs_fn, ) - resp = sexp_response(sexp_src) + resp = sx_response(sx_src) resp.headers["Hx-Push-Url"] = _current_url_without_page() return _vary(resp) diff --git a/orders/sexp/__init__.py b/orders/sx/__init__.py similarity index 100% rename from orders/sexp/__init__.py rename to orders/sx/__init__.py diff --git a/orders/sexp/checkout.sexpr b/orders/sx/checkout.sx similarity index 100% rename from orders/sexp/checkout.sexpr rename to orders/sx/checkout.sx diff --git a/orders/sexp/detail.sexpr b/orders/sx/detail.sx similarity index 100% rename from orders/sexp/detail.sexpr rename to orders/sx/detail.sx diff --git a/orders/sexp/list.sexpr b/orders/sx/list.sx similarity index 100% rename from orders/sexp/list.sexpr rename to orders/sx/list.sx diff --git a/orders/sexp/sexp_components.py b/orders/sx/sx_components.py similarity index 63% rename from orders/sexp/sexp_components.py rename to orders/sx/sx_components.py index 2776a1d..e1a0d66 100644 --- a/orders/sexp/sexp_components.py +++ b/orders/sx/sx_components.py @@ -10,27 +10,27 @@ from __future__ import annotations import os from typing import Any -from shared.sexp.jinja_bridge import load_service_components -from shared.sexp.helpers import ( - call_url, root_header_sexp, - full_page_sexp, header_child_sexp, oob_page_sexp, - sexp_call, SexpExpr, - search_mobile_sexp, search_desktop_sexp, +from shared.sx.jinja_bridge import load_service_components +from shared.sx.helpers import ( + call_url, root_header_sx, + full_page_sx, header_child_sx, oob_page_sx, + sx_call, SxExpr, + search_mobile_sx, search_desktop_sx, ) from shared.infrastructure.urls import market_product_url, cart_url -# Load orders-specific .sexpr components at import time +# Load orders-specific .sx components at import time load_service_components(os.path.dirname(os.path.dirname(__file__))) # --------------------------------------------------------------------------- -# Header helpers (shared auth + orders-specific) — sexp-native +# Header helpers (shared auth + orders-specific) — sx-native # --------------------------------------------------------------------------- -def _auth_nav_sexp(ctx: dict) -> str: - """Auth section desktop nav items as sexp.""" +def _auth_nav_sx(ctx: dict) -> str: + """Auth section desktop nav items as sx.""" parts = [ - sexp_call("nav-link", + sx_call("nav-link", href=call_url(ctx, "account_url", "/newsletters/"), label="newsletters", select_colours=ctx.get("select_colours", ""), @@ -42,21 +42,21 @@ def _auth_nav_sexp(ctx: dict) -> str: return "(<> " + " ".join(parts) + ")" -def _auth_header_sexp(ctx: dict, *, oob: bool = False) -> str: - """Build the account section header row as sexp.""" - return sexp_call( +def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str: + """Build the account section header row as sx.""" + return sx_call( "menu-row-sx", id="auth-row", level=1, colour="sky", link_href=call_url(ctx, "account_url", "/"), link_label="account", icon="fa-solid fa-user", - nav=SexpExpr(_auth_nav_sexp(ctx)), + nav=SxExpr(_auth_nav_sx(ctx)), child_id="auth-header-child", oob=oob, ) -def _orders_header_sexp(ctx: dict, list_url: str) -> str: - """Build the orders section header row as sexp.""" - return sexp_call( +def _orders_header_sx(ctx: dict, list_url: str) -> str: + """Build the orders section header row as sx.""" + return sx_call( "menu-row-sx", id="orders-row", level=2, colour="sky", link_href=list_url, link_label="Orders", icon="fa fa-gbp", @@ -94,7 +94,7 @@ def _order_row_data(order: Any, detail_url: str) -> dict: -def _orders_rows_sexp(orders: list, page: int, total_pages: int, +def _orders_rows_sx(orders: list, page: int, total_pages: int, url_for_fn: Any, qs_fn: Any) -> str: """S-expression wire format for order rows (client renders).""" from shared.utils import route_prefix @@ -103,38 +103,38 @@ def _orders_rows_sexp(orders: list, page: int, total_pages: int, parts = [] for o in orders: d = _order_row_data(o, pfx + url_for_fn("orders.order.order_detail", order_id=o.id)) - parts.append(sexp_call("orders-row-desktop", + parts.append(sx_call("orders-row-desktop", oid=d["oid"], created=d["created"], desc=d["desc"], total=d["total"], pill=d["pill_desktop"], status=d["status"], url=d["url"])) - parts.append(sexp_call("orders-row-mobile", + parts.append(sx_call("orders-row-mobile", oid=d["oid"], created=d["created"], total=d["total"], pill=d["pill_mobile"], status=d["status"], url=d["url"])) if page < total_pages: next_url = pfx + url_for_fn("orders.list_orders") + qs_fn(page=page + 1) - parts.append(sexp_call("infinite-scroll", + parts.append(sx_call("infinite-scroll", url=next_url, page=page, total_pages=total_pages, id_prefix="orders", colspan=5)) else: - parts.append(sexp_call("orders-end-row")) + parts.append(sx_call("orders-end-row")) return "(<> " + " ".join(parts) + ")" -def _orders_main_panel_sexp(orders: list, rows_sexp: str) -> str: - """Main panel with table or empty state (sexp).""" +def _orders_main_panel_sx(orders: list, rows_sx: str) -> str: + """Main panel with table or empty state (sx).""" if not orders: - return sexp_call("orders-empty-state") - return sexp_call("orders-table", rows=SexpExpr(rows_sexp)) + return sx_call("orders-empty-state") + return sx_call("orders-table", rows=SxExpr(rows_sx)) -def _orders_summary_sexp(ctx: dict) -> str: - """Filter section for orders list (sexp).""" - return sexp_call("orders-summary", search_mobile=SexpExpr(search_mobile_sexp(ctx))) +def _orders_summary_sx(ctx: dict) -> str: + """Filter section for orders list (sx).""" + return sx_call("orders-summary", search_mobile=SxExpr(search_mobile_sx(ctx))) @@ -146,31 +146,31 @@ async def render_orders_page(ctx: dict, orders: list, page: int, total_pages: int, search: str | None, search_count: int, url_for_fn: Any, qs_fn: Any) -> str: - """Full page: orders list (sexp wire format).""" + """Full page: orders list (sx wire format).""" from shared.utils import route_prefix ctx["search"] = search ctx["search_count"] = search_count list_url = route_prefix() + url_for_fn("orders.list_orders") - rows = _orders_rows_sexp(orders, page, total_pages, url_for_fn, qs_fn) - main = _orders_main_panel_sexp(orders, rows) + rows = _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn) + main = _orders_main_panel_sx(orders, rows) - hdr = root_header_sexp(ctx) - inner = "(<> " + _auth_header_sexp(ctx) + " " + _orders_header_sexp(ctx, list_url) + ")" - hdr = "(<> " + hdr + " " + header_child_sexp(inner) + ")" + hdr = root_header_sx(ctx) + inner = "(<> " + _auth_header_sx(ctx) + " " + _orders_header_sx(ctx, list_url) + ")" + hdr = "(<> " + hdr + " " + header_child_sx(inner) + ")" - return full_page_sexp(ctx, header_rows=hdr, - filter=_orders_summary_sexp(ctx), - aside=search_desktop_sexp(ctx), + return full_page_sx(ctx, header_rows=hdr, + filter=_orders_summary_sx(ctx), + aside=search_desktop_sx(ctx), content=main) async def render_orders_rows(ctx: dict, orders: list, page: int, total_pages: int, url_for_fn: Any, qs_fn: Any) -> str: - """Pagination: just the table rows (sexp wire format).""" - return _orders_rows_sexp(orders, page, total_pages, url_for_fn, qs_fn) + """Pagination: just the table rows (sx wire format).""" + return _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn) @@ -178,25 +178,25 @@ async def render_orders_oob(ctx: dict, orders: list, page: int, total_pages: int, search: str | None, search_count: int, url_for_fn: Any, qs_fn: Any) -> str: - """OOB response for HTMX navigation to orders list (sexp).""" + """OOB response for HTMX navigation to orders list (sx).""" from shared.utils import route_prefix ctx["search"] = search ctx["search_count"] = search_count list_url = route_prefix() + url_for_fn("orders.list_orders") - rows = _orders_rows_sexp(orders, page, total_pages, url_for_fn, qs_fn) - main = _orders_main_panel_sexp(orders, rows) + rows = _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn) + main = _orders_main_panel_sx(orders, rows) - auth_hdr = _auth_header_sexp(ctx, oob=True) - auth_child_oob = sexp_call("orders-auth-header-child-oob", - inner=SexpExpr(_orders_header_sexp(ctx, list_url))) - root_hdr = root_header_sexp(ctx, oob=True) + auth_hdr = _auth_header_sx(ctx, oob=True) + auth_child_oob = sx_call("orders-auth-header-child-oob", + inner=SxExpr(_orders_header_sx(ctx, list_url))) + root_hdr = root_header_sx(ctx, oob=True) oobs = "(<> " + auth_hdr + " " + auth_child_oob + " " + root_hdr + ")" - return oob_page_sexp(oobs=oobs, - filter=_orders_summary_sexp(ctx), - aside=search_desktop_sexp(ctx), + return oob_page_sx(oobs=oobs, + filter=_orders_summary_sx(ctx), + aside=search_desktop_sx(ctx), content=main) @@ -204,36 +204,36 @@ async def render_orders_oob(ctx: dict, orders: list, page: int, # Single order detail # --------------------------------------------------------------------------- -def _order_items_sexp(order: Any) -> str: - """Render order items list as sexp.""" +def _order_items_sx(order: Any) -> str: + """Render order items list as sx.""" if not order or not order.items: return "" items = [] for item in order.items: prod_url = market_product_url(item.product_slug) if item.product_image: - img = sexp_call( + img = sx_call( "orders-item-image", src=item.product_image, alt=item.product_title or "Product image", ) else: - img = sexp_call("orders-item-no-image") + img = sx_call("orders-item-no-image") - items.append(sexp_call( + items.append(sx_call( "orders-item-row", - href=prod_url, img=SexpExpr(img), + href=prod_url, img=SxExpr(img), title=item.product_title or "Unknown product", pid=str(item.product_id), qty=str(item.quantity), price=f"{item.currency or order.currency or 'GBP'} {item.unit_price or 0:.2f}", )) - items_sexp = "(<> " + " ".join(items) + ")" - return sexp_call("orders-items-section", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(items) + ")" + return sx_call("orders-items-section", items=SxExpr(items_sx)) -def _calendar_items_sexp(calendar_entries: list | None) -> str: - """Render calendar bookings for an order as sexp.""" +def _calendar_items_sx(calendar_entries: list | None) -> str: + """Render calendar bookings for an order as sx.""" if not calendar_entries: return "" items = [] @@ -248,7 +248,7 @@ def _calendar_items_sexp(calendar_entries: list | None) -> str: ds = e.start_at.strftime("%-d %b %Y, %H:%M") if e.start_at else "" if e.end_at: ds += f" \u2013 {e.end_at.strftime('%-d %b %Y, %H:%M')}" - items.append(sexp_call( + items.append(sx_call( "orders-calendar-item", name=e.name, pill=f"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium {pill}", @@ -256,52 +256,52 @@ def _calendar_items_sexp(calendar_entries: list | None) -> str: cost=f"\u00a3{e.cost or 0:.2f}", )) - items_sexp = "(<> " + " ".join(items) + ")" - return sexp_call("orders-calendar-section", items=SexpExpr(items_sexp)) + items_sx = "(<> " + " ".join(items) + ")" + return sx_call("orders-calendar-section", items=SxExpr(items_sx)) -def _order_main_sexp(order: Any, calendar_entries: list | None) -> str: - """Main panel for single order detail (sexp).""" - summary = sexp_call( +def _order_main_sx(order: Any, calendar_entries: list | None) -> str: + """Main panel for single order detail (sx).""" + summary = sx_call( "order-summary-card", order_id=order.id, created_at=order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else None, description=order.description, status=order.status, currency=order.currency, total_amount=f"{order.total_amount:.2f}" if order.total_amount else None, ) - items = _order_items_sexp(order) - calendar = _calendar_items_sexp(calendar_entries) - return sexp_call( + items = _order_items_sx(order) + calendar = _calendar_items_sx(calendar_entries) + return sx_call( "orders-detail-panel", - summary=SexpExpr(summary), - items=SexpExpr(items) if items else None, - calendar=SexpExpr(calendar) if calendar else None, + summary=SxExpr(summary), + items=SxExpr(items) if items else None, + calendar=SxExpr(calendar) if calendar else None, ) -def _order_filter_sexp(order: Any, list_url: str, recheck_url: str, +def _order_filter_sx(order: Any, list_url: str, recheck_url: str, pay_url: str, csrf_token: str) -> str: - """Filter section for single order detail (sexp).""" + """Filter section for single order detail (sx).""" created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014" status = order.status or "pending" pay = "" if status != "paid": - pay = sexp_call("orders-checkout-error-pay-btn", url=pay_url) + pay = sx_call("orders-checkout-error-pay-btn", url=pay_url) - return sexp_call( + return sx_call( "orders-detail-filter", created=created, status=status, list_url=list_url, recheck_url=recheck_url, csrf=csrf_token, - pay=SexpExpr(pay) if pay else None, + pay=SxExpr(pay) if pay else None, ) async def render_order_page(ctx: dict, order: Any, calendar_entries: list | None, url_for_fn: Any) -> str: - """Full page: single order detail (sexp wire format).""" + """Full page: single order detail (sx wire format).""" from shared.utils import route_prefix from shared.browser.app.csrf import generate_csrf_token @@ -311,31 +311,31 @@ async def render_order_page(ctx: dict, order: Any, recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id) pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id) - main = _order_main_sexp(order, calendar_entries) - filt = _order_filter_sexp(order, list_url, recheck_url, pay_url, generate_csrf_token()) + main = _order_main_sx(order, calendar_entries) + filt = _order_filter_sx(order, list_url, recheck_url, pay_url, generate_csrf_token()) # Header stack: root -> auth -> orders -> order - hdr = root_header_sexp(ctx) - order_row = sexp_call( + hdr = root_header_sx(ctx) + order_row = sx_call( "menu-row-sx", id="order-row", level=3, colour="sky", link_href=detail_url, link_label="Order", icon="fa fa-gbp", ) - detail_header = sexp_call( + detail_header = sx_call( "orders-detail-header-stack", - auth=SexpExpr(_auth_header_sexp(ctx)), - orders=SexpExpr(_orders_header_sexp(ctx, list_url)), - order=SexpExpr(order_row), + auth=SxExpr(_auth_header_sx(ctx)), + orders=SxExpr(_orders_header_sx(ctx, list_url)), + order=SxExpr(order_row), ) hdr = "(<> " + hdr + " " + detail_header + ")" - return full_page_sexp(ctx, header_rows=hdr, filter=filt, content=main) + return full_page_sx(ctx, header_rows=hdr, filter=filt, content=main) async def render_order_oob(ctx: dict, order: Any, calendar_entries: list | None, url_for_fn: Any) -> str: - """OOB response for single order detail (sexp).""" + """OOB response for single order detail (sx).""" from shared.utils import route_prefix from shared.browser.app.csrf import generate_csrf_token @@ -345,49 +345,49 @@ async def render_order_oob(ctx: dict, order: Any, recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id) pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id) - main = _order_main_sexp(order, calendar_entries) - filt = _order_filter_sexp(order, list_url, recheck_url, pay_url, generate_csrf_token()) + main = _order_main_sx(order, calendar_entries) + filt = _order_filter_sx(order, list_url, recheck_url, pay_url, generate_csrf_token()) - order_row_oob = sexp_call( + order_row_oob = sx_call( "menu-row-sx", id="order-row", level=3, colour="sky", link_href=detail_url, link_label="Order", icon="fa fa-gbp", oob=True, ) - header_child_oob = sexp_call("orders-header-child-oob", - inner=SexpExpr(order_row_oob)) - root_hdr = root_header_sexp(ctx, oob=True) + header_child_oob = sx_call("orders-header-child-oob", + inner=SxExpr(order_row_oob)) + root_hdr = root_header_sx(ctx, oob=True) oobs = "(<> " + header_child_oob + " " + root_hdr + ")" - return oob_page_sexp(oobs=oobs, filter=filt, content=main) + return oob_page_sx(oobs=oobs, filter=filt, content=main) # --------------------------------------------------------------------------- # Public API: Checkout error # --------------------------------------------------------------------------- -def _checkout_error_filter_sexp() -> str: - return sexp_call("orders-checkout-error-header") +def _checkout_error_filter_sx() -> str: + return sx_call("orders-checkout-error-header") -def _checkout_error_content_sexp(error: str | None, order: Any | None) -> str: +def _checkout_error_content_sx(error: str | None, order: Any | None) -> str: err_msg = error or "Unexpected error while creating the hosted checkout session." - order_sexp = "" + order_sx = "" if order: - order_sexp = sexp_call("orders-checkout-error-order-id", oid=f"#{order.id}") + order_sx = sx_call("orders-checkout-error-order-id", oid=f"#{order.id}") back_url = cart_url("/") - return sexp_call( + return sx_call( "orders-checkout-error-content", msg=err_msg, - order=SexpExpr(order_sexp) if order_sexp else None, + order=SxExpr(order_sx) if order_sx else None, back_url=back_url, ) async def render_checkout_error_page(ctx: dict, error: str | None = None, order: Any | None = None) -> str: - """Full page: checkout error (sexp wire format).""" - hdr = root_header_sexp(ctx) - inner = _auth_header_sexp(ctx) - hdr = "(<> " + hdr + " " + header_child_sexp(inner) + ")" - filt = _checkout_error_filter_sexp() - content = _checkout_error_content_sexp(error, order) - return full_page_sexp(ctx, header_rows=hdr, filter=filt, content=content) + """Full page: checkout error (sx wire format).""" + hdr = root_header_sx(ctx) + inner = _auth_header_sx(ctx) + hdr = "(<> " + hdr + " " + header_child_sx(inner) + ")" + filt = _checkout_error_filter_sx() + content = _checkout_error_content_sx(error, order) + return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content) diff --git a/orders/tests/test_sexp_helpers.py b/orders/tests/test_sx_helpers.py similarity index 86% rename from orders/tests/test_sexp_helpers.py rename to orders/tests/test_sx_helpers.py index 2c5b554..d8420d3 100644 --- a/orders/tests/test_sexp_helpers.py +++ b/orders/tests/test_sx_helpers.py @@ -1,9 +1,9 @@ -"""Unit tests for orders sexp component helpers.""" +"""Unit tests for orders sx component helpers.""" from __future__ import annotations import pytest -from orders.sexp.sexp_components import _status_pill_cls +from orders.sx.sx_components import _status_pill_cls class TestStatusPillCls: diff --git a/relations/bp/actions/routes.py b/relations/bp/actions/routes.py index ed532a2..edd8e5f 100644 --- a/relations/bp/actions/routes.py +++ b/relations/bp/actions/routes.py @@ -83,7 +83,7 @@ def register() -> Blueprint: async def _relate(): """Create a typed relation with registry validation and cardinality enforcement.""" from shared.services.relationships import attach_child, get_children - from shared.sexp.relations import get_relation + from shared.sx.relations import get_relation data = await request.get_json(force=True) rel_type = data.get("relation_type") @@ -136,7 +136,7 @@ def register() -> Blueprint: async def _unrelate(): """Remove a typed relation with registry validation.""" from shared.services.relationships import detach_child - from shared.sexp.relations import get_relation + from shared.sx.relations import get_relation data = await request.get_json(force=True) rel_type = data.get("relation_type") @@ -163,7 +163,7 @@ def register() -> Blueprint: async def _can_relate(): """Check if a relation can be created (cardinality, registry validation).""" from shared.services.relationships import get_children - from shared.sexp.relations import get_relation + from shared.sx.relations import get_relation data = await request.get_json(force=True) rel_type = data.get("relation_type") diff --git a/relations/bp/fragments/routes.py b/relations/bp/fragments/routes.py index d9c69cf..803c0ae 100644 --- a/relations/bp/fragments/routes.py +++ b/relations/bp/fragments/routes.py @@ -25,15 +25,15 @@ def register(): async def get_fragment(fragment_type: str): handler = _handlers.get(fragment_type) if handler is None: - return Response("", status=200, content_type="text/sexp") + return Response("", status=200, content_type="text/sx") src = await handler() - return Response(src, status=200, content_type="text/sexp") + return Response(src, status=200, content_type="text/sx") # --- generic container-nav fragment ---------------------------------------- async def _container_nav_handler(): - from shared.sexp.helpers import sexp_call - from shared.sexp.relations import relations_from + from shared.sx.helpers import sx_call + from shared.sx.relations import relations_from from shared.services.relationships import get_children from shared.infrastructure.urls import events_url, market_url @@ -76,7 +76,7 @@ def register(): path = f"/{slug}/" url_fn = _SERVICE_URL.get(defn.to_type) href = url_fn(path) if url_fn else path - parts.append(sexp_call("relation-nav", + parts.append(sx_call("relation-nav", href=href, name=child.label or "", icon=defn.nav_icon or "", diff --git a/shared/browser/app/errors.py b/shared/browser/app/errors.py index 3d7e922..c393328 100644 --- a/shared/browser/app/errors.py +++ b/shared/browser/app/errors.py @@ -57,9 +57,9 @@ def _error_page(message: str) -> str: ) -def _sexp_error_page(errnum: str, message: str, image: str | None = None) -> str: +def _sx_error_page(errnum: str, message: str, image: str | None = None) -> str: """Render an error page via s-expressions. Bypasses Jinja entirely.""" - from shared.sexp.page import render_page + from shared.sx.page import render_page return render_page( '(~error-page :title title :message message :image image :asset-url "/static")', @@ -147,24 +147,24 @@ async def _rich_error_page(errnum: str, message: str, image: str | None = None) pass # Root header (site nav bar) - from shared.sexp.helpers import ( - root_header_sexp, post_header_sexp, - header_child_sexp, full_page_sexp, sexp_call, + from shared.sx.helpers import ( + root_header_sx, post_header_sx, + header_child_sx, full_page_sx, sx_call, ) - hdr = root_header_sexp(ctx) + hdr = root_header_sx(ctx) # Post breadcrumb if we resolved a post post = (post_data or {}).get("post") or ctx.get("post") or {} if post.get("slug"): ctx["post"] = post - post_row = post_header_sexp(ctx) + post_row = post_header_sx(ctx) if post_row: - hdr = "(<> " + hdr + " " + header_child_sexp(post_row) + ")" + hdr = "(<> " + hdr + " " + header_child_sx(post_row) + ")" # Error content - error_content = sexp_call("error-content", errnum=errnum, message=message, image=image) + error_content = sx_call("error-content", errnum=errnum, message=message, image=image) - return full_page_sexp(ctx, header_rows=hdr, content=error_content) + return full_page_sx(ctx, header_rows=hdr, content=error_content) except Exception: current_app.logger.debug("Rich error page failed, falling back", exc_info=True) return None @@ -202,7 +202,7 @@ def errors(app): ) if html is None: try: - html = _sexp_error_page( + html = _sx_error_page( "404", "NOT FOUND", image="/static/errors/404.gif", ) @@ -224,7 +224,7 @@ def errors(app): ) else: try: - html = _sexp_error_page( + html = _sx_error_page( "403", "FORBIDDEN", image="/static/errors/403.gif", ) @@ -244,7 +244,7 @@ def errors(app): messages = getattr(e, "messages", [str(e)]) if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true": - from shared.sexp.jinja_bridge import render as render_comp + from shared.sx.jinja_bridge import render as render_comp items = "".join( render_comp("error-list-item", message=str(escape(m))) for m in messages if m @@ -266,7 +266,7 @@ def errors(app): # Extract service name from "Fragment account/auth-menu failed: ..." service = msg.split("/")[0].replace("Fragment ", "") if "/" in msg else "unknown" if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true": - from shared.sexp.jinja_bridge import render as render_comp + from shared.sx.jinja_bridge import render as render_comp return await make_response( render_comp("fragment-error", service=str(escape(service))), 503, diff --git a/shared/dev_watcher.py b/shared/dev_watcher.py index 0a7a4c4..ff31297 100644 --- a/shared/dev_watcher.py +++ b/shared/dev_watcher.py @@ -1,7 +1,7 @@ """Watch non-Python files and trigger Hypercorn reload. -Hypercorn --reload only watches .py files. This script watches .sexp, -.sexpr, .js, and .css files and touches a sentinel .py file when they +Hypercorn --reload only watches .py files. This script watches .sx, +.sx, .js, and .css files and touches a sentinel .py file when they change, causing Hypercorn to restart. Usage (from entrypoint.sh, before exec hypercorn): @@ -12,7 +12,7 @@ import os import time import sys -WATCH_EXTENSIONS = {".sexp", ".sexpr", ".js", ".css"} +WATCH_EXTENSIONS = {".sx", ".sx", ".js", ".css"} SENTINEL = os.path.join(os.path.dirname(__file__), "_reload_sentinel.py") POLL_INTERVAL = 1.5 # seconds @@ -33,7 +33,7 @@ def _collect_mtimes(roots): def main(): - # Watch /app/shared and /app//sexp plus static dirs + # Watch /app/shared and /app//sx plus static dirs roots = [] for entry in os.listdir("/app"): full = os.path.join("/app", entry) diff --git a/shared/infrastructure/context.py b/shared/infrastructure/context.py index 067aa8c..f69645d 100644 --- a/shared/infrastructure/context.py +++ b/shared/infrastructure/context.py @@ -18,9 +18,9 @@ from shared.infrastructure.urls import blog_url, market_url, cart_url, events_ur def _qs_filter_fn(): - """Build a qs_filter(dict) wrapper for sexp components, or None. + """Build a qs_filter(dict) wrapper for sx components, or None. - Sexp components call ``qs_fn({"page": 2})``, ``qs_fn({"sort": "az"})``, + Sx components call ``qs_fn({"page": 2})``, ``qs_fn({"sort": "az"})``, ``qs_fn({"labels": ["organic", "local"]})``, etc. Simple keys (page, sort, search, liked, clear_filters) are forwarded diff --git a/shared/infrastructure/factory.py b/shared/infrastructure/factory.py index 323b6cc..1cd2298 100644 --- a/shared/infrastructure/factory.py +++ b/shared/infrastructure/factory.py @@ -28,9 +28,9 @@ from shared.browser.app.errors import errors from .jinja_setup import setup_jinja from .user_loader import load_current_user -from shared.sexp.jinja_bridge import setup_sexp_bridge -from shared.sexp.components import load_shared_components -from shared.sexp.relations import load_relation_registry +from shared.sx.jinja_bridge import setup_sx_bridge +from shared.sx.components import load_shared_components +from shared.sx.relations import load_relation_registry # Async init of config (runs once at import) @@ -111,16 +111,16 @@ def create_base_app( register_db(app) register_redis(app) setup_jinja(app) - setup_sexp_bridge(app) + setup_sx_bridge(app) load_shared_components() load_relation_registry() - # Dev-mode: auto-reload sexp templates when files change on disk + # Dev-mode: auto-reload sx templates when files change on disk if os.getenv("RELOAD") == "true": - from shared.sexp.jinja_bridge import reload_if_changed + from shared.sx.jinja_bridge import reload_if_changed @app.before_request - async def _sexp_hot_reload(): + async def _sx_hot_reload(): reload_if_changed() errors(app) diff --git a/shared/infrastructure/fragments.py b/shared/infrastructure/fragments.py index 62ff2b3..39c35c4 100644 --- a/shared/infrastructure/fragments.py +++ b/shared/infrastructure/fragments.py @@ -78,15 +78,15 @@ async def fetch_fragment( ) -> str: """Fetch a fragment from another app. - Returns an HTML string or a ``SexpExpr`` (when the provider responds - with ``text/sexp``). When *required* is True (default), raises + Returns an HTML string or a ``SxExpr`` (when the provider responds + with ``text/sx``). When *required* is True (default), raises ``FragmentError`` on network errors or non-200 responses. When *required* is False, returns ``""`` on failure. Automatically returns ``""`` when called inside a fragment request to prevent circular dependencies between apps. """ - from shared.sexp.parser import SexpExpr + from shared.sx.parser import SxExpr if _is_fragment_request(): return "" @@ -102,8 +102,8 @@ async def fetch_fragment( ) if resp.status_code == 200: ct = resp.headers.get("content-type", "") - if "text/sexp" in ct: - return SexpExpr(resp.text) + if "text/sx" in ct: + return SxExpr(resp.text) return resp.text msg = f"Fragment {app_name}/{fragment_type} returned {resp.status_code}" if required: diff --git a/shared/static/scripts/sexp.js b/shared/static/scripts/sx.js similarity index 89% rename from shared/static/scripts/sexp.js rename to shared/static/scripts/sx.js index d1d2e56..a8825f0 100644 --- a/shared/static/scripts/sexp.js +++ b/shared/static/scripts/sx.js @@ -1,12 +1,12 @@ /** - * sexp.js — S-expression parser, evaluator, and DOM renderer. + * sx.js — S-expression parser, evaluator, and DOM renderer. * - * Client-side counterpart to shared/sexp/ Python modules. + * Client-side counterpart to shared/sx/ Python modules. * Parses s-expression text, evaluates it, and renders to DOM nodes. * * Usage: - * Sexp.loadComponents('(defcomp ~card (&key title) (div :class "c" title))'); - * const node = Sexp.render('(~card :title "Hello")'); + * Sx.loadComponents('(defcomp ~card (&key title) (div :class "c" title))'); + * const node = Sx.render('(~card :title "Hello")'); * document.body.appendChild(node); */ ;(function (global) { @@ -21,9 +21,9 @@ function isNil(x) { return x === NIL || x === null || x === undefined; } function isTruthy(x) { return x !== false && !isNil(x) && x !== 0 && x !== ""; } - // Note: 0 and "" are falsy in sexp but we match Python semantics where + // Note: 0 and "" are falsy in sx but we match Python semantics where // only nil/false/None are falsy for control flow. Revisit if needed. - function isSexpTruthy(x) { return x !== false && !isNil(x); } + function isSxTruthy(x) { return x !== false && !isNil(x); } function Symbol(name) { this.name = name; } Symbol.prototype.toString = function () { return this.name; }; @@ -243,7 +243,7 @@ PRIMITIVES["pow"] = Math.pow; // Comparison - PRIMITIVES["="] = function (a, b) { return a == b; }; // loose, matches Python sexp + PRIMITIVES["="] = function (a, b) { return a == b; }; // loose, matches Python sx PRIMITIVES["!="] = function (a, b) { return a != b; }; PRIMITIVES["<"] = function (a, b) { return a < b; }; PRIMITIVES[">"] = function (a, b) { return a > b; }; @@ -251,7 +251,7 @@ PRIMITIVES[">="] = function (a, b) { return a >= b; }; // Logic - PRIMITIVES["not"] = function (x) { return !isSexpTruthy(x); }; + PRIMITIVES["not"] = function (x) { return !isSxTruthy(x); }; // String PRIMITIVES["str"] = function () { @@ -326,7 +326,7 @@ // Evaluator // ========================================================================= - function sexpEval(expr, env) { + function sxEval(expr, env) { // Literals if (typeof expr === "number" || typeof expr === "string" || typeof expr === "boolean") return expr; if (isNil(expr)) return NIL; @@ -348,7 +348,7 @@ // Dict literal if (expr && typeof expr === "object" && !Array.isArray(expr) && !expr._sym && !expr._kw && !expr._raw) { var d = {}; - for (var dk in expr) d[dk] = sexpEval(expr[dk], env); + for (var dk in expr) d[dk] = sxEval(expr[dk], env); return d; } @@ -360,7 +360,7 @@ // Non-callable head → data list if (!isSym(head) && !isLambda(head) && !Array.isArray(head)) { - return expr.map(function (x) { return sexpEval(x, env); }); + return expr.map(function (x) { return sxEval(x, env); }); } // Special forms @@ -372,9 +372,9 @@ } // Function call - var fn = sexpEval(head, env); + var fn = sxEval(head, env); var args = []; - for (var ai = 1; ai < expr.length; ai++) args.push(sexpEval(expr[ai], env)); + for (var ai = 1; ai < expr.length; ai++) args.push(sxEval(expr[ai], env)); if (typeof fn === "function") return fn.apply(null, args); if (isLambda(fn)) return callLambda(fn, args, env); @@ -388,7 +388,7 @@ } var local = merge({}, fn.closure, callerEnv); for (var i = 0; i < fn.params.length; i++) local[fn.params[i]] = args[i]; - return sexpEval(fn.body, local); + return sxEval(fn.body, local); } function callComponent(comp, rawArgs, env) { @@ -396,10 +396,10 @@ var i = 0; while (i < rawArgs.length) { if (isKw(rawArgs[i]) && i + 1 < rawArgs.length) { - kwargs[rawArgs[i].name] = sexpEval(rawArgs[i + 1], env); + kwargs[rawArgs[i].name] = sxEval(rawArgs[i + 1], env); i += 2; } else { - children.push(sexpEval(rawArgs[i], env)); + children.push(sxEval(rawArgs[i], env)); i++; } } @@ -409,7 +409,7 @@ local[p] = (p in kwargs) ? kwargs[p] : NIL; } if (comp.hasChildren) local["children"] = children; - return sexpEval(comp.body, local); + return sxEval(comp.body, local); } // --- Special forms ------------------------------------------------------- @@ -417,15 +417,15 @@ var SPECIAL_FORMS = {}; SPECIAL_FORMS["if"] = function (expr, env) { - var cond = sexpEval(expr[1], env); - if (isSexpTruthy(cond)) return sexpEval(expr[2], env); - return expr.length > 3 ? sexpEval(expr[3], env) : NIL; + var cond = sxEval(expr[1], env); + if (isSxTruthy(cond)) return sxEval(expr[2], env); + return expr.length > 3 ? sxEval(expr[3], env) : NIL; }; SPECIAL_FORMS["when"] = function (expr, env) { - if (!isSexpTruthy(sexpEval(expr[1], env))) return NIL; + if (!isSxTruthy(sxEval(expr[1], env))) return NIL; var result = NIL; - for (var i = 2; i < expr.length; i++) result = sexpEval(expr[i], env); + for (var i = 2; i < expr.length; i++) result = sxEval(expr[i], env); return result; }; @@ -437,28 +437,28 @@ for (var i = 0; i < clauses.length; i++) { var test = clauses[i][0]; if ((isSym(test) && (test.name === "else" || test.name === ":else")) || - (isKw(test) && test.name === "else")) return sexpEval(clauses[i][1], env); - if (isSexpTruthy(sexpEval(test, env))) return sexpEval(clauses[i][1], env); + (isKw(test) && test.name === "else")) return sxEval(clauses[i][1], env); + if (isSxTruthy(sxEval(test, env))) return sxEval(clauses[i][1], env); } } else { // Clojure-style for (var j = 0; j < clauses.length - 1; j += 2) { var t = clauses[j]; if ((isKw(t) && t.name === "else") || (isSym(t) && (t.name === ":else" || t.name === "else"))) - return sexpEval(clauses[j + 1], env); - if (isSexpTruthy(sexpEval(t, env))) return sexpEval(clauses[j + 1], env); + return sxEval(clauses[j + 1], env); + if (isSxTruthy(sxEval(t, env))) return sxEval(clauses[j + 1], env); } } return NIL; }; SPECIAL_FORMS["case"] = function (expr, env) { - var val = sexpEval(expr[1], env); + var val = sxEval(expr[1], env); for (var i = 2; i < expr.length - 1; i += 2) { var t = expr[i]; if ((isKw(t) && t.name === "else") || (isSym(t) && (t.name === ":else" || t.name === "else"))) - return sexpEval(expr[i + 1], env); - if (val == sexpEval(t, env)) return sexpEval(expr[i + 1], env); + return sxEval(expr[i + 1], env); + if (val == sxEval(t, env)) return sxEval(expr[i + 1], env); } return NIL; }; @@ -466,8 +466,8 @@ SPECIAL_FORMS["and"] = function (expr, env) { var result = true; for (var i = 1; i < expr.length; i++) { - result = sexpEval(expr[i], env); - if (!isSexpTruthy(result)) return result; + result = sxEval(expr[i], env); + if (!isSxTruthy(result)) return result; } return result; }; @@ -475,8 +475,8 @@ SPECIAL_FORMS["or"] = function (expr, env) { var result = false; for (var i = 1; i < expr.length; i++) { - result = sexpEval(expr[i], env); - if (isSexpTruthy(result)) return result; + result = sxEval(expr[i], env); + if (isSxTruthy(result)) return result; } return result; }; @@ -488,18 +488,18 @@ // Scheme-style for (var i = 0; i < bindings.length; i++) { var vname = isSym(bindings[i][0]) ? bindings[i][0].name : bindings[i][0]; - local[vname] = sexpEval(bindings[i][1], local); + local[vname] = sxEval(bindings[i][1], local); } } else { // Clojure-style for (var j = 0; j < bindings.length; j += 2) { var vn = isSym(bindings[j]) ? bindings[j].name : bindings[j]; - local[vn] = sexpEval(bindings[j + 1], local); + local[vn] = sxEval(bindings[j + 1], local); } } } var result = NIL; - for (var k = 2; k < expr.length; k++) result = sexpEval(expr[k], local); + for (var k = 2; k < expr.length; k++) result = sxEval(expr[k], local); return result; }; @@ -514,7 +514,7 @@ SPECIAL_FORMS["define"] = function (expr, env) { var name = expr[1].name; - var value = sexpEval(expr[2], env); + var value = sxEval(expr[2], env); if (isLambda(value) && !value.name) value.name = name; env[name] = value; return value; @@ -541,29 +541,29 @@ SPECIAL_FORMS["begin"] = SPECIAL_FORMS["do"] = function (expr, env) { var result = NIL; - for (var i = 1; i < expr.length; i++) result = sexpEval(expr[i], env); + for (var i = 1; i < expr.length; i++) result = sxEval(expr[i], env); return result; }; SPECIAL_FORMS["quote"] = function (expr) { return expr[1]; }; SPECIAL_FORMS["set!"] = function (expr, env) { - var v = sexpEval(expr[2], env); + var v = sxEval(expr[2], env); env[expr[1].name] = v; return v; }; SPECIAL_FORMS["->"] = function (expr, env) { - var result = sexpEval(expr[1], env); + var result = sxEval(expr[1], env); for (var i = 2; i < expr.length; i++) { var form = expr[i]; var fn, args; if (Array.isArray(form)) { - fn = sexpEval(form[0], env); + fn = sxEval(form[0], env); args = [result]; - for (var j = 1; j < form.length; j++) args.push(sexpEval(form[j], env)); + for (var j = 1; j < form.length; j++) args.push(sxEval(form[j], env)); } else { - fn = sexpEval(form, env); + fn = sxEval(form, env); args = [result]; } if (typeof fn === "function") result = fn.apply(null, args); @@ -578,48 +578,48 @@ var HO_FORMS = {}; HO_FORMS["map"] = function (expr, env) { - var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env); + var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env); return coll.map(function (item) { return isLambda(fn) ? callLambda(fn, [item], env) : fn(item); }); }; HO_FORMS["map-indexed"] = function (expr, env) { - var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env); + var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env); return coll.map(function (item, i) { return isLambda(fn) ? callLambda(fn, [i, item], env) : fn(i, item); }); }; HO_FORMS["filter"] = function (expr, env) { - var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env); + var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env); return coll.filter(function (item) { var r = isLambda(fn) ? callLambda(fn, [item], env) : fn(item); - return isSexpTruthy(r); + return isSxTruthy(r); }); }; HO_FORMS["reduce"] = function (expr, env) { - var fn = sexpEval(expr[1], env), acc = sexpEval(expr[2], env), coll = sexpEval(expr[3], env); + var fn = sxEval(expr[1], env), acc = sxEval(expr[2], env), coll = sxEval(expr[3], env); for (var i = 0; i < coll.length; i++) acc = isLambda(fn) ? callLambda(fn, [acc, coll[i]], env) : fn(acc, coll[i]); return acc; }; HO_FORMS["some"] = function (expr, env) { - var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env); + var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env); for (var i = 0; i < coll.length; i++) { var r = isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]); - if (isSexpTruthy(r)) return r; + if (isSxTruthy(r)) return r; } return NIL; }; HO_FORMS["every?"] = function (expr, env) { - var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env); + var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env); for (var i = 0; i < coll.length; i++) { - if (!isSexpTruthy(isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]))) return false; + if (!isSxTruthy(isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]))) return false; } return true; }; HO_FORMS["for-each"] = function (expr, env) { - var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env); + var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env); for (var i = 0; i < coll.length; i++) isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]); return NIL; }; @@ -686,7 +686,7 @@ if (typeof expr === "number") return document.createTextNode(String(expr)); // Symbol → evaluate then render - if (isSym(expr)) return renderDOM(sexpEval(expr, env), env); + if (isSym(expr)) return renderDOM(sxEval(expr, env), env); // Keyword → text if (isKw(expr)) return document.createTextNode(expr.name); @@ -707,13 +707,13 @@ var RENDER_FORMS = {}; RENDER_FORMS["if"] = function (expr, env) { - var cond = sexpEval(expr[1], env); - if (isSexpTruthy(cond)) return renderDOM(expr[2], env); + var cond = sxEval(expr[1], env); + if (isSxTruthy(cond)) return renderDOM(expr[2], env); return expr.length > 3 ? renderDOM(expr[3], env) : document.createDocumentFragment(); }; RENDER_FORMS["when"] = function (expr, env) { - if (!isSexpTruthy(sexpEval(expr[1], env))) return document.createDocumentFragment(); + if (!isSxTruthy(sxEval(expr[1], env))) return document.createDocumentFragment(); var frag = document.createDocumentFragment(); for (var i = 2; i < expr.length; i++) frag.appendChild(renderDOM(expr[i], env)); return frag; @@ -727,14 +727,14 @@ var test = clauses[i][0]; if ((isSym(test) && (test.name === "else" || test.name === ":else")) || (isKw(test) && test.name === "else")) return renderDOM(clauses[i][1], env); - if (isSexpTruthy(sexpEval(test, env))) return renderDOM(clauses[i][1], env); + if (isSxTruthy(sxEval(test, env))) return renderDOM(clauses[i][1], env); } } else { for (var j = 0; j < clauses.length - 1; j += 2) { var t = clauses[j]; if ((isKw(t) && t.name === "else") || (isSym(t) && (t.name === ":else" || t.name === "else"))) return renderDOM(clauses[j + 1], env); - if (isSexpTruthy(sexpEval(t, env))) return renderDOM(clauses[j + 1], env); + if (isSxTruthy(sxEval(t, env))) return renderDOM(clauses[j + 1], env); } } return document.createDocumentFragment(); @@ -745,11 +745,11 @@ if (Array.isArray(bindings)) { if (bindings.length && Array.isArray(bindings[0])) { for (var i = 0; i < bindings.length; i++) { - local[isSym(bindings[i][0]) ? bindings[i][0].name : bindings[i][0]] = sexpEval(bindings[i][1], local); + local[isSym(bindings[i][0]) ? bindings[i][0].name : bindings[i][0]] = sxEval(bindings[i][1], local); } } else { for (var j = 0; j < bindings.length; j += 2) { - local[isSym(bindings[j]) ? bindings[j].name : bindings[j]] = sexpEval(bindings[j + 1], local); + local[isSym(bindings[j]) ? bindings[j].name : bindings[j]] = sxEval(bindings[j + 1], local); } } } @@ -764,11 +764,11 @@ return frag; }; - RENDER_FORMS["define"] = function (expr, env) { sexpEval(expr, env); return document.createDocumentFragment(); }; - RENDER_FORMS["defcomp"] = function (expr, env) { sexpEval(expr, env); return document.createDocumentFragment(); }; + RENDER_FORMS["define"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); }; + RENDER_FORMS["defcomp"] = function (expr, env) { sxEval(expr, env); return document.createDocumentFragment(); }; RENDER_FORMS["map"] = function (expr, env) { - var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env); + var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env); var frag = document.createDocumentFragment(); for (var i = 0; i < coll.length; i++) { var val = isLambda(fn) ? renderLambdaDOM(fn, [coll[i]], env) : renderDOM(fn(coll[i]), env); @@ -778,7 +778,7 @@ }; RENDER_FORMS["map-indexed"] = function (expr, env) { - var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env); + var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env); var frag = document.createDocumentFragment(); for (var i = 0; i < coll.length; i++) { var val = isLambda(fn) ? renderLambdaDOM(fn, [i, coll[i]], env) : renderDOM(fn(i, coll[i]), env); @@ -788,12 +788,12 @@ }; RENDER_FORMS["filter"] = function (expr, env) { - var result = sexpEval(expr, env); + var result = sxEval(expr, env); return renderDOM(result, env); }; RENDER_FORMS["for-each"] = function (expr, env) { - var fn = sexpEval(expr[1], env), coll = sexpEval(expr[2], env); + var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env); var frag = document.createDocumentFragment(); for (var i = 0; i < coll.length; i++) { var val = isLambda(fn) ? renderLambdaDOM(fn, [coll[i]], env) : renderDOM(fn(coll[i]), env); @@ -819,7 +819,7 @@ var v = args[i + 1]; kwargs[args[i].name] = (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || isNil(v) || isKw(v)) - ? v : (isSym(v) ? sexpEval(v, env) : v); + ? v : (isSym(v) ? sxEval(v, env) : v); i += 2; } else { children.push(args[i]); @@ -850,7 +850,7 @@ if (name === "raw!") { var frag = document.createDocumentFragment(); for (var ri = 1; ri < expr.length; ri++) { - var val = sexpEval(expr[ri], env); + var val = sxEval(expr[ri], env); if (typeof val === "string") { var tpl = document.createElement("template"); tpl.innerHTML = val; @@ -883,7 +883,7 @@ var comp = env[name]; if (isComponent(comp)) return renderComponentDOM(comp, expr.slice(1), env); // Unknown component — render a visible warning, don't crash - console.warn("sexp.js: unknown component " + name); + console.warn("sx.js: unknown component " + name); var warn = document.createElement("div"); warn.setAttribute("style", "background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;" + @@ -893,11 +893,11 @@ } // Fallback: evaluate then render - return renderDOM(sexpEval(expr, env), env); + return renderDOM(sxEval(expr, env), env); } // Lambda/list head → evaluate - if (isLambda(head) || Array.isArray(head)) return renderDOM(sexpEval(expr, env), env); + if (isLambda(head) || Array.isArray(head)) return renderDOM(sxEval(expr, env), env); // Data list var dl = document.createDocumentFragment(); @@ -915,7 +915,7 @@ var arg = args[i]; if (isKw(arg) && i + 1 < args.length) { var attrName = arg.name; - var attrVal = sexpEval(args[i + 1], env); + var attrVal = sxEval(args[i + 1], env); i += 2; if (isNil(attrVal) || attrVal === false) continue; if (BOOLEAN_ATTRS[attrName]) { @@ -949,7 +949,7 @@ if (isRaw(expr)) return expr.html; if (typeof expr === "string") return escapeText(expr); if (typeof expr === "number") return escapeText(String(expr)); - if (isSym(expr)) return renderStr(sexpEval(expr, env), env); + if (isSym(expr)) return renderStr(sxEval(expr, env), env); if (isKw(expr)) return escapeText(expr.name); if (Array.isArray(expr)) { if (!expr.length) return ""; return renderStrList(expr, env); } if (expr && typeof expr === "object") return ""; @@ -968,7 +968,7 @@ if (name === "raw!") { var ps = []; for (var ri = 1; ri < expr.length; ri++) { - var v = sexpEval(expr[ri], env); + var v = sxEval(expr[ri], env); if (isRaw(v)) ps.push(v.html); else if (typeof v === "string") ps.push(v); else if (!isNil(v)) ps.push(String(v)); @@ -981,12 +981,12 @@ return fs.join(""); } if (name === "if") { - return isSexpTruthy(sexpEval(expr[1], env)) + return isSxTruthy(sxEval(expr[1], env)) ? renderStr(expr[2], env) : (expr.length > 3 ? renderStr(expr[3], env) : ""); } if (name === "when") { - if (!isSexpTruthy(sexpEval(expr[1], env))) return ""; + if (!isSxTruthy(sxEval(expr[1], env))) return ""; var ws = []; for (var wi = 2; wi < expr.length; wi++) ws.push(renderStr(expr[wi], env)); return ws.join(""); @@ -996,11 +996,11 @@ if (Array.isArray(bindings)) { if (bindings.length && Array.isArray(bindings[0])) { for (var li = 0; li < bindings.length; li++) { - local[isSym(bindings[li][0]) ? bindings[li][0].name : bindings[li][0]] = sexpEval(bindings[li][1], local); + local[isSym(bindings[li][0]) ? bindings[li][0].name : bindings[li][0]] = sxEval(bindings[li][1], local); } } else { for (var lj = 0; lj < bindings.length; lj += 2) { - local[isSym(bindings[lj]) ? bindings[lj].name : bindings[lj]] = sexpEval(bindings[lj + 1], local); + local[isSym(bindings[lj]) ? bindings[lj].name : bindings[lj]] = sxEval(bindings[lj + 1], local); } } } @@ -1013,11 +1013,11 @@ for (var bi = 1; bi < expr.length; bi++) bs.push(renderStr(expr[bi], env)); return bs.join(""); } - if (name === "define" || name === "defcomp") { sexpEval(expr, env); return ""; } + if (name === "define" || name === "defcomp") { sxEval(expr, env); return ""; } // Higher-order forms — render-aware (lambda bodies may contain HTML/components) if (name === "map") { - var mapFn = sexpEval(expr[1], env), mapColl = sexpEval(expr[2], env); + var mapFn = sxEval(expr[1], env), mapColl = sxEval(expr[2], env); if (!Array.isArray(mapColl)) return ""; var mapParts = []; for (var mi = 0; mi < mapColl.length; mi++) { @@ -1027,7 +1027,7 @@ return mapParts.join(""); } if (name === "map-indexed") { - var mixFn = sexpEval(expr[1], env), mixColl = sexpEval(expr[2], env); + var mixFn = sxEval(expr[1], env), mixColl = sxEval(expr[2], env); if (!Array.isArray(mixColl)) return ""; var mixParts = []; for (var mxi = 0; mxi < mixColl.length; mxi++) { @@ -1037,12 +1037,12 @@ return mixParts.join(""); } if (name === "filter") { - var filtFn = sexpEval(expr[1], env), filtColl = sexpEval(expr[2], env); + var filtFn = sxEval(expr[1], env), filtColl = sxEval(expr[2], env); if (!Array.isArray(filtColl)) return ""; var filtParts = []; for (var fli = 0; fli < filtColl.length; fli++) { var keep = isLambda(filtFn) ? callLambda(filtFn, [filtColl[fli]], env) : filtFn(filtColl[fli]); - if (isSexpTruthy(keep)) filtParts.push(renderStr(filtColl[fli], env)); + if (isSxTruthy(keep)) filtParts.push(renderStr(filtColl[fli], env)); } return filtParts.join(""); } @@ -1053,13 +1053,13 @@ var comp = env[name]; if (isComponent(comp)) return renderStrComponent(comp, expr.slice(1), env); // Unknown component — return visible warning - console.warn("sexp.js: unknown component " + name); + console.warn("sx.js: unknown component " + name); return '
' + 'Unknown component: ' + escapeText(name) + '
'; } - return renderStr(sexpEval(expr, env), env); + return renderStr(sxEval(expr, env), env); } function renderStrElement(tag, args, env) { @@ -1067,7 +1067,7 @@ var i = 0; while (i < args.length) { if (isKw(args[i]) && i + 1 < args.length) { - var aname = args[i].name, aval = sexpEval(args[i + 1], env); + var aname = args[i].name, aval = sxEval(args[i + 1], env); i += 2; if (isNil(aval) || aval === false) continue; if (BOOLEAN_ATTRS[aname]) { if (aval) attrs.push(" " + aname); } @@ -1099,7 +1099,7 @@ var v = args[i + 1]; kwargs[args[i].name] = (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || isNil(v) || isKw(v)) - ? v : (isSym(v) ? sexpEval(v, env) : v); + ? v : (isSym(v) ? sxEval(v, env) : v); i += 2; } else { children.push(args[i]); i++; } } @@ -1134,7 +1134,7 @@ return s; } - /** Convert snake_case kwargs to kebab-case for sexp conventions. */ + /** Convert snake_case kwargs to kebab-case for sx conventions. */ function toKebab(s) { return s.replace(/_/g, "-"); } // ========================================================================= @@ -1187,7 +1187,7 @@ } } - var Sexp = { + var Sx = { // Types NIL: NIL, Symbol: Symbol, @@ -1198,7 +1198,7 @@ parseAll: parseAll, // Evaluator - eval: function (expr, env) { return sexpEval(expr, env || _componentEnv); }, + eval: function (expr, env) { return sxEval(expr, env || _componentEnv); }, // DOM Renderer render: function (exprOrText, extraEnv) { @@ -1216,7 +1216,7 @@ /** * Render a named component with keyword args (Python-style API). - * Sexp.renderComponent("card", {title: "Hi"}) + * Sx.renderComponent("card", {title: "Hi"}) */ renderComponent: function (name, kwargs, extraEnv) { var fullName = name.charAt(0) === "~" ? name : "~" + name; @@ -1237,52 +1237,52 @@ // Component management loadComponents: function (text) { var exprs = parseAll(text); - for (var i = 0; i < exprs.length; i++) sexpEval(exprs[i], _componentEnv); + for (var i = 0; i < exprs.length; i++) sxEval(exprs[i], _componentEnv); }, getEnv: function () { return _componentEnv; }, // Utility - isTruthy: isSexpTruthy, + isTruthy: isSxTruthy, isNil: isNil, /** - * Mount a sexp expression into a DOM element, replacing its contents. - * Sexp.mount(el, '(~card :title "Hi")') - * Sexp.mount("#target", '(~card :title "Hi")') - * Sexp.mount(el, '(~card :title name)', {name: "Jo"}) + * Mount a sx expression into a DOM element, replacing its contents. + * Sx.mount(el, '(~card :title "Hi")') + * Sx.mount("#target", '(~card :title "Hi")') + * Sx.mount(el, '(~card :title name)', {name: "Jo"}) */ mount: function (target, exprOrText, extraEnv) { var el = typeof target === "string" ? document.querySelector(target) : target; if (!el) return; - var node = Sexp.render(exprOrText, extraEnv); + var node = Sx.render(exprOrText, extraEnv); el.textContent = ""; el.appendChild(node); // Auto-hoist head elements (meta, title, link, script[ld+json]) to _hoistHeadElements(el); // Process sx- attributes and hydrate the newly mounted content if (typeof SxEngine !== "undefined") SxEngine.process(el); - Sexp.hydrate(el); + Sx.hydrate(el); }, /** - * Process all \n{body}') - resp = Response(body, status=status, content_type="text/sexp") + resp = Response(body, status=status, content_type="text/sx") if headers: for k, v in headers.items(): resp.headers[k] = v @@ -357,10 +357,10 @@ def sexp_response(source_or_component: str, status: int = 200, # --------------------------------------------------------------------------- -# Sexp wire-format full page shell +# Sx wire-format full page shell # --------------------------------------------------------------------------- -_SEXP_PAGE_TEMPLATE = """\ +_SX_PAGE_TEMPLATE = """\ @@ -401,19 +401,19 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details. - - - + + + """ -def sexp_page(ctx: dict, page_sexp: str, *, +def sx_page(ctx: dict, page_sx: str, *, meta_html: str = "") -> str: - """Return a minimal HTML shell that boots the page from sexp source. + """Return a minimal HTML shell that boots the page from sx source. - The browser loads component definitions and page sexp, then sexp.js + The browser loads component definitions and page sx, then sx.js renders everything client-side. """ from .jinja_bridge import client_components_tag @@ -421,7 +421,7 @@ def sexp_page(ctx: dict, page_sexp: str, *, # Extract just the inner source from the + # Strip start = components_tag.find(">") + 1 end = components_tag.rfind("") if start > 0 and end > start: @@ -431,13 +431,13 @@ def sexp_page(ctx: dict, page_sexp: str, *, title = ctx.get("base_title", "Rose Ash") csrf = _get_csrf_token() - return _SEXP_PAGE_TEMPLATE.format( + return _SX_PAGE_TEMPLATE.format( title=_html_escape(title), asset_url=asset_url, meta_html=meta_html, csrf=_html_escape(csrf), component_defs=component_defs, - page_sexp=page_sexp, + page_sx=page_sx, ) diff --git a/shared/sexp/html.py b/shared/sx/html.py similarity index 99% rename from shared/sexp/html.py rename to shared/sx/html.py index 818d26a..b978793 100644 --- a/shared/sexp/html.py +++ b/shared/sx/html.py @@ -7,8 +7,8 @@ evaluator and then rendered recursively. Usage:: - from shared.sexp import parse, make_env - from shared.sexp.html import render + from shared.sx import parse, make_env + from shared.sx.html import render expr = parse('(div :class "card" (h1 "Hello") (p "World"))') html = render(expr) diff --git a/shared/sexp/jinja_bridge.py b/shared/sx/jinja_bridge.py similarity index 70% rename from shared/sexp/jinja_bridge.py rename to shared/sx/jinja_bridge.py index 27018ff..ed998f7 100644 --- a/shared/sexp/jinja_bridge.py +++ b/shared/sx/jinja_bridge.py @@ -6,7 +6,7 @@ can coexist during incremental migration: **Jinja → s-expression** (use s-expression components inside Jinja templates):: - {{ sexp('(~link-card :slug "apple" :title "Apple")') | safe }} + {{ sx('(~link-card :slug "apple" :title "Apple")') | safe }} **S-expression → Jinja** (embed Jinja output inside s-expressions):: @@ -14,8 +14,8 @@ can coexist during incremental migration: Setup:: - from shared.sexp.jinja_bridge import setup_sexp_bridge - setup_sexp_bridge(app) # call after setup_jinja(app) + from shared.sx.jinja_bridge import setup_sx_bridge + setup_sx_bridge(app) # call after setup_jinja(app) """ from __future__ import annotations @@ -43,42 +43,39 @@ def get_component_env() -> dict[str, Any]: return _COMPONENT_ENV -def load_sexp_dir(directory: str) -> None: - """Load all .sexp and .sexpr files from a directory and register components.""" +def load_sx_dir(directory: str) -> None: + """Load all .sx files from a directory and register components.""" for filepath in sorted( - glob.glob(os.path.join(directory, "*.sexp")) - + glob.glob(os.path.join(directory, "*.sexpr")) + glob.glob(os.path.join(directory, "*.sx")) ): with open(filepath, encoding="utf-8") as f: register_components(f.read()) # --------------------------------------------------------------------------- -# Dev-mode auto-reload of sexp templates +# Dev-mode auto-reload of sx templates # --------------------------------------------------------------------------- _watched_dirs: list[str] = [] _file_mtimes: dict[str, float] = {} -def watch_sexp_dir(directory: str) -> None: +def watch_sx_dir(directory: str) -> None: """Register a directory for dev-mode file watching.""" _watched_dirs.append(directory) # Seed mtimes for fp in sorted( - glob.glob(os.path.join(directory, "*.sexp")) - + glob.glob(os.path.join(directory, "*.sexpr")) + glob.glob(os.path.join(directory, "*.sx")) ): _file_mtimes[fp] = os.path.getmtime(fp) def reload_if_changed() -> None: - """Re-read sexp files if any have changed on disk. Called per-request in dev.""" + """Re-read sx files if any have changed on disk. Called per-request in dev.""" changed = False for directory in _watched_dirs: for fp in sorted( - glob.glob(os.path.join(directory, "*.sexp")) - + glob.glob(os.path.join(directory, "*.sexpr")) + glob.glob(os.path.join(directory, "*.sx")) ): mtime = os.path.getmtime(fp) if fp not in _file_mtimes or _file_mtimes[fp] != mtime: @@ -87,18 +84,18 @@ def reload_if_changed() -> None: if changed: _COMPONENT_ENV.clear() for directory in _watched_dirs: - load_sexp_dir(directory) + load_sx_dir(directory) def load_service_components(service_dir: str) -> None: - """Load service-specific s-expression components from {service_dir}/sexp/.""" - sexp_dir = os.path.join(service_dir, "sexp") - if os.path.isdir(sexp_dir): - load_sexp_dir(sexp_dir) - watch_sexp_dir(sexp_dir) + """Load service-specific s-expression components from {service_dir}/sx/.""" + sx_dir = os.path.join(service_dir, "sx") + if os.path.isdir(sx_dir): + load_sx_dir(sx_dir) + watch_sx_dir(sx_dir) -def register_components(sexp_source: str) -> None: +def register_components(sx_source: str) -> None: """Parse and evaluate s-expression component definitions into the shared environment. @@ -117,26 +114,26 @@ def register_components(sexp_source: str) -> None: from .evaluator import _eval from .parser import parse_all - exprs = parse_all(sexp_source) + exprs = parse_all(sx_source) for expr in exprs: _eval(expr, _COMPONENT_ENV) # --------------------------------------------------------------------------- -# sexp() — render s-expression from Jinja template +# sx() — render s-expression from Jinja template # --------------------------------------------------------------------------- -def sexp(source: str, **kwargs: Any) -> str: +def sx(source: str, **kwargs: Any) -> str: """Render an s-expression string to HTML. Keyword arguments are merged into the evaluation environment, so Jinja context variables can be passed through:: - {{ sexp('(~link-card :title title :slug slug)', + {{ sx('(~link-card :title title :slug slug)', title=post.title, slug=post.slug) | safe }} This is a synchronous function — suitable for Jinja globals. - For async resolution (with I/O primitives), use ``sexp_async()``. + For async resolution (with I/O primitives), use ``sx_async()``. """ env = dict(_COMPONENT_ENV) env.update(kwargs) @@ -147,8 +144,8 @@ def sexp(source: str, **kwargs: Any) -> str: def render(component_name: str, **kwargs: Any) -> str: """Call a registered component by name with Python kwargs. - Automatically converts Python snake_case to sexp kebab-case. - No sexp strings needed — just a function call. + Automatically converts Python snake_case to sx kebab-case. + No sx strings needed — just a function call. """ name = component_name if component_name.startswith("~") else f"~{component_name}" comp = _COMPONENT_ENV.get(name) @@ -166,13 +163,13 @@ def render(component_name: str, **kwargs: Any) -> str: return _render_component(comp, args, env) -async def sexp_async(source: str, **kwargs: Any) -> str: - """Async version of ``sexp()`` — resolves I/O primitives (frag, query) +async def sx_async(source: str, **kwargs: Any) -> str: + """Async version of ``sx()`` — resolves I/O primitives (frag, query) before rendering. Use when the s-expression contains I/O nodes:: - {{ sexp_async('(frag "blog" "card" :slug "apple")') | safe }} + {{ sx_async('(frag "blog" "card" :slug "apple")') | safe }} """ from .resolver import resolve, RequestContext @@ -202,10 +199,10 @@ def _get_request_context(): # --------------------------------------------------------------------------- def client_components_tag(*names: str) -> str: - """Emit a ' + return f'' -def setup_sexp_bridge(app: Any) -> None: +def setup_sx_bridge(app: Any) -> None: """Register s-expression helpers with a Quart app's Jinja environment. Call this in your app factory after ``setup_jinja(app)``:: - from shared.sexp.jinja_bridge import setup_sexp_bridge - setup_sexp_bridge(app) + from shared.sx.jinja_bridge import setup_sx_bridge + setup_sx_bridge(app) This registers: - - ``sexp(source, **kwargs)`` — sync render (components, pure HTML) - - ``sexp_async(source, **kwargs)`` — async render (with I/O resolution) + - ``sx(source, **kwargs)`` — sync render (components, pure HTML) + - ``sx_async(source, **kwargs)`` — async render (with I/O resolution) """ - app.jinja_env.globals["sexp"] = sexp + app.jinja_env.globals["sx"] = sx app.jinja_env.globals["render"] = render - app.jinja_env.globals["sexp_async"] = sexp_async + app.jinja_env.globals["sx_async"] = sx_async diff --git a/shared/sexp/page.py b/shared/sx/page.py similarity index 84% rename from shared/sexp/page.py rename to shared/sx/page.py index fadbafb..6ca1c27 100644 --- a/shared/sexp/page.py +++ b/shared/sx/page.py @@ -5,13 +5,13 @@ Provides ``render_page()`` for rendering a complete HTML page from an s-expression, bypassing Jinja entirely. Used by error handlers and (eventually) by route handlers for fully-migrated pages. -``render_sexp_response()`` is the main entry point for GET route handlers: +``render_sx_response()`` is the main entry point for GET route handlers: it calls the app's context processor, merges in route-specific kwargs, renders the s-expression to HTML, and returns a Quart ``Response``. Usage:: - from shared.sexp.page import render_page, render_sexp_response + from shared.sx.page import render_page, render_sx_response # Error pages (no context needed) html = render_page( @@ -21,14 +21,14 @@ Usage:: ) # GET route handlers (auto-injects app context) - resp = await render_sexp_response('(~orders-page :orders orders)', orders=orders) + resp = await render_sx_response('(~orders-page :orders orders)', orders=orders) """ from __future__ import annotations from typing import Any -from .jinja_bridge import sexp +from .jinja_bridge import sx SEARCH_HEADERS_MOBILE = '{"X-Origin":"search-mobile","X-Search":"true"}' SEARCH_HEADERS_DESKTOP = '{"X-Origin":"search-desktop","X-Search":"true"}' @@ -37,10 +37,10 @@ SEARCH_HEADERS_DESKTOP = '{"X-Origin":"search-desktop","X-Search":"true"}' def render_page(source: str, **kwargs: Any) -> str: """Render a full HTML page from an s-expression string. - This is a thin wrapper around ``sexp()`` — it exists to make the + This is a thin wrapper around ``sx()`` — it exists to make the intent explicit in call sites (rendering a whole page, not a fragment). """ - return sexp(source, **kwargs) + return sx(source, **kwargs) async def get_template_context(**kwargs: Any) -> dict[str, Any]: @@ -75,7 +75,7 @@ async def get_template_context(**kwargs: Any) -> dict[str, Any]: if key not in ctx: ctx[key] = val - # Expose request-scoped values that sexp components need + # Expose request-scoped values that sx components need from quart import g if "rights" not in ctx: ctx["rights"] = getattr(g, "rights", {}) @@ -84,7 +84,7 @@ async def get_template_context(**kwargs: Any) -> dict[str, Any]: return ctx -async def render_sexp_response(source: str, **kwargs: Any) -> str: +async def render_sx_response(source: str, **kwargs: Any) -> str: """Render an s-expression with the full app template context. Calls the app's registered context processors (which provide @@ -94,4 +94,4 @@ async def render_sexp_response(source: str, **kwargs: Any) -> str: Returns the rendered HTML string (caller wraps in Response as needed). """ ctx = await get_template_context(**kwargs) - return sexp(source, **ctx) + return sx(source, **ctx) diff --git a/shared/sexp/parser.py b/shared/sx/parser.py similarity index 93% rename from shared/sexp/parser.py rename to shared/sx/parser.py index a2964cf..42f3f1f 100644 --- a/shared/sexp/parser.py +++ b/shared/sx/parser.py @@ -22,16 +22,16 @@ from .types import Keyword, Symbol, NIL # --------------------------------------------------------------------------- -# SexpExpr — pre-built sexp source marker +# SxExpr — pre-built sx source marker # --------------------------------------------------------------------------- -class SexpExpr: - """Pre-built sexp source that serialize() outputs unquoted. +class SxExpr: + """Pre-built sx source that serialize() outputs unquoted. - Use this to nest sexp call strings inside other sexp_call() invocations + Use this to nest sx call strings inside other sx_call() invocations without them being quoted as strings:: - sexp_call("parent", child=SexpExpr(sexp_call("child", x=1))) + sx_call("parent", child=SxExpr(sx_call("child", x=1))) # => (~parent :child (~child :x 1)) """ __slots__ = ("source",) @@ -40,16 +40,16 @@ class SexpExpr: self.source = source def __repr__(self) -> str: - return f"SexpExpr({self.source!r})" + return f"SxExpr({self.source!r})" def __str__(self) -> str: return self.source - def __add__(self, other: object) -> "SexpExpr": - return SexpExpr(self.source + str(other)) + def __add__(self, other: object) -> "SxExpr": + return SxExpr(self.source + str(other)) - def __radd__(self, other: object) -> "SexpExpr": - return SexpExpr(str(other) + self.source) + def __radd__(self, other: object) -> "SxExpr": + return SxExpr(str(other) + self.source) # --------------------------------------------------------------------------- @@ -261,7 +261,7 @@ def _parse_map(tok: Tokenizer) -> dict[str, Any]: def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str: """Serialize a value back to s-expression text.""" - if isinstance(expr, SexpExpr): + if isinstance(expr, SxExpr): return expr.source if isinstance(expr, list): @@ -303,11 +303,11 @@ def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str: items.append(serialize(v, indent, pretty)) return "{" + " ".join(items) + "}" - # Catch callables (Python functions leaked into sexp data) + # Catch callables (Python functions leaked into sx data) if callable(expr): import logging - logging.getLogger("sexp").error( - "serialize: callable leaked into sexp data: %r", expr) + logging.getLogger("sx").error( + "serialize: callable leaked into sx data: %r", expr) return "nil" # Fallback for Lambda/Component — show repr diff --git a/shared/sexp/primitives.py b/shared/sx/primitives.py similarity index 100% rename from shared/sexp/primitives.py rename to shared/sx/primitives.py diff --git a/shared/sexp/primitives_io.py b/shared/sx/primitives_io.py similarity index 100% rename from shared/sexp/primitives_io.py rename to shared/sx/primitives_io.py diff --git a/shared/sexp/relations.py b/shared/sx/relations.py similarity index 95% rename from shared/sexp/relations.py rename to shared/sx/relations.py index cbd2bd2..f96c76a 100644 --- a/shared/sexp/relations.py +++ b/shared/sx/relations.py @@ -8,7 +8,7 @@ via ``load_relation_registry()``. from __future__ import annotations -from shared.sexp.types import RelationDef +from shared.sx.types import RelationDef # --------------------------------------------------------------------------- @@ -94,8 +94,8 @@ _BUILTIN_RELATIONS = ''' def load_relation_registry() -> None: """Parse built-in defrelation s-expressions and populate the registry.""" - from shared.sexp.evaluator import evaluate - from shared.sexp.parser import parse + from shared.sx.evaluator import evaluate + from shared.sx.parser import parse tree = parse(_BUILTIN_RELATIONS) evaluate(tree) diff --git a/shared/sexp/resolver.py b/shared/sx/resolver.py similarity index 98% rename from shared/sexp/resolver.py rename to shared/sx/resolver.py index cedef78..f2e470c 100644 --- a/shared/sexp/resolver.py +++ b/shared/sx/resolver.py @@ -13,8 +13,8 @@ This is the DAG execution engine applied to page rendering. The strategy: Usage:: - from shared.sexp import parse - from shared.sexp.resolver import resolve, RequestContext + from shared.sx import parse + from shared.sx.resolver import resolve, RequestContext expr = parse(''' (div :class "page" diff --git a/shared/sexp/templates/cards.sexp b/shared/sx/templates/cards.sx similarity index 100% rename from shared/sexp/templates/cards.sexp rename to shared/sx/templates/cards.sx diff --git a/shared/sexp/templates/controls.sexp b/shared/sx/templates/controls.sx similarity index 100% rename from shared/sexp/templates/controls.sexp rename to shared/sx/templates/controls.sx diff --git a/shared/sexp/templates/fragments.sexp b/shared/sx/templates/fragments.sx similarity index 100% rename from shared/sexp/templates/fragments.sexp rename to shared/sx/templates/fragments.sx diff --git a/shared/sexp/templates/layout.sexp b/shared/sx/templates/layout.sx similarity index 99% rename from shared/sexp/templates/layout.sexp rename to shared/sx/templates/layout.sx index b9e5beb..0b09c1c 100644 --- a/shared/sexp/templates/layout.sexp +++ b/shared/sx/templates/layout.sx @@ -23,7 +23,7 @@ (when content content) (div :class "pb-8"))))))) -(defcomp ~oob-sexp (&key oobs filter aside menu content) +(defcomp ~oob-sx (&key oobs filter aside menu content) (<> (when oobs oobs) (div :id "filter" :sx-swap-oob "outerHTML" diff --git a/shared/sexp/templates/misc.sexp b/shared/sx/templates/misc.sx similarity index 100% rename from shared/sexp/templates/misc.sexp rename to shared/sx/templates/misc.sx diff --git a/shared/sexp/templates/navigation.sexp b/shared/sx/templates/navigation.sx similarity index 100% rename from shared/sexp/templates/navigation.sexp rename to shared/sx/templates/navigation.sx diff --git a/shared/sexp/templates/pages.sexp b/shared/sx/templates/pages.sx similarity index 100% rename from shared/sexp/templates/pages.sexp rename to shared/sx/templates/pages.sx diff --git a/shared/sexp/templates/relations.sexp b/shared/sx/templates/relations.sx similarity index 100% rename from shared/sexp/templates/relations.sexp rename to shared/sx/templates/relations.sx diff --git a/shared/sexp/tests/__init__.py b/shared/sx/tests/__init__.py similarity index 100% rename from shared/sexp/tests/__init__.py rename to shared/sx/tests/__init__.py diff --git a/shared/sexp/tests/test_components.py b/shared/sx/tests/test_components.py similarity index 93% rename from shared/sexp/tests/test_components.py rename to shared/sx/tests/test_components.py index c58fa8f..1d6237f 100644 --- a/shared/sexp/tests/test_components.py +++ b/shared/sx/tests/test_components.py @@ -2,8 +2,8 @@ import pytest -from shared.sexp.jinja_bridge import sexp, _COMPONENT_ENV -from shared.sexp.components import load_shared_components +from shared.sx.jinja_bridge import sx, _COMPONENT_ENV +from shared.sx.components import load_shared_components @pytest.fixture(autouse=True) @@ -19,7 +19,7 @@ def _load_components(): class TestCartMini: def test_empty_cart_shows_logo(self): - html = sexp( + html = sx( '(~cart-mini :cart-count cart-count :blog-url blog-url :cart-url cart-url)', **{"cart-count": 0, "blog-url": "https://blog.example.com/", "cart-url": "https://cart.example.com/"}, ) @@ -29,7 +29,7 @@ class TestCartMini: assert "fa-shopping-cart" not in html def test_nonempty_cart_shows_badge(self): - html = sexp( + html = sx( '(~cart-mini :cart-count cart-count :blog-url blog-url :cart-url cart-url)', **{"cart-count": 3, "blog-url": "https://blog.example.com/", "cart-url": "https://cart.example.com/"}, ) @@ -40,13 +40,13 @@ class TestCartMini: assert "cart.example.com/" in html def test_oob_attribute(self): - html = sexp( + html = sx( '(~cart-mini :cart-count 0 :blog-url "" :cart-url "" :oob "true")', ) assert 'sx-swap-oob="true"' in html def test_no_oob_when_nil(self): - html = sexp( + html = sx( '(~cart-mini :cart-count 0 :blog-url "" :cart-url "")', ) assert "sx-swap-oob" not in html @@ -58,7 +58,7 @@ class TestCartMini: class TestAuthMenu: def test_logged_in(self): - html = sexp( + html = sx( '(~auth-menu :user-email user-email :account-url account-url)', **{"user-email": "alice@example.com", "account-url": "https://account.example.com/"}, ) @@ -69,7 +69,7 @@ class TestAuthMenu: assert "sign in or register" not in html def test_logged_out(self): - html = sexp( + html = sx( '(~auth-menu :account-url account-url)', **{"account-url": "https://account.example.com/"}, ) @@ -77,7 +77,7 @@ class TestAuthMenu: assert "sign in or register" in html def test_desktop_has_data_close_details(self): - html = sexp( + html = sx( '(~auth-menu :user-email "x@y.com" :account-url "http://a")', ) assert "data-close-details" in html @@ -85,7 +85,7 @@ class TestAuthMenu: def test_two_spans_always_present(self): """Both desktop and mobile spans are always rendered.""" for email in ["user@test.com", None]: - html = sexp( + html = sx( '(~auth-menu :user-email user-email :account-url account-url)', **{"user-email": email, "account-url": "http://a"}, ) @@ -99,7 +99,7 @@ class TestAuthMenu: class TestAccountNavItem: def test_renders_link(self): - html = sexp( + html = sx( '(~account-nav-item :href "/orders/" :label "orders")', ) assert 'href="/orders/"' in html @@ -108,7 +108,7 @@ class TestAccountNavItem: assert "sx-disable" in html def test_custom_label(self): - html = sexp( + html = sx( '(~account-nav-item :href "/cart/orders/" :label "my orders")', ) assert ">my orders<" in html @@ -120,7 +120,7 @@ class TestAccountNavItem: class TestCalendarEntryNav: def test_renders_entry(self): - html = sexp( + html = sx( '(~calendar-entry-nav :href "/events/entry/1/" :name "Workshop" :date-str "Jan 15, 2026 at 14:00" :nav-class "btn")', **{"date-str": "Jan 15, 2026 at 14:00", "nav-class": "btn"}, ) @@ -135,7 +135,7 @@ class TestCalendarEntryNav: class TestCalendarLinkNav: def test_renders_calendar_link(self): - html = sexp( + html = sx( '(~calendar-link-nav :href "/events/cal/" :name "Art Events" :nav-class "btn")', **{"nav-class": "btn"}, ) @@ -150,7 +150,7 @@ class TestCalendarLinkNav: class TestMarketLinkNav: def test_renders_market_link(self): - html = sexp( + html = sx( '(~market-link-nav :href "/market/farm/" :name "Farm Shop" :nav-class "btn")', **{"nav-class": "btn"}, ) @@ -165,7 +165,7 @@ class TestMarketLinkNav: class TestPostCard: def test_basic_card(self): - html = sexp( + html = sx( '(~post-card :title "Hello World" :slug "hello" :href "/hello/"' ' :feature-image "/img/hello.jpg" :excerpt "A test post"' ' :status "published" :published-at "15 Jan 2026"' @@ -183,7 +183,7 @@ class TestPostCard: assert "A test post" in html def test_draft_status(self): - html = sexp( + html = sx( '(~post-card :title "Draft" :slug "draft" :href "/draft/"' ' :status "draft" :updated-at "15 Jan 2026"' ' :hx-select "#main-panel")', @@ -194,7 +194,7 @@ class TestPostCard: assert "Updated:" in html def test_draft_with_publish_requested(self): - html = sexp( + html = sx( '(~post-card :title "Pending" :slug "pending" :href "/pending/"' ' :status "draft" :publish-requested true' ' :hx-select "#main-panel")', @@ -204,7 +204,7 @@ class TestPostCard: assert "bg-blue-100" in html def test_no_image(self): - html = sexp( + html = sx( '(~post-card :title "No Img" :slug "no-img" :href "/no-img/"' ' :status "published" :hx-select "#main-panel")', **{"hx-select": "#main-panel"}, @@ -212,8 +212,8 @@ class TestPostCard: assert "Hello" + assert sx('(div "Hello")') == "
Hello
" def test_with_kwargs(self): - html = sexp('(p name)', name="Alice") + html = sx('(p name)', name="Alice") assert html == "

Alice

" def test_multiple_kwargs(self): - html = sexp('(a :href url title)', url="/about", title="About") + html = sx('(a :href url title)', url="/about", title="About") assert html == 'About' def test_escaping(self): - html = sexp('(p text)', text="") + html = sx('(p text)', text="") assert "<script>" in html assert "') assert "defcomp ~test-cct" in tag finally: @@ -150,13 +150,13 @@ class TestClientComponentsTag: def test_roundtrip_through_js(self): """Component emitted by client_components_tag renders identically in JS.""" - from shared.sexp.jinja_bridge import client_components_tag, register_components, _COMPONENT_ENV + from shared.sx.jinja_bridge import client_components_tag, register_components, _COMPONENT_ENV register_components('(defcomp ~test-rt (&key title) (div :class "rt" title))') try: tag = client_components_tag("test-rt") - # Extract the sexp source from the script tag - sexp_source = tag.replace('', '') - js_html = _js_render('(~test-rt :title "hello")', sexp_source) + # Extract the sx source from the script tag + sx_source = tag.replace('', '') + js_html = _js_render('(~test-rt :title "hello")', sx_source) py_html = py_render(parse('(~test-rt :title "hello")'), _COMPONENT_ENV) assert js_html == py_html finally: @@ -179,11 +179,11 @@ class TestPythonParity: '(table (tr (td "cell")))', ] - @pytest.mark.parametrize("sexp_text", CASES) - def test_matches_python(self, sexp_text): - py_html = py_render(parse(sexp_text)) - js_html = _js_render(sexp_text) - assert js_html == py_html, f"Mismatch for {sexp_text!r}:\n PY: {py_html!r}\n JS: {js_html!r}" + @pytest.mark.parametrize("sx_text", CASES) + def test_matches_python(self, sx_text): + py_html = py_render(parse(sx_text)) + js_html = _js_render(sx_text) + assert js_html == py_html, f"Mismatch for {sx_text!r}:\n PY: {py_html!r}\n JS: {js_html!r}" COMP_CASES = [ ( diff --git a/shared/sexp/types.py b/shared/sx/types.py similarity index 100% rename from shared/sexp/types.py rename to shared/sx/types.py diff --git a/shared/tests/test_config.py b/shared/tests/test_config.py index 71bf146..594d19a 100644 --- a/shared/tests/test_config.py +++ b/shared/tests/test_config.py @@ -74,12 +74,12 @@ class TestFreeze: "blog": "https://blog.rose-ash.com", "market": "https://market.rose-ash.com", }, - "features": ["sexp", "federation"], + "features": ["sx", "federation"], "limits": {"max_upload": 10485760}, } frozen = _freeze(raw) assert frozen["app_urls"]["blog"] == "https://blog.rose-ash.com" - assert frozen["features"] == ("sexp", "federation") + assert frozen["features"] == ("sx", "federation") with pytest.raises(TypeError): frozen["app_urls"]["blog"] = "changed" diff --git a/shared/tests/test_jinja_bridge_render.py b/shared/tests/test_jinja_bridge_render.py index c51fdec..124e7c6 100644 --- a/shared/tests/test_jinja_bridge_render.py +++ b/shared/tests/test_jinja_bridge_render.py @@ -1,8 +1,8 @@ """Tests for the render() function and component loading in jinja_bridge. These test functionality added in recent commits (render() API, -load_sexp_dir, snake→kebab conversion) that isn't covered by the existing -shared/sexp/tests/test_jinja_bridge.py. +load_sx_dir, snake→kebab conversion) that isn't covered by the existing +shared/sx/tests/test_jinja_bridge.py. """ from __future__ import annotations @@ -11,10 +11,10 @@ import tempfile import pytest -from shared.sexp.jinja_bridge import ( +from shared.sx.jinja_bridge import ( render, register_components, - load_sexp_dir, + load_sx_dir, _COMPONENT_ENV, ) @@ -43,7 +43,7 @@ class TestRender: assert render("pill", text="Hi") == render("~pill", text="Hi") def test_snake_to_kebab_conversion(self): - """Python snake_case kwargs should map to sexp kebab-case params.""" + """Python snake_case kwargs should map to sx kebab-case params.""" register_components(''' (defcomp ~card (&key nav-html link-href) (div :class "card" (a :href link-href nav-html))) @@ -95,52 +95,52 @@ class TestRender: # --------------------------------------------------------------------------- -# load_sexp_dir +# load_sx_dir # --------------------------------------------------------------------------- -class TestLoadSexpDir: - def test_loads_sexp_files(self): +class TestLoadSxDir: + def test_loads_sx_files(self): with tempfile.TemporaryDirectory() as tmpdir: - # Write a .sexp file - with open(os.path.join(tmpdir, "components.sexp"), "w") as f: + # Write a .sx file + with open(os.path.join(tmpdir, "components.sx"), "w") as f: f.write('(defcomp ~test-comp (&key msg) (div msg))') - load_sexp_dir(tmpdir) + load_sx_dir(tmpdir) html = render("test-comp", msg="loaded!") assert html == "
loaded!
" - def test_loads_sexpr_files(self): + def test_loads_sx_files_alt(self): with tempfile.TemporaryDirectory() as tmpdir: - with open(os.path.join(tmpdir, "nav.sexpr"), "w") as f: + with open(os.path.join(tmpdir, "nav.sx"), "w") as f: f.write('(defcomp ~nav-item (&key href label) (a :href href label))') - load_sexp_dir(tmpdir) + load_sx_dir(tmpdir) html = render("nav-item", href="/about", label="About") assert 'href="/about"' in html def test_loads_multiple_files(self): with tempfile.TemporaryDirectory() as tmpdir: - with open(os.path.join(tmpdir, "a.sexp"), "w") as f: + with open(os.path.join(tmpdir, "a.sx"), "w") as f: f.write('(defcomp ~comp-a (&key x) (b x))') - with open(os.path.join(tmpdir, "b.sexp"), "w") as f: + with open(os.path.join(tmpdir, "b.sx"), "w") as f: f.write('(defcomp ~comp-b (&key y) (i y))') - load_sexp_dir(tmpdir) + load_sx_dir(tmpdir) assert render("comp-a", x="A") == "A" assert render("comp-b", y="B") == "B" def test_empty_directory(self): with tempfile.TemporaryDirectory() as tmpdir: - load_sexp_dir(tmpdir) # should not raise + load_sx_dir(tmpdir) # should not raise - def test_ignores_non_sexp_files(self): + def test_ignores_non_sx_files(self): with tempfile.TemporaryDirectory() as tmpdir: with open(os.path.join(tmpdir, "readme.txt"), "w") as f: - f.write("not a sexp file") - with open(os.path.join(tmpdir, "comp.sexp"), "w") as f: + f.write("not a sx file") + with open(os.path.join(tmpdir, "comp.sx"), "w") as f: f.write('(defcomp ~real (&key v) (span v))') - load_sexp_dir(tmpdir) + load_sx_dir(tmpdir) assert "~real" in _COMPONENT_ENV # txt file should not have been loaded assert len([k for k in _COMPONENT_ENV if k.startswith("~")]) == 1 diff --git a/shared/tests/test_sexp_helpers.py b/shared/tests/test_sx_helpers.py similarity index 95% rename from shared/tests/test_sexp_helpers.py rename to shared/tests/test_sx_helpers.py index 9e6d277..93ca18a 100644 --- a/shared/tests/test_sexp_helpers.py +++ b/shared/tests/test_sx_helpers.py @@ -1,7 +1,7 @@ -"""Tests for shared sexp helper functions (call_url, get_asset_url, etc.).""" +"""Tests for shared sx helper functions (call_url, get_asset_url, etc.).""" from __future__ import annotations -from shared.sexp.helpers import call_url, get_asset_url +from shared.sx.helpers import call_url, get_asset_url # --------------------------------------------------------------------------- diff --git a/test/Dockerfile b/test/Dockerfile index 8970515..1b8451e 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -26,7 +26,7 @@ COPY shared/ ./shared/ COPY test/ ./test-app-tmp/ # Move service files into /app (flatten), but keep Dockerfile.* in place RUN cp -r test-app-tmp/app.py test-app-tmp/path_setup.py \ - test-app-tmp/bp test-app-tmp/sexp test-app-tmp/services \ + test-app-tmp/bp test-app-tmp/sx test-app-tmp/services \ test-app-tmp/runner.py test-app-tmp/__init__.py ./ 2>/dev/null || true && \ rm -rf test-app-tmp diff --git a/test/Dockerfile.unit b/test/Dockerfile.unit index 5e8098e..64af47c 100644 --- a/test/Dockerfile.unit +++ b/test/Dockerfile.unit @@ -8,7 +8,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app -# Node.js for sexp.js parity tests +# Node.js for sx.js parity tests RUN apt-get update && apt-get install -y --no-install-recommends nodejs && \ rm -rf /var/lib/apt/lists/* diff --git a/test/app.py b/test/app.py index 3bd9175..b510bc9 100644 --- a/test/app.py +++ b/test/app.py @@ -1,9 +1,9 @@ from __future__ import annotations import path_setup # noqa: F401 -import sexp.sexp_components as sexp_components # noqa: F401 +import sx.sx_components as sx_components # noqa: F401 from shared.infrastructure.factory import create_base_app -from shared.sexp.jinja_bridge import render +from shared.sx.jinja_bridge import render from bp import register_dashboard from services import register_domain_services @@ -36,7 +36,7 @@ def create_app() -> "Quart": domain_services_fn=register_domain_services, ) - import sexp.sexp_components # noqa: F401 + import sx.sx_components # noqa: F401 app.register_blueprint(register_dashboard(url_prefix="/")) diff --git a/test/bp/dashboard/routes.py b/test/bp/dashboard/routes.py index 6830891..076af58 100644 --- a/test/bp/dashboard/routes.py +++ b/test/bp/dashboard/routes.py @@ -12,9 +12,9 @@ def register(url_prefix: str = "/") -> Blueprint: @bp.get("/") async def index(): """Full page dashboard with last results.""" - from shared.sexp.page import get_template_context + from shared.sx.page import get_template_context from shared.browser.app.csrf import generate_csrf_token - from sexp.sexp_components import render_dashboard_page_sexp + from sx.sx_components import render_dashboard_page_sx import runner ctx = await get_template_context() @@ -24,7 +24,7 @@ def register(url_prefix: str = "/") -> Blueprint: active_filter = request.args.get("filter") active_service = request.args.get("service") - html = await render_dashboard_page_sexp( + html = await render_dashboard_page_sx( ctx, result, running, csrf, active_filter=active_filter, active_service=active_service, @@ -50,7 +50,7 @@ def register(url_prefix: str = "/") -> Blueprint: @bp.get("/test/") async def test_detail(nodeid: str): - """Test detail view — full page or sexp wire format.""" + """Test detail view — full page or sx wire format.""" import runner test = runner.get_test(nodeid) @@ -61,24 +61,24 @@ def register(url_prefix: str = "/") -> Blueprint: is_htmx = bool(request.headers.get("SX-Request") or request.headers.get("HX-Request")) if is_htmx: - # S-expression wire format — sexp.js renders client-side - from shared.sexp.helpers import sexp_response - from sexp.sexp_components import test_detail_sexp - return sexp_response(test_detail_sexp(test)) + # S-expression wire format — sx.js renders client-side + from shared.sx.helpers import sx_response + from sx.sx_components import test_detail_sx + return sx_response(test_detail_sx(test)) # Full page render (direct navigation / refresh) - from shared.sexp.page import get_template_context - from sexp.sexp_components import render_test_detail_page_sexp + from shared.sx.page import get_template_context + from sx.sx_components import render_test_detail_page_sx ctx = await get_template_context() - html = await render_test_detail_page_sexp(ctx, test) + html = await render_test_detail_page_sx(ctx, test) return await make_response(html, 200) @bp.get("/results") async def results(): """HTMX partial — poll target for results table.""" from shared.browser.app.csrf import generate_csrf_token - from sexp.sexp_components import render_results_partial_sexp + from sx.sx_components import render_results_partial_sx import runner result = runner.get_results() @@ -87,7 +87,7 @@ def register(url_prefix: str = "/") -> Blueprint: active_filter = request.args.get("filter") active_service = request.args.get("service") - html = await render_results_partial_sexp( + html = await render_results_partial_sx( result, running, csrf, active_filter=active_filter, active_service=active_service, diff --git a/test/runner.py b/test/runner.py index c0279e4..b8caabb 100644 --- a/test/runner.py +++ b/test/runner.py @@ -17,7 +17,7 @@ _running: bool = False # Each service group runs in its own pytest subprocess with its own PYTHONPATH _SERVICE_GROUPS: list[dict] = [ - {"name": "shared", "dirs": ["shared/tests/", "shared/sexp/tests/"], + {"name": "shared", "dirs": ["shared/tests/", "shared/sx/tests/"], "pythonpath": None}, {"name": "blog", "dirs": ["blog/tests/"], "pythonpath": "/app/blog"}, {"name": "market", "dirs": ["market/tests/"], "pythonpath": "/app/market"}, diff --git a/test/sexp/__init__.py b/test/sx/__init__.py similarity index 100% rename from test/sexp/__init__.py rename to test/sx/__init__.py diff --git a/test/sexp/dashboard.sexpr b/test/sx/dashboard.sx similarity index 100% rename from test/sexp/dashboard.sexpr rename to test/sx/dashboard.sx diff --git a/test/sexp/sexp_components.py b/test/sx/sx_components.py similarity index 63% rename from test/sexp/sexp_components.py rename to test/sx/sx_components.py index 0a064b0..cfba3e8 100644 --- a/test/sexp/sexp_components.py +++ b/test/sx/sx_components.py @@ -4,13 +4,13 @@ from __future__ import annotations import os from datetime import datetime -from shared.sexp.jinja_bridge import load_service_components -from shared.sexp.helpers import ( - sexp_call, SexpExpr, - root_header_sexp, full_page_sexp, header_child_sexp, +from shared.sx.jinja_bridge import load_service_components +from shared.sx.helpers import ( + sx_call, SxExpr, + root_header_sx, full_page_sx, header_child_sx, ) -# Load test-specific .sexpr components at import time +# Load test-specific .sx components at import time load_service_components(os.path.dirname(os.path.dirname(__file__))) @@ -50,9 +50,9 @@ def _filter_tests(tests: list[dict], active_filter: str | None, # Results partial # --------------------------------------------------------------------------- -def test_detail_sexp(test: dict) -> str: +def test_detail_sx(test: dict) -> str: """Return s-expression wire format for a test detail view.""" - inner = sexp_call( + inner = sx_call( "test-detail", nodeid=test["nodeid"], outcome=test["outcome"], @@ -67,31 +67,31 @@ def test_detail_sexp(test: dict) -> str: # --------------------------------------------------------------------------- -# Sexp-native versions — return sexp source (not HTML) +# Sx-native versions — return sx source (not HTML) # --------------------------------------------------------------------------- -def _test_header_sexp(ctx: dict, active_service: str | None = None) -> str: - """Build the Tests menu-row as sexp call.""" - nav = _service_nav_sexp(ctx, active_service) - return sexp_call("menu-row-sx", +def _test_header_sx(ctx: dict, active_service: str | None = None) -> str: + """Build the Tests menu-row as sx call.""" + nav = _service_nav_sx(ctx, active_service) + return sx_call("menu-row-sx", id="test-row", level=1, colour="sky", link_href="/", link_label="Tests", icon="fa fa-flask", - nav=SexpExpr(nav), + nav=SxExpr(nav), child_id="test-header-child", ) -def _service_nav_sexp(ctx: dict, active_service: str | None = None) -> str: - """Service filter nav as sexp.""" +def _service_nav_sx(ctx: dict, active_service: str | None = None) -> str: + """Service filter nav as sx.""" from runner import _SERVICE_ORDER parts = [] - parts.append(sexp_call("nav-link", + parts.append(sx_call("nav-link", href="/", label="all", is_selected="true" if not active_service else None, select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900", )) for svc in _SERVICE_ORDER: - parts.append(sexp_call("nav-link", + parts.append(sx_call("nav-link", href=f"/?service={svc}", label=svc, is_selected="true" if active_service == svc else None, select_colours="aria-selected:bg-sky-200 aria-selected:text-sky-900", @@ -99,19 +99,19 @@ def _service_nav_sexp(ctx: dict, active_service: str | None = None) -> str: return "(<> " + " ".join(parts) + ")" -def _header_stack_sexp(ctx: dict, active_service: str | None = None) -> str: - """Full header stack as sexp.""" - hdr = root_header_sexp(ctx) - inner = _test_header_sexp(ctx, active_service) - child = header_child_sexp(inner) +def _header_stack_sx(ctx: dict, active_service: str | None = None) -> str: + """Full header stack as sx.""" + hdr = root_header_sx(ctx) + inner = _test_header_sx(ctx, active_service) + child = header_child_sx(inner) return "(<> " + hdr + " " + child + ")" -def _test_rows_sexp(tests: list[dict]) -> str: - """Render all test result rows as sexp.""" +def _test_rows_sx(tests: list[dict]) -> str: + """Render all test result rows as sx.""" parts = [] for t in tests: - parts.append(sexp_call("test-row", + parts.append(sx_call("test-row", nodeid=t["nodeid"], outcome=t["outcome"], duration=str(t["duration"]), @@ -120,46 +120,46 @@ def _test_rows_sexp(tests: list[dict]) -> str: return "(<> " + " ".join(parts) + ")" -def _grouped_rows_sexp(tests: list[dict]) -> str: - """Test rows grouped by service as sexp.""" +def _grouped_rows_sx(tests: list[dict]) -> str: + """Test rows grouped by service as sx.""" from runner import group_tests_by_service sections = group_tests_by_service(tests) parts = [] for sec in sections: - parts.append(sexp_call("test-service-header", + parts.append(sx_call("test-service-header", service=sec["service"], total=str(sec["total"]), passed=str(sec["passed"]), failed=str(sec["failed"]), )) - parts.append(_test_rows_sexp(sec["tests"])) + parts.append(_test_rows_sx(sec["tests"])) return "(<> " + " ".join(parts) + ")" -def _results_partial_sexp(result: dict | None, running: bool, csrf: str, +def _results_partial_sx(result: dict | None, running: bool, csrf: str, active_filter: str | None = None, active_service: str | None = None) -> str: - """Results section as sexp.""" + """Results section as sx.""" if running and not result: - summary = sexp_call("test-summary", + summary = sx_call("test-summary", status="running", passed="0", failed="0", errors="0", skipped="0", total="0", duration="...", last_run="in progress", running=True, csrf=csrf, active_filter=active_filter, ) - return "(<> " + summary + " " + sexp_call("test-running-indicator") + ")" + return "(<> " + summary + " " + sx_call("test-running-indicator") + ")" if not result: - summary = sexp_call("test-summary", + summary = sx_call("test-summary", status=None, passed="0", failed="0", errors="0", skipped="0", total="0", duration="0", last_run="never", running=running, csrf=csrf, active_filter=active_filter, ) - return "(<> " + summary + " " + sexp_call("test-no-results") + ")" + return "(<> " + summary + " " + sx_call("test-no-results") + ")" status = "running" if running else result["status"] - summary = sexp_call("test-summary", + summary = sx_call("test-summary", status=status, passed=str(result["passed"]), failed=str(result["failed"]), @@ -174,65 +174,65 @@ def _results_partial_sexp(result: dict | None, running: bool, csrf: str, ) if running: - return "(<> " + summary + " " + sexp_call("test-running-indicator") + ")" + return "(<> " + summary + " " + sx_call("test-running-indicator") + ")" tests = result.get("tests", []) tests = _filter_tests(tests, active_filter, active_service) if not tests: - return "(<> " + summary + " " + sexp_call("test-no-results") + ")" + return "(<> " + summary + " " + sx_call("test-no-results") + ")" has_failures = result["failed"] > 0 or result["errors"] > 0 - rows = _grouped_rows_sexp(tests) - table = sexp_call("test-results-table", - rows=SexpExpr(rows), + rows = _grouped_rows_sx(tests) + table = sx_call("test-results-table", + rows=SxExpr(rows), has_failures=str(has_failures).lower(), ) return "(<> " + summary + " " + table + ")" -def _wrap_results_div_sexp(inner: str, running: bool) -> str: - """Wrap results in a div with HTMX polling (sexp).""" +def _wrap_results_div_sx(inner: str, running: bool) -> str: + """Wrap results in a div with HTMX polling (sx).""" attrs = ':id "test-results" :class "space-y-6 p-4"' if running: attrs += ' :sx-get "/results" :sx-trigger "every 2s" :sx-swap "outerHTML"' return f'(div {attrs} {inner})' -async def render_dashboard_page_sexp(ctx: dict, result: dict | None, +async def render_dashboard_page_sx(ctx: dict, result: dict | None, running: bool, csrf: str, active_filter: str | None = None, active_service: str | None = None) -> str: - """Full page: test dashboard (sexp wire format).""" - hdr = _header_stack_sexp(ctx, active_service) - inner = _results_partial_sexp(result, running, csrf, active_filter, active_service) - content = _wrap_results_div_sexp(inner, running) - return full_page_sexp(ctx, header_rows=hdr, content=content) + """Full page: test dashboard (sx wire format).""" + hdr = _header_stack_sx(ctx, active_service) + inner = _results_partial_sx(result, running, csrf, active_filter, active_service) + content = _wrap_results_div_sx(inner, running) + return full_page_sx(ctx, header_rows=hdr, content=content) -async def render_results_partial_sexp(result: dict | None, running: bool, +async def render_results_partial_sx(result: dict | None, running: bool, csrf: str, active_filter: str | None = None, active_service: str | None = None) -> str: - """HTMX partial: results section (sexp wire format).""" - inner = _results_partial_sexp(result, running, csrf, active_filter, active_service) - return _wrap_results_div_sexp(inner, running) + """HTMX partial: results section (sx wire format).""" + inner = _results_partial_sx(result, running, csrf, active_filter, active_service) + return _wrap_results_div_sx(inner, running) -async def render_test_detail_page_sexp(ctx: dict, test: dict) -> str: - """Full page: test detail (sexp wire format).""" - root_hdr = root_header_sexp(ctx) - test_row = _test_header_sexp(ctx) - detail_row = sexp_call("menu-row-sx", +async def render_test_detail_page_sx(ctx: dict, test: dict) -> str: + """Full page: test detail (sx wire format).""" + root_hdr = root_header_sx(ctx) + test_row = _test_header_sx(ctx) + detail_row = sx_call("menu-row-sx", id="test-detail-row", level=2, colour="sky", link_href=f"/test/{test['nodeid']}", link_label=test["nodeid"].rsplit("::", 1)[-1], ) - inner = "(<> " + test_row + " " + header_child_sexp(detail_row, id="test-header-child") + ")" - hdr = "(<> " + root_hdr + " " + header_child_sexp(inner) + ")" - content = sexp_call("test-detail", + inner = "(<> " + test_row + " " + header_child_sx(detail_row, id="test-header-child") + ")" + hdr = "(<> " + root_hdr + " " + header_child_sx(inner) + ")" + content = sx_call("test-detail", nodeid=test["nodeid"], outcome=test["outcome"], duration=str(test["duration"]), longrepr=test.get("longrepr", ""), ) - return full_page_sexp(ctx, header_rows=hdr, content=content) + return full_page_sx(ctx, header_rows=hdr, content=content)