Remove render_to_sx from public API: enforce sx_call for all service code

Replace ~250 render_to_sx calls across all services with sync sx_call,
converting many async functions to sync where no other awaits remained.
Make render_to_sx/render_to_sx_with_env private (_render_to_sx).
Add (post-header-ctx) IO primitive and shared post/post-admin defmacros.
Convert built-in post/post-admin layouts from Python to register_sx_layout
with .sx defcomps. Remove dead post_admin_mobile_nav_sx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 19:30:45 +00:00
parent 57e0d0c341
commit 959e63d440
61 changed files with 1352 additions and 1208 deletions

View File

@@ -47,7 +47,7 @@ def register() -> Blueprint:
markets, has_more, page_info = await _load_markets(page)
from sxc.pages.renders import render_all_markets_cards
sx_src = await render_all_markets_cards(markets, has_more, page_info, page)
sx_src = render_all_markets_cards(markets, has_more, page_info, page)
return sx_response(sx_src)
return bp

View File

@@ -58,7 +58,7 @@ def register():
resp = await make_response(html)
elif product_info["page"] > 1:
tctx.update(product_info)
sx_src = await render_browse_cards(tctx)
sx_src = render_browse_cards(tctx)
resp = sx_response(sx_src)
else:
sx_src = await render_browse_oob(tctx)
@@ -99,7 +99,7 @@ def register():
resp = await make_response(html)
elif product_info["page"] > 1:
tctx.update(product_info)
sx_src = await render_browse_cards(tctx)
sx_src = render_browse_cards(tctx)
resp = sx_response(sx_src)
else:
sx_src = await render_browse_oob(tctx)
@@ -140,7 +140,7 @@ def register():
resp = await make_response(html)
elif product_info["page"] > 1:
tctx.update(product_info)
sx_src = await render_browse_cards(tctx)
sx_src = render_browse_cards(tctx)
resp = sx_response(sx_src)
else:
sx_src = await render_browse_oob(tctx)

View File

@@ -32,7 +32,7 @@ def register() -> Blueprint:
from sxc.pages.renders import render_page_markets_cards
post_slug = post.get("slug", "")
sx_src = await render_page_markets_cards(markets, has_more, page, post_slug)
sx_src = render_page_markets_cards(markets, has_more, page, post_slug)
return sx_response(sx_src)
return bp

View File

@@ -129,7 +129,7 @@ def register():
from sxc.pages.renders import render_like_toggle_button
if not g.user:
return sx_response(await 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 sx_response(await render_like_toggle_button(product_slug, liked))
return sx_response(render_like_toggle_button(product_slug, liked))
@@ -257,7 +257,7 @@ def register():
from sxc.pages.renders import render_cart_added_response
item_data = getattr(g, "item_data", {})
d = item_data.get("d", {})
return sx_response(await 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

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any
from shared.sx.parser import SxExpr
from shared.sx.helpers import render_to_sx
from shared.sx.helpers import sx_call
from .utils import _set_prices, _price_str
from .filters import _MOBILE_SENTINEL_HS, _DESKTOP_SENTINEL_HS
@@ -14,7 +14,7 @@ from .filters import _MOBILE_SENTINEL_HS, _DESKTOP_SENTINEL_HS
# Product card (browse grid item)
# ---------------------------------------------------------------------------
async def _product_card_sx(p: dict, ctx: dict) -> str:
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
@@ -98,10 +98,10 @@ async def _product_card_sx(p: dict, ctx: dict) -> str:
kwargs["search_mid"] = search_mid
kwargs["search_post"] = search_post
return await render_to_sx("market-product-card", **kwargs)
return sx_call("market-product-card", **kwargs)
async def _product_cards_sx(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
@@ -114,7 +114,7 @@ async def _product_cards_sx(ctx: dict) -> str:
parts = []
for p in products:
parts.append(await _product_card_sx(p, ctx))
parts.append(_product_card_sx(p, ctx))
if page < total_pages:
if callable(qs_fn):
@@ -122,25 +122,25 @@ async def _product_cards_sx(ctx: dict) -> str:
else:
next_qs = f"?page={page + 1}"
next_url = prefix + current_local_href + next_qs
parts.append(await render_to_sx("sentinel-mobile",
parts.append(sx_call("sentinel-mobile",
id=f"sentinel-{page}-m", next_url=next_url,
hyperscript=_MOBILE_SENTINEL_HS))
parts.append(await render_to_sx("sentinel-desktop",
parts.append(sx_call("sentinel-desktop",
id=f"sentinel-{page}-d", next_url=next_url,
hyperscript=_DESKTOP_SENTINEL_HS))
else:
parts.append(await render_to_sx("end-of-results"))
parts.append(sx_call("end-of-results"))
return "(<> " + " ".join(parts) + ")"
async def _like_button_sx(slug: str, liked: bool, csrf: str, ctx: dict) -> str:
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 await render_to_sx(
return sx_call(
"market-like-button",
form_id=f"like-{slug}", action=action, slug=slug,
csrf=csrf, icon_cls=icon_cls,
@@ -151,7 +151,7 @@ async def _like_button_sx(slug: str, liked: bool, csrf: str, ctx: dict) -> str:
# Market cards (all markets / page markets)
# ---------------------------------------------------------------------------
async def _market_card_sx(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 sx."""
from shared.infrastructure.urls import market_url
@@ -173,20 +173,20 @@ async def _market_card_sx(market: Any, page_info: dict, *, show_page_badge: bool
title_sx = ""
if market_href:
title_sx = await render_to_sx("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_sx = await render_to_sx("market-market-card-title", name=name)
title_sx = sx_call("market-market-card-title", name=name)
desc_sx = ""
if description:
desc_sx = await render_to_sx("market-market-card-desc", description=description)
desc_sx = sx_call("market-market-card-desc", description=description)
badge_sx = ""
if show_page_badge and p_title:
badge_href = market_url(f"/{p_slug}/")
badge_sx = await render_to_sx("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 await render_to_sx(
return sx_call(
"market-market-card",
title_content=SxExpr(title_sx) if title_sx else None,
desc_content=SxExpr(desc_sx) if desc_sx else None,
@@ -194,30 +194,30 @@ async def _market_card_sx(market: Any, page_info: dict, *, show_page_badge: bool
)
async def _market_cards_sx(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 sx."""
parts = []
for m in markets:
parts.append(await _market_card_sx(m, page_info, show_page_badge=show_page_badge,
parts.append(_market_card_sx(m, page_info, show_page_badge=show_page_badge,
post_slug=post_slug))
if has_more:
parts.append(await render_to_sx(
parts.append(sx_call(
"sentinel-simple",
id=f"sentinel-{page}", next_url=next_url,
))
return "(<> " + " ".join(parts) + ")"
async def _markets_grid(cards_sx: str) -> str:
def _markets_grid(cards_sx: str) -> str:
"""Wrap market cards in a grid as sx."""
return await render_to_sx("market-markets-grid", cards=SxExpr(cards_sx))
return sx_call("market-markets-grid", cards=SxExpr(cards_sx))
async def _no_markets_sx(message: str = "No markets available") -> str:
def _no_markets_sx(message: str = "No markets available") -> str:
"""Empty state for markets as sx."""
return await render_to_sx("empty-state", icon="fa fa-store", message=message,
return sx_call("empty-state", icon="fa fa-store", message=message,
cls="px-3 py-12 text-center text-stone-400")
@@ -225,14 +225,14 @@ async def _no_markets_sx(message: str = "No markets available") -> str:
# Market landing page
# ---------------------------------------------------------------------------
async def _market_landing_content_sx(post: dict) -> str:
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(await render_to_sx("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(await render_to_sx("market-landing-image", src=post["feature_image"]))
parts.append(sx_call("market-landing-image", src=post["feature_image"]))
if post.get("html"):
parts.append(await render_to_sx("market-landing-html", html=post["html"]))
parts.append(sx_call("market-landing-html", html=post["html"]))
inner = "(<> " + " ".join(parts) + ")" if parts else "(<>)"
return await render_to_sx("market-landing-content", inner=SxExpr(inner))
return sx_call("market-landing-content", inner=SxExpr(inner))

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from shared.sx.parser import SxExpr
from shared.sx.helpers import (
render_to_sx,
sx_call,
search_mobile_sx, search_desktop_sx,
)
@@ -101,31 +101,31 @@ async def _desktop_filter_sx(ctx: dict) -> str:
search_sx = await search_desktop_sx(ctx)
# Category summary + sort + like + labels + stickers
cat_parts = [await render_to_sx("market-filter-category-label", label=category_label)]
cat_parts = [sx_call("market-filter-category-label", label=category_label)]
if sort_options:
cat_parts.append(await _sort_stickers_sx(sort_options, sort, ctx))
cat_parts.append(_sort_stickers_sx(sort_options, sort, ctx))
like_label_parts = [await _like_filter_sx(liked, liked_count, ctx)]
like_label_parts = [_like_filter_sx(liked, liked_count, ctx)]
if labels:
like_label_parts.append(await _labels_filter_sx(labels, selected_labels, ctx, prefix="nav-labels"))
like_label_parts.append(_labels_filter_sx(labels, selected_labels, ctx, prefix="nav-labels"))
like_labels_sx = "(<> " + " ".join(like_label_parts) + ")"
cat_parts.append(await render_to_sx("market-filter-like-labels-nav", inner=SxExpr(like_labels_sx)))
cat_parts.append(sx_call("market-filter-like-labels-nav", inner=SxExpr(like_labels_sx)))
if stickers:
cat_parts.append(await _stickers_filter_sx(stickers, selected_stickers, ctx))
cat_parts.append(_stickers_filter_sx(stickers, selected_stickers, ctx))
if subs_local and top_local_href:
cat_parts.append(await _subcategory_selector_sx(subs_local, top_local_href, sub_slug, ctx))
cat_parts.append(_subcategory_selector_sx(subs_local, top_local_href, sub_slug, ctx))
cat_inner_sx = "(<> " + " ".join(cat_parts) + ")"
cat_summary = await render_to_sx("market-desktop-category-summary", inner=SxExpr(cat_inner_sx))
cat_summary = sx_call("market-desktop-category-summary", inner=SxExpr(cat_inner_sx))
# Brand filter
brand_inner = ""
if brands:
brand_inner = await _brand_filter_sx(brands, selected_brands, ctx)
brand_summary = await render_to_sx("market-desktop-brand-summary",
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_sx, cat_summary, brand_summary]) + ")"
@@ -154,14 +154,14 @@ async def _mobile_filter_summary_sx(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(await render_to_sx("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 = [await render_to_sx("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(await render_to_sx("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(await render_to_sx("market-mobile-chip-liked", inner=SxExpr(liked_inner)))
chip_parts.append(sx_call("market-mobile-chip-liked", inner=SxExpr(liked_inner)))
# Selected labels
if selected_labels:
@@ -169,18 +169,18 @@ async def _mobile_filter_summary_sx(ctx: dict) -> str:
for sl in selected_labels:
for lb in labels:
if lb.get("name") == sl and callable(asset_url_fn):
li_parts = [await render_to_sx(
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(await render_to_sx("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(await render_to_sx("market-mobile-chip-item", inner=SxExpr(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(await render_to_sx("market-mobile-chip-list", items=SxExpr(label_items)))
chip_parts.append(sx_call("market-mobile-chip-list", items=SxExpr(label_items)))
# Selected stickers
if selected_stickers:
@@ -188,18 +188,18 @@ async def _mobile_filter_summary_sx(ctx: dict) -> str:
for ss in selected_stickers:
for st in stickers:
if st.get("name") == ss and callable(asset_url_fn):
si_parts = [await render_to_sx(
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(await render_to_sx("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(await render_to_sx("market-mobile-chip-item", inner=SxExpr(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(await render_to_sx("market-mobile-chip-list", items=SxExpr(sticker_items)))
chip_parts.append(sx_call("market-mobile-chip-list", items=SxExpr(sticker_items)))
# Selected brands
if selected_brands:
@@ -210,21 +210,21 @@ async def _mobile_filter_summary_sx(ctx: dict) -> str:
if br.get("name") == b:
count = br.get("count", 0)
if count:
brand_item_parts.append(await render_to_sx("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(await render_to_sx("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(await render_to_sx("market-mobile-chip-brand-list", items=SxExpr(brand_items)))
chip_parts.append(sx_call("market-mobile-chip-brand-list", items=SxExpr(brand_items)))
chips_sx = "(<> " + " ".join(chip_parts) + ")" if chip_parts else '(<>)'
chips_row = await render_to_sx("market-mobile-chips-row", inner=SxExpr(chips_sx))
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 = await _mobile_filter_content_sx(ctx, prefix)
mobile_filter = _mobile_filter_content_sx(ctx, prefix)
return await render_to_sx(
return sx_call(
"market-mobile-filter-summary",
search_bar=SxExpr(search_bar),
chips=SxExpr(chips_row),
@@ -232,7 +232,7 @@ async def _mobile_filter_summary_sx(ctx: dict) -> str:
)
async def _mobile_filter_content_sx(ctx: dict, prefix: str) -> str:
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", [])
@@ -253,33 +253,33 @@ async def _mobile_filter_content_sx(ctx: dict, prefix: str) -> str:
# Sort options
if sort_options:
parts.append(await _sort_stickers_sx(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(await render_to_sx("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 = [await _like_filter_sx(liked, liked_count, ctx, mobile=True)]
like_label_parts = [_like_filter_sx(liked, liked_count, ctx, mobile=True)]
if labels:
like_label_parts.append(await _labels_filter_sx(labels, selected_labels, ctx, prefix="nav-labels", mobile=True))
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(await render_to_sx("market-mobile-like-labels-row", inner=SxExpr(like_labels_sx)))
parts.append(sx_call("market-mobile-like-labels-row", inner=SxExpr(like_labels_sx)))
# Stickers
if stickers:
parts.append(await _stickers_filter_sx(stickers, selected_stickers, ctx, mobile=True))
parts.append(_stickers_filter_sx(stickers, selected_stickers, ctx, mobile=True))
# Brands
if brands:
parts.append(await _brand_filter_sx(brands, selected_brands, ctx, mobile=True))
parts.append(_brand_filter_sx(brands, selected_brands, ctx, mobile=True))
return "(<> " + " ".join(parts) + ")" if parts else "(<>)"
async def _sort_stickers_sx(sort_options: list, current_sort: str, ctx: dict, mobile: bool = False) -> str:
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", "/")
@@ -297,15 +297,15 @@ async def _sort_stickers_sx(sort_options: list, current_sort: str, ctx: dict, mo
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(await render_to_sx(
item_parts.append(sx_call(
"market-filter-sort-item",
href=href, hx_select=hx_select, ring_cls=ring, src=src, label=label,
))
items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)"
return await render_to_sx("market-filter-sort-row", items=SxExpr(items_sx))
return sx_call("market-filter-sort-row", items=SxExpr(items_sx))
async def _like_filter_sx(liked: bool, liked_count: int, ctx: dict, mobile: bool = False) -> str:
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")
@@ -320,13 +320,13 @@ async def _like_filter_sx(liked: bool, liked_count: int, ctx: dict, mobile: bool
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 await render_to_sx(
return sx_call(
"market-filter-like",
href=href, hx_select=hx_select, icon_cls=icon_cls, size_cls=size,
)
async def _labels_filter_sx(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 sx."""
asset_url_fn = ctx.get("asset_url")
@@ -347,14 +347,14 @@ async def _labels_filter_sx(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(await render_to_sx(
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 "(<>)"
async def _stickers_filter_sx(stickers: list, selected: list, ctx: dict, mobile: bool = False) -> str:
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", "/")
@@ -376,16 +376,16 @@ async def _stickers_filter_sx(stickers: list, selected: list, ctx: dict, mobile:
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(await render_to_sx(
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_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)"
return await render_to_sx("market-filter-stickers-row", items=SxExpr(items_sx))
return sx_call("market-filter-stickers-row", items=SxExpr(items_sx))
async def _brand_filter_sx(brands: list, selected: list, ctx: dict, mobile: bool = False) -> str:
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")
@@ -405,16 +405,16 @@ async def _brand_filter_sx(brands: list, selected: list, ctx: dict, mobile: bool
href = "#"
bg = " bg-yellow-200" if is_sel else ""
cls = "text-md" if count else "text-md text-red-500"
item_parts.append(await render_to_sx(
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_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else "(<>)"
return await render_to_sx("market-filter-brands-panel", items=SxExpr(items_sx))
return sx_call("market-filter-brands-panel", items=SxExpr(items_sx))
async def _subcategory_selector_sx(subs: list, top_href: str, current_sub: str, ctx: dict) -> str:
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
@@ -422,7 +422,7 @@ async def _subcategory_selector_sx(subs: list, top_href: str, current_sub: str,
all_cls = " bg-stone-200 font-medium" if not current_sub else ""
all_full_href = rp + top_href
item_parts = [await render_to_sx(
item_parts = [sx_call(
"market-filter-subcategory-item",
href=all_full_href, hx_select=hx_select, active_cls=all_cls, name="All",
)]
@@ -433,9 +433,9 @@ async def _subcategory_selector_sx(subs: list, top_href: str, current_sub: str,
active = (slug == current_sub)
active_cls = " bg-stone-200 font-medium" if active else ""
full_href = rp + href
item_parts.append(await render_to_sx(
item_parts.append(sx_call(
"market-filter-subcategory-item",
href=full_href, hx_select=hx_select, active_cls=active_cls, name=name,
))
items_sx = "(<> " + " ".join(item_parts) + ")"
return await render_to_sx("market-filter-subcategory-panel", items=SxExpr(items_sx))
return sx_call("market-filter-subcategory-panel", items=SxExpr(items_sx))

View File

@@ -15,7 +15,7 @@ async def _markets_admin_panel_sx(ctx: dict) -> str:
"""Render the markets list + create form panel."""
from quart import g, url_for
from shared.services.registry import services
from shared.sx.helpers import render_to_sx
from shared.sx.helpers import sx_call
from shared.sx.parser import SxExpr
rights = ctx.get("rights") or {}
@@ -32,30 +32,30 @@ async def _markets_admin_panel_sx(ctx: dict) -> str:
form_html = ""
if can_create:
create_url = url_for("page_admin.create_market")
form_html = await render_to_sx("crud-create-form",
form_html = sx_call("crud-create-form",
create_url=create_url, csrf=csrf,
errors_id="market-create-errors",
list_id="markets-list",
placeholder="e.g. Suma, Craft Fair",
btn_label="Add market")
list_html = await _markets_admin_list_sx(ctx, markets)
return await render_to_sx("crud-panel",
list_html = _markets_admin_list_sx(ctx, markets)
return sx_call("crud-panel",
form=SxExpr(form_html), list=SxExpr(list_html),
list_id="markets-list")
async def _markets_admin_list_sx(ctx: dict, markets: list) -> str:
def _markets_admin_list_sx(ctx: dict, markets: list) -> str:
"""Render the markets list items."""
from quart import url_for
from shared.utils import route_prefix
from shared.sx.helpers import render_to_sx
from shared.sx.helpers import sx_call
csrf_token = ctx.get("csrf_token")
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
prefix = route_prefix()
if not markets:
return await render_to_sx("empty-state",
return sx_call("empty-state",
message="No markets yet. Create one above.",
cls="text-gray-500 mt-4")
@@ -67,7 +67,7 @@ async def _markets_admin_list_sx(ctx: dict, markets: list) -> str:
href = prefix + f"/{post_slug}/{m_slug}/"
del_url = url_for("page_admin.delete_market", market_slug=m_slug)
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
parts.append(await render_to_sx("crud-item",
parts.append(sx_call("crud-item",
href=href, name=m_name, slug=m_slug,
del_url=del_url, csrf_hdr=csrf_hdr,
list_id="markets-list",
@@ -116,13 +116,13 @@ async def _h_all_markets_content(**kw):
page_info[p.id] = {"title": p.title, "slug": p.slug}
if not markets:
return await _no_markets_sx()
return _no_markets_sx()
prefix = route_prefix()
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
cards = await _market_cards_sx(markets, page_info, page, has_more, next_url)
return await _markets_grid(cards)
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
return _markets_grid(cards)
async def _h_page_markets_content(slug=None, **kw):
@@ -138,30 +138,30 @@ async def _h_page_markets_content(slug=None, **kw):
post_slug = post.get("slug", "")
if not markets:
return await _no_markets_sx("No markets for this page")
return _no_markets_sx("No markets for this page")
prefix = route_prefix()
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
cards = await _market_cards_sx(markets, {}, page, has_more, next_url,
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
return await _markets_grid(cards)
return _markets_grid(cards)
async def _h_page_admin_content(slug=None, **kw):
from shared.sx.page import get_template_context
from shared.sx.helpers import render_to_sx
from shared.sx.helpers import sx_call
from shared.sx.parser import SxExpr
ctx = await get_template_context()
content = await _markets_admin_panel_sx(ctx)
return await render_to_sx("market-admin-content-wrap", inner=SxExpr(content))
return sx_call("market-admin-content-wrap", inner=SxExpr(content))
async def _h_market_home_content(page_slug=None, market_slug=None, **kw):
def _h_market_home_content(page_slug=None, market_slug=None, **kw):
from quart import g
post_data = getattr(g, "post_data", {})
post = post_data.get("post", {})
return await _market_landing_content_sx(post)
return _market_landing_content_sx(post)
def _h_market_admin_content(page_slug=None, market_slug=None, **kw):

View File

@@ -5,7 +5,7 @@ from typing import Any
from shared.sx.parser import SxExpr
from shared.sx.helpers import (
render_to_sx,
sx_call,
post_header_sx as _post_header_sx,
post_admin_header_sx,
oob_header_sx as _oob_header_sx,
@@ -19,7 +19,7 @@ from .utils import _set_prices, _price_str, _clear_deeper_oob
# Header helpers
# ---------------------------------------------------------------------------
async def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
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
@@ -28,7 +28,7 @@ async def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
sub_slug = ctx.get("sub_slug", "")
hx_select_search = ctx.get("hx_select_search", "#main-panel")
label_sx = await render_to_sx(
label_sx = sx_call(
"market-shop-label",
title=market_title, top_slug=top_slug or "",
sub_div=sub_slug or None,
@@ -39,9 +39,9 @@ async def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
# Build desktop nav from categories
categories = ctx.get("categories", {})
qs = ctx.get("qs", "")
nav_sx = await _desktop_category_nav_sx(ctx, categories, qs, hx_select_search)
nav_sx = _desktop_category_nav_sx(ctx, categories, qs, hx_select_search)
return await render_to_sx(
return sx_call(
"menu-row-sx",
id="market-row", level=2,
link_href=link_href, link_label_content=SxExpr(label_sx),
@@ -50,7 +50,7 @@ async def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
)
async def _desktop_category_nav_sx(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 sx."""
from quart import url_for
@@ -63,7 +63,7 @@ async def _desktop_category_nav_sx(ctx: dict, categories: dict, qs: str,
all_href = prefix + url_for("market.browse.browse_all") + qs
all_active = (category_label == "All Products")
link_parts = [await render_to_sx(
link_parts = [sx_call(
"market-category-link",
href=all_href, hx_select=hx_select, active=all_active,
select_colours=select_colours, label="All",
@@ -72,7 +72,7 @@ async def _desktop_category_nav_sx(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(await render_to_sx(
link_parts.append(sx_call(
"market-category-link",
href=cat_href, hx_select=hx_select, active=cat_active,
select_colours=select_colours, label=cat,
@@ -83,14 +83,14 @@ async def _desktop_category_nav_sx(ctx: dict, categories: dict, qs: str,
admin_sx = ""
if rights and rights.get("admin"):
admin_href = prefix + url_for("defpage_market_admin")
admin_sx = await render_to_sx("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 await render_to_sx("market-desktop-category-nav",
return sx_call("market-desktop-category-nav",
links=SxExpr(links_sx),
admin=SxExpr(admin_sx) if admin_sx else None)
async def _product_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
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
@@ -100,21 +100,21 @@ async def _product_header_sx(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_sx = await render_to_sx("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 = await _prices_header_sx(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(await render_to_sx("market-admin-link", href=admin_href, hx_select=hx_select_search))
nav_parts.append(sx_call("market-admin-link", href=admin_href, hx_select=hx_select_search))
nav_sx = "(<> " + " ".join(nav_parts) + ")"
return await render_to_sx(
return sx_call(
"menu-row-sx",
id="product-row", level=3,
link_href=link_href, link_label_content=SxExpr(label_sx),
@@ -122,7 +122,7 @@ async def _product_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
)
async def _prices_header_sx(d: dict, pr: dict, cart: list, slug: str, ctx: dict) -> str:
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
@@ -133,20 +133,20 @@ async def _prices_header_sx(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_sx = await _cart_add_sx(slug, quantity, cart_action, csrf, cart_url_fn)
add_sx = _cart_add_sx(slug, quantity, cart_action, csrf, cart_url_fn)
parts = [add_sx]
sp_val, rp_val = pr.get("sp_val"), pr.get("rp_val")
if sp_val:
parts.append(await render_to_sx("market-header-price-special-label"))
parts.append(await render_to_sx("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(await render_to_sx("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(await render_to_sx("market-header-price-regular-label"))
parts.append(await render_to_sx("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
@@ -155,23 +155,23 @@ async def _prices_header_sx(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(await render_to_sx("market-header-rrp", rrp=rrp_str))
parts.append(sx_call("market-header-rrp", rrp=rrp_str))
inner_sx = "(<> " + " ".join(parts) + ")"
return await render_to_sx("market-prices-row", inner=SxExpr(inner_sx))
return sx_call("market-prices-row", inner=SxExpr(inner_sx))
async def _cart_add_sx(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 sx."""
if not quantity:
return await render_to_sx(
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 await render_to_sx(
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),
@@ -183,7 +183,7 @@ async def _cart_add_sx(slug: str, quantity: int, action: str, csrf: str,
# Mobile nav panel
# ---------------------------------------------------------------------------
async def _mobile_nav_panel_sx(ctx: dict) -> str:
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
@@ -199,7 +199,7 @@ async def _mobile_nav_panel_sx(ctx: dict) -> str:
all_href = prefix + url_for("market.browse.browse_all") + qs
all_active = (category_label == "All Products")
item_parts = [await render_to_sx(
item_parts = [sx_call(
"market-mobile-all-link",
href=all_href, hx_select=hx_select, active=all_active,
select_colours=select_colours,
@@ -211,10 +211,10 @@ async def _mobile_nav_panel_sx(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_sx = await render_to_sx("market-mobile-chevron")
chevron_sx = sx_call("market-mobile-chevron")
cat_count = data.get("count", 0)
summary_sx = await render_to_sx(
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,
@@ -231,19 +231,19 @@ async def _mobile_nav_panel_sx(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(await render_to_sx(
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_sx = "(<> " + " ".join(sub_link_parts) + ")"
subs_sx = await render_to_sx("market-mobile-subs-panel", links=SxExpr(sub_links_sx))
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_sx = await render_to_sx("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(await render_to_sx(
item_parts.append(sx_call(
"market-mobile-cat-details",
open=cat_active or None,
summary=SxExpr(summary_sx),
@@ -251,20 +251,20 @@ async def _mobile_nav_panel_sx(ctx: dict) -> str:
))
items_sx = "(<> " + " ".join(item_parts) + ")"
return await render_to_sx("market-mobile-nav-wrapper", items=SxExpr(items_sx))
return sx_call("market-mobile-nav-wrapper", items=SxExpr(items_sx))
# ---------------------------------------------------------------------------
# Product admin header
# ---------------------------------------------------------------------------
async def _product_admin_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
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 await render_to_sx(
return sx_call(
"menu-row-sx",
id="product-admin-row", level=4,
link_href=link_href, link_label="admin!!", icon="fa fa-cog",
@@ -296,21 +296,21 @@ async def _market_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import render_to_sx_with_env
return await render_to_sx_with_env("market-browse-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)))
market_header=SxExpr(_market_header_sx(ctx)))
async def _market_oob(ctx: dict, **kw: Any) -> str:
oob_hdr = await _oob_header_sx("post-header-child", "market-header-child",
await _market_header_sx(ctx))
return await render_to_sx("market-browse-layout-oob",
_market_header_sx(ctx))
return sx_call("market-browse-layout-oob",
oob_header=SxExpr(oob_hdr),
post_header_oob=SxExpr(await _post_header_sx(ctx, oob=True)),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child")))
async def _market_mobile(ctx: dict, **kw: Any) -> str:
return await _mobile_nav_panel_sx(ctx)
def _market_mobile(ctx: dict, **kw: Any) -> str:
return _mobile_nav_panel_sx(ctx)
async def _market_admin_full(ctx: dict, **kw: Any) -> str:
@@ -318,14 +318,14 @@ async def _market_admin_full(ctx: dict, **kw: Any) -> str:
selected = kw.get("selected", "")
return await render_to_sx_with_env("market-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)),
market_header=SxExpr(_market_header_sx(ctx)),
admin_header=SxExpr(await _market_admin_header_sx(ctx, selected=selected)))
async def _market_admin_oob(ctx: dict, **kw: Any) -> str:
selected = kw.get("selected", "")
return await render_to_sx("market-admin-layout-oob",
market_header_oob=SxExpr(await _market_header_sx(ctx, oob=True)),
return sx_call("market-admin-layout-oob",
market_header_oob=SxExpr(_market_header_sx(ctx, oob=True)),
admin_oob_header=SxExpr(await _oob_header_sx("market-header-child", "market-admin-header-child",
await _market_admin_header_sx(ctx, selected=selected))),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",

View File

@@ -5,7 +5,7 @@ from typing import Any
from shared.sx.parser import SxExpr
from shared.sx.helpers import (
render_to_sx,
sx_call,
post_header_sx as _post_header_sx,
oob_header_sx as _oob_header_sx,
full_page_sx, oob_page_sx,
@@ -25,21 +25,21 @@ from .helpers import _markets_admin_panel_sx
# Browse page
# ---------------------------------------------------------------------------
async def _product_grid(cards_sx: str) -> str:
def _product_grid(cards_sx: str) -> str:
"""Wrap product cards in a grid as sx."""
return await render_to_sx("market-product-grid", cards=SxExpr(cards_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 = await _product_cards_sx(ctx)
content = await _product_grid(cards)
cards = _product_cards_sx(ctx)
content = _product_grid(cards)
from shared.sx.helpers import render_to_sx_with_env
hdr = await render_to_sx_with_env("market-browse-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)))
menu = await _mobile_nav_panel_sx(ctx)
market_header=SxExpr(_market_header_sx(ctx)))
menu = _mobile_nav_panel_sx(ctx)
filter_sx = await _mobile_filter_summary_sx(ctx)
aside_sx = await _desktop_filter_sx(ctx)
@@ -49,17 +49,17 @@ async def render_browse_page(ctx: dict) -> str:
async def render_browse_oob(ctx: dict) -> str:
"""OOB response: product browse."""
cards = await _product_cards_sx(ctx)
content = await _product_grid(cards)
cards = _product_cards_sx(ctx)
content = _product_grid(cards)
oob_hdr = await _oob_header_sx("post-header-child", "market-header-child",
await _market_header_sx(ctx))
oobs = await render_to_sx("market-browse-layout-oob",
_market_header_sx(ctx))
oobs = sx_call("market-browse-layout-oob",
oob_header=SxExpr(oob_hdr),
post_header_oob=SxExpr(await _post_header_sx(ctx, oob=True)),
clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child")))
menu = await _mobile_nav_panel_sx(ctx)
menu = _mobile_nav_panel_sx(ctx)
filter_sx = await _mobile_filter_summary_sx(ctx)
aside_sx = await _desktop_filter_sx(ctx)
@@ -67,9 +67,9 @@ async def render_browse_oob(ctx: dict) -> str:
menu=menu, filter=filter_sx, aside=aside_sx)
async def render_browse_cards(ctx: dict) -> str:
def render_browse_cards(ctx: dict) -> str:
"""Pagination fragment: product cards -- sx wire format."""
return await _product_cards_sx(ctx)
return _product_cards_sx(ctx)
# ---------------------------------------------------------------------------
@@ -78,29 +78,29 @@ async def render_browse_cards(ctx: dict) -> str:
async def render_product_page(ctx: dict, d: dict) -> str:
"""Full page: product detail."""
content = await _product_detail_sx(d, ctx)
meta = await _product_meta_sx(d, ctx)
content = _product_detail_sx(d, ctx)
meta = _product_meta_sx(d, ctx)
from shared.sx.helpers import render_to_sx_with_env
hdr = await render_to_sx_with_env("market-product-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)),
product_header=SxExpr(await _product_header_sx(ctx, d)))
market_header=SxExpr(_market_header_sx(ctx)),
product_header=SxExpr(_product_header_sx(ctx, d)))
return await 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 = await _product_detail_sx(d, ctx)
content = _product_detail_sx(d, ctx)
oobs = await render_to_sx("market-oob-wrap",
parts=SxExpr("(<> " + await _market_header_sx(ctx, oob=True) + " "
oobs = sx_call("market-oob-wrap",
parts=SxExpr("(<> " + _market_header_sx(ctx, oob=True) + " "
+ await _oob_header_sx("market-header-child", "product-header-child",
await _product_header_sx(ctx, d)) + " "
_product_header_sx(ctx, d)) + " "
+ _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child",
"product-row", "product-header-child") + ")"))
menu = await _mobile_nav_panel_sx(ctx)
menu = _mobile_nav_panel_sx(ctx)
return await oob_page_sx(oobs=oobs, content=content, menu=menu)
@@ -110,25 +110,25 @@ 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 = await _product_detail_sx(d, ctx)
content = _product_detail_sx(d, ctx)
from shared.sx.helpers import render_to_sx_with_env
hdr = await render_to_sx_with_env("market-product-admin-layout-full", {},
post_header=SxExpr(await _post_header_sx(ctx)),
market_header=SxExpr(await _market_header_sx(ctx)),
product_header=SxExpr(await _product_header_sx(ctx, d)),
admin_header=SxExpr(await _product_admin_header_sx(ctx, d)))
market_header=SxExpr(_market_header_sx(ctx)),
product_header=SxExpr(_product_header_sx(ctx, d)),
admin_header=SxExpr(_product_admin_header_sx(ctx, d)))
return await 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 = await _product_detail_sx(d, ctx)
content = _product_detail_sx(d, ctx)
oobs = await render_to_sx("market-oob-wrap",
parts=SxExpr("(<> " + await _product_header_sx(ctx, d, oob=True) + " "
oobs = sx_call("market-oob-wrap",
parts=SxExpr("(<> " + _product_header_sx(ctx, d, oob=True) + " "
+ await _oob_header_sx("product-header-child", "product-admin-header-child",
await _product_admin_header_sx(ctx, d)) + " "
_product_admin_header_sx(ctx, d)) + " "
+ _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child",
"product-row", "product-header-child",
@@ -149,7 +149,7 @@ async def render_markets_admin_list_panel(ctx: dict) -> str:
# Public API: POST handler fragment renderers
# ---------------------------------------------------------------------------
async def render_all_markets_cards(markets: list, has_more: bool,
def render_all_markets_cards(markets: list, has_more: bool,
page_info: dict, page: int) -> str:
"""Pagination fragment: all markets cards."""
from quart import url_for
@@ -157,10 +157,10 @@ 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 await _market_cards_sx(markets, page_info, page, has_more, next_url)
return _market_cards_sx(markets, page_info, page, has_more, next_url)
async def render_page_markets_cards(markets: list, has_more: bool,
def render_page_markets_cards(markets: list, has_more: bool,
page: int, post_slug: str) -> str:
"""Pagination fragment: page-scoped markets cards."""
from quart import url_for
@@ -168,11 +168,11 @@ 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 await _market_cards_sx(markets, {}, page, has_more, next_url,
return _market_cards_sx(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
async def render_like_toggle_button(slug: str, liked: bool, *,
def render_like_toggle_button(slug: str, liked: bool, *,
like_url: str | None = None,
item_type: str = "product") -> str:
"""Render a standalone like toggle button for HTMX POST response."""
@@ -193,7 +193,7 @@ async def render_like_toggle_button(slug: str, liked: bool, *,
icon = "fa-regular fa-heart"
label = f"Like this {item_type}"
return await render_to_sx(
return sx_call(
"market-like-toggle-button",
colour=colour, action=like_url,
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
@@ -201,7 +201,7 @@ async def render_like_toggle_button(slug: str, liked: bool, *,
)
async def render_cart_added_response(cart: list, item: Any, d: dict) -> str:
def render_cart_added_response(cart: list, item: Any, d: dict) -> str:
"""Render the HTMX response after add-to-cart.
Returns OOB fragments: cart-mini icon + product add/remove buttons + cart item row.
@@ -217,30 +217,30 @@ async 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 = await render_to_sx("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 = await render_to_sx("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 = await render_to_sx(
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 = await render_to_sx(
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_sx = await render_to_sx(
add_sx = sx_call(
"market-cart-add-oob",
id=f"cart-add-{slug}",
inner=SxExpr(cart_add),

View File

@@ -3,8 +3,8 @@ from __future__ import annotations
from typing import Any
from shared.sx.parser import serialize, SxExpr
from shared.sx.helpers import render_to_sx
from shared.sx.parser import SxExpr
from shared.sx.helpers import sx_call
# ---------------------------------------------------------------------------
@@ -22,7 +22,6 @@ _MARKET_DEEP_IDS = [
def _clear_deeper_oob(*keep_ids: str) -> str:
"""Clear all market header rows/children NOT in keep_ids."""
from shared.sx.helpers import sx_call
to_clear = [i for i in _MARKET_DEEP_IDS if i not in keep_ids]
return " ".join(sx_call("clear-oob-div", id=i) for i in to_clear)
@@ -55,27 +54,27 @@ def _set_prices(item: dict) -> dict:
rp_val=rp_val, rp_raw=rp_raw, rp_cur=rp_cur)
async def _card_price_sx(p: dict) -> str:
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(await render_to_sx("market-price-special", price=sp_str))
parts.append(sx_call("market-price-special", price=sp_str))
if pr["rp_val"]:
parts.append(await render_to_sx("market-price-regular-strike", price=rp_str))
parts.append(sx_call("market-price-regular-strike", price=rp_str))
elif pr["rp_val"]:
parts.append(await render_to_sx("market-price-regular", price=rp_str))
parts.append(sx_call("market-price-regular", price=rp_str))
inner = "(<> " + " ".join(parts) + ")" if parts else None
return await render_to_sx("market-price-line", inner=SxExpr(inner) if inner else None)
return sx_call("market-price-line", inner=SxExpr(inner) if inner else None)
# ---------------------------------------------------------------------------
# Product detail page content
# ---------------------------------------------------------------------------
async def _product_detail_sx(d: dict, ctx: dict) -> str:
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
@@ -97,19 +96,19 @@ async def _product_detail_sx(d: dict, ctx: dict) -> str:
# Like button
like_sx = ""
if user:
like_sx = await _like_button_sx(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(await render_to_sx(
label_parts.append(sx_call(
"market-label-overlay",
src=asset_url_fn("labels/" + l + ".svg"),
))
labels_sx = "(<> " + " ".join(label_parts) + ")" if label_parts else None
gallery_inner = await render_to_sx(
gallery_inner = sx_call(
"market-detail-gallery-inner",
like=SxExpr(like_sx) if like_sx else None,
image=images[0], alt=d.get("title", ""),
@@ -120,9 +119,9 @@ async def _product_detail_sx(d: dict, ctx: dict) -> str:
# Prev/next buttons
nav_buttons = ""
if len(images) > 1:
nav_buttons = await render_to_sx("market-detail-nav-buttons")
nav_buttons = sx_call("market-detail-nav-buttons")
gallery_sx = await render_to_sx(
gallery_sx = sx_call(
"market-detail-gallery",
inner=SxExpr(gallery_inner),
nav=SxExpr(nav_buttons) if nav_buttons else None,
@@ -133,18 +132,18 @@ async def _product_detail_sx(d: dict, ctx: dict) -> str:
if len(images) > 1:
thumb_parts = []
for i, u in enumerate(images):
thumb_parts.append(await render_to_sx(
thumb_parts.append(sx_call(
"market-detail-thumb",
title=f"Image {i+1}", src=u, alt=f"thumb {i+1}",
))
thumbs_sx = "(<> " + " ".join(thumb_parts) + ")"
gallery_parts.append(await render_to_sx("market-detail-thumbs", thumbs=SxExpr(thumbs_sx)))
gallery_parts.append(sx_call("market-detail-thumbs", thumbs=SxExpr(thumbs_sx)))
gallery_final = "(<> " + " ".join(gallery_parts) + ")"
else:
like_sx = ""
if user:
like_sx = await _like_button_sx(slug, liked_by_current_user, csrf, ctx)
gallery_final = await render_to_sx("market-detail-no-image",
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
@@ -152,12 +151,12 @@ async def _product_detail_sx(d: dict, ctx: dict) -> str:
if stickers and callable(asset_url_fn):
sticker_parts = []
for s in stickers:
sticker_parts.append(await render_to_sx(
sticker_parts.append(sx_call(
"market-detail-sticker",
src=asset_url_fn("stickers/" + s + ".svg"), name=s,
))
sticker_items_sx = "(<> " + " ".join(sticker_parts) + ")"
stickers_sx = await render_to_sx("market-detail-stickers", items=SxExpr(sticker_items_sx))
stickers_sx = sx_call("market-detail-stickers", items=SxExpr(sticker_items_sx))
# Right column: prices, description, sections
pr = _set_prices(d)
@@ -167,15 +166,15 @@ async def _product_detail_sx(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(await render_to_sx(
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(await render_to_sx("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_sx = "(<> " + " ".join(extra_parts) + ")"
detail_parts.append(await render_to_sx("market-detail-extras", inner=SxExpr(extras_sx)))
detail_parts.append(sx_call("market-detail-extras", inner=SxExpr(extras_sx)))
# Description
desc_short = d.get("description_short")
@@ -183,28 +182,28 @@ async def _product_detail_sx(d: dict, ctx: dict) -> str:
if desc_short or desc_html_val:
desc_parts: list[str] = []
if desc_short:
desc_parts.append(await render_to_sx("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(await render_to_sx("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(await render_to_sx("market-detail-desc-wrapper", inner=SxExpr(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(await render_to_sx(
sec_parts.append(sx_call(
"market-detail-section",
title=sec.get("title", ""), html=sec.get("html", ""),
))
sec_items_sx = "(<> " + " ".join(sec_parts) + ")"
detail_parts.append(await render_to_sx("market-detail-sections", items=SxExpr(sec_items_sx)))
detail_parts.append(sx_call("market-detail-sections", items=SxExpr(sec_items_sx)))
details_inner_sx = "(<> " + " ".join(detail_parts) + ")" if detail_parts else "(<>)"
details_sx = await render_to_sx("market-detail-right-col", inner=SxExpr(details_inner_sx))
details_sx = sx_call("market-detail-right-col", inner=SxExpr(details_inner_sx))
return await render_to_sx(
return sx_call(
"market-detail-layout",
gallery=SxExpr(gallery_final),
stickers=SxExpr(stickers_sx) if stickers_sx else None,
@@ -216,7 +215,7 @@ async def _product_detail_sx(d: dict, ctx: dict) -> str:
# Product meta (OpenGraph, JSON-LD)
# ---------------------------------------------------------------------------
async def _product_meta_sx(d: dict, ctx: dict) -> str:
def _product_meta_sx(d: dict, ctx: dict) -> str:
"""Build product meta tags as sx (auto-hoisted to <head> by sx.js)."""
import json
from quart import request
@@ -234,34 +233,34 @@ async def _product_meta_sx(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 = [await render_to_sx("market-meta-title", title=title)]
parts.append(await render_to_sx("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(await render_to_sx("market-meta-canonical", href=canonical))
parts.append(sx_call("market-meta-canonical", href=canonical))
# OpenGraph
site_title = ctx.get("base_title", "")
parts.append(await render_to_sx("market-meta-og", property="og:site_name", content=site_title))
parts.append(await render_to_sx("market-meta-og", property="og:type", content="product"))
parts.append(await render_to_sx("market-meta-og", property="og:title", content=title))
parts.append(await render_to_sx("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(await render_to_sx("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(await render_to_sx("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(await render_to_sx("market-meta-og", property="product:price:amount", content=f"{price:.2f}"))
parts.append(await render_to_sx("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(await render_to_sx("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(await render_to_sx("market-meta-twitter", name="twitter:card", content=card_type))
parts.append(await render_to_sx("market-meta-twitter", name="twitter:title", content=title))
parts.append(await render_to_sx("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(await render_to_sx("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 = {
@@ -283,6 +282,6 @@ async def _product_meta_sx(d: dict, ctx: dict) -> str:
"url": canonical,
"availability": "https://schema.org/InStock",
}
parts.append(await render_to_sx("market-meta-jsonld", json=json.dumps(jsonld)))
parts.append(sx_call("market-meta-jsonld", json=json.dumps(jsonld)))
return "(<> " + " ".join(parts) + ")"