""" Shared helper functions for s-expression page rendering. These are used by per-service sexp_components.py files to build common page elements (headers, search, etc.) from template context. """ from __future__ import annotations from typing import Any from markupsafe import escape from .jinja_bridge import render from .page import SEARCH_HEADERS_MOBILE, SEARCH_HEADERS_DESKTOP def call_url(ctx: dict, key: str, path: str = "/") -> str: """Call a URL helper from context (e.g., blog_url, account_url).""" fn = ctx.get(key) if callable(fn): return fn(path) return str(fn or "") + path def get_asset_url(ctx: dict) -> str: """Extract the asset URL base from context.""" au = ctx.get("asset_url") if callable(au): result = au("") return result.rsplit("/", 1)[0] if "/" in result else result return au or "" def root_header_html(ctx: dict, *, oob: bool = False) -> str: """Build the root header row HTML.""" return render( "header-row", cart_mini_html=ctx.get("cart_mini_html", ""), blog_url=call_url(ctx, "blog_url", ""), site_title=ctx.get("base_title", ""), app_label=ctx.get("app_label", ""), nav_tree_html=ctx.get("nav_tree_html", ""), auth_menu_html=ctx.get("auth_menu_html", ""), nav_panel_html=ctx.get("nav_panel_html", ""), oob=oob, ) def search_mobile_html(ctx: dict) -> str: """Build mobile search input HTML.""" return render( "search-mobile", current_local_href=ctx.get("current_local_href", "/"), search=ctx.get("search", ""), search_count=ctx.get("search_count", ""), hx_select=ctx.get("hx_select", "#main-panel"), search_headers_mobile=SEARCH_HEADERS_MOBILE, ) def search_desktop_html(ctx: dict) -> str: """Build desktop search input HTML.""" return render( "search-desktop", current_local_href=ctx.get("current_local_href", "/"), search=ctx.get("search", ""), search_count=ctx.get("search_count", ""), hx_select=ctx.get("hx_select", "#main-panel"), search_headers_desktop=SEARCH_HEADERS_DESKTOP, ) def post_header_html(ctx: dict, *, oob: bool = False) -> str: """Build the post-level header row (level 1). Used by all apps + error pages.""" post = ctx.get("post") or {} slug = post.get("slug", "") if not slug: return "" title = (post.get("title") or "")[:160] feature_image = post.get("feature_image") label_html = render("post-label", feature_image=feature_image, title=title) nav_parts: list[str] = [] page_cart_count = ctx.get("page_cart_count", 0) if page_cart_count and page_cart_count > 0: cart_href = call_url(ctx, "cart_url", f"/{slug}/") nav_parts.append(render("page-cart-badge", href=cart_href, count=str(page_cart_count))) container_nav = ctx.get("container_nav_html", "") if container_nav: nav_parts.append(container_nav) admin_nav = ctx.get("post_admin_nav_html", "") if admin_nav: nav_parts.append(admin_nav) nav_html = "".join(nav_parts) link_href = call_url(ctx, "blog_url", f"/{slug}/") return render("menu-row", id="post-row", level=1, link_href=link_href, link_label_html=label_html, nav_html=nav_html, child_id="post-header-child", oob=oob, external=True, ) def post_admin_header_html(ctx: dict, slug: str, *, oob: bool = False, selected: str = "", admin_href: str = "") -> str: """Shared post admin header row with unified nav across all services. Shows: calendars | markets | payments | entries | data | edit | settings All links are external (cross-service). The *selected* item is highlighted on the nav and shown in white next to the admin label. """ # Label: shield icon + "admin" + optional selected sub-page in white label_html = ' admin' if selected: label_html += f' {escape(selected)}' # Nav items — all external links to the appropriate service select_colours = ctx.get("select_colours", "") base_cls = ("justify-center cursor-pointer flex flex-row items-center" " gap-2 rounded bg-stone-200 text-black p-3") selected_cls = ("justify-center cursor-pointer flex flex-row items-center" " gap-2 rounded !bg-stone-500 !text-white p-3") nav_parts: list[str] = [] items = [ ("events_url", f"/{slug}/admin/", "calendars"), ("market_url", f"/{slug}/admin/", "markets"), ("cart_url", f"/{slug}/admin/payments/", "payments"), ("blog_url", f"/{slug}/admin/entries/", "entries"), ("blog_url", f"/{slug}/admin/data/", "data"), ("blog_url", f"/{slug}/admin/edit/", "edit"), ("blog_url", f"/{slug}/admin/settings/", "settings"), ] for url_key, path, label in items: url_fn = ctx.get(url_key) if not callable(url_fn): continue href = url_fn(path) is_sel = label == selected cls = selected_cls if is_sel else base_cls aria = ' aria-selected="true"' if is_sel else "" nav_parts.append( f'' ) nav_html = "".join(nav_parts) if not admin_href: blog_fn = ctx.get("blog_url") admin_href = blog_fn(f"/{slug}/admin/") if callable(blog_fn) else f"/{slug}/admin/" return render("menu-row", id="post-admin-row", level=2, link_href=admin_href, link_label_html=label_html, nav_html=nav_html, child_id="post-admin-header-child", oob=oob, ) def oob_header_html(parent_id: str, child_id: str, row_html: str) -> str: """Wrap a header row in an OOB swap div with child placeholder.""" return render("oob-header", parent_id=parent_id, child_id=child_id, row_html=row_html, ) def header_child_html(inner_html: str, *, id: str = "root-header-child") -> str: """Wrap inner HTML in a header-child div.""" return render("header-child", id=id, inner_html=inner_html) def error_content_html(errnum: str, message: str, image: str | None = None) -> str: """Render the error content block.""" return render("error-content", errnum=errnum, message=message, image=image) def full_page(ctx: dict, *, header_rows_html: str, filter_html: str = "", aside_html: str = "", content_html: str = "", menu_html: str = "", body_end_html: str = "", meta_html: str = "") -> str: """Render a full app page with the standard layout.""" return render( "app-layout", title=ctx.get("base_title", "Rose Ash"), asset_url=get_asset_url(ctx), meta_html=meta_html, header_rows_html=header_rows_html, menu_html=menu_html, filter_html=filter_html, aside_html=aside_html, content_html=content_html, body_end_html=body_end_html, ) def oob_page(ctx: dict, *, oobs_html: str = "", filter_html: str = "", aside_html: str = "", content_html: str = "", menu_html: str = "") -> str: """Render an OOB response with standard swap targets.""" return render( "oob-response", oobs_html=oobs_html, filter_html=filter_html, aside_html=aside_html, menu_html=menu_html, content_html=content_html, )