- Server sends sexp source text, client (sexp.js) renders everything - SexpExpr marker class for nested sexp composition in serialize() - sexp_page() HTML shell with data-mount="body" for full page loads - sexp_response() returns text/sexp for OOB/partial responses - ~app-body layout component replaces ~app-layout (no raw!) - ~rich-text is the only component using raw! (for CMS HTML content) - Fragment endpoints return text/sexp, auto-wrapped in SexpExpr - All _*_html() helpers converted to _*_sexp() returning sexp source - Head auto-hoist: sexp.js moves meta/title/link/script[ld+json] from rendered body to document.head automatically - Unknown components render warning box instead of crashing page - Component kwargs preserve AST for lazy rendering (fixes <> in kwargs) - Fix unterminated paren in events/sexp/tickets.sexpr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
112 lines
3.8 KiB
Python
112 lines
3.8 KiB
Python
"""
|
|
Base template context shared by all apps.
|
|
|
|
This module no longer imports cart or menu_items services directly.
|
|
Each app provides its own context_fn that calls this base and adds
|
|
app-specific variables (cart data, menu_items, etc.).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from quart import request, g, current_app
|
|
|
|
from shared.config import config
|
|
from shared.utils import host_url
|
|
from shared.browser.app.utils import current_route_relative_path
|
|
from shared.infrastructure.urls import blog_url, market_url, cart_url, events_url
|
|
|
|
|
|
def _qs_filter_fn():
|
|
"""Build a qs_filter(dict) wrapper for sexp components, or None.
|
|
|
|
Sexp 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
|
|
to ``makeqs(**kwargs)``. List-valued keys (labels, stickers, brands)
|
|
represent *replacement* sets, so we rebuild the querystring from the
|
|
current base with those overridden.
|
|
"""
|
|
factory = getattr(g, "makeqs_factory", None)
|
|
if not factory:
|
|
return None
|
|
makeqs = factory()
|
|
|
|
def _qs(d: dict) -> str:
|
|
from shared.browser.app.filters.qs_base import build_qs
|
|
|
|
# Collect list-valued overrides
|
|
list_overrides = {}
|
|
for plural, singular in (("labels", "label"), ("stickers", "sticker"), ("brands", "brand")):
|
|
if plural in d:
|
|
list_overrides[singular] = list(d[plural] or [])
|
|
|
|
simple = {k: v for k, v in d.items()
|
|
if k in ("page", "sort", "search", "liked", "clear_filters")}
|
|
|
|
if not list_overrides:
|
|
return makeqs(**simple)
|
|
|
|
# For list overrides: get the base qs, parse out the overridden keys,
|
|
# then rebuild with the new values.
|
|
base_qs = makeqs(**simple)
|
|
from urllib.parse import parse_qsl, urlencode
|
|
params = [(k, v) for k, v in parse_qsl(base_qs.lstrip("?"))
|
|
if k not in list_overrides]
|
|
for singular, vals in list_overrides.items():
|
|
for v in vals:
|
|
params.append((singular, v))
|
|
return ("?" + urlencode(params)) if params else ""
|
|
|
|
return _qs
|
|
|
|
|
|
async def base_context() -> dict:
|
|
"""
|
|
Common template variables available in every app.
|
|
|
|
Does NOT include cart, calendar_cart_entries, total, calendar_total,
|
|
or menu_items — those are added by each app's context_fn.
|
|
"""
|
|
is_htmx = request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true"
|
|
search = request.headers.get("X-Search", "")
|
|
zap_filter = is_htmx and search == ""
|
|
|
|
def base_url():
|
|
return host_url()
|
|
|
|
hx_select = "#main-panel"
|
|
hx_select_search = (
|
|
hx_select
|
|
+ ", #search-mobile, #search-count-mobile, #search-desktop, #search-count-desktop, #menu-items-nav-wrapper"
|
|
)
|
|
|
|
return {
|
|
"is_htmx": is_htmx,
|
|
"request": request,
|
|
"now": datetime.now(),
|
|
"current_local_href": current_route_relative_path(),
|
|
"config": config(),
|
|
"asset_url": current_app.jinja_env.globals.get("asset_url", lambda p: ""),
|
|
"sort_options": [
|
|
("az", "A\u2013Z", "order/a-z.svg"),
|
|
("za", "Z\u2013A", "order/z-a.svg"),
|
|
("price-asc", "\u00a3 low\u2192high", "order/l-h.svg"),
|
|
("price-desc", "\u00a3 high\u2192low", "order/h-l.svg"),
|
|
],
|
|
"zap_filter": zap_filter,
|
|
"qs_filter": _qs_filter_fn(),
|
|
"print": print,
|
|
"base_url": base_url,
|
|
"app_label": current_app.name,
|
|
"base_title": config()["title"],
|
|
"hx_select": hx_select,
|
|
"hx_select_search": hx_select_search,
|
|
"blog_url": blog_url,
|
|
"market_url": market_url,
|
|
"cart_url": cart_url,
|
|
"events_url": events_url,
|
|
"rights": getattr(g, "rights", {}),
|
|
}
|